Skip to content

Commit d87f4a7

Browse files
authored
Merge pull request #12653 from Alizter/push-skrkpmlkomsw
feat(pkg): autolocking
2 parents e4798dc + 80ffe25 commit d87f4a7

16 files changed

+858
-64
lines changed

src/dune_pkg/opam_repo.ml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,13 @@ let revision t =
128128
| Directory _ -> Code_error.raise "not a git repo" []
129129
;;
130130

131+
let content_digest t =
132+
match t.source with
133+
| Repo repo ->
134+
Rev_store.At_rev.rev repo |> Rev_store.Object.to_hex |> Dune_digest.string
135+
| Directory path -> Path_digest.digest_with_lstat path
136+
;;
137+
131138
let load_opam_package_from_dir ~(dir : Path.t) package =
132139
let opam_file_path = Paths.opam_file package in
133140
match Path.exists (Path.append_local dir opam_file_path) with

src/dune_pkg/opam_repo.mli

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,15 @@ val of_git_repo : Loc.t -> OpamUrl.t -> t Fiber.t
2626
val revision : t -> Rev_store.At_rev.t
2727
val serializable : t -> Serializable.t option
2828

29+
(** [content_digest t] digests the contents of an opam repository. For a Git
30+
repository, this is a digest of the commit SHA. For a directory-based
31+
repository, this is a digest of the directory's contents.
32+
33+
Raises [User_error] in the directory case if the path cannot be accessed or
34+
digested due to permission errors, the directory being deleted or modified
35+
between stat and digest, or other filesystem errors. *)
36+
val content_digest : t -> Dune_digest.t
37+
2938
module Key : sig
3039
type t
3140

src/dune_pkg/package_universe.ml

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -134,14 +134,16 @@ let check_for_unnecessary_packges_in_lock_dir
134134
])
135135
;;
136136

