├── .gitignore ├── CHANGES.md ├── LICENSE.md ├── Makefile ├── README.md ├── deps.ml ├── dune ├── dune-project ├── dune_constraints.ml ├── dune_items.ml ├── dune_project.ml ├── dune_project.mli ├── dune_rules.ml ├── formula.ml ├── formula.mli ├── index.ml ├── main.ml ├── main.mli ├── opam-dune-lint.opam ├── opam-dune-lint.opam.template ├── tests ├── dune ├── test_dune.t ├── test_dune_constraints.t ├── test_dune_copy_install.t ├── test_dune_describe.t ├── test_dune_same_exe_name.t ├── test_dune_stanza_install.t ├── test_empty_dune.t ├── test_fix_bug.t ├── test_fix_bug_59.t ├── test_fix_bug_66.t ├── test_fix_bug_68.t ├── test_opam.t ├── test_opam_update.t ├── test_optional_public_lib.t ├── test_public_lib.t └── test_vendoring.t └── types.ml /.gitignore: -------------------------------------------------------------------------------- 1 | _build 2 | .merlin 3 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | ### v0.6 2 | 3 | - Fix the issue #68, with Sexp format dune parse quoted string by escaping "%{" to become "\%{" that OpamFile module can't parse. So we switch to Csexp format which doesn't change quoted string (@moyodiallo #69). 4 | 5 | - Fix the issue #66, when the content of dune-project file ends up with a comment, the Sexplib parse fails because the last `)` falls into the comment (@moyodiallo #67). 6 | 7 | ### v0.5 8 | 9 | - Fix the lower bound to have 4.08.0 as the minimal version of OCaml (@moyodiallo #64). 10 | 11 | - Fix the issue #61, Dune stanza `(generate_opam_files true)` is same as `(generate_opam_files)` stanza (@devvydeebug #62). 12 | 13 | - Fix the issue #59, Sexplib parse fails because of Dune stanza description quote( `"\|` or `"\>`) (@moyodiallo #60). 14 | 15 | ### v0.4 16 | 17 | - Fix the issue #53. Skip resolving a public library when it is added as optional dependency(dune's libraries stanza) (@moyodiallo #54). 18 | 19 | - Print all the errors before the exit (@moyodiallo #55). 20 | 21 | ### v0.3 22 | 23 | - Fix the issue #51, when there's no package declared in `dune-project` file (@moyodiallo #52). 24 | 25 | - Add support for dune 3.0 , the command `dune external-lib-deps` was removed from 26 | dune. Now, the `opam-dune-lint` command works without `dune build`. (@moyodiallo #46). 27 | 28 | ### v0.2 29 | 30 | - 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. 31 | 32 | - Use quoted versions in the fix suggestion string (@tmcgilchrist #32). Makes copy-and-paste easier for people using it via a web UI. 33 | 34 | - Support older versions of OCaml back to 4.10 (@tmcgilchrist #31). 35 | 36 | - Ignore dependencies on sub-packages (@dra27 #27). Library `foo` may depend on library `foo.bar` but this cannot introduce an opam dependency on `foo` in `foo.opam`. 37 | 38 | - Require opam libraries compatible with the client (@dra27 #26). 39 | 40 | - Add support for multiple dependency clauses for the same package (@kit-ty-kate #25). 41 | 42 | - Upgrade to dune-private-libs 2.8.0 (@kit-ty-kate #20). 43 | 44 | - Remove dependency on ocamlfind, as we don't use it for anything now (@talex5 #18). 45 | 46 | ### v0.1 47 | 48 | Initial release. 49 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 Thomas Leonard 2 | 3 | Permission to use, copy, modify, and distribute this software for any 4 | purpose with or without fee is hereby granted, provided that the above 5 | copyright notice and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: 2 | dune build @install @runtest 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # opam-dune-lint 2 | 3 | `opam-dune-lint` checks that all ocamlfind libraries listed as dune 4 | dependencies have corresponding opam dependencies listed in the opam files. 5 | 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). 6 | 7 | Example: 8 | 9 | ``` 10 | $ ls *.opam 11 | current_ocluster.opam ocluster-api.opam ocluster.opam 12 | 13 | $ opam-dune-lint 14 | current_ocluster.opam: changes needed: 15 | "ppx_deriving" {>= 5.1} [from (ppx), ocurrent-plugin] 16 | ocluster-api.opam: changes needed: 17 | "ppx_deriving" {>= 5.1} [from (ppx), api] 18 | ocluster.opam: changes needed: 19 | "capnp-rpc-lwt" {>= 0.8.0} [from scheduler, worker] 20 | "capnp-rpc-net" {>= 0.8.0} [from scheduler] 21 | "ppx_sexp_conv" {>= v0.14.1} [from (ppx)] 22 | "prometheus" {>= 0.7} [from scheduler] 23 | "alcotest-lwt" {with-test} [from test] (missing {with-test} annotation) 24 | Note: version numbers are just suggestions based on the currently installed version. 25 | Write changes? [y] y 26 | Wrote "dune-project" 27 | ``` 28 | 29 | It works as follows: 30 | 31 | 1. Lists the `*.opam` files in your project's root (ensuring they're up-to-date, if generated). 32 | 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. 33 | 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. 34 | 4. Resolve for each opam library its internal and external ocamlfind library dependencies using the information of 1. and 2. 35 | 5. Filters out vendored dependencies (by ignoring dependencies from subdirectories with their own `dune-project` file). 36 | 6. For each ocamlfind library, it finds the corresponding opam library by 37 | finding its directory and then finding the `*.changes` file saying which 38 | opam package added its `META` file. 39 | 7. Checks that each required opam package is listed in the opam file. 40 | 8. For any missing packages, it offers to add a suitable dependency, using the installed package's version as the default lower-bound. 41 | 42 | `opam-dune-lint` can be run manually to update your project, or as part of CI to check for missing dependencies. 43 | 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. 44 | When run interactively, it asks for confirmation before writing files. 45 | If `stdin` is not a tty, then it does not write changes unless run with `-f`. 46 | -------------------------------------------------------------------------------- /deps.ml: -------------------------------------------------------------------------------- 1 | open Types 2 | open Dune_items 3 | 4 | type t = Dir_set.t Libraries.t 5 | 6 | let dune_describe_external_lib_deps () = Bos.Cmd.(v "dune" % "describe" % "external-lib-deps") 7 | 8 | let dune_describe_entries () = Bos.Cmd.(v "dune" % "describe" % "package-entries") 9 | 10 | let describe_external_lib_deps = 11 | Lazy.from_fun (fun _ -> 12 | sexp @@ dune_describe_external_lib_deps () 13 | |> Describe_external_lib.describe_extern_of_sexp) 14 | 15 | let describe_bin_of_entries = 16 | Lazy.from_fun (fun _ -> 17 | sexp @@ dune_describe_entries () 18 | |> Describe_entries.entries_of_sexp 19 | |> Describe_entries.items_bin_of_entries) 20 | 21 | let has_dune_subproject path = 22 | if Fpath.is_current_dir path then false 23 | else 24 | Fpath.(path / "dune-project") 25 | |> Bos.OS.Path.exists 26 | |> Stdlib.Result.get_ok 27 | 28 | 29 | let parent_path path = if Fpath.is_current_dir path then None else Some (Fpath.parent path) 30 | 31 | let rec should_use_dir ~dir_types path = 32 | match Hashtbl.find_opt dir_types path with 33 | | Some x -> x 34 | | None -> 35 | let r = 36 | match parent_path path with 37 | | Some parent -> 38 | if should_use_dir ~dir_types parent then ( 39 | not (has_dune_subproject path) 40 | ) else false 41 | | None -> 42 | not (has_dune_subproject path) 43 | in 44 | Hashtbl.add dir_types path r; 45 | r 46 | 47 | let copy_rules () = 48 | Lazy.force describe_external_lib_deps 49 | |> List.concat_map 50 | (fun d_item -> 51 | d_item 52 | |> Describe_external_lib.get_item 53 | |> (fun (item:Describe_external_lib.item) -> Fpath.(item.source_dir / "dune")) 54 | |> Dune_rules.Copy_rules.get_copy_rules) 55 | |> Dune_rules.Copy_rules.copy_rules_map 56 | 57 | let bin_of_entries () = Lazy.force describe_bin_of_entries 58 | 59 | let is_bin_name_of_describe_lib bin_name (item:Describe_external_lib.item) = 60 | item.extensions 61 | |> List.exists (fun extension -> 62 | String.equal bin_name (String.cat item.name extension)) 63 | 64 | let find_package_of_exe (item:Describe_external_lib.item) = 65 | match item.package with 66 | | Some p -> Some p 67 | | None -> 68 | (* Only allow for private executables to find the package *) 69 | item.extensions 70 | |> List.find_map (fun extension -> 71 | Option.map 72 | (fun bin_name -> 73 | Option.map 74 | (fun (item:Describe_entries.item) -> item.package) (Item_map.find_opt bin_name @@ bin_of_entries ())) 75 | (Dune_rules.Copy_rules.find_dest_name ~name:(String.cat item.name extension) @@ copy_rules ())) 76 | |> Option.join 77 | 78 | let resolve_internal_deps d_items items_pkg = 79 | (* After the d_items are filtered to the corresponding package request, 80 | * we need to include the internal_deps in order to reach all the deps. 81 | * If the internal dep is a public library we skip the recursive resolve 82 | * because it will be resolve with separate request *) 83 | let open Describe_external_lib in 84 | let get_name = function 85 | | Lib item -> String.cat item.name ".lib" 86 | | Exe item -> String.cat item.name ".exe" 87 | | Test item -> String.cat item.name ".test" 88 | in 89 | let d_items_lib = 90 | d_items 91 | |> List.filter_map (fun d_item -> 92 | if is_lib_item d_item then 93 | let item = get_item d_item in 94 | Some (item.Describe_external_lib.name ^ ".lib", Lib item) 95 | else None) 96 | |> List.to_seq |> Hashtbl.of_seq 97 | in 98 | let rec add_internal acc = function 99 | | [] -> Hashtbl.to_seq_values acc |> List.of_seq 100 | | item::tl -> 101 | if Hashtbl.mem acc (get_name item) then 102 | add_internal acc tl 103 | else begin 104 | Hashtbl.add acc (get_name item) item; 105 | (get_item item).internal_deps 106 | |> List.filter_map (fun (name, _) -> 107 | match Hashtbl.find_opt d_items_lib (String.cat name ".lib") with 108 | | None -> None 109 | | Some d_item_lib -> 110 | if Option.is_some (get_item d_item_lib).package then None 111 | else Some d_item_lib) 112 | |> fun internals -> add_internal acc (tl @ internals) 113 | end 114 | in 115 | add_internal (Hashtbl.create 10) items_pkg 116 | 117 | let get_dune_items dir_types ~pkg ~target = 118 | let d_items = 119 | Lazy.force describe_external_lib_deps 120 | |> List.map (fun d_item -> 121 | let item = Describe_external_lib.get_item d_item in 122 | if Describe_external_lib.is_exe_item d_item && Option.is_none item.package 123 | then 124 | match find_package_of_exe item with 125 | | None -> d_item 126 | | Some pkg -> Describe_external_lib.Exe { item with package = Some pkg } 127 | else d_item) 128 | in 129 | let unresolved_entries = 130 | let exe_items = 131 | List.filter_map (function 132 | | Describe_external_lib.Exe item -> Some item 133 | | _ -> None) d_items 134 | in 135 | bin_of_entries () 136 | |> Item_map.partition (fun _ (entry:Describe_entries.item) -> 137 | exe_items 138 | |> List.exists 139 | (fun (item:Describe_external_lib.item) -> 140 | is_bin_name_of_describe_lib entry.bin_name item 141 | && Option.equal String.equal (Some entry.package) item.package)) 142 | |> snd 143 | in 144 | let d_items = 145 | d_items 146 | |> List.map (function 147 | | Describe_external_lib.Exe item as d_item -> 148 | item.extensions 149 | |> List.find_map (fun extension -> 150 | Item_map.find_opt (String.cat item.name extension) unresolved_entries) 151 | |> (function 152 | | None -> d_item 153 | | Some entry -> Describe_external_lib.Exe { item with package = Some entry.package }) 154 | | d_item -> d_item) 155 | |> List.filter (fun item -> 156 | match (item,target) with 157 | | Describe_external_lib.Test _, `Install -> false 158 | | Describe_external_lib.Test _, `Runtest -> true 159 | | _ , `Runtest -> false 160 | | _, `Install -> true) 161 | |> List.filter (fun d_item -> should_use_dir ~dir_types (Describe_external_lib.get_item d_item).source_dir) 162 | in 163 | List.filter (fun d_item -> 164 | let item = Describe_external_lib.get_item d_item in 165 | (* if an item has no package, we assume it's used for testing *) 166 | if target = `Install then 167 | Option.equal String.equal (Some pkg) item.package 168 | else 169 | Option.equal String.equal (Some pkg) item.package || Option.is_none item.package) d_items 170 | |> resolve_internal_deps d_items 171 | 172 | let lib_deps ~pkg ~target = 173 | get_dune_items (Hashtbl.create 10) ~pkg ~target 174 | |> List.fold_left (fun libs item -> 175 | let item = Describe_external_lib.get_item item in 176 | List.map (fun dep -> fst dep, item.source_dir) item.external_deps 177 | |> List.fold_left (fun acc (lib, path) -> 178 | if Astring.String.take ~sat:((<>) '.') lib <> pkg then 179 | let dirs = Libraries.find_opt lib acc |> Option.value ~default:Dir_set.empty in 180 | Libraries.add lib (Dir_set.add path dirs) acc 181 | else 182 | acc) libs) Libraries.empty 183 | 184 | let get_external_lib_deps ~pkg ~target : t = lib_deps ~pkg ~target 185 | -------------------------------------------------------------------------------- /dune: -------------------------------------------------------------------------------- 1 | (executable 2 | (public_name opam-dune-lint) 3 | (name main) 4 | (libraries astring fmt fmt.tty bos opam-format opam-state cmdliner stdune sexplib str fpath csexp)) 5 | -------------------------------------------------------------------------------- /dune-project: -------------------------------------------------------------------------------- 1 | (lang dune 3.10) 2 | 3 | (name opam-dune-lint) 4 | 5 | (formatting disabled) 6 | 7 | (generate_opam_files true) 8 | 9 | (source 10 | (github ocurrent/opam-dune-lint)) 11 | 12 | (authors "talex5@gmail.com") 13 | 14 | (maintainers "alpha@tarides.com" "Tim McGilchrist ") 15 | 16 | (license ISC) 17 | 18 | (cram enable) 19 | 20 | (package 21 | (name opam-dune-lint) 22 | (synopsis "Ensure dune and opam dependencies are consistent") 23 | (description 24 | "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).") 25 | (depends 26 | (fpath 27 | (>= 0.7.3)) 28 | (astring 29 | (>= 0.8.5)) 30 | (sexplib 31 | (>= v0.14.0)) 32 | (cmdliner 33 | (>= 1.1.0)) 34 | (stdune 35 | (>= 3.10.0)) 36 | (ocaml 37 | (>= 4.08.0)) 38 | (bos 39 | (>= 0.2.0)) 40 | (fmt 41 | (>= 0.8.9)) 42 | (opam-state 43 | (>= 2.1.0)) 44 | opam-format)) 45 | -------------------------------------------------------------------------------- /dune_constraints.ml: -------------------------------------------------------------------------------- 1 | (* This source code is comming from https://github.com/ocurrent/opam-repo-ci/blob/master/lib/lint.ml 2 | * with a slight modification *) 3 | 4 | type error = 5 | | DuneConstraintMissing 6 | | DuneIsBuild 7 | | BadDuneConstraint of string * string 8 | 9 | let is_dune name = 10 | OpamPackage.Name.equal name (OpamPackage.Name.of_string "dune") 11 | 12 | let get_dune_constraint opam = 13 | let get_max = function 14 | | None, None -> None 15 | | Some x, None -> Some x 16 | | None, Some x -> Some x 17 | | Some x, Some y when OpamVersionCompare.compare x y >= 0 -> Some x 18 | | Some _, Some y -> Some y 19 | in 20 | let get_min = function 21 | | None, None | Some _, None | None, Some _ -> None 22 | | Some x, Some y when OpamVersionCompare.compare x y >= 0 -> Some y 23 | | Some x, Some _ -> Some x 24 | in 25 | let is_build = ref false in 26 | let rec get_lower_bound = function 27 | | OpamFormula.Atom (OpamTypes.Constraint ((`Gt | `Geq), OpamTypes.FString version)) -> Some version 28 | | Atom (Filter (FIdent (_, var, _))) when String.equal (OpamVariable.to_string var) "build" -> is_build := true; None (* TODO: remove this hack *) 29 | | Empty | Atom (Filter _) | Atom (Constraint _) -> None 30 | | Block x -> get_lower_bound x 31 | | And (x, y) -> get_max (get_lower_bound x, get_lower_bound y) 32 | | Or (x, y) -> get_min (get_lower_bound x, get_lower_bound y) 33 | in 34 | let rec aux = function 35 | | OpamFormula.Atom (pkg, constr) -> 36 | if is_dune pkg then 37 | let v = get_lower_bound constr in 38 | Some (Option.value ~default:"1.0" v) 39 | else 40 | None 41 | | Empty -> None 42 | | Block x -> aux x 43 | | And (x, y) -> get_max (aux x, aux y) 44 | | Or (x, y) -> get_min (aux x, aux y) 45 | in 46 | (!is_build, aux opam.OpamFile.OPAM.depends) 47 | 48 | let check_dune_constraints ~errors ~dune_version pkg_name opam = 49 | let is_build, dune_constraint = get_dune_constraint opam in 50 | let errors = 51 | match dune_constraint with 52 | | None -> 53 | if is_dune pkg_name then 54 | errors 55 | else 56 | (pkg_name, DuneConstraintMissing) :: errors 57 | | Some dep -> 58 | if OpamVersionCompare.compare dep dune_version >= 0 then 59 | errors 60 | else 61 | (pkg_name, BadDuneConstraint (dep, dune_version)) :: errors 62 | in 63 | if is_build then (pkg_name, DuneIsBuild) :: errors else errors 64 | 65 | let print_msg_of_errors = 66 | List.iter (fun (package, err) -> 67 | let pkg = OpamPackage.Name.to_string package in 68 | match err with 69 | | DuneConstraintMissing -> 70 | Fmt.epr "Warning in %s: The package has a dune-project file but no explicit dependency on dune was found.@." pkg 71 | | DuneIsBuild -> 72 | Fmt.epr "Warning in %s: The package tagged dune as a build dependency. \ 73 | Due to a bug in dune (https://github.com/ocaml/dune/issues/2147) this should never be the case. \ 74 | Please remove the {build} tag from its filter.@." 75 | pkg 76 | | BadDuneConstraint (dep, ver) -> 77 | Fmt.epr 78 | "Error in %s: Your dune-project file indicates that this package requires at least dune %s \ 79 | but your opam file only requires dune >= %s. Please check which requirement is the \ 80 | right one, and fix the other.@." 81 | pkg ver dep 82 | 83 | ) 84 | -------------------------------------------------------------------------------- /dune_items.ml: -------------------------------------------------------------------------------- 1 | open Types 2 | 3 | module Describe_external_lib = struct 4 | module Kind = struct 5 | type t = Required | Optional 6 | 7 | let merge = function 8 | | Required,_ | _, Required -> Required 9 | | Optional,Optional -> Optional 10 | 11 | let is_required = function 12 | | Required -> true 13 | | Optional -> false 14 | end 15 | 16 | type item = 17 | { 18 | name: string; 19 | package: string option; 20 | external_deps : (string * Kind.t) list; 21 | internal_deps : (string * Kind.t) list; 22 | source_dir: Fpath.t; 23 | extensions: string list 24 | } 25 | 26 | let dump_item = 27 | { 28 | name = ""; 29 | package = None; 30 | external_deps = []; 31 | internal_deps = []; 32 | source_dir = Fpath.v "."; 33 | extensions = [] 34 | } 35 | 36 | type t = Lib of item | Exe of item | Test of item 37 | 38 | let get_item = function 39 | | Lib item | Exe item | Test item -> item 40 | 41 | let is_exe_item = function 42 | | Exe _ -> true | _ -> false 43 | 44 | let is_lib_item = function 45 | | Lib _ -> true | _ -> false 46 | 47 | let string_of_atom = function 48 | | Sexp.Atom s -> s 49 | | s -> Fmt.failwith "%a is an atom" Sexp.pp_hum s 50 | 51 | let string_of_list_dep_sexp = function 52 | | Sexp.List [Atom name; Atom kind] -> 53 | if String.equal "required" kind then 54 | (name, Kind.Required) 55 | else 56 | (name, Kind.Optional) 57 | | s -> Fmt.failwith "%s is not 'List[Atom _; Atom _]'" (Sexp.to_string s) 58 | 59 | let decode_item sexps = 60 | let items = 61 | List.fold_left (fun items -> function 62 | | Sexp.List [Atom "names"; List sexps] -> 63 | List.map (fun name -> {dump_item with name = name}) (List.map string_of_atom sexps) 64 | | _ -> items) [] sexps 65 | in 66 | List.fold_left (fun items -> function 67 | | Sexp.List [Atom "names"; List _] -> items 68 | | Sexp.List [Atom "package"; List [Atom p] ] -> 69 | List.map (fun item -> {item with package = Some p}) items 70 | | Sexp.List [Atom "package"; List [] ] -> 71 | List.map (fun item -> {item with package = None}) items 72 | | Sexp.List [Atom "source_dir"; Atom s] -> 73 | List.map (fun item -> {item with source_dir = Fpath.v s}) items 74 | | Sexp.List [Atom "extensions" ; List sexps] -> 75 | List.map (fun item -> {item with extensions = List.map string_of_atom sexps}) items 76 | | Sexp.List [Atom "external_deps" ; List sexps] -> 77 | List.map (fun item -> 78 | {item with external_deps = List.map string_of_list_dep_sexp sexps}) items 79 | | Sexp.List [Atom "internal_deps" ; List sexps] -> 80 | List.map (fun item -> 81 | {item with internal_deps = List.map string_of_list_dep_sexp sexps}) items 82 | | s -> Fmt.failwith "%s is not a good format decoding an item" (Sexp.to_string s) 83 | ) items sexps 84 | 85 | let decode_items sexps : t list = 86 | sexps 87 | |> List.concat_map (function 88 | | Sexp.List [Atom "library"; List sexps] -> decode_item sexps |> List.map (fun item -> Lib item) 89 | | Sexp.List [Atom "tests"; List sexps] -> decode_item sexps |> List.map (fun item -> Test item) 90 | | Sexp.List [Atom "executables"; List sexps] -> decode_item sexps |> List.map (fun item -> Exe item) 91 | | s -> Fmt.failwith "%s is not a good format decoding items" (Sexp.to_string s)) 92 | 93 | let describe_extern_of_sexp : Sexp.t -> t list = function 94 | | Sexp.List [Atom _ctx; List sexps] -> decode_items sexps 95 | | _ -> Fmt.failwith "Invalid format" 96 | 97 | end 98 | 99 | module Describe_entries = struct 100 | 101 | type item = { 102 | source_dir: Fpath.t; 103 | bin_name: string; 104 | kind: string; 105 | dst: string; 106 | section: string; 107 | optional: string; 108 | package: string 109 | } 110 | 111 | let dump_item = { 112 | source_dir = Fpath.v "."; 113 | bin_name = ""; 114 | kind = ""; 115 | dst = ""; 116 | section = ""; 117 | optional = ""; 118 | package = "" 119 | } 120 | 121 | type entry = Bin of item | Other of item 122 | 123 | type t = string * entry list 124 | 125 | let string_of_atom = function 126 | | Sexp.Atom s -> s 127 | | s -> Fmt.failwith "%s is an atom" (Sexp.to_string s) 128 | 129 | (* With "default/lib/bin.exe" or "default/lib/bin.bc.js", it gives "bin.exe" or "bin.bc.js" *) 130 | let bin_name = Filename.basename 131 | 132 | (* With "default/lib/bin.exe", it gives "default/lib" *) 133 | let source_dir = Fpath.parent 134 | 135 | let decode_item sexps = 136 | List.fold_left (fun item -> function 137 | | Sexp.List [Atom "src"; List [_; Atom p] ] -> 138 | {item with source_dir = source_dir (Fpath.v p); bin_name = bin_name p} 139 | | Sexp.List [Atom "kind"; Atom p ] -> {item with kind = p} 140 | | Sexp.List [Atom "dst"; Atom p ] -> {item with dst = p} 141 | | Sexp.List [Atom "section"; Atom p ] -> {item with section = p} 142 | | Sexp.List [Atom "optional"; Atom p ] -> {item with optional = p} 143 | | s -> Fmt.failwith "%s is not a good format decoding an item" (Sexp.to_string s) 144 | ) dump_item sexps 145 | |> (fun item -> match item.section with "BIN" -> Bin item | _ -> Other item) 146 | 147 | let decode_items : Sexp.t list -> entry list = 148 | List.filter_map (function 149 | | Sexp.List [List [Atom "source"; List [Atom "User" ; _ ]]; List [Atom "entry"; List sexps]] 150 | | Sexp.List [List [Atom "entry"; List sexps]; List [Atom "source"; Atom "User"]] -> Some (decode_item sexps) 151 | | Sexp.List [List [Atom "source"; Atom "Dune"]; List _ ] -> None 152 | | s -> Fmt.failwith "%s is not a good format decoding items" (Sexp.to_string s)) 153 | 154 | let decode_entries : Sexp.t -> t = function 155 | | Sexp.List [Atom package; List sexps] -> (package,decode_items sexps) 156 | | _ -> Fmt.failwith "Invalid format" 157 | 158 | let entries_of_sexp : Sexp.t -> t list = function 159 | | Sexp.List sexps -> 160 | sexps 161 | |> List.map (fun x -> 162 | let package, entries = decode_entries x in 163 | (package, List.map (function 164 | | Bin item -> Bin {item with package = package} 165 | | Other item -> Other {item with package = package}) entries)) 166 | | _ -> Fmt.failwith "Invalid format" 167 | 168 | let items_bin_of_entries describe_entries = 169 | List.concat_map snd describe_entries 170 | |> List.filter_map (function Bin item -> Some (item.bin_name,item) | Other _ -> None) 171 | |> List.to_seq |> Item_map.of_seq 172 | end 173 | 174 | module Describe_opam_files = struct 175 | 176 | (** String representing an opam file name eg. foo.opam *) 177 | type path = string 178 | 179 | (** String representing the content of an opam file *) 180 | type content = string 181 | 182 | (** Representing a list of name and content of an opam file *) 183 | type t = (path * content) list 184 | 185 | (** Decode opam files from the command "dune describe opam-files --format csexp" output. *) 186 | let opam_files_of_csexp = function 187 | | Csexp.List sexps -> 188 | sexps 189 | |> List.map (function 190 | | Csexp.List [Atom path; Atom content] -> 191 | (path, content) 192 | | s -> Fmt.failwith "\"%s\" is not a good format decoding an item" (Csexp.to_string s)) 193 | | s -> Fmt.failwith "\"%s\" is not a good format decoding items" (Csexp.to_string s) 194 | 195 | let opamfile_of_content content = OpamFile.OPAM.read_from_string content 196 | 197 | end 198 | -------------------------------------------------------------------------------- /dune_project.ml: -------------------------------------------------------------------------------- 1 | open Types 2 | 3 | module Deps = Deps 4 | 5 | type t = Sexp.t list 6 | 7 | let atom s = Sexp.Atom s 8 | let dune_and x y = Sexp.(List [atom "and"; x; y]) 9 | let lower_bound v = Sexp.(List [atom ">="; atom (OpamPackage.Version.to_string v)]) 10 | 11 | let with_open_out fn file = 12 | let out_file = open_out file in 13 | let r = fn out_file in 14 | (flush out_file; close_out out_file; r) 15 | 16 | (* Dune description stanza admit some quote starting like "\>" or "|>" which fails 17 | when using Sexplib to parse dune-project file.*) 18 | let remove_quoted_string dune_project_s = 19 | let is_quote s = 20 | if String.length s > 2 then 21 | let quote = String.sub s 0 3 in 22 | String.equal quote "\"\\>" || String.equal quote "\"\\|" 23 | else false 24 | in 25 | let first_quote = ref false in 26 | dune_project_s 27 | (* |> Astring.String.cuts ~sep:"\n" *) 28 | |> List.filter_map (fun s -> 29 | let is_quote = is_quote @@ String.trim s in 30 | if is_quote && not (!first_quote) then 31 | (first_quote := true; Some "\"\"") 32 | else if is_quote then None 33 | else Some s) 34 | |> String.concat "\n" 35 | 36 | let parse () = 37 | Stdune.Path.Build.(set_build_dir (Stdune.Path.Outside_build_dir.of_string (Sys.getcwd ()))); 38 | Fpath.of_string "dune-project" 39 | |> Stdlib.Result.get_ok 40 | |> Bos.OS.File.read_lines 41 | |> Stdlib.Result.get_ok 42 | |> remove_quoted_string 43 | |> fun s -> String.concat " " ["(__dune_project__\n";s;"\n)"] 44 | |> Sexp.of_string |> function 45 | | Sexp.List ((Atom "__dune_project__")::sexps) -> sexps 46 | | _ -> Fmt.failwith "Fails to parse 'dune-project' file" 47 | 48 | let generate_opam_enabled = 49 | List.exists (function 50 | | Sexp.List [Sexp.Atom "generate_opam_files"; Atom v] -> bool_of_string v 51 | | Sexp.List [Sexp.Atom "generate_opam_files"] -> true 52 | | _ -> false 53 | ) 54 | 55 | (* ("foo" args) -> ("foo" (f args)) *) 56 | let map_if name f = function 57 | | Sexp.List (Atom head as x :: xs) when head = name -> 58 | Sexp.List (x :: f xs) 59 | | x -> x 60 | 61 | (* (... ("foo" args) ...) -> (... ("foo" (f args)) ...) 62 | (...) -> (... ("foo" (f [] )) ) 63 | *) 64 | let rec update_or_create name f = function 65 | | Sexp.List (Atom head as x :: xs) :: rest when head = name -> 66 | Sexp.List (x :: f xs) :: rest 67 | | [] -> 68 | Sexp.List (atom name :: f []) :: [] 69 | | head :: rest -> head :: update_or_create name f rest 70 | 71 | (* [package_name xs] returns the value of the (name foo) item in [xs]. *) 72 | let package_name = 73 | List.find_map (function 74 | | Sexp.List [Atom "name"; Atom name] -> Some name 75 | | _ -> None 76 | ) 77 | 78 | let rec simplify_and = function 79 | | Sexp.List [Atom "and"; x] -> x 80 | | Sexp.List xs -> List (List.map simplify_and xs) 81 | | x -> x 82 | 83 | (* (foo) -> foo 84 | (foo (and x)) -> (foo x) 85 | *) 86 | let simplify = function 87 | | Sexp.List [Atom _ as x] -> x 88 | | Sexp.List xs -> List (List.map simplify_and xs) 89 | | x -> x 90 | 91 | let rec remove_with_test = function 92 | | [] -> [] 93 | | Sexp.Atom ":with-test" :: xs -> xs 94 | | List x :: xs -> List (remove_with_test x) :: remove_with_test xs 95 | | x :: xs -> x :: remove_with_test xs 96 | 97 | let apply_change items = function 98 | | `Add_build_dep dep -> 99 | let item = Sexp.(List [atom (OpamPackage.name_to_string dep); 100 | lower_bound (OpamPackage.version dep)]) in 101 | item :: items 102 | | `Add_test_dep dep -> 103 | let item = Sexp.(List [atom (OpamPackage.name_to_string dep); 104 | dune_and 105 | (lower_bound (OpamPackage.version dep)) 106 | (atom ":with-test") 107 | ]) 108 | in 109 | item :: items 110 | | `Remove_with_test name -> 111 | List.map (map_if (OpamPackage.Name.to_string name) remove_with_test) items 112 | |> List.map simplify 113 | | `Add_with_test name -> 114 | let name = OpamPackage.Name.to_string name in 115 | items |> List.map (function 116 | | Sexp.List [Atom name2 as a; expr] when name = name2 -> 117 | Sexp.List [a; dune_and (atom ":with-test") expr] 118 | | Atom name2 as a when name = name2 -> 119 | Sexp.List [a; atom ":with-test"] 120 | | x -> x 121 | ) 122 | 123 | let apply_changes ~changes items = 124 | List.fold_left apply_change items changes 125 | 126 | let update (changes:(_ * Change.t list) Paths.t) (t:t) = 127 | let update_package items = 128 | match package_name items with 129 | | None -> failwith "Missing 'name' in (package)!" 130 | | Some name -> 131 | match Paths.find_opt (String.cat name ".opam") changes with 132 | | None -> items 133 | | Some (_opam, changes) -> update_or_create "depends" (apply_changes ~changes) items 134 | in 135 | List.map (map_if "package" update_package) t 136 | 137 | let packages t = 138 | List.filter_map (function 139 | | Sexp.List ((Atom "package")::sexps) -> 140 | Option.some @@ List.filter_map (function 141 | | Sexp.List [Atom "name"; Atom name] -> Some (name ^ ".opam") 142 | | _ -> None) sexps 143 | | _ -> None) t 144 | |> List.flatten 145 | |> fun v -> List.combine v v 146 | |> List.to_seq 147 | |> Libraries.of_seq 148 | 149 | let version t = 150 | List.find_map (function 151 | | Sexp.List [Atom "lang"; Atom "dune"; Atom version] -> Some version 152 | | _ -> None) t 153 | |> function 154 | | None -> Fmt.failwith "dune-project file without `(lang dune _)` stanza" 155 | | Some version -> version 156 | 157 | let dune_format dune = 158 | Bos.OS.Cmd.(in_string dune |> run_io Bos.Cmd.(v "dune" % "format-dune-file") |> out_string ~trim:false) 159 | |> Bos.OS.Cmd.success 160 | |> or_die 161 | 162 | let write_project_file t = 163 | with_open_out (fun ch -> 164 | let f = Format.formatter_of_out_channel ch in 165 | Fmt.str "%a" (Fmt.list ~sep:Fmt.cut Sexp.pp) t |> dune_format |> Fmt.pf f "%s"; 166 | ) "dune-project"; 167 | Fmt.pr "Wrote %S@." "dune-project" 168 | -------------------------------------------------------------------------------- /dune_project.mli: -------------------------------------------------------------------------------- 1 | open Types 2 | 3 | type t 4 | 5 | val parse : unit -> t 6 | (** [parse ()] loads the "dune-project" file. *) 7 | 8 | val generate_opam_enabled : t -> bool 9 | (** Check whether (generate_opam_files true) is present. *) 10 | 11 | val update : (_ * Change.t list) Paths.t -> t -> t 12 | 13 | val write_project_file : t -> unit 14 | 15 | val packages : t -> string Paths.t 16 | 17 | val version : t -> string 18 | 19 | module Deps : sig 20 | type t = Dir_set.t Libraries.t 21 | (** The set of OCamlfind libraries needed, each with the directories needing it. *) 22 | 23 | val get_external_lib_deps : pkg:string -> target:[`Install | `Runtest] -> t 24 | end 25 | -------------------------------------------------------------------------------- /dune_rules.ml: -------------------------------------------------------------------------------- 1 | open Types 2 | 3 | module Copy_rules = struct 4 | 5 | let sexp_of_file file = 6 | try Sexp.load_sexps @@ Fpath.to_string file with 7 | | Sexp.Parse_error _ as e -> 8 | (Fmt.pr "Error parsing 'dune file' output:\n"; raise e) 9 | 10 | type t = 11 | { 12 | target: string; 13 | from_name: string; 14 | to_name: string; 15 | dep: string; 16 | package: string 17 | } 18 | 19 | let dump_copy = { 20 | target = ""; 21 | from_name = ""; 22 | to_name = ""; 23 | dep = ""; 24 | package = "" 25 | } 26 | 27 | let rules = Hashtbl.create 10 28 | 29 | let copy_rules_of_sexp sexps = 30 | let is_action_copy sexp = 31 | sexp 32 | |> (function 33 | | Sexp.List l -> if List.mem (Sexp.Atom "rule") l then l else [] 34 | | _ -> []) 35 | |> List.exists (function 36 | | Sexp.List [ Atom "action"; List [ Atom "copy"; _; _]] -> true 37 | | _ -> false) 38 | in 39 | let copy_rule_of_sexp sexp = 40 | match sexp with 41 | | Sexp.List sexps -> 42 | List.fold_left (fun copy _sexp -> 43 | match _sexp with 44 | | Sexp.List [Atom "action"; List [ _; Atom f; Atom t]] -> 45 | {{copy with from_name = f } with to_name = t} 46 | | Sexp.List [Atom "deps"; List [Atom "package"; Atom s]]-> {copy with package = s} 47 | | Sexp.List [Atom "deps"; List [Atom "package"; Atom p]; Atom d] 48 | | Sexp.List [Atom "deps"; Atom d; List [Atom "package"; Atom p]] -> 49 | {{copy with package = p} with dep = d} 50 | | Sexp.List [Atom "deps"; Atom s] -> {copy with dep = s} 51 | | Sexp.List [Atom "target"; Atom s] -> {copy with target = s} 52 | | Sexp.Atom "rule" -> copy 53 | | _ -> copy 54 | ) dump_copy sexps 55 | | s -> Fmt.failwith "%s is not a rule" (Sexp.to_string s) 56 | in 57 | sexps 58 | |> List.filter_map (fun rule -> 59 | if not (is_action_copy rule) then 60 | None 61 | else 62 | rule 63 | |> copy_rule_of_sexp 64 | |> fun copy -> 65 | if String.equal copy.to_name "%{target}" && String.equal copy.from_name "%{deps}" then 66 | (*when we got `(action (copy %{deps} %{target}))` *) 67 | Some {{copy with to_name = copy.target} with from_name = copy.dep} 68 | else Some copy) 69 | 70 | let copy_rules_map = 71 | List.fold_left (fun map copy -> Item_map.add copy.from_name copy map) Item_map.empty 72 | 73 | let get_copy_rules file = 74 | match Hashtbl.find_opt rules file with 75 | | None when OS.Path.exists file |> Stdlib.Result.get_ok -> 76 | let copy_rules = copy_rules_of_sexp (sexp_of_file file) in 77 | Hashtbl.add rules file copy_rules; copy_rules 78 | | None -> Hashtbl.add rules file []; [] 79 | | Some copy_rules -> copy_rules 80 | 81 | let find_dest_name ~name rules = 82 | let rec find_dest_name name rules = 83 | match Item_map.find_opt name rules with 84 | | None -> Some name 85 | | Some t -> find_dest_name t.to_name rules 86 | in 87 | match Item_map.find_opt name rules with 88 | | None -> None (* Not found in the first step *) 89 | | Some t -> find_dest_name t.to_name rules 90 | end 91 | -------------------------------------------------------------------------------- /formula.ml: -------------------------------------------------------------------------------- 1 | open OpamTypes 2 | 3 | let with_test = OpamVariable.of_string "with-test" 4 | 5 | (* Before: "foo" {with-test} 6 | After: "foo" 7 | *) 8 | let rec remove_with_test : filter -> filter = function 9 | | FIdent ([], var, None) when OpamVariable.to_string var = "with-test" -> FBool true 10 | | FBool _ | FString _ | FIdent _ | FDefined _ | FUndef _ | FNot _ | FOr _ | FOp _ as x -> x 11 | | FAnd (x, y) -> FAnd (remove_with_test x, remove_with_test y) 12 | 13 | let formula_of_filter = function 14 | | FBool true -> Empty 15 | | expr -> Atom (Filter expr) 16 | 17 | let map_filter f = OpamFormula.map (function 18 | | Constraint x -> Atom (Constraint x) 19 | | Filter x -> formula_of_filter (f x) 20 | ) 21 | 22 | let apply_with_test_change (formula : filter filter_or_constraint OpamFormula.formula) = function 23 | | `Remove_with_test _name -> map_filter remove_with_test formula 24 | | `Add_with_test _name -> 25 | OpamFormula.ands [ 26 | formula; 27 | formula_of_filter (FIdent ([], with_test, None)) 28 | ] 29 | 30 | let update_depends (depends : filtered_formula) = function 31 | | `Add_build_dep dep -> 32 | let expr = OpamFormula.Atom (Constraint (`Geq, FString (OpamPackage.version_to_string dep))) in 33 | OpamFormula.And (depends, OpamFormula.Atom (OpamPackage.name dep, expr)) 34 | | `Add_test_dep dep -> 35 | let expr = OpamFormula.ands [ 36 | OpamFormula.Atom (Constraint (`Geq, FString (OpamPackage.version_to_string dep))); 37 | OpamFormula.Atom (Filter (FIdent ([], with_test, None))); 38 | ] 39 | in 40 | OpamFormula.ands [depends; OpamFormula.Atom (OpamPackage.name dep, expr)] 41 | | `Remove_with_test name | `Add_with_test name as change -> 42 | let update (name2, formula) = 43 | if name <> name2 then OpamFormula.Atom (name2, formula) 44 | else OpamFormula.Atom (name, apply_with_test_change formula change) 45 | in 46 | OpamFormula.map update depends 47 | 48 | let rec flatten : _ OpamFormula.formula -> _ list = function 49 | | Empty -> [] 50 | | Atom (name, f) -> [(OpamPackage.Name.to_string name, f)] 51 | | Block x -> flatten x 52 | | And (x, y) -> flatten x @ flatten y 53 | | Or (x, y) -> flatten x @ flatten y 54 | 55 | (* with-test dependencies are not available in the plain build environment. *) 56 | let build_env x = 57 | match OpamVariable.Full.to_string x with 58 | | "with-test" -> Some (OpamTypes.B false) 59 | | _ -> None 60 | 61 | let available_in_build_env = 62 | let open OpamTypes in function 63 | | Filter f -> OpamFilter.eval_to_bool ~default:true build_env f 64 | | Constraint _ -> true 65 | 66 | let classify deps : [`Build | `Test] OpamPackage.Name.Map.t = 67 | flatten deps 68 | |> List.fold_left (fun acc (name, formula) -> 69 | let ty = if OpamFormula.eval available_in_build_env formula then `Build else `Test in 70 | let update x = match x, ty with 71 | | `Build, `Build | `Test, `Test -> x 72 | | `Test, `Build | `Build, `Test -> `Build 73 | in 74 | OpamPackage.Name.Map.update (OpamPackage.Name.of_string name) update ty acc 75 | ) OpamPackage.Name.Map.empty 76 | -------------------------------------------------------------------------------- /formula.mli: -------------------------------------------------------------------------------- 1 | open OpamTypes 2 | 3 | val classify : filtered_formula -> [`Build | `Test] OpamPackage.Name.Map.t 4 | 5 | val update_depends : 6 | filtered_formula -> 7 | [< `Add_build_dep of package 8 | | `Add_test_dep of package 9 | | `Add_with_test of name 10 | | `Remove_with_test of name ] -> 11 | filtered_formula 12 | -------------------------------------------------------------------------------- /index.ml: -------------------------------------------------------------------------------- 1 | module Owner = Map.Make(String) 2 | 3 | type t = OpamPackage.t Owner.t 4 | 5 | (* Update the index to record that [changes] came from [pkg]. *) 6 | let update_index t changes ~pkg = 7 | OpamStd.String.Map.fold (fun file op acc -> 8 | match op with 9 | | OpamDirTrack.Added _ -> 10 | begin match String.split_on_char '/' file with 11 | | ["lib"; lib; "META"] -> Owner.add lib pkg acc 12 | | _ -> acc 13 | end 14 | | _ -> acc 15 | ) changes t 16 | 17 | let create () = 18 | let root = OpamStateConfig.opamroot () in 19 | ignore (OpamStateConfig.load_defaults ~lock_kind:`Lock_read root); 20 | OpamGlobalState.with_ `Lock_none @@ fun gt -> 21 | let switch = OpamStateConfig.get_switch () in 22 | let installed = (OpamSwitchState.load_selections ~lock_kind:`Lock_read gt switch).sel_installed in 23 | OpamPackage.Set.fold (fun pkg acc -> 24 | let changes = OpamPath.Switch.changes gt.root switch (OpamPackage.name pkg) in 25 | match OpamFile.Changes.read_opt changes with 26 | | None -> Fmt.pr "WARNING: no .changes found for %S!@." (OpamPackage.to_string pkg); acc 27 | | Some changes -> update_index acc changes ~pkg 28 | ) installed Owner.empty 29 | -------------------------------------------------------------------------------- /main.ml: -------------------------------------------------------------------------------- 1 | open Types 2 | 3 | type check = Added | Deleted | Changed 4 | 5 | let string_of_check = function 6 | | Added -> "added" 7 | | Deleted -> "deleted" 8 | | Changed -> "changed" 9 | 10 | let or_die = function 11 | | Ok x -> x 12 | | Error (`Msg m) -> failwith m 13 | 14 | let dune_describe_opam_files = Bos.Cmd.(v "dune" % "describe" % "opam-files" % "--format" % "csexp") 15 | 16 | let () = 17 | (* When run as a plugin, opam helpfully scrubs the environment. 18 | Get the settings back again. *) 19 | let env = 20 | Bos.Cmd.(v "opam" % "config" % "env" % "--sexp") 21 | |> Bos.OS.Cmd.run_out 22 | |> Bos.OS.Cmd.to_string 23 | |> or_die 24 | |> Sexplib.Sexp.of_string 25 | in 26 | match env with 27 | | Sexplib.Sexp.List vars -> 28 | vars |> List.iter (function 29 | | Sexplib.Sexp.List [Atom name; Atom value] -> Unix.putenv name value 30 | | x -> Fmt.epr "WARNING: bad sexp from opam config env: %a@." Sexplib.Sexp.pp_hum x 31 | ) 32 | | x -> Fmt.epr "WARNING: bad sexp from opam config env: %a@." Sexplib.Sexp.pp_hum x 33 | 34 | let get_libraries ~pkg ~target = Dune_project.Deps.get_external_lib_deps ~pkg ~target 35 | 36 | let to_opam ~index lib = 37 | match Astring.String.take ~sat:((<>) '.') lib with 38 | | "threads" | "unix" | "str" | "compiler-libs" 39 | | "bigarray" | "dynlink" | "ocamldoc" | "stdlib" 40 | | "bytes" | "runtime_events" -> None (* Distributed with OCaml *) 41 | | lib -> 42 | match Index.Owner.find_opt lib index with 43 | | Some pkg -> Some pkg 44 | | None -> 45 | Fmt.pr "WARNING: can't find opam package providing %S!@." lib; 46 | Some (OpamPackage.create (OpamPackage.Name.of_string lib) (OpamPackage.Version.of_string "0")) 47 | 48 | (* Convert a map of (ocamlfind-library -> hints) to a map of (opam-package -> hints). *) 49 | let to_opam_set ~index libs = 50 | Libraries.fold (fun lib dirs acc -> 51 | match to_opam ~index lib with 52 | | Some pkg -> OpamPackage.Map.update pkg (Dir_set.union dirs) Dir_set.empty acc 53 | | None -> acc 54 | ) libs OpamPackage.Map.empty 55 | 56 | let get_opam_files () = 57 | Sys.readdir "." 58 | |> Array.to_list 59 | |> List.filter (fun name -> Filename.check_suffix name ".opam") 60 | |> List.fold_left (fun acc path -> 61 | let opam = OpamFile.OPAM.read (OpamFile.make (OpamFilename.raw path)) in 62 | Paths.add path opam acc 63 | ) Paths.empty 64 | 65 | let updated_opam_files_content () = 66 | csexp dune_describe_opam_files 67 | |> List.map Dune_items.Describe_opam_files.opam_files_of_csexp |> List.flatten 68 | |> List.fold_left (fun acc (path,opam) -> Paths.add path opam acc) Paths.empty 69 | 70 | let check_identical _path a b = 71 | match a, b with 72 | | Some a, Some b -> 73 | if OpamFile.OPAM.effectively_equal a b then None 74 | else Some Changed 75 | | Some _, None -> Some Deleted 76 | | None, Some _ -> Some Added 77 | | None, None -> assert false 78 | 79 | let pp_problems f = function 80 | | [] -> Fmt.pf f " %a" Fmt.(styled `Green string) "OK" 81 | | problems -> Fmt.pf f " changes needed:@,%a" Fmt.(list ~sep:cut Change_with_hint.pp) problems 82 | 83 | let display path (_opam, problems) = 84 | let pkg = Filename.chop_suffix path ".opam" in 85 | Fmt.pr "@[%a.opam:%a@]@." 86 | Fmt.(styled `Bold string) pkg 87 | pp_problems problems 88 | 89 | let generate_report ~index ~opam pkg = 90 | let build = get_libraries ~pkg ~target:`Install |> to_opam_set ~index in 91 | let test = get_libraries ~pkg ~target:`Runtest |> to_opam_set ~index in 92 | let opam_deps = 93 | OpamFormula.And (OpamFile.OPAM.depends opam, OpamFile.OPAM.depopts opam) 94 | |> Formula.classify in 95 | let build_problems = 96 | OpamPackage.Map.to_seq build 97 | |> List.of_seq 98 | |> List.concat_map (fun (dep, hint) -> 99 | let dep_name = OpamPackage.name dep in 100 | match OpamPackage.Name.Map.find_opt dep_name opam_deps with 101 | | Some `Build -> [] 102 | | Some `Test -> [`Remove_with_test dep_name, hint] 103 | | None -> [`Add_build_dep dep, hint] 104 | ) 105 | in 106 | let test_problems = 107 | test 108 | |> OpamPackage.Map.to_seq 109 | |> List.of_seq 110 | |> List.concat_map (fun (dep, hint) -> 111 | if OpamPackage.Map.mem dep build then [] 112 | else ( 113 | let dep_name = OpamPackage.name dep in 114 | match OpamPackage.Name.Map.find_opt dep_name opam_deps with 115 | | Some `Test -> [] 116 | | Some `Build -> [`Add_with_test dep_name, hint] 117 | | None -> [`Add_test_dep dep, hint] 118 | ) 119 | ) 120 | in 121 | build_problems @ test_problems 122 | 123 | let update_opam_file path = function 124 | | (_, []) -> () 125 | | (opam, changes) -> 126 | let depends = List.fold_left Formula.update_depends opam.OpamFile.OPAM.depends changes in 127 | let opam = OpamFile.OPAM.with_depends depends opam in 128 | let path = OpamFile.make (OpamFilename.raw (path)) in 129 | OpamFile.OPAM.write_with_preserved_format path opam; 130 | Fmt.pr "Wrote %S@." (OpamFile.to_string path) 131 | 132 | let confirm_with_user () = 133 | if Unix.(isatty stdin) then ( 134 | prerr_string "Write changes? [y] "; 135 | flush stderr; 136 | match input_line stdin |> String.lowercase_ascii with 137 | | "" | "y" | "yes" -> true 138 | | _ -> 139 | Fmt.pr "Aborted.@."; 140 | false 141 | ) else ( 142 | Fmt.pr "Run with -f to apply changes in non-interactive mode.@."; 143 | false 144 | ) 145 | 146 | let write_file path content = 147 | let chan = open_out path in 148 | output_string chan content; 149 | flush chan; 150 | close_out chan 151 | 152 | let main force dir = 153 | Sys.chdir dir; 154 | let index = Index.create () in 155 | let project = Dune_project.parse () in 156 | let old_opam_files = get_opam_files () in 157 | let packages = Dune_project.packages project in 158 | 159 | (* some dune project file has no package description 160 | * and avoid removing all the opam files *) 161 | if not (Paths.is_empty packages) then ( 162 | old_opam_files |> Paths.iter (fun path _ -> if not (Paths.mem path packages) then Sys.remove path) 163 | (* prevent `dune describe opam-files` to fail when there is a opam file `*.opam` 164 | * that its package description is missing in dune-project file. 165 | * The error from dune will be: 166 | * Error: This opam file doesn't have a corresponding (package ...) stanza in 167 | * the dune-project file. Since you have at least one other (package ...) stanza 168 | * in your dune-project file, you must a (package ...) stanza for each opam 169 | * package in your project. *) 170 | ); 171 | 172 | let opam_files_content = updated_opam_files_content () in 173 | let opam_files = 174 | opam_files_content 175 | |> Paths.mapi (fun path content -> 176 | let opamfile = Dune_items.Describe_opam_files.opamfile_of_content content in 177 | match Paths.find_opt path old_opam_files with 178 | | None -> opamfile 179 | | Some opam -> 180 | let depends = OpamFile.OPAM.depends opam in 181 | OpamFile.OPAM.with_depends depends opamfile) 182 | in 183 | if Paths.is_empty opam_files then failwith "No *.opam files found!"; 184 | let stale_files = Paths.merge check_identical old_opam_files opam_files in 185 | stale_files |> Paths.iter (fun path msg -> 186 | (match msg with 187 | | Added -> write_file path (Paths.find path opam_files_content) 188 | | Deleted -> () (* Already removed*) 189 | | Changed -> 190 | OpamFile.OPAM.write_with_preserved_format (OpamFile.make (OpamFilename.raw (path))) (Paths.find path opam_files) 191 | ); 192 | Fmt.pr "%s: %s after its upgrade from 'dune describe opam-files'!@." path (string_of_check msg) 193 | ); 194 | opam_files |> Paths.mapi (fun path opam -> 195 | (opam, generate_report ~index ~opam (Filename.chop_suffix path ".opam")) 196 | ) 197 | |> fun report -> 198 | Paths.iter display report; 199 | if Paths.exists (fun _ (_, changes) -> List.exists Change_with_hint.includes_version changes) report then 200 | Fmt.pr "Note: version numbers are just suggestions based on the currently installed version.@."; 201 | let report = Paths.map (fun (opam, changes) -> opam, List.map Change_with_hint.remove_hint changes) report in 202 | let have_changes = Paths.exists (fun _ -> function (_, []) -> false | _ -> true) report in 203 | if have_changes then ( 204 | if force || confirm_with_user () then ( 205 | if Dune_project.generate_opam_enabled project then ( 206 | project 207 | |> Dune_project.update report 208 | |> Dune_project.write_project_file; 209 | updated_opam_files_content () |> Paths.iter (fun path content -> write_file path content); 210 | ) else ( 211 | Paths.iter update_opam_file report 212 | ) 213 | ) else ( 214 | exit 1 215 | ) 216 | ); 217 | let dune_version = Dune_project.version project in 218 | get_opam_files () 219 | |> Paths.to_seq 220 | |> List.of_seq 221 | |> List.concat_map (fun (path, opam) -> 222 | let pkg_name = (OpamPackage.Name.of_string (Filename.chop_suffix path ".opam")) in 223 | Dune_constraints.check_dune_constraints ~errors:[] ~dune_version pkg_name opam) 224 | |> (fun errors -> 225 | Dune_constraints.print_msg_of_errors errors; 226 | List.find_opt (function (_, Dune_constraints.BadDuneConstraint _) -> true | _ -> false) errors 227 | |> function None -> () | Some _ -> exit 1 228 | ); 229 | if not (Paths.is_empty stale_files) then exit 1 230 | 231 | open Cmdliner 232 | 233 | let dir = 234 | Arg.value @@ 235 | Arg.pos 0 Arg.dir "." @@ 236 | Arg.info 237 | ~doc:"Root of dune project to check" 238 | ~docv:"DIR" 239 | [] 240 | 241 | let force = 242 | Arg.value @@ 243 | Arg.flag @@ 244 | Arg.info 245 | ~doc:"Update files without confirmation" 246 | ["f"; "force"] 247 | 248 | let cmd = 249 | let doc = "keep dune and opam files in sync" in 250 | let info = Cmd.info "opam-dune-lint" ~doc in 251 | let term = Term.(const main $ force $ dir) in 252 | Cmd.v info term 253 | 254 | let () = 255 | Fmt_tty.setup_std_outputs (); 256 | exit @@ Cmd.eval cmd 257 | -------------------------------------------------------------------------------- /main.mli: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocurrent/opam-dune-lint/04aed0bd0e3e766f8e45c4af55a2051afd0b3b36/main.mli -------------------------------------------------------------------------------- /opam-dune-lint.opam: -------------------------------------------------------------------------------- 1 | # This file is generated by dune, edit dune-project instead 2 | opam-version: "2.0" 3 | synopsis: "Ensure dune and opam dependencies are consistent" 4 | description: 5 | "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)." 6 | maintainer: ["alpha@tarides.com" "Tim McGilchrist "] 7 | authors: ["talex5@gmail.com"] 8 | license: "ISC" 9 | homepage: "https://github.com/ocurrent/opam-dune-lint" 10 | bug-reports: "https://github.com/ocurrent/opam-dune-lint/issues" 11 | depends: [ 12 | "dune" {>= "3.10"} 13 | "fpath" {>= "0.7.3"} 14 | "astring" {>= "0.8.5"} 15 | "sexplib" {>= "v0.14.0"} 16 | "cmdliner" {>= "1.1.0"} 17 | "stdune" {>= "3.10.0"} 18 | "ocaml" {>= "4.08.0"} 19 | "bos" {>= "0.2.0"} 20 | "fmt" {>= "0.8.9"} 21 | "opam-state" {>= "2.1.0"} 22 | "opam-format" 23 | "odoc" {with-doc} 24 | ] 25 | build: [ 26 | ["dune" "subst"] {dev} 27 | [ 28 | "dune" 29 | "build" 30 | "-p" 31 | name 32 | "-j" 33 | jobs 34 | "@install" 35 | "@runtest" {with-test} 36 | "@doc" {with-doc} 37 | ] 38 | ] 39 | dev-repo: "git+https://github.com/ocurrent/opam-dune-lint.git" 40 | flags: plugin 41 | -------------------------------------------------------------------------------- /opam-dune-lint.opam.template: -------------------------------------------------------------------------------- 1 | flags: plugin 2 | -------------------------------------------------------------------------------- /tests/dune: -------------------------------------------------------------------------------- 1 | (cram 2 | (deps %{bin:opam-dune-lint}) 3 | (enabled_if 4 | (<> %{architecture} "i386"))) 5 | -------------------------------------------------------------------------------- /tests/test_dune.t: -------------------------------------------------------------------------------- 1 | Create a simple dune project: 2 | 3 | $ cat > dune-project << EOF 4 | > (lang dune 2.7) 5 | > (generate_opam_files true) 6 | > (package 7 | > (name test) 8 | > (synopsis "Test package") 9 | > (depends 10 | > (ocamlfind (>= 1.0)) 11 | > libfoo)) 12 | > EOF 13 | 14 | $ cat > dune << EOF 15 | > (executable 16 | > (name main) 17 | > (public_name main) 18 | > (modules main) 19 | > (libraries findlib fmt)) 20 | > (test 21 | > (name test) 22 | > (modules test) 23 | > (libraries bos opam-state)) 24 | > EOF 25 | 26 | $ touch main.ml test.ml 27 | $ dune build 28 | 29 | $ export OPAM_DUNE_LINT_TESTS=y 30 | 31 | Check that the missing libraries are detected: 32 | 33 | $ opam-dune-lint = "1.0"} [from /] 36 | "bos" {with-test & >= "1.0"} [from /] 37 | "opam-state" {with-test & >= "1.0"} [from /] 38 | Note: version numbers are just suggestions based on the currently installed version. 39 | Run with -f to apply changes in non-interactive mode. 40 | [1] 41 | 42 | Check that the missing libraries get added: 43 | 44 | $ opam-dune-lint -f 45 | test.opam: changes needed: 46 | "fmt" {>= "1.0"} [from /] 47 | "bos" {with-test & >= "1.0"} [from /] 48 | "opam-state" {with-test & >= "1.0"} [from /] 49 | Note: version numbers are just suggestions based on the currently installed version. 50 | Wrote "dune-project" 51 | 52 | $ cat dune-project | sed 's/= [^)}]*/= */g' 53 | (lang dune 2.7) 54 | 55 | (generate_opam_files true) 56 | 57 | (package 58 | (name test) 59 | (synopsis "Test package") 60 | (depends 61 | (opam-state 62 | (and 63 | (>= *) 64 | :with-test)) 65 | (bos 66 | (and 67 | (>= *) 68 | :with-test)) 69 | (fmt 70 | (>= *)) 71 | (ocamlfind 72 | (>= *)) 73 | libfoo)) 74 | 75 | Check adding and removing of test markers: 76 | 77 | $ cat > dune-project << EOF 78 | > (lang dune 2.7) 79 | > (generate_opam_files true) 80 | > (package 81 | > (name test) 82 | > (synopsis "Test package") 83 | > (depends 84 | > opam-state 85 | > (bos (>= 1.0)) 86 | > (fmt :with-test) 87 | > (ocamlfind (and (>= 1.0) :with-test)) 88 | > libfoo)) 89 | > EOF 90 | 91 | $ dune build @install 92 | 93 | $ opam-dune-lint -f 94 | test.opam: changes needed: 95 | "fmt" [from /] (remove {with-test}) 96 | "ocamlfind" [from /] (remove {with-test}) 97 | "bos" {with-test} [from /] (missing {with-test} annotation) 98 | "opam-state" {with-test} [from /] (missing {with-test} annotation) 99 | Wrote "dune-project" 100 | 101 | $ cat dune-project | sed 's/= [^)}]*/= */g' 102 | (lang dune 2.7) 103 | 104 | (generate_opam_files true) 105 | 106 | (package 107 | (name test) 108 | (synopsis "Test package") 109 | (depends 110 | (opam-state :with-test) 111 | (bos 112 | (and 113 | :with-test 114 | (>= *))) 115 | fmt 116 | (ocamlfind 117 | (>= *)) 118 | libfoo)) 119 | 120 | $ opam-dune-lint 121 | test.opam: OK 122 | -------------------------------------------------------------------------------- /tests/test_dune_constraints.t: -------------------------------------------------------------------------------- 1 | Create a simple dune project: for testing opam-dune-lint, if dune constraint matches 2 | the dune-project file 3 | 4 | 5 | $ cat > dune-project << EOF 6 | > (lang dune 2.7) 7 | > (package 8 | > (name test) 9 | > (synopsis "Test package")) 10 | > EOF 11 | 12 | $ cat > test.opam << EOF 13 | > # Preserve comments 14 | > opam-version: "2.0" 15 | > synopsis: "Test package" 16 | > build: [ 17 | > ["dune" "build"] 18 | > ] 19 | > depends: [ 20 | > "ocamlfind" {>= "1.0"} 21 | > "libfoo" 22 | > ] 23 | > EOF 24 | 25 | $ cat > dune << EOF 26 | > (executable 27 | > (name main) 28 | > (public_name main) 29 | > (modules main) 30 | > (libraries findlib fmt)) 31 | > (test 32 | > (name test) 33 | > (modules test) 34 | > (libraries bos opam-state)) 35 | > EOF 36 | 37 | $ touch main.ml test.ml 38 | $ dune build 39 | 40 | Replace all version numbers with "1.0" to get predictable output. 41 | 42 | $ export OPAM_DUNE_LINT_TESTS=y 43 | 44 | Check that the missing libraries get added: 45 | 46 | $ opam-dune-lint -f 47 | test.opam: changes needed: 48 | "fmt" {>= "1.0"} [from /] 49 | "bos" {with-test & >= "1.0"} [from /] 50 | "opam-state" {with-test & >= "1.0"} [from /] 51 | Note: version numbers are just suggestions based on the currently installed version. 52 | Wrote "./test.opam" 53 | Warning in test: The package has a dune-project file but no explicit dependency on dune was found. 54 | 55 | $ cat test.opam | sed 's/= [^&)}]*/= */g' 56 | # Preserve comments 57 | opam-version: "2.0" 58 | synopsis: "Test package" 59 | build: [ 60 | ["dune" "build"] 61 | ] 62 | depends: [ 63 | "ocamlfind" {>= *} 64 | "libfoo" 65 | "fmt" {>= *} 66 | "bos" {>= *& with-test} 67 | "opam-state" {>= *& with-test} 68 | ] 69 | 70 | $ cat > test.opam << EOF 71 | > # Preserve comments 72 | > opam-version: "2.0" 73 | > synopsis: "Test package" 74 | > build: [ 75 | > ["dune" "build"] 76 | > ] 77 | > depends: [ 78 | > "ocamlfind" {>= "1.0"} 79 | > "libfoo" 80 | > "dune" {> "2.7" & build} 81 | > ] 82 | > EOF 83 | $ dune build 84 | 85 | Check that the missing libraries get added: 86 | 87 | $ opam-dune-lint -f 88 | test.opam: changes needed: 89 | "fmt" {>= "1.0"} [from /] 90 | "bos" {with-test & >= "1.0"} [from /] 91 | "opam-state" {with-test & >= "1.0"} [from /] 92 | Note: version numbers are just suggestions based on the currently installed version. 93 | Wrote "./test.opam" 94 | Warning in test: 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. 95 | 96 | $ cat > test.opam << EOF 97 | > # Preserve comments 98 | > opam-version: "2.0" 99 | > synopsis: "Test package" 100 | > build: [ 101 | > ["dune" "build"] 102 | > ] 103 | > depends: [ 104 | > "ocamlfind" {>= "1.0"} 105 | > "libfoo" 106 | > "dune" {> "1.0" & build} 107 | > ] 108 | > EOF 109 | 110 | $ cat > test1.opam << EOF 111 | > # Preserve comments 112 | > opam-version: "2.0" 113 | > synopsis: "Test package" 114 | > build: [ 115 | > ["dune" "build"] 116 | > ] 117 | > depends: [ 118 | > "ocamlfind" {>= "1.0"} 119 | > "libfoo" 120 | > "dune" {> "1.0" & build} 121 | > ] 122 | > EOF 123 | 124 | $ cat > dune-project << EOF 125 | > (lang dune 2.7) 126 | > (package 127 | > (name test) 128 | > (synopsis "Test package")) 129 | > (package 130 | > (name test1) 131 | > (synopsis "Test1 package")) 132 | > EOF 133 | 134 | $ cat > dune << EOF 135 | > (executable 136 | > (name main) 137 | > (public_name main) 138 | > (package test) 139 | > (modules main) 140 | > (libraries findlib fmt)) 141 | > (test 142 | > (name test) 143 | > (modules test) 144 | > (libraries bos opam-state)) 145 | > EOF 146 | $ dune build 147 | 148 | Check that the missing libraries get added and print all errors before the exit: 149 | 150 | $ opam-dune-lint -f 151 | test.opam: changes needed: 152 | "fmt" {>= "1.0"} [from /] 153 | "bos" {with-test & >= "1.0"} [from /] 154 | "opam-state" {with-test & >= "1.0"} [from /] 155 | test1.opam: changes needed: 156 | "bos" {with-test & >= "1.0"} [from /] 157 | "opam-state" {with-test & >= "1.0"} [from /] 158 | Note: version numbers are just suggestions based on the currently installed version. 159 | Wrote "./test.opam" 160 | Wrote "./test1.opam" 161 | Warning in test: 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. 162 | Error in test: Your dune-project file indicates that this package requires at least dune 2.7 but your opam file only requires dune >= 1.0. Please check which requirement is the right one, and fix the other. 163 | Warning in test1: 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. 164 | Error in test1: Your dune-project file indicates that this package requires at least dune 2.7 but your opam file only requires dune >= 1.0. Please check which requirement is the right one, and fix the other. 165 | [1] 166 | -------------------------------------------------------------------------------- /tests/test_dune_copy_install.t: -------------------------------------------------------------------------------- 1 | Create a simple dune project and use "install" stanza: 2 | 3 | $ cat > dune-project << EOF 4 | > (lang dune 2.7) 5 | > (generate_opam_files true) 6 | > (package 7 | > (name test) 8 | > (synopsis "Test package") 9 | > (depends 10 | > (ocamlfind (>= 1.0)) 11 | > libfoo)) 12 | > (package 13 | > (name zombie) 14 | > (synopsis "Zombie package")) 15 | > EOF 16 | 17 | $ cat > dune << EOF 18 | > (executable 19 | > (name main) 20 | > (modules main) 21 | > (libraries findlib fmt)) 22 | > (test 23 | > (name test) 24 | > (modules test) 25 | > (libraries bos opam-state)) 26 | > (rule 27 | > (target main-copy.exe) 28 | > (deps 29 | > (package zombie) main.exe) 30 | > (action 31 | > (copy main.exe main-copy.exe))) 32 | > (rule 33 | > (alias runtest) 34 | > (deps 35 | > (package zombie)) 36 | > (action (progn))) 37 | > (install 38 | > (section bin) 39 | > (package test) 40 | > (files (main-copy.exe as main.exe))) 41 | > EOF 42 | 43 | $ touch main.ml test.ml 44 | $ dune build 45 | 46 | $ export OPAM_DUNE_LINT_TESTS=y 47 | 48 | Check that the missing libraries are detected: 49 | 50 | $ opam-dune-lint = "1.0"} [from /] 53 | "bos" {with-test & >= "1.0"} [from /] 54 | "opam-state" {with-test & >= "1.0"} [from /] 55 | zombie.opam: changes needed: 56 | "bos" {with-test & >= "1.0"} [from /] 57 | "opam-state" {with-test & >= "1.0"} [from /] 58 | Note: version numbers are just suggestions based on the currently installed version. 59 | Run with -f to apply changes in non-interactive mode. 60 | [1] 61 | 62 | Check that the missing libraries get added: 63 | 64 | $ opam-dune-lint -f 65 | test.opam: changes needed: 66 | "fmt" {>= "1.0"} [from /] 67 | "bos" {with-test & >= "1.0"} [from /] 68 | "opam-state" {with-test & >= "1.0"} [from /] 69 | zombie.opam: changes needed: 70 | "bos" {with-test & >= "1.0"} [from /] 71 | "opam-state" {with-test & >= "1.0"} [from /] 72 | Note: version numbers are just suggestions based on the currently installed version. 73 | Wrote "dune-project" 74 | 75 | $ cat dune-project | sed 's/= [^)}]*/= */g' 76 | (lang dune 2.7) 77 | 78 | (generate_opam_files true) 79 | 80 | (package 81 | (name test) 82 | (synopsis "Test package") 83 | (depends 84 | (opam-state 85 | (and 86 | (>= *) 87 | :with-test)) 88 | (bos 89 | (and 90 | (>= *) 91 | :with-test)) 92 | (fmt 93 | (>= *)) 94 | (ocamlfind 95 | (>= *)) 96 | libfoo)) 97 | 98 | (package 99 | (name zombie) 100 | (synopsis "Zombie package") 101 | (depends 102 | (opam-state 103 | (and 104 | (>= *) 105 | :with-test)) 106 | (bos 107 | (and 108 | (>= *) 109 | :with-test)))) 110 | 111 | Check adding and removing of test markers: 112 | 113 | $ cat > dune-project << EOF 114 | > (lang dune 2.7) 115 | > (generate_opam_files true) 116 | > (package 117 | > (name test) 118 | > (synopsis "Test package") 119 | > (depends 120 | > opam-state 121 | > (bos (>= 1.0)) 122 | > (fmt :with-test) 123 | > (ocamlfind (and (>= 1.0) :with-test)) 124 | > libfoo)) 125 | > (package 126 | > (name zombie) 127 | > (synopsis "Zombie package")) 128 | > EOF 129 | 130 | $ dune build @install 131 | 132 | $ opam-dune-lint -f 133 | test.opam: changes needed: 134 | "fmt" [from /] (remove {with-test}) 135 | "ocamlfind" [from /] (remove {with-test}) 136 | "bos" {with-test} [from /] (missing {with-test} annotation) 137 | "opam-state" {with-test} [from /] (missing {with-test} annotation) 138 | zombie.opam: changes needed: 139 | "bos" {with-test & >= "1.0"} [from /] 140 | "opam-state" {with-test & >= "1.0"} [from /] 141 | Note: version numbers are just suggestions based on the currently installed version. 142 | Wrote "dune-project" 143 | 144 | $ cat dune-project | sed 's/= [^)}]*/= */g' 145 | (lang dune 2.7) 146 | 147 | (generate_opam_files true) 148 | 149 | (package 150 | (name test) 151 | (synopsis "Test package") 152 | (depends 153 | (opam-state :with-test) 154 | (bos 155 | (and 156 | :with-test 157 | (>= *))) 158 | fmt 159 | (ocamlfind 160 | (>= *)) 161 | libfoo)) 162 | 163 | (package 164 | (name zombie) 165 | (synopsis "Zombie package") 166 | (depends 167 | (opam-state 168 | (and 169 | (>= *) 170 | :with-test)) 171 | (bos 172 | (and 173 | (>= *) 174 | :with-test)))) 175 | 176 | $ opam-dune-lint 177 | test.opam: OK 178 | zombie.opam: OK 179 | -------------------------------------------------------------------------------- /tests/test_dune_describe.t: -------------------------------------------------------------------------------- 1 | Create a simple dune project: 2 | 3 | $ cat > dune-project << EOF 4 | > (lang dune 2.8) 5 | > (generate_opam_files true) 6 | > (package 7 | > (name test) 8 | > (synopsis "Test package") 9 | > (depends 10 | > (ocamlfind (>= 1.0)) 11 | > libfoo)) 12 | > EOF 13 | 14 | $ cat > dune << EOF 15 | > (executable 16 | > (name main) 17 | > (public_name main) 18 | > (modules main) 19 | > (libraries findlib fmt) 20 | > (modes byte)) 21 | > (library 22 | > (name lib) 23 | > (package test) 24 | > (modules lib) 25 | > (libraries bos) 26 | > (modes byte)) 27 | > (test 28 | > (package test) 29 | > (name test) 30 | > (modules test) 31 | > (libraries bos opam-state) 32 | > (modes byte)) 33 | > EOF 34 | 35 | $ touch main.ml test.ml lib.ml 36 | $ dune describe external-lib-deps 37 | (default 38 | ((executables 39 | ((names (main)) 40 | (extensions (.bc)) 41 | (package (test)) 42 | (source_dir .) 43 | (external_deps 44 | ((findlib required) 45 | (fmt required))) 46 | (internal_deps ()))) 47 | (library 48 | ((names (lib)) 49 | (extensions ()) 50 | (package (test)) 51 | (source_dir .) 52 | (external_deps ((bos required))) 53 | (internal_deps ()))) 54 | (tests 55 | ((names (test)) 56 | (extensions (.bc)) 57 | (package (test)) 58 | (source_dir .) 59 | (external_deps 60 | ((bos required) 61 | (opam-state required))) 62 | (internal_deps ()))))) 63 | $ dune describe package-entries 64 | ((test 65 | (((source Dune) 66 | (entry 67 | ((src 68 | (In_build_dir default/META.test)) 69 | (kind file) 70 | (dst META) 71 | (section LIB) 72 | (optional false)))) 73 | ((source 74 | (User 75 | ((pos_fname dune) 76 | (start 77 | ((pos_lnum 7) 78 | (pos_bol 101) 79 | (pos_cnum 101))) 80 | (stop 81 | ((pos_lnum 12) 82 | (pos_bol 170) 83 | (pos_cnum 184)))))) 84 | (entry 85 | ((src 86 | (In_build_dir default/.lib.objs/byte/lib.cmi)) 87 | (kind file) 88 | (dst __private__/lib/.public_cmi/lib.cmi) 89 | (section LIB) 90 | (optional false)))) 91 | ((source 92 | (User 93 | ((pos_fname dune) 94 | (start 95 | ((pos_lnum 7) 96 | (pos_bol 101) 97 | (pos_cnum 101))) 98 | (stop 99 | ((pos_lnum 12) 100 | (pos_bol 170) 101 | (pos_cnum 184)))))) 102 | (entry 103 | ((src 104 | (In_build_dir default/.lib.objs/byte/lib.cmt)) 105 | (kind file) 106 | (dst __private__/lib/.public_cmi/lib.cmt) 107 | (section LIB) 108 | (optional false)))) 109 | ((source 110 | (User 111 | ((pos_fname dune) 112 | (start 113 | ((pos_lnum 7) 114 | (pos_bol 101) 115 | (pos_cnum 101))) 116 | (stop 117 | ((pos_lnum 12) 118 | (pos_bol 170) 119 | (pos_cnum 184)))))) 120 | (entry 121 | ((src 122 | (In_build_dir default/lib.cma)) 123 | (kind file) 124 | (dst __private__/lib/lib.cma) 125 | (section LIB) 126 | (optional false)))) 127 | ((source 128 | (User 129 | ((pos_fname dune) 130 | (start 131 | ((pos_lnum 7) 132 | (pos_bol 101) 133 | (pos_cnum 101))) 134 | (stop 135 | ((pos_lnum 12) 136 | (pos_bol 170) 137 | (pos_cnum 184)))))) 138 | (entry 139 | ((src 140 | (In_build_dir default/lib.ml)) 141 | (kind file) 142 | (dst __private__/lib/lib.ml) 143 | (section LIB) 144 | (optional false)))) 145 | ((source Dune) 146 | (entry 147 | ((src 148 | (In_build_dir default/test.dune-package)) 149 | (kind file) 150 | (dst dune-package) 151 | (section LIB) 152 | (optional false)))) 153 | ((source Dune) 154 | (entry 155 | ((src 156 | (In_build_dir default/test.opam)) 157 | (kind file) 158 | (dst opam) 159 | (section LIB) 160 | (optional false)))) 161 | ((source 162 | (User 163 | ((pos_fname dune) 164 | (start 165 | ((pos_lnum 2) 166 | (pos_bol 12) 167 | (pos_cnum 19))) 168 | (stop 169 | ((pos_lnum 2) 170 | (pos_bol 12) 171 | (pos_cnum 23)))))) 172 | (entry 173 | ((src 174 | (In_build_dir default/main.bc)) 175 | (kind file) 176 | (dst main) 177 | (section BIN) 178 | (optional false))))))) 179 | $ dune describe opam-files 180 | ((test.opam 181 | "# This file is generated by dune, edit dune-project instead\nopam-version: \"2.0\"\nsynopsis: \"Test package\"\ndepends: [\n \"dune\" {>= \"2.8\"}\n \"ocamlfind\" {>= \"1.0\"}\n \"libfoo\"\n \"odoc\" {with-doc}\n]\nbuild: [\n [\"dune\" \"subst\"] {dev}\n [\n \"dune\"\n \"build\"\n \"-p\"\n name\n \"-j\"\n jobs\n \"@install\"\n \"@runtest\" {with-test}\n \"@doc\" {with-doc}\n ]\n]\n")) 182 | -------------------------------------------------------------------------------- /tests/test_dune_same_exe_name.t: -------------------------------------------------------------------------------- 1 | This is a test inspired when testing opam-dune-lint against 2 | dune project "https://github.com/ocaml/dune/". There is 2 executables 3 | with the same name in different directory. The public executable was also 4 | taking the deps from the private library. 5 | 6 | $ mkdir bin bench 7 | $ cat > dune-project << EOF 8 | > (lang dune 2.7) 9 | > (generate_opam_files true) 10 | > (package 11 | > (name test) 12 | > (synopsis "Test package") 13 | > (depends 14 | > (ocamlfind (>= 1.0)) 15 | > libfoo)) 16 | > EOF 17 | 18 | $ cat > bench/dune << EOF 19 | > (executable 20 | > (name main) 21 | > (modules main) 22 | > (libraries sexplib cmdliner)) 23 | > EOF 24 | 25 | $ cat > bin/dune << EOF 26 | > (executable 27 | > (name main) 28 | > (public_name main) 29 | > (modules main) 30 | > (libraries findlib fmt)) 31 | > (test 32 | > (name test) 33 | > (modules test) 34 | > (libraries bos opam-state)) 35 | > EOF 36 | 37 | $ touch bin/main.ml bin/test.ml bench/main.ml 38 | $ dune build 39 | 40 | $ export OPAM_DUNE_LINT_TESTS=y 41 | 42 | Check that the missing libraries are detected: 43 | 44 | $ opam-dune-lint = "1.0"} [from bin] 47 | "bos" {with-test & >= "1.0"} [from bin] 48 | "opam-state" {with-test & >= "1.0"} [from bin] 49 | Note: version numbers are just suggestions based on the currently installed version. 50 | Run with -f to apply changes in non-interactive mode. 51 | [1] 52 | 53 | Check that the missing libraries get added: 54 | 55 | $ opam-dune-lint -f 56 | test.opam: changes needed: 57 | "fmt" {>= "1.0"} [from bin] 58 | "bos" {with-test & >= "1.0"} [from bin] 59 | "opam-state" {with-test & >= "1.0"} [from bin] 60 | Note: version numbers are just suggestions based on the currently installed version. 61 | Wrote "dune-project" 62 | -------------------------------------------------------------------------------- /tests/test_dune_stanza_install.t: -------------------------------------------------------------------------------- 1 | Create a simple dune project and use "install" stanza: 2 | 3 | $ cat > dune-project << EOF 4 | > (lang dune 2.7) 5 | > (generate_opam_files true) 6 | > (package 7 | > (name test) 8 | > (synopsis "Test package") 9 | > (depends 10 | > (ocamlfind (>= 1.0)) 11 | > libfoo)) 12 | > EOF 13 | 14 | $ cat > dune << EOF 15 | > (executable 16 | > (name main) 17 | > (modules main) 18 | > (libraries findlib fmt)) 19 | > (test 20 | > (name test) 21 | > (modules test) 22 | > (libraries bos opam-state)) 23 | > (install 24 | > (section bin) 25 | > (package test) 26 | > (files main.exe)) 27 | > EOF 28 | 29 | $ touch main.ml test.ml 30 | $ dune build 31 | 32 | $ export OPAM_DUNE_LINT_TESTS=y 33 | 34 | Check that the missing libraries are detected: 35 | 36 | $ opam-dune-lint = "1.0"} [from /] 39 | "bos" {with-test & >= "1.0"} [from /] 40 | "opam-state" {with-test & >= "1.0"} [from /] 41 | Note: version numbers are just suggestions based on the currently installed version. 42 | Run with -f to apply changes in non-interactive mode. 43 | [1] 44 | 45 | Check that the missing libraries get added: 46 | 47 | $ opam-dune-lint -f 48 | test.opam: changes needed: 49 | "fmt" {>= "1.0"} [from /] 50 | "bos" {with-test & >= "1.0"} [from /] 51 | "opam-state" {with-test & >= "1.0"} [from /] 52 | Note: version numbers are just suggestions based on the currently installed version. 53 | Wrote "dune-project" 54 | 55 | $ cat dune-project | sed 's/= [^)}]*/= */g' 56 | (lang dune 2.7) 57 | 58 | (generate_opam_files true) 59 | 60 | (package 61 | (name test) 62 | (synopsis "Test package") 63 | (depends 64 | (opam-state 65 | (and 66 | (>= *) 67 | :with-test)) 68 | (bos 69 | (and 70 | (>= *) 71 | :with-test)) 72 | (fmt 73 | (>= *)) 74 | (ocamlfind 75 | (>= *)) 76 | libfoo)) 77 | 78 | Check adding and removing of test markers: 79 | 80 | $ cat > dune-project << EOF 81 | > (lang dune 2.7) 82 | > (generate_opam_files true) 83 | > (package 84 | > (name test) 85 | > (synopsis "Test package") 86 | > (depends 87 | > opam-state 88 | > (bos (>= 1.0)) 89 | > (fmt :with-test) 90 | > (ocamlfind (and (>= 1.0) :with-test)) 91 | > libfoo)) 92 | > EOF 93 | 94 | $ dune build @install 95 | 96 | $ opam-dune-lint -f 97 | test.opam: changes needed: 98 | "fmt" [from /] (remove {with-test}) 99 | "ocamlfind" [from /] (remove {with-test}) 100 | "bos" {with-test} [from /] (missing {with-test} annotation) 101 | "opam-state" {with-test} [from /] (missing {with-test} annotation) 102 | Wrote "dune-project" 103 | 104 | $ cat dune-project | sed 's/= [^)}]*/= */g' 105 | (lang dune 2.7) 106 | 107 | (generate_opam_files true) 108 | 109 | (package 110 | (name test) 111 | (synopsis "Test package") 112 | (depends 113 | (opam-state :with-test) 114 | (bos 115 | (and 116 | :with-test 117 | (>= *))) 118 | fmt 119 | (ocamlfind 120 | (>= *)) 121 | libfoo)) 122 | 123 | $ opam-dune-lint 124 | test.opam: OK 125 | -------------------------------------------------------------------------------- /tests/test_empty_dune.t: -------------------------------------------------------------------------------- 1 | Create a dune project with no depends section: 2 | 3 | $ cat > dune-project << EOF 4 | > (lang dune 2.7) 5 | > (generate_opam_files true) 6 | > (package 7 | > (name test) 8 | > (synopsis "Test package")) 9 | > EOF 10 | 11 | $ cat > dune << EOF 12 | > (executable 13 | > (name main) 14 | > (public_name main) 15 | > (modules main) 16 | > (libraries findlib fmt)) 17 | > (test 18 | > (name test) 19 | > (modules test) 20 | > (libraries bos opam-state)) 21 | > EOF 22 | 23 | $ touch main.ml test.ml 24 | $ dune build 25 | 26 | Replace all version numbers with "1.0" to get predictable outut. 27 | 28 | $ export OPAM_DUNE_LINT_TESTS=y 29 | 30 | Check that all the libraries get added: 31 | 32 | $ opam-dune-lint -f 33 | test.opam: changes needed: 34 | "fmt" {>= "1.0"} [from /] 35 | "ocamlfind" {>= "1.0"} [from /] 36 | "bos" {with-test & >= "1.0"} [from /] 37 | "opam-state" {with-test & >= "1.0"} [from /] 38 | Note: version numbers are just suggestions based on the currently installed version. 39 | Wrote "dune-project" 40 | 41 | $ cat dune-project | sed 's/= [^)}]*/= */g' 42 | (lang dune 2.7) 43 | 44 | (generate_opam_files true) 45 | 46 | (package 47 | (name test) 48 | (synopsis "Test package") 49 | (depends 50 | (opam-state 51 | (and 52 | (>= *) 53 | :with-test)) 54 | (bos 55 | (and 56 | (>= *) 57 | :with-test)) 58 | (ocamlfind 59 | (>= *)) 60 | (fmt 61 | (>= *)))) 62 | -------------------------------------------------------------------------------- /tests/test_fix_bug.t: -------------------------------------------------------------------------------- 1 | This was a bug when the `dune-project` file has no package declaration in it: 2 | opam-dune-lint was printing the follwing error : (Failure "No *.opam files found!") 3 | 4 | $ cat > dune-project << EOF 5 | > (lang dune 2.7) 6 | > EOF 7 | 8 | $ cat > test.opam << EOF 9 | > # Preserve comments 10 | > opam-version: "2.0" 11 | > synopsis: "Test package" 12 | > build: [ 13 | > ["dune" "build"] 14 | > ] 15 | > depends: [ 16 | > "ocamlfind" {>= "1.0"} 17 | > "libfoo" 18 | > "dune" {>= "2.7"} 19 | > ] 20 | > EOF 21 | 22 | $ cat > dune << EOF 23 | > (executable 24 | > (name main) 25 | > (public_name main) 26 | > (modules main) 27 | > (libraries findlib fmt)) 28 | > (test 29 | > (name test) 30 | > (modules test) 31 | > (libraries bos opam-state)) 32 | > EOF 33 | 34 | $ touch main.ml test.ml 35 | $ dune build 36 | 37 | Replace all version numbers with "1.0" to get predictable output. 38 | 39 | $ export OPAM_DUNE_LINT_TESTS=y 40 | 41 | Check that the missing libraries get added: 42 | 43 | $ opam-dune-lint -f 44 | test.opam: changes needed: 45 | "fmt" {>= "1.0"} [from /] 46 | "bos" {with-test & >= "1.0"} [from /] 47 | "opam-state" {with-test & >= "1.0"} [from /] 48 | Note: version numbers are just suggestions based on the currently installed version. 49 | Wrote "./test.opam" 50 | 51 | $ sed 's/= [^&)}]*/= */g' test.opam 52 | # Preserve comments 53 | opam-version: "2.0" 54 | synopsis: "Test package" 55 | build: [ 56 | ["dune" "build"] 57 | ] 58 | depends: [ 59 | "ocamlfind" {>= *} 60 | "libfoo" 61 | "dune" {>= *} 62 | "fmt" {>= *} 63 | "bos" {>= *& with-test} 64 | "opam-state" {>= *& with-test} 65 | ] 66 | -------------------------------------------------------------------------------- /tests/test_fix_bug_59.t: -------------------------------------------------------------------------------- 1 | Sexplib fails to parse `"\>` or `"\|`, those are finely parsed by dune. (issue #59) 2 | opam-dune-lint does not need the description section. 3 | 4 | $ cat > dune-project << EOF 5 | > (lang dune 2.7) 6 | > (generate_opam_files) 7 | > (name dummy) 8 | > 9 | > (package 10 | > (name dummy) 11 | > (description 12 | > "\> Dummy 13 | > )) 14 | > EOF 15 | 16 | $ dune build 17 | $ opam-dune-lint -f 18 | dummy.opam: OK 19 | 20 | $ cat > dune-project << EOF 21 | > (lang dune 2.7) 22 | > (generate_opam_files) 23 | > (name dummy) 24 | > 25 | > (package 26 | > (name dummy) 27 | > (description 28 | > "\> Dummy 29 | > "\| Dummy other 30 | > )) 31 | > EOF 32 | 33 | $ dune build 34 | $ opam-dune-lint -f 35 | dummy.opam: OK 36 | 37 | $ cat > dune-project << EOF 38 | > (lang dune 2.7) 39 | > (generate_opam_files) 40 | > (name dummy) 41 | > 42 | > (package 43 | > (name dummy) 44 | > (description 45 | > "\> Dummy 46 | > "\| Dummy other 47 | > "\> Dummy other 48 | > )) 49 | > EOF 50 | 51 | $ dune build 52 | $ opam-dune-lint -f 53 | dummy.opam: OK 54 | -------------------------------------------------------------------------------- /tests/test_fix_bug_66.t: -------------------------------------------------------------------------------- 1 | opam-dune-lint is using a kind of hack to parse `dune-project` string, 2 | wrapping it up as "(__dune_project";`dune_project_file_string`;")" S-expression. When 3 | `dune_project_file_string` ends up with a comment, the ")" fall into the last comment itself. 4 | 5 | $ cat > dune-project << EOF 6 | > (lang dune 2.7) 7 | > (generate_opam_files) 8 | > ; comment added at end 9 | > EOF 10 | 11 | $ cat > empty.opam << EOF 12 | > opam-version: "2.0" 13 | > EOF 14 | 15 | $ touch dune 16 | $ opam-dune-lint 17 | empty.opam: changed after its upgrade from 'dune describe opam-files'! 18 | empty.opam: OK 19 | Warning in empty: The package has a dune-project file but no explicit dependency on dune was found. 20 | [1] 21 | -------------------------------------------------------------------------------- /tests/test_fix_bug_68.t: -------------------------------------------------------------------------------- 1 | "dune describe opam-files" give an S-expression in which quoted string are escaped. 2 | the string "%{" is escaped with `\` to gives "\%{". And OpamFile fails parsing this 3 | output. 4 | 5 | 6 | $ cat > dune-project << EOF 7 | > (lang dune 2.7) 8 | > (package 9 | > (name test) 10 | > (synopsis "Test package")) 11 | > EOF 12 | 13 | $ cat > test.opam << EOF 14 | > # Preserve comments 15 | > opam-version: "2.0" 16 | > synopsis: "Test package" 17 | > build: [ 18 | > ["dune" "build" "--use-libev" "%{conf-libev:installed}%" ] 19 | > ] 20 | > depends: [ 21 | > "ocamlfind" {>= "1.0"} 22 | > "libfoo" 23 | > ] 24 | > EOF 25 | 26 | $ cat > dune << EOF 27 | > (executable 28 | > (name main) 29 | > (public_name main) 30 | > (modules main) 31 | > (libraries findlib fmt)) 32 | > (test 33 | > (name test) 34 | > (modules test) 35 | > (libraries bos opam-state)) 36 | > EOF 37 | 38 | $ touch main.ml test.ml 39 | $ dune build 40 | 41 | Replace all version numbers with "1.0" to get predictable output. 42 | 43 | $ export OPAM_DUNE_LINT_TESTS=y 44 | 45 | Check that the missing libraries get added: 46 | 47 | $ opam-dune-lint -f 48 | test.opam: changes needed: 49 | "fmt" {>= "1.0"} [from /] 50 | "bos" {with-test & >= "1.0"} [from /] 51 | "opam-state" {with-test & >= "1.0"} [from /] 52 | Note: version numbers are just suggestions based on the currently installed version. 53 | Wrote "./test.opam" 54 | Warning in test: The package has a dune-project file but no explicit dependency on dune was found. 55 | -------------------------------------------------------------------------------- /tests/test_opam.t: -------------------------------------------------------------------------------- 1 | Create a simple dune project: 2 | 3 | $ cat > dune-project << EOF 4 | > (lang dune 2.7) 5 | > (package 6 | > (name test) 7 | > (synopsis "Test package")) 8 | > EOF 9 | 10 | $ cat > test.opam << EOF 11 | > # Preserve comments 12 | > opam-version: "2.0" 13 | > synopsis: "Test package" 14 | > build: [ 15 | > ["dune" "build"] 16 | > ] 17 | > depends: [ 18 | > "ocamlfind" {>= "1.0"} 19 | > "libfoo" 20 | > "dune" {>= "2.7"} 21 | > ] 22 | > EOF 23 | 24 | $ cat > dune << EOF 25 | > (executable 26 | > (name main) 27 | > (public_name main) 28 | > (modules main) 29 | > (libraries findlib fmt)) 30 | > (test 31 | > (name test) 32 | > (modules test) 33 | > (libraries bos opam-state)) 34 | > EOF 35 | 36 | $ touch main.ml test.ml 37 | $ dune build 38 | 39 | Replace all version numbers with "1.0" to get predictable output. 40 | 41 | $ export OPAM_DUNE_LINT_TESTS=y 42 | 43 | Check that the missing libraries get added: 44 | 45 | $ opam-dune-lint -f 46 | test.opam: changes needed: 47 | "fmt" {>= "1.0"} [from /] 48 | "bos" {with-test & >= "1.0"} [from /] 49 | "opam-state" {with-test & >= "1.0"} [from /] 50 | Note: version numbers are just suggestions based on the currently installed version. 51 | Wrote "./test.opam" 52 | 53 | $ cat test.opam | sed 's/= [^&)}]*/= */g' 54 | # Preserve comments 55 | opam-version: "2.0" 56 | synopsis: "Test package" 57 | build: [ 58 | ["dune" "build"] 59 | ] 60 | depends: [ 61 | "ocamlfind" {>= *} 62 | "libfoo" 63 | "dune" {>= *} 64 | "fmt" {>= *} 65 | "bos" {>= *& with-test} 66 | "opam-state" {>= *& with-test} 67 | ] 68 | -------------------------------------------------------------------------------- /tests/test_opam_update.t: -------------------------------------------------------------------------------- 1 | Create a simple dune project: testing if opam-dune-lint update opam files 2 | using the dune-project file, before the linting process. 3 | 4 | $ cat > dune-project << EOF 5 | > (lang dune 2.7) 6 | > (generate_opam_files true) 7 | > (package 8 | > (name test) 9 | > (synopsis "Test package") 10 | > (depends cmdliner)) 11 | > EOF 12 | 13 | $ cat > dune << EOF 14 | > (executable 15 | > (name main) 16 | > (public_name main) 17 | > (modules main) 18 | > (libraries findlib fmt)) 19 | > (test 20 | > (name test) 21 | > (modules test) 22 | > (libraries bos opam-state)) 23 | > EOF 24 | 25 | $ touch main.ml test.ml 26 | $ dune build 27 | $ cat test.opam 28 | # This file is generated by dune, edit dune-project instead 29 | opam-version: "2.0" 30 | synopsis: "Test package" 31 | depends: [ 32 | "dune" {>= "2.7"} 33 | "cmdliner" 34 | "odoc" {with-doc} 35 | ] 36 | build: [ 37 | ["dune" "subst"] {dev} 38 | [ 39 | "dune" 40 | "build" 41 | "-p" 42 | name 43 | "-j" 44 | jobs 45 | "@install" 46 | "@runtest" {with-test} 47 | "@doc" {with-doc} 48 | ] 49 | ] 50 | 51 | Replace all version numbers with "1.0" to get predictable output. 52 | 53 | $ export OPAM_DUNE_LINT_TESTS=y 54 | 55 | Check that the missing libraries get added: 56 | 57 | $ opam-dune-lint -f 58 | test.opam: changes needed: 59 | "fmt" {>= "1.0"} [from /] 60 | "ocamlfind" {>= "1.0"} [from /] 61 | "bos" {with-test & >= "1.0"} [from /] 62 | "opam-state" {with-test & >= "1.0"} [from /] 63 | Note: version numbers are just suggestions based on the currently installed version. 64 | Wrote "dune-project" 65 | 66 | $ cat test.opam | sed 's/= [^&)}]*/= */g' 67 | # This file is generated by dune, edit dune-project instead 68 | opam-version: "2.0" 69 | synopsis: "Test package" 70 | depends: [ 71 | "dune" {>= *} 72 | "opam-state" {>= *& with-test} 73 | "bos" {>= *& with-test} 74 | "ocamlfind" {>= *} 75 | "fmt" {>= *} 76 | "cmdliner" 77 | "odoc" {with-doc} 78 | ] 79 | build: [ 80 | ["dune" "subst"] {dev} 81 | [ 82 | "dune" 83 | "build" 84 | "-p" 85 | name 86 | "-j" 87 | jobs 88 | "@install" 89 | "@runtest" {with-test} 90 | "@doc" {with-doc} 91 | ] 92 | ] 93 | -------------------------------------------------------------------------------- /tests/test_optional_public_lib.t: -------------------------------------------------------------------------------- 1 | Inspired from test_vendoring.t test. Create a project with an public optional libraries 2 | It fixes the bug #53. The fix is about to not resolve an public optional library when used as dependency. 3 | 4 | $ mkdir bin lib optional 5 | 6 | $ cat > dune-project << EOF 7 | > (lang dune 2.7) 8 | > (generate_opam_files true) 9 | > (package 10 | > (name main) 11 | > (synopsis "Main package") 12 | > (depends libfoo)) 13 | > (package 14 | > (name optional) 15 | > (synopsis "Optional package") 16 | > (depends bos)) 17 | > EOF 18 | 19 | $ cat > bin/dune << EOF 20 | > (executable 21 | > (name main) 22 | > (package main) 23 | > (public_name main) 24 | > (libraries lib)) 25 | > EOF 26 | 27 | $ cat > lib/dune << EOF 28 | > (library 29 | > (name lib) 30 | > (public_name main) 31 | > (libraries findlib 32 | > (select file.ml from 33 | > (optional -> file.enabled.ml) 34 | > ( -> file.disabled.ml)))) 35 | > EOF 36 | 37 | $ cat > optional/dune << EOF 38 | > (library 39 | > (name optinal) 40 | > (public_name optional) 41 | > (libraries bos)) 42 | > EOF 43 | 44 | $ touch bin/main.ml lib/lib.ml lib/file.disabled.ml lib/file.enabled.ml optional/optional.ml 45 | $ dune build 46 | 47 | Replace all version numbers with "1.0" to get predictable outut. 48 | 49 | $ export OPAM_DUNE_LINT_TESTS=y 50 | 51 | Check configuration: 52 | 53 | $ dune external-lib-deps -p main @install 54 | dune: This subcommand has been moved to dune describe external-lib-deps. 55 | [1] 56 | 57 | Check that the missing findlib for "lib" is detected, but not "optional"'s dependency 58 | on "bos": 59 | 60 | $ opam-dune-lint = "1.0"} [from lib] 63 | optional.opam: OK 64 | Note: version numbers are just suggestions based on the currently installed version. 65 | Run with -f to apply changes in non-interactive mode. 66 | [1] 67 | -------------------------------------------------------------------------------- /tests/test_public_lib.t: -------------------------------------------------------------------------------- 1 | Create a simple dune project and test when a public library as internal dep is not recursively resolved: 2 | 3 | $ cat > dune-project << EOF 4 | > (lang dune 2.7) 5 | > (generate_opam_files true) 6 | > (package 7 | > (name test) 8 | > (synopsis "Test package") 9 | > (depends 10 | > (ocamlfind (>= 1.0)) 11 | > libfoo)) 12 | > (package 13 | > (name lib) 14 | > (synopsis "Lib package") 15 | > (depends sexplib)) 16 | > EOF 17 | 18 | $ cat > dune << EOF 19 | > (library 20 | > (public_name lib) 21 | > (modules lib) 22 | > (libraries sexplib)) 23 | > (executable 24 | > (name main) 25 | > (modules main) 26 | > (libraries lib findlib fmt)) 27 | > (test 28 | > (name test) 29 | > (modules test) 30 | > (libraries lib bos opam-state)) 31 | > (install 32 | > (section bin) 33 | > (package test) 34 | > (files main.exe)) 35 | > EOF 36 | 37 | $ touch main.ml test.ml lib.ml 38 | $ dune build 39 | 40 | $ export OPAM_DUNE_LINT_TESTS=y 41 | 42 | Check that the missing libraries are detected: 43 | 44 | $ opam-dune-lint = "1.0"} [from /] 47 | "opam-state" {with-test & >= "1.0"} [from /] 48 | test.opam: changes needed: 49 | "fmt" {>= "1.0"} [from /] 50 | "bos" {with-test & >= "1.0"} [from /] 51 | "opam-state" {with-test & >= "1.0"} [from /] 52 | Note: version numbers are just suggestions based on the currently installed version. 53 | Run with -f to apply changes in non-interactive mode. 54 | [1] 55 | 56 | Check that the missing libraries get added: 57 | 58 | $ opam-dune-lint -f 59 | lib.opam: changes needed: 60 | "bos" {with-test & >= "1.0"} [from /] 61 | "opam-state" {with-test & >= "1.0"} [from /] 62 | test.opam: changes needed: 63 | "fmt" {>= "1.0"} [from /] 64 | "bos" {with-test & >= "1.0"} [from /] 65 | "opam-state" {with-test & >= "1.0"} [from /] 66 | Note: version numbers are just suggestions based on the currently installed version. 67 | Wrote "dune-project" 68 | 69 | $ cat dune-project | sed 's/= [^)}]*/= */g' 70 | (lang dune 2.7) 71 | 72 | (generate_opam_files true) 73 | 74 | (package 75 | (name test) 76 | (synopsis "Test package") 77 | (depends 78 | (opam-state 79 | (and 80 | (>= *) 81 | :with-test)) 82 | (bos 83 | (and 84 | (>= *) 85 | :with-test)) 86 | (fmt 87 | (>= *)) 88 | (ocamlfind 89 | (>= *)) 90 | libfoo)) 91 | 92 | (package 93 | (name lib) 94 | (synopsis "Lib package") 95 | (depends 96 | (opam-state 97 | (and 98 | (>= *) 99 | :with-test)) 100 | (bos 101 | (and 102 | (>= *) 103 | :with-test)) 104 | sexplib)) 105 | 106 | Check adding and removing of test markers: 107 | 108 | $ cat > dune-project << EOF 109 | > (lang dune 2.7) 110 | > (generate_opam_files true) 111 | > (package 112 | > (name test) 113 | > (synopsis "Test package") 114 | > (depends 115 | > opam-state 116 | > (bos (>= 1.0)) 117 | > (fmt :with-test) 118 | > (ocamlfind (and (>= 1.0) :with-test)) 119 | > libfoo)) 120 | > (package 121 | > (name lib) 122 | > (synopsis "Lib package") 123 | > (depends sexplib)) 124 | > EOF 125 | 126 | $ dune build @install 127 | 128 | $ opam-dune-lint -f 129 | lib.opam: changes needed: 130 | "bos" {with-test & >= "1.0"} [from /] 131 | "opam-state" {with-test & >= "1.0"} [from /] 132 | test.opam: changes needed: 133 | "fmt" [from /] (remove {with-test}) 134 | "ocamlfind" [from /] (remove {with-test}) 135 | "bos" {with-test} [from /] (missing {with-test} annotation) 136 | "opam-state" {with-test} [from /] (missing {with-test} annotation) 137 | Note: version numbers are just suggestions based on the currently installed version. 138 | Wrote "dune-project" 139 | 140 | $ cat dune-project | sed 's/= [^)}]*/= */g' 141 | (lang dune 2.7) 142 | 143 | (generate_opam_files true) 144 | 145 | (package 146 | (name test) 147 | (synopsis "Test package") 148 | (depends 149 | (opam-state :with-test) 150 | (bos 151 | (and 152 | :with-test 153 | (>= *))) 154 | fmt 155 | (ocamlfind 156 | (>= *)) 157 | libfoo)) 158 | 159 | (package 160 | (name lib) 161 | (synopsis "Lib package") 162 | (depends 163 | (opam-state 164 | (and 165 | (>= *) 166 | :with-test)) 167 | (bos 168 | (and 169 | (>= *) 170 | :with-test)) 171 | sexplib)) 172 | 173 | $ opam-dune-lint 174 | lib.opam: OK 175 | test.opam: OK 176 | -------------------------------------------------------------------------------- /tests/test_vendoring.t: -------------------------------------------------------------------------------- 1 | Create a project with vendored libraries. 2 | `bin` depends on `lib` (an internal library). 3 | `lib` depends on `vendored`. 4 | We want to record the dependencies of `bin` and `lib` in the opam file, but not the dependencies of `vendored`, 5 | since they should be listed in the vendored opam files instead. 6 | 7 | $ mkdir bin lib vendored 8 | 9 | $ cat > dune-project << EOF 10 | > (lang dune 2.7) 11 | > (generate_opam_files true) 12 | > (package 13 | > (name main) 14 | > (synopsis "Main package") 15 | > (depends libfoo)) 16 | > EOF 17 | 18 | $ cat > dune << EOF 19 | > (vendored_dirs vendored) 20 | > EOF 21 | 22 | $ cat > bin/dune << EOF 23 | > (executable 24 | > (name main) 25 | > (public_name main) 26 | > (libraries lib)) 27 | > EOF 28 | 29 | $ cat > lib/dune << EOF 30 | > (library 31 | > (name lib) 32 | > (libraries findlib vendored)) 33 | > EOF 34 | 35 | $ cat > vendored/dune << EOF 36 | > (library 37 | > (name vendored) 38 | > (public_name vendored) 39 | > (libraries bos)) 40 | > EOF 41 | 42 | $ cat > vendored/dune-project << EOF 43 | > (lang dune 2.7) 44 | > EOF 45 | 46 | $ touch bin/main.ml lib/lib.ml 47 | $ (cd vendored && touch vendored.ml vendored.opam) 48 | $ dune build 49 | 50 | Replace all version numbers with "1.0" to get predictable outut. 51 | 52 | $ export OPAM_DUNE_LINT_TESTS=y 53 | 54 | Check configuration: 55 | 56 | $ dune external-lib-deps -p main @install 57 | dune: This subcommand has been moved to dune describe external-lib-deps. 58 | [1] 59 | 60 | Check that the missing findlib for "lib" is detected, but not "vendored"'s dependency 61 | on "bos": 62 | 63 | $ opam-dune-lint = "1.0"} [from lib] 66 | Note: version numbers are just suggestions based on the currently installed version. 67 | Run with -f to apply changes in non-interactive mode. 68 | [1] 69 | -------------------------------------------------------------------------------- /types.ml: -------------------------------------------------------------------------------- 1 | module Dir_set = Set.Make(Fpath) 2 | 3 | module Paths = Map.Make(String) 4 | 5 | module Libraries = Map.Make(String) 6 | 7 | module Dir_map = Map.Make(String) 8 | 9 | module Item_map = Map.Make(String) 10 | 11 | module Sexp = Sexplib.Sexp 12 | 13 | module Stdune = Stdune 14 | 15 | include Bos 16 | 17 | module Change = struct 18 | type t = 19 | [ `Remove_with_test of OpamPackage.Name.t 20 | | `Add_with_test of OpamPackage.Name.t 21 | | `Add_build_dep of OpamPackage.t 22 | | `Add_test_dep of OpamPackage.t ] 23 | end 24 | 25 | module List = struct 26 | include List 27 | let rec concat_map f = function 28 | | [] -> [] 29 | | x::xs -> prepend_concat_map (f x) f xs 30 | and prepend_concat_map ys f xs = 31 | match ys with 32 | | [] -> concat_map f xs 33 | | y::ys -> y::prepend_concat_map ys f xs 34 | let find_map f l = 35 | let rec find f = function 36 | | [] -> None 37 | | x::tl -> let v = f x in if Option.is_some v then v else find f tl 38 | in find f l 39 | end 40 | 41 | module String = struct 42 | include String 43 | let cat = (^) 44 | end 45 | 46 | module Change_with_hint = struct 47 | type t = Change.t * Dir_set.t 48 | 49 | let pp_name = Fmt.using OpamPackage.Name.to_string Fmt.(quote string) 50 | 51 | let version_to_string = 52 | if Sys.getenv_opt "OPAM_DUNE_LINT_TESTS" = Some "y" then Fun.const "1.0" 53 | else OpamPackage.version_to_string 54 | 55 | let includes_version (c, _) = 56 | match c with 57 | | `Remove_with_test _ 58 | | `Add_with_test _ -> false 59 | | `Add_build_dep _ 60 | | `Add_test_dep _ -> true 61 | 62 | let pp f (c, dirs) = 63 | let dirs = 64 | Dir_set.map (fun path -> if Fpath.is_current_dir path then Fpath.v "/" else path) dirs 65 | in 66 | let change, hint = 67 | match c with 68 | | `Remove_with_test name -> Fmt.str "%a" pp_name name, ["(remove {with-test})"] 69 | | `Add_with_test name -> Fmt.str "%a {with-test}" pp_name name, ["(missing {with-test} annotation)"] 70 | | `Add_build_dep dep -> Fmt.str "%a {>= \"%s\"}" pp_name (OpamPackage.name dep) (version_to_string dep), [] 71 | | `Add_test_dep dep -> Fmt.str "%a {with-test & >= \"%s\"}" pp_name (OpamPackage.name dep) (version_to_string dep), [] 72 | in 73 | let hint = 74 | if Dir_set.is_empty dirs then hint 75 | else Fmt.str "[from @[%a@]]" Fmt.(list ~sep:comma Fpath.pp) (Dir_set.elements dirs) :: hint 76 | in 77 | if hint = [] then 78 | Fmt.string f change 79 | else 80 | Fmt.pf f "@[%-40s %a@]" change Fmt.(list ~sep:sp string) hint 81 | 82 | let remove_hint (t:t) = fst t 83 | end 84 | 85 | let or_die = function 86 | | Ok x -> x 87 | | Error (`Msg m) -> failwith m 88 | 89 | let sexp cmd = 90 | Bos.OS.Cmd.run_out (cmd) 91 | |> Bos.OS.Cmd.to_string 92 | |> or_die 93 | |> String.trim 94 | |> (fun s -> 95 | try Sexp.of_string s with 96 | | Sexp.Parse_error _ as e -> 97 | Fmt.epr "Error parsing '%s' output:\n" (Bos.Cmd.to_string cmd); raise e) 98 | 99 | let csexp cmd = 100 | Bos.OS.Cmd.run_out (cmd) 101 | |> Bos.OS.Cmd.to_string 102 | |> or_die 103 | |> String.trim 104 | |> (fun s -> 105 | match Csexp.parse_string_many s with 106 | | Ok csexp -> csexp 107 | | Error msg -> 108 | Fmt.epr "Error parsing '%s' output:\n%S" (Bos.Cmd.to_string cmd) (snd msg); exit 1) 109 | --------------------------------------------------------------------------------