Skip to content

Commit d5b4236

Browse files
committed
feat(pkg): relocatable compiler support
Signed-off-by: Ali Caglayan <alizter@gmail.com>
3 parents 3d8e3ce + f4b854f + 0487d07 commit d5b4236

17 files changed

+797
-13
lines changed

src/dune_pkg/lock_pkg.ml

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,13 +97,30 @@ let opam_variable_to_slang ~loc packages variable =
9797
with the following syntax:
9898
9999
"%{?conf-g++:your-variable:}%"
100+
101+
Also handles the case where the conditional syntax %{pkg:var?then:else}%
102+
was not correctly parsed by OpamTypesBase.filter_ident_of_string, which
103+
only looks for '?' in the package part, not in the variable part.
100104
*)
101105
let desugar_special_string_interpolation_syntax
102106
((packages, variable, string_converter) as fident)
103107
=
108+
let var_str = OpamVariable.to_string variable in
104109
match string_converter with
110+
| None when String.contains var_str '?' ->
111+
(* Handle pkg:var?then or pkg:var?then:else syntax where the '?' ended up
112+
in the variable name instead of being parsed as a converter *)
113+
(match String.lsplit2 var_str ~on:'?' with
114+
| Some (var, rest) ->
115+
let converter =
116+
match String.lsplit2 rest ~on:':' with
117+
| Some (then_, else_) -> Some (then_, else_)
118+
| None -> Some (rest, "")
119+
in
120+
(packages, OpamVariable.of_string var, converter)
121+
| None -> fident)
105122
| Some (package_and_variable, "")
106-
when List.is_empty packages && String.is_empty (OpamVariable.to_string variable) ->
123+
when List.is_empty packages && String.is_empty var_str ->
107124
(match String.lsplit2 package_and_variable ~on:':' with
108125
| Some (package, variable) ->
109126
( [ Some (OpamPackage.Name.of_string package) ]

src/dune_rules/pkg_rules.ml

Lines changed: 65 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -449,6 +449,9 @@ module Pkg = struct
449449
; files_dir : Path.Build.t option
450450
; pkg_digest : Pkg_digest.t
451451
; mutable exported_env : string Env_update.t list
452+
; is_relocatable_compiler_context : bool
453+
(* true if relocatable-compiler is in the lock directory, meaning
454+
the compiler should be treated as a regular package *)
452455
}
453456

454457
module Top_closure = Top_closure.Make (Id.Set) (Monad.Id)
@@ -525,13 +528,17 @@ module Pkg = struct
525528

526529
let install_roots t =
527530
let default_install_roots = Paths.install_roots t.paths in
528-
match Pkg_toolchain.is_compiler_and_toolchains_enabled t.info.name with
529-
| false -> default_install_roots
530-
| true ->
531-
(* Compiler packages store their libraries in a subdirectory named "ocaml". *)
532-
{ default_install_roots with
533-
lib_root = Path.relative default_install_roots.lib_root "ocaml"
534-
}
531+
(* Skip toolchain handling if this is a relocatable compiler context *)
532+
if t.is_relocatable_compiler_context
533+
then default_install_roots
534+
else (
535+
match Pkg_toolchain.is_compiler_and_toolchains_enabled t.info.name ~dep_names:[] with
536+
| false -> default_install_roots
537+
| true ->
538+
(* Compiler packages store their libraries in a subdirectory named "ocaml". *)
539+
{ default_install_roots with
540+
lib_root = Path.relative default_install_roots.lib_root "ocaml"
541+
})
535542
;;
536543

537544
(* Given a list of packages, construct an env containing variables
@@ -1360,6 +1367,11 @@ module DB = struct
13601367
{ id = Id.gen (); pkg_digest_table; system_provided }
13611368
;;
13621369

1370+
let all_package_names t =
1371+
Pkg_digest.Map.fold t.pkg_digest_table ~init:[] ~f:(fun entry acc ->
1372+
entry.pkg.info.name :: acc)
1373+
;;
1374+
13631375
let pkg_digest_of_name lock_dir platform pkg_name ~system_provided =
13641376
let entries_by_name =
13651377
Pkg_table.entries_by_name_of_lock_dir lock_dir ~platform ~system_provided
@@ -1584,9 +1596,14 @@ end = struct
15841596
let install_command = Option.map install_command ~f:relocate in
15851597
let build_command = choose_for_current_platform build_command in
15861598
let build_command = Option.map build_command ~f:relocate_build in
1599+
(* Check if relocatable-compiler is in the lock directory. This means
1600+
the compiler should be treated as a regular package, not cached in
1601+
the toolchains directory. *)
1602+
let all_pkg_names = DB.all_package_names db in
1603+
let is_relocatable_compiler_context = Pkg_toolchain.is_relocatable_compiler all_pkg_names in
15871604
let paths =
15881605
let paths = Paths.map_path write_paths ~f:Path.build in
1589-
match Pkg_toolchain.is_compiler_and_toolchains_enabled info.name with
1606+
match Pkg_toolchain.is_compiler_and_toolchains_enabled info.name ~dep_names:all_pkg_names with
15901607
| false -> paths
15911608
| true ->
15921609
(* Modify the environment as well as build and install commands for
@@ -1623,6 +1640,7 @@ end = struct
16231640
; files_dir
16241641
; pkg_digest
16251642
; exported_env = []
1643+
; is_relocatable_compiler_context
16261644
}
16271645
in
16281646
let+ exported_env =
@@ -1929,6 +1947,38 @@ module Install_action = struct
19291947
Some (entry.section, dst)
19301948
;;
19311949

1950+
let rec resolve_symlinks_in dir =
1951+
match Readdir.read_directory_with_kinds dir with
1952+
| Error e -> Unix_error.Detailed.raise e
1953+
| Ok entries ->
1954+
List.iter entries ~f:(fun (fname, kind) ->
1955+
let path = Filename.concat dir fname in
1956+
match (kind : Unix.file_kind) with
1957+
| S_DIR -> resolve_symlinks_in path
1958+
| S_LNK ->
1959+
(match Fpath.follow_symlink path with
1960+
| Error (Unix_error e) -> Unix_error.Detailed.raise e
1961+
| Error Not_a_symlink ->
1962+
Code_error.raise
1963+
"resolve_symlinks_in: not a symlink"
1964+
[ "path", Dyn.string path ]
1965+
| Error Max_depth_exceeded ->
1966+
User_error.raise
1967+
[ Pp.textf
1968+
"Unable to resolve symlink %s: too many levels of symbolic links"
1969+
path
1970+
]
1971+
| Ok resolved ->
1972+
(match Unix.lstat resolved with
1973+
| { Unix.st_kind = S_REG; _ } ->
1974+
Fpath.unlink_exn path;
1975+
Io.portable_hardlink
1976+
~src:(Path.of_string resolved)
1977+
~dst:(Path.of_string path)
1978+
| _ -> ()))
1979+
| _ -> ())
1980+
;;
1981+
19321982
let action
19331983
{ package
19341984
; install_file
@@ -2016,9 +2066,14 @@ module Install_action = struct
20162066
let+ variables = Async.async (fun () -> read_variables config_file) in
20172067
{ Install_cookie.Gen.files; variables }
20182068
in
2019-
(* Produce the cookie file in the standard path *)
2020-
let cookie_file = Path.build @@ Paths.install_cookie' target_dir in
20212069
Async.async (fun () ->
2070+
(* Resolve symlinks in target_dir so that the cache can store them. The
2071+
dune cache doesn't support symlinks, so we replace them with hardlinks
2072+
to their targets. *)
2073+
if Path.Untracked.exists (Path.build target_dir)
2074+
then resolve_symlinks_in (Path.Build.to_string target_dir);
2075+
(* Produce the cookie file in the standard path *)
2076+
let cookie_file = Path.build @@ Paths.install_cookie' target_dir in
20222077
cookie_file |> Path.parent_exn |> Path.mkdir_p;
20232078
Install_cookie.dump cookie_file cookies)
20242079
;;

src/dune_rules/pkg_toolchain.ml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,15 @@ let installation_prefix pkg =
4747
Path.Outside_build_dir.relative pkg_dir "target"
4848
;;
4949

50-
let is_compiler_and_toolchains_enabled name =
50+
let relocatable_package_name = Package.Name.of_string "relocatable-compiler"
51+
52+
let is_relocatable_compiler dep_names =
53+
List.exists dep_names ~f:(Package.Name.equal relocatable_package_name)
54+
;;
55+
56+
let is_compiler_and_toolchains_enabled name ~dep_names =
5157
match Config.get Compile_time.toolchains with
58+
| `Enabled when is_relocatable_compiler dep_names -> false
5259
| `Enabled -> Dune_pkg.Dev_tool.is_compiler_package name
5360
| `Disabled -> false
5461
;;

src/dune_rules/pkg_toolchain.mli

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ open Import
55
Set to [Dune_util.cache_home_dir/toolchains]. *)
66
val base_dir : unit -> Path.Outside_build_dir.t
77

8+
(** Returns true if relocatable-compiler is in the given list of package names.
9+
When true, the compiler should be treated as a regular package. *)
10+
val is_relocatable_compiler : Package.Name.t list -> bool
11+
812
(** Dune will download and build the ocaml-base-compiler and
913
ocaml-variants packages into a user-wide directory (shared among
1014
projects) rather than using the usual package management mechanism to
@@ -18,7 +22,10 @@ val base_dir : unit -> Path.Outside_build_dir.t
1822
manage their compiler installation with opam or a system package
1923
manager, as compilers packages that would be installed by dune will
2024
not work correctly. *)
21-
val is_compiler_and_toolchains_enabled : Package.Name.t -> bool
25+
val is_compiler_and_toolchains_enabled
26+
: Package.Name.t
27+
-> dep_names:Package.Name.t list
28+
-> bool
2229

2330
(** Returns the path to the directory containing the given package within the
2431
toolchain directory. This will be something like

test/blackbox-tests/test-cases/pkg/convert-opam-commands.t

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,7 +269,9 @@ Package with package conjunction and string selections inside variable interpola
269269
> build: [
270270
> [ "echo" "a %{installed}% b" ]
271271
> [ "echo" "c %{installed?x:y}% d" ]
272+
> [ "echo" "c2 %{installed?x}% d2" ]
272273
> [ "echo" "e %{foo:installed?x:y}% f" ]
274+
> [ "echo" "e2 %{foo:installed?x}% f2" ]
273275
> [ "echo" "g %{foo+bar+_:installed?x:y}% h" ]
274276
> # The "enable" variable is syntactic sugar around "installed" in some (but not all) cases.
275277
> # Its intention appears to be for use with ./configure scripts that take --enable-<feature> or
@@ -318,6 +320,12 @@ preserved between opam and dune.
318320
(if (catch_undefined_var %{pkg-self:installed} false) x y)
319321
"")
320322
" d"))
323+
(run
324+
echo
325+
(concat
326+
"c2 "
327+
(if (catch_undefined_var %{pkg-self:installed} false) x "")
328+
" d2"))
321329
(run
322330
echo
323331
(concat
@@ -326,6 +334,12 @@ preserved between opam and dune.
326334
(if (catch_undefined_var %{pkg:foo:installed} false) x y)
327335
"")
328336
" f"))
337+
(run
338+
echo
339+
(concat
340+
"e2 "
341+
(if (catch_undefined_var %{pkg:foo:installed} false) x "")
342+
" f2"))
329343
(run
330344
echo
331345
(concat
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
Test symlinks using absolute paths that point inside the target directory.
2+
Since we use realpath, these resolve correctly and work with caching.
3+
4+
$ export DUNE_CACHE=enabled
5+
$ export DUNE_CACHE_ROOT=$PWD/_cache
6+
7+
$ make_lockdir
8+
9+
$ make_lockpkg foo <<EOF
10+
> (version 0.0.1)
11+
> (build
12+
> (progn
13+
> (run true BUILDING_FOO_PACKAGE)
14+
> (run mkdir -p %{lib}/%{pkg-self:name})
15+
> (write-file %{lib}/%{pkg-self:name}/real.txt "real content\n")
16+
> (run sh -c "ln -s \$(realpath %{lib}/%{pkg-self:name}/real.txt) %{lib}/%{pkg-self:name}/link.txt")
17+
> (write-file %{lib}/%{pkg-self:name}/META "")))
18+
> EOF
19+
20+
$ cat > dune-project <<EOF
21+
> (lang dune 3.22)
22+
> (package
23+
> (name x)
24+
> (allow_empty)
25+
> (depends foo))
26+
> EOF
27+
28+
$ build_pkg foo
29+
30+
Verify the build ran:
31+
32+
$ count_trace BUILDING_FOO_PACKAGE
33+
1
34+
35+
The symlink is resolved to a regular file:
36+
37+
$ dune_cmd stat kind $(get_build_pkg_dir foo)/target/lib/foo/link.txt
38+
regular file
39+
40+
Both files are hardlinked:
41+
42+
$ dune_cmd stat hardlinks $(get_build_pkg_dir foo)/target/lib/foo/real.txt
43+
3
44+
$ dune_cmd stat hardlinks $(get_build_pkg_dir foo)/target/lib/foo/link.txt
45+
3
46+
47+
Clean and rebuild to verify cache restore:
48+
49+
$ rm -rf _build
50+
$ build_pkg foo
51+
52+
$ count_trace BUILDING_FOO_PACKAGE
53+
0
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
Test that packages with symlinks in their install output are properly cached.
2+
Dune resolves symlinks to hardlinks so that the cache can store them.
3+
4+
The compiler is one such package that has this kind of layout.
5+
6+
$ export DUNE_CACHE=enabled
7+
$ export DUNE_CACHE_ROOT=$PWD/_cache
8+
9+
$ make_lockdir
10+
11+
Create a package that installs both a regular file and a symlink to it.
12+
This is similar to how compilers install binaries (e.g., ocamlc -> ocamlc.opt).
13+
14+
$ make_lockpkg foo <<EOF
15+
> (version 0.0.1)
16+
> (build
17+
> (progn
18+
> (run true BUILDING_FOO_PACKAGE)
19+
> (run mkdir -p %{lib}/%{pkg-self:name})
20+
> (write-file %{lib}/%{pkg-self:name}/real.txt "real content\n")
21+
> (run ln -s real.txt %{lib}/%{pkg-self:name}/link.txt)
22+
> (write-file %{lib}/%{pkg-self:name}/META "")))
23+
> EOF
24+
25+
$ cat > dune-project <<EOF
26+
> (lang dune 3.22)
27+
> (package
28+
> (name x)
29+
> (allow_empty)
30+
> (depends foo))
31+
> EOF
32+
33+
Build the package:
34+
35+
$ build_pkg foo
36+
37+
Verify the build ran by checking the trace for our marker command:
38+
39+
$ count_trace BUILDING_FOO_PACKAGE
40+
1
41+
42+
Check that the files exist:
43+
44+
$ ls $(get_build_pkg_dir foo)/target/lib/foo | sort
45+
META
46+
link.txt
47+
real.txt
48+
49+
The symlink has been resolved to a regular file (hardlink to the target):
50+
51+
$ dune_cmd stat kind $(get_build_pkg_dir foo)/target/lib/foo/link.txt
52+
regular file
53+
54+
Check hardlink count. Files should have hardlinks > 1 indicating they are cached.
55+
real.txt has 3 hardlinks: original, link.txt (resolved), and cache entry.
56+
57+
$ dune_cmd stat hardlinks $(get_build_pkg_dir foo)/target/lib/foo/real.txt
58+
3
59+
$ dune_cmd stat hardlinks $(get_build_pkg_dir foo)/target/lib/foo/META
60+
2
61+
62+
Clean and rebuild to verify cache restore:
63+
64+
$ rm -rf _build
65+
$ build_pkg foo
66+
67+
The build command should not be rerun since it will be restored from cache.
68+
69+
$ count_trace BUILDING_FOO_PACKAGE
70+
0
71+
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
Test that broken symlinks are detected during symlink resolution.
2+
3+
$ make_lockdir
4+
5+
$ make_lockpkg foo <<EOF
6+
> (version 0.0.1)
7+
> (build
8+
> (progn
9+
> (run mkdir -p %{lib}/%{pkg-self:name})
10+
> (run ln -s nonexistent.txt %{lib}/%{pkg-self:name}/broken.txt)
11+
> (write-file %{lib}/%{pkg-self:name}/META "")))
12+
> EOF
13+
14+
$ cat > dune-project <<EOF
15+
> (lang dune 3.22)
16+
> (package
17+
> (name x)
18+
> (allow_empty)
19+
> (depends foo))
20+
> EOF
21+
22+
The broken symlink is detected:
23+
24+
$ build_pkg foo 2>&1 | sanitize_pkg_digest foo.0.0.1 \
25+
> | dune_cmd subst '\.sandbox/[a-f0-9]+' '.sandbox/$SANDBOX'
26+
Error:
27+
readlink(_build/.sandbox/$SANDBOX/_private/default/.pkg/foo.0.0.1-DIGEST_HASH/target/lib/foo/nonexistent.txt): No such file or directory
28+
-> required by
29+
_build/_private/default/.pkg/foo.0.0.1-DIGEST_HASH/target
30+
[1]

0 commit comments

Comments
 (0)