Skip to content

Commit 0e10e56

Browse files
committed
fix(pkg): pass explicit -z/-j flags for non-auto-detecting tar
OpenBSD tar doesn't auto-detect compression format and requires explicit -z (gzip) or -j (bzip2) flags. This adds tar implementation detection via --version output: - 'bsdtar' or 'libarchive' -> BSD tar (auto-detects compression) - 'GNU tar' -> GNU tar (auto-detects compression) - Otherwise -> Other (needs explicit flags) Fixes #10123 Signed-off-by: Ali Caglayan <alizter@gmail.com>
1 parent 94e6294 commit 0e10e56

File tree

3 files changed

+118
-54
lines changed

3 files changed

+118
-54
lines changed

src/dune_pkg/archive_driver.ml

Lines changed: 66 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,15 @@ open Stdune
22
module Process = Dune_engine.Process
33
open Fiber.O
44

5+
(** Tar implementation type, detected via --version output:
6+
- Libarchive: libarchive/bsdtar - auto-detects compression, can extract zip
7+
- Gnu: GNU tar - auto-detects compression, no zip support
8+
- Other: Unknown (e.g. OpenBSD) - needs explicit -z/-j flags *)
9+
type tar_impl =
10+
| Libarchive
11+
| Gnu
12+
| Other
13+
514
module Command = struct
615
type t =
716
{ bin : Path.t
@@ -16,8 +25,45 @@ type t =
1625

1726
let which bin_name = Bin.which ~path:(Env_path.path Env.initial) bin_name
1827

19-
let make_tar_args ~archive ~target_in_temp =
20-
[ "xf"; Path.to_string archive; "-C"; Path.to_string target_in_temp ]
28+
(* Regexes for detecting tar implementation from --version output *)
29+
let libarchive_re = Re.compile (Re.alt [ Re.str "bsdtar"; Re.str "libarchive" ])
30+
let gnu_tar_re = Re.compile (Re.str "GNU tar")
31+
32+
(** Detect tar implementation by running --version *)
33+
let detect_tar_impl bin =
34+
let+ output, _ = Process.run_capture ~display:Quiet Return bin [ "--version" ] in
35+
if Re.execp libarchive_re output
36+
then Libarchive
37+
else if Re.execp gnu_tar_re output
38+
then Gnu
39+
else Other
40+
;;
41+
42+
(** Generate tar arguments, adding -z/-j for non-auto-detecting tar *)
43+
let make_tar_args ~tar_impl ~archive ~target_in_temp =
44+
let decompress_flag =
45+
match tar_impl with
46+
| Libarchive | Gnu -> [] (* auto-detect compression *)
47+
| Other ->
48+
(* Need explicit flags for tar implementations that don't auto-detect *)
49+
let archive_str = Path.to_string archive in
50+
if
51+
Filename.check_suffix archive_str ".tar.gz"
52+
|| Filename.check_suffix archive_str ".tgz"
53+
then [ "-z" ]
54+
else if
55+
Filename.check_suffix archive_str ".tar.bz2"
56+
|| Filename.check_suffix archive_str ".tbz"
57+
then [ "-j" ]
58+
else
59+
(* CR-soon alizter: handle .tar.xz (-J) and .tar.lzma (--lzma) which
60+
OPAM also supports. Should error with a clear message if the tar
61+
implementation doesn't support auto-detection for these formats. *)
62+
[]
63+
in
64+
[ "-x" ]
65+
@ decompress_flag
66+
@ [ "-f"; Path.to_string archive; "-C"; Path.to_string target_in_temp ]
2167
;;
2268

2369
let make_zip_args ~archive ~target_in_temp =
@@ -27,12 +73,10 @@ let make_zip_args ~archive ~target_in_temp =
2773
let tar =
2874
let command =
2975
Fiber.Lazy.create (fun () ->
30-
match
31-
(* Test for tar before bsdtar as tar is more likely to be installed
32-
and both work equally well for tarballs. *)
33-
List.find_map [ "tar"; "bsdtar" ] ~f:which
34-
with
35-
| Some bin -> Fiber.return { Command.bin; make_args = make_tar_args }
76+
match List.find_map [ "tar"; "bsdtar" ] ~f:which with
77+
| Some bin ->
78+
let+ tar_impl = detect_tar_impl bin in
79+
{ Command.bin; make_args = make_tar_args ~tar_impl }
3680
| None ->
3781
Fiber.return
3882
@@ User_error.raise
@@ -43,13 +87,16 @@ let tar =
4387
{ command; suffixes = [ ".tar"; ".tar.gz"; ".tgz"; ".tar.bz2"; ".tbz" ] }
4488
;;
4589

46-
let which_bsdtar (bin_name : string) =
47-
match which bin_name with
48-
| None -> Fiber.return None
49-
| Some bin ->
50-
let+ output, _error = Process.run_capture ~display:Quiet Return bin [ "--version" ] in
51-
let re = Re.compile (Re.str "bsdtar") in
52-
if Re.execp re output then Some bin else None
90+
let rec find_libarchive_tar = function
91+
| [] -> Fiber.return None
92+
| name :: rest ->
93+
(match which name with
94+
| None -> find_libarchive_tar rest
95+
| Some bin ->
96+
detect_tar_impl bin
97+
>>= (function
98+
| Libarchive -> Fiber.return (Some bin)
99+
| Gnu | Other -> find_libarchive_tar rest))
53100
;;
54101

55102
let zip =
@@ -58,18 +105,11 @@ let zip =
58105
match which "unzip" with
59106
| Some bin -> Fiber.return { Command.bin; make_args = make_zip_args }
60107
| None ->
61-
let rec find_tar programs =
62-
match programs with
63-
| [] -> Fiber.return None
64-
| x :: xs ->
65-
let* res = which_bsdtar x in
66-
(match res with
67-
| Some _ -> Fiber.return res
68-
| None -> find_tar xs)
69-
in
70-
let* program = find_tar [ "bsdtar"; "tar" ] in
108+
(* Only libarchive can extract zip, so find a tar that is libarchive *)
109+
let* program = find_libarchive_tar [ "bsdtar"; "tar" ] in
71110
(match program with
72-
| Some bin -> Fiber.return { Command.bin; make_args = make_tar_args }
111+
| Some bin ->
112+
Fiber.return { Command.bin; make_args = make_tar_args ~tar_impl:Libarchive }
73113
| None ->
74114
Fiber.return
75115
@@ User_error.raise

test/blackbox-tests/test-cases/pkg/openbsd-tar.t

Lines changed: 23 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
Test that demonstrates the issue with OpenBSD-style tar (#10123).
1+
Test that OpenBSD-style tar works when dune passes explicit flags (#10123).
22

33
Unlike GNU tar and BSD tar (libarchive), OpenBSD tar requires explicit
44
flags for compressed archives: -z for gzip, -j for bzip2, etc.
@@ -17,17 +17,17 @@ Set up a fake tar that behaves like OpenBSD tar:
1717
> echo "tar (OpenBSD)"
1818
> ;;
1919
> *)
20-
> # Dune passes: xf archive -C target (or xzf archive -C target with fix)
21-
> flags="$1"
22-
> archive="$2"
23-
> target="$4"
20+
> # Dune passes: -x -z -f archive -C target (or -x -j -f ... for bzip2)
21+
> # $4 = archive, $6 = target
22+
> archive="$4"
23+
> target="$6"
2424
> # Require correct flag for compressed archives
2525
> case "$archive" in
2626
> *.tar.gz|*.tgz)
27-
> echo "$flags" | grep -q 'z' || { echo "tar: Cannot open: compressed archive requires -z flag" >&2; exit 1; }
27+
> echo "$@" | grep -q '\-z' || { echo "tar: Cannot open: compressed archive requires -z flag" >&2; exit 1; }
2828
> ;;
2929
> *.tar.bz2|*.tbz)
30-
> echo "$flags" | grep -q 'j' || { echo "tar: Cannot open: compressed archive requires -j flag" >&2; exit 1; }
30+
> echo "$@" | grep -q '\-j' || { echo "tar: Cannot open: compressed archive requires -j flag" >&2; exit 1; }
3131
> ;;
3232
> esac
3333
> # Success - create fake extracted content
@@ -48,7 +48,7 @@ Set up fake PATH with only our OpenBSD-style tar:
4848
$ ln -s $(which grep) .fakebin/grep
4949
$ ln -s ../.binaries/openbsd-tar .fakebin/tar
5050

51-
Test with a .tar.gz file - fails because dune doesn't pass -z flag:
51+
Test with a .tar.gz file:
5252

5353
$ echo "fake tarball" > test.tar.gz
5454

@@ -59,13 +59,15 @@ Test with a .tar.gz file - fails because dune doesn't pass -z flag:
5959
> (version dev)
6060
> EOF
6161

62-
$ PATH=.fakebin build_pkg foo 2>&1 | grep -A2 '^Error'
63-
Error: failed to extract 'test.tar.gz'
64-
Reason: 'tar' failed with non-zero exit code '1' and output:
65-
- tar: Cannot open: compressed archive requires -z flag
66-
[1]
62+
$ PATH=.fakebin build_pkg foo
6763

68-
Test with a .tbz file - fails because dune doesn't pass -j flag:
64+
Verify that -z flag was passed:
65+
66+
$ dune trace cat | jq -c 'include "dune"; processes | select(.args.prog | contains("tar")) | .args.process_args | map(if (startswith("/") or startswith("_build")) then "PATH" else . end)'
67+
["--version"]
68+
["-x","-z","-f","PATH","-C","PATH"]
69+
70+
Test with a .tbz file (bzip2 compressed):
6971

7072
$ echo "fake tarball" > test.tbz
7173

@@ -76,8 +78,10 @@ Test with a .tbz file - fails because dune doesn't pass -j flag:
7678
> (version dev)
7779
> EOF
7880

79-
$ PATH=.fakebin build_pkg bar 2>&1 | grep -A2 '^Error'
80-
Error: failed to extract 'test.tbz'
81-
Reason: 'tar' failed with non-zero exit code '1' and output:
82-
- tar: Cannot open: compressed archive requires -j flag
83-
[1]
81+
$ PATH=.fakebin build_pkg bar
82+
83+
Verify that -j flag was passed:
84+
85+
$ dune trace cat | jq -c 'include "dune"; processes | select(.args.prog | contains("tar")) | .args.process_args | map(if (startswith("/") or startswith("_build")) then "PATH" else . end)'
86+
["--version"]
87+
["-x","-j","-f","PATH","-C","PATH"]

test/blackbox-tests/test-cases/pkg/zip-extract-fail.t

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,19 @@ expected location:
1010
> #!/usr/bin/env sh
1111
> case "$1" in
1212
> --version)
13-
> echo "tar"
13+
> echo "GNU tar"
1414
> ;;
1515
> *)
16-
> cp "$2" "$4/$(basename "${2%.zip}")"
16+
> # Parse: -x -f archive -C target
17+
> archive=""; target=""
18+
> while [ $# -gt 0 ]; do
19+
> case "$1" in
20+
> -f) archive="$2"; shift 2 ;;
21+
> -C) target="$2"; shift 2 ;;
22+
> *) shift ;;
23+
> esac
24+
> done
25+
> cp "$archive" "$target/$(basename "${archive%.zip}")"
1726
> esac
1827
> EOF
1928
$ chmod +x .binaries/gnutar
@@ -24,13 +33,23 @@ expected location:
2433
> echo "bsdtar"
2534
> ;;
2635
> *)
27-
> cp "$2" "$4/$(basename "${2%.zip}")"
36+
> # Parse: -x -f archive -C target
37+
> archive=""; target=""
38+
> while [ $# -gt 0 ]; do
39+
> case "$1" in
40+
> -f) archive="$2"; shift 2 ;;
41+
> -C) target="$2"; shift 2 ;;
42+
> *) shift ;;
43+
> esac
44+
> done
45+
> cp "$archive" "$target/$(basename "${archive%.zip}")"
2846
> esac
2947
> EOF
3048
$ chmod +x .binaries/bsdtar
3149
$ cat > .binaries/unzip << 'EOF'
3250
> #!/usr/bin/env sh
33-
> cp "$2" "$4/$(basename "${2%.zip}")"
51+
> # unzip archive -d target
52+
> cp "$1" "$3/$(basename "${1%.zip}")"
3453
> EOF
3554
$ chmod +x .binaries/unzip
3655

