Skip to content

Commit e3e935a

Browse files
committed
feat(pkg): allow lock dir pkgs to dep on workspace pkgs (in&out)
Signed-off-by: Ali Caglayan <alizter@gmail.com>
1 parent 90d851d commit e3e935a

17 files changed

Lines changed: 389 additions & 218 deletions

File tree

bin/describe/describe_pkg.ml

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,18 @@ module Local_package = Dune_pkg.Local_package
55
module Show_lock = struct
66
let print_lock lock_dir_arg () =
77
let open Fiber.O in
8-
let* lock_dir_paths =
9-
Memo.run (Workspace.workspace ())
10-
>>| Pkg.Pkg_common.Lock_dirs_arg.lock_dirs_of_workspace lock_dir_arg
8+
let* lock_dir_paths, external_packages =
9+
Memo.run
10+
(let open Memo.O in
11+
let* workspace = Workspace.workspace () in
12+
let+ local_packages = Pkg.Pkg_common.find_local_packages in
13+
( Pkg.Pkg_common.Lock_dirs_arg.lock_dirs_of_workspace lock_dir_arg workspace
14+
, Package_name.Map.keys local_packages |> Package_name.Set.of_list ))
1115
in
1216
Fiber.parallel_map lock_dir_paths ~f:(fun lock_dir_path ->
1317
let lock_dir_path = Path.source lock_dir_path in
1418
let+ platform = Pkg.Pkg_common.solver_env_from_system_and_context ~lock_dir_path in
15-
let lock_dir = Lock_dir.read_disk_exn lock_dir_path in
19+
let lock_dir = Lock_dir.read_disk_exn lock_dir_path ~external_packages in
1620
let packages =
1721
Lock_dir.Packages.pkgs_on_platform_by_name lock_dir.packages ~platform
1822
|> Package_name.Map.values
@@ -121,14 +125,14 @@ module List_locked_dependencies = struct
121125
|> Pp.vbox
122126
;;
123127

124-
let enumerate_lock_dirs_by_path workspace ~lock_dirs =
128+
let enumerate_lock_dirs_by_path workspace ~lock_dirs ~external_packages =
125129
let lock_dirs =
126130
Pkg.Pkg_common.Lock_dirs_arg.lock_dirs_of_workspace lock_dirs workspace
127131
in
128132
List.filter_map lock_dirs ~f:(fun lock_dir_path ->
129133
if Fpath.exists (Path.Source.to_string lock_dir_path)
130134
then (
131-
match Lock_dir.read_disk_exn (Path.source lock_dir_path) with
135+
match Lock_dir.read_disk_exn (Path.source lock_dir_path) ~external_packages with
132136
| lock_dir -> Some (lock_dir_path, lock_dir)
133137
| exception User_error.E e ->
134138
User_warning.emit
@@ -144,11 +148,17 @@ module List_locked_dependencies = struct
144148
let list_locked_dependencies ~transitive ~lock_dirs () =
145149
let open Fiber.O in
146150
let* lock_dirs_by_path, local_packages =
147-
let open Memo.O in
148-
Memo.both
149-
(Workspace.workspace () >>| enumerate_lock_dirs_by_path ~lock_dirs)
150-
Pkg.Pkg_common.find_local_packages
151-
|> Memo.run
151+
Memo.run
152+
(let open Memo.O in
153+
let* local_packages = Pkg.Pkg_common.find_local_packages in
154+
let external_packages =
155+
Package_name.Map.keys local_packages |> Package_name.Set.of_list
156+
in
157+
let+ lock_dirs_by_path =
158+
Workspace.workspace ()
159+
>>| enumerate_lock_dirs_by_path ~lock_dirs ~external_packages
160+
in
161+
lock_dirs_by_path, local_packages)
152162
in
153163
let+ pp =
154164
Fiber.parallel_map lock_dirs_by_path ~f:(fun (lock_dir_path, lock_dir) ->

bin/lock_dev_tool.ml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,11 @@ let lockdir_status dev_tool =
169169
| false -> Memo.return `No_lockdir
170170
| true ->
171171
let dev_tool_lock_dir = Path.external_ dev_tool_lock_dir in
172-
(match Lock_dir.read_disk dev_tool_lock_dir with
172+
(* Dev tool lockdirs are produced by the solver running over a single
173+
package and never reference workspace packages, so validate strictly. *)
174+
(match
175+
Lock_dir.read_disk dev_tool_lock_dir ~external_packages:Package_name.Set.empty
176+
with
173177
| Error _ -> Memo.return `No_lockdir
174178
| Ok { packages; _ } ->
175179
let* platform =

bin/pkg/outdated.ml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,10 @@ let find_outdated_packages ~transitive ~lock_dirs_arg () =
1515
~repositories:(repositories_of_lock_dir workspace ~lock_dir_path)
1616
and+ local_packages = Memo.run find_local_packages
1717
and+ platform = solver_env_from_system_and_context ~lock_dir_path in
18-
let lock_dir = Dune_pkg.Lock_dir.read_disk_exn lock_dir_path in
18+
let external_packages =
19+
Package_name.Map.keys local_packages |> Package_name.Set.of_list
20+
in
21+
let lock_dir = Dune_pkg.Lock_dir.read_disk_exn lock_dir_path ~external_packages in
1922
let packages =
2023
Dune_pkg.Lock_dir.Packages.pkgs_on_platform_by_name lock_dir.packages ~platform
2124
in

bin/pkg/validate_lock_dir.ml

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,15 @@ let info =
1515
(* CR-someday alizter: The logic here is a little more complicated than it needs
1616
to be and can be simplified. *)
1717

18-
let enumerate_lock_dirs_by_path ~lock_dirs () =
18+
let enumerate_lock_dirs_by_path ~lock_dirs ~external_packages () =
1919
let open Memo.O in
2020
let+ per_contexts =
2121
Workspace.workspace () >>| Pkg_common.Lock_dirs_arg.lock_dirs_of_workspace lock_dirs
2222
in
2323
List.filter_map per_contexts ~f:(fun lock_dir_path ->
2424
if Fpath.exists (Path.Source.to_string lock_dir_path)
2525
then (
26-
match Lock_dir.read_disk_exn (Path.source lock_dir_path) with
26+
match Lock_dir.read_disk_exn (Path.source lock_dir_path) ~external_packages with
2727
| lock_dir -> Some (Ok (lock_dir_path, lock_dir))
2828
| exception User_error.E e -> Some (Error (lock_dir_path, `Parse_error e)))
2929
else None)
@@ -32,8 +32,16 @@ let enumerate_lock_dirs_by_path ~lock_dirs () =
3232
let validate_lock_dirs ~lock_dirs () =
3333
let open Fiber.O in
3434
let* lock_dirs_by_path, local_packages =
35-
Memo.both (enumerate_lock_dirs_by_path ~lock_dirs ()) Pkg_common.find_local_packages
36-
|> Memo.run
35+
Memo.run
36+
(let open Memo.O in
37+
let* local_packages = Pkg_common.find_local_packages in
38+
let external_packages =
39+
Package_name.Map.keys local_packages |> Package_name.Set.of_list
40+
in
41+
let+ lock_dirs_by_path =
42+
enumerate_lock_dirs_by_path ~lock_dirs ~external_packages ()
43+
in
44+
lock_dirs_by_path, local_packages)
3745
in
3846
if List.is_empty lock_dirs_by_path
3947
then

src/dune_pkg/lock_dir.ml

Lines changed: 67 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1148,11 +1148,14 @@ type missing_dependency =
11481148
; loc : Loc.t
11491149
}
11501150

1151-
(* [validate_packages packages] returns
1151+
(* [validate_packages packages ~external_packages] returns
11521152
[Error (`Missing_dependencies missing_dependencies)] where
11531153
[missing_dependencies] is a non-empty list with an element for each package
1154-
dependency which doesn't have a corresponding entry in [packages]. *)
1155-
let validate_packages packages =
1154+
dependency which doesn't have a corresponding entry in [packages] and is
1155+
not in [external_packages]. The [external_packages] set names dependency
1156+
targets that callers vouch for outside the lockdir (e.g. workspace
1157+
packages whose install entries are materialised by the build path). *)
1158+
let validate_packages packages ~external_packages =
11561159
let missing_dependencies =
11571160
Packages.to_pkg_list packages
11581161
|> List.concat_map ~f:(fun (dependant_package : Pkg.t) ->
@@ -1163,6 +1166,7 @@ let validate_packages packages =
11631166
if
11641167
Package_name.Map.mem packages depend.name
11651168
|| Package_name.equal depend.name Dune_dep.name
1169+
|| Package_name.Set.mem external_packages depend.name
11661170
then None
11671171
else Some { dependant_package; dependency = depend.name; loc = depend.loc })))
11681172
in
@@ -1184,7 +1188,11 @@ let create_latest_version
11841188
Package_name.Map.map packages ~f:(fun (pkg : Pkg.t) ->
11851189
Package_version.Map.singleton pkg.info.version pkg)
11861190
in
1187-
(match validate_packages packages with
1191+
(* The solver rejects workspace dependencies, so a well-formed solver
1192+
output should never contain references to packages outside the
1193+
lockdir. We pass [external_packages:empty] here so this assert catches
1194+
any solver bug that lets such a reference slip through. *)
1195+
(match validate_packages packages ~external_packages:Package_name.Set.empty with
11881196
| Ok () -> ()
11891197
| Error (`Missing_dependencies missing_dependencies) ->
11901198
List.map missing_dependencies ~f:(fun { dependant_package; dependency; loc = _ } ->
@@ -1658,40 +1666,6 @@ struct
16581666
package_name
16591667
;;
16601668

1661-
let check_packages packages ~lock_dir_path =
1662-
match validate_packages packages with
1663-
| Ok () -> Ok ()
1664-
| Error (`Missing_dependencies missing_dependencies) ->
1665-
List.iter missing_dependencies ~f:(fun { dependant_package; dependency; loc } ->
1666-
User_message.prerr
1667-
(User_message.make
1668-
~loc
1669-
[ Pp.textf
1670-
"The package %S depends on the package %S, but %S does not appear in \
1671-
the lockdir %s."
1672-
(Package_name.to_string dependant_package.info.name)
1673-
(Package_name.to_string dependency)
1674-
(Package_name.to_string dependency)
1675-
(Path.to_string_maybe_quoted lock_dir_path)
1676-
]));
1677-
Error
1678-
(User_error.make
1679-
~hints:
1680-
[ Pp.concat
1681-
~sep:Pp.space
1682-
[ Pp.text
1683-
"This could indicate that the lockdir is corrupted. Delete it and \
1684-
then regenerate it by running:"
1685-
; User_message.command "dune pkg lock"
1686-
]
1687-
]
1688-
[ Pp.textf
1689-
"At least one package dependency is itself not present as a package in \
1690-
the lockdir %s."
1691-
(Path.to_string_maybe_quoted lock_dir_path)
1692-
])
1693-
;;
1694-
16951669
let load lock_dir_path =
16961670
let event =
16971671
Dune_trace.(
@@ -1735,25 +1709,15 @@ struct
17351709
pkg)
17361710
>>| Packages.of_pkg_list
17371711
in
1738-
let result =
1739-
check_packages packages ~lock_dir_path
1740-
|> Result.map ~f:(fun () ->
1741-
{ version
1742-
; dependency_hash
1743-
; packages
1744-
; ocaml
1745-
; repos
1746-
; expanded_solver_variable_bindings
1747-
; solved_for_platforms
1748-
})
1749-
in
17501712
Option.iter (Dune_trace.global ()) ~f:(fun trace -> Dune_trace.Out.finish trace event);
1751-
result
1752-
;;
1753-
1754-
let load_exn lock_dir_path =
1755-
let open Io.O in
1756-
load lock_dir_path >>| User_error.ok_exn
1713+
{ version
1714+
; dependency_hash
1715+
; packages
1716+
; ocaml
1717+
; repos
1718+
; expanded_solver_variable_bindings
1719+
; solved_for_platforms
1720+
}
17571721
;;
17581722
end
17591723

@@ -1771,8 +1735,53 @@ module Load_immediate = Make_load (struct
17711735
let with_lexbuf_from_file = Io.with_lexbuf_from_file
17721736
end)
17731737

1774-
let read_disk = Load_immediate.load
1775-
let read_disk_exn = Load_immediate.load_exn
1738+
(* Verify every dependency edge in [packages] resolves to a known target:
1739+
either another package in the lockdir, dune itself, or a name in
1740+
[external_packages] (typically workspace packages whose install entries
1741+
are materialised outside the lockdir). *)
1742+
let check_packages packages ~lock_dir_path ~external_packages =
1743+
match validate_packages packages ~external_packages with
1744+
| Ok () -> Ok ()
1745+
| Error (`Missing_dependencies missing_dependencies) ->
1746+
List.iter missing_dependencies ~f:(fun { dependant_package; dependency; loc } ->
1747+
User_message.prerr
1748+
(User_message.make
1749+
~loc
1750+
[ Pp.textf
1751+
"The package %S depends on the package %S, but %S does not appear in the \
1752+
lockdir %s."
1753+
(Package_name.to_string dependant_package.info.name)
1754+
(Package_name.to_string dependency)
1755+
(Package_name.to_string dependency)
1756+
(Path.to_string_maybe_quoted lock_dir_path)
1757+
]));
1758+
Error
1759+
(User_error.make
1760+
~hints:
1761+
[ Pp.concat
1762+
~sep:Pp.space
1763+
[ Pp.text
1764+
"This could indicate that the lockdir is corrupted. Delete it and \
1765+
then regenerate it by running:"
1766+
; User_message.command "dune pkg lock"
1767+
]
1768+
]
1769+
[ Pp.textf
1770+
"At least one package dependency is itself not present as a package in the \
1771+
lockdir %s."
1772+
(Path.to_string_maybe_quoted lock_dir_path)
1773+
])
1774+
;;
1775+
1776+
let read_disk lock_dir_path ~external_packages =
1777+
let t = Load_immediate.load lock_dir_path in
1778+
check_packages t.packages ~lock_dir_path ~external_packages
1779+
|> Result.map ~f:(fun () -> t)
1780+
;;
1781+
1782+
let read_disk_exn lock_dir_path ~external_packages =
1783+
read_disk lock_dir_path ~external_packages |> User_error.ok_exn
1784+
;;
17761785

17771786
let transitive_dependency_closure t ~platform start =
17781787
let missing_packages =

src/dune_pkg/lock_dir.mli

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -157,8 +157,27 @@ module Write_disk : sig
157157
val commit : t -> unit
158158
end
159159

160-
val read_disk : Path.t -> (t, User_message.t) result
161-
val read_disk_exn : Path.t -> t
160+
(** Read a lockdir from disk and verify that every dependency edge resolves
161+
to either another package in the lockdir, dune itself, or a name in
162+
[external_packages]. Pass [external_packages:Package_name.Set.empty]
163+
to require the lockdir to be self-contained; pass workspace package
164+
names to permit hand-written lockdirs that reference them. *)
165+
val read_disk
166+
: Path.t
167+
-> external_packages:Package_name.Set.t
168+
-> (t, User_message.t) result
169+
170+
val read_disk_exn : Path.t -> external_packages:Package_name.Set.t -> t
171+
172+
(** Verify the dependency graph of a loaded lockdir against an
173+
[external_packages] set. Reports each missing edge with its source
174+
location and returns a single [User_message.t] describing the overall
175+
failure. *)
176+
val check_packages
177+
: Packages.t
178+
-> lock_dir_path:Path.t
179+
-> external_packages:Package_name.Set.t
180+
-> (unit, User_message.t) result
162181

163182
module Make_load (Io : sig
164183
include Monad.S
@@ -167,8 +186,10 @@ module Make_load (Io : sig
167186
val readdir_with_kinds : Path.t -> (Filename.t * Unix.file_kind) list t
168187
val with_lexbuf_from_file : Path.t -> f:(Lexing.lexbuf -> 'a) -> 'a t
169188
end) : sig
170-
val load : Path.t -> (t, User_message.t) result Io.t
171-
val load_exn : Path.t -> t Io.t
189+
(** Load a lockdir from disk without validating its dependency graph.
190+
Callers are responsible for running {!check_packages} with the
191+
appropriate [external_packages] set. *)
192+
val load : Path.t -> t Io.t
172193
end
173194

174195
(** [transitive_dependency_closure t ~platform names] returns the set of package names

0 commit comments

Comments
 (0)