├── .gitattributes ├── .github └── workflows │ ├── changelog.yml │ └── pr-number.yml ├── .gitignore ├── .ocamlformat ├── CHANGES.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE.md ├── Makefile ├── README.md ├── RELEASING.md ├── bin ├── dune └── opam_monorepo.ml ├── cli ├── common.ml ├── common.mli ├── default.ml ├── default.mli ├── depext.ml ├── depext.mli ├── dune ├── import.ml ├── list_cmd.ml ├── list_cmd.mli ├── lock.ml ├── lock.mli ├── prompt.ml ├── prompt.mli ├── pull.ml └── pull.mli ├── doc ├── concepts.mld ├── dune ├── faq.mld ├── index.mld ├── lock.mld ├── manage-duniverse.mld ├── opam-provided.mld └── workflows.mld ├── dune ├── dune-project ├── dune-workspace ├── lib ├── config.ml ├── dev_repo.ml ├── dev_repo.mli ├── dune ├── dune_file.ml ├── dune_file.mli ├── duniverse.ml ├── duniverse.mli ├── exec.ml ├── exec.mli ├── git.ml ├── git.mli ├── import.ml ├── lockfile.ml ├── lockfile.mli ├── opam.ml ├── opam.mli ├── opam_solve.ml ├── opam_solve.mli ├── parallel.ml ├── parallel.mli ├── persist.ml ├── persist.mli ├── pin_depends.ml ├── pin_depends.mli ├── pp.ml ├── pp.mli ├── pp_combinators.ml ├── pp_combinators.mli ├── project.ml ├── project.mli ├── pull.ml ├── pull.mli ├── serial_shape.ml ├── serial_shape.mli ├── source_opam_config.ml ├── source_opam_config.mli ├── uri_utils.ml └── uri_utils.mli ├── opam-monorepo.opam ├── opam-monorepo.opam.locked ├── opam-monorepo.opam.template ├── stdext ├── dune ├── list.ml ├── list.mli ├── map.ml ├── map.mli ├── option.ml ├── option.mli ├── result.ml ├── result.mli ├── string.ml └── string.mli └── test ├── bin ├── cli-args-in-lockfile.t │ ├── cli-args-in-lockfile.opam │ └── run.t ├── compiler-beta.t │ ├── dont-want-beta.opam │ ├── repo-with-beta-ocaml │ │ ├── packages │ │ │ ├── ocaml-base-compiler │ │ │ │ └── ocaml-base-compiler.4.14.0~beta1 │ │ │ │ │ └── opam │ │ │ └── ocaml │ │ │ │ └── ocaml.4.14.0 │ │ │ │ └── opam │ │ └── repo │ ├── requires-beta.opam │ └── run.t ├── config-cli-args.t │ ├── repo │ │ ├── packages │ │ │ ├── b │ │ │ │ └── b.1 │ │ │ │ │ └── opam │ │ │ └── c │ │ │ │ └── c.1 │ │ │ │ └── opam │ │ └── repo │ └── run.t ├── depext.t │ ├── depext.opam │ ├── repo │ │ ├── packages │ │ │ └── b │ │ │ │ └── b.1 │ │ │ │ └── opam │ │ └── repo │ └── run.t ├── dune ├── empty-duniverse.t │ ├── empty-duniverse.opam │ └── run.t ├── error-on-directory-conflict.t │ ├── repo │ │ └── packages │ │ │ ├── bar │ │ │ └── bar.0.1.0 │ │ │ │ └── opam │ │ │ ├── foo-branch │ │ │ └── foo-branch.0.1.0 │ │ │ │ └── opam │ │ │ └── foo │ │ │ └── foo.0.1.0 │ │ │ └── opam │ ├── run.t │ └── x.opam ├── explicit-repo.t │ ├── explicit-repo.opam │ └── run.t ├── gen_minimal_repo.ml ├── invalid-package-version.t │ ├── existing.opam │ ├── multiple-constraint.opam │ ├── repo │ │ ├── packages │ │ │ ├── a │ │ │ │ ├── a.0.1 │ │ │ │ │ └── opam │ │ │ │ ├── a.1.0 │ │ │ │ │ └── opam │ │ │ │ └── a.1.1 │ │ │ │ │ └── opam │ │ │ └── depends-on-min-a │ │ │ │ └── depends-on-min-a.1 │ │ │ │ └── opam │ │ └── repo │ ├── run.t │ └── toonew.opam ├── local-solver-picks-higher-version.t │ ├── higher-version.opam │ ├── repo │ │ ├── packages │ │ │ └── a │ │ │ │ ├── a.0.1 │ │ │ │ └── opam │ │ │ │ └── a.0.2 │ │ │ │ └── opam │ │ └── repo │ └── run.t ├── lockfile-version.t │ ├── lockfile-version.opam │ └── run.t ├── minimal-update.t │ ├── minimal-update.opam │ ├── repo │ │ ├── packages │ │ │ ├── a │ │ │ │ └── a.0.1 │ │ │ │ │ └── opam │ │ │ ├── b │ │ │ │ └── b.0.1 │ │ │ │ │ └── opam │ │ │ └── c │ │ │ │ └── c.0.1 │ │ │ │ └── opam │ │ └── repo │ └── run.t ├── missing-dev-repo.t │ ├── missing-dev-repo.opam │ ├── repo │ │ ├── packages │ │ │ └── no-dev-repo │ │ │ │ └── no-dev-repo.1 │ │ │ │ └── opam │ │ └── repo │ └── run.t ├── missing-dune-project.t │ ├── missing-dune-project-as-well.opam │ ├── missing-dune-project.opam │ └── run.t ├── no-build-command.t │ ├── repo │ │ ├── packages │ │ │ ├── depend-with-dune │ │ │ │ └── depend-with-dune.0.1 │ │ │ │ │ └── opam │ │ │ ├── depend-without-dune │ │ │ │ └── depend-without-dune.0.1 │ │ │ │ │ └── opam │ │ │ ├── with-dune │ │ │ │ └── with-dune.0.1 │ │ │ │ │ └── opam │ │ │ └── without-dune │ │ │ │ └── without-dune.0.1 │ │ │ │ └── opam │ │ └── repo │ ├── run.t │ ├── test-depend-with-dune.opam │ └── test-depend-without-dune.opam ├── opam-provided.t │ ├── opam-provided.opam │ ├── repo │ │ ├── packages │ │ │ ├── b │ │ │ │ └── b.1 │ │ │ │ │ └── opam │ │ │ └── depends-on-b │ │ │ │ └── depends-on-b.1 │ │ │ │ └── opam │ │ └── repo │ ├── reverse-transitive.opam │ ├── run.t │ ├── transitive.opam │ ├── vendored.opam │ ├── warning-list.opam │ └── warning.opam ├── pinning.t │ ├── opam-monorepo-test-other │ │ └── opam-monorepo-test-other.opam │ ├── run.t │ └── this │ │ └── this.opam ├── preserve-symlinks.t │ ├── preserve-symlinks.opam │ ├── repo │ │ ├── packages │ │ │ └── symlinked-dep │ │ │ │ └── symlinked-dep.1.0 │ │ │ │ └── opam │ │ └── repo │ └── run.t ├── pull-invalid-path.t │ ├── lockfile-refers-to-duniverse-directory.opam.locked │ ├── lockfile-refers-to-parent-directory.opam.locked │ └── run.t ├── recursive-local-package-search.t │ ├── mkdir │ ├── repo │ │ ├── packages │ │ │ └── in-repo │ │ │ │ └── in-repo.0 │ │ │ │ └── opam │ │ └── repo │ ├── root.opam │ ├── run.t │ └── some_dir │ │ └── some-local-pkg.opam ├── require-cross-compiling-packages.t │ ├── mirage-opam-overlays │ │ ├── packages │ │ │ └── b │ │ │ │ └── b.0.1+dune+mirage │ │ │ │ └── opam │ │ └── repo │ ├── opam-overlays │ │ ├── packages │ │ │ └── b │ │ │ │ └── b.0.1+dune │ │ │ │ └── opam │ │ └── repo │ ├── opam-repository │ │ ├── packages │ │ │ ├── b │ │ │ │ └── b.0.1 │ │ │ │ │ └── opam │ │ │ ├── base-bigarray │ │ │ │ └── base-bigarray.base │ │ │ │ │ └── opam │ │ │ ├── base-threads │ │ │ │ └── base-threads.base │ │ │ │ │ └── opam │ │ │ ├── base-unix │ │ │ │ └── base-unix.base │ │ │ │ │ └── opam │ │ │ ├── dune │ │ │ │ └── dune.2.9.1 │ │ │ │ │ └── opam │ │ │ ├── ocaml-base-compiler │ │ │ │ └── ocaml-base-compiler.4.13.1 │ │ │ │ │ └── opam │ │ │ ├── ocaml-config │ │ │ │ └── ocaml-config.2 │ │ │ │ │ └── opam │ │ │ ├── ocaml-options-vanilla │ │ │ │ └── ocaml-options-vanilla.1 │ │ │ │ │ └── opam │ │ │ └── ocaml │ │ │ │ └── ocaml.4.13.1 │ │ │ │ └── opam │ │ └── repo │ ├── run.t │ ├── with-mirage.opam │ └── without-mirage.opam └── simple-lock.t │ ├── repo │ ├── packages │ │ ├── b │ │ │ └── b.1 │ │ │ │ └── opam │ │ └── c │ │ │ └── c.1 │ │ │ └── opam │ └── repo │ ├── run.t │ └── simple-lock.opam └── lib ├── dune ├── test_dev_repo.ml ├── test_dev_repo.mli ├── test_dune_file.ml ├── test_dune_file.mli ├── test_duniverse.ml ├── test_duniverse.mli ├── test_duniverse_lib.ml ├── test_duniverse_lib.mli ├── test_git.ml ├── test_git.mli ├── test_opam.ml ├── test_opam.mli ├── test_parallel.ml ├── test_parallel.mli ├── test_pin_depends.ml ├── test_pin_depends.mli ├── test_serial_shape.ml ├── test_serial_shape.mli ├── test_solve.ml ├── test_solve.mli ├── test_source_opam_config.ml ├── test_source_opam_config.mli ├── test_uri_utils.ml ├── test_uri_utils.mli ├── testable.ml └── testable.mli /.gitattributes: -------------------------------------------------------------------------------- 1 | duniverse/** linguist-generated=true 2 | -------------------------------------------------------------------------------- /.github/workflows/changelog.yml: -------------------------------------------------------------------------------- 1 | name: Changelog check 2 | 3 | on: 4 | pull_request: 5 | branches: [ main ] 6 | types: [ opened, synchronize, reopened, labeled, unlabeled ] 7 | 8 | jobs: 9 | Changelog-Entry-Check: 10 | name: Check Changelog Action 11 | runs-on: ubuntu-20.04 12 | steps: 13 | - uses: tarides/changelog-check-action@v1 14 | -------------------------------------------------------------------------------- /.github/workflows/pr-number.yml: -------------------------------------------------------------------------------- 1 | name: PR number update 2 | on: [pull_request_target] 3 | jobs: 4 | PR-Number-Update: 5 | runs-on: ubuntu-20.04 6 | steps: 7 | - name: Update PR number 8 | uses: tarides/pr-number-action@v1.1 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | _build 2 | .merlin 3 | *.install 4 | examples/**/dune-get 5 | examples/**/duniverse 6 | .DS_Store 7 | .*.swp 8 | _opam 9 | *.lock 10 | .vscode 11 | _ocaml 12 | duniverse 13 | -------------------------------------------------------------------------------- /.ocamlformat: -------------------------------------------------------------------------------- 1 | version=0.26.1 2 | profile=conventional 3 | ocaml-version=4.10 4 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | This project has adopted the [OCaml Code of Conduct](https://github.com/ocaml/code-of-conduct/blob/main/CODE_OF_CONDUCT.md). 4 | 5 | # Enforcement 6 | 7 | This project follows the OCaml Code of Conduct [enforcement policy](https://github.com/ocaml/code-of-conduct/blob/main/CODE_OF_CONDUCT.md#enforcement). 8 | 9 | To report any violations, please contact: 10 | 11 | * Marek Kubica 12 | * Steve Sherratt 13 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to `opam-monorepo` 2 | 3 | ## Setting Up Your Development Environment 4 | 5 | You can clone `opam-monorepo` by running: 6 | ``` 7 | git clone git@github.com:ocamllabs/opam-monorepo.git 8 | cd opam-monorepo 9 | ``` 10 | 11 | `opam-monorepo` uses itself to lock its dependencies. These are saved in 12 | `opam-monorepo.opam.locked`. 13 | 14 | You will need to setup a local switch and install Dune and OCaml as specified 15 | by the lock file: 16 | ``` 17 | opam switch create --empty ./ 18 | opam install --deps-only --ignore-pin-depends ./opam-monorepo.opam.locked 19 | ``` 20 | 21 | You will need to fetch the of the dependencies sources locally to be able to 22 | build and run the test with the exact same libraries used by the CI. To do so, 23 | run: 24 | ``` 25 | opam monorepo pull 26 | ``` 27 | 28 | If you don't already have the plugin installed, opam will suggest that you install 29 | it. Once this is done it will proceed with actually running the plugin command. 30 | `opam monorepo pull` will fetch all the sources and properly set them up in a 31 | `duniverse/` folder. 32 | 33 | Note that this repository's lock file should be handled using the version of 34 | `opam-monorepo` corresponding to the lockfile. This will always correspond 35 | to a released version. We do not use the dev version of opam-monorepo to prevent 36 | bootstrapping issues. 37 | 38 | From there you can now run the usual Dune commands to build and run the tests: 39 | ``` 40 | dune build 41 | dune runtest 42 | ``` 43 | 44 | Remember to run the above mentioned steps often (e.g., every time you checkout a 45 | branch) so that your local `duniverse/` and switch stay up to date! 46 | 47 | ## Formatting 48 | 49 | This repository uses Dune and OCamlformat to format `dune`, `.ml` and `.mli` files. 50 | 51 | To format everything run: 52 | ``` 53 | dune build @fmt --auto-promote 54 | ``` 55 | 56 | Remember to run this command before you commit any change! 57 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 Anil Madhavapeddy and other authors 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 | .PHONY: build clean test doc install format 2 | 3 | PREFIX ?= /usr/local/bin 4 | 5 | build: 6 | dune build 7 | 8 | clean: 9 | dune clean 10 | 11 | doc: 12 | dune build @doc 13 | 14 | test: 15 | dune runtest 16 | 17 | docker-test: 18 | DOCKER_BUILDKIT=1 BUILDKIT_PROGRESS=plain docker build -f Dockerfile.test . 19 | 20 | install: 21 | mkdir -p $(PREFIX) 22 | cp _build/install/default/bin/duniverse $(PREFIX)/duniverse 23 | 24 | update: build 25 | dune exec -- duniverse init duniverse alcotest -vvv 26 | rm -rf duniverse 27 | dune exec -- duniverse pull 28 | 29 | format: 30 | - dune build @fmt 2> /dev/null 31 | dune promote 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://img.shields.io/endpoint?url=https%3A%2F%2Fci.ocamllabs.io%2Fbadge%2Ftarides%2Fopam-monorepo%2Fmain&logo=ocaml)](https://ci.ocamllabs.io/github/tarides/opam-monorepo) [![Documentation](https://img.shields.io/badge/doc-online-blue)](https://tarides.github.io/opam-monorepo/opam-monorepo/index.html) 2 | 3 | # `opam-monorepo` 4 | 5 | `opam-monorepo` is an opam plugin designed to assemble standalone Dune workspaces 6 | with your projects and all of their dependencies, letting you build it all from scratch 7 | using only `dune` and `ocaml`. 8 | 9 | [Documentation on `opam-monorepo`](https://tarides.github.io/opam-monorepo/opam-monorepo/index.html) 10 | is available in the repository as well as available online. 11 | 12 | ## Installation 13 | 14 | You can simply install it via opam in your current switch by running: 15 | 16 | ``` 17 | opam install opam-monorepo 18 | ``` 19 | 20 | Note that once it's installed you can invoke it as an opam command: 21 | ``` 22 | opam monorepo ... 23 | ``` 24 | 25 | Note that opam knows about available plugins and will offer to install `opam-monorepo` 26 | if you try to invoke it without having installed it beforehand. 27 | 28 | ## Usage 29 | 30 | The basic usage for `opam-monorepo` is to start by running the following command from the root 31 | of your project: 32 | ``` 33 | opam monorepo lock 34 | ``` 35 | 36 | This will generate a project-wide lock file under `.opam.locked` that contains: 37 | - The full list of your direct and transitive opam dependencies, according to the specifications 38 | in the `.opam` files at your project's root, with hard version constraints (`{= `) in 39 | the `depends` field. 40 | - A `pin-depends` field filled with reproducible pins, either to tarballs or to Git repos with an 41 | explicit commit hash for all the above dependencies, except for `ocaml`, `dune`, and any virtual 42 | opam package. 43 | - A few extra fields meant to be interprated by `opam monorepo` 44 | 45 | This lock file can then be consumed by the following command: 46 | ``` 47 | opam monorepo pull 48 | ``` 49 | which will fetch the sources of all the locked dependencies into a `duniverse/` folder at the root of 50 | your project, marking them as `vendored_dirs` (see 51 | [Dune's documentation](https://dune.readthedocs.io/en/latest/dune-files.html#vendored-dirs-since-1-11)) 52 | so that Dune will only build the artifacts you need from that folder. 53 | 54 | From that point you should be able to run `dune build` and `dune runtest` as you normally would and 55 | build your entire project from scratch! 56 | 57 | ### The `lock` Command 58 | 59 | It's important to note that `opam monorepo lock` will only succeed if all of your non-virtual and 60 | non "base" dependencies (e.g., `ocaml` or `dune`) build with Dune (i.e., directly depend on the `dune` 61 | or `jbuilder` packages). 62 | If that's not the case, the solver will report which packages don't build with dune. 63 | 64 | We maintain a [separate opam repository](https://github.com/dune-universe/opam-overlays) with Dune 65 | ports of commonly used opam packages. If you have non-Dune dependencies, we advise adding 66 | this repository before running `opam monorepo lock`. You can do so by running the following command: 67 | ``` 68 | opam repository add dune-universe git+https://github.com/dune-universe/opam-overlays.git 69 | ``` 70 | Note that if it's not setup, the plugin will warn you. 71 | 72 | The `lock` command takes your global and switch's opam configurations into account, meaning any 73 | opam repository or pins you set up will be picked up by the solver when resolving the full set of 74 | your project's dependencies. 75 | 76 | The generated lock file is meant to be compatible with `opam` in such a way that running 77 | 78 | ``` 79 | opam install . --locked 80 | ``` 81 | 82 | should give you the same versions you would using `opam monorepo pull` in 83 | a reproducible way (i.e., independently of any change that might have happened on the upstream 84 | `opam-repository`) thanks to the `pin-depends`. 85 | You can use that property to your advantage by allowing one to choose between a "monorepo" or a 86 | regular opam workflow depending on the situation. 87 | 88 | You can also exclude packages from the set of packages to 89 | be vendored by `opam-monorepo`. To do so, specify an additional field in 90 | your opam file: 91 | 92 | ``` 93 | x-opam-monorepo-opam-provided: ["ocamlformat" "patdiff"] 94 | ``` 95 | 96 | This will exclude the packages from the list of packages `opam-monorepo` will 97 | pull, so they can be installed via `opam` manually. 98 | 99 | ### `opam monorepo pull` 100 | 101 | The `pull` command fetches the sources using the URLs in the lock file. It benefits from the opam 102 | cache but its outcome does not depend on your opam configuration. 103 | 104 | ## Monorepo Projects 105 | 106 | If you wish to use `opam-monorepo` to manage your dependencies, we suggest that you Git version the 107 | lock file but not the content of the `duniverse/`. 108 | 109 | If you use [ocaml-ci](https://github.com/ocurrent/ocaml-ci) and have an `opam-monorepo` lock file at 110 | the root of your project, it will detect it is an `opam-monorepo` project and start a specific 111 | pipeline. It will use the plugin to assemble a Dune workspace with your dependencies rather 112 | than installing them through opam. 113 | -------------------------------------------------------------------------------- /bin/dune: -------------------------------------------------------------------------------- 1 | (executable 2 | (name opam_monorepo) 3 | (public_name opam-monorepo) 4 | (libraries cmdliner duniverse_lib duniverse_cli)) 5 | -------------------------------------------------------------------------------- /bin/opam_monorepo.ml: -------------------------------------------------------------------------------- 1 | (* Copyright (c) 2018 Anil Madhavapeddy 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 | * 15 | *) 16 | 17 | open Duniverse_cli 18 | 19 | let cmds = [ Lock.cmd; Pull.cmd; Depext.cmd; List_cmd.cmd ] 20 | 21 | let init_opam () = 22 | OpamSystem.init (); 23 | let root = OpamStateConfig.opamroot () in 24 | ignore (OpamStateConfig.load_defaults ~lock_kind:`Lock_read root); 25 | OpamFormatConfig.init (); 26 | OpamCoreConfig.init ~safe_mode:true (); 27 | OpamRepositoryConfig.init (); 28 | OpamStateConfig.init ~root_dir:root () 29 | 30 | let default_run () = `Help (`Pager, None) 31 | let default = Cmdliner.Term.(ret (const default_run $ const ())) 32 | 33 | let info = 34 | let open Cmdliner in 35 | let doc = "the spice of build life" in 36 | let sdocs = Manpage.s_common_options in 37 | let man_xrefs = [ `Tool "dune"; `Tool "opam"; `Tool "git" ] in 38 | let man = 39 | [ 40 | `S Manpage.s_description; 41 | `P 42 | "The $(tname) plugin provides a convenient interface to bridge the \ 43 | $(b,opam) package manager with having a local copy of all the source \ 44 | code required to build a project using the $(b,dune) build tool."; 45 | `P 46 | "It works by analysing opam package metadata and calculating a set of \ 47 | URLs that can be downloaded or cloned into the local repository into \ 48 | a $(b,duniverse/) subdirectory. Once the external code has been \ 49 | pulled into the repository, a single $(b,dune build) command is \ 50 | sufficient to build the whole project in a standalone fashion, \ 51 | without opam being required. This is a particularly convenient way of \ 52 | publishing CLI tools to users who do not need the full power of opam."; 53 | `P 54 | "You can access the functionality directly via the $(i,monorepo-lock) \ 55 | and $(i,monorepo-pull) commands,"; 56 | `P 57 | "Also see $(i,https://github.com/realworldocaml/book) for an example \ 58 | of the usage of this tool."; 59 | ] 60 | in 61 | Cmd.info "opam-monorepo" ~version:Common.Arg.version ~doc ~man_xrefs ~sdocs 62 | ~man 63 | 64 | let main = Cmdliner.Cmd.group ~default info cmds 65 | 66 | let () = 67 | init_opam (); 68 | Stdlib.exit @@ Cmdliner.Cmd.eval' main 69 | -------------------------------------------------------------------------------- /cli/common.ml: -------------------------------------------------------------------------------- 1 | open Import 2 | 3 | module Arg = struct 4 | let named f = Cmdliner.Term.(app (const f)) 5 | let fpath = Cmdliner.Arg.conv ~docv:"PATH" (Fpath.of_string, Fpath.pp) 6 | 7 | let root = 8 | let doc = 9 | "Use this directory as the project root instead of the current working \ 10 | directory." 11 | in 12 | named 13 | (fun x -> `Root x) 14 | Cmdliner.Arg.( 15 | value 16 | & opt fpath (Fpath.v (Sys.getcwd ())) 17 | & info [ "r"; "root" ] ~docv:"DIR" ~doc) 18 | 19 | let lockfile = 20 | let doc = 21 | "Path to the lockfile to use or generate. Defaults \ 22 | $(b,.opam.locked)" 23 | in 24 | named 25 | (fun x -> `Lockfile x) 26 | Cmdliner.Arg.( 27 | value & opt (some fpath) None & info [ "l"; "lockfile" ] ~doc) 28 | 29 | let yes = 30 | let doc = "Do not prompt for confirmation and always assume yes" in 31 | named 32 | (fun x -> `Yes x) 33 | Cmdliner.Arg.(value & flag & info [ "y"; "yes" ] ~doc) 34 | 35 | let non_empty_list_opt = 36 | Cmdliner.Term.const (function [] -> None | l -> Some l) 37 | 38 | let keep_git_dir = 39 | let doc = 40 | "Keep the `.git' directory if the pulled vendored source is a git \ 41 | repository." 42 | in 43 | named 44 | (fun x -> `Keep_git_dir x) 45 | Cmdliner.Arg.(value & flag & info [ "keep-git-dir" ] ~doc) 46 | 47 | let keep_symlinked_dir = 48 | let doc = 49 | "Preserve directories that are symlinked. This can be useful when \ 50 | working on the dependencies of projects." 51 | in 52 | named 53 | (fun x -> `Keep_symlinked_dir x) 54 | Cmdliner.Arg.(value & flag & info [ "keep-symlinked-dir" ] ~doc) 55 | 56 | let duniverse_repos = 57 | let open Cmdliner in 58 | let docv = "REPOSITORIES" in 59 | let doc = 60 | "The list of $(docv) from your duniverse to process. If none is \ 61 | provided, all will be processed." 62 | in 63 | named 64 | (fun x -> `Duniverse_repos x) 65 | Term.( 66 | non_empty_list_opt 67 | $ Arg.(value & pos_all string [] & info ~doc ~docv [])) 68 | 69 | let thread_safe_reporter reporter = 70 | let lock = Mutex.create () in 71 | let { Logs.report } = reporter in 72 | let oui src level ~over k msgf = 73 | Mutex.lock lock; 74 | let x = report src level ~over k msgf in 75 | Mutex.unlock lock; 76 | x 77 | in 78 | Logs.{ report = oui } 79 | 80 | let setup_logs () = 81 | Printexc.record_backtrace true; 82 | let setup_log style_renderer level = 83 | Fmt_tty.setup_std_outputs ?style_renderer (); 84 | Logs.set_level level; 85 | Logs.set_reporter (thread_safe_reporter (Logs_fmt.reporter ())) 86 | in 87 | let global_option_section = "COMMON OPTIONS" in 88 | let open Cmdliner.Term in 89 | const setup_log 90 | $ Fmt_cli.style_renderer ~docs:global_option_section () 91 | $ Logs_cli.level ~docs:global_option_section () 92 | 93 | let version = 94 | match Build_info.V1.version () with 95 | | None -> "n/a" 96 | | Some v -> Build_info.V1.Version.to_string v 97 | end 98 | 99 | let regular_error_exit = 100 | Cmdliner.Cmd.Exit.info ~doc:"on regular opam-monorepo errors" 1 101 | 102 | let exit_codes = regular_error_exit :: Cmdliner.Cmd.Exit.defaults 103 | 104 | module Term = struct 105 | let result_to_exit term = 106 | let to_exit result = 107 | match result with 108 | | Ok () -> 0 109 | | Error (`Msg msg) -> 110 | Logs.err (fun l -> l "%s" msg); 111 | Cmdliner.Cmd.Exit.info_code regular_error_exit 112 | in 113 | Cmdliner.Term.(const to_exit $ term) 114 | end 115 | 116 | module Logs = struct 117 | let app ?src f = 118 | Logs.app ?src (fun l -> 119 | f (fun ?header ?tags fmt -> 120 | l ?header ?tags ("%a" ^^ fmt) D.Pp.Styled.header ())) 121 | end 122 | 123 | (** Filters the duniverse according to the CLI provided list of repos *) 124 | let filter_duniverse ~to_consider (duniverse : D.Duniverse.t) = 125 | match to_consider with 126 | | None -> Ok duniverse 127 | | Some to_consider -> ( 128 | let repos_map = 129 | String.Map.of_list_map_exn duniverse ~f:(fun src -> (src.dir, src)) 130 | in 131 | let unmatched, found = 132 | List.partition_map to_consider ~f:(fun asked -> 133 | match String.Map.find repos_map asked with 134 | | None -> Either.Left asked 135 | | Some found -> Either.Right found) 136 | in 137 | match unmatched with 138 | | [] -> Ok found 139 | | _ -> 140 | let sep fmt () = Fmt.pf fmt " " in 141 | Rresult.R.error_msgf 142 | "The following repos are not in your duniverse: %a" 143 | Fmt.(list ~sep string) 144 | unmatched) 145 | 146 | let find_lockfile_aux ~explicit_lockfile repo = 147 | let open Result.O in 148 | match explicit_lockfile with 149 | | Some file -> Ok file 150 | | None -> ( 151 | let* local_lockfiles = D.Project.local_lockfiles repo in 152 | match local_lockfiles with 153 | | [] -> 154 | Rresult.R.error_msg 155 | "No lockfile: try running `opam monorepo lock` first" 156 | | [ file ] -> Ok file 157 | | lockfile_paths -> 158 | let sep = Fmt.(const char ' ') in 159 | Rresult.R.error_msgf 160 | "Found several lockfiles: %a\n\ 161 | Please pick one explicitly using the %a option" 162 | (Fmt.list ~sep D.Pp.Styled.path) 163 | lockfile_paths 164 | Fmt.(styled `Bold string) 165 | "--lockfile") 166 | 167 | let find_lockfile ~explicit_lockfile ?(quiet = false) root = 168 | let open Result.O in 169 | find_lockfile_aux ~explicit_lockfile root >>= fun file -> 170 | if not quiet then 171 | Logs.app (fun l -> l "Using lockfile %a" D.Pp.Styled.path file); 172 | D.Lockfile.load ~opam_monorepo_cwd:root ~file 173 | -------------------------------------------------------------------------------- /cli/common.mli: -------------------------------------------------------------------------------- 1 | open Duniverse_lib 2 | 3 | module Logs : sig 4 | val app : ?src:Logs.src -> 'a Logs.log 5 | (** Formats the arguments and logs the resulting message with app level, preceded by a sexy looking 6 | ["==> "] blue header. *) 7 | end 8 | 9 | module Arg : sig 10 | val named : ('a -> 'b) -> 'a Cmdliner.Term.t -> 'b Cmdliner.Term.t 11 | (** Use this to wrap your arguments in a polymorphic variant constructor to avoid 12 | confusion when they are later passed to your main function. 13 | Example: [named (fun x -> `My_arg x] Arg.(value ...)] *) 14 | 15 | val fpath : Fpath.t Cmdliner.Arg.conv 16 | 17 | val root : [ `Root of Fpath.t ] Cmdliner.Term.t 18 | (** CLI option to specify the root directory of the project. Used to find root packages, 19 | duniverse files and directories. Defaults to the current directory. *) 20 | 21 | val lockfile : [ `Lockfile of Fpath.t option ] Cmdliner.Term.t 22 | (** CLI option to specify the path to the lockfile to use or generate. *) 23 | 24 | val yes : [ `Yes of bool ] Cmdliner.Term.t 25 | (** CLI flag to skip any prompt and perform actions straight away. The value of this flag 26 | must be passed to [Prompt.confirm]. *) 27 | 28 | val duniverse_repos : 29 | [ `Duniverse_repos of string list option ] Cmdliner.Term.t 30 | (** CLI arguments consisting of the list of source deps repo to process. If [None], 31 | the whole duniverse should be processed. If [Some l] then [l] is non empty. *) 32 | 33 | val keep_git_dir : [ `Keep_git_dir of bool ] Cmdliner.Term.t 34 | (** CLI flag to keep the [.git] directory after pulling the vendored sources. *) 35 | 36 | val keep_symlinked_dir : [ `Keep_symlinked_dir of bool ] Cmdliner.Term.t 37 | (** CLI flag to preserve vendored directories that are symlinked. *) 38 | 39 | val setup_logs : unit -> unit Cmdliner.Term.t 40 | (** Adds the common options -v and --version and sets up the logs before being passed as [()] to a 41 | command. *) 42 | 43 | val version : string 44 | (** CLI version string *) 45 | end 46 | 47 | module Term : sig 48 | val result_to_exit : 49 | (unit, [< `Msg of string ]) result Cmdliner.Term.t -> int Cmdliner.Term.t 50 | (** Converts a [_ result Term.t] to an [int Term.t] embedding the right exit 51 | status. *) 52 | end 53 | 54 | val exit_codes : Cmdliner.Cmd.Exit.info list 55 | 56 | val filter_duniverse : 57 | to_consider:string list option -> 58 | Duniverse.t -> 59 | (Duniverse.t, Rresult.R.msg) result 60 | (** Filters the duniverse according to the CLI provided list of repos or returns an error 61 | if some of the provided packages don't match any of the duniverse repositories. *) 62 | 63 | val find_lockfile : 64 | explicit_lockfile:Fpath.t option -> 65 | ?quiet:bool -> 66 | Fpath.t -> 67 | (Lockfile.t, Rresult.R.msg) result 68 | -------------------------------------------------------------------------------- /cli/default.ml: -------------------------------------------------------------------------------- 1 | let run () = `Help (`Pager, None) 2 | let term = Cmdliner.Term.(ret (const run $ const ())) 3 | -------------------------------------------------------------------------------- /cli/default.mli: -------------------------------------------------------------------------------- 1 | val term : unit Cmdliner.Term.t 2 | -------------------------------------------------------------------------------- /cli/depext.ml: -------------------------------------------------------------------------------- 1 | open Import 2 | 3 | let should_install ~yes pkgs = 4 | Prompt.confirm 5 | ~question:(fun l -> 6 | l 7 | "@[The following packages will be installed through %a:@ \ 8 | %a@].@.Do you want to continue?" 9 | Fmt.(styled `Underline string) 10 | "sudo" 11 | Fmt.(list ~sep:(any "@ ") (styled `Bold string)) 12 | pkgs) 13 | ~yes 14 | 15 | let available_packages config pkgs = 16 | match OpamSysInteract.packages_status config pkgs with 17 | | available_pkgs, _not_found_pkgs -> Ok available_pkgs 18 | | exception Failure msg -> Error (`Msg msg) 19 | 20 | let run (`Root root) (`Lockfile explicit_lockfile) dry_run (`Yes yes) () = 21 | let open Result.O in 22 | let* lockfile = Common.find_lockfile ~explicit_lockfile root in 23 | let depexts = D.Lockfile.depexts lockfile in 24 | OpamGlobalState.with_ `Lock_none (fun global_state -> 25 | let env = OpamPackageVar.resolve_global global_state in 26 | let pkgs = 27 | List.fold_left 28 | ~f:(fun acc (pkgs, f) -> 29 | if OpamFilter.eval_to_bool ~default:true env f then 30 | OpamSysPkg.Set.union acc pkgs 31 | else acc) 32 | ~init:OpamSysPkg.Set.empty depexts 33 | in 34 | let* pkgs = available_packages global_state.config pkgs in 35 | match OpamSysPkg.Set.elements pkgs with 36 | | [] -> Ok () 37 | | pkgs_list -> 38 | let pkgs_str = List.map ~f:OpamSysPkg.to_string pkgs_list in 39 | if dry_run then ( 40 | Fmt.pr "%s\n%!" (String.concat ~sep:" " pkgs_str); 41 | Ok ()) 42 | else if should_install ~yes pkgs_str then 43 | try 44 | OpamCoreConfig.update ~confirm_level:`unsafe_yes (); 45 | OpamSysInteract.install global_state.config pkgs; 46 | Ok () 47 | with Failure msg -> Error (`Msg msg) 48 | else Ok ()) 49 | 50 | open Cmdliner 51 | 52 | let info = 53 | let exits = Common.exit_codes in 54 | let doc = Fmt.str "install external dependencies" in 55 | let man = 56 | [ 57 | `S Manpage.s_description; 58 | `P 59 | "This command installs the external dependencies listed in the \ 60 | lockfile."; 61 | ] 62 | in 63 | Cmd.info "depext" ~doc ~exits ~man 64 | 65 | let dry_run = 66 | let doc = 67 | Arg.info ~doc:"Display the system packages instead of installing them." 68 | [ "dry-run" ] 69 | in 70 | Arg.(value & flag doc) 71 | 72 | let term = 73 | let open Term in 74 | Common.Term.result_to_exit 75 | (const run $ Common.Arg.root $ Common.Arg.lockfile $ dry_run 76 | $ Common.Arg.yes $ Common.Arg.setup_logs ()) 77 | 78 | let cmd = Cmd.v info term 79 | -------------------------------------------------------------------------------- /cli/depext.mli: -------------------------------------------------------------------------------- 1 | val cmd : int Cmdliner.Cmd.t 2 | -------------------------------------------------------------------------------- /cli/dune: -------------------------------------------------------------------------------- 1 | (library 2 | (name duniverse_cli) 3 | (libraries 4 | duniverse_lib 5 | fmt.cli 6 | fmt.tty 7 | logs.cli 8 | logs.fmt 9 | cmdliner 10 | dune-build-info 11 | ocaml-version 12 | opam-state)) 13 | -------------------------------------------------------------------------------- /cli/import.ml: -------------------------------------------------------------------------------- 1 | include Stdext 2 | module D = Duniverse_lib 3 | -------------------------------------------------------------------------------- /cli/list_cmd.ml: -------------------------------------------------------------------------------- 1 | open Import 2 | 3 | type t = { 4 | name : OpamPackage.Name.t; 5 | version : OpamPackage.Version.t; 6 | loc : string; 7 | pinned : bool; 8 | descr : string option; 9 | } 10 | 11 | (* Assumes max_len was correctly computed, otherwise the input string might be 12 | truncated. *) 13 | let pad s max_len = Printf.sprintf "%-*s" max_len s 14 | 15 | let guess_pin ~version ~loc = 16 | let version = OpamPackage.Version.to_string version in 17 | (* opam-overlays *) 18 | String.ends_with ~suffix:"+dune" version 19 | || String.starts_with ~prefix:"https://github.com/dune-universe" loc 20 | (* mirage-overlays *) 21 | || String.ends_with ~suffix:"+mirage" version 22 | 23 | let pp_name = Fmt.(styled `Bold string) 24 | let pp_version = Fmt.(styled `Magenta string) 25 | let pp_pin_version = Fmt.(styled `Blue string) 26 | 27 | let pp_pin_loc ppf s = 28 | Fmt.pf ppf "pinned at %a" Fmt.(styled `Underline string) s 29 | 30 | let compare_pkg x y = OpamPackage.Name.compare x.name y.name 31 | 32 | let pp ~max_name ~max_version ~short ppf t = 33 | if short then Duniverse_lib.Opam.Pp.package_name ppf t.name 34 | else 35 | let padded_name = pad (OpamPackage.Name.to_string t.name) max_name in 36 | let padded_version = 37 | pad (OpamPackage.Version.to_string t.version) max_version 38 | in 39 | if t.pinned then 40 | Fmt.pf ppf "%a %a %a" pp_name padded_name pp_pin_version padded_version 41 | pp_pin_loc t.loc 42 | else 43 | Fmt.pf ppf "%a %a %s" pp_name padded_name pp_version padded_version 44 | (match t.descr with None -> "--" | Some d -> d) 45 | 46 | let pkgs_of_repo (t : D.Duniverse.resolved D.Duniverse.Repo.t) = 47 | List.map 48 | ~f:(fun (pkg : OpamPackage.t) -> 49 | let name = pkg.name in 50 | let version = pkg.version in 51 | let loc = D.Duniverse.Repo.Url.to_string t.url in 52 | let pinned = 53 | match t.url with Git _ -> true | _ -> guess_pin ~version ~loc 54 | in 55 | let descr = None in 56 | { name; version; descr; pinned; loc }) 57 | t.provided_packages 58 | 59 | let pkgs_of_duniverse t = 60 | let pkgs = List.map ~f:pkgs_of_repo t in 61 | let pkgs = List.flatten pkgs in 62 | List.sort ~cmp:compare_pkg pkgs 63 | 64 | let with_descr pkgs = 65 | OpamGlobalState.with_ `Lock_none (fun global_state -> 66 | OpamSwitchState.with_ `Lock_none global_state (fun switch_state -> 67 | List.map 68 | ~f:(fun pkg -> 69 | let opam = OpamPackage.create pkg.name pkg.version in 70 | match OpamSwitchState.opam switch_state opam with 71 | | opam -> { pkg with descr = OpamFile.OPAM.synopsis opam } 72 | | exception Not_found -> 73 | (* This pkg.version doesn't exist in the switch and therefore 74 | comes from a pin *) 75 | { pkg with pinned = true }) 76 | pkgs)) 77 | 78 | let run (`Root root) (`Lockfile explicit_lockfile) short () = 79 | let open Result.O in 80 | let* lockfile = Common.find_lockfile ~explicit_lockfile ~quiet:short root in 81 | let+ duniverse = D.Lockfile.to_duniverse lockfile in 82 | let pkgs = pkgs_of_duniverse duniverse in 83 | let pkgs = with_descr pkgs in 84 | let max_name, max_version = 85 | List.fold_left 86 | ~f:(fun (max_name, max_version) t -> 87 | ( max (String.length (OpamPackage.Name.to_string t.name)) max_name, 88 | max 89 | (String.length (OpamPackage.Version.to_string t.version)) 90 | max_version )) 91 | ~init:(0, 0) pkgs 92 | in 93 | let pp = pp ~max_name ~max_version ~short in 94 | if not short then 95 | Common.Logs.app (fun l -> 96 | let duniverse = Fpath.(v (Sys.getcwd ()) // D.Config.vendor_dir / "") in 97 | l "The vendor directory is %a" D.Pp.Styled.path duniverse); 98 | List.iter ~f:(fun pkg -> Fmt.pr "%a\n" pp pkg) pkgs 99 | 100 | open Cmdliner 101 | 102 | let short = 103 | let doc = 104 | Arg.info ~doc:"Don't print a header, and only display package names." 105 | [ "short"; "s" ] 106 | in 107 | Arg.(value & flag doc) 108 | 109 | let info = 110 | let exits = Common.exit_codes in 111 | let doc = Fmt.str "Display the list of monorepo packages" in 112 | let man = 113 | [ 114 | `S Manpage.s_description; 115 | `P 116 | "List the opam packages present in the lockfile that must be installed \ 117 | in source mode in the duniverse."; 118 | `P 119 | "Unless the --short switch is used, the output format displays one \ 120 | package per line, and each line contains the name of the package, the \ 121 | installed version and a source location. In color mode, pinned \ 122 | packages and packages defined in overlays have a blue version."; 123 | ] 124 | in 125 | Cmd.info "list" ~doc ~exits ~man 126 | 127 | let term = 128 | let open Term in 129 | Common.Term.result_to_exit 130 | (const run $ Common.Arg.root $ Common.Arg.lockfile $ short 131 | $ Common.Arg.setup_logs ()) 132 | 133 | let cmd = Cmd.v info term 134 | -------------------------------------------------------------------------------- /cli/list_cmd.mli: -------------------------------------------------------------------------------- 1 | val cmd : int Cmdliner.Cmd.t 2 | -------------------------------------------------------------------------------- /cli/lock.mli: -------------------------------------------------------------------------------- 1 | val cmd : int Cmdliner.Cmd.t 2 | -------------------------------------------------------------------------------- /cli/prompt.ml: -------------------------------------------------------------------------------- 1 | let ask f = 2 | Logs.app (fun l -> 3 | f (fun ?header ?tags fmt -> 4 | l ?header ?tags 5 | ("%a" ^^ fmt ^^ " [Y/n]") 6 | Duniverse_lib.Pp.Styled.question_header ())) 7 | 8 | let confirm ~question ~yes = 9 | let rec loop () = 10 | ask question; 11 | match String.lowercase_ascii (read_line ()) with 12 | | "" | "y" | "yes" -> true 13 | | "n" | "no" -> false 14 | | _ -> 15 | Logs.app (fun l -> 16 | l 17 | "Please answer with \"y\" for yes, \"n\" for no or just hit \ 18 | enter for the default"); 19 | loop () 20 | in 21 | if yes then true else loop () 22 | 23 | let confirm_or_abort ~question ~yes = 24 | if confirm ~question ~yes then Ok () 25 | else Error (`Msg "Aborting on user demand") 26 | -------------------------------------------------------------------------------- /cli/prompt.mli: -------------------------------------------------------------------------------- 1 | val confirm : question:('a, unit) Logs.msgf -> yes:bool -> bool 2 | (** Promtps the user for confirmation. 3 | [confirm ~question ~yes] uses the message formatting function [question] to format and log 4 | a message with the app level and wait for a yes or no answer from the user. 5 | Returns [true] for yes. Defaults to yes if the user just press enter. 6 | If [yes] then just skip the prompt and returns [true]. 7 | E.g. [confirm ~question:(fun l -> l "Do you want some %a?" Fmt.(styled `Bold string) "coffee")] *) 8 | 9 | val confirm_or_abort : 10 | question:('a, unit) Logs.msgf -> yes:bool -> (unit, Rresult.R.msg) result 11 | (** Same as [confirm] but returns [Ok ()] for yes and [Error (`Msg "Aborting on user demand")] for 12 | no *) 13 | -------------------------------------------------------------------------------- /cli/pull.ml: -------------------------------------------------------------------------------- 1 | open Import 2 | 3 | let min_dune_ver = D.Dune_file.Lang.duniverse_minimum_version 4 | 5 | let should_update_lang ~yes () = 6 | Prompt.confirm 7 | ~question:(fun l -> l "Should I update your dune-project?") 8 | ~yes 9 | 10 | let log_version_update ~dune_project_path = 11 | Common.Logs.app (fun l -> 12 | l "Setting dune language version to %a in %a" D.Dune_file.Lang.pp_version 13 | min_dune_ver D.Pp.Styled.path dune_project_path) 14 | 15 | let suggest_updating_version ~yes ~version ~dune_project_path ~content = 16 | let pp_current = D.Pp.Styled.bad D.Dune_file.Lang.pp_version in 17 | let pp_required = D.Pp.Styled.good D.Dune_file.Lang.pp_version in 18 | Common.Logs.app (fun l -> 19 | l "You are using version %a of the dune language" pp_current version); 20 | Common.Logs.app (fun l -> 21 | l "Duniverse requires version %a or above" pp_required min_dune_ver); 22 | if should_update_lang ~yes () then ( 23 | let updated = D.Dune_file.Lang.update ~version:min_dune_ver content in 24 | log_version_update ~dune_project_path; 25 | Bos.OS.File.write dune_project_path updated) 26 | else Ok () 27 | 28 | let check_dune_lang_version ~yes ~root = 29 | let open Result.O in 30 | let dune_project_path = D.Project.dune_project root in 31 | Logs.debug (fun l -> 32 | l "Looking for dune-project file in %a" D.Pp.Styled.path dune_project_path); 33 | let* found_dune_project = Bos.OS.File.exists dune_project_path in 34 | if found_dune_project then 35 | let* content = Bos.OS.File.read dune_project_path in 36 | match D.Dune_file.Lang.from_content content with 37 | | Error (`Msg msg) -> 38 | Logs.warn (fun l -> l "%s" msg); 39 | Ok () 40 | | Ok version -> ( 41 | match 42 | D.Dune_file.Lang.(compare_version version duniverse_minimum_version) 43 | with 44 | | n when n >= 0 -> Ok () 45 | | _ -> 46 | suggest_updating_version ~yes ~version ~dune_project_path ~content) 47 | else ( 48 | Logs.debug (fun l -> l "No dune-project found"); 49 | Ok ()) 50 | 51 | let run (`Yes yes) (`Root root) (`Lockfile explicit_lockfile) 52 | (`Keep_git_dir keep_git_dir) (`Keep_symlinked_dir keep_symlinked_dir) 53 | (`Duniverse_repos duniverse_repos) () = 54 | let open Result.O in 55 | let* lockfile = Common.find_lockfile ~explicit_lockfile root in 56 | let* duniverse = D.Lockfile.to_duniverse lockfile in 57 | match duniverse with 58 | | [] -> 59 | Common.Logs.app (fun l -> 60 | l "No dependencies to pull, there's nothing to be done here!"); 61 | Ok () 62 | | duniverse -> 63 | let full = 64 | match (duniverse_repos, keep_symlinked_dir) with 65 | | None, false -> true 66 | | _ -> false 67 | in 68 | let* duniverse = 69 | Common.filter_duniverse ~to_consider:duniverse_repos duniverse 70 | in 71 | let* () = check_dune_lang_version ~yes ~root in 72 | OpamGlobalState.with_ `Lock_none @@ fun global_state -> 73 | let* locked_ocaml_version = 74 | D.Lockfile.ocaml_version lockfile 75 | |> Option.to_result ~none:(`Msg "OCaml compiler not in lockfile") 76 | in 77 | let* pulled = 78 | D.Pull.duniverse ~global_state ~root ~full 79 | ~preserve_symlinks:keep_symlinked_dir ~trim_clone:(not keep_git_dir) 80 | duniverse 81 | in 82 | let* switch_ocaml_version = 83 | let+ version = D.Exec.ocaml_version () in 84 | OpamPackage.Version.of_string (Ocaml_version.to_string version) 85 | in 86 | (match 87 | D.Opam.version_is_at_least locked_ocaml_version switch_ocaml_version 88 | with 89 | | true -> () 90 | | false -> 91 | Logs.warn (fun l -> 92 | l 93 | "Pulling duniverse succeeded but the version of the OCaml \ 94 | compiler does not match the lockfile: %a in lockfile, yet %a \ 95 | in switch.\n\ 96 | You might want to change the compiler version of your switch \ 97 | accordingly:\n\ 98 | opam install %a.%a --update-invariant" D.Opam.Pp.version 99 | locked_ocaml_version D.Opam.Pp.version switch_ocaml_version 100 | D.Opam.Pp.package_name D.Config.compiler_package_name 101 | D.Opam.Pp.version locked_ocaml_version)); 102 | Ok pulled 103 | 104 | let info = 105 | let open Cmdliner in 106 | let doc = "fetch the dependencies sources as specified by the lockfile" in 107 | let exits = Common.exit_codes in 108 | let man = 109 | [ 110 | `S Manpage.s_description; 111 | `P 112 | "This command fetches the sources of the dependencies according to the \ 113 | lockfile calculated with $(b,opam monorepo lock), stores them in the \ 114 | $(b,duniverse/) directory in the repository and set it up so they are \ 115 | treated as vendored code by dune."; 116 | `P 117 | "The previous content of the $(b,duniverse/) folder is deleted upon \ 118 | calling this command, unless a subset of repositories to pull is \ 119 | explicitly passed on the command line."; 120 | ] 121 | in 122 | Cmd.info "pull" ~doc ~exits ~man 123 | 124 | let term = 125 | Common.Term.result_to_exit 126 | Cmdliner.Term.( 127 | const run $ Common.Arg.yes $ Common.Arg.root $ Common.Arg.lockfile 128 | $ Common.Arg.keep_git_dir $ Common.Arg.keep_symlinked_dir 129 | $ Common.Arg.duniverse_repos $ Common.Arg.setup_logs ()) 130 | 131 | let cmd = Cmdliner.Cmd.v info term 132 | -------------------------------------------------------------------------------- /cli/pull.mli: -------------------------------------------------------------------------------- 1 | val cmd : int Cmdliner.Cmd.t 2 | -------------------------------------------------------------------------------- /doc/concepts.mld: -------------------------------------------------------------------------------- 1 | {1 Concepts} 2 | 3 | An [opam-monorepo] project uses 3 components in the source tree: 4 | - a set of opam files containing specifications about the dependencies (for 5 | example, a dependency on [lwt >= 4.0.0]) 6 | - a lock file (with extension [.opam.locked]) containing references to exact 7 | dependencies (for example, [lwt = 4.2.1]) and how to get them (URLs, hashes, 8 | etc.). It also contains information about transitive dependencies, not just 9 | the ones mentioned in opam files. 10 | - a [duniverse] folder containing the sources for all the packages 11 | mentioned in the lock file 12 | 13 | The two important commands are: 14 | - {{!page-lock}[opam monorepo lock]}, which will read the opam file and compute 15 | a solution using the opam repository and the opam solver. It will create or 16 | update the lock file with this solution. 17 | - [opam monorepo pull] will read the lock file, download the dependencies, and 18 | unpack them into the [duniverse/] folder. 19 | -------------------------------------------------------------------------------- /doc/dune: -------------------------------------------------------------------------------- 1 | (documentation) 2 | -------------------------------------------------------------------------------- /doc/faq.mld: -------------------------------------------------------------------------------- 1 | {1 FAQ} 2 | 3 | {2 Is This For Me?} 4 | 5 | This might be the right thing for you if all your dependencies use Dune and you 6 | are under full control of the build and deployment pipeline for your piece of 7 | software (e.g., if you are in a position to write a [dune-workspace] file). 8 | 9 | For example: 10 | - You are building and deploying a piece of software in a Docker image. 11 | - You are building a website using a static-site generator and examples checked 12 | with MDX. 13 | - You are checking the impact of some changes in a library and all packages that 14 | depend on it. 15 | 16 | {2 About opam} 17 | 18 | Opam is a package manager that 19 | - determines which versions of a library to use for a project 20 | - executes build instructions for each of them so that a compiled form is 21 | available 22 | 23 | Instead of the second step, [opam-monorepo] copies the source code of these 24 | dependencies into a Dune workspace, which Dune compiles directly. 25 | 26 | In a way, this is a new kind of package management that uses source code in 27 | every step: 28 | 29 | - "Binary" package managers (apt, rpm) download the code as binary and install 30 | it as binary. 31 | - "Source" package managers download the code as source and install it as 32 | binary. Opam is in this category. 33 | - [opam-monorepo] downloads code as source and installs it as source. 34 | 35 | {2 New Workflows [opam-monorepo] Enable} 36 | 37 | With the sources available, certain tasks are easier: 38 | - Using Merlin to navigate the project (there's no distinction between jumping 39 | to a definition in the project’s code or in its dependencies) 40 | - Editing a dependency and rebuilding the project (even in watch mode) 41 | - Upstreaming changes made to dependencies (a dependency is just a subdirectory) 42 | - Cross-compilation - the details of how to build some native code can come late 43 | in the pipeline, which isn't a problem if the sources are available 44 | 45 | {2 Why is [opam monorepo] an opam Plugin?} 46 | 47 | Even though [opam-monorepo] doesn't require installing packages through opam, it 48 | is distributed as an opam plugin. There are two reasons for this: 49 | - It uses opam libraries internally to interact with the package repository and 50 | respect the local opam configuration (repositories, pins, etc.) 51 | - Opam plugins have a special behaviour; they can be installed globally, so one 52 | does not have to install [opam-monorepo] through opam in various projects. 53 | -------------------------------------------------------------------------------- /doc/index.mld: -------------------------------------------------------------------------------- 1 | {0 [opam monorepo] Docs} 2 | 3 | [opam-monorepo] is an opam plugin that assembles a Dune workspace for an OCaml 4 | project using a precise lock file. When a project is in the same workspace as 5 | all of its dependencies, it's self-contained, so Dune can build it without 6 | relying on opam or external system libraries. 7 | 8 | Table of contents: 9 | - {{!page-faq}A FAQ} to answer common questions about [opam monorepo] 10 | - {{!page-concepts}A description of the concepts} behind [opam monorepo]. 11 | - Some {{!page-workflows}common workflows} used in [opam monorepo] projects. 12 | - In depths documentation on {{!page-lock}locking dependencies}, the [lock] 13 | command and how to get the best of it. 14 | - A {{!page-"opam-provided"} way to use [opam-monorepo] with non-[dune] dependencies} 15 | for when it is not possible to use [dune] 16 | -------------------------------------------------------------------------------- /doc/opam-provided.mld: -------------------------------------------------------------------------------- 1 | {1 [opam]-provided Dependencies} 2 | 3 | This section documents a feature that’s useful when some dependencies cannot be 4 | installed via [opam-monorepo]. This workflow isn’t meant for regular use but 5 | only as a way to use [opam-monorepo] in cases where it wouldn't be otherwise 6 | possible. 7 | 8 | We suggest minimizing its usage and prefer to use (and contribute) [dune-ports] 9 | from the [opam-overlays] repository. It's usage is safest with 10 | leaf-dependencies and those that don’t have dependency intersections with 11 | vendored packages. Due to possibly of unexpected interactions, this feature is 12 | only meant for advanced users, and the solutions it produces might be in flux. 13 | 14 | {2 Use Cases} 15 | 16 | Sometimes it's not possible to put all your dependencies into the [duniverse], 17 | be it due to your dependencies not building with [dune] or some tooling that 18 | should be just installed via [opam]. Such examples include: 19 | 20 | {ul 21 | {- Packages not building with Dune} 22 | {- Packages that should not be vendored for legal reasons} 23 | } 24 | 25 | These users can opt-in to having some of their dependencies provided by [opam] 26 | instead of [opam-monorepo] pulling them into the [duniverse]. 27 | 28 | {2 Initial Setup} 29 | 30 | With the default configuration, [opam-monorepo] runs in full-[duniverse] mode, 31 | i.e., requiring all the dependencies of your opam files to be able to be 32 | built as part of the [duniverse]. 33 | 34 | As such it's only recommended that packages are installed via [opam] for which 35 | there is no way to build them with [dune], e.g., by using the 36 | {{:https://github.com/dune-universe/opam-overlays}opam-overlays} repository 37 | with Dune ports. 38 | 39 | To let [opam-monorepo] know a package is to be installed via [opam], it needs 40 | to be marked as such in the opam file. For this, it needs to be added as normal 41 | in the [depends] field, as well in addition into the new 42 | [x-opam-monorepo-opam-provided] field. This field takes a single package or a 43 | list of packages to be installed via [opam] instead of being pulled into the 44 | [duniverse]. 45 | 46 | To configure [opam-monorepo] to avoid pulling in package [foo], specify it in 47 | the list of packages provided by opam: 48 | 49 | {[ 50 | depends: [ 51 | "foo" 52 | ] 53 | x-opam-monorepo-opam-provided: ["foo"] 54 | ]} 55 | 56 | This can either be specified directly in the opam file or, if you use Dune to 57 | generate opam files, in the [.template] file. In such case, make sure to 58 | regenerate the opam file. 59 | 60 | As a shortcut syntax, if there’s only one package, it’s possible to leave out 61 | the list and just specify the package itself: 62 | 63 | {[ 64 | depends: [ 65 | "foo" 66 | ] 67 | x-opam-monorepo-opam-provided: "foo" 68 | ]} 69 | 70 | {2 Usage} 71 | 72 | After you configured your opam file, you have to {e lock} the environment. 73 | 74 | {[ 75 | $ opam monorepo lock 76 | ]} 77 | 78 | After this succeeds, you will have an [.opam.locked] file containing all 79 | your project’s dependencies (including transitive dependencies). The ones that 80 | [opam-monorepo] will pull into the [duniverse] are marked as [vendor]. 81 | 82 | {[ 83 | $ opam install --ignore-pin-depends --deps-only ./ --locked 84 | $ opam-monorepo pull 85 | ]} 86 | 87 | {2 How It Works} 88 | 89 | In addition to calculating the dependencies of the packages that will be 90 | included in the [duniverse], [opam-monorepo] calculates the dependencies of the 91 | packages to be installed via [opam]. [opam-monorepo] then writes a lock file 92 | that sets a variable on all the dependencies it will pull, whereas 93 | [opam]-provided dependencies don't have variables set. In opam, unset variables 94 | are false; thus opam will ignore the packages that [opam-monorepo] will pull. 95 | 96 | The [opam]-provided subset of dependencies is then {e excluded} from the 97 | [duniverse] packages. 98 | 99 | {3 Resolution of Dependency Overlaps} 100 | 101 | There are cases where a dependency package appears multiple times in the 102 | project’s dependency tree. It’s possible that a package is part of the packages 103 | included in the [duniverse], as well as packages installed by [opam]. 104 | 105 | In such cases there are two possibilities: 106 | 107 | {ol 108 | {- Install the package in [opam], exclude it from duniverse} 109 | {- Install the package in [opam], include a duplicate in the duniverse} 110 | } 111 | 112 | Both these approaches have advantages and disadvantages. [opam-monorepo] 113 | currently chooses #2 to maximize the amount of packages that are vendored, thus 114 | increasing the size of the reproducible set. Yet this doesn’t mean that two 115 | different versions of the package can be installed. The set of packages in opam 116 | and the [duniverse] must be co-installable! 117 | -------------------------------------------------------------------------------- /dune: -------------------------------------------------------------------------------- 1 | (env 2 | (release 3 | (flags 4 | (:standard -w -3)))) 5 | 6 | (vendored_dirs stdune) 7 | -------------------------------------------------------------------------------- /dune-project: -------------------------------------------------------------------------------- 1 | (lang dune 3.6) 2 | (generate_opam_files true) 3 | (name opam-monorepo) 4 | 5 | (cram enable) 6 | 7 | (source (github tarides/opam-monorepo)) 8 | (license ISC) 9 | (authors "Anil Madhavapeddy" "Nathan Rebours" "Lucas Pluvinage" "Jules Aguillon") 10 | (maintainers "anil@recoil.org") 11 | (documentation "https://tarides.github.io/opam-monorepo") 12 | 13 | (package 14 | (name opam-monorepo) 15 | (synopsis "Assemble and manage fully vendored Dune repositories") 16 | (description "The opam monorepo plugin provides a convenient interface to bridge the 17 | opam package manager with having a local copy of all the source 18 | code required to build a project using the dune build tool.") 19 | (depends 20 | (ocaml (>= 4.13.0)) 21 | dune-build-info 22 | bos 23 | (cmdliner (>= 1.1.0)) 24 | fmt 25 | logs 26 | (opam-file-format (>= 2.1.0)) 27 | (opam-format (>= 2.2.1)) 28 | (opam-state (>= 2.2.1)) 29 | (opam-0install (>= 0.5)) 30 | ocaml-version 31 | sexplib 32 | uri 33 | (alcotest :with-test)) 34 | (conflicts 35 | (dune-build-info (or (= 2.7.0) (= 2.7.1))) 36 | (dune-configurator (or (= 2.7.0) (= 2.7.1))))) 37 | -------------------------------------------------------------------------------- /dune-workspace: -------------------------------------------------------------------------------- 1 | (lang dune 1.1) 2 | (env (static (flags -cclib -static))) 3 | -------------------------------------------------------------------------------- /lib/config.ml: -------------------------------------------------------------------------------- 1 | (* Copyright (c) 2018 Anil Madhavapeddy 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 | * 15 | *) 16 | open Import 17 | 18 | (* These packages are usually used only for legacy building. Since 19 | opam-monorepo requires everything to build with dune (which is special 20 | cased) we can ignore these packages if they're in the dependency formula *) 21 | let skip_packages = 22 | [ "jbuilder"; "dune"; "ocamlbuild"; "ocamlmod"; "oasis"; "ocamlify" ] 23 | |> List.map ~f:OpamPackage.Name.of_string 24 | |> OpamPackage.Name.Set.of_list 25 | 26 | (* The "ocaml" package is used in various other places so it is kept apart 27 | in the list of package names *) 28 | let compiler_package_name = OpamPackage.Name.of_string "ocaml" 29 | 30 | let compiler_package_names = 31 | [ compiler_package_name; OpamPackage.Name.of_string "ocaml-compiler" ] 32 | |> OpamPackage.Name.Set.of_list 33 | 34 | let duniverse_opam_repo = 35 | "git+https://github.com/dune-universe/opam-overlays.git" 36 | 37 | let vendor_dir = Fpath.v "duniverse" 38 | let duniverse_log = Fpath.v ".duniverse-log" 39 | let lockfile_ext = ".opam.locked" 40 | 41 | (* variable to use for vendoring *) 42 | let vendor_variable = OpamVariable.of_string "vendor" 43 | -------------------------------------------------------------------------------- /lib/dev_repo.ml: -------------------------------------------------------------------------------- 1 | open Import 2 | 3 | type t = string 4 | 5 | let compare = String.compare 6 | let from_string s = s 7 | let to_string t = t 8 | let pp = Fmt.string 9 | 10 | let repo_name_regexp = 11 | let open Re in 12 | compile 13 | (seq [ 14 | opt (char '/'); 15 | rep (char '.'); 16 | group (rep1 (compl [ char '.'; char '/' ])); 17 | opt (seq [ 18 | char '.'; 19 | rep (compl [ char '/' ])]); 20 | rep (char '/'); 21 | stop 22 | ]) 23 | 24 | let repo_name t = 25 | let path = Uri.of_string t |> Uri.path in 26 | match Re.exec_opt repo_name_regexp path with 27 | | Some group -> 28 | Ok (Re.Group.get group 1) 29 | | None -> 30 | Rresult.R.error_msgf 31 | "unexpected empty string while computing name for dev_repo: \"%s\"" t 32 | 33 | module Map = Map.Make (struct 34 | type nonrec t = t 35 | 36 | let compare = compare 37 | end) 38 | 39 | module Tbl = Hashtbl.Make (struct 40 | type nonrec t = t 41 | 42 | let hash = Hashtbl.hash 43 | let equal = String.equal 44 | end) 45 | -------------------------------------------------------------------------------- /lib/dev_repo.mli: -------------------------------------------------------------------------------- 1 | open Import 2 | 3 | type t 4 | 5 | val from_string : string -> t 6 | val to_string : t -> string 7 | val pp : t Fmt.t 8 | 9 | val repo_name : t -> (string, Rresult.R.msg) result 10 | (** Computes a name for the repo by applying the following method: 11 | 1. Start with the path component of the repo's uri 12 | 2. Remove any trailing "/" characters 13 | 3. Take the substring to the right of the right-most "/" character to obtain 14 | the final part of the path 15 | 4. Remove any leading "." characters 16 | 5. Take the substring to the left of the left-most "." character 17 | 18 | E.g. [repo_name (from_string "https://github.com/ocamllabs/opam-monorepo.git")] 19 | returns ["opam-monorepo"]. 20 | 21 | Returns an error if the result would be the empty string. *) 22 | 23 | module Map : Map.S with type key = t 24 | module Tbl : Hashtbl.S with type key = t 25 | -------------------------------------------------------------------------------- /lib/dune: -------------------------------------------------------------------------------- 1 | (library 2 | (name duniverse_lib) 3 | (libraries 4 | bos 5 | cmdliner 6 | fmt 7 | logs 8 | ocaml-version 9 | opam-0install 10 | opam-file-format 11 | opam-format 12 | sexplib 13 | stdext 14 | threads 15 | uri)) 16 | -------------------------------------------------------------------------------- /lib/dune_file.ml: -------------------------------------------------------------------------------- 1 | open! Import 2 | 3 | module Lang = struct 4 | type version = int * int 5 | 6 | let pp_version fmt (major, minor) = Format.fprintf fmt "%d.%d" major minor 7 | let version_to_string version = Format.asprintf "%a" pp_version version 8 | 9 | let stanza_regexp = 10 | let open Re in 11 | compile 12 | (seq 13 | [ 14 | bos; 15 | char '('; 16 | rep blank; 17 | str "lang"; 18 | rep1 blank; 19 | str "dune"; 20 | rep1 blank; 21 | group (rep1 digit); 22 | char '.'; 23 | group (rep1 digit); 24 | rep blank; 25 | char ')'; 26 | rep blank; 27 | opt (char '\r'); 28 | eol; 29 | ]) 30 | 31 | let from_match group = 32 | let major_str = Re.Group.get group 1 in 33 | let minor_str = Re.Group.get group 2 in 34 | match (int_of_string_opt major_str, int_of_string_opt minor_str) with 35 | | Some major, Some minor -> Ok (major, minor) 36 | | _ -> 37 | Rresult.R.error_msgf 38 | "Invalid dune-project file: invalid lang version %s.%s" major_str 39 | minor_str 40 | 41 | let from_content content = 42 | match Re.all stanza_regexp content with 43 | | [ group ] -> from_match group 44 | | _ -> 45 | (* We match on [bos], meaning the only possible case here is the empty 46 | list *) 47 | Rresult.R.error_msg 48 | "Invalid dune-project file: It does not start with a valid lang \ 49 | stanza" 50 | 51 | let update_stanza ~version group = 52 | let new_ver = version_to_string version in 53 | let old_ver = 54 | Printf.sprintf "%s.%s" (Re.Group.get group 1) (Re.Group.get group 2) 55 | in 56 | let stanza = Re.Group.get group 0 in 57 | Re.replace_string (Re.compile (Re.str old_ver)) ~by:new_ver stanza 58 | 59 | let update ~version content = 60 | Re.replace ~all:false stanza_regexp ~f:(update_stanza ~version) content 61 | 62 | let compare_version (major, minor) (major', minor') = 63 | match Int.compare major major' with 64 | | 0 -> Int.compare minor minor' 65 | | _ as ord -> ord 66 | 67 | let duniverse_minimum_version = (1, 11) 68 | end 69 | 70 | module Raw = struct 71 | let as_sexps path = 72 | try Ok (Sexplib.Sexp.load_sexps (Fpath.to_string path)) with 73 | | Sexplib.Sexp.Parse_error pe -> 74 | Error 75 | (`Msg 76 | (Format.asprintf "Failed to parse dune file %a: %s" Fpath.pp path 77 | pe.err_msg)) 78 | | Failure _ -> 79 | Error 80 | (`Msg 81 | (Format.asprintf "Failed to parse dune file %a: Invalid sexp" 82 | Fpath.pp path)) 83 | 84 | let comment s = Printf.sprintf "; %s" s 85 | let vendored_dirs glob = Printf.sprintf "(vendored_dirs %s)" glob 86 | 87 | let duniverse_dune_content = 88 | [ 89 | comment "This file is generated by opam-monorepo."; 90 | comment 91 | "Be aware that it is likely to be overwritten by your next opam \ 92 | monorepo pull invocation."; 93 | ""; 94 | vendored_dirs "*"; 95 | ] 96 | end 97 | 98 | module Project = struct 99 | module OV = Ocaml_version 100 | 101 | let rec name sexps = 102 | match (sexps : Sexplib0.Sexp.t list) with 103 | | [] -> Error (`Msg "Missing a name field in the dune-project file") 104 | | List [ Atom "name"; Atom name ] :: _ -> Ok name 105 | | _ :: tl -> name tl 106 | end 107 | -------------------------------------------------------------------------------- /lib/dune_file.mli: -------------------------------------------------------------------------------- 1 | module Raw : sig 2 | val as_sexps : Fpath.t -> (Sexplib0.Sexp.t list, [> `Msg of string ]) result 3 | (** Parses a dune file as a list of S-expressions. *) 4 | 5 | val comment : string -> string 6 | 7 | val vendored_dirs : string -> string 8 | (** [vendored_dirs glob] returns a stanza marking directories matching [glob] as vendored *) 9 | 10 | val duniverse_dune_content : string list 11 | (** The content of the duniverse/dune file as a list of lines *) 12 | end 13 | 14 | module Lang : sig 15 | type version = int * int 16 | 17 | val compare_version : version -> version -> int 18 | val pp_version : version Fmt.t 19 | 20 | val duniverse_minimum_version : version 21 | (** The minimum dune lang version required by duniverse *) 22 | 23 | val from_content : string -> (version, [> `Msg of string ]) result 24 | (** Extract the lang version from the content of the entire dune-project *) 25 | 26 | val update : version:version -> string -> string 27 | (** Update the content of the entire dune-project, setting the lang version 28 | to [version]. 29 | Return the string unmodified if there was previously no lang stanza. *) 30 | end 31 | 32 | module Project : sig 33 | val name : Sexplib0.Sexp.t list -> (string, [> `Msg of string ]) result 34 | (** Returns the dune-project's name given the content of the file as a list of S-expressions, 35 | if any. *) 36 | end 37 | -------------------------------------------------------------------------------- /lib/duniverse.mli: -------------------------------------------------------------------------------- 1 | type unresolved = Git.Ref.t 2 | type resolved = Git.Ref.resolved 3 | 4 | module Repo : sig 5 | module Url : sig 6 | type 'ref t = Git of { repo : string; ref : 'ref } | Other of string 7 | 8 | val equal : ('ref -> 'ref -> bool) -> 'ref t -> 'ref t -> bool 9 | val pp : 'ref Fmt.t -> 'ref t Fmt.t 10 | val to_string : resolved t -> string 11 | val to_opam_url : resolved t -> OpamUrl.t 12 | 13 | val from_opam_url : OpamUrl.t -> (resolved t, [ `Msg of string ]) result 14 | (** Converts an [OpamUrl.t] to a resolved URL. Assumes the ref after the "#" is 15 | a commit hash. Returns an error on git URLs with no such ref. *) 16 | end 17 | 18 | type 'ref t = { 19 | dir : string; 20 | url : 'ref Url.t; 21 | hashes : OpamHash.t list; 22 | provided_packages : OpamPackage.t list; 23 | } 24 | (** Type of dependencies to clone in the duniverse *) 25 | 26 | val equal : ('ref -> 'ref -> bool) -> 'ref t -> 'ref t -> bool 27 | 28 | (**/**) 29 | 30 | (* Exposed for test purposes only *) 31 | 32 | val pp : 'ref Fmt.t -> 'ref t Fmt.t 33 | 34 | module Package : sig 35 | type t = { 36 | opam : OpamPackage.t; 37 | dev_repo : string; 38 | url : unresolved Url.t; 39 | hashes : OpamHash.t list; 40 | pinned : bool; 41 | } 42 | 43 | val equal : t -> t -> bool 44 | val pp : t Fmt.t 45 | 46 | val from_package_summary : 47 | get_default_branch:(string -> (string, Rresult.R.msg) result) -> 48 | Opam.Package_summary.t -> 49 | (t option, [ `Msg of string ]) result 50 | end 51 | 52 | val from_packages : 53 | dev_repo:Dev_repo.t -> 54 | Package.t list -> 55 | (unresolved t, Rresult.R.msg) result 56 | 57 | (**/**) 58 | end 59 | 60 | type t = resolved Repo.t list 61 | (** The type of dependencies to be pulled into the duniverse *) 62 | 63 | val equal : t -> t -> bool 64 | 65 | val from_dependency_entries : 66 | get_default_branch:(string -> (string, Rresult.R.msg) result) -> 67 | Opam.Dependency_entry.t list -> 68 | (unresolved Repo.t list, [ `Msg of string ]) result 69 | (** Build opamverse and duniverse from a list of [Types.Opam.entry] values. 70 | It filters out virtual packages and packages with unknown dev-repo. *) 71 | 72 | val resolve : 73 | resolve_ref: 74 | (repo:string -> ref:unresolved -> (resolved, Rresult.R.msg) result) -> 75 | unresolved Repo.t list -> 76 | (t, Rresult.R.msg) result 77 | (** Apply the given [resolve_ref] function to bind each source repo to a specific commit 78 | rather than a "floating" ref. *) 79 | 80 | (**/**) 81 | 82 | (* Exposed for test purposes only *) 83 | 84 | val pp : t Fmt.t 85 | 86 | (**/**) 87 | -------------------------------------------------------------------------------- /lib/exec.mli: -------------------------------------------------------------------------------- 1 | (* Copyright (c) 2018 Anil Madhavapeddy 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 | * 15 | *) 16 | 17 | val map : ('a -> ('b, 'c) result) -> 'a list -> ('b list, 'c) result 18 | val iter : ('a -> (unit, 'b) result) -> 'a list -> (unit, 'b) result 19 | val opam_version : unit -> (string, [> Rresult.R.msg ]) result 20 | 21 | val ocaml_version : 22 | ?ocamlc:Fpath.t -> unit -> (Ocaml_version.t, [> Rresult.R.msg ]) result 23 | 24 | val dune_version : unit -> (string, [> Rresult.R.msg ]) result 25 | 26 | val dune_build : 27 | root:Fpath.t -> 28 | ?profile:string -> 29 | string list -> 30 | (unit, [> Rresult.R.msg ]) result 31 | 32 | val dune_install : 33 | root:Fpath.t -> 34 | prefix:Fpath.t -> 35 | sections:string list -> 36 | string list -> 37 | (unit, [> Rresult.R.msg ]) result 38 | 39 | val install_ocaml_to : 40 | prefix:Fpath.t -> src:Fpath.t -> unit -> (unit, [> Rresult.R.msg ]) result 41 | 42 | val install_dune_to : 43 | prefix:Fpath.t -> src:Fpath.t -> unit -> (unit, [> Rresult.R.msg ]) result 44 | 45 | val git_default_branch : 46 | remote:string -> unit -> (string, [> Rresult.R.msg ]) result 47 | (** Return the default branch for the given remote name by running git remote show [remote] and 48 | parsing the output looking for HEAD branch: *) 49 | 50 | val git_shallow_clone : 51 | output_dir:Fpath.t -> 52 | remote:string -> 53 | ref:string -> 54 | unit -> 55 | (unit, [> Rresult.R.msg ]) result 56 | 57 | val git_rev_parse : 58 | repo:Fpath.t -> ref:string -> unit -> (string, [> Rresult.R.msg ]) result 59 | 60 | val git_unshallow : repo:Fpath.t -> unit -> (unit, [> Rresult.R.msg ]) result 61 | 62 | val git_add_and_commit : 63 | repo:Fpath.t -> 64 | message:string -> 65 | Bos.Cmd.t -> 66 | (unit, [> Rresult.R.msg ]) result 67 | (** [git_add_and_commit ~repo ~message files] adds [files] to [repo] and commits them with 68 | [message]. *) 69 | 70 | val is_git_repo_clean : 71 | repo:Fpath.t -> unit -> (bool, [> Rresult.R.msg ]) result 72 | (** Return whether the given repo is clean, ie return true if there is no uncommitted changes *) 73 | 74 | val git_checkout : 75 | ?args:Bos.Cmd.t -> repo:Fpath.t -> string -> (unit, [> Rresult.R.msg ]) result 76 | (** [git_checkout ~args ~repo branch] checks out the git repository in [repo] to branch [branch] 77 | with the extra arguments [args]. *) 78 | 79 | val git_checkout_or_branch : 80 | repo:Fpath.t -> string -> (unit, [> Rresult.R.msg ]) result 81 | (** [git_checkout ~repo branch] checks out the git repository in [repo] to branch [branch] creating 82 | it if it doesn't exist yet. *) 83 | 84 | val git_add_all_and_commit : 85 | repo:Fpath.t -> message:string -> unit -> (unit, [> Rresult.R.msg ]) result 86 | (** [git_add_all_and_commit ~repo ~message ()] runs git add -am [message] in [repo]. *) 87 | 88 | val git_merge : 89 | ?args:Bos.Cmd.t -> 90 | from:string -> 91 | repo:Fpath.t -> 92 | unit -> 93 | (unit, [> Rresult.R.msg ]) result 94 | (** [git_merge ~args ~repo branch] merges [from] into [repo]'s current active branch with the extra 95 | arguments [args]. *) 96 | 97 | val git_resolve : 98 | remote:string -> ref:Git.Ref.t -> (Git.Ref.resolved, Rresult.R.msg) result 99 | (** [git_resolve ~remote ~ref] runs git ls-remote to resolve the given ref to a commit hash *) 100 | 101 | val git_branch : 102 | repo:Fpath.t -> 103 | ref:Git.Ref.t -> 104 | branch_name:string -> 105 | (unit, [> Rresult.R.msg ]) result 106 | 107 | val git_submodule_add : 108 | repo:Fpath.t -> 109 | remote_name:string -> 110 | ref:Git.Ref.t -> 111 | branch:string -> 112 | target_path:string -> 113 | ?force:bool -> 114 | unit -> 115 | (unit, [> Rresult.R.msg ]) result 116 | (** [git_submodule_add] will run [git submodule] for [remote_name] and initialise 117 | it into [target_path] for commit [ref] and on the remote [branch]. *) 118 | 119 | val git_update_index : 120 | repo:Fpath.t -> 121 | ?add:bool -> 122 | cacheinfo:int * string * Fpath.t -> 123 | unit -> 124 | (unit, [> Rresult.R.msg ]) result 125 | (** [git_update_index] will add the [cacheinfo] (a tuple of mode, hash and target path) 126 | to the index, and append it to the cache if [add] is [true]. *) 127 | 128 | val git_remote_add : 129 | repo:Fpath.t -> 130 | remote_url:string -> 131 | remote_name:string -> 132 | (unit, [> Rresult.R.msg ]) result 133 | (** Uses git remote add in repo **) 134 | 135 | val git_remote_remove : 136 | repo:Fpath.t -> remote_name:string -> (unit, [> Rresult.R.msg ]) result 137 | (** Uses git remote remove in repo **) 138 | 139 | val git_fetch_to : 140 | repo:Fpath.t -> 141 | remote_name:string -> 142 | ref:string -> 143 | branch:string -> 144 | ?force:bool -> 145 | unit -> 146 | (unit, [> Rresult.R.msg ]) result 147 | (** [git_fetch_to ~remote_name ~tag ~branch] Fetches tag from remote_name into a given branch **) 148 | 149 | val git_init_bare : repo:Fpath.t -> (unit, [> Rresult.R.msg ]) result 150 | (** [git_init path] Initialize Git as a bare repo in given path **) 151 | 152 | val git_clone : 153 | branch:string -> 154 | remote:string -> 155 | output_dir:Fpath.t -> 156 | (unit, [> Rresult.R.msg ]) result 157 | (** [git_clone ~branch ~remote ~output_dir] Git clone branch from remote in output_dir **) 158 | 159 | val git_clone_or_pull : 160 | branch:string -> 161 | remote:string -> 162 | output_dir:Fpath.t -> 163 | (unit, [> Rresult.R.msg ]) result 164 | 165 | val git_rename_branch_to : 166 | repo:Fpath.t -> branch:string -> (unit, [> Rresult.R.msg ]) result 167 | (** [git_rename_branch_to ~branch] Sets repo's branch name to branch. **) 168 | 169 | val git_remotes : repo:Fpath.t -> (string list, [> Rresult.R.msg ]) result 170 | (** [git_remotes repo] List remotes of the git project located in repo. **) 171 | 172 | val git_branch_exists : repo:Fpath.t -> branch:string -> bool 173 | (** [git_branch_exists repo branch] Returns true if branch exists in repo. **) 174 | -------------------------------------------------------------------------------- /lib/git.ml: -------------------------------------------------------------------------------- 1 | open Import 2 | 3 | module Ls_remote = struct 4 | let non_packed_suffix = "^{}" 5 | let ref_arg ref = Bos.Cmd.(v ref % (ref ^ non_packed_suffix)) 6 | 7 | let parse_output_line s = 8 | match String.extract_blank_separated_words s with 9 | | [ commit; ref ] -> Ok (commit, ref) 10 | | _ -> 11 | Error (`Msg (Printf.sprintf "Invalid git ls-remote output line: %S" s)) 12 | 13 | type search_result = { 14 | maybe_packed : string option; 15 | not_packed : string option; 16 | } 17 | 18 | let interpret_search_result sr = 19 | match sr with 20 | | { maybe_packed = None; not_packed = None } -> None 21 | | { maybe_packed = Some commit; not_packed = None } 22 | | { maybe_packed = _; not_packed = Some commit } -> 23 | Some commit 24 | 25 | let search_ref target lines = 26 | let target_not_packed = target ^ non_packed_suffix in 27 | let f acc (commit, full_ref) = 28 | if String.equal full_ref target then 29 | { acc with maybe_packed = Some commit } 30 | else if String.equal full_ref target_not_packed then 31 | { acc with not_packed = Some commit } 32 | else acc 33 | in 34 | List.fold_left ~f ~init:{ maybe_packed = None; not_packed = None } lines 35 | 36 | let is_commit target = List.exists ~f:(fun (commit, _) -> commit = target) 37 | 38 | let looks_like_commit ref = 39 | String.length ref > 6 40 | && Astring.(String.for_all Char.Ascii.is_hex_digit ref) 41 | 42 | let commit_pointed_by ~ref output_lines = 43 | let log_approx () = 44 | Logs.debug (fun l -> 45 | l "Ref '%s' looks like a commit but hasn't been found in the remote." 46 | ref); 47 | Ok ref 48 | in 49 | let open Result.O in 50 | match output_lines with 51 | | [ "" ] when looks_like_commit ref -> log_approx () 52 | | [ "" ] -> Error `No_such_ref 53 | | _ -> ( 54 | let* parsed_lines = Result.List.map ~f:parse_output_line output_lines in 55 | let search prefix = 56 | let result = search_ref (prefix ^ ref) parsed_lines in 57 | interpret_search_result result 58 | in 59 | match (search "", search "refs/tags/", search "refs/heads/") with 60 | | Some _, Some _, Some _ 61 | | Some _, Some _, None 62 | | Some _, None, Some _ 63 | | None, Some _, Some _ -> 64 | Error `Multiple_such_refs 65 | | Some commit, None, None 66 | | None, Some commit, None 67 | | None, None, Some commit -> 68 | Ok commit 69 | | None, None, None when is_commit ref parsed_lines -> Ok ref 70 | | None, None, None when looks_like_commit ref -> log_approx () 71 | | None, None, None -> Error `No_such_ref) 72 | 73 | let parse_ref_output_line ~symref line = 74 | match String.extract_blank_separated_words line with 75 | | [ "ref:"; branch; ref ] when ref = symref -> Some branch 76 | | _ -> None 77 | 78 | let extract_branch branch_ref = 79 | let refs_heads = "refs/heads/" in 80 | if String.starts_with ~prefix:refs_heads branch_ref then 81 | let pos = String.length refs_heads in 82 | let len = String.length branch_ref - pos in 83 | Ok (String.sub branch_ref ~pos ~len) 84 | else Error 85 | (`Msg 86 | (Printf.sprintf 87 | "Invalid `git ls-remote --symref` output. Failed to extract \ 88 | branch from ref `%s`." 89 | branch_ref)) 90 | 91 | let branch_of_symref ~symref output_lines = 92 | match List.filter_map ~f:(parse_ref_output_line ~symref) output_lines with 93 | | [] -> Error `Not_a_symref 94 | | [ ref ] -> extract_branch ref 95 | | _ -> 96 | Error 97 | (`Msg 98 | "Invalid `git ls-remote --symref` output. Too many lines starting \ 99 | by `ref:`.") 100 | end 101 | 102 | module Ref = struct 103 | type t = string 104 | 105 | let equal = String.equal 106 | let compare = String.compare 107 | let pp = Format.pp_print_string 108 | 109 | type resolved = { t : t; commit : string } 110 | 111 | let equal_resolved r r' = equal r.t r'.t && String.equal r.commit r'.commit 112 | 113 | let pp_resolved fmt resolved = 114 | Format.fprintf fmt "@[{ t = %a;@ commit = %s }@]" pp resolved.t 115 | resolved.commit 116 | end 117 | -------------------------------------------------------------------------------- /lib/git.mli: -------------------------------------------------------------------------------- 1 | module Ls_remote : sig 2 | val ref_arg : string -> Bos.Cmd.t 3 | (** [ref_arg ref] returns the CLI arguments to pass to git ls-remote 4 | to find the commit pointed by [ref] even the target repository uses packed-refs. *) 5 | 6 | val commit_pointed_by : 7 | ref:string -> 8 | string list -> 9 | (string, [> `No_such_ref | `Multiple_such_refs | `Msg of string ]) result 10 | (** [commit_pointed_by ~ref ls_remote_output] parses the output from git ls-remote 11 | and returns the commit pointed by [ref] if it can be determined from it. 12 | It will work even if the repo uses packed-refs. *) 13 | 14 | val branch_of_symref : 15 | symref:string -> 16 | string list -> 17 | (string, [> `Not_a_symref | `Msg of string ]) result 18 | (** [ref_of_symref ~symref ls_remote_output] parses the output from git ls-remote --symref 19 | and returns the underlying branch pointed by the symbolic ref [symref]. *) 20 | 21 | (**/**) 22 | 23 | (* Exposed for test purposes only *) 24 | 25 | val parse_output_line : string -> (string * string, Rresult.R.msg) result 26 | (** Parse the given git ls-remote output line and return the pair 27 | [(commit_hash, fully_qualified_ref)]. *) 28 | 29 | (**/**) 30 | end 31 | 32 | module Ref : sig 33 | type t = string 34 | 35 | val compare : t -> t -> int 36 | val equal : t -> t -> bool 37 | val pp : t Fmt.t 38 | 39 | type resolved = { t : t; commit : string } [@@deriving sexp] 40 | 41 | val equal_resolved : resolved -> resolved -> bool 42 | val pp_resolved : resolved Fmt.t 43 | end 44 | -------------------------------------------------------------------------------- /lib/import.ml: -------------------------------------------------------------------------------- 1 | include StdLabels 2 | include Stdext 3 | -------------------------------------------------------------------------------- /lib/lockfile.mli: -------------------------------------------------------------------------------- 1 | type t 2 | 3 | module Depends : sig 4 | type dependency = { package : OpamPackage.t; vendored : bool } 5 | type t = dependency list 6 | end 7 | 8 | val create : 9 | source_config:Source_opam_config.t -> 10 | root_packages:OpamPackage.Name.Set.t -> 11 | dependency_entries:Opam.Dependency_entry.t list -> 12 | root_depexts:(OpamSysPkg.Set.t * OpamTypes.filter) list list -> 13 | duniverse:Duniverse.t -> 14 | unit -> 15 | t 16 | 17 | val depends : t -> Depends.t 18 | val to_duniverse : t -> (Duniverse.t, [ `Msg of string ]) result 19 | val ocaml_version : t -> OpamPackage.Version.t option 20 | 21 | val save : 22 | opam_monorepo_cwd:Fpath.t -> 23 | cli_args:string list -> 24 | file:Fpath.t -> 25 | t -> 26 | (unit, [ `Msg of string ]) result 27 | 28 | val load : 29 | opam_monorepo_cwd:Fpath.t -> file:Fpath.t -> (t, [ `Msg of string ]) result 30 | 31 | val depexts : t -> (OpamSysPkg.Set.t * OpamTypes.filter) list 32 | -------------------------------------------------------------------------------- /lib/opam_solve.mli: -------------------------------------------------------------------------------- 1 | open Import 2 | 3 | type explicit_repos = string list 4 | type opam_env = OpamVariable.variable_contents String.Map.t 5 | type switch = OpamStateTypes.unlocked OpamStateTypes.switch_state 6 | type ('context, 'diagnostics) t 7 | type switch_diagnostics 8 | type explicit_repos_diagnostics 9 | type mock_diagnostics 10 | 11 | val local_opam_config_solver : (switch, switch_diagnostics) t 12 | 13 | val explicit_repos_solver : 14 | (opam_env * explicit_repos, explicit_repos_diagnostics) t 15 | 16 | val mock_solver : 17 | (opam_env * OpamFile.OPAM.t list * OpamPackage.t list, mock_diagnostics) t 18 | 19 | val calculate : 20 | build_only:bool -> 21 | allow_jbuilder:bool -> 22 | require_cross_compile:bool -> 23 | preferred_versions:OpamTypes.version OpamPackage.Name.Map.t -> 24 | local_opam_files:(OpamTypes.version * OpamFile.OPAM.t) OpamPackage.Name.Map.t -> 25 | target_packages:OpamPackage.Name.Set.t -> 26 | opam_provided:OpamPackage.Name.Set.t -> 27 | pin_depends:(OpamTypes.version * OpamFile.OPAM.t) OpamPackage.Name.Map.t -> 28 | ?ocaml_version:string -> 29 | ('context, 'diagnostics) t -> 30 | 'context -> 31 | ( Opam.Dependency_entry.t list, 32 | [> `Diagnostics of 'diagnostics | `Msg of string ] ) 33 | result 34 | (** Calculates a solution for the provided local packages and their Opam files 35 | containing their regular and test dependencies using the provided opam switch 36 | state. Uses [Opam_0install]. 37 | If [build_only] then no test dependencies are taken into account. If [ocaml_version] 38 | is provided, the solution will contain that concrete version of ocaml. *) 39 | 40 | val diagnostics_message : 41 | verbose:bool -> (_, 'diagnostics) t -> 'diagnostics -> [> `Msg of string ] 42 | 43 | val not_buildable_with_dune : 44 | (_, 'diagnostics) t -> 'diagnostics -> OpamPackage.Name.t list 45 | 46 | val unavailable_versions_due_to_constraints : 47 | (_, 'diagnostics) t -> 48 | 'diagnostics -> 49 | (OpamPackage.Name.t * OpamFormula.version_formula) list 50 | -------------------------------------------------------------------------------- /lib/parallel.ml: -------------------------------------------------------------------------------- 1 | open Import 2 | 3 | let n_threads = 24 4 | 5 | (** Creates a thread-safe list with a pop function. *) 6 | let protected_list lst = 7 | let list = ref lst in 8 | let list_lock = Mutex.create () in 9 | fun () -> 10 | Mutex.lock list_lock; 11 | let hd = 12 | match !list with 13 | | [] -> None 14 | | hd :: tl -> 15 | list := tl; 16 | Some hd 17 | in 18 | Mutex.unlock list_lock; 19 | hd 20 | 21 | let map ~f l = 22 | let pop = protected_list l in 23 | let rec worker result = 24 | match pop () with 25 | | None -> () 26 | | Some hd -> 27 | result := f hd :: !result; 28 | worker result 29 | in 30 | List.init ~len:n_threads ~f:(fun _ -> 31 | let result = ref [] in 32 | (Thread.create worker result, result)) 33 | |> List.fold_left 34 | ~f:(fun acc (thread, result_ref) -> 35 | Thread.join thread; 36 | !result_ref @ acc) 37 | ~init:[] 38 | -------------------------------------------------------------------------------- /lib/parallel.mli: -------------------------------------------------------------------------------- 1 | val map : f:('a -> 'b) -> 'a list -> 'b list 2 | (** [map ~f lst] Applies f to each element of lst and returns the resulting list. 3 | This is done concurrently using threads, so f needs to be thread-safe. Also 4 | the list order might not be preserved. *) 5 | -------------------------------------------------------------------------------- /lib/persist.ml: -------------------------------------------------------------------------------- 1 | (* Copyright (c) 2018 Anil Madhavapeddy 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 | * 15 | *) 16 | 17 | open Bos 18 | open Import 19 | open Result.O 20 | 21 | let load_sexp label conv file = 22 | Logs.debug (fun l -> l "Reading file %a for %s" Fpath.pp file label); 23 | let* b = OS.File.read file in 24 | try Sexplib.Sexp.of_string b |> conv |> Result.ok 25 | with exn -> 26 | Rresult.R.error_msg 27 | (Fmt.str "Error parsing %a: %s" Fpath.pp file (Printexc.to_string exn)) 28 | 29 | let save_sexp label conv file v = 30 | Logs.debug (fun l -> l "Writing file %a for %s" Fpath.pp file label); 31 | let b = Sexplib.Sexp.to_string_hum (conv v) in 32 | OS.File.write file (b ^ "\n") 33 | 34 | let write_lines_hum path content = OS.File.write_lines path (content @ [ "" ]) 35 | -------------------------------------------------------------------------------- /lib/persist.mli: -------------------------------------------------------------------------------- 1 | val load_sexp : 2 | string -> (Sexplib.Sexp.t -> 'a) -> Fpath.t -> ('a, [> Rresult.R.msg ]) result 3 | 4 | val save_sexp : 5 | string -> 6 | ('a -> Sexplib.Sexp.t) -> 7 | Fpath.t -> 8 | 'a -> 9 | (unit, [> Rresult.R.msg ]) result 10 | 11 | val write_lines_hum : 12 | Fpath.t -> string list -> (unit, [> Rresult.R.msg ]) result 13 | (** Same as [Bos.OS.File.write_lines] but adds a newline at the end of the file. *) 14 | -------------------------------------------------------------------------------- /lib/pin_depends.ml: -------------------------------------------------------------------------------- 1 | open Import 2 | 3 | type t = OpamPackage.t * OpamUrl.t 4 | 5 | let equal (pkg, url) (pkg', url') = OpamPackage.equal pkg pkg' && url = url' 6 | 7 | let pp fmt (pkg, url) = 8 | Format.fprintf fmt "(%a, %a)" Opam.Pp.package pkg Opam.Pp.url url 9 | 10 | let sort_uniq pin_depends = 11 | let open Result.O in 12 | let add acc ((pkg, url) as t) = 13 | let name = OpamPackage.name pkg in 14 | match OpamPackage.Name.Map.find_opt name acc with 15 | | None -> Ok (OpamPackage.Name.Map.add name (pkg, url) acc) 16 | | Some t' when equal t t' -> Ok acc 17 | | Some (pkg', url') -> 18 | Rresult.R.error_msgf 19 | "Package %a is pinned to different versions/url:\n\ 20 | \ - %a: %a\n\ 21 | \ - %a: %a" Opam.Pp.package_name name Opam.Pp.package pkg Opam.Pp.url 22 | url Opam.Pp.package pkg' Opam.Pp.url url' 23 | in 24 | let+ map = 25 | Result.List.fold_left ~init:OpamPackage.Name.Map.empty ~f:add pin_depends 26 | in 27 | OpamPackage.Name.Map.values map 28 | 29 | let group_by_url t_list = 30 | List.fold_left 31 | ~f:(fun acc (pkg, url) -> 32 | OpamUrl.Map.update url (fun l -> pkg :: l) [ pkg ] acc) 33 | ~init:OpamUrl.Map.empty t_list 34 | -------------------------------------------------------------------------------- /lib/pin_depends.mli: -------------------------------------------------------------------------------- 1 | open! Import 2 | 3 | type t = OpamPackage.t * OpamUrl.t 4 | 5 | val equal : t -> t -> bool 6 | val pp : Format.formatter -> t -> unit 7 | 8 | val sort_uniq : t list -> (t list, [> `Msg of string ]) result 9 | (** Sorts and deduplicate all the combined pin-depends of a repository. 10 | Returns an error if the same package is pinned twice to different URLs or versions. *) 11 | 12 | val group_by_url : t list -> OpamPackage.t list OpamUrl.Map.t 13 | -------------------------------------------------------------------------------- /lib/pp.ml: -------------------------------------------------------------------------------- 1 | let plural_int fmt = 2 | Fmt.using (fun n -> if n > 1 then "s" else "") Fmt.string fmt 3 | 4 | let plural fmt l = Fmt.using List.length plural_int fmt l 5 | 6 | module Styled = struct 7 | let header = Fmt.(styled `Blue (const string "==> ")) 8 | let question_header = Fmt.(styled `Magenta (const string "??? ")) 9 | let header_indent = Fmt.(const string " ") 10 | let branch = Fmt.(styled `Cyan string) 11 | let commit = branch 12 | let package_name = Fmt.(styled `Yellow string) 13 | let path fmt path = Fmt.(styled `Cyan Fpath.pp) fmt (Fpath.normalize path) 14 | let good pp = Fmt.(styled `Green pp) 15 | let bad pp = Fmt.(styled `Red pp) 16 | 17 | let cached fmt cached = 18 | if cached then Fmt.(styled `Green (const string " [CACHED]")) fmt () else () 19 | end 20 | -------------------------------------------------------------------------------- /lib/pp.mli: -------------------------------------------------------------------------------- 1 | val plural : 'a list Fmt.t 2 | val plural_int : int Fmt.t 3 | 4 | module Styled : sig 5 | val header : unit Fmt.t 6 | val question_header : unit Fmt.t 7 | val header_indent : unit Fmt.t 8 | val branch : string Fmt.t 9 | val commit : string Fmt.t 10 | val package_name : string Fmt.t 11 | val path : Fpath.t Fmt.t 12 | val good : 'a Fmt.t -> 'a Fmt.t 13 | val bad : 'a Fmt.t -> 'a Fmt.t 14 | 15 | val cached : bool Fmt.t 16 | (** [cached fmt c] formats [" [CACHED]"] if [c] is [true] and formats nothing 17 | otherwise. 18 | You should use this to format suffixes of logs that described actions that 19 | can be cached. *) 20 | end 21 | -------------------------------------------------------------------------------- /lib/pp_combinators.ml: -------------------------------------------------------------------------------- 1 | module Ocaml = struct 2 | let string fmt s = Format.fprintf fmt "%S" s 3 | let bool fmt b = Format.fprintf fmt "%B" b 4 | 5 | let option ?(brackets = true) pp_a fmt = function 6 | | None -> Format.fprintf fmt "None" 7 | | Some a when brackets -> Format.fprintf fmt "Some (%a)" pp_a a 8 | | Some a -> Format.fprintf fmt "Some %a" pp_a a 9 | 10 | let list pp_a fmt = function 11 | | [] -> Format.fprintf fmt "[]" 12 | | [ a ] -> Format.fprintf fmt "[%a]" pp_a a 13 | | l -> 14 | let pp_sep fmt () = Format.fprintf fmt ";@ " in 15 | Format.fprintf fmt "@[[@ %a@ ]@]" 16 | (Format.pp_print_list ~pp_sep pp_a) 17 | l 18 | 19 | let pair pp_a pp_b fmt (a, b) = 20 | Format.fprintf fmt "@[(@ %a,@ %a@ )@]" pp_a a pp_b b 21 | end 22 | 23 | module Opam = struct 24 | module type Printable = sig 25 | type t 26 | 27 | val pp : t Fmt.t 28 | end 29 | 30 | module Make_Set (M : OpamStd.SET) (P : Printable with type t = M.elt) : sig 31 | val pp : ?sep:unit Fmt.t -> M.t Fmt.t 32 | end = struct 33 | let pp ?sep = Fmt.iter ?sep M.iter P.pp 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/pp_combinators.mli: -------------------------------------------------------------------------------- 1 | module Ocaml : sig 2 | (** Combinators to pretty print values as their OCaml representation *) 3 | 4 | val string : string Fmt.t 5 | (** [string fmt s] pretty prints [s], with quotes *) 6 | 7 | val bool : bool Fmt.t 8 | 9 | val option : ?brackets:bool -> 'a Fmt.t -> 'a option Fmt.t 10 | (** [ocaml_option ~brackets pp_a fmt a_option] pretty prints an ocaml option as 11 | ["None"], ["Some %a"] or ["Some (%a)"] if [brackets] is true. It uses [pp_a] to print 12 | the value wrapped in the option. 13 | [brackets] default to false. *) 14 | 15 | val list : 'a Fmt.t -> 'a list Fmt.t 16 | (** [ocaml_list pp_a fmt l] pretty prints l as an ocaml looking list, using [pp_a] to format 17 | individual elements. *) 18 | 19 | val pair : 'a Fmt.t -> 'b Fmt.t -> ('a * 'b) Fmt.t 20 | end 21 | 22 | module Opam : sig 23 | module type Printable = sig 24 | type t 25 | 26 | val pp : t Fmt.t 27 | end 28 | 29 | module Make_Set (M : OpamStd.SET) (_ : Printable with type t = M.elt) : sig 30 | val pp : ?sep:unit Fmt.t -> M.t Fmt.t 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/project.ml: -------------------------------------------------------------------------------- 1 | open Import 2 | 3 | type t = Fpath.t 4 | 5 | (** Do not search for opam files in those folders *) 6 | let folder_ignore_list = 7 | [ "_build"; "_opam"; Fpath.to_string Config.vendor_dir ] 8 | 9 | let has_repo_file opath = 10 | let repo_file = OpamRepositoryPath.repo opath in 11 | match OpamFile.Repo.read_opt repo_file with 12 | | None -> false 13 | | Some _ -> true 14 | | (exception OpamPp.Bad_format _) 15 | | (exception OpamPp.Bad_format_list _) 16 | | (exception OpamPp.Bad_version _) -> 17 | false 18 | 19 | let has_packages_folder opath = 20 | let packages = OpamRepositoryPath.packages_dir opath in 21 | OpamFilename.exists_dir packages 22 | 23 | let is_opam_repo dir = 24 | let open Result.O in 25 | let* is_dir = Bos.OS.Dir.exists dir in 26 | if not is_dir then Ok false 27 | else 28 | let opath = OpamFilename.Dir.of_string (Fpath.to_string dir) in 29 | Ok (has_packages_folder opath && has_repo_file opath) 30 | 31 | let is_ignore_listed path = 32 | List.mem ~set:folder_ignore_list (Fpath.to_string (Fpath.base path)) 33 | 34 | let search_for_local_packages path = 35 | let open Result.O in 36 | if is_ignore_listed path then Ok false 37 | else 38 | let* is_opam_repo = is_opam_repo path in 39 | Ok (not is_opam_repo) 40 | 41 | let local_packages ~recurse t = 42 | let open Result.O in 43 | let* exists = Bos.OS.Dir.exists t in 44 | if not exists then Ok [] 45 | else 46 | let traverse = 47 | if recurse then `Sat search_for_local_packages 48 | else `Sat (fun p -> Ok (Fpath.equal p t)) 49 | in 50 | Bos.OS.Path.fold 51 | ~elements:(`Sat (fun p -> Ok (Fpath.has_ext ".opam" p))) 52 | ~traverse 53 | (fun path acc -> 54 | let pkg_name = 55 | Fpath.(basename (rem_ext path)) |> OpamPackage.Name.of_string 56 | in 57 | Fpath.(pkg_name, t // path) :: acc) 58 | [] [ t ] 59 | 60 | let all_local_packages t = local_packages ~recurse:true t 61 | let dune_project t = Fpath.(t / "dune-project") 62 | 63 | let name t = 64 | let open Result.O in 65 | let dune_project = dune_project t in 66 | let* exists = Bos.OS.File.exists dune_project in 67 | match exists with 68 | | true -> Dune_file.Raw.as_sexps dune_project >>= Dune_file.Project.name 69 | | false -> 70 | Rresult.R.error_msgf "Missing dune-project file at the root: %a" Fpath.pp 71 | dune_project 72 | 73 | let lockfile ~name t = Fpath.(t / (name ^ Config.lockfile_ext)) 74 | 75 | let lockfile ~target_packages t = 76 | let open Result.O in 77 | match target_packages with 78 | | [ name ] -> 79 | let name = OpamPackage.Name.to_string name in 80 | Ok (lockfile ~name t) 81 | | _ -> 82 | let+ name = name t in 83 | lockfile ~name t 84 | 85 | let local_lockfiles repo = 86 | let open Result.O in 87 | let+ content = Bos.OS.Dir.contents ~dotfiles:false repo in 88 | List.filter 89 | ~f:(fun path -> 90 | let is_file = 91 | match Bos.OS.File.exists path with Ok b -> b | _ -> false 92 | in 93 | is_file && Fpath.has_ext Config.lockfile_ext path) 94 | content 95 | -------------------------------------------------------------------------------- /lib/project.mli: -------------------------------------------------------------------------------- 1 | (** Utility functions to extract project specific path and values *) 2 | 3 | type t = Fpath.t 4 | (** The type of projects. 5 | 6 | What we consider a project here is the root of a dune project/workspace *) 7 | 8 | val local_packages : 9 | recurse:bool -> 10 | t -> 11 | ((OpamPackage.Name.t * Fpath.t) list, [> Rresult.R.msg ]) result 12 | (** Returns the locally defined opam packages as an association list from package names to 13 | to the corresponding .opam file path. 14 | Only considers packages defined at the repo's root unless [recurse] is [true]. *) 15 | 16 | val all_local_packages : 17 | t -> ((OpamPackage.Name.t * Fpath.t) list, [> Rresult.R.msg ]) result 18 | (** [all_local_packages t] is [local_packages ~recurse:true t]. *) 19 | 20 | val dune_project : t -> Fpath.t 21 | (** Returns the path to the dune-project file. *) 22 | 23 | val name : t -> (string, [> `Msg of string ]) result 24 | (** Returns the name of the project, as set in the dune-project. *) 25 | 26 | val lockfile : 27 | target_packages:OpamPackage.Name.t list -> 28 | t -> 29 | (Fpath.t, [> `Msg of string ]) result 30 | (** Returns the path to the opam-monorepo lockfile to generate for the given 31 | project and lockfile target packages. 32 | If there is a single target package, then it is the [".opam.locked"] 33 | file at the root of the project. 34 | If it contains multiple packages, then it's the [".opam.locked"] file 35 | at the root of the project, where is the name as defined in the 36 | dune-project file. *) 37 | 38 | val local_lockfiles : t -> (Fpath.t list, Rresult.R.msg) result 39 | (** Returns all the lockfiles located at the root of the project i.e. all 40 | .opam.locked files. *) 41 | -------------------------------------------------------------------------------- /lib/pull.ml: -------------------------------------------------------------------------------- 1 | open Import 2 | 3 | (* Check that [output_dir] is strictly a descendant of [duniverse_dir] *) 4 | let is_in_universe_dir ~duniverse_dir ~output_dir = 5 | Fpath.(is_prefix (normalize duniverse_dir) (normalize output_dir)) 6 | && not (String.equal (Fpath.filename output_dir) "") 7 | 8 | (* Delete version control metadata and vendor subdirectory *) 9 | let do_trim_clone output_dir = 10 | let open Result.O in 11 | let* () = 12 | Bos.OS.Dir.delete ~must_exist:false ~recurse:true 13 | Fpath.(output_dir / ".git") 14 | in 15 | Bos.OS.Dir.delete ~recurse:true Fpath.(output_dir // Config.vendor_dir) 16 | 17 | let pull ?(trim_clone = false) ~global_state ~duniverse_dir src_dep = 18 | let open Result.O in 19 | let open Duniverse.Repo in 20 | let { dir; url; hashes; _ } = src_dep in 21 | let output_dir = Fpath.(duniverse_dir / dir) in 22 | if is_in_universe_dir ~duniverse_dir ~output_dir then 23 | let url = Url.to_opam_url url in 24 | let open OpamProcess.Job.Op in 25 | Opam.pull_tree ~url ~hashes ~dir:output_dir global_state @@| fun result -> 26 | let* () = result in 27 | if trim_clone then do_trim_clone output_dir else Ok () 28 | else 29 | let error = 30 | Rresult.R.error_msgf 31 | "Refusing to pull %s into directory %s as it is not inside the \ 32 | directory %s" 33 | (Url.to_string url) 34 | (Fpath.to_string output_dir) 35 | (Fpath.to_string duniverse_dir) 36 | in 37 | Done error 38 | 39 | let pull_source_dependencies ?trim_clone ~global_state ~duniverse_dir src_deps = 40 | let open Result.O in 41 | let jobs = !OpamStateConfig.r.dl_jobs in 42 | let* _ = 43 | OpamParallel.map ~jobs 44 | ~command:(pull ?trim_clone ~global_state ~duniverse_dir) 45 | src_deps 46 | |> List.fold_left 47 | ~f:(fun acc r -> 48 | match acc, r with 49 | | Error _ as e, _ | Ok _, (Error _ as e) -> e 50 | | Ok acc, Ok r -> Ok (r :: acc)) 51 | ~init:(Ok []) 52 | |> Result.map List.rev 53 | in 54 | let total = List.length src_deps in 55 | let pp_count = Pp.Styled.good Fmt.int in 56 | Logs.app (fun l -> 57 | l "Successfully pulled %a/%a repositories" pp_count total pp_count total); 58 | Ok () 59 | 60 | let mark_duniverse_content_as_vendored ~duniverse_dir = 61 | let open Result.O in 62 | let dune_file = Fpath.(duniverse_dir / "dune") in 63 | let content = Dune_file.Raw.duniverse_dune_content in 64 | Logs.debug (fun l -> 65 | l "Writing %a:\n %s" Pp.Styled.path dune_file 66 | (String.concat ~sep:"\n" content)); 67 | let* () = Persist.write_lines_hum dune_file content in 68 | Logs.debug (fun l -> l "Successfully wrote %a" Pp.Styled.path dune_file); 69 | Ok () 70 | 71 | let pre_pull_clean_up ~full ~preserve_symlinks ~duniverse_dir duniverse = 72 | let open Result.O in 73 | match full with 74 | | true -> 75 | let* () = 76 | Bos.OS.Dir.delete ~must_exist:false ~recurse:true duniverse_dir 77 | in 78 | Ok [] 79 | | false -> 80 | let* preserved = 81 | Result.List.map duniverse ~f:(fun { Duniverse.Repo.dir; _ } -> 82 | let directory = Fpath.(duniverse_dir / dir) in 83 | let* stat = Bos.OS.Path.symlink_stat directory in 84 | match (preserve_symlinks, stat.st_kind) with 85 | | true, S_LNK -> Ok (Some dir) 86 | | _, _ -> 87 | let* () = 88 | Bos.OS.Dir.delete ~must_exist:false ~recurse:true directory 89 | in 90 | Ok None) 91 | in 92 | preserved |> List.filter_map ~f:Fun.id |> Result.ok 93 | 94 | let duniverse_documentation = 95 | {|# duniverse 96 | 97 | This folder contains vendored source code of the dependencies of the project, 98 | created by the [opam-monorepo](https://github.com/ocamllabs/opam-monorepo) 99 | tool. You can find the packages and versions that are included in this folder 100 | in the `.opam.locked` files. 101 | 102 | To update the packages do not modify the files and directories by hand, instead 103 | use `opam-monorepo` to keep the lockfiles and directory contents accurate and 104 | in sync: 105 | 106 | ```sh 107 | opam monorepo lock 108 | opam monorepo pull 109 | ``` 110 | 111 | If you happen to include the `duniverse/` folder in your Git repository make 112 | sure to commit all files: 113 | 114 | ```sh 115 | git add -A duniverse/ 116 | ``` 117 | 118 | For more information check out the homepage and manual of `opam-monorepo`. 119 | |} 120 | 121 | let write_duniverse_dir_documentation ~duniverse_dir = 122 | let open Result.O in 123 | let file_name = Fpath.v "README.md" in 124 | let readme_file = Fpath.(duniverse_dir // file_name) in 125 | let* written = 126 | Bos.OS.File.with_output readme_file 127 | (fun output () -> 128 | let content = Bytes.of_string duniverse_documentation in 129 | output (Some (content, 0, Bytes.length content)) |> Result.ok) 130 | () 131 | in 132 | written 133 | 134 | let filter_preserved ~preserved duniverse = 135 | List.filter 136 | ~f:(fun { Duniverse.Repo.dir; _ } -> not @@ List.mem dir ~set:preserved) 137 | duniverse 138 | 139 | let duniverse ~full ~preserve_symlinks ~root ~global_state ~trim_clone duniverse 140 | = 141 | if duniverse = [] then Ok () 142 | else 143 | let open Result.O in 144 | let duniverse_dir = Fpath.(root // Config.vendor_dir) in 145 | let* preserved = 146 | pre_pull_clean_up ~full ~preserve_symlinks ~duniverse_dir duniverse 147 | in 148 | let duniverse = filter_preserved ~preserved duniverse in 149 | let* _created = Bos.OS.Dir.create duniverse_dir in 150 | let* () = mark_duniverse_content_as_vendored ~duniverse_dir in 151 | let* () = write_duniverse_dir_documentation ~duniverse_dir in 152 | pull_source_dependencies ~global_state ~trim_clone ~duniverse_dir duniverse 153 | -------------------------------------------------------------------------------- /lib/pull.mli: -------------------------------------------------------------------------------- 1 | val duniverse : 2 | full:bool -> 3 | preserve_symlinks:bool -> 4 | root:Fpath.t -> 5 | global_state:OpamStateTypes.unlocked OpamStateTypes.global_state -> 6 | trim_clone:bool -> 7 | Duniverse.t -> 8 | (unit, [> Rresult.R.msg ]) result 9 | (** [duniverse ~full ~preserve_symlinks ~root ~global_state duniverse] 10 | pulls duniverse repositories into the [Config.vendor_dir] of the given project [root]. 11 | If [full] is [true] then the [vendor_dir] is entirely deleted before pulling. 12 | Otherwise it will traverse the subfolders corresponding to the repos in [duniverse] 13 | and delete those that aren't symlinks if [preserve_symlinks] is set. *) 14 | -------------------------------------------------------------------------------- /lib/serial_shape.mli: -------------------------------------------------------------------------------- 1 | (** Helpers to describe the OCaml shape of data and serialize/deserialize 2 | it into different formats. *) 3 | 4 | module Conv : sig 5 | type ('repr, 'true_type) t 6 | (** Type of converters from representation types to the true type. 7 | The representation type is the simplified type used for serialization 8 | while the true type is the type of the value as we'd like to use it 9 | in OCaml code. 10 | For instance a set is usually converted to a list before serializing 11 | it. In that scenario, the list is the representatio type and the set 12 | the true type. *) 13 | 14 | val make : 15 | from_repr:('repr -> ('true_type, Rresult.R.msg) result) -> 16 | to_repr:('true_type -> 'repr) -> 17 | ?equal:('true_type -> 'true_type -> bool) -> 18 | ?pp:'true_type Fmt.t -> 19 | unit -> 20 | ('repr, 'true_type) t 21 | (** Build a converter out of conversion functions between the repr 22 | and true type. 23 | [equal] and [pp] are optional and mostly used for testing of this 24 | module. *) 25 | end 26 | 27 | type 'a t 28 | (** Type for generic data shape *) 29 | 30 | val bool : bool t 31 | val string : string t 32 | val list : 'a t -> 'a list t 33 | val pair : 'a t -> 'b t -> ('a * 'b) t 34 | val conv : ('repr, 'true_type) Conv.t -> 'repr t -> 'true_type t 35 | val choice2 : 'a t -> 'b t -> [ `C1 of 'a | `C2 of 'b ] t 36 | 37 | val choice3 : 'a t -> 'b t -> 'c t -> [ `C1 of 'a | `C2 of 'b | `C3 of 'c ] t 38 | (** [choice3 s s' s''] allows for any of [s], [s'] or [s'']. 39 | In case of ambiguity (for instance pairs and lists can be ambiguous when 40 | converting from opam values), the priority order is the order of 41 | arguments. In other words if the data could be interpreted both as 42 | [s] and [s'], it will be interpreted as [s]. 43 | Note that since cmdliner arguments have no particular structure 44 | and everything is encoded as strings, you should always make string 45 | the lowest priority. *) 46 | 47 | val from_opam_val : 48 | 'a t -> OpamParserTypes.FullPos.value -> ('a, Rresult.R.msg) result 49 | (** Deserialize from opam values *) 50 | 51 | val to_opam_val : 'a t -> 'a -> OpamParserTypes.FullPos.value 52 | (** Serialize into opam values *) 53 | 54 | val cmdliner_conv : 'a t -> 'a Cmdliner.Arg.conv 55 | (** Derive a Cmliner converter from a shape *) 56 | 57 | (**/**) 58 | 59 | (** Undocumented. Exposed for testing only *) 60 | 61 | val equal : 'a t -> 'a -> 'a -> bool 62 | val pp : 'a t -> 'a Fmt.t 63 | 64 | (**/**) 65 | -------------------------------------------------------------------------------- /lib/source_opam_config.mli: -------------------------------------------------------------------------------- 1 | (** Utilities for extracting configuration from target packages opam 2 | files available in the project sources in Opam extensions *) 3 | 4 | open Import 5 | 6 | type t = { 7 | global_vars : OpamVariable.variable_contents String.Map.t option; 8 | repositories : OpamUrl.Set.t option; 9 | opam_provided : OpamPackage.Name.Set.t option; 10 | } 11 | (** Type for solver configuration bits encoded in opam extensions 12 | of target packages opam files. 13 | [@repositories] is the explicit list of opam-repositories URLs to use 14 | for opam metadata 15 | [@global_vars] allows the user to define the opam variables to be used by 16 | the solver when running in reproducible mode. *) 17 | 18 | val get : 19 | opam_monorepo_cwd:Fpath.t -> OpamFile.OPAM.t -> (t, Rresult.R.msg) result 20 | (** Parses the config from the opam extensions in the given opam file. 21 | If the extensions are missing, the corresponding field is set to [None]. 22 | [opam_monorepo_cwd] is the absolute path from which opam monorepo was 23 | invoked. It is used to rewrite local FS URLS (["file://"]) as these have 24 | to be absolute. This makes it possible to refer to repositories defined 25 | locally, in the project. *) 26 | 27 | val set : opam_monorepo_cwd:Fpath.t -> t -> OpamFile.OPAM.t -> OpamFile.OPAM.t 28 | (** Writes the given config into the extensions of the given opam file. 29 | If a config field is [None] the corresponding field won't be added 30 | to the opam file. 31 | [opam_monorepo_cwd] is the absolute path from which opam monorepo was 32 | invoked. It is used to rewrite local FS URLS (["file://"]) as these have 33 | to be absolute. This makes it possible to refer to repositories defined 34 | locally, in the project. *) 35 | 36 | val merge : t list -> (t, Rresult.R.msg) result 37 | (** Merges config from different opam files into a single, shared config. *) 38 | 39 | type adjustment 40 | (** Type of configuration adjustment. 41 | Defines values to overwrite fields of a configuration and values 42 | to merge into existing fields. *) 43 | 44 | val cli_adjustment : adjustment Cmdliner.Term.t 45 | (** Set of CLI options used to overwrite or complement fields of a config. *) 46 | 47 | val make : 48 | opam_monorepo_cwd:Fpath.t -> 49 | adjustment:adjustment -> 50 | local_opam_files_config:t -> 51 | (t, Rresult.R.msg) result 52 | (** Assembles the final config by properly combining all sources. 53 | If a field is defined (i.e. is not [None]) in [overwrite_config], 54 | its value will be used, ignoring the field value from both other sources. 55 | If it is not, the combined value of [add_config] and 56 | [local_opam_files_config] will be used. *) 57 | 58 | (**/**) 59 | 60 | (** Undocumented *) 61 | 62 | module Private : sig 63 | (** Private API module used to expose functions for testing purposes. 64 | DO NOT USE! *) 65 | module Opam_repositories : sig 66 | val from_opam_value : 67 | OpamParserTypes.FullPos.value -> 68 | (OpamUrl.Set.t, [ `Msg of string ]) result 69 | 70 | val to_opam_value : OpamUrl.Set.t -> OpamParserTypes.FullPos.value 71 | val cmdliner_conv : OpamUrl.Set.t Cmdliner.Arg.conv 72 | end 73 | 74 | module Opam_global_vars : sig 75 | val from_opam_value : 76 | OpamParserTypes.FullPos.value -> 77 | (OpamVariable.variable_contents String.Map.t, Rresult.R.msg) result 78 | 79 | val to_opam_value : 80 | OpamVariable.variable_contents String.Map.t -> 81 | OpamParserTypes.FullPos.value 82 | 83 | val cmdliner_conv : 84 | OpamVariable.variable_contents String.Map.t Cmdliner.Arg.conv 85 | end 86 | 87 | module Opam_provided : sig 88 | val from_opam_value : 89 | OpamParserTypes.FullPos.value -> 90 | (OpamPackage.Name.Set.t, Rresult.R.msg) result 91 | 92 | val to_opam_value : OpamPackage.Name.Set.t -> OpamParserTypes.FullPos.value 93 | val cmdliner_conv : OpamPackage.Name.Set.t Cmdliner.Arg.conv 94 | end 95 | 96 | module Opam_repositories_url_rewriter : sig 97 | val rewrite_one_in : 98 | opam_monorepo_cwd:string -> OpamUrl.t -> (OpamUrl.t, Rresult.R.msg) result 99 | 100 | val rewrite_one_out : opam_monorepo_cwd:string -> OpamUrl.t -> OpamUrl.t 101 | end 102 | end 103 | 104 | (**/**) 105 | -------------------------------------------------------------------------------- /lib/uri_utils.ml: -------------------------------------------------------------------------------- 1 | open Import 2 | 3 | module Normalized = struct 4 | type t = Github of { user : string; repo : string } | Other of Uri.t 5 | 6 | let of_uri uri = 7 | match Uri.host uri with 8 | | Some "github.com" -> ( 9 | let path = Uri.path uri in 10 | match Option.map 11 | (fun idx -> 12 | (String.sub path ~pos:0 ~len:idx, 13 | String.sub path ~pos:(succ idx) ~len:(String.length path - idx - 1))) 14 | (String.index_opt path '/') with 15 | | None -> Other uri 16 | | Some (user, gitrepo) -> ( 17 | match Option.map 18 | (fun idx -> 19 | (String.sub gitrepo ~pos:0 ~len:idx, 20 | String.sub gitrepo ~pos:(succ idx) ~len:(String.length gitrepo - idx - 1))) 21 | (String.rindex_opt gitrepo '.') with 22 | | None -> Github { user; repo = gitrepo } 23 | | Some (repo, "git") -> Github { user; repo } 24 | | Some _ -> Other uri)) 25 | | Some _ | None -> Other uri 26 | 27 | let equal a b = 28 | match (a, b) with 29 | | Other a, Other b -> Uri.equal a b 30 | | Github { user; repo }, Github { user = user'; repo = repo' } -> 31 | String.equal user user' && String.equal repo repo' 32 | | _, _ -> false 33 | 34 | let pp ppf = function 35 | | Github { user; repo } -> Fmt.pf ppf "" user repo 36 | | Other uri -> Fmt.pf ppf "" Uri.pp uri 37 | end 38 | -------------------------------------------------------------------------------- /lib/uri_utils.mli: -------------------------------------------------------------------------------- 1 | (** One way normalization of URIs. 2 | Not meant to expose the normalized value of the URI again *) 3 | module Normalized : sig 4 | type t 5 | (** Abstracts away the actual value which is not to be used directly *) 6 | 7 | val of_uri : Uri.t -> t 8 | (** Returns a canonical representation of the URI *) 9 | 10 | val equal : t -> t -> bool 11 | (** Determines whether two normalized URIs are equal *) 12 | 13 | val pp : t Fmt.t 14 | (** Pretty printer for normalized URLs. *) 15 | end 16 | -------------------------------------------------------------------------------- /opam-monorepo.opam: -------------------------------------------------------------------------------- 1 | # This file is generated by dune, edit dune-project instead 2 | opam-version: "2.0" 3 | synopsis: "Assemble and manage fully vendored Dune repositories" 4 | description: """ 5 | The opam monorepo plugin provides a convenient interface to bridge the 6 | opam package manager with having a local copy of all the source 7 | code required to build a project using the dune build tool.""" 8 | maintainer: ["anil@recoil.org"] 9 | authors: [ 10 | "Anil Madhavapeddy" "Nathan Rebours" "Lucas Pluvinage" "Jules Aguillon" 11 | ] 12 | license: "ISC" 13 | homepage: "https://github.com/tarides/opam-monorepo" 14 | doc: "https://tarides.github.io/opam-monorepo" 15 | bug-reports: "https://github.com/tarides/opam-monorepo/issues" 16 | depends: [ 17 | "dune" {>= "3.6"} 18 | "ocaml" {>= "4.13.0"} 19 | "dune-build-info" 20 | "bos" 21 | "cmdliner" {>= "1.1.0"} 22 | "fmt" 23 | "logs" 24 | "opam-file-format" {>= "2.1.0"} 25 | "opam-format" {>= "2.2.1"} 26 | "opam-state" {>= "2.2.1"} 27 | "opam-0install" {>= "0.5"} 28 | "ocaml-version" 29 | "sexplib" 30 | "uri" 31 | "alcotest" {with-test} 32 | "odoc" {with-doc} 33 | ] 34 | conflicts: [ 35 | "dune-build-info" {= "2.7.0" | = "2.7.1"} 36 | "dune-configurator" {= "2.7.0" | = "2.7.1"} 37 | ] 38 | dev-repo: "git+https://github.com/tarides/opam-monorepo.git" 39 | build: [ "dune" "build" "-p" name "-j" jobs "@install" "@runtest" {with-test} ] 40 | flags: [ plugin ] 41 | -------------------------------------------------------------------------------- /opam-monorepo.opam.template: -------------------------------------------------------------------------------- 1 | build: [ "dune" "build" "-p" name "-j" jobs "@install" "@runtest" {with-test} ] 2 | flags: [ plugin ] 3 | -------------------------------------------------------------------------------- /stdext/dune: -------------------------------------------------------------------------------- 1 | (library 2 | (name stdext)) 3 | -------------------------------------------------------------------------------- /stdext/list.ml: -------------------------------------------------------------------------------- 1 | include ListLabels 2 | -------------------------------------------------------------------------------- /stdext/list.mli: -------------------------------------------------------------------------------- 1 | include module type of struct 2 | include ListLabels 3 | end 4 | -------------------------------------------------------------------------------- /stdext/map.ml: -------------------------------------------------------------------------------- 1 | module type S = sig 2 | include MoreLabels.Map.S 3 | 4 | val mem : 'a t -> key -> bool 5 | val set : 'a t -> key -> 'a -> 'a t 6 | val find : 'a t -> key -> 'a option 7 | val update : 'a t -> key -> f:('a option -> 'a option) -> 'a t 8 | val values : 'a t -> 'a list 9 | val keys : 'a t -> key list 10 | val of_list : (key * 'a) list -> ('a t, key * 'a * 'a) Result.t 11 | val of_list_exn : (key * 'a) list -> 'a t 12 | val of_list_map_exn : 'a list -> f:('a -> key * 'b) -> 'b t 13 | end 14 | 15 | module type Key = sig 16 | include MoreLabels.Map.OrderedType 17 | end 18 | 19 | module Make (Key : Key) : S with type key = Key.t = struct 20 | include MoreLabels.Map.Make (Key) 21 | 22 | let mem t k = mem k t 23 | let find key t = find_opt t key 24 | let update t k ~f = update ~key:k ~f t 25 | let set t k v = add ~key:k ~data:v t 26 | let foldi t ~init ~f = fold t ~init ~f:(fun ~key ~data acc -> f key data acc) 27 | let values t = foldi t ~init:[] ~f:(fun _ v l -> v :: l) |> List.rev 28 | let keys t = foldi t ~init:[] ~f:(fun k _ l -> k :: l) |> List.rev 29 | 30 | let of_list = 31 | let rec loop acc = function 32 | | [] -> Result.Ok acc 33 | | (k, v) :: l -> ( 34 | match find acc k with 35 | | None -> loop (set acc k v) l 36 | | Some v_old -> Error (k, v_old, v)) 37 | in 38 | fun l -> loop empty l 39 | 40 | let of_list_map = 41 | let rec loop f acc = function 42 | | [] -> Result.Ok acc 43 | | x :: l -> 44 | let k, v = f x in 45 | if not (mem acc k) then loop f (set acc k v) l else Error k 46 | in 47 | fun l ~f -> 48 | match loop f empty l with 49 | | Result.Ok _ as x -> x 50 | | Error k -> ( 51 | match 52 | List.filter l ~f:(fun x -> 53 | match Key.compare (fst (f x)) k with 0 -> true | _ -> false) 54 | with 55 | | x :: y :: _ -> Error (k, x, y) 56 | | _ -> assert false) 57 | 58 | let of_list_map_exn t ~f = 59 | match of_list_map t ~f with 60 | | Result.Ok x -> x 61 | | Error (_, _, _) -> invalid_arg "Map.of_list_map_exn" 62 | 63 | let of_list_exn l = 64 | match of_list l with 65 | | Result.Ok x -> x 66 | | Error (_, _, _) -> invalid_arg "Map.of_list_exn" 67 | end 68 | -------------------------------------------------------------------------------- /stdext/map.mli: -------------------------------------------------------------------------------- 1 | module type S = sig 2 | include MoreLabels.Map.S 3 | 4 | val mem : 'a t -> key -> bool 5 | val set : 'a t -> key -> 'a -> 'a t 6 | val find : 'a t -> key -> 'a option 7 | val update : 'a t -> key -> f:('a option -> 'a option) -> 'a t 8 | val values : 'a t -> 'a list 9 | val keys : 'a t -> key list 10 | val of_list : (key * 'a) list -> ('a t, key * 'a * 'a) Result.t 11 | val of_list_exn : (key * 'a) list -> 'a t 12 | val of_list_map_exn : 'a list -> f:('a -> key * 'b) -> 'b t 13 | end 14 | 15 | module type Key = sig 16 | include MoreLabels.Map.OrderedType 17 | end 18 | 19 | module Make (Key : Key) : S with type key = Key.t 20 | -------------------------------------------------------------------------------- /stdext/option.ml: -------------------------------------------------------------------------------- 1 | include Stdlib.Option 2 | 3 | module O = struct 4 | let ( >>= ) = bind 5 | let ( >>| ) opt f = map f opt 6 | let ( let* ) = ( >>= ) 7 | let ( let+ ) = ( >>| ) 8 | end 9 | -------------------------------------------------------------------------------- /stdext/option.mli: -------------------------------------------------------------------------------- 1 | include module type of struct 2 | include Stdlib.Option 3 | end 4 | 5 | module O : sig 6 | val ( >>= ) : 'a t -> ('a -> 'b t) -> 'b t 7 | val ( >>| ) : 'a t -> ('a -> 'b) -> 'b t 8 | val ( let* ) : 'a t -> ('a -> 'b t) -> 'b t 9 | val ( let+ ) : 'a t -> ('a -> 'b) -> 'b t 10 | end 11 | -------------------------------------------------------------------------------- /stdext/result.ml: -------------------------------------------------------------------------------- 1 | include Stdlib.Result 2 | 3 | module O = struct 4 | let ( >>= ) = Stdlib.Result.bind 5 | let ( >>| ) res f = Stdlib.Result.map f res 6 | let ( let* ) = ( >>= ) 7 | let ( let+ ) = ( >>| ) 8 | end 9 | 10 | module List = struct 11 | open O 12 | module List = Stdlib.List 13 | 14 | let map ~f l = 15 | let rec aux acc = function 16 | | [] -> Ok (List.rev acc) 17 | | hd :: tl -> ( 18 | match f hd with 19 | | Ok hd' -> aux (hd' :: acc) tl 20 | | Error err -> Error err) 21 | in 22 | aux [] l 23 | 24 | let rec iter ~f l = 25 | match l with 26 | | [] -> Ok () 27 | | hd :: tl -> 28 | let* () = f hd in 29 | iter ~f tl 30 | 31 | let rec fold_left t ~f ~init = 32 | match t with 33 | | [] -> Ok init 34 | | x :: xs -> 35 | let* init = f init x in 36 | fold_left xs ~f ~init 37 | 38 | let rec exists t ~f = 39 | match t with 40 | | [] -> Ok false 41 | | x :: xs -> 42 | let* p = f x in 43 | if p then Ok true else exists xs ~f 44 | end 45 | -------------------------------------------------------------------------------- /stdext/result.mli: -------------------------------------------------------------------------------- 1 | include module type of struct 2 | include Stdlib.Result 3 | end 4 | 5 | module O : sig 6 | val ( >>= ) : ('a, 'err) t -> ('a -> ('b, 'err) t) -> ('b, 'err) t 7 | val ( >>| ) : ('a, 'err) t -> ('a -> 'b) -> ('b, 'err) t 8 | val ( let* ) : ('a, 'err) t -> ('a -> ('b, 'err) t) -> ('b, 'err) t 9 | val ( let+ ) : ('a, 'err) t -> ('a -> 'b) -> ('b, 'err) t 10 | end 11 | 12 | module List : sig 13 | val map : f:('a -> ('b, 'err) t) -> 'a list -> ('b list, 'err) t 14 | val iter : f:('a -> (unit, 'err) t) -> 'a list -> (unit, 'err) t 15 | 16 | val fold_left : 17 | 'a list -> f:('acc -> 'a -> ('acc, 'c) t) -> init:'acc -> ('acc, 'c) t 18 | 19 | val exists : 'a list -> f:('a -> (bool, 'err) t) -> (bool, 'err) t 20 | (** Same as [List.exists] with a predicate that can return an error. 21 | Returns [Ok true] if there is at least one element in the list 22 | that satisfies the predicate [f]. 23 | Returns [Ok false] on empty lists. 24 | Returns [Error _] immediatly if the predicate does. *) 25 | end 26 | -------------------------------------------------------------------------------- /stdext/string.ml: -------------------------------------------------------------------------------- 1 | module String = Stdlib.String 2 | include StringLabels 3 | 4 | let extract_words s ~is_word_char = 5 | let rec skip_blanks i = 6 | if i = length s then [] 7 | else if is_word_char s.[i] then parse_word i (i + 1) 8 | else skip_blanks (i + 1) 9 | and parse_word i j = 10 | if j = length s then [ sub s ~pos:i ~len:(j - i) ] 11 | else if is_word_char s.[j] then parse_word i (j + 1) 12 | else sub s ~pos:i ~len:(j - i) :: skip_blanks (j + 1) 13 | in 14 | skip_blanks 0 15 | 16 | let extract_blank_separated_words s = 17 | extract_words s ~is_word_char:(function ' ' | '\t' -> false | _ -> true) 18 | 19 | module Map = Map.Make (String) 20 | -------------------------------------------------------------------------------- /stdext/string.mli: -------------------------------------------------------------------------------- 1 | include module type of struct 2 | include StringLabels 3 | end 4 | 5 | val extract_blank_separated_words : string -> string list 6 | 7 | module Map : Map.S with type key = string 8 | -------------------------------------------------------------------------------- /test/bin/cli-args-in-lockfile.t/cli-args-in-lockfile.opam: -------------------------------------------------------------------------------- 1 | opam-version: "2.0" 2 | depends: [ 3 | "dune" 4 | "ocaml" 5 | ] 6 | -------------------------------------------------------------------------------- /test/bin/cli-args-in-lockfile.t/run.t: -------------------------------------------------------------------------------- 1 | When generating a lockfile, the CLI arguments passed to lock 2 | should be save to the lockfile 3 | 4 | $ gen-minimal-repo 5 | $ opam-monorepo lock --recurse --opam-provided [b] --opam-repositories '[file://$OPAM_MONOREPO_CWD/minimal-repo]' --ocaml-version 4.13.1 > /dev/null 6 | $ opam show --just-file -fx-opam-monorepo-cli-args ./cli-args-in-lockfile.opam.locked 7 | --recurse, --opam-provided, [b], --opam-repositories, [file://$OPAM_MONOREPO_CWD/minimal-repo], --ocaml-version, 4.13.1 8 | -------------------------------------------------------------------------------- /test/bin/compiler-beta.t/dont-want-beta.opam: -------------------------------------------------------------------------------- 1 | opam-version: "2.0" 2 | depends: [ 3 | "ocaml" 4 | "dune" 5 | ] 6 | x-opam-monorepo-opam-repositories: [ 7 | "file://$OPAM_MONOREPO_CWD/minimal-repo" 8 | "file://$OPAM_MONOREPO_CWD/repo-with-beta-ocaml" 9 | ] 10 | -------------------------------------------------------------------------------- /test/bin/compiler-beta.t/repo-with-beta-ocaml/packages/ocaml-base-compiler/ocaml-base-compiler.4.14.0~beta1/opam: -------------------------------------------------------------------------------- 1 | opam-version: "2.0" 2 | synopsis: "First beta release of OCaml 4.14.0" 3 | maintainer: "platform@lists.ocaml.org" 4 | license: "LGPL-2.1-or-later WITH OCaml-LGPL-linking-exception" 5 | authors: "Xavier Leroy and many contributors" 6 | homepage: "https://ocaml.org" 7 | bug-reports: "https://github.com/ocaml/opam-repository/issues" 8 | dev-repo: "git+https://github.com/ocaml/ocaml#4.14" 9 | depends: [ 10 | "ocaml" {= "4.14.0" & post} 11 | "base-unix" {post} 12 | "base-bigarray" {post} 13 | "base-threads" {post} 14 | "ocaml-options-vanilla" {post} 15 | "ocaml-beta" {opam-version < "2.1.0"} 16 | ] 17 | conflict-class: "ocaml-core-compiler" 18 | flags: [ compiler avoid-version ] 19 | setenv: CAML_LD_LIBRARY_PATH = "%{lib}%/stublibs" 20 | build: [ 21 | [ 22 | "./configure" 23 | "--prefix=%{prefix}%" 24 | "--docdir=%{doc}%/ocaml" 25 | "-C" 26 | "CC=cc" {os = "openbsd" | os = "macos"} 27 | "ASPP=cc -c" {os = "openbsd" | os = "macos"} 28 | ] 29 | [make "-j%{jobs}%"] 30 | ] 31 | install: [make "install"] 32 | url { 33 | src: "https://github.com/ocaml/ocaml/archive/4.14.0-beta1.tar.gz" 34 | checksum: "sha256=1b02000867fd20af59c2afa1eda411d064f02cf96227f7f3e07fbeb5492abe95" 35 | } 36 | extra-files: ["ocaml-base-compiler.install" "md5=3e969b841df1f51ca448e6e6295cb451"] 37 | post-messages: [ 38 | "A failure in the middle of the build may be caused by build parallelism 39 | (enabled by default). 40 | Please file a bug report at https://github.com/ocaml/opam-repository/issues" 41 | {failure & jobs > 1} 42 | "You can try installing again including --jobs=1 43 | to force a sequential build instead." 44 | {failure & jobs > 1 & opam-version >= "2.0.5"} 45 | ] 46 | -------------------------------------------------------------------------------- /test/bin/compiler-beta.t/repo-with-beta-ocaml/packages/ocaml/ocaml.4.14.0/opam: -------------------------------------------------------------------------------- 1 | opam-version: "2.0" 2 | license: "LGPL-2.1-or-later WITH OCaml-LGPL-linking-exception" 3 | synopsis: "The OCaml compiler (virtual package)" 4 | description: """ 5 | This package requires a matching implementation of OCaml, 6 | and polls it to initialise specific variables like `ocaml:native-dynlink`""" 7 | maintainer: "platform@lists.ocaml.org" 8 | depends: [ 9 | "ocaml-config" {>= "2"} 10 | "ocaml-base-compiler" {>= "4.14.0~" & < "4.14.1~"} | 11 | "ocaml-variants" {>= "4.14.0~" & < "4.14.1~"} | 12 | "ocaml-system" {>= "4.14.0" & < "4.14.1~"} 13 | ] 14 | setenv: [ 15 | [CAML_LD_LIBRARY_PATH = "%{_:stubsdir}%"] 16 | [CAML_LD_LIBRARY_PATH += "%{lib}%/stublibs"] 17 | [OCAML_TOPLEVEL_PATH = "%{toplevel}%"] 18 | ] 19 | build: ["ocaml" "%{ocaml-config:share}%/gen_ocaml_config.ml" _:version _:name] 20 | build-env: CAML_LD_LIBRARY_PATH = "" 21 | homepage: "https://ocaml.org" 22 | bug-reports: "https://github.com/ocaml/opam-repository/issues" 23 | authors: [ 24 | "Xavier Leroy" 25 | "Damien Doligez" 26 | "Alain Frisch" 27 | "Jacques Garrigue" 28 | "Didier Rémy" 29 | "Jérôme Vouillon" 30 | ] 31 | flags: conf 32 | -------------------------------------------------------------------------------- /test/bin/compiler-beta.t/repo-with-beta-ocaml/repo: -------------------------------------------------------------------------------- 1 | opam-version: "2.0" 2 | -------------------------------------------------------------------------------- /test/bin/compiler-beta.t/requires-beta.opam: -------------------------------------------------------------------------------- 1 | opam-version: "2.0" 2 | depends: [ 3 | "ocaml" {>= "4.14"} 4 | "dune" 5 | ] 6 | x-opam-monorepo-opam-repositories: [ 7 | "file://$OPAM_MONOREPO_CWD/minimal-repo" 8 | "file://$OPAM_MONOREPO_CWD/repo-with-beta-ocaml" 9 | ] 10 | -------------------------------------------------------------------------------- /test/bin/compiler-beta.t/run.t: -------------------------------------------------------------------------------- 1 | We have a simple project that only depends on OCaml 2 | 3 | $ opam show --just-file -fdepends ./dont-want-beta.opam 4 | ocaml, dune 5 | 6 | We use the minimal repository which includes OCaml 4.13.1 7 | 8 | $ gen-minimal-repo 9 | 10 | We also use an extra repository which contains the packages of the ongoing beta 11 | release of OCaml 4.14.0 i.e. ocaml.4.14.0 and ocaml-base-compiler.4.14.0~beta1. 12 | Those are the actual opam files extracted from upstream opam-repository. The 13 | relevant part to this feature is that the ocaml-base-compiler package has the 14 | avoid-version flag: 15 | 16 | $ opam show --just-file -fflags repo-with-beta-ocaml/packages/ocaml-base-compiler/ocaml-base-compiler.4.14.0~beta1/opam 17 | compiler avoid-version 18 | 19 | Our package does not define an upper bound on ocaml but the beta should not be 20 | selected by default if there exists another solution, therefore here the solver 21 | should pick 4.13.1: 22 | 23 | $ opam-monorepo lock dont-want-beta > /dev/null 24 | $ opam show --just-file -fdepends ./dont-want-beta.opam.locked | grep ocaml 25 | "ocaml" {= "4.13.1"} 26 | "ocaml-base-compiler" {= "4.13.1"} 27 | "ocaml-config" {= "2"} 28 | "ocaml-options-vanilla" {= "1"} 29 | 30 | Now if we require ocaml >= 4.14, as the following package does: 31 | 32 | $ opam show --just-file -fdepends ./requires-beta.opam 33 | "ocaml" {>= "4.14"} "dune" 34 | 35 | The solver should be able to select the beta compiler since it is the only 36 | satisfying version available: 37 | 38 | $ opam-monorepo lock requires-beta > /dev/null 39 | $ opam show --just-file -fdepends ./requires-beta.opam.locked | grep ocaml 40 | "ocaml" {= "4.14.0"} 41 | "ocaml-base-compiler" {= "4.14.0~beta1"} 42 | "ocaml-config" {= "2"} 43 | "ocaml-options-vanilla" {= "1"} 44 | -------------------------------------------------------------------------------- /test/bin/config-cli-args.t/repo/packages/b/b.1/opam: -------------------------------------------------------------------------------- 1 | opam-version: "2.0" 2 | depends: [ 3 | "dune" 4 | ] 5 | dev-repo: "git+https://github.com/b/b" 6 | url { 7 | src: "https://b.com/b.tbz" 8 | checksum: [ 9 | "sha256=0000000000000000000000000000000000000000000000000000000000000000" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /test/bin/config-cli-args.t/repo/packages/c/c.1/opam: -------------------------------------------------------------------------------- 1 | opam-version: "2.0" 2 | depends: [ 3 | "dune" 4 | ] 5 | dev-repo: "git+https://github.com/c/c" 6 | url { 7 | src: "https://c.com/c.tbz" 8 | checksum: [ 9 | "sha256=0000000000000000000000000000000000000000000000000000000000000001" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /test/bin/config-cli-args.t/repo/repo: -------------------------------------------------------------------------------- 1 | opam-version: "2.0" 2 | -------------------------------------------------------------------------------- /test/bin/config-cli-args.t/run.t: -------------------------------------------------------------------------------- 1 | We have a local package 'config-cli-args' that has the following 2 | opam file: 3 | 4 | $ cat > config-cli-args.opam << EOF 5 | > opam-version: "2.0" 6 | > depends: [ 7 | > "dune" 8 | > "b" 9 | > "c" 10 | > ] 11 | > EOF 12 | 13 | It needs to use two local repositories for 'lock' to be successful. 14 | The minimal repo that we generate here: 15 | 16 | $ gen-minimal-repo 17 | 18 | that contains 'dune' but also the locally defined repo that contains 19 | 'b' and 'c'. 20 | At the moment it has no 'x-opam-monorepo-opam-repositories' extension set. 21 | We can use the '--opam-repositories' option to set it via the command line: 22 | 23 | $ opam-monorepo lock --opam-repositories '[file://$OPAM_MONOREPO_CWD/minimal-repo,file://$OPAM_MONOREPO_CWD/repo]' > /dev/null 24 | $ opam show --just-file -fx-opam-monorepo-opam-repositories ./config-cli-args.opam.locked 25 | file://$OPAM_MONOREPO_CWD/minimal-repo, file://$OPAM_MONOREPO_CWD/repo 26 | 27 | We can see from the generated lock file that it used the set of repos we provided on 28 | the command line. 29 | 30 | Now we can also use the '--add-opam-repositories' option to complement the local 31 | opam extensions. 32 | 33 | $ echo 'x-opam-monorepo-opam-repositories: ["file://$OPAM_MONOREPO_CWD/minimal-repo"]' >> ./config-cli-args.opam 34 | $ opam show --just-file -fx-opam-monorepo-opam-repositories ./config-cli-args.opam 35 | file://$OPAM_MONOREPO_CWD/minimal-repo 36 | 37 | Here we just added one of the two repos to our opam file. We can tell the solver 38 | to use an additional one by invoking: 39 | 40 | $ rm ./config-cli-args.opam.locked 41 | $ opam-monorepo lock --add-opam-repositories '[file://$OPAM_MONOREPO_CWD/repo]' > /dev/null 42 | $ opam show --just-file -fx-opam-monorepo-opam-repositories ./config-cli-args.opam.locked 43 | file://$OPAM_MONOREPO_CWD/minimal-repo, file://$OPAM_MONOREPO_CWD/repo 44 | 45 | Once again, we can see from the generated lock file that it used both repos. 46 | 47 | Note that you can use the '--opam-repositories' option to overwrite any local 48 | extension. 49 | 50 | $ sed -i -e '$d' ./config-cli-args.opam 51 | $ echo 'x-opam-monorepo-opam-repositories: ["https://a.com/a.tbz"]' >> ./config-cli-args.opam 52 | $ opam show --just-file -fx-opam-monorepo-opam-repositories ./config-cli-args.opam 53 | https://a.com/a.tbz 54 | 55 | Here we replaced our opam-repositories to point to 'https://a.com/a.tbz'. We 56 | can once again use '--opam-repositories' as in our first 'lock' attempt to 57 | overwrite that and use the proper repositories instead: 58 | 59 | $ opam-monorepo lock --opam-repositories '[file://$OPAM_MONOREPO_CWD/minimal-repo,file://$OPAM_MONOREPO_CWD/repo]' > /dev/null 60 | $ opam show --just-file -fx-opam-monorepo-opam-repositories ./config-cli-args.opam.locked 61 | file://$OPAM_MONOREPO_CWD/minimal-repo, file://$OPAM_MONOREPO_CWD/repo 62 | -------------------------------------------------------------------------------- /test/bin/depext.t/depext.opam: -------------------------------------------------------------------------------- 1 | opam-version: "2.0" 2 | depends: [ 3 | "dune" 4 | "b" 5 | ] 6 | x-opam-monorepo-opam-repositories: [ 7 | "file://$OPAM_MONOREPO_CWD/minimal-repo" 8 | "file://$OPAM_MONOREPO_CWD/repo" 9 | ] 10 | -------------------------------------------------------------------------------- /test/bin/depext.t/repo/packages/b/b.1/opam: -------------------------------------------------------------------------------- 1 | opam-version: "2.0" 2 | depends: [ 3 | "dune" 4 | ] 5 | depexts: [ 6 | ["libfantasydependency"] 7 | ] 8 | dev-repo: "git+https://github.com/b/b" 9 | url { 10 | src: "https://b.com/b.tbz" 11 | checksum: [ 12 | "sha256=0000000000000000000000000000000000000000000000000000000000000000" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /test/bin/depext.t/repo/repo: -------------------------------------------------------------------------------- 1 | opam-version: "2.0" 2 | -------------------------------------------------------------------------------- /test/bin/depext.t/run.t: -------------------------------------------------------------------------------- 1 | We want to make sure picking depexts works, even if we vendor packages 2 | 3 | We setup the default base repository 4 | 5 | $ gen-minimal-repo 6 | 7 | Here we define a package test that depends on a package `b`: 8 | 9 | $ opam show --just-file --raw -fdepends ./depext.opam 10 | dune, b 11 | 12 | Package `b` has a `depext` on a fantasy OS package. We deliberately pick a 13 | fantasy name here to make sure the behaviour is the same on all platforms. 14 | 15 | $ opam show --just-file --raw -fdepexts ./repo/packages/b/b.1/opam 16 | libfantasydependency 17 | 18 | We lock and expect the OS package to be part of the locked Opam file. 19 | 20 | $ opam-monorepo lock > /dev/null 21 | $ opam show --just-file --raw -fdepexts ./depext.opam.locked 22 | libfantasydependency 23 | -------------------------------------------------------------------------------- /test/bin/dune: -------------------------------------------------------------------------------- 1 | (cram 2 | (applies_to :whole_subtree) 3 | (deps 4 | %{bin:opam-monorepo} 5 | %{bin:gen-minimal-repo} 6 | (sandbox preserve_file_kind))) 7 | 8 | (executable 9 | (name gen_minimal_repo) 10 | (libraries opam-state opam-file-format opam-format stdext)) 11 | 12 | (env 13 | (_ 14 | (binaries 15 | (gen_minimal_repo.exe as gen-minimal-repo)))) 16 | -------------------------------------------------------------------------------- /test/bin/empty-duniverse.t/empty-duniverse.opam: -------------------------------------------------------------------------------- 1 | opam-version: "2.0" 2 | depends: [ 3 | "ocaml" 4 | "dune" 5 | ] 6 | x-opam-monorepo-opam-repositories: [ 7 | "file://$OPAM_MONOREPO_CWD/minimal-repo" 8 | ] 9 | -------------------------------------------------------------------------------- /test/bin/empty-duniverse.t/run.t: -------------------------------------------------------------------------------- 1 | One should be able to generate a lock file where no dependencies need to 2 | be vendored and that should result in an empty duniverse. 3 | That can happen if one has no external deps besides 'dune' and 'ocaml' 4 | or if those are marked as 'opam-provided' for example. 5 | 6 | Here our local package only depends on 'ocaml' and 'dune': 7 | 8 | $ opam show --just-file -fdepends ./empty-duniverse.opam 9 | ocaml, dune 10 | 11 | We should be able to successfully lock: 12 | 13 | $ gen-minimal-repo 14 | $ opam-monorepo lock > /dev/null 15 | 16 | And the lock file should not contain anything to vendor: 17 | 18 | $ opam show --just-file -fdepends ./empty-duniverse.opam.locked | grep "?vendor" 19 | [1] 20 | 21 | Finally, we should be able to pull this lock file, the tool will inform us that 22 | there is nothing to pull and will not create a duniverse: 23 | 24 | $ opam-monorepo pull 25 | ==> Using lockfile $TESTCASE_ROOT/empty-duniverse.opam.locked 26 | ==> No dependencies to pull, there's nothing to be done here! 27 | $ find . -type d -name 'duniverse' 28 | -------------------------------------------------------------------------------- /test/bin/error-on-directory-conflict.t/repo/packages/bar/bar.0.1.0/opam: -------------------------------------------------------------------------------- 1 | opam-version: "2.0" 2 | dev-repo: "git+https://github.com/bar/project" 3 | depends: [ 4 | "dune" 5 | ] 6 | build: [ "dune" "build" ] 7 | url { 8 | src: "https://test.com/bar.tbz" 9 | checksum: [ 10 | "sha256=0000000000000000000000000000000000000000000000000000000000000000" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /test/bin/error-on-directory-conflict.t/repo/packages/foo-branch/foo-branch.0.1.0/opam: -------------------------------------------------------------------------------- 1 | opam-version: "2.0" 2 | dev-repo: "git+https://github.com/foo/project.git#branch" 3 | depends: [ 4 | "dune" 5 | ] 6 | build: [ "dune" "build" ] 7 | url { 8 | src: "https://test.com/foo-branch.tbz" 9 | checksum: [ 10 | "sha256=0000000000000000000000000000000000000000000000000000000000000000" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /test/bin/error-on-directory-conflict.t/repo/packages/foo/foo.0.1.0/opam: -------------------------------------------------------------------------------- 1 | opam-version: "2.0" 2 | dev-repo: "git+https://github.com/foo/project" 3 | depends: [ 4 | "dune" 5 | ] 6 | build: [ "dune" "build" ] 7 | url { 8 | src: "https://test.com/foo.tbz" 9 | checksum: [ 10 | "sha256=0000000000000000000000000000000000000000000000000000000000000000" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /test/bin/error-on-directory-conflict.t/run.t: -------------------------------------------------------------------------------- 1 | We have a project which depends on multiple packages with different dev-repos 2 | but which happen to resolve to the same directory under duniverse. This tests 3 | that such a situation results in an explicit error. 4 | 5 | $ gen-minimal-repo 6 | $ opam-monorepo lock 7 | ==> Using 1 locally scanned package as the target. 8 | ==> Found 11 opam dependencies for the target package. 9 | ==> Querying opam database for their metadata and Dune compatibility. 10 | ==> Calculating exact pins for each of them. 11 | opam-monorepo: [ERROR] Multiple dev-repos would be vendored into the directory: duniverse/project 12 | Dev-repos: 13 | - git+https://github.com/foo/project.git#branch 14 | - git+https://github.com/foo/project 15 | - git+https://github.com/bar/project 16 | [1] 17 | -------------------------------------------------------------------------------- /test/bin/error-on-directory-conflict.t/x.opam: -------------------------------------------------------------------------------- 1 | opam-version: "2.0" 2 | depends: [ 3 | "foo" 4 | "foo-branch" 5 | "bar" 6 | ] 7 | x-opam-monorepo-opam-repositories: [ 8 | "file://$OPAM_MONOREPO_CWD/minimal-repo" 9 | "file://$OPAM_MONOREPO_CWD/repo" 10 | ] 11 | -------------------------------------------------------------------------------- /test/bin/explicit-repo.t/explicit-repo.opam: -------------------------------------------------------------------------------- 1 | opam-version: "2.0" 2 | depends: [ 3 | "dune" 4 | "git-dep" 5 | ] 6 | x-opam-monorepo-opam-repositories: [ 7 | "file://$OPAM_MONOREPO_CWD/minimal-repo" 8 | "git+file://$OPAM_MONOREPO_CWD/git-repository" 9 | ] 10 | -------------------------------------------------------------------------------- /test/bin/explicit-repo.t/run.t: -------------------------------------------------------------------------------- 1 | We want to specify an opam-repository that is a git URL. For reproducibility we 2 | want the locked repository URL to be fixed to the commit that we have used when 3 | locking. 4 | 5 | We have a project that depends on a package "git-dep". 6 | 7 | $ opam show --just-file --raw -fdepends ./explicit-repo.opam 8 | dune, git-dep 9 | 10 | We start with a minimal `opam-repository`, but the package "git-dep" is not in 11 | the minimal repository created locally: 12 | 13 | $ gen-minimal-repo 14 | $ ls minimal-repo/packages | grep git-dep 15 | [1] 16 | 17 | "git-dep" is in a repository that is accessible via git, create this repository: 18 | 19 | $ mkdir -p git-repository/packages/git-dep/git-dep.1.0 20 | $ cat >git-repository/packages/git-dep/git-dep.1.0/opam < opam-version: "2.0" 22 | > depends: [ 23 | > "dune" 24 | > ] 25 | > EOF 26 | $ cat >git-repository/repo < opam-version: "2.0" 28 | > EOF 29 | $ (cd git-repository ; git init ; git add -A ; git commit -m "initial commit") > /dev/null 2>&1 30 | $ SHORT_HASH1=$(git -C git-repository rev-parse --short HEAD) 31 | 32 | To find this package, we need to have the git repository in our 33 | `x-opam-monorepo-opam-repositories` list: 34 | 35 | $ opam show --just-file --raw -fx-opam-monorepo-opam-repositories ./explicit-repo.opam 36 | file://$OPAM_MONOREPO_CWD/minimal-repo, git+file://$OPAM_MONOREPO_CWD/git-repository 37 | 38 | Having it in there should make opam-monorepo able to find the package and lock 39 | successfully and picked up the package: 40 | 41 | $ opam-monorepo lock explicit-repo > /dev/null 42 | $ opam show --just-file --raw -fdepends ./explicit-repo.opam.locked | grep git-dep 43 | "git-dep" {= "1.0"} 44 | 45 | To make the build reproducible, opam-monorepo should have replaced the URL to 46 | the git repo by an URL to the git repo that features the commit hash of the 47 | repository at the time of locking, so users of the lock file will check out the 48 | git repository in the same state as the creator of the lock file. 49 | 50 | Therefore the list of repositories in the lockfile should have the a git repo 51 | that's the same as the previous git-repository path but end with "#$SHORT_HASH" 52 | 53 | $ opam show --just-file --raw -fx-opam-monorepo-opam-repositories ./explicit-repo.opam.locked > locked-repos 54 | $ sed "s/\(.*\)$SHORT_HASH1.*/\1/" locked-repos 55 | git+file://$OPAM_MONOREPO_CWD/git-repository# 56 | 57 | We also need to make sure that the git cache gets updated when the repository is updated: 58 | 59 | $ mkdir -p git-repository/packages/git-dep/git-dep.2.0 60 | $ cat >git-repository/packages/git-dep/git-dep.2.0/opam < opam-version: "2.0" 62 | > depends: [ 63 | > "dune" 64 | > ] 65 | > EOF 66 | > (cd git-repository ; git add -A ; git commit -m "New release") > /dev/null 2>&1 67 | $ SHORT_HASH2=$(git -C git-repository rev-parse --short HEAD) 68 | 69 | Thus a new version of the package has been released in the git repo. Locking it 70 | anew should pick this version of the dependency as well as the recording the 71 | new version of the git opam repository. 72 | 73 | $ opam-monorepo lock explicit-repo > /dev/null 74 | $ opam show --just-file --raw -fdepends ./explicit-repo.opam.locked | grep git-dep 75 | "git-dep" {= "2.0"} 76 | $ opam show --just-file --raw -fx-opam-monorepo-opam-repositories ./explicit-repo.opam.locked > locked-repos 77 | $ sed "s/\(.*\)$SHORT_HASH2.*/\1/" locked-repos 78 | git+file://$OPAM_MONOREPO_CWD/git-repository# 79 | -------------------------------------------------------------------------------- /test/bin/invalid-package-version.t/existing.opam: -------------------------------------------------------------------------------- 1 | opam-version: "2.0" 2 | depends: [ 3 | "dune" 4 | "a" 5 | ] 6 | x-opam-monorepo-opam-repositories: [ 7 | "file://$OPAM_MONOREPO_CWD/minimal-repo" 8 | "file://$OPAM_MONOREPO_CWD/repo" 9 | ] 10 | -------------------------------------------------------------------------------- /test/bin/invalid-package-version.t/multiple-constraint.opam: -------------------------------------------------------------------------------- 1 | opam-version: "2.0" 2 | depends: [ 3 | "dune" 4 | "depends-on-min-a" 5 | "a" {< "2.0"} 6 | ] 7 | x-opam-monorepo-opam-repositories: [ 8 | "file://$OPAM_MONOREPO_CWD/minimal-repo" 9 | "file://$OPAM_MONOREPO_CWD/repo" 10 | ] 11 | -------------------------------------------------------------------------------- /test/bin/invalid-package-version.t/repo/packages/a/a.0.1/opam: -------------------------------------------------------------------------------- 1 | opam-version: "2.0" 2 | dev-repo: "git+https://a.com/a.git" 3 | depends: [ 4 | "dune" 5 | ] 6 | build: [ "dune" "build" ] 7 | url { 8 | src: "https://a.com/a.0.1.tbz" 9 | checksum: [ 10 | "sha256=0000000000000000000000000000000000000000000000000000000000000000" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /test/bin/invalid-package-version.t/repo/packages/a/a.1.0/opam: -------------------------------------------------------------------------------- 1 | opam-version: "2.0" 2 | dev-repo: "git+https://a.com/a.git" 3 | depends: [ 4 | "ocaml" 5 | ] 6 | build: [ "make" ] 7 | url { 8 | src: "https://a.com/a.1.0.tbz" 9 | checksum: [ 10 | "sha256=0000000000000000000000000000000000000000000000000000000000000000" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /test/bin/invalid-package-version.t/repo/packages/a/a.1.1/opam: -------------------------------------------------------------------------------- 1 | opam-version: "2.0" 2 | dev-repo: "git+https://a.com/a.git" 3 | depends: [ 4 | "ocaml" 5 | ] 6 | build: [ "make" ] 7 | url { 8 | src: "https://a.com/a.1.1.tbz" 9 | checksum: [ 10 | "sha256=0000000000000000000000000000000000000000000000000000000000000000" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /test/bin/invalid-package-version.t/repo/packages/depends-on-min-a/depends-on-min-a.1/opam: -------------------------------------------------------------------------------- 1 | opam-version: "2.0" 2 | dev-repo: "git+https://a.com/depends-on-min-a.git" 3 | depends: [ 4 | "dune" 5 | "a" {>= "1.0"} 6 | ] 7 | url { 8 | src: "https://a.com/depends-on-min-a.0.1.tbz" 9 | checksum: [ 10 | "sha256=0000000000000000000000000000000000000000000000000000000000000000" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /test/bin/invalid-package-version.t/repo/repo: -------------------------------------------------------------------------------- 1 | opam-version: "2.0" 2 | -------------------------------------------------------------------------------- /test/bin/invalid-package-version.t/run.t: -------------------------------------------------------------------------------- 1 | We want to make sure the error messages are sensible, and as such if the user 2 | picks a version that doesn't exist we want to make them aware of it. 3 | 4 | We setup the default base repository 5 | 6 | $ gen-minimal-repo 7 | 8 | Here we define a package test that depends on a package `a`: 9 | 10 | $ opam show --just-file --raw -fdepends ./existing.opam 11 | dune, a 12 | 13 | We have a local repo that defines a package `a` that satisfies the predicate, 14 | with a version that is valid and can be picked. 15 | 16 | $ cat repo/packages/a/a.0.1/opam > /dev/null 17 | 18 | opam-monorepo solver should successfully pick a.0.1: 19 | 20 | $ opam-monorepo lock existing 21 | ==> Using 1 locally scanned package as the target. 22 | ==> Found 9 opam dependencies for the target package. 23 | ==> Querying opam database for their metadata and Dune compatibility. 24 | ==> Calculating exact pins for each of them. 25 | ==> Wrote lockfile with 1 entries to $TESTCASE_ROOT/existing.opam.locked. You can now run opam monorepo pull to fetch their sources. 26 | $ grep "\"a\"\s\+{" existing.opam.locked 27 | "a" {= "0.1" & ?vendor} 28 | 29 | Yet if we attempt to use the same package, but pick a version that doesn't 30 | exist in our repo: 31 | 32 | $ opam show --just-file --raw -fdepends ./toonew.opam 33 | "dune" "a" {>= "1.0"} 34 | 35 | opam-monorepo should fail with some error code and display an error message 36 | that there is no version of `a` that matches the constraint. 37 | 38 | (grep appends a NUL byte at the end, hence the head call, this is not important 39 | to the test) 40 | 41 | $ opam-monorepo lock toonew 2> errors 42 | ==> Using 1 locally scanned package as the target. 43 | [1] 44 | $ grep "opam-monorepo: \[ERROR\]" < errors 45 | opam-monorepo: [ERROR] There is no eligible version of a that matches >= 1.0 46 | opam-monorepo: [ERROR] Can't find all required versions. 47 | 48 | We should also produce the right error message with all the constraints when we have multiple constaints 49 | 50 | $ opam show --just-file --raw -fdepends ./multiple-constraint.opam 51 | "dune" "depends-on-min-a" "a" {< "2.0"} 52 | $ opam-monorepo lock multiple-constraint 2> errors 53 | ==> Using 1 locally scanned package as the target. 54 | [1] 55 | $ grep "opam-monorepo: \[ERROR\]" < errors 56 | opam-monorepo: [ERROR] There is no eligible version of a that matches >= 1.0 57 | opam-monorepo: [ERROR] Can't find all required versions. 58 | -------------------------------------------------------------------------------- /test/bin/invalid-package-version.t/toonew.opam: -------------------------------------------------------------------------------- 1 | opam-version: "2.0" 2 | depends: [ 3 | "dune" 4 | "a" {>= "1.0"} 5 | ] 6 | x-opam-monorepo-opam-repositories: [ 7 | "file://$OPAM_MONOREPO_CWD/minimal-repo" 8 | "file://$OPAM_MONOREPO_CWD/repo" 9 | ] 10 | -------------------------------------------------------------------------------- /test/bin/local-solver-picks-higher-version.t/higher-version.opam: -------------------------------------------------------------------------------- 1 | opam-version: "2.0" 2 | depends: [ 3 | "dune" 4 | "a" 5 | ] 6 | x-opam-monorepo-opam-repositories: [ 7 | "file://$OPAM_MONOREPO_CWD/minimal-repo" 8 | "file://$OPAM_MONOREPO_CWD/repo" 9 | ] 10 | -------------------------------------------------------------------------------- /test/bin/local-solver-picks-higher-version.t/repo/packages/a/a.0.1/opam: -------------------------------------------------------------------------------- 1 | opam-version: "2.0" 2 | dev-repo: "git+https://a.com/a.git" 3 | depends: [ 4 | "dune" 5 | ] 6 | url { 7 | src: "https://a.com/a.0.1.tbz" 8 | checksum: [ 9 | "sha256=0000000000000000000000000000000000000000000000000000000000000000" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /test/bin/local-solver-picks-higher-version.t/repo/packages/a/a.0.2/opam: -------------------------------------------------------------------------------- 1 | opam-version: "2.0" 2 | dev-repo: "git+https://a.com/a.git" 3 | depends: [ 4 | "dune" 5 | ] 6 | build: [ "dune" "build" ] 7 | url { 8 | src: "https://a.com/a.0.2.tbz" 9 | checksum: [ 10 | "sha256=0000000000000000000000000000000000000000000000000000000000000001" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /test/bin/local-solver-picks-higher-version.t/repo/repo: -------------------------------------------------------------------------------- 1 | opam-version: "2.0" 2 | -------------------------------------------------------------------------------- /test/bin/local-solver-picks-higher-version.t/run.t: -------------------------------------------------------------------------------- 1 | With opam-0install solver, the context is responsible for defining the 2 | preference order between versions of a package. 3 | To stick to the default behaviour of the switch based context, 4 | the repository list based context should properly sort the candidates so 5 | that the solver picks the highest satisfying version. 6 | 7 | We setup the default base repository 8 | 9 | $ gen-minimal-repo 10 | 11 | Here we define a package higher-version that depends on a package `a`: 12 | 13 | $ cat higher-version.opam 14 | opam-version: "2.0" 15 | depends: [ 16 | "dune" 17 | "a" 18 | ] 19 | x-opam-monorepo-opam-repositories: [ 20 | "file://$OPAM_MONOREPO_CWD/minimal-repo" 21 | "file://$OPAM_MONOREPO_CWD/repo" 22 | ] 23 | 24 | We have a local repo that defines two versions of `a`, both being satisfying 25 | solutions: 26 | 27 | $ cat repo/packages/a/a.0.1/opam 28 | opam-version: "2.0" 29 | dev-repo: "git+https://a.com/a.git" 30 | depends: [ 31 | "dune" 32 | ] 33 | url { 34 | src: "https://a.com/a.0.1.tbz" 35 | checksum: [ 36 | "sha256=0000000000000000000000000000000000000000000000000000000000000000" 37 | ] 38 | } 39 | $ cat repo/packages/a/a.0.2/opam 40 | opam-version: "2.0" 41 | dev-repo: "git+https://a.com/a.git" 42 | depends: [ 43 | "dune" 44 | ] 45 | build: [ "dune" "build" ] 46 | url { 47 | src: "https://a.com/a.0.2.tbz" 48 | checksum: [ 49 | "sha256=0000000000000000000000000000000000000000000000000000000000000001" 50 | ] 51 | } 52 | 53 | opam-monorepo solver should pick a.0.2 here: 54 | 55 | $ opam-monorepo lock 56 | ==> Using 1 locally scanned package as the target. 57 | ==> Found 9 opam dependencies for the target package. 58 | ==> Querying opam database for their metadata and Dune compatibility. 59 | ==> Calculating exact pins for each of them. 60 | ==> Wrote lockfile with 1 entries to $TESTCASE_ROOT/higher-version.opam.locked. You can now run opam monorepo pull to fetch their sources. 61 | $ cat higher-version.opam.locked | grep "\"a\"\s\+{" 62 | "a" {= "0.2" & ?vendor} 63 | -------------------------------------------------------------------------------- /test/bin/lockfile-version.t/lockfile-version.opam: -------------------------------------------------------------------------------- 1 | opam-version: "2.0" 2 | depends: [ 3 | "ocaml" 4 | "dune" 5 | ] 6 | x-opam-monorepo-opam-repositories: [ 7 | "file://$OPAM_MONOREPO_CWD/minimal-repo" 8 | ] 9 | -------------------------------------------------------------------------------- /test/bin/lockfile-version.t/run.t: -------------------------------------------------------------------------------- 1 | We have a simple project with a single package that we will use to generate 2 | a lockfile for our current version. 3 | 4 | $ gen-minimal-repo 5 | $ opam-monorepo lock > /dev/null 6 | $ opam show --just-file --raw -fx-opam-monorepo-version ./lockfile-version.opam.locked 7 | 0.3 8 | 9 | This is our current version. 10 | At the moment we are not backward compatible so if we downgrade this version number, 11 | pull will refuse the lockfile and suggest it is regenerated with the current plugin: 12 | 13 | $ sed -i -e "s/x-opam-monorepo-version: \"0.3\"/x-opam-monorepo-version: \"0.2\"/" lockfile-version.opam.locked 14 | $ opam-monorepo pull > /dev/null 15 | opam-monorepo: [ERROR] opam-monorepo lockfile version 0.2 is too old. Please regenerate the lockfile using your current opam-monorepo plugin or install an older version of the plugin. 16 | [1] 17 | 18 | We also obviously do not support future versions and they should be rejected as well, 19 | suggesting that the user upgrade their plugin to be able to interpret that lockfile: 20 | 21 | $ sed -i -e "s/x-opam-monorepo-version: \"0.2\"/x-opam-monorepo-version: \"0.999\"/" lockfile-version.opam.locked 22 | $ opam-monorepo pull > /dev/null 23 | opam-monorepo: [ERROR] Incompatible opam-monorepo lockfile version 0.999. Please upgrade your opam-monorepo plugin. 24 | [1] 25 | -------------------------------------------------------------------------------- /test/bin/minimal-update.t/minimal-update.opam: -------------------------------------------------------------------------------- 1 | opam-version: "2.0" 2 | depends: [ 3 | "dune" 4 | "a" 5 | "b" 6 | ] 7 | x-opam-monorepo-opam-repositories: [ 8 | "file://$OPAM_MONOREPO_CWD/minimal-repo" 9 | "file://$OPAM_MONOREPO_CWD/repo" 10 | ] 11 | -------------------------------------------------------------------------------- /test/bin/minimal-update.t/repo/packages/a/a.0.1/opam: -------------------------------------------------------------------------------- 1 | opam-version: "2.0" 2 | depends: [ 3 | "dune" 4 | ] 5 | build: [ "dune" "build" ] 6 | dev-repo: "git+https://github.com/a/a" 7 | url { 8 | src: "https://a.com/a.tbz" 9 | checksum: [ 10 | "sha256=0000000000000000000000000000000000000000000000000000000000000000" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /test/bin/minimal-update.t/repo/packages/b/b.0.1/opam: -------------------------------------------------------------------------------- 1 | opam-version: "2.0" 2 | depends: [ 3 | "dune" 4 | ] 5 | build: [ "dune" "build" ] 6 | dev-repo: "git+https://github.com/b/b" 7 | url { 8 | src: "https://b.com/b.tbz" 9 | checksum: [ 10 | "sha256=0000000000000000000000000000000000000000000000000000000000000001" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /test/bin/minimal-update.t/repo/packages/c/c.0.1/opam: -------------------------------------------------------------------------------- 1 | opam-version: "2.0" 2 | depends: [ 3 | "dune" 4 | ] 5 | build: [ "dune" "build" ] 6 | dev-repo: "git+https://github.com/c/c" 7 | url { 8 | src: "https://c.com/c.tbz" 9 | checksum: [ 10 | "sha256=0000000000000000000000000000000000000000000000000000000000000002" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /test/bin/minimal-update.t/repo/repo: -------------------------------------------------------------------------------- 1 | opam-version: "2.0" 2 | -------------------------------------------------------------------------------- /test/bin/minimal-update.t/run.t: -------------------------------------------------------------------------------- 1 | We have a simple project with a single package which depends on 2 | packages 'a' and 'b': 3 | 4 | $ opam show --just-file -fdepends ./minimal-update.opam 5 | dune, a, b 6 | 7 | We run an initial lock and get the following locked verions of 8 | our dependencies: 9 | 10 | $ gen-minimal-repo 11 | $ opam-monorepo lock > /dev/null 12 | $ opam show --just-file -fdepends ./minimal-update.opam.locked | grep -e "\"b\"" -e "\"a\"" 13 | "a" {= "0.1" & ?vendor} 14 | "b" {= "0.1" & ?vendor} 15 | 16 | Now we add a dependency to 'c' to our project 17 | 18 | $ sed -i'' -e 's/"b"/&\n "c"/' ./minimal-update.opam 19 | $ opam show --just-file -fdepends ./minimal-update.opam 20 | dune, a, b, c 21 | 22 | We would then like to update our lock file to include 'c' but we do not 23 | want to update the other dependencies if it is not required 24 | 25 | Both 'a' and 'b' got a new release: 26 | 27 | $ mkdir repo/packages/a/a.0.2 28 | $ cp repo/packages/a/a.0.1/opam repo/packages/a/a.0.2/opam 29 | $ mkdir repo/packages/b/b.0.2 30 | $ cp repo/packages/b/b.0.1/opam repo/packages/b/b.0.2/opam 31 | 32 | To stick to the minimal lock file update, we use the --minimal-update 33 | flag. This should add c to the lock file while keeping 'a' and 'b' to their 34 | previous version. 35 | 36 | $ opam-monorepo lock --minimal-update > /dev/null 37 | $ opam show --just-file -fdepends ./minimal-update.opam.locked | grep -e "\"b\"" -e "\"a\"" -e "\"c\"" 38 | "a" {= "0.1" & ?vendor} 39 | "b" {= "0.1" & ?vendor} 40 | "c" {= "0.1" & ?vendor} 41 | 42 | Now say we want to also update 'b' because we need a feature that is only 43 | available in the latest version: 44 | 45 | $ sed -i'' -e 's/"b"/"b" {>= "0.2"}/' minimal-update.opam 46 | $ opam show --just-file -fdepends ./minimal-update.opam 47 | "dune" "a" "b" {>= "0.2"} "c" 48 | 49 | Locking with --minimal-update should update 'b' but not 'a': 50 | 51 | $ opam-monorepo lock --minimal-update > /dev/null 52 | $ opam show --just-file -fdepends ./minimal-update.opam.locked | grep -e "\"b\"" -e "\"a\"" -e "\"c\"" 53 | "a" {= "0.1" & ?vendor} 54 | "b" {= "0.2" & ?vendor} 55 | "c" {= "0.1" & ?vendor} 56 | 57 | Alternatively, if we remove a dependency: 58 | 59 | $ sed -i'' -e '/"c"/d' ./minimal-update.opam 60 | $ opam show --just-file -fdepends ./minimal-update.opam 61 | "dune" "a" "b" {>= "0.2"} 62 | 63 | Locking with `--minimal-update` should still allow removing the unnecessary 64 | package from the lock file: 65 | 66 | $ opam-monorepo lock --minimal-update > /dev/null 67 | $ opam show --just-file -fdepends ./minimal-update.opam.locked | grep -e "\"b\"" -e "\"a\"" -e "\"c\"" 68 | "a" {= "0.1" & ?vendor} 69 | "b" {= "0.2" & ?vendor} 70 | -------------------------------------------------------------------------------- /test/bin/missing-dev-repo.t/missing-dev-repo.opam: -------------------------------------------------------------------------------- 1 | opam-version: "2.0" 2 | depends: [ 3 | "dune" 4 | "no-dev-repo" 5 | ] 6 | x-opam-monorepo-opam-repositories: [ 7 | "file://$OPAM_MONOREPO_CWD/minimal-repo" 8 | "file://$OPAM_MONOREPO_CWD/repo" 9 | ] 10 | -------------------------------------------------------------------------------- /test/bin/missing-dev-repo.t/repo/packages/no-dev-repo/no-dev-repo.1/opam: -------------------------------------------------------------------------------- 1 | opam-version: "2.0" 2 | depends: [ 3 | "dune" 4 | ] 5 | build: [ "dune" "build" ] 6 | url { 7 | src: "https://b.com/b.tbz" 8 | checksum: [ 9 | "sha256=0000000000000000000000000000000000000000000000000000000000000000" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /test/bin/missing-dev-repo.t/repo/repo: -------------------------------------------------------------------------------- 1 | opam-version: "2.0" 2 | -------------------------------------------------------------------------------- /test/bin/missing-dev-repo.t/run.t: -------------------------------------------------------------------------------- 1 | We have a simple project with a single package defined at the root. 2 | It has a `x-opam-monorepo-opam-repositories` field set to use a local 3 | opam-repository for locking 4 | 5 | $ cat missing-dev-repo.opam 6 | opam-version: "2.0" 7 | depends: [ 8 | "dune" 9 | "no-dev-repo" 10 | ] 11 | x-opam-monorepo-opam-repositories: [ 12 | "file://$OPAM_MONOREPO_CWD/minimal-repo" 13 | "file://$OPAM_MONOREPO_CWD/repo" 14 | ] 15 | 16 | We provided a minimal opam-repository but locking should be successful. 17 | 18 | $ gen-minimal-repo 19 | $ opam-monorepo lock 20 | ==> Using 1 locally scanned package as the target. 21 | ==> Found 9 opam dependencies for the target package. 22 | ==> Querying opam database for their metadata and Dune compatibility. 23 | ==> Calculating exact pins for each of them. 24 | opam-monorepo: [WARNING] Package no-dev-repo.1 has no dev-repo specified, but it needs a dev-repo to be successfully included in the duniverse. 25 | ==> Wrote lockfile with 0 entries to $TESTCASE_ROOT/missing-dev-repo.opam.locked. You can now run opam monorepo pull to fetch their sources. 26 | -------------------------------------------------------------------------------- /test/bin/missing-dune-project.t/missing-dune-project-as-well.opam: -------------------------------------------------------------------------------- 1 | opam-version: "2.0" 2 | -------------------------------------------------------------------------------- /test/bin/missing-dune-project.t/missing-dune-project.opam: -------------------------------------------------------------------------------- 1 | opam-version: "2.0" 2 | -------------------------------------------------------------------------------- /test/bin/missing-dune-project.t/run.t: -------------------------------------------------------------------------------- 1 | We have a simple project with two local packages, defined at the root 2 | 3 | $ cat missing-dune-project.opam 4 | opam-version: "2.0" 5 | 6 | $ cat missing-dune-project-as-well.opam 7 | opam-version: "2.0" 8 | 9 | The project has no dune-project file. That means that if we run `opam-monorepo lock`, 10 | it will have more than one target: `a` and `b`. It therefore has to determine the name of the 11 | lockfile based on the project's name in the dune-project file. It's expected to fail but it should 12 | to that nicely, letting the user know that it couldn't infer the lockfile and that they should 13 | either explicitly specify it on the command line or add a valid dune-project file at the root. 14 | 15 | $ opam-monorepo lock 16 | ==> Using 2 locally scanned packages as the targets. 17 | opam-monorepo: [ERROR] Could not infer the target lockfile name: Missing dune-project file at the root: $TESTCASE_ROOT/dune-project 18 | Try setting it explicitly using --lockfile or add a project name in a root dune-project file. 19 | [1] 20 | -------------------------------------------------------------------------------- /test/bin/no-build-command.t/repo/packages/depend-with-dune/depend-with-dune.0.1/opam: -------------------------------------------------------------------------------- 1 | opam-version: "2.0" 2 | dev-repo: "git+https://github.com/test-no-build-command/test" 3 | depends: [ 4 | "with-dune" 5 | ] 6 | dev-repo: "git+https://github.com/b/b" 7 | url { 8 | src: "https://test.com/test.tbz" 9 | checksum: [ 10 | "sha256=0000000000000000000000000000000000000000000000000000000000000000" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /test/bin/no-build-command.t/repo/packages/depend-without-dune/depend-without-dune.0.1/opam: -------------------------------------------------------------------------------- 1 | opam-version: "2.0" 2 | dev-repo: "git+https://github.com/test-no-build-command/test" 3 | depends: [ 4 | "without-dune" 5 | ] 6 | dev-repo: "git+https://github.com/b/b" 7 | url { 8 | src: "https://test.com/test.tbz" 9 | checksum: [ 10 | "sha256=0000000000000000000000000000000000000000000000000000000000000000" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /test/bin/no-build-command.t/repo/packages/with-dune/with-dune.0.1/opam: -------------------------------------------------------------------------------- 1 | opam-version: "2.0" 2 | dev-repo: "git+https://github.com/test-no-build-command/test" 3 | depends: [ 4 | "dune" 5 | ] 6 | build: [ "dune" "build" ] 7 | url { 8 | src: "https://test.com/test.tbz" 9 | checksum: [ 10 | "sha256=0000000000000000000000000000000000000000000000000000000000000000" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /test/bin/no-build-command.t/repo/packages/without-dune/without-dune.0.1/opam: -------------------------------------------------------------------------------- 1 | opam-version: "2.0" 2 | dev-repo: "git+https://github.com/test-no-build-command/test" 3 | depends: [] 4 | build: [ 5 | ["make"] 6 | ] 7 | url { 8 | src: "https://test.com/test.tbz" 9 | checksum: [ 10 | "sha256=0000000000000000000000000000000000000000000000000000000000000000" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /test/bin/no-build-command.t/repo/repo: -------------------------------------------------------------------------------- 1 | opam-version: "2.0" 2 | -------------------------------------------------------------------------------- /test/bin/no-build-command.t/run.t: -------------------------------------------------------------------------------- 1 | Test that packages with no build commands and no direct dependency on dune and 2 | no build command are not assumed to not build with dune. 3 | 4 | The repo contains 4 packages: 5 | - with-dune: directly depends on dune and has a build command 6 | - without-dune: doesn't depend on dune and has a build command 7 | - depend-with-dune: depends on with-dune and doesn't have a build command 8 | - depend-without-dune: depends on without-dune and doesn't have a build command 9 | 10 | This test asserts that it's an error to generate a lockfile for a package 11 | depending on depend-without-dune due to the transitive dependency on 12 | without-dune, but that it's not an error to generate a lockfile for a package 13 | which only depends on depend-with-dune, despite the latter not directly 14 | depending on dune. 15 | 16 | We setup the default base repository 17 | 18 | $ gen-minimal-repo 19 | 20 | Attempt to generate a lockfile for a package which depends on 21 | depend-without-dune (this should fail due to the transitive dependency on 22 | without-dune which has a build command but doesn't depend on dune) 23 | 24 | $ opam-monorepo lock test-depend-without-dune.opam 2>&1 | grep -o "Some dependencies cannot be built with dune!" 25 | Some dependencies cannot be built with dune! 26 | 27 | Attempt to generate a lockfile for a package which depends only on 28 | depend-with-dune 29 | 30 | $ opam-monorepo lock test-depend-with-dune.opam > /dev/null 31 | -------------------------------------------------------------------------------- /test/bin/no-build-command.t/test-depend-with-dune.opam: -------------------------------------------------------------------------------- 1 | opam-version: "2.0" 2 | depends: [ 3 | "depend-with-dune" 4 | ] 5 | x-opam-monorepo-opam-repositories: [ 6 | "file://$OPAM_MONOREPO_CWD/minimal-repo" 7 | "file://$OPAM_MONOREPO_CWD/repo" 8 | ] 9 | -------------------------------------------------------------------------------- /test/bin/no-build-command.t/test-depend-without-dune.opam: -------------------------------------------------------------------------------- 1 | opam-version: "2.0" 2 | depends: [ 3 | "depend-without-dune" 4 | ] 5 | x-opam-monorepo-opam-repositories: [ 6 | "file://$OPAM_MONOREPO_CWD/minimal-repo" 7 | "file://$OPAM_MONOREPO_CWD/repo" 8 | ] 9 | -------------------------------------------------------------------------------- /test/bin/opam-provided.t/opam-provided.opam: -------------------------------------------------------------------------------- 1 | opam-version: "2.0" 2 | depends: [ 3 | "dune" 4 | "b" 5 | ] 6 | x-opam-monorepo-opam-repositories: [ 7 | "file://$OPAM_MONOREPO_CWD/minimal-repo" 8 | "file://$OPAM_MONOREPO_CWD/repo" 9 | ] 10 | x-opam-monorepo-opam-provided: "b" 11 | -------------------------------------------------------------------------------- /test/bin/opam-provided.t/repo/packages/b/b.1/opam: -------------------------------------------------------------------------------- 1 | opam-version: "2.0" 2 | depends: [ 3 | "dune" 4 | ] 5 | dev-repo: "git+https://github.com/b/b" 6 | build: [ "dune" "build" ] 7 | url { 8 | src: "https://b.com/b.tbz" 9 | checksum: [ 10 | "sha256=0000000000000000000000000000000000000000000000000000000000000000" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /test/bin/opam-provided.t/repo/packages/depends-on-b/depends-on-b.1/opam: -------------------------------------------------------------------------------- 1 | opam-version: "2.0" 2 | depends: [ 3 | "dune" 4 | "b" 5 | ] 6 | build: [ "dune" "build" ] 7 | dev-repo: "git+https://github.com/b/depends-on-b" 8 | url { 9 | src: "https://b.com/depends-on-b.tbz" 10 | checksum: [ 11 | "sha256=0000000000000000000000000000000000000000000000000000000000000000" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /test/bin/opam-provided.t/repo/repo: -------------------------------------------------------------------------------- 1 | opam-version: "2.0" 2 | -------------------------------------------------------------------------------- /test/bin/opam-provided.t/reverse-transitive.opam: -------------------------------------------------------------------------------- 1 | opam-version: "2.0" 2 | depends: [ 3 | "dune" 4 | "depends-on-b" 5 | ] 6 | x-opam-monorepo-opam-repositories: [ 7 | "file://$OPAM_MONOREPO_CWD/minimal-repo" 8 | "file://$OPAM_MONOREPO_CWD/repo" 9 | ] 10 | x-opam-monorepo-opam-provided: "depends-on-b" 11 | -------------------------------------------------------------------------------- /test/bin/opam-provided.t/run.t: -------------------------------------------------------------------------------- 1 | We have a package that should not be installed via Opam. There might be various 2 | reasons we might not want that (e.g., it cannot be built with Dune), it is 3 | meant as a helper binary etc. 4 | 5 | We start with a minimal `opam-repository`: 6 | 7 | $ gen-minimal-repo 8 | 9 | We have a file that depends on package "b". 10 | 11 | $ opam show --just-file --raw -fdepends ./vendored.opam 12 | dune, b 13 | 14 | This package does not set any [opam]-provided packages: 15 | 16 | $ opam show --just-file --raw -fx-opam-monorepo-opam-provided ./vendored.opam 17 | 18 | 19 | It should lock successfully. 20 | 21 | $ opam-monorepo lock vendored > /dev/null 22 | 23 | The lockfile should thus contain the package "b" and mark it as `vendor` since 24 | `opam-monorepo` will vendor it. It should also add it to the duniverse-dirs. 25 | 26 | $ opam show --just-file --raw -fdepends ./vendored.opam.locked 27 | "b" {= "1" & ?vendor} 28 | "base-bigarray" {= "base"} 29 | "base-threads" {= "base"} 30 | "base-unix" {= "base"} 31 | "dune" {= "2.9.1"} 32 | "ocaml" {= "4.13.1"} 33 | "ocaml-base-compiler" {= "4.13.1"} 34 | "ocaml-config" {= "2"} 35 | "ocaml-options-vanilla" {= "1"} 36 | $ opam show --just-file --raw -fx-opam-monorepo-duniverse-dirs ./vendored.opam.locked 37 | [ 38 | "https://b.com/b.tbz" 39 | "b" 40 | ["sha256=0000000000000000000000000000000000000000000000000000000000000000"] 41 | ] 42 | 43 | Let's now check with the same Opam file but this one adds `opam`-provided. 44 | Asking for the value will return that "b" will be provided by Opam: 45 | 46 | $ opam show --just-file --raw -fx-opam-monorepo-opam-provided ./opam-provided.opam 47 | b 48 | $ opam-monorepo lock opam-provided > /dev/null 49 | 50 | We should see that this package is not marked as `vendor` so if we run 51 | `opam` on it, it will install the package "b". 52 | 53 | $ opam show --just-file --raw -fdepends ./opam-provided.opam.locked 54 | "b" {= "1"} 55 | "base-bigarray" {= "base"} 56 | "base-threads" {= "base"} 57 | "base-unix" {= "base"} 58 | "dune" {= "2.9.1"} 59 | "ocaml" {= "4.13.1"} 60 | "ocaml-base-compiler" {= "4.13.1"} 61 | "ocaml-config" {= "2"} 62 | "ocaml-options-vanilla" {= "1"} 63 | 64 | It also should not be in `duniverse-dirs` in the locked Opam file: 65 | 66 | $ opam show --just-file --raw -fx-opam-monorepo-duniverse-dirs ./opam-provided.opam.locked 67 | 68 | 69 | Alternatively this can be done using the CLI options generated for any elements 70 | of the solver config, including `opam-provided`: 71 | 72 | $ opam-monorepo lock vendored --opam-provided b > /dev/null 73 | $ opam show --just-file -fdepends ./vendored.opam.locked | grep '"b"' 74 | "b" {= "1"} 75 | 76 | As demonstrated above, the CLI argument is taken into account and 'b' is 77 | not vendored. 78 | 79 | What happens in the case that a package would be ok to vendor but the 80 | transitive dependency is `opam`-provided? In this case we have a package 81 | `transitive` that depends on packages that will 82 | depend transitively on an `opam`-provided package. 83 | 84 | $ opam show --just-file --raw -fdepends ./transitive.opam 85 | dune, depends-on-b 86 | $ opam show --just-file --raw -fx-opam-monorepo-opam-provided ./transitive.opam 87 | b 88 | 89 | Locking it should work as usual 90 | 91 | $ opam-monorepo lock transitive > /dev/null 92 | $ opam show --just-file --raw -fdepends ./transitive.opam.locked 93 | "b" {= "1"} 94 | "base-bigarray" {= "base"} 95 | "base-threads" {= "base"} 96 | "base-unix" {= "base"} 97 | "depends-on-b" {= "1" & ?vendor} 98 | "dune" {= "2.9.1"} 99 | "ocaml" {= "4.13.1"} 100 | "ocaml-base-compiler" {= "4.13.1"} 101 | "ocaml-config" {= "2"} 102 | "ocaml-options-vanilla" {= "1"} 103 | 104 | `depends-on-b` is vendored (since it can) but "b" is `opam`-provided. Neat. 105 | 106 | Now for the reverse case, `depends-on-b` is `opam`-provided. 107 | 108 | $ opam show --just-file --raw -fdepends ./reverse-transitive.opam 109 | dune, depends-on-b 110 | $ opam show --just-file --raw -fx-opam-monorepo-opam-provided ./reverse-transitive.opam 111 | depends-on-b 112 | 113 | Since it is, we need to make its dependency, "b", also `opam`-provided. 114 | 115 | $ opam-monorepo lock reverse-transitive > /dev/null 116 | $ opam show --just-file --raw -fdepends ./reverse-transitive.opam.locked 117 | "b" {= "1"} 118 | "base-bigarray" {= "base"} 119 | "base-threads" {= "base"} 120 | "base-unix" {= "base"} 121 | "depends-on-b" {= "1"} 122 | "dune" {= "2.9.1"} 123 | "ocaml" {= "4.13.1"} 124 | "ocaml-base-compiler" {= "4.13.1"} 125 | "ocaml-config" {= "2"} 126 | "ocaml-options-vanilla" {= "1"} 127 | 128 | It should also work to pass the version of the compiler and be respected for 129 | both `opam`-provided packages as well as those to be vendored: 130 | 131 | $ opam-monorepo lock opam-provided --ocaml-version=4.13.1 > /dev/null 132 | -------------------------------------------------------------------------------- /test/bin/opam-provided.t/transitive.opam: -------------------------------------------------------------------------------- 1 | opam-version: "2.0" 2 | depends: [ 3 | "dune" 4 | "depends-on-b" 5 | ] 6 | x-opam-monorepo-opam-repositories: [ 7 | "file://$OPAM_MONOREPO_CWD/minimal-repo" 8 | "file://$OPAM_MONOREPO_CWD/repo" 9 | ] 10 | x-opam-monorepo-opam-provided: "b" 11 | -------------------------------------------------------------------------------- /test/bin/opam-provided.t/vendored.opam: -------------------------------------------------------------------------------- 1 | opam-version: "2.0" 2 | depends: [ 3 | "dune" 4 | "b" 5 | ] 6 | x-opam-monorepo-opam-repositories: [ 7 | "file://$OPAM_MONOREPO_CWD/minimal-repo" 8 | "file://$OPAM_MONOREPO_CWD/repo" 9 | ] 10 | -------------------------------------------------------------------------------- /test/bin/opam-provided.t/warning-list.opam: -------------------------------------------------------------------------------- 1 | opam-version: "2.0" 2 | depends: [ 3 | "dune" 4 | ] 5 | x-opam-monorepo-opam-repositories: [ 6 | "file://$OPAM_MONOREPO_CWD/minimal-repo" 7 | ] 8 | x-opam-monorepo-opam-provided: [42 "fourtytwo"] 9 | -------------------------------------------------------------------------------- /test/bin/opam-provided.t/warning.opam: -------------------------------------------------------------------------------- 1 | opam-version: "2.0" 2 | depends: [ 3 | "dune" 4 | ] 5 | x-opam-monorepo-opam-repositories: [ 6 | "file://$OPAM_MONOREPO_CWD/minimal-repo" 7 | ] 8 | x-opam-monorepo-opam-provided: 42 9 | -------------------------------------------------------------------------------- /test/bin/pinning.t/opam-monorepo-test-other/opam-monorepo-test-other.opam: -------------------------------------------------------------------------------- 1 | opam-version: "2.0" 2 | synopsis: "Dummy package for testing" 3 | maintainer: ["marek@tarides.com"] 4 | authors: ["Marek Kubica"] 5 | dev-repo: "git+http://example.com/opam-monorepo-test-other" 6 | depends: [ 7 | "dune" 8 | ] 9 | -------------------------------------------------------------------------------- /test/bin/pinning.t/run.t: -------------------------------------------------------------------------------- 1 | We have a simple project which depends on another package 2 | 3 | $ opam show --just-file -fdepends ./this/this.opam 4 | dune, opam-monorepo-test-other 5 | 6 | `git ls-remote` is a bit stupid and for some reason requires a valid `.git` 7 | folder and the dune cram tests don't have one. So let's create one here: 8 | 9 | $ git init > /dev/null 2>&1 10 | 11 | Now let's create a git pin: 12 | 13 | $ cd opam-monorepo-test-other 14 | $ git init > /dev/null 2>&1 15 | $ git add opam-monorepo-test-other.opam 16 | $ git commit -m "Initial commit" > /dev/null 17 | $ cd .. 18 | 19 | Let's add this pin and pin it to `HEAD`. 20 | 21 | $ cd this 22 | $ opam pin --yes --no-action add --kind git opam-monorepo-test-other '../opam-monorepo-test-other#HEAD' > /dev/null 2>&1 23 | 24 | Locking with this pin should work. 25 | 26 | $ opam-monorepo lock > /dev/null 2>&1 27 | 28 | We need to clean up since this modifies global state. 29 | 30 | $ opam pin remove opam-monorepo-test-other > /dev/null 31 | $ cd .. 32 | -------------------------------------------------------------------------------- /test/bin/pinning.t/this/this.opam: -------------------------------------------------------------------------------- 1 | opam-version: "2.0" 2 | depends: [ 3 | "dune" 4 | "opam-monorepo-test-other" 5 | ] 6 | -------------------------------------------------------------------------------- /test/bin/preserve-symlinks.t/preserve-symlinks.opam: -------------------------------------------------------------------------------- 1 | opam-version: "2.0" 2 | depends: [ 3 | "dune" 4 | "symlinked-dep" 5 | ] 6 | x-opam-monorepo-opam-repositories: [ 7 | "file://$OPAM_MONOREPO_CWD/minimal-repo" 8 | "file://$OPAM_MONOREPO_CWD/repo" 9 | ] 10 | -------------------------------------------------------------------------------- /test/bin/preserve-symlinks.t/repo/packages/symlinked-dep/symlinked-dep.1.0/opam: -------------------------------------------------------------------------------- 1 | opam-version: "2.0" 2 | dev-repo: "git+https://a.com/symlinked-dep.git" 3 | depends: [ 4 | "ocaml" 5 | "dune" 6 | ] 7 | build: [ 8 | ["dune" "build"] 9 | ] 10 | url { 11 | src: "https://a.com/symlinked-dep.1.0.tbz" 12 | checksum: [ 13 | "sha256=0000000000000000000000000000000000000000000000000000000000000000" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /test/bin/preserve-symlinks.t/repo/repo: -------------------------------------------------------------------------------- 1 | opam-version: "2.0" 2 | -------------------------------------------------------------------------------- /test/bin/preserve-symlinks.t/run.t: -------------------------------------------------------------------------------- 1 | If users want to symlink out folders of the `duniverse` to work on them, it 2 | should be possible to `pull` without the symlink being overwritten. 3 | 4 | Say, we have a project that depends on a package `symlinked-dep`. 5 | 6 | $ opam show --just-file --raw -fdepends ./preserve-symlinks.opam 7 | dune, symlinked-dep 8 | 9 | We start with a minimal `opam-repository` as well as a local repo that has a 10 | package called `symlinked-dep` which should make opam-monorepo be able to pick 11 | up this dependency into the lockfile: 12 | 13 | $ gen-minimal-repo 14 | $ opam-monorepo lock > /dev/null 15 | $ opam show --just-file --raw -fdepends ./preserve-symlinks.opam.locked | grep symlinked-dep 16 | "symlinked-dep" {= "1.0" & ?vendor} 17 | 18 | Let's say we have `symlinked-dep` as a repo somewhere else, where we're working 19 | on it. 20 | 21 | $ mkdir symlinked-dep 22 | $ echo "Yes, it is here" > symlinked-dep/symlinked-dep-is-here 23 | 24 | We now create a `duniverse/` folder that has a symlink to `symlinked-dep`. This 25 | should allow `dune` to pick it up. 26 | 27 | $ mkdir duniverse 28 | $ ln -s ../symlinked-dep duniverse/symlinked-dep 29 | $ cat duniverse/symlinked-dep/symlinked-dep-is-here 30 | Yes, it is here 31 | 32 | Now we ask to pull, while telling opam-monorepo to preserve symlinks: 33 | 34 | $ opam-monorepo pull --keep-symlinked-dir > /dev/null 2>&1 35 | 36 | This should leave us with a `duniverse/` folder where the previous symlink 37 | still exists and still points to the right place: 38 | 39 | $ cat duniverse/symlinked-dep/symlinked-dep-is-here 40 | Yes, it is here 41 | -------------------------------------------------------------------------------- /test/bin/pull-invalid-path.t/lockfile-refers-to-duniverse-directory.opam.locked: -------------------------------------------------------------------------------- 1 | opam-version: "2.0" 2 | synopsis: "opam-monorepo generated lockfile" 3 | maintainer: "opam-monorepo" 4 | depends: [ 5 | "foo" {= "1" & ?vendor} 6 | "ocaml" {= "4.14.0"} 7 | ] 8 | pin-depends: [ 9 | ["foo.1" "https://foo.com/foo.tbz"] 10 | ] 11 | x-opam-monorepo-duniverse-dirs: [ 12 | [ 13 | "https://foo.com/foo.tbz" 14 | "" # <--------------------------- repo name is the empty string 15 | [ 16 | "sha256=0000000000000000000000000000000000000000000000000000000000000000" 17 | ] 18 | ] 19 | ] 20 | x-opam-monorepo-root-packages: ["foo"] 21 | x-opam-monorepo-version: "0.3" 22 | -------------------------------------------------------------------------------- /test/bin/pull-invalid-path.t/lockfile-refers-to-parent-directory.opam.locked: -------------------------------------------------------------------------------- 1 | opam-version: "2.0" 2 | synopsis: "opam-monorepo generated lockfile" 3 | maintainer: "opam-monorepo" 4 | depends: [ 5 | "foo" {= "1" & ?vendor} 6 | "ocaml" {= "4.14.0"} 7 | ] 8 | pin-depends: [ 9 | ["foo.1" "https://foo.com/foo.tbz"] 10 | ] 11 | x-opam-monorepo-duniverse-dirs: [ 12 | [ 13 | "https://foo.com/foo.tbz" 14 | ".." # <--------------------------- repo name is ".." 15 | [ 16 | "sha256=0000000000000000000000000000000000000000000000000000000000000000" 17 | ] 18 | ] 19 | ] 20 | x-opam-monorepo-root-packages: ["foo"] 21 | x-opam-monorepo-version: "0.3" 22 | -------------------------------------------------------------------------------- /test/bin/pull-invalid-path.t/run.t: -------------------------------------------------------------------------------- 1 | Running `opam monorepo pull` uses dev repo names (usually derived from the 2 | dev_repo field in the package's opam file) to name the directories that will 3 | contain the source code of packages inside the "duniverse" directory. We want to 4 | make sure that adversarial dev repo names don't cause files to be created or 5 | deleted outside the duniverse directory. 6 | 7 | Attempt to pull with a lockfile containing a package whose name is the empty 8 | string: 9 | $ opam-monorepo pull --lockfile=lockfile-refers-to-duniverse-directory.opam.locked 10 | ==> Using lockfile lockfile-refers-to-duniverse-directory.opam.locked 11 | opam-monorepo: [ERROR] Refusing to pull https://foo.com/foo.tbz into directory $TESTCASE_ROOT/duniverse/ as it is not inside the directory $TESTCASE_ROOT/duniverse 12 | [1] 13 | 14 | Attempt to pull with a lockfile containing a package whose name is "..": 15 | $ opam-monorepo pull --lockfile=lockfile-refers-to-parent-directory.opam.locked 16 | ==> Using lockfile lockfile-refers-to-parent-directory.opam.locked 17 | opam-monorepo: [ERROR] Refusing to pull https://foo.com/foo.tbz into directory $TESTCASE_ROOT/duniverse/.. as it is not inside the directory $TESTCASE_ROOT/duniverse 18 | [1] 19 | -------------------------------------------------------------------------------- /test/bin/recursive-local-package-search.t/mkdir: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarides/opam-monorepo/2d7e3a3a3c7d7527b005442798ea5af8b0043b0a/test/bin/recursive-local-package-search.t/mkdir -------------------------------------------------------------------------------- /test/bin/recursive-local-package-search.t/repo/packages/in-repo/in-repo.0/opam: -------------------------------------------------------------------------------- 1 | opam-version: "2.0" 2 | -------------------------------------------------------------------------------- /test/bin/recursive-local-package-search.t/repo/repo: -------------------------------------------------------------------------------- 1 | opam-version: "2.0" 2 | -------------------------------------------------------------------------------- /test/bin/recursive-local-package-search.t/root.opam: -------------------------------------------------------------------------------- 1 | opam-version: "2.0" 2 | depends: [ 3 | "dune" 4 | ] 5 | -------------------------------------------------------------------------------- /test/bin/recursive-local-package-search.t/run.t: -------------------------------------------------------------------------------- 1 | opam-monorepo looks for local packages definition recursively. 2 | We want to make sure that it does not search for them in some specific places 3 | such as the _build, _opam and duniverse directories. 4 | We also want to make sure it is possible to define an opam-repository 5 | locally and that it won't treat the packages defined in there as local. 6 | 7 | In this project we have defined packages in each of those type of folders 8 | plus a package called root at the root and one called some-local-pkg in a 9 | sub dir. 10 | 11 | Running opam monorepo lock --recurse should only pick those last two as targets 12 | 13 | $ opam-monorepo lock --lockfile test.opam.locked --recurse 2>&1 | grep locally 14 | ==> Using 2 locally scanned packages as the targets. 15 | $ cat test.opam.locked | grep root-packages 16 | x-opam-monorepo-root-packages: ["root" "some-local-pkg"] 17 | -------------------------------------------------------------------------------- /test/bin/recursive-local-package-search.t/some_dir/some-local-pkg.opam: -------------------------------------------------------------------------------- 1 | opam-version: "2.0" 2 | -------------------------------------------------------------------------------- /test/bin/require-cross-compiling-packages.t/mirage-opam-overlays/packages/b/b.0.1+dune+mirage/opam: -------------------------------------------------------------------------------- 1 | opam-version: "2.0" 2 | dev-repo: "git+https://b.com/b.git" 3 | depends: [ 4 | "dune" 5 | ] 6 | build: [ "dune" "build" ] 7 | tags: ["cross-compile"] 8 | url { 9 | src: "https://mirage.com/b.0.1-dune-mirage.tbz" 10 | checksum: "sha256=0000000000000000000000000000000000000000000000000000000000000002" 11 | } 12 | -------------------------------------------------------------------------------- /test/bin/require-cross-compiling-packages.t/mirage-opam-overlays/repo: -------------------------------------------------------------------------------- 1 | opam-version: "2.0" 2 | -------------------------------------------------------------------------------- /test/bin/require-cross-compiling-packages.t/opam-overlays/packages/b/b.0.1+dune/opam: -------------------------------------------------------------------------------- 1 | opam-version: "2.0" 2 | dev-repo: "git+https://b.com/b.git" 3 | depends: [ 4 | "dune" 5 | ] 6 | build: [ "dune" "build" ] 7 | url { 8 | src: "https://dune.com/b.0.1-dune.tbz" 9 | checksum: "sha256=0000000000000000000000000000000000000000000000000000000000000001" 10 | } 11 | -------------------------------------------------------------------------------- /test/bin/require-cross-compiling-packages.t/opam-overlays/repo: -------------------------------------------------------------------------------- 1 | opam-version: "2.0" 2 | -------------------------------------------------------------------------------- /test/bin/require-cross-compiling-packages.t/opam-repository/packages/b/b.0.1/opam: -------------------------------------------------------------------------------- 1 | opam-version: "2.0" 2 | dev-repo: "git+https://b.com/b.git" 3 | build: [ "make" ] 4 | url { 5 | src: "https://b.com/b.0.1.tbz" 6 | checksum: "sha256=0000000000000000000000000000000000000000000000000000000000000000" 7 | } 8 | -------------------------------------------------------------------------------- /test/bin/require-cross-compiling-packages.t/opam-repository/packages/base-bigarray/base-bigarray.base/opam: -------------------------------------------------------------------------------- 1 | opam-version: "2.0" 2 | maintainer: "https://github.com/ocaml/opam-repository/issues" 3 | description: """ 4 | Bigarray library distributed with the OCaml compiler 5 | """ 6 | 7 | -------------------------------------------------------------------------------- /test/bin/require-cross-compiling-packages.t/opam-repository/packages/base-threads/base-threads.base/opam: -------------------------------------------------------------------------------- 1 | opam-version: "2.0" 2 | maintainer: "https://github.com/ocaml/opam-repository/issues" 3 | description: """ 4 | Threads library distributed with the OCaml compiler 5 | """ 6 | 7 | -------------------------------------------------------------------------------- /test/bin/require-cross-compiling-packages.t/opam-repository/packages/base-unix/base-unix.base/opam: -------------------------------------------------------------------------------- 1 | opam-version: "2.0" 2 | maintainer: "https://github.com/ocaml/opam-repository/issues" 3 | description: """ 4 | Unix library distributed with the OCaml compiler 5 | """ 6 | 7 | -------------------------------------------------------------------------------- /test/bin/require-cross-compiling-packages.t/opam-repository/packages/dune/dune.2.9.1/opam: -------------------------------------------------------------------------------- 1 | opam-version: "2.0" 2 | synopsis: "Fast, portable, and opinionated build system" 3 | description: """ 4 | 5 | dune is a build system that was designed to simplify the release of 6 | Jane Street packages. It reads metadata from "dune" files following a 7 | very simple s-expression syntax. 8 | 9 | dune is fast, has very low-overhead, and supports parallel builds on 10 | all platforms. It has no system dependencies; all you need to build 11 | dune or packages using dune is OCaml. You don't need make or bash 12 | as long as the packages themselves don't use bash explicitly. 13 | 14 | dune supports multi-package development by simply dropping multiple 15 | repositories into the same directory. 16 | 17 | It also supports multi-context builds, such as building against 18 | several opam roots/switches simultaneously. This helps maintaining 19 | packages across several versions of OCaml and gives cross-compilation 20 | for free. 21 | """ 22 | maintainer: ["Jane Street Group, LLC "] 23 | authors: ["Jane Street Group, LLC "] 24 | license: "MIT" 25 | homepage: "https://github.com/ocaml/dune" 26 | doc: "https://dune.readthedocs.io/" 27 | bug-reports: "https://github.com/ocaml/dune/issues" 28 | conflicts: [ 29 | "merlin" {< "3.4.0"} 30 | "ocaml-lsp-server" {< "1.3.0"} 31 | "dune-configurator" {< "2.3.0"} 32 | "odoc" {< "1.3.0"} 33 | "dune-release" {< "1.3.0"} 34 | "js_of_ocaml-compiler" {< "3.6.0"} 35 | "jbuilder" {= "transition"} 36 | ] 37 | dev-repo: "git+https://github.com/ocaml/dune.git" 38 | build: [ 39 | # opam 2 sets OPAM_SWITCH_PREFIX, so we don't need a hardcoded path 40 | ["ocaml" "configure.ml" "--libdir" lib] {opam-version < "2"} 41 | ["ocaml" "bootstrap.ml" "-j" jobs] 42 | ["./dune.exe" "build" "-p" name "--profile" "dune-bootstrap" "-j" jobs] 43 | ] 44 | depends: [ 45 | # Please keep the lower bound in sync with .github/workflows/workflow.yml, 46 | # dune-project and min_ocaml_version in bootstrap.ml 47 | ("ocaml" {>= "4.08"} | ("ocaml" {< "4.08~~"} & "ocamlfind-secondary")) 48 | "base-unix" 49 | "base-threads" 50 | ] 51 | x-commit-hash: "e41c66259135d6d1d72b031be6684bf8826a2586" 52 | url { 53 | src: "https://github.com/ocaml/dune/releases/download/2.9.1/dune-2.9.1.tbz" 54 | checksum: [ 55 | "sha256=b374feb22b34099ccc6dd32128e18d088ff9a81837952b29f05110b308c09f26" 56 | "sha512=fce1aa520db785c25ded75a959e9dafeb7887d4f5deeb14b044cd5b9e2d235dca24589d794d2f01513765bc4764cf72f8659bd15f3a4fc06efa61363dc5d709b" 57 | ] 58 | } 59 | -------------------------------------------------------------------------------- /test/bin/require-cross-compiling-packages.t/opam-repository/packages/ocaml-base-compiler/ocaml-base-compiler.4.13.1/opam: -------------------------------------------------------------------------------- 1 | opam-version: "2.0" 2 | synopsis: "Official release 4.13.1" 3 | maintainer: "platform@lists.ocaml.org" 4 | license: "LGPL-2.1-or-later WITH OCaml-LGPL-linking-exception" 5 | authors: "Xavier Leroy and many contributors" 6 | homepage: "https://ocaml.org" 7 | bug-reports: "https://github.com/ocaml/opam-repository/issues" 8 | dev-repo: "git+https://github.com/ocaml/ocaml" 9 | depends: [ 10 | "ocaml" {= "4.13.1" & post} 11 | "base-unix" {post} 12 | "base-bigarray" {post} 13 | "base-threads" {post} 14 | "ocaml-options-vanilla" {post} 15 | ] 16 | conflict-class: "ocaml-core-compiler" 17 | flags: compiler 18 | setenv: CAML_LD_LIBRARY_PATH = "%{lib}%/stublibs" 19 | build: [ 20 | [ 21 | "./configure" 22 | "--prefix=%{prefix}%" 23 | "-C" 24 | "CC=cc" {os = "openbsd" | os = "macos"} 25 | "ASPP=cc -c" {os = "openbsd" | os = "macos"} 26 | ] 27 | [make "-j%{jobs}%"] 28 | ] 29 | install: [make "install"] 30 | url { 31 | src: "https://github.com/ocaml/ocaml/archive/4.13.1.tar.gz" 32 | checksum: "sha256=194c7988cc1fd1c64f53f32f2f7551e5309e44d914d6efc7e2e4d002296aeac4" 33 | } 34 | extra-files: ["ocaml-base-compiler.install" "md5=3e969b841df1f51ca448e6e6295cb451"] 35 | post-messages: [ 36 | "A failure in the middle of the build may be caused by build parallelism 37 | (enabled by default). 38 | Please file a bug report at https://github.com/ocaml/opam-repository/issues" 39 | {failure & jobs > 1} 40 | "You can try installing again including --jobs=1 41 | to force a sequential build instead." 42 | {failure & jobs > 1 & opam-version >= "2.0.5"} 43 | ] 44 | -------------------------------------------------------------------------------- /test/bin/require-cross-compiling-packages.t/opam-repository/packages/ocaml-config/ocaml-config.2/opam: -------------------------------------------------------------------------------- 1 | opam-version: "2.0" 2 | synopsis: "OCaml Switch Configuration" 3 | description: """ 4 | This package is used by the OCaml package to set-up its variables.""" 5 | maintainer: "platform@lists.ocaml.org" 6 | authors: [ 7 | "Louis Gesbert " 8 | "David Allsopp " 9 | ] 10 | homepage: "https://opam.ocaml.org/" 11 | bug-reports: "https://github.com/ocaml/opam/issues" 12 | depends: [ 13 | "ocaml-base-compiler" {>= "4.12.0~"} | 14 | "ocaml-variants" {>= "4.12.0~"} | 15 | "ocaml-system" {>= "4.12.0~"} 16 | ] 17 | substs: "gen_ocaml_config.ml" 18 | extra-files: [ 19 | ["gen_ocaml_config.ml.in" "md5=a4b41e3236593d8271295b84b0969172"] 20 | ["ocaml-config.install" "md5=8e50c5e2517d3463b3aad649748cafd7"] 21 | ] 22 | -------------------------------------------------------------------------------- /test/bin/require-cross-compiling-packages.t/opam-repository/packages/ocaml-options-vanilla/ocaml-options-vanilla.1/opam: -------------------------------------------------------------------------------- 1 | opam-version: "2.0" 2 | synopsis: "Ensure that OCaml is compiled with no special options enabled" 3 | depends: [ 4 | "ocaml-base-compiler" {post} | 5 | "ocaml-system" {post} | 6 | "ocaml-variants" {post & >= "4.12.0~"} 7 | ] 8 | conflicts: [ 9 | "ocaml-option-32bit" 10 | "ocaml-option-afl" 11 | "ocaml-option-bytecode-only" 12 | "ocaml-option-default-unsafe-string" 13 | "ocaml-option-flambda" 14 | "ocaml-option-fp" 15 | "ocaml-option-musl" 16 | "ocaml-option-no-flat-float-array" 17 | "ocaml-option-spacetime" 18 | "ocaml-option-static" 19 | "ocaml-option-nnp" 20 | "ocaml-option-nnpchecker" 21 | ] 22 | maintainer: "platform@lists.ocaml.org" 23 | flags: compiler 24 | -------------------------------------------------------------------------------- /test/bin/require-cross-compiling-packages.t/opam-repository/packages/ocaml/ocaml.4.13.1/opam: -------------------------------------------------------------------------------- 1 | opam-version: "2.0" 2 | license: "LGPL-2.1-or-later WITH OCaml-LGPL-linking-exception" 3 | synopsis: "The OCaml compiler (virtual package)" 4 | description: """ 5 | This package requires a matching implementation of OCaml, 6 | and polls it to initialise specific variables like `ocaml:native-dynlink`""" 7 | maintainer: "platform@lists.ocaml.org" 8 | depends: [ 9 | "ocaml-config" {>= "2"} 10 | "ocaml-base-compiler" {>= "4.13.1~" & < "4.13.2~"} | 11 | "ocaml-variants" {>= "4.13.1~" & < "4.13.2~"} | 12 | "ocaml-system" {>= "4.13.1" & < "4.13.2~"} 13 | ] 14 | setenv: [ 15 | [CAML_LD_LIBRARY_PATH = "%{_:stubsdir}%"] 16 | [CAML_LD_LIBRARY_PATH += "%{lib}%/stublibs"] 17 | [OCAML_TOPLEVEL_PATH = "%{toplevel}%"] 18 | ] 19 | build: ["ocaml" "%{ocaml-config:share}%/gen_ocaml_config.ml" _:version _:name] 20 | build-env: CAML_LD_LIBRARY_PATH = "" 21 | homepage: "https://ocaml.org" 22 | bug-reports: "https://github.com/ocaml/opam-repository/issues" 23 | authors: [ 24 | "Xavier Leroy" 25 | "Damien Doligez" 26 | "Alain Frisch" 27 | "Jacques Garrigue" 28 | "Didier Rémy" 29 | "Jérôme Vouillon" 30 | ] 31 | -------------------------------------------------------------------------------- /test/bin/require-cross-compiling-packages.t/opam-repository/repo: -------------------------------------------------------------------------------- 1 | opam-version: "2.0" 2 | -------------------------------------------------------------------------------- /test/bin/require-cross-compiling-packages.t/run.t: -------------------------------------------------------------------------------- 1 | Here we have three local repositories: one that roughly represents 2 | opam-repository, one which corresponds to opam-overlays and one 3 | to mirage-opam-overlays. 4 | 5 | In this scenario we depend on a package b that does not build with dune in its 6 | original 0.1 release. There is a 0.1+dune port of it in opam-overlays: 7 | 8 | $ cat opam-overlays/packages/b/b.0.1+dune/opam 9 | opam-version: "2.0" 10 | dev-repo: "git+https://b.com/b.git" 11 | depends: [ 12 | "dune" 13 | ] 14 | build: [ "dune" "build" ] 15 | url { 16 | src: "https://dune.com/b.0.1-dune.tbz" 17 | checksum: "sha256=0000000000000000000000000000000000000000000000000000000000000001" 18 | } 19 | 20 | This dune port cannot be cross compiled in its current form so the mirage 21 | maintainers created a 0.1+dune+mirage port in mirage-opam-overlays: 22 | 23 | $ cat mirage-opam-overlays/packages/b/b.0.1+dune+mirage/opam 24 | opam-version: "2.0" 25 | dev-repo: "git+https://b.com/b.git" 26 | depends: [ 27 | "dune" 28 | ] 29 | build: [ "dune" "build" ] 30 | tags: ["cross-compile"] 31 | url { 32 | src: "https://mirage.com/b.0.1-dune-mirage.tbz" 33 | checksum: "sha256=0000000000000000000000000000000000000000000000000000000000000002" 34 | } 35 | 36 | You'll note the "cross-compile" tag that we use to mark packages that can be 37 | cross compiled in a dune-workspace. 38 | 39 | For testing purposes we define two opam files: without-mirage.opam and with-mirage.opam 40 | that are essencially the same: they both depends on b but the latter configures 41 | the solver to use the mirage overlays in addition with upstream and 42 | dune-universe: 43 | 44 | $ opam show --just-file -fdepends ./without-mirage.opam 45 | dune, b 46 | $ opam show --just-file -fx-opam-monorepo-opam-repositories --raw ./without-mirage.opam 47 | file://$OPAM_MONOREPO_CWD/opam-repository, file://$OPAM_MONOREPO_CWD/opam-overlays 48 | $ opam show --just-file -fdepends ./with-mirage.opam 49 | dune, b 50 | $ opam show --just-file -fx-opam-monorepo-opam-repositories --raw ./with-mirage.opam 51 | file://$OPAM_MONOREPO_CWD/opam-repository, file://$OPAM_MONOREPO_CWD/opam-overlays, file://$OPAM_MONOREPO_CWD/mirage-opam-overlays 52 | 53 | Until there is a new release, everything goes fine. If we don't add the mirage 54 | overlays the solver picks the dune port as expected: 55 | 56 | $ opam-monorepo lock without-mirage > /dev/null 57 | $ opam show --just-file -fdepends ./without-mirage.opam.locked | grep "\"b\"" 58 | "b" {= "0.1+dune" & ?vendor} 59 | 60 | If we add the mirage overlays, the mirage port gets picked instead as its 61 | version is higher (+mirage): 62 | 63 | $ opam-monorepo lock with-mirage > /dev/null 64 | $ opam show --just-file -fdepends ./with-mirage.opam.locked | grep "\"b\"" 65 | "b" {= "0.1+dune+mirage" & ?vendor} 66 | 67 | So far so good. Problems arise when a new release of b hits upstream. We managed 68 | to upstream the dune port before `0.2` so the `0.2` release builds with dune. 69 | 70 | $ mkdir opam-repository/packages/b/b.0.2 71 | $ cat >opam-repository/packages/b/b.0.2/opam < opam-version: "2.0" 73 | > dev-repo: "git+https://b.com/b.git" 74 | > depends: [ 75 | > "dune" 76 | > ] 77 | > build: [ "dune" "build" ] 78 | > url { 79 | > src: "https://b.com/b.0.2.tbz" 80 | > checksum: "sha256=0000000000000000000000000000000000000000000000000000000000000003" 81 | > } 82 | > EOF 83 | 84 | Regular users of opam-monorepo will get the 0.2 version and be happy with it: 85 | 86 | $ opam-monorepo lock without-mirage > /dev/null 87 | $ opam show --just-file -fdepends ./without-mirage.opam.locked | grep "\"b\"" 88 | "b" {= "0.2" & ?vendor} 89 | 90 | Mirage users on the other hand will get it as well, meaning they can't cross 91 | compile their unikernel anymore. The solver is happy but this will cause errors 92 | at build time for them: 93 | 94 | $ opam-monorepo lock with-mirage > /dev/null 95 | $ grep "\"b\"\s\+{" with-mirage.opam.locked 96 | "b" {= "0.2" & ?vendor} 97 | 98 | We added the --require-cross-compile flag to select packages that cross compile 99 | when available. 100 | Here, if we don't add mirage overlays and run the solver with this flag, we 101 | still get the latest release: 102 | 103 | $ opam-monorepo lock --require-cross-compile without-mirage > /dev/null 104 | $ opam show --just-file -fdepends ./without-mirage.opam.locked | grep "\"b\"" 105 | "b" {= "0.2" & ?vendor} 106 | 107 | If we run it with mirage overlays though, it will detect that there exist 108 | versions that cross compile and favor those instead: 109 | 110 | $ opam-monorepo lock --require-cross-compile with-mirage > /dev/null 111 | $ opam show --just-file -fdepends ./with-mirage.opam.locked | grep "\"b\"" 112 | "b" {= "0.1+dune+mirage" & ?vendor} 113 | 114 | Note that if the upstream released version does cross compile, it can add the 115 | tag to be picked instead: 116 | 117 | $ echo "tags: [\"cross-compile\"]" >> opam-repository/packages/b/b.0.2/opam 118 | $ opam-monorepo lock --require-cross-compile with-mirage > /dev/null 119 | $ opam show --just-file -fdepends ./with-mirage.opam.locked | grep "\"b\"" 120 | "b" {= "0.2" & ?vendor} 121 | -------------------------------------------------------------------------------- /test/bin/require-cross-compiling-packages.t/with-mirage.opam: -------------------------------------------------------------------------------- 1 | opam-version: "2.0" 2 | depends: [ 3 | "dune" 4 | "b" 5 | ] 6 | x-opam-monorepo-opam-repositories: [ 7 | "file://$OPAM_MONOREPO_CWD/opam-repository" 8 | "file://$OPAM_MONOREPO_CWD/opam-overlays" 9 | "file://$OPAM_MONOREPO_CWD/mirage-opam-overlays" 10 | ] 11 | -------------------------------------------------------------------------------- /test/bin/require-cross-compiling-packages.t/without-mirage.opam: -------------------------------------------------------------------------------- 1 | opam-version: "2.0" 2 | depends: [ 3 | "dune" 4 | "b" 5 | ] 6 | x-opam-monorepo-opam-repositories: [ 7 | "file://$OPAM_MONOREPO_CWD/opam-repository" 8 | "file://$OPAM_MONOREPO_CWD/opam-overlays" 9 | ] 10 | -------------------------------------------------------------------------------- /test/bin/simple-lock.t/repo/packages/b/b.1/opam: -------------------------------------------------------------------------------- 1 | opam-version: "2.0" 2 | depends: [ 3 | "dune" 4 | ] 5 | build: [ "dune" "build" ] 6 | dev-repo: "git+https://github.com/b/b" 7 | url { 8 | src: "https://b.com/b.tbz" 9 | checksum: [ 10 | "sha256=0000000000000000000000000000000000000000000000000000000000000000" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /test/bin/simple-lock.t/repo/packages/c/c.1/opam: -------------------------------------------------------------------------------- 1 | opam-version: "2.0" 2 | depends: [ 3 | "dune" 4 | ] 5 | build: [ "dune" "build" ] 6 | dev-repo: "git+https://github.com/c/c" 7 | url { 8 | src: "https://c.com/c.tbz" 9 | checksum: [ 10 | "sha256=0000000000000000000000000000000000000000000000000000000000000001" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /test/bin/simple-lock.t/repo/repo: -------------------------------------------------------------------------------- 1 | opam-version: "2.0" 2 | -------------------------------------------------------------------------------- /test/bin/simple-lock.t/run.t: -------------------------------------------------------------------------------- 1 | We have a simple project with a single package defined at the root. 2 | It has a `x-opam-monorepo-opam-repositories` field set to use a local 3 | opam-repository for locking 4 | 5 | $ cat simple-lock.opam 6 | opam-version: "2.0" 7 | depends: [ 8 | "dune" 9 | "b" 10 | "c" 11 | ] 12 | x-opam-monorepo-opam-repositories: [ 13 | "file://$OPAM_MONOREPO_CWD/minimal-repo" 14 | "file://$OPAM_MONOREPO_CWD/repo" 15 | ] 16 | 17 | We provided a minimal opam-repository but locking should be successful. 18 | 19 | $ gen-minimal-repo 20 | $ opam-monorepo lock 21 | ==> Using 1 locally scanned package as the target. 22 | ==> Found 10 opam dependencies for the target package. 23 | ==> Querying opam database for their metadata and Dune compatibility. 24 | ==> Calculating exact pins for each of them. 25 | ==> Wrote lockfile with 2 entries to $TESTCASE_ROOT/simple-lock.opam.locked. You can now run opam monorepo pull to fetch their sources. 26 | 27 | The lockfile should contain the base packages, dune and our 2 dependencies 28 | `b` and `c` which should be pulled in the duniverse 29 | 30 | $ cat simple-lock.opam.locked 31 | opam-version: "2.0" 32 | synopsis: "opam-monorepo generated lockfile" 33 | maintainer: "opam-monorepo" 34 | depends: [ 35 | "b" {= "1" & ?vendor} 36 | "base-bigarray" {= "base"} 37 | "base-threads" {= "base"} 38 | "base-unix" {= "base"} 39 | "c" {= "1" & ?vendor} 40 | "dune" {= "2.9.1"} 41 | "ocaml" {= "4.13.1"} 42 | "ocaml-base-compiler" {= "4.13.1"} 43 | "ocaml-config" {= "2"} 44 | "ocaml-options-vanilla" {= "1"} 45 | ] 46 | pin-depends: [ 47 | ["b.1" "https://b.com/b.tbz"] 48 | ["c.1" "https://c.com/c.tbz"] 49 | ] 50 | x-opam-monorepo-duniverse-dirs: [ 51 | [ 52 | "https://b.com/b.tbz" 53 | "b" 54 | [ 55 | "sha256=0000000000000000000000000000000000000000000000000000000000000000" 56 | ] 57 | ] 58 | [ 59 | "https://c.com/c.tbz" 60 | "c" 61 | [ 62 | "sha256=0000000000000000000000000000000000000000000000000000000000000001" 63 | ] 64 | ] 65 | ] 66 | x-opam-monorepo-opam-repositories: [ 67 | "file://$OPAM_MONOREPO_CWD/minimal-repo" "file://$OPAM_MONOREPO_CWD/repo" 68 | ] 69 | x-opam-monorepo-root-packages: ["simple-lock"] 70 | x-opam-monorepo-version: "0.3" 71 | -------------------------------------------------------------------------------- /test/bin/simple-lock.t/simple-lock.opam: -------------------------------------------------------------------------------- 1 | opam-version: "2.0" 2 | depends: [ 3 | "dune" 4 | "b" 5 | "c" 6 | ] 7 | x-opam-monorepo-opam-repositories: [ 8 | "file://$OPAM_MONOREPO_CWD/minimal-repo" 9 | "file://$OPAM_MONOREPO_CWD/repo" 10 | ] 11 | -------------------------------------------------------------------------------- /test/lib/dune: -------------------------------------------------------------------------------- 1 | (test 2 | (name test_duniverse_lib) 3 | (libraries alcotest duniverse_lib logs.fmt) 4 | (action 5 | (run %{test} -e))) 6 | -------------------------------------------------------------------------------- /test/lib/test_dev_repo.ml: -------------------------------------------------------------------------------- 1 | let test_repo_name = 2 | let make_test ~dev_repo ~expected () = 3 | let test_name = Printf.sprintf "repo_name: %s" dev_repo in 4 | let test_fun () = 5 | let actual = Duniverse_lib.Dev_repo.(repo_name (from_string dev_repo)) in 6 | Alcotest.(check (result string Testable.r_msg)) test_name expected actual 7 | in 8 | (test_name, `Quick, test_fun) 9 | in 10 | [ 11 | make_test ~dev_repo:"git://github.com/ocamllabs/opam-monorepo" 12 | ~expected:(Ok "opam-monorepo") (); 13 | make_test ~dev_repo:"git://github.com/ocamllabs/opam-monorepo.git" 14 | ~expected:(Ok "opam-monorepo") (); 15 | make_test ~dev_repo:"git+https://github.com/ocamllabs/opam-monorepo.git" 16 | ~expected:(Ok "opam-monorepo") (); 17 | make_test ~dev_repo:"git+https://github.com/ocamllabs/opam-monorepo/" 18 | ~expected:(Ok "opam-monorepo") (); 19 | make_test ~dev_repo:"git+https://github.com/ocamllabs/opam-monorepo.1/" 20 | ~expected:(Ok "opam-monorepo") (); 21 | ] 22 | |> List.append 23 | (let invalid_repos = 24 | [ 25 | ""; 26 | "."; 27 | "/"; 28 | "./."; 29 | "..."; 30 | "///"; 31 | "https://github.com"; 32 | "https://github.com/"; 33 | ] 34 | in 35 | let make_test_error dev_repo = 36 | make_test ~dev_repo 37 | ~expected: 38 | (Rresult.R.error_msgf 39 | "unexpected empty string while computing name for dev_repo: \ 40 | \"%s\"" 41 | dev_repo) 42 | () 43 | in 44 | List.map make_test_error invalid_repos) 45 | 46 | let suite = ("Dev_repo", List.concat [ test_repo_name ]) 47 | -------------------------------------------------------------------------------- /test/lib/test_dev_repo.mli: -------------------------------------------------------------------------------- 1 | val suite : string * unit Alcotest.test_case list 2 | -------------------------------------------------------------------------------- /test/lib/test_dune_file.ml: -------------------------------------------------------------------------------- 1 | module Lang = struct 2 | let from_content = 3 | let make_test ~name ~content ~expected () = 4 | let test_name = Printf.sprintf "Lang.from_content: %s" name in 5 | let test_fun () = 6 | let actual = Duniverse_lib.Dune_file.Lang.from_content content in 7 | Alcotest.(check (result (pair int int) Testable.r_msg)) 8 | test_name expected actual 9 | in 10 | (test_name, `Quick, test_fun) 11 | in 12 | let invalid_dune_project = 13 | Error 14 | (`Msg 15 | "Invalid dune-project file: It does not start with a valid lang \ 16 | stanza") 17 | in 18 | [ 19 | make_test ~name:"Empty dune-project" ~content:"" 20 | ~expected:invalid_dune_project (); 21 | make_test ~name:"Valid version" 22 | ~expected:(Ok (1, 2)) 23 | ~content:"(lang dune 1.2)" (); 24 | make_test ~name:"Newlines" 25 | ~expected:(Ok (1, 2)) 26 | ~content:"(lang dune 1.2)\n(name my-project)\n\n" (); 27 | make_test ~name:"Windows newlines" 28 | ~expected:(Ok (1, 2)) 29 | ~content:"(lang dune 1.2)\r\n(name my-project)\r\n\r\n" (); 30 | make_test ~name:"Invalid version" 31 | ~expected: 32 | (Error 33 | (`Msg 34 | "Invalid dune-project file: invalid lang version \ 35 | 1.999999999999999999999999999")) 36 | ~content:"(lang dune 1.999999999999999999999999999)" (); 37 | make_test ~name:"Invalid lang stanza" ~expected:invalid_dune_project 38 | ~content:"(lang dune ver)\n(name my-project)\n" (); 39 | make_test ~name:"Does not start with lang stanza" 40 | ~expected:invalid_dune_project 41 | ~content:"\n\n(lang dune ver)\n(name my-project)\n" (); 42 | ] 43 | 44 | let update = 45 | let make_test ~name ~version ~content ~expected () = 46 | let test_name = Printf.sprintf "Lang.update: %s" name in 47 | let test_fun () = 48 | let actual = Duniverse_lib.Dune_file.Lang.update ~version content in 49 | Alcotest.(check string) test_name expected actual 50 | in 51 | (test_name, `Quick, test_fun) 52 | in 53 | [ 54 | make_test ~name:"No stanza" ~version:(1, 3) ~expected:"(name my-project)" 55 | ~content:"(name my-project)" (); 56 | make_test ~name:"Replace" ~version:(1, 3) ~expected:"(lang dune 1.3)" 57 | ~content:"(lang dune 1.2)" (); 58 | make_test ~name:"Newlines" ~version:(1, 3) 59 | ~expected:"(lang dune 1.3)\n(name my-project)\n" 60 | ~content:"(lang dune 1.2)\n(name my-project)\n" (); 61 | make_test ~name:"Windows newlines" ~version:(1, 3) 62 | ~expected:"(lang dune 1.3)\r\n(name my-project)\r\n" 63 | ~content:"(lang dune 1.2)\r\n(name my-project)\r\n" (); 64 | ] 65 | end 66 | 67 | let suite = ("Dune_file", List.concat [ Lang.from_content; Lang.update ]) 68 | -------------------------------------------------------------------------------- /test/lib/test_dune_file.mli: -------------------------------------------------------------------------------- 1 | val suite : string * unit Alcotest.test_case list 2 | -------------------------------------------------------------------------------- /test/lib/test_duniverse.mli: -------------------------------------------------------------------------------- 1 | val suite : string * unit Alcotest.test_case list 2 | -------------------------------------------------------------------------------- /test/lib/test_duniverse_lib.ml: -------------------------------------------------------------------------------- 1 | let () = 2 | Logs.set_level (Some Logs.Debug); 3 | Logs.set_reporter (Logs_fmt.reporter ()) 4 | 5 | let () = 6 | Alcotest.run "Duniverse" 7 | [ 8 | Test_dev_repo.suite; 9 | Test_dune_file.suite; 10 | Test_duniverse.suite; 11 | Test_git.suite; 12 | Test_opam.suite; 13 | Test_parallel.suite; 14 | Test_pin_depends.suite; 15 | Test_serial_shape.suite; 16 | Test_source_opam_config.suite; 17 | Test_uri_utils.suite; 18 | Test_solve.suite; 19 | ] 20 | -------------------------------------------------------------------------------- /test/lib/test_duniverse_lib.mli: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarides/opam-monorepo/2d7e3a3a3c7d7527b005442798ea5af8b0043b0a/test/lib/test_duniverse_lib.mli -------------------------------------------------------------------------------- /test/lib/test_git.mli: -------------------------------------------------------------------------------- 1 | val suite : string * unit Alcotest.test_case list 2 | -------------------------------------------------------------------------------- /test/lib/test_opam.mli: -------------------------------------------------------------------------------- 1 | val suite : string * unit Alcotest.test_case list 2 | -------------------------------------------------------------------------------- /test/lib/test_parallel.ml: -------------------------------------------------------------------------------- 1 | let test_parallel = 2 | let make_test ~name ~input ~f ~expected () = 3 | let test_name = Printf.sprintf "map: %s" name in 4 | let test_fun () = 5 | let actual = Duniverse_lib.Parallel.map ~f input in 6 | Alcotest.(check (slist int ( - ))) test_name expected actual 7 | in 8 | (test_name, `Quick, test_fun) 9 | in 10 | let id x = x in 11 | let wait_and_sum x = 12 | let rng = Random.float 0.5 in 13 | Thread.delay rng; 14 | x + 1 15 | in 16 | let input = List.init 50 id in 17 | let input_plus_1 = List.map (fun x -> x + 1) input in 18 | [ 19 | make_test ~name:"Identity" ~input ~f:id ~expected:input (); 20 | make_test ~name:"Sleep and add 1" ~input ~f:wait_and_sum 21 | ~expected:input_plus_1 (); 22 | ] 23 | 24 | let suite = ("Parallel", test_parallel) 25 | -------------------------------------------------------------------------------- /test/lib/test_parallel.mli: -------------------------------------------------------------------------------- 1 | val suite : string * unit Alcotest.test_case list 2 | -------------------------------------------------------------------------------- /test/lib/test_pin_depends.ml: -------------------------------------------------------------------------------- 1 | let t = 2 | Alcotest.testable Duniverse_lib.Pin_depends.pp Duniverse_lib.Pin_depends.equal 3 | 4 | let test_sort_uniq = 5 | let convert (pkg, url) = (OpamPackage.of_string pkg, OpamUrl.parse url) in 6 | let make_test ~name ~input ~expected () = 7 | let test_name = Printf.sprintf "sort_uniq: %s" name in 8 | let test_fun () = 9 | let input = List.map convert input in 10 | let expected = Rresult.R.map (List.map convert) expected in 11 | let actual = Duniverse_lib.Pin_depends.sort_uniq input in 12 | Alcotest.(check (result (list t) Testable.r_msg)) 13 | test_name expected actual 14 | in 15 | (test_name, `Quick, test_fun) 16 | in 17 | [ 18 | make_test ~name:"Empty" ~input:[] ~expected:(Ok []) (); 19 | make_test ~name:"Preserve" 20 | ~input:[ ("a.1", "git+https://repo.com") ] 21 | ~expected:(Ok [ ("a.1", "git+https://repo.com") ]) 22 | (); 23 | make_test ~name:"Dedup" 24 | ~input: 25 | [ ("a.1", "git+https://repo.com"); ("a.1", "git+https://repo.com") ] 26 | ~expected:(Ok [ ("a.1", "git+https://repo.com") ]) 27 | (); 28 | make_test ~name:"Different versions" 29 | ~input: 30 | [ ("a.1", "git+https://repo.com"); ("a.2", "git+https://repo.com") ] 31 | ~expected: 32 | (Rresult.R.error_msg 33 | "Package a is pinned to different versions/url:\n\ 34 | \ - a.2: git+https://repo.com\n\ 35 | \ - a.1: git+https://repo.com") 36 | (); 37 | make_test ~name:"Different URLs" 38 | ~input: 39 | [ 40 | ("a.1", "git+https://repo.com#master"); 41 | ("a.1", "git+https://repo.com#branch"); 42 | ] 43 | ~expected: 44 | (Rresult.R.error_msg 45 | "Package a is pinned to different versions/url:\n\ 46 | \ - a.1: git+https://repo.com#branch\n\ 47 | \ - a.1: git+https://repo.com#master") 48 | (); 49 | ] 50 | 51 | let suite = ("Pin_depends", List.concat [ test_sort_uniq ]) 52 | -------------------------------------------------------------------------------- /test/lib/test_pin_depends.mli: -------------------------------------------------------------------------------- 1 | val suite : string * unit Alcotest.test_case list 2 | -------------------------------------------------------------------------------- /test/lib/test_serial_shape.mli: -------------------------------------------------------------------------------- 1 | val suite : string * unit Alcotest.test_case list 2 | -------------------------------------------------------------------------------- /test/lib/test_solve.ml: -------------------------------------------------------------------------------- 1 | let calculate universe ?(pins = []) root expected = 2 | let preferred_versions = OpamPackage.Name.Map.empty in 3 | let local_opam_files = OpamPackage.Name.Map.empty in 4 | let target_packages = 5 | OpamPackage.Name.Set.singleton (OpamFile.OPAM.name root) 6 | in 7 | let opam_provided = OpamPackage.Name.Set.empty in 8 | let pin_depends = OpamPackage.Name.Map.empty in 9 | let opam_env = Stdext.String.Map.empty in 10 | let pkgs = root :: universe in 11 | let solver = Duniverse_lib.Opam_solve.mock_solver in 12 | match 13 | Duniverse_lib.Opam_solve.calculate ~build_only:false ~allow_jbuilder:false 14 | ~require_cross_compile:false ~preferred_versions ~local_opam_files 15 | ~target_packages ~opam_provided ~pin_depends solver (opam_env, pkgs, pins) 16 | with 17 | | Ok es -> 18 | let es = 19 | List.map 20 | (fun es -> 21 | let pkg = 22 | es.Duniverse_lib.Opam.Dependency_entry.package_summary.package 23 | in 24 | ( OpamPackage.(Name.to_string (name pkg)), 25 | OpamPackage.(Version.to_string (version pkg)) )) 26 | es 27 | in 28 | Alcotest.(check (slist (pair string string) compare)) __LOC__ expected es 29 | | Error (`Diagnostics d) -> 30 | let (`Msg e) = 31 | Duniverse_lib.Opam_solve.diagnostics_message ~verbose:false solver d 32 | in 33 | Alcotest.failf "error: diagnostic %s" e 34 | | Error (`Msg e) -> Alcotest.fail e 35 | 36 | let opam_file stanzas = 37 | let opam_stanzas = {|opam-version: "2.0"|} :: stanzas in 38 | let pp_stanzas = Fmt.list ~sep:(Fmt.any "\n") Fmt.string in 39 | let opam_string = Format.asprintf "%a" pp_stanzas opam_stanzas in 40 | OpamFile.OPAM.read_from_string opam_string 41 | 42 | let quoted = Printf.sprintf "%s: %S" 43 | let name = quoted "name" 44 | let version = quoted "version" 45 | let dev_repo = quoted "dev-repo" 46 | 47 | let url ~src ?checksum () = 48 | match checksum with 49 | | Some checksum -> 50 | Printf.sprintf {|url { 51 | src: %S 52 | checksum: "sha256=%064d" 53 | }|} src 54 | checksum 55 | | None -> Printf.sprintf {|url { 56 | src: %S 57 | }|} src 58 | 59 | let pp_list = Fmt.list ~sep:Fmt.sp Fmt.Dump.string 60 | let depends pkgs = Format.asprintf "depends: [%a]" pp_list pkgs 61 | let conflict_class cls = Format.asprintf "conflict-class: [%a]" pp_list cls 62 | 63 | let ocaml_base_compiler, ocaml_base_expected = 64 | let n = "ocaml-base-compiler" in 65 | let v = "3.14" in 66 | (opam_file [ name n; version v ], (n, v)) 67 | 68 | let simple () = 69 | let universe = 70 | [ 71 | ocaml_base_compiler; 72 | opam_file [ name "p1"; version "1" ]; 73 | opam_file [ name "p1"; version "2" ]; 74 | opam_file [ name "p2"; version "1" ]; 75 | ] 76 | in 77 | let root = opam_file [ name "root"; version "0"; depends [ "p1"; "p2" ] ] in 78 | calculate universe root 79 | [ ocaml_base_expected; ("p1", "2"); ("p2", "1"); ("root", "0") ] 80 | 81 | let conflicts () = 82 | let universe = 83 | [ 84 | ocaml_base_compiler; 85 | opam_file [ name "p1"; version "1" ]; 86 | opam_file [ name "p1"; version "2"; {|conflicts: ["p2" {= "1"}]|} ]; 87 | opam_file [ name "p2"; version "1" ]; 88 | ] 89 | in 90 | let root = opam_file [ name "root"; version "0"; depends [ "p1"; "p2" ] ] in 91 | calculate universe root 92 | [ ocaml_base_expected; ("p1", "1"); ("p2", "1"); ("root", "0") ] 93 | 94 | let conflict_class () = 95 | let universe = 96 | [ 97 | ocaml_base_compiler; 98 | opam_file [ name "p1"; version "1"; conflict_class [ "x" ] ]; 99 | opam_file [ name "p1"; version "2"; conflict_class [ "y" ] ]; 100 | opam_file [ name "p2"; version "1"; conflict_class [ "y" ] ]; 101 | ] 102 | in 103 | let root = opam_file [ name "root"; version "0"; depends [ "p1"; "p2" ] ] in 104 | calculate universe root 105 | [ ocaml_base_expected; ("p1", "1"); ("p2", "1"); ("root", "0") ] 106 | 107 | let universe_with_url = 108 | [ 109 | ocaml_base_compiler; 110 | opam_file 111 | [ 112 | name "p1"; 113 | version "1"; 114 | dev_repo "x"; 115 | url ~src:"https://p.com/p.tbz" ~checksum:1 (); 116 | ]; 117 | opam_file 118 | [ 119 | name "p1"; 120 | version "2"; 121 | dev_repo "x"; 122 | url ~src:"https://p.com/p.tbz" ~checksum:2 (); 123 | ]; 124 | opam_file 125 | [ 126 | name "p2"; 127 | version "1"; 128 | dev_repo "x"; 129 | url ~src:"https://p.com/p.tbz" ~checksum:1 (); 130 | ]; 131 | opam_file 132 | [ 133 | name "p2"; 134 | version "2"; 135 | dev_repo "x"; 136 | url ~src:"https://p.com/p.tbz" ~checksum:2 (); 137 | ]; 138 | ] 139 | 140 | let conflict_url () = 141 | let universe = universe_with_url in 142 | let root = 143 | opam_file [ name "root"; version "0"; {|depends: ["p1" {= "1"} "p2"] |} ] 144 | in 145 | calculate universe root 146 | [ ocaml_base_expected; ("p1", "1"); ("p2", "1"); ("root", "0") ] 147 | 148 | let no_conflict_with_pin () = 149 | let p2_dev = 150 | opam_file 151 | [ name "p2"; version "0"; dev_repo "x"; url ~src:"git+https://x#hash" () ] 152 | in 153 | let universe = p2_dev :: universe_with_url in 154 | let root = opam_file [ name "root"; version "0"; depends [ "p1"; "p2" ] ] in 155 | calculate 156 | ~pins:[ OpamPackage.of_string "p2.0" ] 157 | universe root 158 | [ ocaml_base_expected; ("p1", "2"); ("p2", "0"); ("root", "0") ] 159 | 160 | let suite = 161 | ( "solve", 162 | [ 163 | Alcotest.test_case "simple" `Quick simple; 164 | Alcotest.test_case "conflicts" `Quick conflicts; 165 | Alcotest.test_case "conflict_class" `Quick conflict_class; 166 | Alcotest.test_case "conflict_url" `Quick conflict_url; 167 | Alcotest.test_case "no_conflict_with_pin" `Quick no_conflict_with_pin; 168 | ] ) 169 | -------------------------------------------------------------------------------- /test/lib/test_solve.mli: -------------------------------------------------------------------------------- 1 | val suite : unit Alcotest.test 2 | -------------------------------------------------------------------------------- /test/lib/test_source_opam_config.mli: -------------------------------------------------------------------------------- 1 | val suite : string * unit Alcotest.test_case list 2 | -------------------------------------------------------------------------------- /test/lib/test_uri_utils.ml: -------------------------------------------------------------------------------- 1 | module Normalized = Duniverse_lib.Uri_utils.Normalized 2 | 3 | let test_canonical_uri = 4 | let make_test ~supplied:(a, b) ~expected = 5 | let a = Uri.of_string a in 6 | let a' = Normalized.of_uri a in 7 | let b = Uri.of_string b in 8 | let b' = Normalized.of_uri b in 9 | let test_name = Fmt.str "Comparing %a and %a" Uri.pp a Uri.pp b in 10 | let test_fun () = 11 | let actual = Normalized.equal a' b' in 12 | Alcotest.(check bool) test_name expected actual 13 | in 14 | (test_name, `Quick, test_fun) 15 | in 16 | [ 17 | make_test 18 | ~supplied: 19 | ( "git+https://github.com/mirage/mirage-clock.git", 20 | "git+https://github.com/mirage/mirage-clock.git" ) 21 | ~expected:true; 22 | make_test 23 | ~supplied: 24 | ( "git://github.com/mirage/mirage-clock.git", 25 | "git+https://github.com/mirage/mirage-clock.git" ) 26 | ~expected:true; 27 | make_test 28 | ~supplied: 29 | ( "https://github.com/mirage/mirage-clock.git", 30 | "git+https://github.com/mirage/mirage-clock.git" ) 31 | ~expected:true; 32 | make_test 33 | ~supplied: 34 | ( "git+https://github.com/mirage/mirage-clock.git#master", 35 | "git+https://github.com/mirage/mirage-clock.git" ) 36 | ~expected:true; 37 | make_test 38 | ~supplied: 39 | ( "git+https://github.com/mirage/mirage-clock", 40 | "git+https://github.com/mirage/mirage-clock.git" ) 41 | ~expected:true; 42 | make_test 43 | ~supplied: 44 | ( "git+https://github.com/mirage/mirage-foo.git", 45 | "git+https://github.com/mirage/mirage-bar.git" ) 46 | ~expected:false; 47 | make_test 48 | ~supplied: 49 | ( "git+https://github.com/mirage/mirage.git", 50 | "git+https://github.com/anchorage/mirage.git" ) 51 | ~expected:false; 52 | ] 53 | 54 | let suite = ("Uri_utils", test_canonical_uri) 55 | -------------------------------------------------------------------------------- /test/lib/test_uri_utils.mli: -------------------------------------------------------------------------------- 1 | val suite : string * unit Alcotest.test_case list 2 | -------------------------------------------------------------------------------- /test/lib/testable.ml: -------------------------------------------------------------------------------- 1 | let r_msg = 2 | Alcotest.testable Rresult.R.pp_msg (fun (`Msg s) (`Msg s') -> 3 | String.equal s s') 4 | 5 | let sexp = Alcotest.testable Sexplib0.Sexp.pp Sexplib0.Sexp.equal 6 | 7 | let opam_package_name_set = 8 | Alcotest.testable Duniverse_lib.Opam.Pp.Package_name_set.pp 9 | OpamPackage.Name.Set.equal 10 | -------------------------------------------------------------------------------- /test/lib/testable.mli: -------------------------------------------------------------------------------- 1 | val r_msg : Rresult.R.msg Alcotest.testable 2 | val sexp : Sexplib0.Sexp.t Alcotest.testable 3 | val opam_package_name_set : OpamPackage.Name.Set.t Alcotest.testable 4 | --------------------------------------------------------------------------------