Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix for "dune external-lib-deps" command #46

Merged
merged 31 commits into from
Sep 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
34b777e
Replace "dune external-lib-deps" command
moyodiallo Sep 20, 2022
c545a64
Replace dune-lang by sexplib
moyodiallo Nov 28, 2022
ddb6b53
Replacing 'dune external-lib-deps'
moyodiallo Jan 12, 2023
196c27e
Remove 'dune describe' command
moyodiallo Feb 9, 2023
0350774
Get the correct libs for target: @test.
moyodiallo Feb 9, 2023
4244181
Format before writing 'dune-project' file
moyodiallo Feb 9, 2023
c854b1d
Fix test output format
moyodiallo Feb 9, 2023
50b2bca
Clean some part of the code
moyodiallo Feb 9, 2023
e52b612
Reorganize the code and improve the command
moyodiallo Apr 3, 2023
cb0d906
refactor: Avoid a recursive resolve on a public library or executable
moyodiallo Apr 4, 2023
6fb36c8
refactor the source code
moyodiallo Apr 7, 2023
3b75eaf
When an executable is copied before installed in dune
moyodiallo Apr 14, 2023
1c51ba4
Refactor: Add the extensions of executables
moyodiallo Apr 18, 2023
f55312d
Refactor the code
moyodiallo Apr 19, 2023
dd60e7e
Remove "dune build" and improve parsing copy
moyodiallo Apr 20, 2023
bcb5b46
Add tests for "dune describe" command
moyodiallo Apr 20, 2023
059ce20
Refactor the code
moyodiallo Apr 20, 2023
023e508
Make the top level expressions lazy and that also increase performance
moyodiallo Jun 7, 2023
7140d99
Add copy rules when 'dune' file exist
moyodiallo Jun 8, 2023
2cb9578
Add a correction about unresolved entries
moyodiallo Jun 8, 2023
ee1c7ea
The description is no longer valid
moyodiallo Jun 8, 2023
37b64c1
Upgrade to match the dune commands that used
moyodiallo Jul 7, 2023
f59878c
Update dependencies
moyodiallo Aug 3, 2023
aa488d0
Set the minimal version of OCaml to 4.08.0
moyodiallo Aug 4, 2023
1e81f81
Refactoring the code
moyodiallo Aug 5, 2023
895275e
Fix the update of opam files
moyodiallo Aug 30, 2023
86ba28c
check if the dune constraint matches the dune-project file
moyodiallo Sep 5, 2023
02ae26b
Add some tests
moyodiallo Sep 7, 2023
6b24915
Refactoring the source code
moyodiallo Sep 18, 2023
54a27d8
Refactoring the source code
moyodiallo Sep 22, 2023
c0f18ae
Merge branch 'master' into dune-external-libs
moyodiallo Sep 22, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
### Unreleased

