1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254
|
open Dune_config
open Import
module Lock_dir = Dune_pkg.Lock_dir
module Pin = Dune_pkg.Pin
let is_enabled =
lazy
(match Config.get Dune_rules.Compile_time.lock_dev_tools with
| `Enabled -> true
| `Disabled -> false)
;;
(* Returns a version constraint accepting (almost) all versions whose prefix is
the given version. This allows alternative distributions of packages to be
chosen, such as choosing "ocamlformat.0.26.2+binary" when .ocamlformat
contains "version=0.26.2". *)
let relaxed_version_constraint_of_version version =
let open Dune_lang in
let min_version = Package_version.to_string version in
(* The goal here is to add a suffix to [min_version] to construct a version
number higher than than any version number likely to appear with
[min_version] as a prefix. "_" is the highest ascii symbol that can appear
in version numbers, excluding "~" which has a special meaning. It's
conceivable that one or two consecutive "_" characters may be used in a
version, so this appends "___" to [min_version].
Read more at: https://opam.ocaml.org/doc/Manual.html#Version-ordering
*)
let max_version = min_version ^ "___MAX_VERSION" in
Package_constraint.And
[ Package_constraint.Uop
(Relop.Gte, Package_constraint.Value.String_literal min_version)
; Package_constraint.Uop
(Relop.Lte, Package_constraint.Value.String_literal max_version)
]
;;
(* The solver satisfies dependencies for local packages, but dev tools
are not local packages. As a workaround, create an empty local package
which depends on the dev tool package. *)
let make_local_package_wrapping_dev_tool ~dev_tool ~dev_tool_version ~extra_dependencies
: Dune_pkg.Local_package.t
=
let dev_tool_pkg_name = Dune_pkg.Dev_tool.package_name dev_tool in
let dependency =
let open Dune_lang in
let open Package_dependency in
let constraint_ =
Option.map dev_tool_version ~f:relaxed_version_constraint_of_version
in
{ name = dev_tool_pkg_name; constraint_ }
in
let local_package_name =
Package_name.of_string (Package_name.to_string dev_tool_pkg_name ^ "_dev_tool_wrapper")
in
{ Dune_pkg.Local_package.name = local_package_name
; version = Dune_pkg.Lock_dir.Pkg_info.default_version
; dependencies =
Dune_pkg.Dependency_formula.of_dependencies (dependency :: extra_dependencies)
; conflicts = []
; depopts = []
; pins = Package_name.Map.empty
; conflict_class = []
; loc = Loc.none
; command_source = Opam_file { build = []; install = [] }
}
;;
let solve ~dev_tool ~local_packages =
let open Memo.O in
let* solver_env_from_current_system =
Pkg_common.poll_solver_env_from_current_system ()
|> Memo.of_reproducible_fiber
>>| Option.some
and* workspace =
let+ workspace = Workspace.workspace () in
match Config.get Dune_rules.Compile_time.bin_dev_tools with
| `Enabled ->
Workspace.add_repo workspace Dune_pkg.Pkg_workspace.Repository.binary_packages
| `Disabled -> workspace
in
let lock_dir = Lock_dir.dev_tool_lock_dir_path dev_tool in
Memo.of_reproducible_fiber
@@ Lock.solve
workspace
~local_packages
~project_pins:Pin.DB.empty
~solver_env_from_current_system
~version_preference:None
~lock_dirs:[ lock_dir ]
~print_perf_stats:false
~portable_lock_dir:false
;;
let compiler_package_name = Package_name.of_string "ocaml"
(* Some dev tools must be built with the same version of the ocaml
compiler as the project. This function returns the version of the
"ocaml" package used to compile the project in the default build
context.
TODO: This only makes sure that the version of compiler used to
build the dev tool matches the version of the compiler used to
build this project. This will fail if the project is built with a
custom compiler (e.g. ocaml-variants) since the version of the
compiler will be the same between the project and dev tool while
they still use different compilers. A more robust solution would be
to ensure that the exact compiler package used to build the dev
tool matches the package used to build the compiler. *)
let locked_ocaml_compiler_version () =
let open Memo.O in
let context =
(* Dev tools are only ever built with the default context. *)
Context_name.default
in
let* result = Dune_rules.Lock_dir.get context
and* platform =
Pkg_common.poll_solver_env_from_current_system () |> Memo.of_reproducible_fiber
in
match result with
| Error _ ->
User_error.raise
[ Pp.text "Unable to load the lockdir for the default build context." ]
~hints:
[ Pp.concat
~sep:Pp.space
[ Pp.text "Try running"; User_message.command "dune pkg lock" ]
]
| Ok { packages; _ } ->
let packages = Lock_dir.Packages.pkgs_on_platform_by_name packages ~platform in
(match Package_name.Map.find packages compiler_package_name with
| None ->
User_error.raise
[ Pp.textf
"The lockdir doesn't contain a lockfile for the package %S."
(Package_name.to_string compiler_package_name)
]
~hints:
[ Pp.concat
~sep:Pp.space
[ Pp.textf
"Add a dependency on %S to one of the packages in dune-project and \
then run"
(Package_name.to_string compiler_package_name)
; User_message.command "dune pkg lock"
]
]
| Some pkg -> Memo.return pkg.info.version)
;;
(* Returns a dependency constraint on the version of the ocaml
compiler in the lockdir associated with the default context. *)
let locked_ocaml_compiler_constraint () =
let open Dune_lang in
let open Memo.O in
let+ ocaml_compiler_version = locked_ocaml_compiler_version () in
let constraint_ =
Some
(Package_constraint.Uop
(Eq, String_literal (Package_version.to_string ocaml_compiler_version)))
in
{ Package_dependency.name = compiler_package_name; constraint_ }
;;
let extra_dependencies dev_tool =
let open Memo.O in
match Dune_pkg.Dev_tool.needs_to_build_with_same_compiler_as_project dev_tool with
| false -> Memo.return []
| true ->
let+ constraint_ = locked_ocaml_compiler_constraint () in
[ constraint_ ]
;;
let lockdir_status dev_tool =
let open Memo.O in
let dev_tool_lock_dir = Lock_dir.dev_tool_lock_dir_path dev_tool in
match Lock_dir.read_disk dev_tool_lock_dir with
| Error _ -> Memo.return `No_lockdir
| Ok { packages; _ } ->
(match Dune_pkg.Dev_tool.needs_to_build_with_same_compiler_as_project dev_tool with
| false -> Memo.return `Lockdir_ok
| true ->
let* platform =
Pkg_common.poll_solver_env_from_current_system () |> Memo.of_reproducible_fiber
in
let packages = Lock_dir.Packages.pkgs_on_platform_by_name packages ~platform in
(match Package_name.Map.find packages compiler_package_name with
| None -> Memo.return `No_compiler_lockfile_in_lockdir
| Some { info; _ } ->
let+ ocaml_compiler_version = locked_ocaml_compiler_version () in
(match Package_version.equal info.version ocaml_compiler_version with
| true -> `Lockdir_ok
| false ->
`Dev_tool_needs_to_be_relocked_because_project_compiler_version_changed
(User_message.make
[ Pp.textf
"The version of the compiler package (%S) in this project's \
lockdir has changed to %s (formerly the compiler version was %s). \
The dev-tool %S will be re-locked and rebuilt with this version \
of the compiler."
(Package_name.to_string compiler_package_name)
(Package_version.to_string ocaml_compiler_version)
(Package_version.to_string info.version)
(Dune_pkg.Dev_tool.package_name dev_tool |> Package_name.to_string)
]))))
;;
(* [lock_dev_tool_at_version dev_tool version] generates the lockdir for the
dev tool [dev_tool]. If [version] is [Some v] then version [v] of the tool
will be chosen by the solver. Otherwise the solver is free to choose the
appropriate version of the tool to install. *)
let lock_dev_tool_at_version dev_tool version =
let open Memo.O in
let* need_to_solve =
lockdir_status dev_tool
>>| function
| `Lockdir_ok -> false
| `No_lockdir -> true
| `No_compiler_lockfile_in_lockdir ->
Console.print
[ Pp.textf
"The lockdir for %s lacks a lockfile for %s. Regenerating..."
(Dune_pkg.Dev_tool.package_name dev_tool |> Package_name.to_string)
(Package_name.to_string compiler_package_name)
];
true
| `Dev_tool_needs_to_be_relocked_because_project_compiler_version_changed message ->
Console.print_user_message message;
true
in
if need_to_solve
then
let* extra_dependencies = extra_dependencies dev_tool in
let local_pkg =
make_local_package_wrapping_dev_tool
~dev_tool
~dev_tool_version:version
~extra_dependencies
in
let local_packages = Package_name.Map.singleton local_pkg.name local_pkg in
solve ~dev_tool ~local_packages
else Memo.return ()
;;
let lock_ocamlformat () =
let version = Dune_pkg.Ocamlformat.version_of_current_project's_ocamlformat_config () in
lock_dev_tool_at_version Ocamlformat version
;;
let lock_dev_tool dev_tool =
match (dev_tool : Dune_pkg.Dev_tool.t) with
| Ocamlformat -> lock_ocamlformat ()
| other -> lock_dev_tool_at_version other None
;;
|