From 13acd704b57845d67f189c3a9691e82c5c9dac5c Mon Sep 17 00:00:00 2001 From: Alejandro R Mosteo Date: Tue, 22 Jun 2021 17:55:12 +0200 Subject: [PATCH] Pins in the manifest (#743) * Added data structures * Pins may appear in manifest, being ignored * Loading of user pins complete * Pins are downloaded or skipped as needed * Pins are properly pruned, and info displayed Minor testsuite tweak for a change in logging format * Added new-format pins to alire.toml for self-build * Allow selective update of pins as for regular deps * Local pins work with new manifest syntax * Fixed bug in which version pins were not used * Make version explicit key in user pin * Fix bug about confirming empty updates * Roots: Conflated dep updating into single Sync We had two confusing Update_Dependencies and Update_And_Deploy_Dependencies that were in practice doing almost the same. There is now a single Sync_Dependencies. * Disable tests that rely on `alr pin` For now, these cannot work as we are going to remove the ability to edit pins via `alr with`/`alr pin`. This functionality could be reintroduced at a later time. * More tests temporarily disabled Most of those should be reimplemented in their manual edition alternative * Disable code for pin edition in command-line * Changes to allow pins to non-dependencies This is purely for user comfort and will probably result in dependencies having to be added at publish time. However, if we manage to restore command-line pinning, we can remove that pain by adding missing dependencies at that time. * Tests in tests/pin using new pins Some tests are not easily portable without support from `alr with --use`. Since that should be easy to implement later, they are disabled for now and will be enabled in a subsequent patch. * Documentation on new pins * Fix for testcase on Windows using path separators * Spelling fixes, and exclude lockfiles from check * New test for various invalid pin entries in manifest Also the required code changes to pass the test * Fixes from code self-review * Fixes suggested during code review * Fix for missed update when there is no lockfile We were creating an empty lockfile, which was newer than the manifest, and thus not triggering the expected automatic update. * Test to check pins are applied with no prior lockfile This was a bug detected and corrected in the previous commit --- .github/workflows/spellcheck.yml | 1 + .gitmodules | 3 + alire.gpr | 1 + alire.lock | 14 + alire.toml | 14 + alr_env.gpr | 1 + config/alr_config.ads | 4 + config/alr_config.gpr | 14 + config/alr_config.h | 5 + deps/optional | 1 + dev/edit.sh | 1 + doc/catalog-format-spec.md | 68 ++- doc/user-changes.md | 17 + src/alire/alire-crate_configuration.adb | 9 +- src/alire/alire-crates.adb | 10 + src/alire/alire-dependencies-containers.ads | 6 + src/alire/alire-dependencies-states.ads | 29 ++ src/alire/alire-environment.adb | 12 +- .../alire-externals-softlinks-holders.ads | 4 + src/alire/alire-externals.adb | 12 +- src/alire/alire-milestones-holders.ads | 4 + src/alire/alire-optional.ads | 12 + src/alire/alire-releases.adb | 25 ++ src/alire/alire-releases.ads | 15 + src/alire/alire-roots.adb | 407 ++++++++++++++---- src/alire/alire-roots.ads | 71 ++- src/alire/alire-solutions-diffs.adb | 2 +- src/alire/alire-solutions.adb | 45 +- src/alire/alire-solutions.ads | 101 ++++- src/alire/alire-solver.adb | 26 +- src/alire/alire-solver.ads | 7 +- src/alire/alire-toml_adapters.ads | 14 + src/alire/alire-toml_keys.ads | 1 + src/alire/alire-toml_load.adb | 11 + src/alire/alire-toml_load.ads | 2 + src/alire/alire-user_pins-maps.adb | 42 ++ src/alire/alire-user_pins-maps.ads | 19 + src/alire/alire-user_pins.adb | 92 ++++ src/alire/alire-user_pins.ads | 107 +++++ src/alr/alr-commands-pin.adb | 19 + src/alr/alr-commands-pin.ads | 4 + src/alr/alr-commands-update.adb | 7 +- src/alr/alr-commands-withing.adb | 4 +- src/alr/alr-commands.adb | 9 +- .../indirect-link/my_index/index/index.toml | 0 .../my_index/index/ti/tier1/tier1-1.0.0.toml | 0 .../my_index/index/ti/tier2/tier2-1.0.0.toml | 0 .../my_index/index/ti/tier3/tier3-1.0.0.toml | 0 .../get/indirect-link/test.py | 0 .../get/indirect-link/test.yaml | 0 .../index/he/hello1/hello1-0.1.0.toml | 0 .../index/he/hello2/hello2-0.1.0.toml | 0 .../pin/all/my_index/index/index.toml | 0 testsuite/{tests => disabled}/pin/all/test.py | 0 .../{tests => disabled}/pin/all/test.yaml | 0 .../pin/dir-mismatch/test.py | 0 .../pin/dir-mismatch/test.yaml | 0 .../{tests => disabled}/pin/remote/test.py | 6 +- .../{tests => disabled}/pin/remote/test.yaml | 0 .../my_index/crates/crate_1234/.emptydir | 0 .../my_index/crates/crate_1234/alire.toml | 0 .../crates/crate_1234/alire/.emptydir | 0 .../linked-paths/my_index/index/index.toml | 0 .../printenv/linked-paths/test.py | 0 .../printenv/linked-paths/test.yaml | 0 .../with/changes-info/test.py | 0 .../with/changes-info/test.yaml | 0 .../with/pin-dir-crate-autodetect/test.py | 0 .../with/pin-dir-crate-autodetect/test.yaml | 0 .../with/pin-dir-crate/test.py | 0 .../with/pin-dir-crate/test.yaml | 0 .../with/pin-dir-mismatch/test.py | 0 .../with/pin-dir-mismatch/test.yaml | 0 .../crates/libhello_1.0.0/libhello.gpr | 0 .../crates/libhello_1.0.0/src/libhello.ads | 0 .../with/pin-dir/my_index/index/index.toml | 0 .../index/li/libhello/libhello-1.0.0.toml | 0 .../{tests => disabled}/with/pin-dir/test.py | 0 .../with/pin-dir/test.yaml | 0 .../with/pin-transitive/test.py | 2 +- .../disabled/with/pin-transitive/test.yaml | 4 + .../with/tree-switch/test.py | 0 .../with/tree-switch/test.yaml | 0 .../with/versions-switch/test.py | 0 .../with/versions-switch/test.yaml | 0 testsuite/drivers/alr.py | 77 ++++ testsuite/tests/pin/change-type/test.py | 10 +- testsuite/tests/pin/dir-crate/test.py | 4 +- testsuite/tests/pin/downgrade/test.py | 4 +- .../my_index/crates/crate/.emptydir | 0 .../my_index/index/cr/crate/crate-1.0.0.toml | 9 + .../my_index/index/index.toml | 1 + .../tests/pin/manifest-invalid-pins/test.py | 83 ++++ .../tests/pin/manifest-invalid-pins/test.yaml | 4 + testsuite/tests/pin/missing-version/test.py | 8 +- .../tests/pin/pin-dir-with-regular/test.py | 4 +- testsuite/tests/pin/pin-dir/test.py | 6 +- testsuite/tests/pin/post-update/test.py | 8 +- .../pin/twice-in-manifest/my_index/index.toml | 1 + .../my_index/li/libhello/libhello-1.0.0.toml | 9 + testsuite/tests/pin/twice-in-manifest/test.py | 32 ++ .../tests/pin/twice-in-manifest/test.yaml | 5 + testsuite/tests/pin/unneeded-held/test.py | 4 +- testsuite/tests/pin/unpin/test.py | 13 +- testsuite/tests/pin/without-lockfile/test.py | 31 ++ .../tests/pin/without-lockfile/test.yaml | 4 + .../tests/printenv/with-external/test.py | 7 +- testsuite/tests/update/manual-once/test.py | 4 +- testsuite/tests/update/missing-deps/test.py | 7 +- testsuite/tests/update/pinned/test.py | 8 +- testsuite/tests/with/pin-transitive/test.yaml | 1 - .../tests/workflows/init-with-pin/test.py | 4 +- 112 files changed, 1434 insertions(+), 198 deletions(-) create mode 100644 config/alr_config.ads create mode 100644 config/alr_config.gpr create mode 100644 config/alr_config.h create mode 160000 deps/optional create mode 100755 dev/edit.sh create mode 100644 src/alire/alire-externals-softlinks-holders.ads create mode 100644 src/alire/alire-milestones-holders.ads create mode 100644 src/alire/alire-optional.ads create mode 100644 src/alire/alire-user_pins-maps.adb create mode 100644 src/alire/alire-user_pins-maps.ads create mode 100644 src/alire/alire-user_pins.adb create mode 100644 src/alire/alire-user_pins.ads rename testsuite/{tests => disabled}/get/indirect-link/my_index/index/index.toml (100%) rename testsuite/{tests => disabled}/get/indirect-link/my_index/index/ti/tier1/tier1-1.0.0.toml (100%) rename testsuite/{tests => disabled}/get/indirect-link/my_index/index/ti/tier2/tier2-1.0.0.toml (100%) rename testsuite/{tests => disabled}/get/indirect-link/my_index/index/ti/tier3/tier3-1.0.0.toml (100%) rename testsuite/{tests => disabled}/get/indirect-link/test.py (100%) rename testsuite/{tests => disabled}/get/indirect-link/test.yaml (100%) rename testsuite/{tests => disabled}/pin/all/my_index/index/he/hello1/hello1-0.1.0.toml (100%) rename testsuite/{tests => disabled}/pin/all/my_index/index/he/hello2/hello2-0.1.0.toml (100%) rename testsuite/{tests => disabled}/pin/all/my_index/index/index.toml (100%) rename testsuite/{tests => disabled}/pin/all/test.py (100%) rename testsuite/{tests => disabled}/pin/all/test.yaml (100%) rename testsuite/{tests => disabled}/pin/dir-mismatch/test.py (100%) rename testsuite/{tests => disabled}/pin/dir-mismatch/test.yaml (100%) rename testsuite/{tests => disabled}/pin/remote/test.py (87%) rename testsuite/{tests => disabled}/pin/remote/test.yaml (100%) rename testsuite/{tests => disabled}/printenv/linked-paths/my_index/crates/crate_1234/.emptydir (100%) rename testsuite/{tests => disabled}/printenv/linked-paths/my_index/crates/crate_1234/alire.toml (100%) rename testsuite/{tests => disabled}/printenv/linked-paths/my_index/crates/crate_1234/alire/.emptydir (100%) rename testsuite/{tests => disabled}/printenv/linked-paths/my_index/index/index.toml (100%) rename testsuite/{tests => disabled}/printenv/linked-paths/test.py (100%) rename testsuite/{tests => disabled}/printenv/linked-paths/test.yaml (100%) rename testsuite/{tests => disabled}/with/changes-info/test.py (100%) rename testsuite/{tests => disabled}/with/changes-info/test.yaml (100%) rename testsuite/{tests => disabled}/with/pin-dir-crate-autodetect/test.py (100%) rename testsuite/{tests => disabled}/with/pin-dir-crate-autodetect/test.yaml (100%) rename testsuite/{tests => disabled}/with/pin-dir-crate/test.py (100%) rename testsuite/{tests => disabled}/with/pin-dir-crate/test.yaml (100%) rename testsuite/{tests => disabled}/with/pin-dir-mismatch/test.py (100%) rename testsuite/{tests => disabled}/with/pin-dir-mismatch/test.yaml (100%) rename testsuite/{tests => disabled}/with/pin-dir/my_index/crates/libhello_1.0.0/libhello.gpr (100%) rename testsuite/{tests => disabled}/with/pin-dir/my_index/crates/libhello_1.0.0/src/libhello.ads (100%) rename testsuite/{tests => disabled}/with/pin-dir/my_index/index/index.toml (100%) rename testsuite/{tests => disabled}/with/pin-dir/my_index/index/li/libhello/libhello-1.0.0.toml (100%) rename testsuite/{tests => disabled}/with/pin-dir/test.py (100%) rename testsuite/{tests => disabled}/with/pin-dir/test.yaml (100%) rename testsuite/{tests => disabled}/with/pin-transitive/test.py (94%) create mode 100644 testsuite/disabled/with/pin-transitive/test.yaml rename testsuite/{tests => disabled}/with/tree-switch/test.py (100%) rename testsuite/{tests => disabled}/with/tree-switch/test.yaml (100%) rename testsuite/{tests => disabled}/with/versions-switch/test.py (100%) rename testsuite/{tests => disabled}/with/versions-switch/test.yaml (100%) create mode 100644 testsuite/tests/pin/manifest-invalid-pins/my_index/crates/crate/.emptydir create mode 100644 testsuite/tests/pin/manifest-invalid-pins/my_index/index/cr/crate/crate-1.0.0.toml create mode 100644 testsuite/tests/pin/manifest-invalid-pins/my_index/index/index.toml create mode 100644 testsuite/tests/pin/manifest-invalid-pins/test.py create mode 100644 testsuite/tests/pin/manifest-invalid-pins/test.yaml create mode 100644 testsuite/tests/pin/twice-in-manifest/my_index/index.toml create mode 100644 testsuite/tests/pin/twice-in-manifest/my_index/li/libhello/libhello-1.0.0.toml create mode 100644 testsuite/tests/pin/twice-in-manifest/test.py create mode 100644 testsuite/tests/pin/twice-in-manifest/test.yaml create mode 100644 testsuite/tests/pin/without-lockfile/test.py create mode 100644 testsuite/tests/pin/without-lockfile/test.yaml delete mode 100644 testsuite/tests/with/pin-transitive/test.yaml diff --git a/.github/workflows/spellcheck.yml b/.github/workflows/spellcheck.yml index c5964002..88d010e2 100644 --- a/.github/workflows/spellcheck.yml +++ b/.github/workflows/spellcheck.yml @@ -15,3 +15,4 @@ jobs: github_token: ${{ secrets.github_token }} reporter: github-pr-review locale: "US" + exclude: "*.lock* diff --git a/.gitmodules b/.gitmodules index d5d0af06..df753f7f 100644 --- a/.gitmodules +++ b/.gitmodules @@ -36,3 +36,6 @@ path = deps/spdx url = https://github.com/Fabien-Chouteau/spdx_ada branch = 319ca7bcc1e2eb1843aad1f64aca3ecba91a2bcc +[submodule "deps/optional"] + path = deps/optional + url = https://github.com/mosteo/optional diff --git a/alire.gpr b/alire.gpr index 7cb5bd14..91785fd5 100644 --- a/alire.gpr +++ b/alire.gpr @@ -5,6 +5,7 @@ with "ajunitgen"; with "ansi"; with "gnatcoll"; with "minirest"; +with "optional"; with "semantic_versioning"; with "simple_logging"; with "uri"; diff --git a/alire.lock b/alire.lock index 2c84f1a6..e3203f1f 100644 --- a/alire.lock +++ b/alire.lock @@ -107,6 +107,20 @@ remote = true commit = "4550aa356d55b9cd55f26acd34701f646021c5ff" url = "git+https://github.com/mosteo/minirest.git" [[solution.state]] +crate = "optional" +fulfilment = "linked" +pinned = false +transitivity = "direct" +versions = "~0.0.0" +[solution.state.link] +kind = "softlink" +path = "file:alire/cache/pins/optional_0.1.0-rc1_30aaee65" +relative = true +remote = true +[solution.state.link.origin] +commit = "30aaee65d89d5a9ca1c71f6d38e4462fae2ef4ce" +url = "git+https://github.com/mosteo/optional.git" +[[solution.state]] crate = "semantic_versioning" fulfilment = "linked" pinned = false diff --git a/alire.toml b/alire.toml index 2c4bdfbf..c8677d0e 100644 --- a/alire.toml +++ b/alire.toml @@ -21,6 +21,7 @@ ajunitgen = "^1.0.1" ansiada = "~0.1" gnatcoll = "^21" minirest = "~0.2" +optional = "~0.0.0" semantic_versioning = "^2" simple_logging = "^1.2" uri_ada = "^1" @@ -29,3 +30,16 @@ spdx = "~0.2" # Building alr requires the explicit setting of this variable [gpr-set-externals."case(os)"] macos = { OS = "macOS" } + +# Most dependencies require precise versions during the development cycle: +[[pins]] +aaa = { url = "https://github.com/mosteo/aaa.git", commit = "4b4aa047f29a4270c5b5003468617e153977ab97" } +ada_toml = { url = "https://github.com/pmderodat/ada-toml.git", commit = "ade3cc905cef405dbf53e16a54f6fb458482710f" } +ajunitgen = { url = "https://github.com/mosteo/ajunitgen.git", commit = "e5d01db5e7834d15c4066f0a8e33d780deae3cc9" } +ansiada = { url = "https://github.com/mosteo/ansi-ada.git", commit = "acf9afca3afe1f8b8843c061f3cef860d7567307" } +gnatcoll = { url = "https://github.com/alire-project/gnatcoll-core.git", commit = "f3bd1c51d12962879f52733e790b394f5bbfe05f" } +minirest = { url = "https://github.com/mosteo/minirest.git", commit = "4550aa356d55b9cd55f26acd34701f646021c5ff" } +optional = { url = "https://github.com/mosteo/optional.git", commit = "30aaee65d89d5a9ca1c71f6d38e4462fae2ef4ce" } +semantic_versioning = { url = "https://github.com/alire-project/semantic_versioning.git", commit = "82c28f773d0e3126d7cdf6e4ded228d2b733441e" } +simple_logging = { url = "https://github.com/alire-project/simple_logging.git", commit = "02a7de7568af6af7cedd1048901fae8e9477b1d9" } +uri_ada = { url = "https://github.com/mosteo/uri-ada.git", commit = "b61eba59099b3ab39e59e228fe4529927f9e849e" } diff --git a/alr_env.gpr b/alr_env.gpr index 16c7f08b..64b70b39 100644 --- a/alr_env.gpr +++ b/alr_env.gpr @@ -9,6 +9,7 @@ aggregate project Alr_Env is "deps/ansi", "deps/gnatcoll-slim", "deps/minirest", + "deps/optional", "deps/semantic_versioning", "deps/simple_logging", "deps/uri-ada", diff --git a/config/alr_config.ads b/config/alr_config.ads new file mode 100644 index 00000000..db126edb --- /dev/null +++ b/config/alr_config.ads @@ -0,0 +1,4 @@ +-- Configuration for alr generated by Alire +package alr_Config is + +end alr_Config; diff --git a/config/alr_config.gpr b/config/alr_config.gpr new file mode 100644 index 00000000..802354b4 --- /dev/null +++ b/config/alr_config.gpr @@ -0,0 +1,14 @@ +-- Configuration for alr generated by Alire +with "aaa.gpr"; +with "ajunitgen.gpr"; +with "ansi.gpr"; +with "gnatcoll.gpr"; +with "minirest.gpr"; +with "optional.gpr"; +with "semantic_versioning.gpr"; +with "simple_logging.gpr"; +with "spdx.gpr"; +with "uri.gpr"; +abstract project alr_Config is + +end alr_Config; diff --git a/config/alr_config.h b/config/alr_config.h new file mode 100644 index 00000000..5f67d9ae --- /dev/null +++ b/config/alr_config.h @@ -0,0 +1,5 @@ +/* Configuration for alr generated by Alire */ +#ifndef ALR_CONFIG_H +#define ALR_CONFIG_H + +#endif diff --git a/deps/optional b/deps/optional new file mode 160000 index 00000000..eb929e67 --- /dev/null +++ b/deps/optional @@ -0,0 +1 @@ +Subproject commit eb929e67ccd357881997d4eed5e4477144923d7c diff --git a/dev/edit.sh b/dev/edit.sh new file mode 100755 index 00000000..dcc39a49 --- /dev/null +++ b/dev/edit.sh @@ -0,0 +1 @@ +gnatstudio -P alr_env & diff --git a/doc/catalog-format-spec.md b/doc/catalog-format-spec.md index 8775e95c..24d39c3d 100644 --- a/doc/catalog-format-spec.md +++ b/doc/catalog-format-spec.md @@ -14,7 +14,7 @@ appear, or are optional, in a local manifest). These differences are highlighted in the following descriptions, where necessary. Each TOML description file contains exactly one release, except for the special -external definitions that are described in their own section. +external definitions that are described in their own section. ## Information encoding @@ -247,7 +247,7 @@ static, i.e. they cannot depend on the context. Available constraint operators are the usual Ada relationals (`=`, `/=`, `>`, `>=`, `<`, `<=`) plus caret (`^`, any upwards version within the same major point) - and tilde (\~, any upwards version within the same minor point). + and tilde (\~, any upwards version within the same minor point). **Note that caret and tilde do not have any special behavior for pre-1 versions.** This means, for example, that `^0.2` will still mean any release @@ -283,7 +283,7 @@ static, i.e. they cannot depend on the context. TAG = "" ``` - - `gpr-set-externals`: optional dynamic table, setting values of project + - `gpr-set-externals`: optional dynamic table, setting values of project external variables when building the project. This should not be used to specify default values, the default values must be specified in the `.gpr` project file. Expressions are accepted before the mapping. For instance: @@ -355,7 +355,7 @@ static, i.e. they cannot depend on the context. - `post-build`: the command is to be run right after GPRbuild has been run. This kind of action is run only for the root crate in a workspace. - - `test`: the command is run on demand for crate testing within the Alire + - `test`: the command is run on demand for crate testing within the Alire ecosystem (using `alr test`). This kind of action is fun only for the root crate being tested. @@ -508,6 +508,64 @@ static, i.e. they cannot depend on the context. crate_2.var1 = "Debug" ``` +## Work-in-progress dependency overrides + +It is usual to develop several interdependent crates at the same time. In this scenario, it is often impractical to rely on indexed releases which are not intended to be modified. Instead, one would prefer to use a work-in-progress version of a crate to fulfill some dependency. + +Alire provides *pins* to support this use case. Pins override dependencies, they are intended to be used locally, and to be fulfilled by proper dependencies once a crate is ready to be published. The use of pins is based on two ideas: + +* Dependencies are given, as normally, in the `depends-on` array of the manifest, even for those dependencies to be pinned. This way, once the release is ready, pins are simply removed and the actual dependencies are used in their place. +* Dependency overrides, aka *pins*, are given under the `[[pins]]` array of the manifest. + +Three kinds of pins are available, all of them with the syntax: + +`crate_name = { pin_attributes }` + +The specific pin kinds and their attributes are: + +* Pins to versions: used to force the use of a particular version of an indexed crate. + + * `version`: a string containing a single version to be used. + * `crate_name = { version = "1.2+hotfix-1" }` + +* Pins to local crates: a local directory will fulfill the crate dependency, no matter what version is given in its local manifest. "Raw" Ada projects without an Alire manifest can be used too, as long as their project file matches the crate name and it is located in the directory given as override. + + * `path`: an absolute or relative path to the crate directory. + * `crate_name = { path = "../my/wip/crate" }` + + For the common case of directories containing an Alire manifest, dependencies and pins will be included recursively in the build context. + +* Pins to git repositories: the repository will be cloned locally and its directory will be used as in the previous case. Currently, this pin may optionally include a commit to fix the checkout to be used. Otherwise, the default branch will be used, and running `alr update` will refresh the checkout. + + * `url`: the URL of a git repository + * `commit` (optional): a complete git commit hash. + * `crate_name = { url = "https://my/repo.git" } # Updatable pin` + * `crate_name = { url = "https://my/repo.git", commit="abcdef..." } # Fixed pin` + +### Using pins for crate testing + +Pins are also useful to have a separate test project that depends on your main crate. The recommended setup is as follows: + +``` +/path/to/my_crate +├── alire.toml +└── tests + └── alire.toml +``` + +I.e., a `tests` crate is initialized within the main `my_crate`. In `tests` manifest, you have a dependency and local relative path pin for `my_crate`: + +```toml +# tests/alire.toml +[[depends-on]] +my_crate = "*" # Any version of the main crate +aunit = "*" # We can have dependencies for testing only +[[pins]] +my_crate = { path = ".." } # Overridden by the latest sources +``` + + Then, `my_crate` is published normally, and `tests` can be used locally for any kind of testing needed on `my_crate` without polluting `my_crate` manifest with test specifics (like extra dependencies used by the test setup). + ## External releases The above information applies to regular releases distributed from sources @@ -733,7 +791,7 @@ String variables can be used to define the URL of a website or service: URL_Name = {type = "String", default = "example.com"} ``` -#### PID coefficients +#### PID coefficients Real variables can be used for PID coefficients: ```toml diff --git a/doc/user-changes.md b/doc/user-changes.md index 14245ecc..ef80bac3 100644 --- a/doc/user-changes.md +++ b/doc/user-changes.md @@ -6,6 +6,23 @@ stay on top of `alr` new features. ## Release `1.1` +### Pins stored in the manifest + +PR [#743](https://github.com/alire-project/alire/pull/743). + +The options to modify pins through the command-line (`with --use`, `alr pin +[--unpin] crate` have been disabled in favor of direct edition of the manifest. +This way, pins are more robust against lockfile format changes. These kinds of +pins exist: + +``` +[[pins]] +foo = { version = "1.3.2+bugfix" } # Require a specific version +bar = { path = "../my/bar" } # Use a local crate to override a dependency +baz = { url = "https://github.com/baz.git" } # No commit, will use HEAD, will update on `alr update` +gru = { url = "https://gitlab.com/gru.git" commit="123456890abcdef..." } # Explicit commit, won't update +``` + ### Automatic GPR 'with' now in crate configuration PR [#740](https://github.com/alire-project/alire/pull/740). diff --git a/src/alire/alire-crate_configuration.adb b/src/alire/alire-crate_configuration.adb index bc647423..f2c8aa31 100644 --- a/src/alire/alire-crate_configuration.adb +++ b/src/alire/alire-crate_configuration.adb @@ -8,6 +8,7 @@ with Alire.Solutions; with Alire.Releases; with Alire.Roots; with Alire.Origins; +with Alire.Warnings; with Alire.Directories; @@ -28,8 +29,8 @@ package body Alire.Crate_Configuration is begin if not Solution.Is_Complete then - Trace.Warning ("Generating possibly incomplete configuration" - & " because of missing dependencies"); + Warnings.Warn_Once ("Generating possibly incomplete configuration" + & " because of missing dependencies"); end if; for Rel of Solution.Releases.Including (Root.Release) loop @@ -75,8 +76,8 @@ package body Alire.Crate_Configuration is begin if not Solution.Is_Complete then - Trace.Warning ("Generating possibly incomplete configuration" - & " because of missing dependencies"); + Warnings.Warn_Once ("Generating possibly incomplete configuration" + & " because of missing dependencies"); end if; for Rel of Solution.Releases.Including (Root.Release) loop diff --git a/src/alire/alire-crates.adb b/src/alire/alire-crates.adb index 81a0e2dd..dae448d5 100644 --- a/src/alire/alire-crates.adb +++ b/src/alire/alire-crates.adb @@ -2,6 +2,7 @@ with Alire.Origins; with Alire.Properties.Labeled; with Alire.TOML_Keys; with Alire.TOML_Load; +with Alire.User_Pins.Maps; with Alire.Utils.TTY; with TOML; @@ -144,6 +145,7 @@ package body Alire.Crates is declare Unused_Avail : Conditional.Availability; Unused_Deps : Conditional.Dependencies; + Unused_Pins : User_Pins.Maps.Map; Properties : Conditional.Properties; begin TOML_Load.Load_Crate_Section @@ -152,8 +154,16 @@ package body Alire.Crates is From => From, Props => Properties, Deps => Unused_Deps, + Pins => Unused_Pins, Avail => Unused_Avail); + Assert (Unused_Deps.Is_Empty, + "Unexpected dependencies in external definition"); + Assert (Unused_Pins.Is_Empty, + "Unexpected pins in external definition"); + Assert (Unused_Avail.Is_Empty, + "Unexpected availability in external definition"); + case Policy is when Policies.Merge_Priorizing_Existing => if This.Externals.Properties.Is_Empty then diff --git a/src/alire/alire-dependencies-containers.ads b/src/alire/alire-dependencies-containers.ads index cde3f099..52b8ce99 100644 --- a/src/alire/alire-dependencies-containers.ads +++ b/src/alire/alire-dependencies-containers.ads @@ -1,6 +1,8 @@ with Ada.Containers.Indefinite_Doubly_Linked_Lists; with Ada.Containers.Indefinite_Ordered_Sets; +with Optional.Values; + package Alire.Dependencies.Containers with Preelaborate is package Lists is new @@ -8,6 +10,10 @@ package Alire.Dependencies.Containers with Preelaborate is type List is new Lists.List with null record; + package Optionals is new Optional.Values (Dependency, Image); + + subtype Optional is Optionals.Optional; + package Sets is new Ada.Containers.Indefinite_Ordered_Sets (Dependency, Lexicographical_Sort); diff --git a/src/alire/alire-dependencies-states.ads b/src/alire/alire-dependencies-states.ads index 5d8e39fa..3c6ea412 100644 --- a/src/alire/alire-dependencies-states.ads +++ b/src/alire/alire-dependencies-states.ads @@ -22,6 +22,12 @@ package Alire.Dependencies.States is type State (<>) is new Dependency with private; + overriding function "=" (L, R : State) return Boolean; + -- For some unclear reason, the default implementation reports differences + -- for identical states. Suspecting the Indefinite_Holders therein to be + -- the culprits. We override to rely on the same information the user sees, + -- thus avoiding any inconsistent "want to confirm?" empty updates. + ------------------ -- Constructors -- ------------------ @@ -61,6 +67,9 @@ package Alire.Dependencies.States is with Pre => Base.Crate = Using.Name; -- Uses release to fulfill this dependency in a copy of Base + function Unlinking (Base : State) return State; + -- Unlinks the crate in a copy of Base, becoming Missed + function Unpinning (Base : State) return State; -- Removes the pin in a copy of Base @@ -202,6 +211,15 @@ private Transitivity : Transitivities := Unknown; end record; + --------- + -- "=" -- + --------- + + overriding function "=" (L, R : State) return Boolean + is (L.Image = R.Image); + -- TODO: this is likely not efficient. We should dig more to find why some + -- apparently identical states are reported as different. + ------------------- -- As_Dependency -- ------------------- @@ -477,6 +495,17 @@ private else "") & ")"); + --------------- + -- Unlinking -- + --------------- + + function Unlinking (Base : State) return State + is (Base.As_Dependency with + Name_Len => Base.Name_Len, + Fulfilled => (Fulfillment => Missed), + Pinning => Base.Pinning, + Transitivity => Base.Transitivity); + --------------- -- Unpinning -- --------------- diff --git a/src/alire/alire-environment.adb b/src/alire/alire-environment.adb index b450869a..00d2c83e 100644 --- a/src/alire/alire-environment.adb +++ b/src/alire/alire-environment.adb @@ -83,6 +83,8 @@ package body Alire.Environment is -- Load -- ---------- + Already_Warned : Boolean := False; + procedure Load (This : in out Context; Root : in out Alire.Roots.Root) is @@ -95,17 +97,21 @@ package body Alire.Environment is -- Warnings when setting up an incomplete environment if not Solution.Is_Complete then - Trace.Debug ("Generating incomplete environment" + Trace.Debug ("Generating possibly incomplete environment" & " because of missing dependencies"); -- Normally we would generate a warning, but since that will pollute -- the output making it unusable, for once we write directly to -- stderr (unless quiet is in effect): - if not Alire_Early_Elaboration.Switch_Q then + if not Alire_Early_Elaboration.Switch_Q and then not Already_Warned + then + Already_Warned := True; + GNAT.IO.Put_Line (GNAT.IO.Standard_Error, - TTY.Warn ("warn:") & " Generating incomplete environment" + TTY.Warn ("warn:") + & " Generating possibly incomplete environment" & " because of missing dependencies"); end if; end if; diff --git a/src/alire/alire-externals-softlinks-holders.ads b/src/alire/alire-externals-softlinks-holders.ads new file mode 100644 index 00000000..adb75baa --- /dev/null +++ b/src/alire/alire-externals-softlinks-holders.ads @@ -0,0 +1,4 @@ +with Ada.Containers.Indefinite_Holders; + +package Alire.Externals.Softlinks.Holders is + new Ada.Containers.Indefinite_Holders (Softlinks.External); diff --git a/src/alire/alire-externals.adb b/src/alire/alire-externals.adb index 6ea3746d..10d0309a 100644 --- a/src/alire/alire-externals.adb +++ b/src/alire/alire-externals.adb @@ -7,6 +7,7 @@ with Alire.Externals.Softlinks; with Alire.Externals.Unindexed; with Alire.TOML_Keys; with Alire.TOML_Load; +with Alire.User_Pins.Maps; with TOML; @@ -61,7 +62,8 @@ package body Alire.Externals is end if; end Validate; - Deps : Conditional.Dependencies; + Unused_Deps : Conditional.Dependencies; + Unused_Pins : User_Pins.Maps.Map; begin @@ -82,9 +84,15 @@ package body Alire.Externals is Section => Crates.External_Private_Section, From => From, Props => Ext.Properties, - Deps => Deps, + Deps => Unused_Deps, + Pins => Unused_Pins, Avail => Ext.Available); + Assert (Unused_Deps.Is_Empty, + "Unexpected dependencies in external definition"); + Assert (Unused_Pins.Is_Empty, + "Unexpected pins in external definition"); + From.Report_Extra_Keys; -- Table must be exhausted at this point end return; diff --git a/src/alire/alire-milestones-holders.ads b/src/alire/alire-milestones-holders.ads new file mode 100644 index 00000000..331f11b3 --- /dev/null +++ b/src/alire/alire-milestones-holders.ads @@ -0,0 +1,4 @@ +with Ada.Containers.Indefinite_Holders; + +package Alire.Milestones.Holders is + new Ada.Containers.Indefinite_Holders (Milestone); diff --git a/src/alire/alire-optional.ads b/src/alire/alire-optional.ads new file mode 100644 index 00000000..9120113e --- /dev/null +++ b/src/alire/alire-optional.ads @@ -0,0 +1,12 @@ +with Optional.Values; + +package Alire.Optional with Preelaborate is + + -- Optional basic types + + function String_Image (Str : Standard.String) + return Standard.String is (Str); + package Strings is new Standard.Optional.Values (String, String_Image); + subtype String is Strings.Optional; + +end Alire.Optional; diff --git a/src/alire/alire-releases.adb b/src/alire/alire-releases.adb index ca9cf654..ce2f4818 100644 --- a/src/alire/alire-releases.adb +++ b/src/alire/alire-releases.adb @@ -91,6 +91,26 @@ package body Alire.Releases is return False; end Check_Caret_Warning; + ------------------- + -- Dependency_On -- + ------------------- + + function Dependency_On (R : Release; + Crate : Crate_Name; + P : Alire.Properties.Vector := + Alire.Properties.No_Properties) + return Alire.Dependencies.Containers.Optional + is + begin + for Dep of R.Flat_Dependencies (P) loop + if Dep.Crate = Crate then + return Alire.Dependencies.Containers.Optionals.Unit (Dep); + end if; + end loop; + + return Alire.Dependencies.Containers.Optionals.Empty; + end Dependency_On; + ------------ -- Deploy -- ------------ @@ -234,6 +254,7 @@ package body Alire.Releases is Version => Base.Version, Origin => Base.Origin, Dependencies => Base.Dependencies, + Pins => Base.Pins, Forbidden => Base.Forbidden, Properties => Base.Properties, Available => Base.Available) @@ -288,6 +309,7 @@ package body Alire.Releases is Origin => Origin, Notes => Notes, Dependencies => Dependencies, + Pins => <>, Forbidden => Conditional.For_Dependencies.Empty, Properties => Properties, Available => Available); @@ -320,6 +342,7 @@ package body Alire.Releases is Origin => Origin, Notes => "", Dependencies => Dependencies, + Pins => <>, Forbidden => Conditional.For_Dependencies.Empty, Properties => Properties, Available => Conditional.Empty @@ -728,6 +751,7 @@ package body Alire.Releases is From => From, Props => This.Properties, Deps => This.Dependencies, + Pins => This.Pins, Avail => This.Available); -- Consolidate/validate some properties as fields: @@ -907,6 +931,7 @@ package body Alire.Releases is Origin => R.Origin, Notes => R.Notes, Dependencies => R.Dependencies.Evaluate (P), + Pins => R.Pins, Forbidden => R.Forbidden.Evaluate (P), Properties => R.Properties.Evaluate (P), Available => R.Available.Evaluate (P)); diff --git a/src/alire/alire-releases.ads b/src/alire/alire-releases.ads index 5ce47359..5a4bd299 100644 --- a/src/alire/alire-releases.ads +++ b/src/alire/alire-releases.ads @@ -13,6 +13,7 @@ with Alire.Properties.Labeled; with Alire.Properties.Licenses; with Alire.TOML_Adapters; with Alire.TOML_Keys; +with Alire.User_Pins.Maps; with Alire.Utils; with Semantic_Versioning; @@ -144,6 +145,14 @@ package Alire.Releases is return Conditional.Dependencies; -- Retrieve only the dependencies that apply on platform P + function Dependency_On (R : Release; + Crate : Crate_Name; + P : Alire.Properties.Vector := + Alire.Properties.No_Properties) + return Alire.Dependencies.Containers.Optional; + -- If R.Flat_Dependencies contains Crate, that dependency will be returned, + -- Empty otherwise. + function Flat_Dependencies (R : Release; P : Alire.Properties.Vector := Alire.Properties.No_Properties) @@ -185,6 +194,8 @@ package Alire.Releases is -- Only explicitly declared ones -- Under some conditions (usually current platform) + function Pins (R : Release) return User_Pins.Maps.Map; + function Project_Paths (R : Release; P : Alire.Properties.Vector) return Utils.String_Set; @@ -336,6 +347,7 @@ private Origin : Origins.Origin; Notes : Description_String (1 .. Notes_Len); Dependencies : Conditional.Dependencies; + Pins : User_Pins.Maps.Map; Forbidden : Conditional.Dependencies; Properties : Conditional.Properties; Available : Conditional.Availability; @@ -473,4 +485,7 @@ private function Version_Image (R : Release) return String is (Semantic_Versioning.Image (R.Version)); + function Pins (R : Release) return User_Pins.Maps.Map + is (R.Pins); + end Alire.Releases; diff --git a/src/alire/alire-roots.adb b/src/alire/alire-roots.adb index 34b3ca51..56d16c5c 100644 --- a/src/alire/alire-roots.adb +++ b/src/alire/alire-roots.adb @@ -6,11 +6,14 @@ with Alire.Directories; with Alire.Environment; with Alire.Externals.Softlinks; with Alire.Manifest; +with Alire.Optional; with Alire.Origins.Deployers; with Alire.OS_Lib; with Alire.Roots.Optional; with Alire.Solutions.Diffs; +with Alire.User_Pins.Maps; with Alire.Utils.TTY; +with Alire.Utils.User_Input; with Alire.VCSs.Git; with GNAT.OS_Lib; @@ -288,6 +291,186 @@ package body Alire.Roots is end Deploy_Dependencies; + ---------------------- + -- Apply_Local_Pins -- + ---------------------- + + procedure Apply_Local_Pins (This : in out Root) is + use type Solutions.Solution; + Sol : Solutions.Solution := This.Solution; + begin + for I in Release (This).Pins.Iterate loop + declare + use all type User_Pins.Kinds; + use User_Pins.Maps.Pin_Maps; + Crate : constant Crate_Name := Key (I); + Pin : constant User_Pins.Pin := Element (I); + begin + + -- A pin for a non-dependency requires that we add a generic + -- dependency to the solution first. + + if not Sol.Depends_On (Crate) then + Sol := Sol.Depending_On + (Dependencies.New_Dependency (Crate, Semver.Extended.Any)); + end if; + + case Pin.Kind is + when To_Version => + Sol := Sol.Resetting (Crate).Pinning (Crate, Pin.Version); + when To_Path => + Sol := Sol.Resetting (Crate).Linking (Crate, Pin.Path); + when To_Git => + null; -- Not considered here + end case; + end; + end loop; + + if Sol /= This.Solution then + Solutions.Diffs.Between (This.Solution, Sol).Print + (Changed_Only => True, + Level => Trace.Detail); + Trace.Detail ("Local pins updated and committed to lockfile"); + This.Set (Solution => Sol); + end if; + end Apply_Local_Pins; + + ----------------- + -- Deploy_Pins -- + ----------------- + + procedure Deploy_Pins (This : in out Root; + Exhaustive : Boolean; + Allowed : Containers.Crate_Name_Sets.Set := + Containers.Crate_Name_Sets.Empty_Set) is + use User_Pins.Maps.Pin_Maps; + Rel : constant Alire.Releases.Release := Release (This); + Pins : constant User_Pins.Maps.Map := Rel.Pins; + + -------------------- + -- Needs_Updating -- + -------------------- + + function Needs_Updating (Crate : Crate_Name; + Pin : User_Pins.Pin) return Boolean + is + use type Alire.Optional.String; + begin + + -- Early reject if the crate is not among the allowed ones + + if not Allowed.Is_Empty and then not Allowed.Contains (Crate) then + return False; + end if; + + -- Regular checks if the crate is in the update set + + return + -- Any new pin needs downloading + not This.Solution.Links.Contains (Crate) + + -- Manual update requested for pins without a precise commit + or else (Exhaustive and then not Pin.Commit.Has_Element) + + -- Auto update for pins which weren't remote and now are + or else not This.Solution.State (Crate).Link.Is_Remote + + -- Auto update for pins whose commit has changed in manifest wrt + -- lockfile. + or else (Pin.Commit.Has_Element and then + Pin.Commit /= This.Solution.State (Crate) + .Link.Remote.Commit); + end Needs_Updating; + + begin + if (for some Pin of Pins => Pin.Is_Remote) then + Put_Info ("Checking remote pins..."); + end if; + + for I in Pins.Iterate loop + if Pins (I).Is_Remote then + if Needs_Updating (Key (I), Pins (I)) then + + Put_Info ("Deploying pin for crate: " & TTY.Name (Key (I))); + + declare + use type Solutions.Solution; + Crate : constant Crate_Name := Key (I); + Pin : constant User_Pins.Pin := Element (I); + Result : constant Remote_Pin_Result := + This.Pinned_To_Remote + (Dependency => + Conditional.New_Dependency + (Rel.Dependency_On (Crate) + .Or_Else (Dependencies.From_String + ((+Crate) & "*"))), + URL => Pin.URL, + Commit => Pin.Commit.Or_Else (""), + Must_Depend => True); + begin + -- Pin deployed, solution can be stored accordingly + if This.Solution /= Result.Solution then + Solutions + .Diffs.Between (This.Solution, Result.Solution) + .Print (Changed_Only => True, + Level => Trace.Detail); + This.Set (Solution => Result.Solution); + Trace.Detail ("Remote pins committed to disk"); + end if; + end; + else + + Trace.Detail ("Skipping pre-existing pin for crate: " + & TTY.Name (Key (I))); + + end if; + end if; + end loop; + end Deploy_Pins; + + ---------------- + -- Prune_Pins -- + ---------------- + + procedure Prune_Pins (This : in out Root) is + use type Solutions.Solution; + Valid_Pins : constant User_Pins.Maps.Map := Release (This).Pins; + Pruned_Sol : Solutions.Solution := This.Solution; + begin + for State of This.Solution.All_Dependencies loop + if State.Is_User_Pinned and then + not Valid_Pins.Contains (State.Crate) + then + Pruned_Sol := Pruned_Sol.User_Unpinning (State.Crate); + Put_Info ("Unpinning crate " & TTY.Name (State.Crate)); + end if; + end loop; + + if Pruned_Sol /= This.Solution then + Solutions.Diffs.Between (This.Solution, Pruned_Sol).Print + (Changed_Only => True, + Level => Trace.Detail); + Trace.Detail ("Pin-pruned solution committed to disk"); + This.Set (Pruned_Sol); + end if; + end Prune_Pins; + + ----------------------------- + -- Sync_Pins_From_Manifest -- + ----------------------------- + + procedure Sync_Pins_From_Manifest + (This : in out Root; + Exhaustive : Boolean; + Allowed : Containers.Crate_Name_Sets.Set := + Containers.Crate_Name_Sets.Empty_Set) + is + begin + This.Deploy_Pins (Exhaustive, Allowed); + This.Apply_Local_Pins; + This.Prune_Pins; + end Sync_Pins_From_Manifest; + --------------- -- Is_Stored -- --------------- @@ -559,15 +742,26 @@ package body Alire.Roots is File_Time_Stamp (This.Crate_File) > File_Time_Stamp (This.Lock_File); end Is_Lockfile_Outdated; - ---------------------------- - -- Sync_Solution_And_Deps -- - ---------------------------- + ------------------------ + -- Sync_From_Manifest -- + ------------------------ - procedure Sync_Solution_And_Deps (This : in out Root) is + procedure Sync_From_Manifest (This : in out Root; + Silent : Boolean; + Force : Boolean := False) is + Old_Solution : constant Solutions.Solution := This.Solution; begin - if This.Is_Lockfile_Outdated then - Trace.Info ("Detected changes in manifest, updating workspace..."); - This.Update_And_Deploy_Dependencies (Confirm => False); + if Force or else This.Is_Lockfile_Outdated then + Put_Info ("Detected changes in manifest, synchronizing workspace..."); + + This.Sync_Pins_From_Manifest (Exhaustive => False); + -- Normally we do not want to re-fetch remote pins, so we request + -- a non-exhaustive sync of pins, that will anyway detect evident + -- changes (new/removed pins, changed explicit commits). + + This.Sync_Dependencies (Old => Old_Solution, + Silent => Silent); + -- Don't ask for confirmation as this is an automatic update in -- reaction to a manually edited manifest, and we need the lockfile -- to match the manifest. As any change in dependencies will be @@ -580,8 +774,9 @@ package body Alire.Roots is -- to manually mark the lockfile as older. Trace.Info (""); -- Separate changes from what caused the sync + end if; - elsif (for some Rel of This.Solution.Releases => + if (for some Rel of This.Solution.Releases => This.Solution.State (Rel.Name).Is_Solved and then not GNAT.OS_Lib.Is_Directory (This.Release_Base (Rel.Name))) or else @@ -594,7 +789,7 @@ package body Alire.Roots is This.Deploy_Dependencies; end if; - end Sync_Solution_And_Deps; + end Sync_From_Manifest; ------------------------------------------- -- Sync_Manifest_And_Lockfile_Timestamps -- @@ -611,6 +806,28 @@ package body Alire.Roots is end if; end Sync_Manifest_And_Lockfile_Timestamps; + ------------ + -- Update -- + ------------ + + procedure Update (This : in out Root; + Allowed : Containers.Crate_Name_Sets.Set) + is + Old : constant Solutions.Solution := This.Solution; + begin + This.Sync_Pins_From_Manifest (Exhaustive => True, + Allowed => Allowed); + -- Just in case, retry all pins. This is necessary so pins without an + -- explicit commit are updated to HEAD. + + -- And look for updates in dependencies + + This.Sync_Dependencies + (Allowed => Allowed, + Old => Old, + Silent => Alire.Utils.User_Input.Not_Interactive); + end Update; + -------------------- -- Compute_Update -- -------------------- @@ -649,80 +866,98 @@ package body Alire.Roots is Options => Options); end Compute_Update; - ------------------------- - -- Update_Dependencies -- - ------------------------- + ----------------------- + -- Sync_Dependencies -- + ----------------------- - procedure Update_Dependencies + procedure Sync_Dependencies (This : in out Root; Silent : Boolean; + Old : Solutions.Solution := Solutions.Empty_Invalid_Solution; Options : Solver.Query_Options := Solver.Default_Options; Allowed : Containers.Crate_Name_Sets.Set := Alire.Containers.Crate_Name_Sets.Empty_Set) is - Old : constant Solutions.Solution := This.Solution; begin + declare + -- Shadow the argument with the one we want to use everywhere. Note + -- that this old is only used for comparison, as the stored solution + -- may already include changes caused by pin preparations, and + -- furthermore the stored root is the one we need to pass to the + -- solver (as it contains the pins). + Old : constant Solutions.Solution := + (if Sync_Dependencies.Old.Is_Attempted + then Sync_Dependencies.Old + else This.Solution); - -- Ensure requested crates are in solution first. - - for Crate of Allowed loop - if not Old.Depends_On (Crate) then - Raise_Checked_Error ("Requested crate is not a dependency: " - & TTY.Name (Crate)); - end if; + begin - if Old.Pins.Contains (Crate) then - -- The solver will never update a pinned crate, so we may allow - -- this to be attempted but it will have no effect. - Recoverable_Error - ("Requested crate is pinned and cannot be updated: " - & Alire.Utils.TTY.Name (Crate)); - end if; - end loop; + -- Ensure requested crates are in solution first. - declare - Needed : constant Solutions.Solution := This.Compute_Update - (Allowed, Options); - Diff : constant Solutions.Diffs.Diff := Old.Changes (Needed); - begin - -- Early exit when there are no changes + for Crate of Allowed loop + if not Old.Depends_On (Crate) then + Raise_Checked_Error ("Requested crate is not a dependency: " + & TTY.Name (Crate)); + end if; - if not Alire.Force and not Diff.Contains_Changes then - if not Needed.Is_Complete then - Trace.Warning - ("There are missing dependencies" - & " (use `alr with --solve` for details)."); + if Old.Pins.Contains (Crate) then + -- The solver will never update a pinned crate, so we may allow + -- this to be attempted but it will have no effect. + Recoverable_Error + ("Requested crate is pinned and cannot be updated: " + & Alire.Utils.TTY.Name (Crate)); end if; + end loop; - This.Sync_Manifest_And_Lockfile_Timestamps; - -- Just in case manual changes in manifest don't modify solution + declare + Needed : constant Solutions.Solution := This.Compute_Update + (Allowed, Options); + Diff : constant Solutions.Diffs.Diff := Old.Changes (Needed); + begin + -- Early exit when there are no changes - Trace.Info ("Nothing to update."); + if not Alire.Force and not Diff.Contains_Changes then + if not Needed.Is_Complete then + Trace.Warning + ("There are missing dependencies" + & " (use `alr with --solve` for details)."); + end if; - return; - end if; + This.Sync_Manifest_And_Lockfile_Timestamps; + -- In case manual changes in manifest do not modify the + -- solution. - -- Show changes and optionally ask user to apply them + Trace.Info ("Nothing to update."); - if Silent then - Trace.Info ("Dependencies automatically updated as follows:"); - Diff.Print; - elsif not Utils.User_Input.Confirm_Solution_Changes (Diff) then - Trace.Detail ("Update abandoned."); - return; - end if; + else - -- Apply the update + -- Show changes and optionally ask user to apply them - This.Set (Solution => Needed); - This.Deploy_Dependencies; + if Silent then + Trace.Info + ("Dependencies automatically updated as follows:"); + Diff.Print; + elsif not Utils.User_Input.Confirm_Solution_Changes (Diff) then + Trace.Detail ("Update abandoned."); + return; + end if; + + end if; + + -- Apply the update. We do this even when no changes were + -- detected, as pin evaluation may have temporarily stored + -- unsolved dependencies which have been re-solved now. + + This.Set (Solution => Needed); + This.Deploy_Dependencies; - -- Update/Create configuration files - This.Generate_Configuration; + -- Update/Create configuration files + This.Generate_Configuration; - Trace.Detail ("Update completed"); + Trace.Detail ("Update completed"); + end; end; - end Update_Dependencies; + end Sync_Dependencies; ---------------------- -- Pinned_To_Remote -- @@ -748,7 +983,7 @@ package body Alire.Roots is Dep.Crate.As_String = Requested_Crate) then Raise_Checked_Error - ("Cannot continue because the requested crate is not a dependency: " + ("Cannot continue because the requested pin is not a dependency: " & Requested_Crate); end if; @@ -784,7 +1019,22 @@ package body Alire.Roots is Origins.Deployers.New_Deployer (Origins.New_Git (URL, Commit)); begin - Depl.Deploy (Temp.Filename).Assert; + + -- Skip checkout if link is already in the solution and with the same + -- commit. + + if Requested_Crate /= "" and then + This.Solution.Depends_On (+Requested_Crate) and then + This.Solution.Links.Contains (+Requested_Crate) and then + This.Solution.State (+Requested_Crate).Link.Remote.Commit = Commit + then + Trace.Debug ("Skipping checkout of remote link " + & TTY.Name (Requested_Crate) + & "#" + & TTY.URL (Commit)); + else + Depl.Deploy (Temp.Filename).Assert; + end if; -- Identify containing release, and if satisfying move it to its -- final location in the release cache. @@ -881,39 +1131,6 @@ package body Alire.Roots is end; end Pinned_To_Remote; - ------------------------------------ - -- Update_And_Deploy_Dependencies -- - ------------------------------------ - - procedure Update_And_Deploy_Dependencies - (This : in out Roots.Root; - Options : Solver.Query_Options := Solver.Default_Options; - Confirm : Boolean := not Utils.User_Input.Not_Interactive) - is - Prev : constant Solutions.Solution := This.Solution; - Next : constant Solutions.Solution := - This.Compute_Update (Options => Options); - Diff : constant Solutions.Diffs.Diff := Prev.Changes (Next); - begin - if Diff.Contains_Changes then - if not Confirm or else - Utils.User_Input.Confirm_Solution_Changes (Diff) - then - if not Confirm then - Trace.Info ("Changes to dependency solution:"); - Diff.Print (Changed_Only => not Alire.Detailed); - end if; - - This.Set (Solution => Next); - This.Deploy_Dependencies; - end if; - end if; - - -- Update/Create configuration files - This.Generate_Configuration; - - end Update_And_Deploy_Dependencies; - -------------------- -- Write_Manifest -- -------------------- diff --git a/src/alire/alire-roots.ads b/src/alire/alire-roots.ads index bdc43ef6..4853f682 100644 --- a/src/alire/alire-roots.ads +++ b/src/alire/alire-roots.ads @@ -9,7 +9,7 @@ with Alire.Properties; with Alire.Releases; with Alire.Solutions; with Alire.Solver; -with Alire.Utils.User_Input; +with Alire.Utils; package Alire.Roots is @@ -122,12 +122,17 @@ package Alire.Roots is -- conceivably we could use checksums to make it more robust against -- automated changes within the same second. - procedure Sync_Solution_And_Deps (This : in out Root); - -- Ensure that dependencies are up to date in regard to the lockfile and - -- manifest: if the manifest is newer than the lockfile, resolve again, - -- as dependencies may have been edited by hand. Otherwise, ensure that - -- releases in the lockfile are actually on disk (may be missing if cache - -- was deleted, or the crate was just cloned). + procedure Sync_From_Manifest (This : in out Root; + Silent : Boolean; + Force : Boolean := False); + -- If the lockfile timestamp is outdated w.r.t the manifest, or Force, do + -- as follows: 1) Pre-deploy any remote pins in the manifest so they are + -- usable when solving, and apply any local/version pins. 2) Ensure that + -- dependencies are up to date in regard to the lockfile and manifest: if + -- the manifest is newer than the lockfile, resolve again, as dependencies + -- may have been edited by hand. 3) Ensure that releases in the lockfile + -- are actually on disk (may be missing if cache was deleted, or the crate + -- was just cloned). When Silent, run as in non-interactive mode. procedure Sync_Manifest_And_Lockfile_Timestamps (This : Root) with Post => not This.Is_Lockfile_Outdated; @@ -147,23 +152,40 @@ package Alire.Roots is -- This function loads configured indexes from disk. No changes are -- applied to This root. + procedure Update (This : in out Root; + Allowed : Containers.Crate_Name_Sets.Set); + -- Full update, explicitly requested. Will fetch/prune pins, update any + -- updatable crates. Equivalent to `alr update`. Allowed is an optionally + -- empty set of crates to which the update will be limited. Everything is + -- updatable if Allowed.Is_Empty. + procedure Deploy_Dependencies (This : in out Root); -- Download all dependencies not already on disk from This.Solution - procedure Update_Dependencies + procedure Sync_Dependencies (This : in out Root; Silent : Boolean; + Old : Solutions.Solution := Solutions.Empty_Invalid_Solution; Options : Solver.Query_Options := Solver.Default_Options; Allowed : Containers.Crate_Name_Sets.Set := Alire.Containers.Crate_Name_Sets.Empty_Set); - -- Resolve and update all or given crates in a root. When silent, run - -- as in non-interactive mode as this is an automatically-triggered update. - - procedure Update_And_Deploy_Dependencies - (This : in out Roots.Root; - Options : Solver.Query_Options := Solver.Default_Options; - Confirm : Boolean := not Utils.User_Input.Not_Interactive); - -- Call Update and Deploy_Dependencies in succession for the given root + -- Resolve and update all or given crates in a root, and regenerate + -- configuration. When Silent, run as in non-interactive mode as this is an + -- automatically-triggered update. Old_Sol is used to present differences, + -- and when left at the default invalid argument value, This.Solution will + -- be used as old solution. + + procedure Sync_Pins_From_Manifest + (This : in out Root; + Exhaustive : Boolean; + Allowed : Containers.Crate_Name_Sets.Set := + Containers.Crate_Name_Sets.Empty_Set); + -- Checks additions/removals of pins, and fetches remote pins. When not + -- Exhaustive, a pin that is already in the solution is not re-downloaded. + -- This is to avoid re-fetching all pins after each manifest edition. + -- New pins, or pins with a changed commit are always downloaded. An update + -- requested by the user (`alr update`) will be exhaustive. Allowed + -- restricts which crates are affected. procedure Write_Manifest (This : Root); -- Generates the crate.toml manifest at the appropriate location for Root @@ -229,4 +251,21 @@ private Cached_Solution : Cached_Solutions.Cache; end record; + procedure Apply_Local_Pins (This : in out Root); + -- Apply version/path pins from the manifest. Remote pins are dealt with by + -- Deploy_Pins, as they are costlier and have more involved processing. + + procedure Deploy_Pins (This : in out Root; + Exhaustive : Boolean; + Allowed : Containers.Crate_Name_Sets.Set := + Containers.Crate_Name_Sets.Empty_Set); + -- Download any remote pins in the manifest. When not Exhaustive, a pin + -- that is already in the solution is not re-downloaded. This is to avoid + -- re-fetching all pins after each manifest edition. New pins are always + -- downloaded. An update requested by the user (`alr update`) will be + -- exhaustive. Allowed restricts which crates are affected + + procedure Prune_Pins (This : in out Root); + -- Remove any pins in the solution that are not in the manifest + end Alire.Roots; diff --git a/src/alire/alire-solutions-diffs.adb b/src/alire/alire-solutions-diffs.adb index 6e4fc1b6..d986ea41 100644 --- a/src/alire/alire-solutions-diffs.adb +++ b/src/alire/alire-solutions-diffs.adb @@ -383,7 +383,7 @@ package body Alire.Solutions.Diffs is if Changed then Table.Print (Level); else - Trace.Log (Prefix & "No changes between former an new solution.", + Trace.Log (Prefix & "No changes between former and new solution.", Level); end if; end Print; diff --git a/src/alire/alire-solutions.adb b/src/alire/alire-solutions.adb index 841027ee..c49c70e9 100644 --- a/src/alire/alire-solutions.adb +++ b/src/alire/alire-solutions.adb @@ -380,7 +380,43 @@ package body Alire.Solutions is if Dep.Is_Pinned then Dependencies := Dependencies and - Conditional.New_Dependency (Dep.Crate, Dep.Versions); + Conditional.New_Dependency (Dep.Crate, + Dep.Pin_Version); + end if; + end loop; + end return; + end Pins; + + --------------- + -- User_Pins -- + --------------- + + function User_Pins (This : Solution) return Conditional.Dependencies is + begin + return Dependencies : Conditional.Dependencies := This.Pins do + for Dep of This.Dependencies loop + if Dep.Is_Linked then + Dependencies + .Append (Conditional.New_Dependency (Dep.As_Dependency)); + end if; + end loop; + end return; + end User_Pins; + + ---------- + -- Pins -- + ---------- + + function Pins (This : Solution) return Dependency_Map + is + begin + return Result : Dependency_Map do + for State of This.Dependencies loop + if State.Is_Pinned then + Result.Insert (State.Crate, + Alire.Dependencies.New_Dependency + (State.Crate, + State.Pin_Version)); end if; end loop; end return; @@ -806,7 +842,12 @@ package body Alire.Solutions is return Result : Solution := This do for Dep of Src.Dependencies loop if Dep.Is_Pinned then - Result.Dependencies.Include (Dep.Crate, Dep); + + -- We need to copy the pin version; the solving status might + -- have changed, so we do not just blindly copy the old pin + -- into the new solution. + + Result := Result.Pinning (Dep.Crate, Dep.Pin_Version); end if; end loop; end return; diff --git a/src/alire/alire-solutions.ads b/src/alire/alire-solutions.ads index c6399f26..91d5bba6 100644 --- a/src/alire/alire-solutions.ads +++ b/src/alire/alire-solutions.ads @@ -110,6 +110,12 @@ package Alire.Solutions is -- release is fulfilling, by default we don't create its dependency (it -- must exist previously). + function Resetting (This : Solution; + Crate : Crate_Name) + return Solution; + -- Equivalent to .Missing (Crate).User_Unpinning (Crate). That is, remove + -- any fulfillment and any pinning. + function Linking (This : Solution; Crate : Crate_Name; Link : Externals.Softlinks.External) @@ -147,12 +153,31 @@ package Alire.Solutions is return Solution; -- Change transitivity + function Unlinking (This : Solution; + Crate : Crate_Name) + return Solution; + -- Unpin a crate. If the crate was not linked or not in the solution + -- nothing will be done. If it was, it is now missing. + function Unpinning (This : Solution; Crate : Crate_Name) return Solution; -- Unpin a crate. If the crate was not pinned or not in the solution -- nothing will be done. + function User_Unpinning (This : Solution; + Crate : Crate_Name) + return Solution; + -- Remove either a pin or a link for a crate; e.g. same as calling + -- Unpinning and Unlinking in succession. Nothing will be done if + -- crate wasn't in the solution. + + function Unsolving (This : Solution; + Crate : Crate_Name) + return Solution; + -- Remove links, pins, releases... and mark the crate as missing. If not in + -- the solution, nothing will be done. + function With_Pins (This, Src : Solution) return Solution; -- Copy pins from Src to This and return it @@ -173,6 +198,9 @@ package Alire.Solutions is -- Dependency name closure, independent of the status in the solution, as -- found by the solver starting from the direct dependencies. + function All_Dependencies (This : Solution) return State_Map; + -- Get all states in the solution to e.g. iterate over + function Dependencies_That (This : Solution; Check : not null access function (Dep : Dependency_State) return Boolean) @@ -233,11 +261,18 @@ package Alire.Solutions is -- Return crates for which there is neither hint nor proper versions function Pins (This : Solution) return Conditional.Dependencies; - -- Return all pinned dependencies as a dependency tree containing exact - -- versions. + -- Return all version-pinned dependencies as a dependency tree containing + -- exact versions. NOTE that the original dependency is thus lost in this + -- info. function Pins (This : Solution) return Dependency_Map; - -- return all pinned dependencies as plain dependencies for a exact version + -- return all version-pinned dependencies as plain dependencies for a exact + -- version. NOTE that the original dependency is thus lost. + + function User_Pins (This : Solution) return Conditional.Dependencies; + -- Return all version- or link-pinned dependencies; equivalent to Pins and + -- Links. NOTE that the original dependency is lost for the case of version + -- pins, as only the pinned version is returned. function Releases (This : Solution) return Release_Map; -- Returns the proper releases in the solution (regular and detected @@ -342,6 +377,13 @@ private -- Begin of implementation + ---------------------- + -- All_Dependencies -- + ---------------------- + + function All_Dependencies (This : Solution) return State_Map + is (This.Dependencies); + ----------------- -- Composition -- ----------------- @@ -515,13 +557,6 @@ private This.Dependencies.Including (This.Dependencies (Crate).Pinning (Version))); - ---------- - -- Pins -- - ---------- - - function Pins (This : Solution) return Dependency_Map - is (This.Dependencies_That (States.Is_Pinned'Access)); - -------------- -- Required -- -------------- @@ -529,6 +564,15 @@ private function Required (This : Solution) return State_Map'Class is (This.Dependencies); + --------------- + -- Resetting -- + --------------- + + function Resetting (This : Solution; + Crate : Crate_Name) + return Solution + is (This.Missing (Crate).User_Unpinning (Crate)); + ------------- -- Setting -- ------------- @@ -551,6 +595,20 @@ private return Dependency_State is (This.Dependencies (Crate)); + --------------- + -- Unlinking -- + --------------- + + function Unlinking (This : Solution; + Crate : Crate_Name) + return Solution + is (if This.Dependencies.Contains (Crate) + then (Solved => True, + Dependencies => + This.Dependencies.Including + (This.Dependencies (Crate).Unlinking)) + else This); + --------------- -- Unpinning -- --------------- @@ -565,4 +623,27 @@ private (This.Dependencies (Crate).Unpinning)) else This); + --------------- + -- Unsolving -- + --------------- + + function Unsolving (This : Solution; + Crate : Crate_Name) + return Solution + is (if This.Dependencies.Contains (Crate) + then (Solved => True, + Dependencies => + This.Dependencies.Including + (This.Dependencies (Crate).Unlinking.Unpinning.Missing)) + else This); + + -------------------- + -- User_Unpinning -- + -------------------- + + function User_Unpinning (This : Solution; + Crate : Crate_Name) + return Solution + is (This.Unpinning (Crate).Unlinking (Crate)); + end Alire.Solutions; diff --git a/src/alire/alire-solver.adb b/src/alire/alire-solver.adb index 33619970..c4f4a14b 100644 --- a/src/alire/alire-solver.adb +++ b/src/alire/alire-solver.adb @@ -699,8 +699,28 @@ package body Alire.Solver is end if; end Detect_Unavailable_Direct_Dependencies; + ---------------- + -- Trace_Pins -- + ---------------- + + procedure Trace_Pins is + begin + if (for some State of Current.All_Dependencies => + State.Is_User_Pinned) + then + Trace.Detail ("User pins to apply:"); + for State of Current.All_Dependencies loop + if State.Is_User_Pinned then + Trace.Detail (" " & State.TTY_Image); + end if; + end loop; + else + Trace.Detail ("No user pins to apply"); + end if; + end Trace_Pins; + Full_Dependencies : constant Conditional.Dependencies := - Tree'(Current.Pins and Deps).Evaluate (Props); + Tree'(Current.User_Pins and Deps).Evaluate (Props); -- Include pins before other dependencies. This ensures their dependency -- can only be solved with the pinned version, and they are attempted -- first to avoid wasteful trial-and-error with other versions. @@ -713,6 +733,10 @@ package body Alire.Solver is Trace.Detail ("Solving dependencies with options: " & Image (Options)); + Trace.Detail ("Root dependency tree is: " + & Full_Dependencies.Image_One_Line); + Trace_Pins; + -- Warn if we foresee things taking a loong time... if Options.Completeness = All_Incomplete then diff --git a/src/alire/alire-solver.ads b/src/alire/alire-solver.ads index 9270eb5e..c8db29ed 100644 --- a/src/alire/alire-solver.ads +++ b/src/alire/alire-solver.ads @@ -3,6 +3,7 @@ with Alire.Index; with Alire.Properties; with Alire.Solutions; with Alire.Types; +with Alire.User_Pins.Maps; with Semantic_Versioning.Extended; @@ -53,8 +54,8 @@ package Alire.Solver is -- releases will be used normally; otherwise a crate with only externals -- will always cause failure. + subtype Pin_Map is User_Pins.Maps.Map; subtype Release is Types.Release; - subtype Solution is Solutions.Solution; -- The dependency solver (Resolve subprogram, below) receives a @@ -132,8 +133,8 @@ package Alire.Solver is Options : Query_Options := Default_Options) return Solution; -- Exhaustively look for a solution to the given dependencies, under the - -- given platform properties and lookup options. A current solution may - -- be given and pinned releases will be reused. + -- given platform properties and lookup options. Pins can be supplied to + -- override Deps. function Is_Resolvable (Deps : Types.Abstract_Dependencies; Props : Properties.Vector; diff --git a/src/alire/alire-toml_adapters.ads b/src/alire/alire-toml_adapters.ads index ddd97c73..5446d7bc 100644 --- a/src/alire/alire-toml_adapters.ads +++ b/src/alire/alire-toml_adapters.ads @@ -64,6 +64,9 @@ package Alire.TOML_Adapters with Preelaborate is -- Return the requested Key value, checking it matches type Kind. If type -- mismatch or missing key raise a Checked_Error. + function Contains (Queue : Key_Queue; Key : String) return Boolean; + -- Says if one of the keys in the wrapped table is Key + function Pop (Queue : Key_Queue) return TOML.TOML_Value; -- Return a value discarding its key; if no values left No_TOML_Value is -- returned. @@ -158,6 +161,8 @@ package Alire.TOML_Adapters with Preelaborate is private + use type UString; -- Allows comparisons between strings and unbounded + type Key_Queue is new Ada.Finalization.Limited_Controlled with record Value : TOML.TOML_Value; end record; @@ -165,6 +170,15 @@ private overriding procedure Finalize (This : in out Key_Queue); + -------------- + -- Contains -- + -------------- + + function Contains (Queue : Key_Queue; Key : String) return Boolean + is (Queue.Unwrap.Kind in TOML.TOML_Table + and then + (for some Table_Key of Queue.Unwrap.Keys => Key = Table_Key)); + ------------ -- Unwrap -- ------------ diff --git a/src/alire/alire-toml_keys.ads b/src/alire/alire-toml_keys.ads index 21cd4707..92d90108 100644 --- a/src/alire/alire-toml_keys.ads +++ b/src/alire/alire-toml_keys.ads @@ -36,6 +36,7 @@ package Alire.TOML_Keys with Preelaborate is OS : constant String := "os"; Path : constant String := "path"; Pinned : constant String := "pinned"; + Pins : constant String := "pins"; Project_File : constant String := "project-files"; Provides : constant String := "provides"; Tag : constant String := "tags"; diff --git a/src/alire/alire-toml_load.adb b/src/alire/alire-toml_load.adb index 59ffa23c..53d6561b 100644 --- a/src/alire/alire-toml_load.adb +++ b/src/alire/alire-toml_load.adb @@ -73,8 +73,10 @@ package body Alire.TOML_Load is From : TOML_Adapters.Key_Queue; Props : in out Conditional.Properties; Deps : in out Conditional.Dependencies; + Pins : in out User_Pins.Maps.Map; Avail : in out Conditional.Availability) is + pragma Unreferenced (Pins); use TOML; use type Conditional.Dependencies; use type Conditional.Properties; @@ -124,6 +126,15 @@ package body Alire.TOML_Load is & TOML_Keys.Depends_On); end if; + -- Process user pins + + if From.Contains (TOML_Keys.Pins) then + Pins := User_Pins.Maps.From_TOML + (From.Descend + (From.Checked_Pop (TOML_Keys.Pins, TOML_Array), + Context => TOML_Keys.Pins)); + end if; + -- TODO: Process Forbidden -- Process Available diff --git a/src/alire/alire-toml_load.ads b/src/alire/alire-toml_load.ads index 775580bb..1073d95c 100644 --- a/src/alire/alire-toml_load.ads +++ b/src/alire/alire-toml_load.ads @@ -1,6 +1,7 @@ with Alire.Conditional; with Alire.Crates; with Alire.TOML_Adapters; +with Alire.User_Pins.Maps; with TOML; @@ -21,6 +22,7 @@ package Alire.TOML_Load is From : TOML_Adapters.Key_Queue; Props : in out Conditional.Properties; Deps : in out Conditional.Dependencies; + Pins : in out User_Pins.Maps.Map; Avail : in out Conditional.Availability); -- Loads parts of a manifest, taking into account if we are loading -- a indexed release, a local release, a external shared section or diff --git a/src/alire/alire-user_pins-maps.adb b/src/alire/alire-user_pins-maps.adb new file mode 100644 index 00000000..cb11dd82 --- /dev/null +++ b/src/alire/alire-user_pins-maps.adb @@ -0,0 +1,42 @@ +package body Alire.User_Pins.Maps is + + --------------- + -- From_TOML -- + --------------- + + function From_TOML (This : TOML_Adapters.Key_Queue) return Map is + Result : Map; + begin + -- Each array entry may contain several pins (just like dependencies). + -- We pass those one by one to the pin loader. + + for I in 1 .. This.Unwrap.Length loop + declare + Table : constant TOML.TOML_Value := This.Unwrap.Item (I); + begin + This.Assert (Table.Kind in TOML.TOML_Table, + "expected a table with = but got: " + & Table.Kind'Image); + + for Key of Table.Keys loop + if Result.Contains (+(+Key)) then + This.Checked_Error ("pin for crate " & (+Key) + & " is specified more than once"); + end if; + + -- Obtain a single pin + + Result.Insert (+(+Key), + User_Pins.From_TOML + (This.Descend + (Value => Table.Get (Key), + Context => +Key))); + end loop; + end; + end loop; + + return Result; + + end From_TOML; + +end Alire.User_Pins.Maps; diff --git a/src/alire/alire-user_pins-maps.ads b/src/alire/alire-user_pins-maps.ads new file mode 100644 index 00000000..2b686bb2 --- /dev/null +++ b/src/alire/alire-user_pins-maps.ads @@ -0,0 +1,19 @@ +with Ada.Containers.Indefinite_Ordered_Maps; + +with Alire.Errors; + +with TOML; + +package Alire.User_Pins.Maps is + + package Pin_Maps is + new Ada.Containers.Indefinite_Ordered_Maps (Crate_Name, Pin); + + type Map is new Pin_Maps.Map with null record; + + function From_TOML (This : TOML_Adapters.Key_Queue) return Map + with Pre => This.Unwrap.Kind in TOML.TOML_Array + or else raise Checked_Error + with Errors.Set ("array expected but got a " & This.Unwrap.Kind'Image); + +end Alire.User_Pins.Maps; diff --git a/src/alire/alire-user_pins.adb b/src/alire/alire-user_pins.adb new file mode 100644 index 00000000..14e4d63a --- /dev/null +++ b/src/alire/alire-user_pins.adb @@ -0,0 +1,92 @@ +with Alire.Origins; + +with TOML; + +package body Alire.User_Pins is + + package Keys is + Commit : constant String := "commit"; + Path : constant String := "path"; + URL : constant String := "url"; + Version : constant String := "version"; + end Keys; + + --------------- + -- From_TOML -- + --------------- + + function From_TOML (This : TOML_Adapters.Key_Queue) return Pin is + + ---------------- + -- From_Table -- + ---------------- + + function From_Table (This : TOML_Adapters.Key_Queue) return Pin is + use TOML; + begin + if This.Contains (Keys.Version) then + return Pin' + (Kind => To_Version, + Version => Semantic_Versioning.Parse + (This.Checked_Pop (Keys.Version, TOML_String).As_String)); + + elsif This.Contains (Keys.Path) then + return Result : constant Pin := + (Kind => To_Path, + Path => +This.Checked_Pop (Keys.Path, TOML_String).As_String) + do + if not GNAT.OS_Lib.Is_Directory (+Result.Path) then + Raise_Checked_Error ("Pin path is not a valid directory: " + & (+Result.Path)); + end if; + This.Report_Extra_Keys; + end return; + + elsif This.Contains (Keys.URL) then + return Result : Pin := + (Kind => To_Git, + URL => +This.Checked_Pop (Keys.URL, TOML_String).As_String, + Commit => <>) + do + if This.Contains (Keys.Commit) then + Result.Commit := + +This.Checked_Pop (Keys.Commit, TOML_String).As_String; + This.Assert (+Result.Commit in Origins.Git_Commit, + "invalid commit: " & (+Result.Commit)); + end if; + + This.Report_Extra_Keys; + end return; + + else + Trace.Error ("Unexpected key in pin, got:"); + This.Print; + Raise_Checked_Error ("invalid pin description"); + end if; + end From_Table; + + begin + case This.Unwrap.Kind is + when TOML.TOML_String => + return Pin' + (Kind => To_Version, + Version => Semantic_Versioning.Parse (This.Unwrap.As_String)); + + when TOML.TOML_Table => + return Result : constant Pin := From_Table (This) do + This.Report_Extra_Keys; + end return; + + when others => + Raise_Checked_Error + ("improper format for pin, string or table expected but got a " + & This.Unwrap.Kind'Image); + + end case; + exception + when E : Semantic_Versioning.Malformed_Input => + Log_Exception (E); + Raise_Checked_Error ("Malformed semantic version in pin"); + end From_TOML; + +end Alire.User_Pins; diff --git a/src/alire/alire-user_pins.ads b/src/alire/alire-user_pins.ads new file mode 100644 index 00000000..637d2eef --- /dev/null +++ b/src/alire/alire-user_pins.ads @@ -0,0 +1,107 @@ +with Alire.Optional; +with Alire.TOML_Adapters; + +with Semantic_Versioning; + +package Alire.User_Pins is + + -- User-facing representation of pins. These are loaded from the manifest. + -- Internally, a user pin can be either a pin to a version, or a softlink + -- to a folder. Note that, as they cannot exist in the index (we would not + -- want an indexed crate to depend on unknown folders/remotes), there is no + -- need to generate TOML for them. + + -- The information provided by the user in the pin is not complete to work + -- with. The root must check if a remote pin was already retrieved, and if + -- it matches the user description, or else fetch it, when a change in the + -- manifest is detected. + + type Kinds is (To_Git, + To_Path, + To_Version); + + type Pin (Kind : Kinds) is tagged private; + + function Is_Remote (This : Pin) return Boolean; + -- A pin to a remote source such as git, source archives, etc + + -- Version attributes + + function Version (This : Pin) return Semantic_Versioning.Version + with Pre => This.Kind = To_Version; + + -- Local path attributes + + function Path (This : Pin) return Any_Path + with Pre => This.Kind = To_Path; + + -- Remote attributes + + function URL (This : Pin) return Alire.URL + with Pre => This.Is_Remote; + + function Commit (This : Pin) return Optional.String + with Pre => This.Is_Remote; + + function From_TOML (This : TOML_Adapters.Key_Queue) return Pin; + -- Expects the rhs of a crate = entry. The rhs is always a table. + + -- The TOML representation of a pin is similar to a dependency, but instead + -- of a version set, we get either a precise version, or an url + commit: + -- [[pins]] + -- foo = "3.4" -- OR: + -- foo = { version = "5.6" } + -- foo = { path = "/path/to/folder" } + -- bar = { url = "git+https://blah", [commit = "deadbeef"] } + +private + + type Pin (Kind : Kinds) is tagged record + case Kind is + when To_Git => + URL : UString; + Commit : UString; -- Optional + when To_Path => + Path : UString; + when To_Version => + Version : Semantic_Versioning.Version; + end case; + end record; + + ------------ + -- Commit -- + ------------ + + function Commit (This : Pin) return Optional.String + is (if +This.Commit = "" + then Optional.Strings.Empty + else Optional.Strings.Unit (+This.Commit)); + + --------------- + -- Is_Remote -- + --------------- + + function Is_Remote (This : Pin) return Boolean + is (This.Kind in To_Git); + + ---------- + -- Path -- + ---------- + + function Path (This : Pin) return Any_Path is (+This.Path); + + --------- + -- URL -- + --------- + + function URL (This : Pin) return Alire.URL + is (+This.URL); + + ------------- + -- Version -- + ------------- + + function Version (This : Pin) return Semantic_Versioning.Version + is (This.Version); + +end Alire.User_Pins; diff --git a/src/alr/alr-commands-pin.adb b/src/alr/alr-commands-pin.adb index 386204fb..31b62fe1 100644 --- a/src/alr/alr-commands-pin.adb +++ b/src/alr/alr-commands-pin.adb @@ -17,6 +17,20 @@ package body Alr.Commands.Pin is package Semver renames Semantic_Versioning; package TTY renames Alire.Utils.TTY; + ---------------------- + -- Warn_Manual_Only -- + ---------------------- + + procedure Warn_Manual_Only is + begin + Reportaise_Command_Failed + ("Pin adition/removal is currently not implemented via command-line. " + & "Please edit the manifest manually to add/remove pins. " + & "Find the syntax for pins at " + & TTY.URL ("https://github.com/alire-project/alire/blob/master/" + & "doc/catalog-format-spec.md")); + end Warn_Manual_Only; + -------------------- -- Change_One_Pin -- -------------------- @@ -159,6 +173,11 @@ package body Alr.Commands.Pin is ("Pin expects a single crate or crate=version argument"); end if; + -- Anything beyond this point is modifying pins, which is currently + -- unimplemented. + + Warn_Manual_Only; + -- Apply changes; declare diff --git a/src/alr/alr-commands-pin.ads b/src/alr/alr-commands-pin.ads index ffb27997..6dffb875 100644 --- a/src/alr/alr-commands-pin.ads +++ b/src/alr/alr-commands-pin.ads @@ -26,6 +26,10 @@ package Alr.Commands.Pin is & " | crate --use= [--commit=REF]" & " | --all]"); + procedure Warn_Manual_Only; + -- For the time being, we do not allow creating pins via command line. + -- Warn the user that pins have to be added manually to the manifest. + private type Command is new Commands.Command with record diff --git a/src/alr/alr-commands-update.adb b/src/alr/alr-commands-update.adb index 64527ff5..55c3c915 100644 --- a/src/alr/alr-commands-update.adb +++ b/src/alr/alr-commands-update.adb @@ -1,6 +1,5 @@ with Alire.Containers; with Alire.Errors; -with Alire.Utils.User_Input; with Alr.Commands.Index; @@ -43,11 +42,7 @@ package body Alr.Commands.Update is Cmd.Requires_Full_Index; - Cmd.Root.Update_Dependencies - (Allowed => Parse_Allowed, - Options => (Age => Query_Policy, - others => <>), - Silent => Alire.Utils.User_Input.Not_Interactive); + Cmd.Root.Update (Parse_Allowed); end Execute; ---------------------- diff --git a/src/alr/alr-commands-withing.adb b/src/alr/alr-commands-withing.adb index e5bd3365..d8ddce65 100644 --- a/src/alr/alr-commands-withing.adb +++ b/src/alr/alr-commands-withing.adb @@ -8,12 +8,12 @@ with Alire.Dependencies.Diffs; with Alire.Index; with Alire.Manifest; with Alire.Releases; -with Alire.Roots.Optional; with Alire.Solutions; with Alire.Solver; with Alire.URI; with Alire.Utils.User_Input; +with Alr.Commands.Pin; with Alr.Commands.User_Input; with Alr.OS_Lib; with Alr.Platform; @@ -567,6 +567,8 @@ package body Alr.Commands.Withing is -- Must be Add, but it could be regular or softlink if Cmd.URL.all /= "" then + Pin.Warn_Manual_Only; + if Cmd.Commit.all /= "" or else Alire.URI.Is_HTTP_Or_Git (Cmd.URL.all) then diff --git a/src/alr/alr-commands.adb b/src/alr/alr-commands.adb index 6b340eb0..fe27ddd1 100644 --- a/src/alr/alr-commands.adb +++ b/src/alr/alr-commands.adb @@ -610,7 +610,7 @@ package body Alr.Commands is if Checked.Solution.Is_Attempted then -- Check deps on disk match those in lockfile Cmd.Requires_Full_Index (Strict => False); - Checked.Sync_Solution_And_Deps; + Checked.Sync_From_Manifest (Silent => True); return; else Notify_Of_Initialization; @@ -627,8 +627,8 @@ package body Alr.Commands is Trace.Warning ("This workspace was created with a previous alr version." & " Internal data is going to be updated and, as a result," - & " any existing pins will be unpinned and will need to be" - & " manually recreated."); + & " a fresh solution will be computed that may result in" + & " crate upgrades"); Alire.Directories.Backup_If_Existing (Checked.Lock_File, Base_Dir => Alire.Paths.Working_Folder_Inside_Root); @@ -668,7 +668,8 @@ package body Alr.Commands is if Sync then Cmd.Requires_Full_Index (Strict => False); - Checked.Update_Dependencies (Silent => True); + Checked.Sync_From_Manifest (Silent => True, Force => True); + -- As we just created the empty lockfile, we force the update end if; end; end Requires_Valid_Session; diff --git a/testsuite/tests/get/indirect-link/my_index/index/index.toml b/testsuite/disabled/get/indirect-link/my_index/index/index.toml similarity index 100% rename from testsuite/tests/get/indirect-link/my_index/index/index.toml rename to testsuite/disabled/get/indirect-link/my_index/index/index.toml diff --git a/testsuite/tests/get/indirect-link/my_index/index/ti/tier1/tier1-1.0.0.toml b/testsuite/disabled/get/indirect-link/my_index/index/ti/tier1/tier1-1.0.0.toml similarity index 100% rename from testsuite/tests/get/indirect-link/my_index/index/ti/tier1/tier1-1.0.0.toml rename to testsuite/disabled/get/indirect-link/my_index/index/ti/tier1/tier1-1.0.0.toml diff --git a/testsuite/tests/get/indirect-link/my_index/index/ti/tier2/tier2-1.0.0.toml b/testsuite/disabled/get/indirect-link/my_index/index/ti/tier2/tier2-1.0.0.toml similarity index 100% rename from testsuite/tests/get/indirect-link/my_index/index/ti/tier2/tier2-1.0.0.toml rename to testsuite/disabled/get/indirect-link/my_index/index/ti/tier2/tier2-1.0.0.toml diff --git a/testsuite/tests/get/indirect-link/my_index/index/ti/tier3/tier3-1.0.0.toml b/testsuite/disabled/get/indirect-link/my_index/index/ti/tier3/tier3-1.0.0.toml similarity index 100% rename from testsuite/tests/get/indirect-link/my_index/index/ti/tier3/tier3-1.0.0.toml rename to testsuite/disabled/get/indirect-link/my_index/index/ti/tier3/tier3-1.0.0.toml diff --git a/testsuite/tests/get/indirect-link/test.py b/testsuite/disabled/get/indirect-link/test.py similarity index 100% rename from testsuite/tests/get/indirect-link/test.py rename to testsuite/disabled/get/indirect-link/test.py diff --git a/testsuite/tests/get/indirect-link/test.yaml b/testsuite/disabled/get/indirect-link/test.yaml similarity index 100% rename from testsuite/tests/get/indirect-link/test.yaml rename to testsuite/disabled/get/indirect-link/test.yaml diff --git a/testsuite/tests/pin/all/my_index/index/he/hello1/hello1-0.1.0.toml b/testsuite/disabled/pin/all/my_index/index/he/hello1/hello1-0.1.0.toml similarity index 100% rename from testsuite/tests/pin/all/my_index/index/he/hello1/hello1-0.1.0.toml rename to testsuite/disabled/pin/all/my_index/index/he/hello1/hello1-0.1.0.toml diff --git a/testsuite/tests/pin/all/my_index/index/he/hello2/hello2-0.1.0.toml b/testsuite/disabled/pin/all/my_index/index/he/hello2/hello2-0.1.0.toml similarity index 100% rename from testsuite/tests/pin/all/my_index/index/he/hello2/hello2-0.1.0.toml rename to testsuite/disabled/pin/all/my_index/index/he/hello2/hello2-0.1.0.toml diff --git a/testsuite/tests/pin/all/my_index/index/index.toml b/testsuite/disabled/pin/all/my_index/index/index.toml similarity index 100% rename from testsuite/tests/pin/all/my_index/index/index.toml rename to testsuite/disabled/pin/all/my_index/index/index.toml diff --git a/testsuite/tests/pin/all/test.py b/testsuite/disabled/pin/all/test.py similarity index 100% rename from testsuite/tests/pin/all/test.py rename to testsuite/disabled/pin/all/test.py diff --git a/testsuite/tests/pin/all/test.yaml b/testsuite/disabled/pin/all/test.yaml similarity index 100% rename from testsuite/tests/pin/all/test.yaml rename to testsuite/disabled/pin/all/test.yaml diff --git a/testsuite/tests/pin/dir-mismatch/test.py b/testsuite/disabled/pin/dir-mismatch/test.py similarity index 100% rename from testsuite/tests/pin/dir-mismatch/test.py rename to testsuite/disabled/pin/dir-mismatch/test.py diff --git a/testsuite/tests/pin/dir-mismatch/test.yaml b/testsuite/disabled/pin/dir-mismatch/test.yaml similarity index 100% rename from testsuite/tests/pin/dir-mismatch/test.yaml rename to testsuite/disabled/pin/dir-mismatch/test.yaml diff --git a/testsuite/tests/pin/remote/test.py b/testsuite/disabled/pin/remote/test.py similarity index 87% rename from testsuite/tests/pin/remote/test.py rename to testsuite/disabled/pin/remote/test.py index d26f6f56..756a520b 100644 --- a/testsuite/tests/pin/remote/test.py +++ b/testsuite/disabled/pin/remote/test.py @@ -32,8 +32,10 @@ def verify(head): run_alr("build") # Prepare for next test - run_alr("with", "--del", "upstream") # Remove dependency - shutil.rmtree("alire") # Total cleanup not relying on alr + run_alr("with", "--del", "upstream") # Remove dependency + p = run_alr("pin") + assert_eq(f"There are no pins\n", p.out) # Ensure pin is gone + shutil.rmtree("alire") # Total cleanup outside of alr # Initialize a git repo that will act as the "online" remote diff --git a/testsuite/tests/pin/remote/test.yaml b/testsuite/disabled/pin/remote/test.yaml similarity index 100% rename from testsuite/tests/pin/remote/test.yaml rename to testsuite/disabled/pin/remote/test.yaml diff --git a/testsuite/tests/printenv/linked-paths/my_index/crates/crate_1234/.emptydir b/testsuite/disabled/printenv/linked-paths/my_index/crates/crate_1234/.emptydir similarity index 100% rename from testsuite/tests/printenv/linked-paths/my_index/crates/crate_1234/.emptydir rename to testsuite/disabled/printenv/linked-paths/my_index/crates/crate_1234/.emptydir diff --git a/testsuite/tests/printenv/linked-paths/my_index/crates/crate_1234/alire.toml b/testsuite/disabled/printenv/linked-paths/my_index/crates/crate_1234/alire.toml similarity index 100% rename from testsuite/tests/printenv/linked-paths/my_index/crates/crate_1234/alire.toml rename to testsuite/disabled/printenv/linked-paths/my_index/crates/crate_1234/alire.toml diff --git a/testsuite/tests/printenv/linked-paths/my_index/crates/crate_1234/alire/.emptydir b/testsuite/disabled/printenv/linked-paths/my_index/crates/crate_1234/alire/.emptydir similarity index 100% rename from testsuite/tests/printenv/linked-paths/my_index/crates/crate_1234/alire/.emptydir rename to testsuite/disabled/printenv/linked-paths/my_index/crates/crate_1234/alire/.emptydir diff --git a/testsuite/tests/printenv/linked-paths/my_index/index/index.toml b/testsuite/disabled/printenv/linked-paths/my_index/index/index.toml similarity index 100% rename from testsuite/tests/printenv/linked-paths/my_index/index/index.toml rename to testsuite/disabled/printenv/linked-paths/my_index/index/index.toml diff --git a/testsuite/tests/printenv/linked-paths/test.py b/testsuite/disabled/printenv/linked-paths/test.py similarity index 100% rename from testsuite/tests/printenv/linked-paths/test.py rename to testsuite/disabled/printenv/linked-paths/test.py diff --git a/testsuite/tests/printenv/linked-paths/test.yaml b/testsuite/disabled/printenv/linked-paths/test.yaml similarity index 100% rename from testsuite/tests/printenv/linked-paths/test.yaml rename to testsuite/disabled/printenv/linked-paths/test.yaml diff --git a/testsuite/tests/with/changes-info/test.py b/testsuite/disabled/with/changes-info/test.py similarity index 100% rename from testsuite/tests/with/changes-info/test.py rename to testsuite/disabled/with/changes-info/test.py diff --git a/testsuite/tests/with/changes-info/test.yaml b/testsuite/disabled/with/changes-info/test.yaml similarity index 100% rename from testsuite/tests/with/changes-info/test.yaml rename to testsuite/disabled/with/changes-info/test.yaml diff --git a/testsuite/tests/with/pin-dir-crate-autodetect/test.py b/testsuite/disabled/with/pin-dir-crate-autodetect/test.py similarity index 100% rename from testsuite/tests/with/pin-dir-crate-autodetect/test.py rename to testsuite/disabled/with/pin-dir-crate-autodetect/test.py diff --git a/testsuite/tests/with/pin-dir-crate-autodetect/test.yaml b/testsuite/disabled/with/pin-dir-crate-autodetect/test.yaml similarity index 100% rename from testsuite/tests/with/pin-dir-crate-autodetect/test.yaml rename to testsuite/disabled/with/pin-dir-crate-autodetect/test.yaml diff --git a/testsuite/tests/with/pin-dir-crate/test.py b/testsuite/disabled/with/pin-dir-crate/test.py similarity index 100% rename from testsuite/tests/with/pin-dir-crate/test.py rename to testsuite/disabled/with/pin-dir-crate/test.py diff --git a/testsuite/tests/with/pin-dir-crate/test.yaml b/testsuite/disabled/with/pin-dir-crate/test.yaml similarity index 100% rename from testsuite/tests/with/pin-dir-crate/test.yaml rename to testsuite/disabled/with/pin-dir-crate/test.yaml diff --git a/testsuite/tests/with/pin-dir-mismatch/test.py b/testsuite/disabled/with/pin-dir-mismatch/test.py similarity index 100% rename from testsuite/tests/with/pin-dir-mismatch/test.py rename to testsuite/disabled/with/pin-dir-mismatch/test.py diff --git a/testsuite/tests/with/pin-dir-mismatch/test.yaml b/testsuite/disabled/with/pin-dir-mismatch/test.yaml similarity index 100% rename from testsuite/tests/with/pin-dir-mismatch/test.yaml rename to testsuite/disabled/with/pin-dir-mismatch/test.yaml diff --git a/testsuite/tests/with/pin-dir/my_index/crates/libhello_1.0.0/libhello.gpr b/testsuite/disabled/with/pin-dir/my_index/crates/libhello_1.0.0/libhello.gpr similarity index 100% rename from testsuite/tests/with/pin-dir/my_index/crates/libhello_1.0.0/libhello.gpr rename to testsuite/disabled/with/pin-dir/my_index/crates/libhello_1.0.0/libhello.gpr diff --git a/testsuite/tests/with/pin-dir/my_index/crates/libhello_1.0.0/src/libhello.ads b/testsuite/disabled/with/pin-dir/my_index/crates/libhello_1.0.0/src/libhello.ads similarity index 100% rename from testsuite/tests/with/pin-dir/my_index/crates/libhello_1.0.0/src/libhello.ads rename to testsuite/disabled/with/pin-dir/my_index/crates/libhello_1.0.0/src/libhello.ads diff --git a/testsuite/tests/with/pin-dir/my_index/index/index.toml b/testsuite/disabled/with/pin-dir/my_index/index/index.toml similarity index 100% rename from testsuite/tests/with/pin-dir/my_index/index/index.toml rename to testsuite/disabled/with/pin-dir/my_index/index/index.toml diff --git a/testsuite/tests/with/pin-dir/my_index/index/li/libhello/libhello-1.0.0.toml b/testsuite/disabled/with/pin-dir/my_index/index/li/libhello/libhello-1.0.0.toml similarity index 100% rename from testsuite/tests/with/pin-dir/my_index/index/li/libhello/libhello-1.0.0.toml rename to testsuite/disabled/with/pin-dir/my_index/index/li/libhello/libhello-1.0.0.toml diff --git a/testsuite/tests/with/pin-dir/test.py b/testsuite/disabled/with/pin-dir/test.py similarity index 100% rename from testsuite/tests/with/pin-dir/test.py rename to testsuite/disabled/with/pin-dir/test.py diff --git a/testsuite/tests/with/pin-dir/test.yaml b/testsuite/disabled/with/pin-dir/test.yaml similarity index 100% rename from testsuite/tests/with/pin-dir/test.yaml rename to testsuite/disabled/with/pin-dir/test.yaml diff --git a/testsuite/tests/with/pin-transitive/test.py b/testsuite/disabled/with/pin-transitive/test.py similarity index 94% rename from testsuite/tests/with/pin-transitive/test.py rename to testsuite/disabled/with/pin-transitive/test.py index ba8df745..69f7a1a0 100644 --- a/testsuite/tests/with/pin-transitive/test.py +++ b/testsuite/disabled/with/pin-transitive/test.py @@ -10,7 +10,7 @@ from drivers.asserts import assert_eq # The test will create ./indirect, ./direct, and ./nest/base crates. # Then they are pinned as base -> direct -> indirect. As "base" has a different -# relative path to "indirect" that "direct", this is also checked. +# relative path to "indirect" than "direct", this is also checked. init_local_crate(name="indirect", binary=False, enter=False) diff --git a/testsuite/disabled/with/pin-transitive/test.yaml b/testsuite/disabled/with/pin-transitive/test.yaml new file mode 100644 index 00000000..648ca9d1 --- /dev/null +++ b/testsuite/disabled/with/pin-transitive/test.yaml @@ -0,0 +1,4 @@ +driver: python-script +indexes: + basic_index: + in_fixtures: true \ No newline at end of file diff --git a/testsuite/tests/with/tree-switch/test.py b/testsuite/disabled/with/tree-switch/test.py similarity index 100% rename from testsuite/tests/with/tree-switch/test.py rename to testsuite/disabled/with/tree-switch/test.py diff --git a/testsuite/tests/with/tree-switch/test.yaml b/testsuite/disabled/with/tree-switch/test.yaml similarity index 100% rename from testsuite/tests/with/tree-switch/test.yaml rename to testsuite/disabled/with/tree-switch/test.yaml diff --git a/testsuite/tests/with/versions-switch/test.py b/testsuite/disabled/with/versions-switch/test.py similarity index 100% rename from testsuite/tests/with/versions-switch/test.py rename to testsuite/disabled/with/versions-switch/test.py diff --git a/testsuite/tests/with/versions-switch/test.yaml b/testsuite/disabled/with/versions-switch/test.yaml similarity index 100% rename from testsuite/tests/with/versions-switch/test.yaml rename to testsuite/disabled/with/versions-switch/test.yaml diff --git a/testsuite/drivers/alr.py b/testsuite/drivers/alr.py index 953de28b..379c097e 100644 --- a/testsuite/drivers/alr.py +++ b/testsuite/drivers/alr.py @@ -2,6 +2,7 @@ Helpers to run alr in the testsuite. """ +import os import os.path from shutil import copytree @@ -201,3 +202,79 @@ def init_local_crate(name="xxx", binary=True, enter=True): run_alr("init", name, "--bin" if binary else "--lib") if enter: os.chdir(name) + + +def alr_lockfile(): + return "alire.lock" + + +def alr_manifest(): + return "alire.toml" + + +def alr_unpin(crate, manual=True, fail_if_missing=True): + """ + Unpin a crate, if pinned, or no-op otherwise. Will edit the manifest or use + the command-line, according to manual. Must be run in a crate root. + """ + + if manual: + # Locate and remove the lines with the pin + with open("alire.toml", "rt") as manifest: + found = False + lines = manifest.readlines() + orig = lines + for i in range(1, len(lines)): + if lines[i].startswith(f"{crate} =") \ + and lines[i-1] == "[[pins]]\n": + lines.pop(i) + lines.pop(i-1) + found = True + break + + # Write the new manifest + if found: + with open("alire.toml", "wt") as manifest: + manifest.writelines(lines) + + # Make the lockfile "older" (otherwise timestamp is identical) + os.utime("alire.lock", (0, 0)) + run_alr("pin") # Ensure changes don't affect next command output + elif fail_if_missing: + raise RuntimeError + (f"Could not unpin crate {crate} in lines:\n" + str(orig)) + + else: + raise NotImplementedError("Unimplemented") + + +def alr_pin(crate, version="", path="", url="", commit="", manual=True): + """ + Pin a crate, either manually or using the command-line interface. Use only + one of version, path, url. Must be run in a crate root. + """ + + if manual: + alr_unpin(crate, fail_if_missing=False) # Just in case + + if version != "": + pin_line = f'{crate} = {{ version = "{version}" }}' + elif path != "": + pin_line = f'{crate} = {{ path = "{path}" }}' + elif url != "" and commit != "": + pin_line = f'{crate} = {{ url = "{path}", commit = "{commit}" }}' + elif url != "": + pin_line = f'{crate} = {{ url = "{path}" }}' + else: + raise ValueError("Specify either version, path or url") + + with open("alire.toml", "at") as manifest: + manifest.writelines(["\n[[pins]]\n", pin_line + "\n"]) + + # Make the lockfile "older" (otherwise timestamp is identical) + os.utime("alire.lock", (0, 0)) + + run_alr("pin") # so the changes in the manifest are applied + + else: + raise NotImplementedError("Unimplemented") diff --git a/testsuite/tests/pin/change-type/test.py b/testsuite/tests/pin/change-type/test.py index 7b12e490..957f2811 100644 --- a/testsuite/tests/pin/change-type/test.py +++ b/testsuite/tests/pin/change-type/test.py @@ -1,11 +1,11 @@ """ -Change a pinned dependency from a version to a folder and back +Change a pinned dependency from a version to a folder and back, using manifest """ import os import re -from drivers.alr import run_alr +from drivers.alr import run_alr, alr_pin from drivers.asserts import assert_match from drivers.helpers import dir_separator @@ -23,13 +23,13 @@ os.chdir('xxx') run_alr('with', 'libhello^1') # Pin to a version -p = run_alr('pin', 'libhello=1.0') +alr_pin('libhello', version='1.0') # Check that it shows as such in the solution check_version_pin() # Repin to a folder -run_alr('pin', 'libhello', '--use', '../crates/libhello_1.0.0') +alr_pin('libhello', path='../crates/libhello_1.0.0') # Check that it shows as such in the solution p = run_alr('show', '--solve') @@ -41,7 +41,7 @@ assert_match('.*Dependencies \(external\):.*' p.out, flags=re.S) # Repin to a version and check again -p = run_alr('pin', 'libhello=1.0') +alr_pin('libhello', version='1.0') check_version_pin() diff --git a/testsuite/tests/pin/dir-crate/test.py b/testsuite/tests/pin/dir-crate/test.py index 938c16fa..8b0c6ff0 100644 --- a/testsuite/tests/pin/dir-crate/test.py +++ b/testsuite/tests/pin/dir-crate/test.py @@ -5,7 +5,7 @@ Detect that a given folder to pin contains a crate and use its info import os import re -from drivers.alr import run_alr +from drivers.alr import run_alr, alr_pin from drivers.asserts import assert_match from drivers.helpers import dir_separator from glob import glob @@ -25,7 +25,7 @@ run_alr('with', 'hello', '--force') # Pin the hello crate as local dir dependency. The version in the folder is # different to the one we had in the solution, so this should cause a downgrade # but with complete solution. Now hello=1 --> libhello=1.1. -run_alr('pin', 'hello', '--use', '..' + dir_separator() + target) +alr_pin('hello', path='../' + target) # Verify that hello dependencies are detected and used, and are the ones # corresponding to the linked dir versions. diff --git a/testsuite/tests/pin/downgrade/test.py b/testsuite/tests/pin/downgrade/test.py index 9b1f1363..0e264667 100644 --- a/testsuite/tests/pin/downgrade/test.py +++ b/testsuite/tests/pin/downgrade/test.py @@ -4,7 +4,7 @@ Test that a pin to a lower version downgrades and retrieves the new version import os -from drivers.alr import run_alr +from drivers.alr import run_alr, alr_pin from drivers.asserts import assert_eq, assert_match from drivers.helpers import check_line_in @@ -40,7 +40,7 @@ p = run_alr('show', '--solve') check_child('0.2.0', p.out, pinned=False) # Pin it to a downgrade -run_alr('pin', 'libchild=0.1') +alr_pin('libchild', version='0.1') # Verify new version p = run_alr('show', '--solve') diff --git a/testsuite/tests/pin/manifest-invalid-pins/my_index/crates/crate/.emptydir b/testsuite/tests/pin/manifest-invalid-pins/my_index/crates/crate/.emptydir new file mode 100644 index 00000000..e69de29b diff --git a/testsuite/tests/pin/manifest-invalid-pins/my_index/index/cr/crate/crate-1.0.0.toml b/testsuite/tests/pin/manifest-invalid-pins/my_index/index/cr/crate/crate-1.0.0.toml new file mode 100644 index 00000000..97fb716e --- /dev/null +++ b/testsuite/tests/pin/manifest-invalid-pins/my_index/index/cr/crate/crate-1.0.0.toml @@ -0,0 +1,9 @@ +description = "Sample crate" +name = "crate" +version = "1.0.0" +licenses = [] +maintainers = ["any@bo.dy"] +maintainers-logins = ["someone"] + +[origin] +url = "file:../../../crates/crate" diff --git a/testsuite/tests/pin/manifest-invalid-pins/my_index/index/index.toml b/testsuite/tests/pin/manifest-invalid-pins/my_index/index/index.toml new file mode 100644 index 00000000..346c93fc --- /dev/null +++ b/testsuite/tests/pin/manifest-invalid-pins/my_index/index/index.toml @@ -0,0 +1 @@ +version = "1.0" diff --git a/testsuite/tests/pin/manifest-invalid-pins/test.py b/testsuite/tests/pin/manifest-invalid-pins/test.py new file mode 100644 index 00000000..ed1faafc --- /dev/null +++ b/testsuite/tests/pin/manifest-invalid-pins/test.py @@ -0,0 +1,83 @@ +""" +Verify various bad pins in the manifest are rejected +""" + +from drivers.alr import run_alr, init_local_crate, alr_lockfile, alr_manifest +from drivers.helpers import lines_of +from drivers.asserts import assert_match + +import os.path + + +def check(pin, error): + """ + Insert a pin at the end of the manifest, verify that it is rejected, and + remove it from the manifest. Check the error produced against the one given + """ + with open(alr_manifest(), "a") as manifest: + manifest.write("\n[[pins]]\n" + pin + "\n") + + # Remove lockfile to ensure reload + if os.path.exists(alr_lockfile()): + os.remove(alr_lockfile()) + + p = run_alr("pin", complain_on_error=False) + assert p.status != 0, "Unexpected success for pin: " + pin + assert_match(".*Cannot continue with invalid session.*" + + error + ".*\n", + p.out) + + # Restore the manifest + lines = lines_of(alr_manifest()) + lines.pop() + with open(alr_manifest(), "w") as manifest: + manifest.write("".join(lines)) + + # Verify the manifest is OK again + run_alr("pin") + + +init_local_crate() + +# Invalid empty pins +check("foo = { }", "invalid pin description") +check("foo = ''", "Malformed semantic version in pin") + +# Single string must be a version +check("foo = 'https://github.com/alire-project/alire.git'", + "Malformed semantic version in pin") + +# Mixed incompatible fields +check("foo = { version = '3', url='https://' }", + "forbidden extra entries") +check("foo = { path = '/', url='https://' }", + "forbidden extra entries") +check("foo = { path = '/', version='6976' }", + "forbidden extra entries") +check("foo = { path = '/', " + "commit = '0123456789012345678901234567890123456789' }", + "forbidden extra entries") + +# Extraneous field +check("foo = { wont_ever_exist='' }", "invalid pin description") + +# Commit without url +check("foo = { commit = '0123456789012345678901234567890123456789' }", + "invalid pin description") + +# Invalid commit +check("foo = { url = 'https://', commit = '1234abcd' }", + "invalid commit") + +# Extra unknown field +check("foo = { version = '3', wont_exist='' }", + "forbidden extra entries") +check("foo = { path = '/', wont_exist='' }", + "forbidden extra entries") +check("foo = { url = 'https://', wont_exist='' }", + "forbidden extra entries") +check("foo = { url = 'https://', wont_exist='', " + "commit = '0123456789012345678901234567890123456789' }", + "forbidden extra entries") + +print('SUCCESS') diff --git a/testsuite/tests/pin/manifest-invalid-pins/test.yaml b/testsuite/tests/pin/manifest-invalid-pins/test.yaml new file mode 100644 index 00000000..0a859639 --- /dev/null +++ b/testsuite/tests/pin/manifest-invalid-pins/test.yaml @@ -0,0 +1,4 @@ +driver: python-script +indexes: + my_index: + in_fixtures: false diff --git a/testsuite/tests/pin/missing-version/test.py b/testsuite/tests/pin/missing-version/test.py index 119b2bd2..137eb9b4 100644 --- a/testsuite/tests/pin/missing-version/test.py +++ b/testsuite/tests/pin/missing-version/test.py @@ -1,11 +1,11 @@ """ -Pin forcibly a dependencies that cause missing dependencies +Pin forcibly a dependency that cause missing dependencies """ import re import os -from drivers.alr import run_alr +from drivers.alr import run_alr, alr_pin from drivers.asserts import assert_match from glob import glob @@ -19,7 +19,7 @@ run_alr('with', 'hello>0') # 1st test: pin to an existing version that brings in missing dependencies. # Pinning hello=3 brings in a libhello^3 dependency that is unavailable, so: # xxx=0.0.0 -> hello=3.0.0 --> libhello^3 (missing) -run_alr('pin', '--force', 'hello=3') +alr_pin('hello', version='3') # Check solution is as expected p = run_alr('with', '--solve') @@ -32,7 +32,7 @@ assert_match('.*Dependencies \(solution\):\n' # 2nd test: directly pin to a missing version (hello=5). This causes libhello # to disappear from the solution, since hello's dependencies are now unknown: # xxx=0.0.0 -> hello=5 (missing) -run_alr('pin', '--force', 'hello=5') +alr_pin('hello', version='5') # Check solution is as expected p = run_alr('with', '--solve') diff --git a/testsuite/tests/pin/pin-dir-with-regular/test.py b/testsuite/tests/pin/pin-dir-with-regular/test.py index 31f25b11..d6511d25 100644 --- a/testsuite/tests/pin/pin-dir-with-regular/test.py +++ b/testsuite/tests/pin/pin-dir-with-regular/test.py @@ -5,7 +5,7 @@ Test that pinning another crate doesn't affect a regularly solved one import os import re -from drivers.alr import run_alr +from drivers.alr import run_alr, alr_pin from drivers.asserts import assert_match # Initialize a workspace, enter, and add a regular dependency @@ -19,7 +19,7 @@ run_alr('with', 'libhello') run_alr('with', 'unobtanium', '--force') # Pin the missing crate -run_alr('pin', 'unobtanium', '--use', '/') +alr_pin('unobtanium', path='/') # Check the solution shows both pinned dir and regular dependency p = run_alr('with', '--solve') diff --git a/testsuite/tests/pin/pin-dir/test.py b/testsuite/tests/pin/pin-dir/test.py index dfab92df..39103b27 100644 --- a/testsuite/tests/pin/pin-dir/test.py +++ b/testsuite/tests/pin/pin-dir/test.py @@ -5,7 +5,7 @@ Replacing a dependency with a pinned folder import os import re -from drivers.alr import run_alr +from drivers.alr import run_alr, alr_pin, alr_unpin from drivers.asserts import assert_match from drivers.helpers import dir_separator, with_project @@ -22,7 +22,7 @@ assert p.status != 0, "Build should fail" # Add normally and then pin, check that it builds run_alr('with', 'libhello') -run_alr('pin', 'libhello', '--use', '../crates/libhello_1.0.0') +alr_pin('libhello', path='../crates/libhello_1.0.0') run_alr('build') # Check the pin shows in the solution @@ -38,7 +38,7 @@ assert_match('.*Dependencies \(external\):.*' # Check that unpinning the dependency works and now the dependency is show # as a regular one from the index -run_alr('pin', '--unpin', 'libhello') +alr_unpin('libhello') p = run_alr('show', '--solve') assert_match('.*Dependencies \(solution\):' '.*libhello=1.0.0.*', diff --git a/testsuite/tests/pin/post-update/test.py b/testsuite/tests/pin/post-update/test.py index 988a310d..bfdb104b 100644 --- a/testsuite/tests/pin/post-update/test.py +++ b/testsuite/tests/pin/post-update/test.py @@ -4,7 +4,7 @@ Test that a pinned release does not get updated import os -from drivers.alr import run_alr +from drivers.alr import run_alr, alr_pin, alr_unpin from drivers.asserts import assert_eq, assert_match from drivers.helpers import check_line_in @@ -35,10 +35,10 @@ os.chdir('xxx') run_alr('with', 'libchild=0.1') # Pin it -run_alr('pin', 'libchild') +alr_pin('libchild', version="0.1") # To avoid pinning and downgrading (that's a different test), we depend on -# a crate that indirectly depends on libchild. This way we can remove the exact +# a crate that also depends on libchild. This way we can remove the exact # libchild dependency and verify the pin holds run_alr('with', 'libparent') @@ -55,7 +55,7 @@ p = run_alr('show', '--solve') check_child('0.1.0', p.out, pinned=True) # Unpin and check upgraded solution -run_alr('pin', '--unpin', 'libchild') +alr_unpin('libchild') p = run_alr('show', '--solve') check_child('0.2.0', p.out, pinned=False) diff --git a/testsuite/tests/pin/twice-in-manifest/my_index/index.toml b/testsuite/tests/pin/twice-in-manifest/my_index/index.toml new file mode 100644 index 00000000..346c93fc --- /dev/null +++ b/testsuite/tests/pin/twice-in-manifest/my_index/index.toml @@ -0,0 +1 @@ +version = "1.0" diff --git a/testsuite/tests/pin/twice-in-manifest/my_index/li/libhello/libhello-1.0.0.toml b/testsuite/tests/pin/twice-in-manifest/my_index/li/libhello/libhello-1.0.0.toml new file mode 100644 index 00000000..f1eb7f5e --- /dev/null +++ b/testsuite/tests/pin/twice-in-manifest/my_index/li/libhello/libhello-1.0.0.toml @@ -0,0 +1,9 @@ +description = "libhello" +name = "libhello" +version = "1.0.0" +licenses = "GPL-3.0-only" +maintainers = ["some@one.com"] +maintainers-logins = ["mylogin"] + +[origin] +url = "file:../../../crates/libhello_1.0.0" diff --git a/testsuite/tests/pin/twice-in-manifest/test.py b/testsuite/tests/pin/twice-in-manifest/test.py new file mode 100644 index 00000000..f4e440be --- /dev/null +++ b/testsuite/tests/pin/twice-in-manifest/test.py @@ -0,0 +1,32 @@ +""" +Detect a crate with two pins in the manifest +""" + +import os +import re + +from drivers.alr import run_alr, alr_pin +from drivers.asserts import assert_match +from drivers.helpers import dir_separator + + +# Initialize a workspace, enter, and add a regular dependency +run_alr('init', '--bin', 'xxx') +os.chdir('xxx') +run_alr('with', 'libhello^1') + +# Pin to a version +alr_pin('libhello', version='1.0') + +# Manually force a second pin +with open("alire.toml", "at") as manifest: + manifest.write("[[pins]]\n") + manifest.write("libhello = { path = '.' }\n") + +# Check that the second pin is rejected +p = run_alr('pin', complain_on_error=False) +assert_match('.*pin for crate libhello is specified more than once.*', + p.out) + + +print('SUCCESS') diff --git a/testsuite/tests/pin/twice-in-manifest/test.yaml b/testsuite/tests/pin/twice-in-manifest/test.yaml new file mode 100644 index 00000000..b7da6fe7 --- /dev/null +++ b/testsuite/tests/pin/twice-in-manifest/test.yaml @@ -0,0 +1,5 @@ +driver: python-script +indexes: + my_index: + in_fixtures: false + copy_crates_src: true diff --git a/testsuite/tests/pin/unneeded-held/test.py b/testsuite/tests/pin/unneeded-held/test.py index 8ff3c8b4..1d9612c2 100644 --- a/testsuite/tests/pin/unneeded-held/test.py +++ b/testsuite/tests/pin/unneeded-held/test.py @@ -4,7 +4,7 @@ Test that removing a pinned dependency keeps the pinned release in the solution import os -from drivers.alr import run_alr +from drivers.alr import run_alr, alr_pin from drivers.asserts import assert_eq, assert_match from drivers.helpers import check_line_in @@ -21,7 +21,7 @@ os.chdir('xxx') run_alr('with', 'libparent') # Pin the child -run_alr('pin', 'libchild') +alr_pin('libchild', version='0.2') # Remove parent run_alr('with', '--del', 'libparent') diff --git a/testsuite/tests/pin/unpin/test.py b/testsuite/tests/pin/unpin/test.py index 824d135e..ee8b1a15 100644 --- a/testsuite/tests/pin/unpin/test.py +++ b/testsuite/tests/pin/unpin/test.py @@ -4,7 +4,7 @@ Test unpinning import os -from drivers.alr import run_alr +from drivers.alr import run_alr, alr_pin, alr_unpin from drivers.asserts import assert_eq from drivers.helpers import check_line_in @@ -17,12 +17,19 @@ os.chdir('xxx') run_alr('with', 'libhello') # Pin the version of libhello and verify pin is there -run_alr('pin', 'libhello') +alr_pin('libhello', version="1") p = run_alr('pin') assert_eq('libhello 1.0.0\n', p.out) +# Update and verify that the pin has survived +run_alr('update') +p = run_alr('pin') +assert_eq('libhello 1.0.0\n', p.out) + +# Delete lockfile and verify the pin has survived + # Unpin and verify pin is not there -run_alr('pin', '--unpin', 'libhello') +alr_unpin('libhello') p = run_alr('pin') assert_eq('There are no pins\n', p.out) diff --git a/testsuite/tests/pin/without-lockfile/test.py b/testsuite/tests/pin/without-lockfile/test.py new file mode 100644 index 00000000..c5ce1f55 --- /dev/null +++ b/testsuite/tests/pin/without-lockfile/test.py @@ -0,0 +1,31 @@ +""" +Verify that pins when there is no lockfile are correctly applied on first run +""" + +from drivers.alr import run_alr, alr_pin, init_local_crate, alr_lockfile +from drivers.asserts import assert_eq + +import os + + +fake_dep = "unobtainium" + +# Create a crate +init_local_crate() + +# Add a dependency +run_alr("with", fake_dep, "--force") + +# Pin to a local folder +os.mkdir(fake_dep) +alr_pin(fake_dep, path=fake_dep) + +# Remove the lockfile +os.remove(alr_lockfile()) + +# Check that the pin is applied on next command run +p = run_alr("pin") +assert_eq(f"{fake_dep} file:{fake_dep} \n", p.out) + + +print('SUCCESS') diff --git a/testsuite/tests/pin/without-lockfile/test.yaml b/testsuite/tests/pin/without-lockfile/test.yaml new file mode 100644 index 00000000..8929d590 --- /dev/null +++ b/testsuite/tests/pin/without-lockfile/test.yaml @@ -0,0 +1,4 @@ +driver: python-script +indexes: + basic_index: + in_fixtures: true diff --git a/testsuite/tests/printenv/with-external/test.py b/testsuite/tests/printenv/with-external/test.py index ab8072ea..6a816384 100644 --- a/testsuite/tests/printenv/with-external/test.py +++ b/testsuite/tests/printenv/with-external/test.py @@ -21,15 +21,16 @@ os.chdir('libhello_0.9.0_filesystem') p = run_alr('printenv', quiet=False) assert_eq(0, p.status) -expected_gpr_path="" +expected_gpr_path = "" if platform.system() == 'Windows': expected_gpr_path = '.*\\\\libhello_0.9.0_filesystem' else: expected_gpr_path = '.*/libhello_0.9.0_filesystem' # Check the printenv output -assert_match('warn: Generating incomplete environment' # Note: this warning is - ' because of missing dependencies\n' # via stderr so it's OK +assert_match('warn: Generating possibly incomplete environment' + ' because of missing dependencies\n' + # Note: this warning is via stderr so it's OK '.*' 'export ALIRE="True"\n' '.*' diff --git a/testsuite/tests/update/manual-once/test.py b/testsuite/tests/update/manual-once/test.py index f93b5167..370710eb 100644 --- a/testsuite/tests/update/manual-once/test.py +++ b/testsuite/tests/update/manual-once/test.py @@ -21,7 +21,7 @@ def prepare_crate(name): info = os.stat("alire.toml") os.utime("alire.lock", (info.st_atime, info.st_mtime - 1)) -warning_text = "Detected changes in manifest, updating workspace" +warning_text = "Detected changes in manifest, synchronizing workspace" # Test when directly doing an update. Should report no changes. prepare_crate("test1") @@ -34,7 +34,7 @@ assert warning_text not in p.out # Test when doing other things. Should warn once of possible changes. prepare_crate("test2") p = run_alr("with", quiet=False) # First run must warn -assert_match(warning_text + ".*", p.out) +assert_match(".*" + warning_text + ".*", p.out) p = run_alr("with", quiet=False) # Second run must not warn assert warning_text not in p.out diff --git a/testsuite/tests/update/missing-deps/test.py b/testsuite/tests/update/missing-deps/test.py index 30b6b9a2..560bde38 100644 --- a/testsuite/tests/update/missing-deps/test.py +++ b/testsuite/tests/update/missing-deps/test.py @@ -1,11 +1,12 @@ """ -Check that updating an incomplete solution is doable resulting in no changes +Check that updating an incomplete solution is doable resulting in no changes. +This is labeled manual because the pin is added through the manifest. """ import re import os -from drivers.alr import run_alr +from drivers.alr import run_alr, alr_pin from drivers.asserts import assert_match from glob import glob @@ -14,7 +15,7 @@ from glob import glob run_alr('init', '--bin', 'xxx') os.chdir('xxx') run_alr('with', 'libhello') -run_alr('pin', '--force', 'libhello=3') +alr_pin('libhello', version="3") # See that updating succeeds run_alr('update') diff --git a/testsuite/tests/update/pinned/test.py b/testsuite/tests/update/pinned/test.py index 03e455a8..a0ddabba 100644 --- a/testsuite/tests/update/pinned/test.py +++ b/testsuite/tests/update/pinned/test.py @@ -1,11 +1,11 @@ """ -Check that updating a pinned crate results in a recoverable error +Check that updating a manifest-pinned crate results in a recoverable error """ import re import os -from drivers.alr import run_alr +from drivers.alr import run_alr, alr_pin from drivers.asserts import assert_match from glob import glob @@ -13,8 +13,8 @@ from glob import glob # Add a dependency and force it missing by pinning it to non-existing version run_alr('init', '--bin', 'xxx') os.chdir('xxx') -run_alr('with', 'libhello^1') # This causes libhello=1.1 -run_alr('pin', 'libhello=1') # Downgrade to 1.0 +run_alr('with', 'libhello^1') # This causes libhello=1.1 +alr_pin('libhello', version='1') # Downgrade to 1.0 # Check that updating without specific crate does not err run_alr('update') diff --git a/testsuite/tests/with/pin-transitive/test.yaml b/testsuite/tests/with/pin-transitive/test.yaml deleted file mode 100644 index 32c747b3..00000000 --- a/testsuite/tests/with/pin-transitive/test.yaml +++ /dev/null @@ -1 +0,0 @@ -driver: python-script diff --git a/testsuite/tests/workflows/init-with-pin/test.py b/testsuite/tests/workflows/init-with-pin/test.py index 6828f1cd..d823f966 100644 --- a/testsuite/tests/workflows/init-with-pin/test.py +++ b/testsuite/tests/workflows/init-with-pin/test.py @@ -4,7 +4,7 @@ Test a basic init-with-pin-run workflow. import os -from drivers.alr import run_alr +from drivers.alr import run_alr, alr_pin from drivers.asserts import assert_eq from drivers.helpers import check_line_in @@ -29,7 +29,7 @@ with open('xxx.gpr', 'w') as f: f.write(content) # Pin the version of libhello and verify pin is there -run_alr('pin', 'libhello') +alr_pin('libhello', version='1.0') p = run_alr('pin') assert_eq('libhello 1.0.0\n', p.out) -- 2.39.5