@@ -40,6 +59,7 @@ Set up a folder that we will inject as fake PATH:
4059
$ ln -s $(which dune) .fakebin/dune
4160
$ ln -s $(which sh) .fakebin/sh
4261
$ ln -s $(which cp) .fakebin/cp
62+
$ ln -s $(which basename) .fakebin/basename
4363
$ show_path() {
4464
> ls .fakebin | sort | xargs
4565
> }
@@ -66,7 +86,7 @@ Build the package in an environment without unzip, or tar, or bsdtar.
6686
variable can escape to subseqent shell invocations on MacOS.)
6787

6888
$ show_path
69-
cp dune sh
89+
basename cp dune sh
7090
$ (PATH=.fakebin build_pkg foo 2>&1 | grep '^Error:' -A 3)
7191
Error: No program found to extract zip file. Tried:
7292
- unzip
@@ -78,7 +98,7 @@ Build with only GNU tar that can't extract ZIP archives:
7898

7999
$ ln -s ../.binaries/gnutar .fakebin/tar
80100
$ show_path
81-
cp dune sh tar
101+
basename cp dune sh tar
82102
$ (PATH=.fakebin build_pkg foo 2>&1 | grep '^Error:' -A 3)
83103
Error: No program found to extract zip file. Tried:
84104
- unzip
@@ -91,7 +111,7 @@ Build with bsdtar that can extract ZIP archives, without unzip. It should work:
91111
$ rm .fakebin/tar
92112
$ ln -s ../.binaries/bsdtar .fakebin/tar
93113
$ show_path
94-
cp dune sh tar
114+
basename cp dune sh tar
95115
$ (PATH=.fakebin build_pkg foo)
96116

97117
Build the package with bsdtar and tar. Now our fake bsdtar will get picked up
@@ -101,13 +121,13 @@ and used to extract:
101121
$ ln -s ../.binaries/gnutar .fakebin/tar
102122
$ ln -s ../.binaries/bsdtar .fakebin/bsdtar
103123
$ show_path
104-
bsdtar cp dune sh tar
124+
basename bsdtar cp dune sh tar
105125
$ (PATH=.fakebin build_pkg foo)
106126

107127
Build with unzip only:
108128

109129
$ ln -s ../.binaries/unzip .fakebin/unzip
110130
$ rm .fakebin/bsdtar .fakebin/tar
111131
$ show_path
112-
cp dune sh unzip
132+
basename cp dune sh unzip
113133
$ (PATH=.fakebin build_pkg foo)

0 commit comments

Comments
 (0)