137-
let up_to_date local_packages ~dependency_hash:saved_dependency_hash =
137+
let dependency_digest local_packages =
138138
let local_packages =
139139
Package_name.Map.values local_packages |> List.map ~f:Local_package.for_solver
140140
in
141-
let dependency_hash =
142-
Local_package.For_solver.non_local_dependencies local_packages
143-
|> Local_package.Dependency_hash.of_dependency_formula
144-
in
141+
Local_package.For_solver.non_local_dependencies local_packages
142+
|> Local_package.Dependency_hash.of_dependency_formula
143+
;;
144+
145+
let up_to_date local_packages ~dependency_hash:saved_dependency_hash =
146+
let dependency_hash = dependency_digest local_packages in
145147
match saved_dependency_hash, dependency_hash with
146148
| None, None -> `Valid
147149
| Some lock_dir_dependency_hash, Some non_local_dependencies_hash

src/dune_pkg/package_universe.mli

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ val create
1414
-> Lock_dir.t
1515
-> (t, User_message.t) result
1616

17+
val dependency_digest
18+
: Local_package.t Package_name.Map.t
19+
-> Local_package.Dependency_hash.t option
20+
1721
(** Verifies if the dependencies described in the project file are still
1822
synchronized with the dependencies selected in the lock directroy. If it is
1923
not the case, it returns the hash of the new dependency set. *)

src/dune_pkg/path_digest.ml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
open Import
2+
3+
let digest_with_lstat path =
4+
match Path.lstat path with
5+
| Error e ->
6+
User_error.raise
7+
[ Pp.textf "Can't stat path %S:" (Path.to_string path); Unix_error.Detailed.pp e ]
8+
| Ok stats ->
9+
let stats_for_digest = Dune_digest.Stats_for_digest.of_unix_stats stats in
10+
(match Dune_digest.path_with_stats ~allow_dirs:true path stats_for_digest with
11+
| Ok digest -> digest
12+
| Error (Unix_error e) ->
13+
User_error.raise
14+
[ Pp.textf "Can't digest path %S:" (Path.to_string path)
15+
; Unix_error.Detailed.pp e
16+
]
17+
| Error Unexpected_kind ->
18+
User_error.raise
19+
[ Pp.textf
20+
"Can't digest path %S: Unexpected file kind %S (%s)"
21+
(Path.to_string path)
22+
(File_kind.to_string stats_for_digest.st_kind)
23+
(File_kind.to_string_hum stats_for_digest.st_kind)
24+
])
25+
;;

src/dune_pkg/path_digest.mli

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
open Import
2+
3+
(** [digest_with_lstat path] stats the [path] using [lstat] (without following
4+
symlinks) and computes a digest of its contents. Directories are allowed
5+
and will be digested recursively.
6+
7+
Raises [User_error] if the path cannot be stat'd (e.g., does not exist or
8+
permission denied), cannot be digested (e.g., I/O error during reading), or
9+
has an unexpected file kind (e.g., socket, FIFO). *)
10+
val digest_with_lstat : Path.t -> Dune_digest.t

src/dune_pkg/resolved_package.ml

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,11 @@ let opam_file = function
3838
| Rest t -> t.opam_file
3939
;;
4040

41+
let extra_files = function
42+
| Dune -> None
43+
| Rest t -> Some t.extra_files
44+
;;
45+
4146
let add_opam_package_to_opam_file package opam_file =
4247
opam_file
4348
|> OpamFile.OPAM.with_version (OpamPackage.version package)
@@ -188,3 +193,50 @@ let get_opam_package_files resolved_packages =
188193
| Some _ -> Some (Option.value files ~default:[]))
189194
|> Int.Map.values
190195
;;
196+
197+
let digest_extra_files : extra_files -> Dune_digest.t = function
198+
| Inside_files_dir path_opt ->
199+
(match path_opt with
200+
| None ->
201+
Sexp.List [ Atom "inside_files_dir"; Atom "none" ]
202+
|> Sexp.to_string
203+
|> Dune_digest.string
204+
| Some path -> Path_digest.digest_with_lstat path)
205+
| Git_files (path_opt, rev) ->
206+
let path_str =
207+
match path_opt with
208+
| None -> "None"
209+
| Some p -> sprintf "Some %s" (Path.Local.to_string p)
210+
in
211+
Sexp.List
212+
[ Atom "git_files"
213+
; Atom path_str
214+
; Atom (Rev_store.At_rev.rev rev |> Rev_store.Object.to_hex)
215+
]
216+
|> Sexp.to_string
217+
|> Dune_digest.string
218+
;;
219+
220+
let digest res_pkg =
221+
(* We are explicitly ignoring [loc] here because we don't need to take into
222+
account the location of the opam file. *)
223+
Sexp.record
224+
[ "opam_file", Atom (OpamFile.OPAM.write_to_string (opam_file res_pkg))
225+
; ( "package"
226+
, let opam_pkg = package res_pkg in
227+
Sexp.record
228+
[ "name", Atom (OpamPackage.name opam_pkg |> OpamPackage.Name.to_string)
229+
; "version", Atom (OpamPackage.version opam_pkg |> OpamPackage.Version.to_string)
230+
] )
231+
; "dune_build", Atom (dune_build res_pkg |> Bool.to_string)
232+
; ( "extra_files"
233+
, Atom
234+
(extra_files res_pkg
235+
|> Option.map ~f:digest_extra_files
236+
|> Dune_digest.Feed.compute_digest
237+
(Dune_digest.Feed.option Dune_digest.Feed.digest)
238+
|> Dune_digest.to_string) )
239+
]
240+
|> Sexp.to_string
241+
|> Dune_digest.string
242+
;;

src/dune_pkg/resolved_package.mli

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,12 @@ val local_package
3535
val get_opam_package_files
3636
: t list
3737
-> (File_entry.t list list, User_message.t) result Fiber.t
38+
39+
(** [digest t] computes a digest of the resolved package contents, excluding the
40+
source location. For directory-based extra files, the digest of the
41+
directory contents is included. For git-based extra files, the commit SHA is
42+
included.
43+
44+
Raises [User_error] if extra files in a directory cannot be accessed or
45+
digested due to permission errors, filesystem errors. *)
46+
val digest : t -> Dune_digest.t

src/dune_rules/lock_action.ml

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
open Import
2+
3+
include struct
4+
open Dune_pkg
5+
module Solver_env = Solver_env
6+
module Opam_repo = Opam_repo
7+
module Local_package = Local_package
8+
module Resolved_package = Resolved_package
9+
module Version_preference = Version_preference
10+
module Package_universe = Package_universe
11+
module Package_dependency = Package_dependency
12+
module Opam_solver = Opam_solver
13+
module Sys_poll = Sys_poll
14+
end
15+
16+
module Spec = struct
17+
type ('path, 'target) t =
18+
{ target : 'target
19+
; lock_dir : 'path
20+
; packages : Local_package.t Package.Name.Map.t
21+
; repos : Opam_repo.t list
22+
; solver_env_from_context : Solver_env.t
23+
; unset_solver_vars : Package_variable_name.Set.t
24+
; constraints : Package_dependency.t list
25+
; selected_depopts : Package.Name.t list
26+
; pins : Resolved_package.t Package.Name.Map.t
27+
; version_preference : Version_preference.t
28+
}
29+
30+
let name = "lock"
31+
let version = 1
32+
let bimap t f g = { t with lock_dir = f t.lock_dir; target = g t.target }
33+
let is_useful_to ~memoize = memoize
34+
35+
let encode
36+
{ target
37+
; lock_dir
38+
; packages
39+
; repos
40+
; solver_env_from_context
41+
; unset_solver_vars
42+
; constraints
43+
; selected_depopts
44+
; pins
45+
; version_preference
46+
}
47+
encode_path
48+
encode_target
49+
=
50+
Sexp.record
51+
[ "target", encode_target target
52+
; "lock_dir", encode_path lock_dir
53+
; ( "packages"
54+
, match Package_universe.dependency_digest packages with
55+
| None -> Atom "no packages"
56+
| Some hash ->
57+
List [ Atom "hash"; Atom (Local_package.Dependency_hash.to_string hash) ] )
58+
; ( "repos"
59+
, List
60+
(List.map repos ~f:(fun repo ->
61+
Sexp.Atom (Opam_repo.content_digest repo |> Dune_digest.to_string))) )
62+
; ( "solver_env_from_context"
63+
, Atom
64+
(Dune_digest.Feed.compute_digest
65+
Solver_env.digest_feed
66+
solver_env_from_context
67+
|> Dune_digest.to_string) )
68+
; ( "unset_solver_vars"
69+
, List
70+
(Package_variable_name.Set.to_list unset_solver_vars
71+
|> List.sort ~compare:Package_variable_name.compare
72+
|> List.map ~f:(fun var -> Sexp.Atom (Package_variable_name.to_string var)))
73+
)
74+
; ( "constraints"
75+
, List
76+
(List.sort constraints ~compare:(fun a b ->
77+
Dune_lang.Package_name.compare
78+
a.Package_dependency.name
79+
b.Package_dependency.name)
80+
|> List.map ~f:(fun { Package_dependency.name; constraint_ } ->
81+
let name = Dune_lang.Package_name.to_string name in
82+
let constraint_ =
83+
match constraint_ with
84+
| None -> "no constraints"
85+
| Some c -> Package_dependency.Constraint.to_dyn c |> Dyn.to_string
86+
in
87+
Sexp.List [ Sexp.Atom name; Sexp.Atom constraint_ ])) )
88+
; ( "selected_depopts"
89+
, List
90+
(List.sort selected_depopts ~compare:Dune_lang.Package_name.compare
91+
|> List.map ~f:(fun pkg_name ->
92+
Sexp.Atom (Dune_lang.Package_name.to_string pkg_name))) )
93+
; ( "pins"
94+
, List
95+
(Dune_lang.Package_name.Map.to_list pins
96+
|> List.sort ~compare:(fun (a, _) (b, _) ->
97+
Dune_lang.Package_name.compare a b)
98+
|> List.map ~f:(fun (pkg_name, resolved_pkg) ->
99+
let name = Dune_lang.Package_name.to_string pkg_name in
100+
let digest =
101+
Resolved_package.digest resolved_pkg |> Dune_digest.to_string
102+
in
103+
Sexp.List [ Sexp.Atom name; Sexp.Atom digest ])) )
104+
; ( "version_preference"
105+
, Atom
106+
(match version_preference with
107+
| Oldest -> "oldest"
108+
| Newest -> "newest") )
109+
]
110+
;;
111+
112+
let action
113+
{ target
114+
; lock_dir = _
115+
; packages
116+
; repos
117+
; solver_env_from_context
118+
; unset_solver_vars
119+
; constraints
120+
; selected_depopts
121+
; pins
122+
; version_preference
123+
}
124+
~ectx:_
125+
~eenv:{ Action.Ext.Exec.env; _ }
126+
=
127+
let open Fiber.O in
128+
let* () = Fiber.return () in
129+
let local_packages = Package.Name.Map.map packages ~f:Local_package.for_solver in
130+
(* Whether or not the lock directory we are creating is portable or not
131+
doesn't concern us. We therefore set it as non-portable. *)
132+
let portable_lock_dir = false in
133+
let* solver_env =
134+
let open Fiber.O in
135+
let+ solver_env_from_current_system =
136+
Sys_poll.make ~path:(Env_path.path env) |> Sys_poll.solver_env_from_current_system
137+
in
138+
let solver_env =
139+
[ solver_env_from_current_system; solver_env_from_context ]
140+
|> List.fold_left ~init:Solver_env.with_defaults ~f:Solver_env.extend
141+
in
142+
Solver_env.unset_multi solver_env unset_solver_vars
143+
in
144+
let* solver_result =
145+
Opam_solver.solve_lock_dir
146+
solver_env
147+
version_preference
148+
repos
149+
~pins
150+
~local_packages
151+
~constraints
152+
~selected_depopts
153+
~portable_lock_dir
154+
in
155+
match solver_result with
156+
| Error (`Manifest_error diagnostic) -> raise (User_error.E diagnostic)
157+
| Error (`Solve_error diagnostic) -> User_error.raise [ diagnostic ]
158+
| Ok { pinned_packages; files; lock_dir; _ } ->
159+
let lock_dir_path = Path.build target in
160+
let+ lock_dir =
161+
Dune_pkg.Lock_dir.compute_missing_checksums ~pinned_packages lock_dir
162+
in
163+
Dune_pkg.Lock_dir.Write_disk.prepare
164+
~portable_lock_dir
165+
~lock_dir_path
166+
~files
167+
lock_dir
168+
|> Dune_pkg.Lock_dir.Write_disk.commit
169+
;;
170+
end
171+
172+
module A = Action_ext.Make (Spec)
173+
174+
let action
175+
~target
176+
~lock_dir
177+
~packages
178+
~repos
179+
~solver_env_from_context
180+
~unset_solver_vars
181+
~constraints
182+
~selected_depopts
183+
~pins
184+
~version_preference
185+
=
186+
A.action
187+
{ Spec.target
188+
; lock_dir
189+
; packages
190+
; repos
191+
; solver_env_from_context
192+
; unset_solver_vars
193+
; constraints
194+
; selected_depopts
195+
; pins
196+
; version_preference
197+
}
198+
;;

src/dune_rules/lock_action.mli

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
open Import
2+
3+
val action
4+
: target:Path.Build.t
5+
-> lock_dir:Path.t
6+
-> packages:Dune_pkg.Local_package.t Package.Name.Map.t
7+
-> repos:Dune_pkg.Opam_repo.t list
8+
-> solver_env_from_context:Dune_pkg.Solver_env.t
9+
-> unset_solver_vars:Package_variable_name.Set.t
10+
-> constraints:Dune_pkg.Package_dependency.t list
11+
-> selected_depopts:Dune_pkg.Package_name.t list
12+
-> pins:Dune_pkg.Resolved_package.t Dune_lang.Package_name.Map.t
13+
-> version_preference:Dune_pkg.Version_preference.t
14+
-> Action.t

0 commit comments

Comments
 (0)