- Add support for dune 3.0 , the command `dune external-lib-deps` was removed from
dune. Now, the `opam-dune-lint` command works without `dune build`. (@moyodiallo #46).

### v0.2

- Cope with missing `(depends ...)` in `dune-project` (@talex5 #33). We tried to add the missing packages to an existing depends field, but if there wasn't one at all then we did nothing.
Expand Down
13 changes: 7 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,15 @@ Wrote "dune-project"
It works as follows:

1. Lists the `*.opam` files in your project's root (ensuring they're up-to-date, if generated).
2. Runs `dune external-lib-deps --only-packages $PKG --unstable-by-dir @install` and `... @runtest` to get each package's ocamlfind dependencies.
3. Filters out local dependencies using `dune describe` (for now; would be good to lint these too in future, but needs a different code path).
4. Filters out vendored dependencies (by ignoring dependencies from subdirectories with their own `dune-project` file).
5. For each ocamlfind library, it finds the corresponding opam library by
2. Runs `dune describe external-lib-deps` to get all externals and internals ocamlfind libraries for all dune libraries, executables and tests. The information about the package is also known except for the private executables.
3. Runs `dune describe package-entries` to get all packages entries, this is for considering the external ocamlfind libraries of a private executable, because in Dune it is possible to install a private executable.
4. Resolve for each opam library its internal and external ocamlfind library dependencies using the information of 1. and 2.
5. Filters out vendored dependencies (by ignoring dependencies from subdirectories with their own `dune-project` file).
6. For each ocamlfind library, it finds the corresponding opam library by
finding its directory and then finding the `*.changes` file saying which
opam package added its `META` file.
6. Checks that each required opam package is listed in the opam file.
7. For any missing packages, it offers to add a suitable dependency, using the installed package's version as the default lower-bound.
7. Checks that each required opam package is listed in the opam file.
8. For any missing packages, it offers to add a suitable dependency, using the installed package's version as the default lower-bound.

moyodiallo marked this conversation as resolved.
Show resolved Hide resolved
`opam-dune-lint` can be run manually to update your project, or as part of CI to check for missing dependencies.
It exits with a non-zero status if changes are needed, or if the opam files were not up-to-date with the `dune-project` file.
Expand Down
184 changes: 184 additions & 0 deletions deps.ml
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
open Types
open Dune_items

type t = Dir_set.t Libraries.t

let dune_describe_external_lib_deps () = Bos.Cmd.(v "dune" % "describe" % "external-lib-deps")

let dune_describe_entries () = Bos.Cmd.(v "dune" % "describe" % "package-entries")

let describe_external_lib_deps =
Lazy.from_fun (fun _ ->
sexp @@ dune_describe_external_lib_deps ()
|> Describe_external_lib.describe_extern_of_sexp)

let describe_bin_of_entries =
Lazy.from_fun (fun _ ->
sexp @@ dune_describe_entries ()
|> Describe_entries.entries_of_sexp
|> Describe_entries.items_bin_of_entries)

let has_dune_subproject path =
if Fpath.is_current_dir path then false
else
Fpath.(path / "dune-project")
|> Bos.OS.Path.exists
|> Stdlib.Result.get_ok


let parent_path path = if Fpath.is_current_dir path then None else Some (Fpath.parent path)

let rec should_use_dir ~dir_types path =
match Hashtbl.find_opt dir_types path with
| Some x -> x
| None ->
let r =
match parent_path path with
| Some parent ->
if should_use_dir ~dir_types parent then (
not (has_dune_subproject path)
) else false
| None ->
not (has_dune_subproject path)
in
Hashtbl.add dir_types path r;
r

let copy_rules () =
Lazy.force describe_external_lib_deps
|> List.concat_map
(fun d_item ->
d_item
|> Describe_external_lib.get_item
|> (fun (item:Describe_external_lib.item) -> Fpath.(item.source_dir / "dune"))
|> Dune_rules.Copy_rules.get_copy_rules)
|> Dune_rules.Copy_rules.copy_rules_map

let bin_of_entries () = Lazy.force describe_bin_of_entries

let is_bin_name_of_describe_lib bin_name (item:Describe_external_lib.item) =
item.extensions
|> List.exists (fun extension ->
String.equal bin_name (String.cat item.name extension))

let find_package_of_exe (item:Describe_external_lib.item) =
match item.package with
| Some p -> Some p
| None ->
(* Only allow for private executables to find the package *)
item.extensions
|> List.find_map (fun extension ->
Option.map
(fun bin_name ->
Option.map
(fun (item:Describe_entries.item) -> item.package) (Item_map.find_opt bin_name @@ bin_of_entries ()))
(Dune_rules.Copy_rules.find_dest_name ~name:(String.cat item.name extension) @@ copy_rules ()))
|> Option.join

let resolve_internal_deps d_items items_pkg =
(* After the d_items are filtered to the corresponding package request,
* we need to include the internal_deps in order to reach all the deps.
* If the internal dep is a public library we skip the recursive resolve
* because it will be resolve with separate request *)
let open Describe_external_lib in
let get_name = function
| Lib item -> String.cat item.name ".lib"
| Exe item -> String.cat item.name ".exe"
| Test item -> String.cat item.name ".test"
in
let d_items_lib =
d_items
|> List.filter_map (fun d_item ->
if is_lib_item d_item then
let item = get_item d_item in
Some (item.Describe_external_lib.name ^ ".lib", Lib item)
else None)
|> List.to_seq |> Hashtbl.of_seq
in
let rec add_internal acc = function
| [] -> Hashtbl.to_seq_values acc |> List.of_seq
| item::tl ->
if Hashtbl.mem acc (get_name item) then
add_internal acc tl
else begin
Hashtbl.add acc (get_name item) item;
(get_item item).internal_deps
|> List.filter_map (fun (name, k) ->
match Hashtbl.find_opt d_items_lib (String.cat name ".lib") with
| None -> None
| Some d_item_lib ->
if Kind.is_required k && Option.is_some (get_item d_item_lib).package then None
else Some d_item_lib)
|> fun internals -> add_internal acc (tl @ internals)
end
in
add_internal (Hashtbl.create 10) items_pkg

let get_dune_items dir_types ~pkg ~target =
let d_items =
Lazy.force describe_external_lib_deps
|> List.map (fun d_item ->
let item = Describe_external_lib.get_item d_item in
if Describe_external_lib.is_exe_item d_item && Option.is_none item.package
then
match find_package_of_exe item with
| None -> d_item
| Some pkg -> Describe_external_lib.Exe { item with package = Some pkg }
else d_item)
in
let unresolved_entries =
let exe_items =
List.filter_map (function
| Describe_external_lib.Exe item -> Some item
| _ -> None) d_items
in
bin_of_entries ()
|> Item_map.partition (fun _ (entry:Describe_entries.item) ->
exe_items
|> List.exists
(fun (item:Describe_external_lib.item) ->
is_bin_name_of_describe_lib entry.bin_name item
&& Option.equal String.equal (Some entry.package) item.package))
|> snd
in
let d_items =
d_items
|> List.map (function
| Describe_external_lib.Exe item as d_item ->
item.extensions
|> List.find_map (fun extension ->
Item_map.find_opt (String.cat item.name extension) unresolved_entries)
|> (function
| None -> d_item
| Some entry -> Describe_external_lib.Exe { item with package = Some entry.package })
| d_item -> d_item)
|> List.filter (fun item ->
match (item,target) with
| Describe_external_lib.Test _, `Install -> false
| Describe_external_lib.Test _, `Runtest -> true
| _ , `Runtest -> false
| _, `Install -> true)
|> List.filter (fun d_item -> should_use_dir ~dir_types (Describe_external_lib.get_item d_item).source_dir)
in
List.filter (fun d_item ->
let item = Describe_external_lib.get_item d_item in
(* if an item has no package, we assume it's used for testing *)
if target = `Install then
Option.equal String.equal (Some pkg) item.package
else
Option.equal String.equal (Some pkg) item.package || Option.is_none item.package) d_items
|> resolve_internal_deps d_items

let lib_deps ~pkg ~target =
get_dune_items (Hashtbl.create 10) ~pkg ~target
|> List.fold_left (fun libs item ->
let item = Describe_external_lib.get_item item in
List.map (fun dep -> fst dep, item.source_dir) item.external_deps
|> List.fold_left (fun acc (lib, path) ->
if Astring.String.take ~sat:((<>) '.') lib <> pkg then
let dirs = Libraries.find_opt lib acc |> Option.value ~default:Dir_set.empty in
Libraries.add lib (Dir_set.add path dirs) acc
else
acc) libs) Libraries.empty

let get_external_lib_deps ~pkg ~target : t = lib_deps ~pkg ~target
2 changes: 1 addition & 1 deletion dune
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
(executable
(public_name opam-dune-lint)
(name main)
(libraries astring fmt fmt.tty bos opam-format opam-state dune-private-libs.dune-lang cmdliner sexplib))
(libraries astring fmt fmt.tty bos opam-format opam-state cmdliner stdune sexplib str fpath))
41 changes: 30 additions & 11 deletions dune-project
Original file line number Diff line number Diff line change
@@ -1,25 +1,44 @@
(lang dune 2.7)
(lang dune 3.10)

(name opam-dune-lint)

(formatting disabled)
moyodiallo marked this conversation as resolved.
Show resolved Hide resolved

(generate_opam_files true)
(source (github ocurrent/opam-dune-lint))

(source
(github ocurrent/opam-dune-lint))

(authors "talex5@gmail.com")

(maintainers "alpha@tarides.com" "Tim McGilchrist <timmcgil@gmail.com>")

(license ISC)

(cram enable)

(package
(name opam-dune-lint)
(synopsis "Ensure dune and opam dependencies are consistent")
(description
"opam-dune-lint checks that all ocamlfind libraries listed as dune dependencies have corresponding opam dependencies listed in the opam files. If not, it offers to add them (either to your opam files, or to your dune-project if you're generating your opam files from that).")
(depends
(astring (>= 0.8.5))
(sexplib (>= v0.14.0))
(cmdliner (>= 1.1.0))
(dune-private-libs (and (>= 2.8.0) (< 3.0)))
(dune (< 3.0))
(ocaml (>= 4.10.0))
(bos (>= 0.2.0))
(fmt (>= 0.8.9))
(opam-state (>= 2.0.9))
(fpath
(>= 0.7.3))
(astring
(>= 0.8.5))
(sexplib
(>= v0.14.0))
(cmdliner
(>= 1.1.0))
(stdune
(>= 3.10.0))
(ocaml
(>= 4.08.0))
(bos
(>= 0.2.0))
(fmt
(>= 0.8.9))
(opam-state
(>= 2.1.0))
opam-format))
82 changes: 82 additions & 0 deletions dune_constraints.ml
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
(* This source code is comming from https://github.com/ocurrent/opam-repo-ci/blob/master/lib/lint.ml
* with a slight modification *)

type error =
| DuneConstraintMissing
| DuneIsBuild
| BadDuneConstraint of string * string

let is_dune name =
OpamPackage.Name.equal name (OpamPackage.Name.of_string "dune")

let get_dune_constraint opam =
let get_max = function
| None, None -> None
| Some x, None -> Some x
| None, Some x -> Some x
| Some x, Some y when OpamVersionCompare.compare x y >= 0 -> Some x
| Some _, Some y -> Some y
in
let get_min = function
| None, None | Some _, None | None, Some _ -> None
| Some x, Some y when OpamVersionCompare.compare x y >= 0 -> Some y
| Some x, Some _ -> Some x
in
let is_build = ref false in
let rec get_lower_bound = function
| OpamFormula.Atom (OpamTypes.Constraint ((`Gt | `Geq), OpamTypes.FString version)) -> Some version
| Atom (Filter (FIdent (_, var, _))) when String.equal (OpamVariable.to_string var) "build" -> is_build := true; None (* TODO: remove this hack *)
| Empty | Atom (Filter _) | Atom (Constraint _) -> None
| Block x -> get_lower_bound x
| And (x, y) -> get_max (get_lower_bound x, get_lower_bound y)
| Or (x, y) -> get_min (get_lower_bound x, get_lower_bound y)
in
let rec aux = function
| OpamFormula.Atom (pkg, constr) ->
if is_dune pkg then
let v = get_lower_bound constr in
Some (Option.value ~default:"1.0" v)
else
None
| Empty -> None
| Block x -> aux x
| And (x, y) -> get_max (aux x, aux y)
| Or (x, y) -> get_min (aux x, aux y)
in
(!is_build, aux opam.OpamFile.OPAM.depends)

let check_dune_constraints ~errors ~dune_version pkg_name opam =
let is_build, dune_constraint = get_dune_constraint opam in
let errors =
match dune_constraint with
| None ->
if is_dune pkg_name then
errors
else
(pkg_name, DuneConstraintMissing) :: errors
| Some dep ->
if OpamVersionCompare.compare dep dune_version >= 0 then
errors
else
(pkg_name, BadDuneConstraint (dep, dune_version)) :: errors
in
if is_build then (pkg_name, DuneIsBuild) :: errors else errors

let print_msg_of_errors =
List.iter (fun (package, err) ->
let pkg = OpamPackage.Name.to_string package in
match err with
| DuneConstraintMissing ->
Fmt.epr "Warning in %s: The package has a dune-project file but no explicit dependency on dune was found.@." pkg
| DuneIsBuild ->
Fmt.epr "Warning in %s: The package tagged dune as a build dependency. \
Due to a bug in dune (https://github.com/ocaml/dune/issues/2147) this should never be the case. \
Please remove the {build} tag from its filter.@."
pkg
| BadDuneConstraint (dep, ver) ->
Fmt.failwith
"Error in %s: Your dune-project file indicates that this package requires at least dune %s \
but your opam file only requires dune >= %s. Please check which requirement is the right one, and fix the other."
pkg ver dep

)
Loading