From ac9f1d80bc2c266400d9a58ff22803b5e4b41721 Mon Sep 17 00:00:00 2001 From: "Alejandro R. Mosteo" Date: Wed, 13 Mar 2024 14:59:15 +0100 Subject: [PATCH 01/53] Bump versions to 2.1-dev --- RELEASING.md | 6 ++++-- alire.toml | 2 +- doc/user-changes.md | 4 +++- src/alire/alire-version.ads | 2 +- 4 files changed, 9 insertions(+), 5 deletions(-) diff --git a/RELEASING.md b/RELEASING.md index 5cd6eb8e9..eb3a31357 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -1,8 +1,10 @@ ## Checklist for releasing a new version 1. [ ] Run local-only tests (`/testsuite/run-dev-sh`) -1. [ ] Update version in `Alire.Version`. -1. [ ] Update version in `alire.toml`. +1. [ ] Update versions + - `Alire.Version` + - `alire.toml` + - `user-changes.md` 1. [ ] Create test release in own fork. - To verify builds succeed. - As the Windows build can rarely fail, this provides a backup .exe diff --git a/alire.toml b/alire.toml index 73465f3e6..81deb0d6a 100644 --- a/alire.toml +++ b/alire.toml @@ -1,7 +1,7 @@ name = "alr" description = "Command-line tool from the Alire project" -version = "2.0" +version = "2.1-dev" authors = ["Alejandro R. Mosteo", "Fabien Chouteau", "Pierre-Marie de Rodat"] maintainers = ["alejandro@mosteo.com", "chouteau@adacore.com"] diff --git a/doc/user-changes.md b/doc/user-changes.md index 08bb3e8d2..1cd072e0b 100644 --- a/doc/user-changes.md +++ b/doc/user-changes.md @@ -4,7 +4,9 @@ This document is a development diary summarizing changes in `alr` that notably affect the user experience. It is intended as a one-stop point for users to stay on top of `alr` new features. -## Release `2.0-dev` +## Release `2.1` + +## Release `2.0` ### `ALIRE_SETTINGS_DIR` replaces `ALR_CONFIG` diff --git a/src/alire/alire-version.ads b/src/alire/alire-version.ads index e1447f456..8c471d642 100644 --- a/src/alire/alire-version.ads +++ b/src/alire/alire-version.ads @@ -2,7 +2,7 @@ package Alire.Version with Preelaborate is -- Remember to update Alire.Index branch if needed too - Current : constant String := "2.0"; + Current : constant String := "2.1-dev"; -- 2.0.0: alr settings refactor and minor fixes -- 2.0.0-rc1: release candidate for 2.0 -- 2.0.0-b1: first public release on the 2.0 branch From fd99bea2e8efb28fb543483520643e106bf17c92 Mon Sep 17 00:00:00 2001 From: "Alejandro R. Mosteo" Date: Wed, 13 Mar 2024 22:19:49 +0100 Subject: [PATCH 02/53] Updated BREAKING.md --- BREAKING.md | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/BREAKING.md b/BREAKING.md index c23c39804..4809195fb 100644 --- a/BREAKING.md +++ b/BREAKING.md @@ -1,9 +1,19 @@ Log of breaking changes in index or alr. -### alr 2.0.0 + index 1.2.2 +### alr 3.0.0 + index 1.3.0 + +- alr: removed `ALR_CONFIG` environment variable. +- alr: removed `alr config` command. + +### alr 2.0.0 + index 1.3.0 + +- index:`binary` property required in binary origins. +- index: Paths in `[environment]` must be portable (using forward slashes). +- alr: removed `alr toolchain`'s `--install, --uninstall, --install-dir`. +- alr: deprecated (but still working) `ALR_CONFIG`, `alr config`. ### alr 1.2.2 + index 1.2.1 -- Unable to load externals containing regex special characters in the system +- alr: unable to load externals containing regex special characters in the system package name (fixed in #1545). -- Paths in `[environment]` are not converted to the native platform convention. +- alr: paths in `[environment]` are not converted to the native platform convention. From 19d35bea0d3294637f8ce9ce630f2d52378a2722 Mon Sep 17 00:00:00 2001 From: Alejandro R Mosteo Date: Thu, 14 Mar 2024 12:11:12 +0100 Subject: [PATCH 03/53] Alire.Utils.Regex: initialize matches explicitly (#1637) --- src/alire/alire-utils-regex.adb | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/alire/alire-utils-regex.adb b/src/alire/alire-utils-regex.adb index 903864bfc..aee4a5086 100644 --- a/src/alire/alire-utils-regex.adb +++ b/src/alire/alire-utils-regex.adb @@ -44,8 +44,10 @@ package body Alire.Utils.Regex is end Count_Parentheses; use GNAT.Regpat; - Matches : Match_Array (1 .. Count_Parentheses); - -- This is a safe estimation, as some '(' may not be part of a capture + Matches : Match_Array (1 .. Count_Parentheses) := (others => No_Match); + -- This is a safe estimation, as some '(' may not be part of a capture. + -- Initialization added just in case given the discussion at + -- https://forum.ada-lang.io/t/regpat-bug-or-handling-error/705/5 begin Match (Regex, Text, Matches); From e6e1021b931f61b41ae3d4ac9a8ab5078cfc9447 Mon Sep 17 00:00:00 2001 From: Alejandro R Mosteo Date: Thu, 14 Mar 2024 12:30:21 +0100 Subject: [PATCH 04/53] Remove unused entity (#1636) --- src/alire/alire-paths.ads | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/alire/alire-paths.ads b/src/alire/alire-paths.ads index 5b4a06824..6019836bb 100644 --- a/src/alire/alire-paths.ads +++ b/src/alire/alire-paths.ads @@ -27,9 +27,6 @@ package Alire.Paths with Preelaborate is Scripts_Graph_Easy : constant String := "graph-easy"; -- Script for ASCII graphs - Complete_Flag : constant String := "complete_copy"; - -- We use /alire/ to mark that a file operation was completed - private Crate_File_Extension_With_Dot : constant String := ".toml"; From 1c4f7c54b4c2677b3bde335b309ed9ff59a93afa Mon Sep 17 00:00:00 2001 From: Alejandro R Mosteo Date: Fri, 15 Mar 2024 10:16:30 +0100 Subject: [PATCH 05/53] Remove use of GNAT Community Edition (#1641) setup-alire@v3 no longer uses GNAT CE so we can always test with GNAT FSF. --- .github/workflows/ci-community.yml | 81 ------------------------------ .github/workflows/nightly.yml | 8 +-- 2 files changed, 1 insertion(+), 88 deletions(-) delete mode 100644 .github/workflows/ci-community.yml diff --git a/.github/workflows/ci-community.yml b/.github/workflows/ci-community.yml deleted file mode 100644 index ca4028e04..000000000 --- a/.github/workflows/ci-community.yml +++ /dev/null @@ -1,81 +0,0 @@ -name: CI Community -# Check proper build using the community edition of the AdaCore toolchain - -on: - pull_request: - paths-ignore: - - 'doc/**' - - '**.md' - - '**.rst' - - '**.txt' - workflow_dispatch: - -jobs: - - build: - name: ce${{matrix.version}} on ${{ matrix.os }} - - runs-on: ${{ matrix.os }} - - strategy: - fail-fast: false # Attempt to generate as many of them as possible - matrix: - os: - - macos-latest - - ubuntu-latest - - windows-latest - version: - - 2020 - - 2021 - exclude: - - os: macos-latest - version: 2021 # it was never released for macOS - - steps: - - name: Check out repository - uses: actions/checkout@v2 - with: - submodules: true - - # Until some stable alr with `alr install` is available, we cannot rely on - # the alr-install action, as that introduces a circular dependency. If a - # nightly build were to fail, there's no way to do an `alr install` anymore - # TODO: replace with `alr-install` once alr 2.0 is out. - - # We cannot use variable names in the action reference, so we need to make - # them explicit twice - - name: Install Community 2020 toolchain - uses: ada-actions/toolchain@ce2020 - if: ${{ matrix.version == '2020' }} - with: - distrib: community - - - name: Install Community 2021 toolchain - uses: ada-actions/toolchain@ce2021 - if: ${{ matrix.version == '2021' }} - with: - distrib: community - - - name: Install Python 3.x (required for the testsuite) - uses: actions/setup-python@v2 - with: - python-version: '3.x' - - - name: Run test script - run: scripts/ci-github.sh - shell: bash - - - name: Upload logs (if failed) - if: failure() - uses: actions/upload-artifact@master - with: - name: e3-log-linux.zip - path: testsuite/out - - - name: Upload artifact - uses: actions/upload-artifact@v2 - with: - name: alr-bin-${{ matrix.os }}.zip - path: | - bin/alr* - LICENSE.txt \ No newline at end of file diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index a82ddfd01..f90be8deb 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -31,14 +31,8 @@ jobs: with: submodules: true - # Until some stable alr with `alr install` is available, we cannot rely on - # the alr-install action, as that introduces a circular dependency. If a - # nightly build were to fail, there's no way to do an `alr install` anymore - # TODO: replace with `alr-install` once alr 2.0 is out. - name: Install FSF toolchain - uses: ada-actions/toolchain@ce2020 - with: - distrib: community + uses: alire-project/setup-alire@v3 - name: Install Python 3.x (required for the testsuite) uses: actions/setup-python@v2 From 69dbb85181cc9883dab8f770a670058b58c9257d Mon Sep 17 00:00:00 2001 From: Alejandro R Mosteo Date: Fri, 15 Mar 2024 10:16:54 +0100 Subject: [PATCH 06/53] Alire.Features: restore proper config deprecation (#1640) --- src/alire/alire-features.ads | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/alire/alire-features.ads b/src/alire/alire-features.ads index c55661baf..0afd82253 100644 --- a/src/alire/alire-features.ads +++ b/src/alire/alire-features.ads @@ -10,7 +10,7 @@ package Alire.Features is use type Min_Version; - Config_Deprecated : constant On_Version := +"1.0"; + Config_Deprecated : constant On_Version := +"3.0"; -- We migrate ALR_CONFIG to ALIRE_SETTINGS_DIR, but allow the use of the -- former with a warning during our next major release to ease transition. -- Likewise for the -c/--config switch From 74ed1821862dce7d21535dec0310d7f2ca2c52b5 Mon Sep 17 00:00:00 2001 From: Fabien Chouteau Date: Fri, 15 Mar 2024 10:18:28 +0100 Subject: [PATCH 07/53] Enforce usage of Sementic_Versioning when printing Alire/Alr version (#1643) * Enforce usage of Sementic_Versioning when printing Alire/Alr version To ensure a consistent formatting whatever the version. The version string is now private, only the semver type is visible. * Enforce usage of Sementic_Versioning for index versions --- src/alire/alire-github.adb | 4 +-- src/alire/alire-index.ads | 39 +++++++++++++++++++-------- src/alire/alire-publish-submit.adb | 2 +- src/alire/alire-settings-edit.adb | 4 +-- src/alire/alire-toml_index.adb | 5 ++-- src/alire/alire-version-semver.ads | 14 ---------- src/alire/alire-version.ads | 16 +++++++++-- src/alire/alire_early_elaboration.adb | 4 +-- src/alr/alr-commands-version.adb | 8 +++--- src/alr/alr-commands.adb | 3 +-- src/alr/alr-commands.ads | 2 +- 11 files changed, 58 insertions(+), 43 deletions(-) delete mode 100644 src/alire/alire-version-semver.ads diff --git a/src/alire/alire-github.adb b/src/alire/alire-github.adb index 20da5062d..e9ab5f209 100644 --- a/src/alire/alire-github.adb +++ b/src/alire/alire-github.adb @@ -304,7 +304,7 @@ package body Alire.GitHub is Args => "state" = "closed"); begin Comment (Number, - "Closed using `alr " & Version.Current & "` with reason: " + "Closed using `alr " & Version.Current.Image & "` with reason: " & Reason); end Close; @@ -423,7 +423,7 @@ package body Alire.GitHub is Mutation : constant String := "mutation { markPullRequestReadyForReview (input: { " - & "clientMutationId: ""alr-" & Version.Current & """, " + & "clientMutationId: ""alr-" & Version.Current.Image & """, " & "pullRequestId: ""PRID"" }) {clientMutationId}}"; Response : constant Minirest.Response diff --git a/src/alire/alire-index.ads b/src/alire/alire-index.ads index 2e581d033..485dc5501 100644 --- a/src/alire/alire-index.ads +++ b/src/alire/alire-index.ads @@ -49,21 +49,14 @@ package Alire.Index is -- The branch used for the community index. Must be updated when new index -- features are introduced. - Min_Compatible_Version : constant String := "1.1"; - -- Update as needed in case of backward-incompatible changes - - Max_Compatible_Version : constant String := - AAA.Strings.Tail (Community_Branch, '-'); + Min_Compatible_Version : constant Semantic_Versioning.Version; + -- Based on the constant defined in private section -- We store here the indexes we are able to load. As long as we do not -- break back compatibility, we can keep on simply updating the minor value - Valid_Versions : constant Semantic_Versioning.Extended.Version_Set := - Semantic_Versioning.Extended.Value - ("^" & Min_Compatible_Version - & " & <=" & Max_Compatible_Version); + Valid_Versions : constant Semantic_Versioning.Extended.Version_Set; - Version : constant Semantic_Versioning.Version := - Semantic_Versioning.New_Version (Max_Compatible_Version); + Version : constant Semantic_Versioning.Version; -- The index version understood by alire must match the one in the indexes -- being loaded. @@ -173,4 +166,28 @@ package Alire.Index is -- applied during load: -- * Whether some origin is not in our allowed hosting sites. +private + + -- The string constants for versions are kept in the private section to + -- avoid using them in command output or other message. The only option + -- is to use the Sementic_Versioning.Image function which will provide a + -- consistant output. + + Min_Compatible_Version_Str : constant String := "1.1"; + -- Update as needed in case of backward-incompatible changes + + Max_Compatible_Version_Str : constant String := + AAA.Strings.Tail (Community_Branch, '-'); + + Min_Compatible_Version : constant Semantic_Versioning.Version := + Semantic_Versioning.New_Version (Min_Compatible_Version_Str); + + Valid_Versions : constant Semantic_Versioning.Extended.Version_Set := + Semantic_Versioning.Extended.Value + ("^" & Min_Compatible_Version_Str + & " & <=" & Max_Compatible_Version_Str); + + Version : constant Semantic_Versioning.Version := + Semantic_Versioning.New_Version (Max_Compatible_Version_Str); + end Alire.Index; diff --git a/src/alire/alire-publish-submit.adb b/src/alire/alire-publish-submit.adb index 27cae24a8..122a5ec0e 100644 --- a/src/alire/alire-publish-submit.adb +++ b/src/alire/alire-publish-submit.adb @@ -386,7 +386,7 @@ package body Alire.Publish.Submit is Title => Context.PR_Name, Message => "Created via `alr publish` with `alr " - & Version.Current & "`"); + & Version.Current.Image & "`"); begin Put_Success ("Pull request created successfully"); Put_Info ("Visit " & TTY.URL (States.Webpage (Number)) diff --git a/src/alire/alire-settings-edit.adb b/src/alire/alire-settings-edit.adb index 0962c1345..61e5a9c3e 100644 --- a/src/alire/alire-settings-edit.adb +++ b/src/alire/alire-settings-edit.adb @@ -8,7 +8,7 @@ with Alire.Platforms.Folders; with Alire.Platforms.Current; with Alire.Settings.Builtins; with Alire.Utils.Text_Files; -with Alire.Version.Semver; +with Alire.Version; with Alire.Warnings; with CLIC.Config.Edit; @@ -224,7 +224,7 @@ package body Alire.Settings.Edit is begin -- Warn or fail depending on version if OS_Lib.Getenv (Environment.Config, Unset) /= Unset then - if Version.Semver.Current < Features.Config_Deprecated then + if Version.Current < Features.Config_Deprecated then Warnings.Warn_Once (Msg, Level => Warning); else Raise_Checked_Error (Msg); diff --git a/src/alire/alire-toml_index.adb b/src/alire/alire-toml_index.adb index 07c3aac60..b60e714b0 100644 --- a/src/alire/alire-toml_index.adb +++ b/src/alire/alire-toml_index.adb @@ -203,14 +203,13 @@ package body Alire.TOML_Index is & ") is newer than that expected by alr (" & Alire.Index.Version.Image & ")." & " You may have to update alr"); - elsif Loading_Index_Version < Semver.Parse - (Alire.Index.Min_Compatible_Version) + elsif Loading_Index_Version < Alire.Index.Min_Compatible_Version then Set_Error (Result, Filename, "index version (" & Loading_Index_Version.Image & ") is too old. The minimum compatible version is " - & Alire.Index.Min_Compatible_Version & ASCII.LF + & Alire.Index.Min_Compatible_Version.Image & ASCII.LF & (if Index.Name = Alire.Index.Community_Name then " Resetting the community index (" & TTY.Terminal ("alr index --reset-community") diff --git a/src/alire/alire-version-semver.ads b/src/alire/alire-version-semver.ads deleted file mode 100644 index aeead5a9d..000000000 --- a/src/alire/alire-version-semver.ads +++ /dev/null @@ -1,14 +0,0 @@ -with Semantic_Versioning; - -package Alire.Version.Semver is - - -- Convenience to be able to use the current version directly as a - -- comparable proper semantic version version. - - package Semver renames Semantic_Versioning; - - subtype Version is Semver.Version; - - Current : constant Version := Semver.New_Version (Alire.Version.Current); - -end Alire.Version.Semver; diff --git a/src/alire/alire-version.ads b/src/alire/alire-version.ads index 8c471d642..6a0e4f70d 100644 --- a/src/alire/alire-version.ads +++ b/src/alire/alire-version.ads @@ -1,8 +1,18 @@ -package Alire.Version with Preelaborate is +with Semantic_Versioning; + +package Alire.Version is + + package Semver renames Semantic_Versioning; + + subtype Version is Semver.Version; + + Current : constant Version; + +private -- Remember to update Alire.Index branch if needed too - Current : constant String := "2.1-dev"; + Current_Str : constant String := "2.1-dev"; -- 2.0.0: alr settings refactor and minor fixes -- 2.0.0-rc1: release candidate for 2.0 -- 2.0.0-b1: first public release on the 2.0 branch @@ -23,4 +33,6 @@ package Alire.Version with Preelaborate is -- 0.8.1-dev: update to devel-0.5 index branch -- 0.8.0-dev: post-0.7-beta changes + Current : constant Version := Semver.New_Version (Current_Str); + end Alire.Version; diff --git a/src/alire/alire_early_elaboration.adb b/src/alire/alire_early_elaboration.adb index d819982ca..0f92508fd 100644 --- a/src/alire/alire_early_elaboration.adb +++ b/src/alire/alire_early_elaboration.adb @@ -4,7 +4,7 @@ with Ada.Directories; with Alire.Features; with Alire.Settings.Edit.Early_Load; -with Alire.Version.Semver; +with Alire.Version; with GNAT.Command_Line; with GNAT.OS_Lib; @@ -110,7 +110,7 @@ package body Alire_Early_Elaboration is use type Alire.Version.Semver.Version; Config_Deprecated : constant Boolean - := Alire.Version.Semver.Current >= Alire.Features.Config_Deprecated; + := Alire.Version.Current >= Alire.Features.Config_Deprecated; procedure Check_Config_Deprecated is begin diff --git a/src/alr/alr-commands-version.adb b/src/alr/alr-commands-version.adb index bd145df5e..f4fd73a32 100644 --- a/src/alr/alr-commands-version.adb +++ b/src/alr/alr-commands-version.adb @@ -11,6 +11,7 @@ with Alire.Properties; with Alire.Roots.Optional; with Alire.Toolchains; with Alire.Utils.Tables; +with Alire.Version; with Alr.Bootstrap; @@ -61,9 +62,10 @@ package body Alr.Commands.Version is end if; Table.Append ("APPLICATION").Append ("").New_Row; - Table.Append ("alr version:").Append (Alire.Version.Current).New_Row; + Table.Append ("alr version:") + .Append (Alire.Version.Current.Image).New_Row; Table.Append ("libalire version:") - .Append (Alire.Version.Current).New_Row; + .Append (Alire.Version.Current.Image).New_Row; Table.Append ("compilation date:") .Append (GNAT.Source_Info.Compilation_ISO_Date & " " & GNAT.Source_Info.Compilation_Time).New_Row; @@ -193,7 +195,7 @@ package body Alr.Commands.Version is procedure Print_Version is begin - Trace.Always ("alr " & Alire.Version.Current); + Trace.Always ("alr " & Alire.Version.Current.Image); end Print_Version; end Alr.Commands.Version; diff --git a/src/alr/alr-commands.adb b/src/alr/alr-commands.adb index 64d9155f5..ef7584827 100644 --- a/src/alr/alr-commands.adb +++ b/src/alr/alr-commands.adb @@ -20,7 +20,6 @@ with Alire.Platforms.Current; with Alire.Root; with Alire.Solutions; with Alire.Toolchains; -with Alire.Version.Semver; with Alr.Commands.Action; with Alr.Commands.Build; @@ -143,7 +142,7 @@ package body Alr.Commands is use CLIC.Subcommand; use type Alire.Version.Semver.Version; begin - if Alire.Version.Semver.Current < Features.Config_Deprecated then + if Alire.Version.Current < Features.Config_Deprecated then Define_Switch (Config, Command_Line_Config_Path'Access, "-c=", "--config=", diff --git a/src/alr/alr-commands.ads b/src/alr/alr-commands.ads index 54cfcf24a..824db39f1 100644 --- a/src/alr/alr-commands.ads +++ b/src/alr/alr-commands.ads @@ -139,7 +139,7 @@ private package Sub_Cmd is new CLIC.Subcommand.Instance (Main_Command_Name => "alr", - Version => Alire.Version.Current, + Version => Alire.Version.Current.Image, Put => GNAT.IO.Put, Put_Line => GNAT.IO.Put_Line, Put_Error => Put_Error, From 5845cc26e64eef2f1d09cbf3e71a62942485e698 Mon Sep 17 00:00:00 2001 From: Fabien Chouteau Date: Fri, 15 Mar 2024 10:19:21 +0100 Subject: [PATCH 08/53] Move `upgrading.md` to `doc/` dir to make it available on the website (#1645) --- UPGRADING.md => doc/upgrading.md | 2 ++ 1 file changed, 2 insertions(+) rename UPGRADING.md => doc/upgrading.md (99%) diff --git a/UPGRADING.md b/doc/upgrading.md similarity index 99% rename from UPGRADING.md rename to doc/upgrading.md index 9f796b8be..72dcd2bb2 100644 --- a/UPGRADING.md +++ b/doc/upgrading.md @@ -1,3 +1,5 @@ +# Upgrading + ## Upgrading from 1.x to 2.x There are no special preparations to be made in advance to upgrading. However, From f05472637e0277356230e1b667c0554a57a125d6 Mon Sep 17 00:00:00 2001 From: Alejandro R Mosteo Date: Fri, 15 Mar 2024 14:09:28 +0100 Subject: [PATCH 09/53] QR pointing to alire.ada.dev (#1649) --- resources/alr-qr-code-gradient.png | Bin 0 -> 106767 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 resources/alr-qr-code-gradient.png diff --git a/resources/alr-qr-code-gradient.png b/resources/alr-qr-code-gradient.png new file mode 100644 index 0000000000000000000000000000000000000000..a3a33abda505938b87b06071f5a110fc08a802be GIT binary patch literal 106767 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

{<{`H&gT+2{5NuUXU96 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≧^Yq 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 adipc2*6Q2yFVO0yJ~}1 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%0Av|{@-U3xHk85qQB1nwp)i& zN(0}~u%4?lju5=1VFd$%*gz_tFW*kr2?8@+OAdqciSgw2VjtMdFFFbXp95Ee6$Vf6 zRa=1fll{9(PZ|)&W!nCnqO)3(rBom0;WdTV;~!-#^do0vjNfk2;h=wl`M(Tv}+xap-a6^*S53rG++G_Q#HRS-o1Z*evhPkCXm& zlk(rlre0p|{~ve){N}{NY)KM8XK*F@tXT!Re*n1c;wA8;+Q-0%xiC#H_Se?+wYf*T zM1kS=_E7qSr5{h4qnf-b3RvMd1BVd4*Z=?i``zyR%ejsUz#U@yfXlY!Yrh17O+N)R z-E9A_tLyE4-#ouAa&ud()JDJMpBM7izR3UoEni$W>PkuUg`gU?NU8ZX$~N;#VYywo z^XeN=KN5ISn_13{3nk70`+?pDE`?qnzq1H9o#eQ>@QHTsKQUxCrKw-b0lN70_4WIK zN#fqWZ`;l9id}gIJWBB===`P+hxvs;DJ_8e*b%nWq@+i{&c;1Z6%QQD+4XYUf<3C< z(?B;a)PGB_7rUQcBq3netfRXxJhG-gum(BZn-u`9`2T78{y+Zzzxcm-^|0uYr+V$H z;Q3aMdyF4hvn4D5rhc*e*}D6VYuMSmzSVzxg-M_LTX!MuCnweC@A(K!tNZ_c1J<5E z=j}K%+x-2T&FBBUaJR2|v+=muT`?X|QFHOWQw56FSHS(>yZ-`50(U;0)-G4I;^j7n z(np=@aljiR>OYFt9~2i_z=cI$_Oasgw&kEo1lYH_y&;jA-*I!F-LDtG(~&;k0ZIWA zCos_^9l%K49!b2Q6NiA+$Gd9adFcOszrVjR*&SFVi#~q*7Dv7 zSe%2-)_TAByq)iMj>1P+%rLvPxB5FM>Vf;RKD=sc_&96KiEv6f_<^R9m=68d7l(2+0 zGcb8ITv%p$7f3TONLT_V7=VDIR|QmNGb{v7)&YTn$s|z5V`u?R^8=}d3&8nFAP|u9 z1Qi<$PQXcVAmxxTs(EMx*z2Gt?Z61r@N{tuAsB7n{xe#AmfKm(rzZu9Y)@A|mvv4F FO#tq_E8zeD literal 0 HcmV?d00001 From 88a1194648d7bd4da9ff645666e46f19a11c844f Mon Sep 17 00:00:00 2001 From: Alejandro R Mosteo Date: Mon, 18 Mar 2024 13:00:56 +0100 Subject: [PATCH 10/53] Update msys2 installer (#1648) * Update msys2 installer * Ensure msys2 from commit is tested --- .github/workflows/ci-windows.yml | 7 +++++++ RELEASING.md | 1 + .../os_windows/alire-settings-builtins-windows.ads | 11 +++++++---- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci-windows.yml b/.github/workflows/ci-windows.yml index d0258d925..a909227d7 100644 --- a/.github/workflows/ci-windows.yml +++ b/.github/workflows/ci-windows.yml @@ -13,6 +13,7 @@ on: env: ALIRE_OS: windows + MSYS64_ROOT: C:\Users\runneradmin\AppData\Local\alire\cache\msys64 MINGW64_PATH: C:\Users\runneradmin\AppData\Local\alire\cache\msys64\mingw64\bin MSYS2_PATH: C:\Users\runneradmin\AppData\Local\alire\cache\msys64\usr\bin PACMAN: C:\Users\runneradmin\AppData\Local\alire\cache\msys64\usr\bin\pacman --noconfirm @@ -38,6 +39,12 @@ jobs: - name: Build alr run: gprbuild -j0 -p -P alr_env + - name: Remove previous alr's msys2 (so a new one can be tested) + shell: pwsh + run: | + if (Test-Path "${{env.MSYS64_ROOT}}") { + Remove-Item "${{env.MSYS64_ROOT}}" -Recurse -Force } + - name: Display built alr and trigger install of msys2 run: ./bin/alr version diff --git a/RELEASING.md b/RELEASING.md index eb3a31357..8531e44e4 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -1,5 +1,6 @@ ## Checklist for releasing a new version +1. [ ] Update Msys2 installer at https://github.com/msys2/msys2-installer/releases/ 1. [ ] Run local-only tests (`/testsuite/run-dev-sh`) 1. [ ] Update versions - `Alire.Version` diff --git a/src/alire/os_windows/alire-settings-builtins-windows.ads b/src/alire/os_windows/alire-settings-builtins-windows.ads index 19d4f4c86..cc27adc83 100644 --- a/src/alire/os_windows/alire-settings-builtins-windows.ads +++ b/src/alire/os_windows/alire-settings-builtins-windows.ads @@ -4,10 +4,13 @@ pragma Unreferenced (Alire.Settings.Edit.Early_Load); package Alire.Settings.Builtins.Windows is - Default_Msys2_Installer : constant String := "msys2-x86_64-20221216.exe"; - Default_Msys2_Installer_URL : constant String := - "https://github.com/msys2/msys2-installer/releases/download/2022-12-16/" - & Default_Msys2_Installer; + pragma Style_Checks ("M200"); + Default_Msys2_Installer_URL : constant String + := "https://github.com/msys2/msys2-installer/releases/download/2024-01-13/msys2-x86_64-20240113.exe"; + pragma Style_Checks ("M80"); + + Default_Msys2_Installer : constant String + := AAA.Strings.Split (Default_Msys2_Installer_URL, '/').Last_Element; -- MSYS2 From 3ada2e2d4bd3fe51f13d1bde4624efaadb8535b2 Mon Sep 17 00:00:00 2001 From: Alejandro R Mosteo Date: Mon, 18 Mar 2024 13:01:16 +0100 Subject: [PATCH 11/53] Use oldest Ubuntu LTS for releases (#1647) --- .github/workflows/ci-linux.yml | 2 +- .github/workflows/nightly.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci-linux.yml b/.github/workflows/ci-linux.yml index 69fb61380..7992d2199 100644 --- a/.github/workflows/ci-linux.yml +++ b/.github/workflows/ci-linux.yml @@ -22,7 +22,7 @@ jobs: build: name: CI on Linux - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - name: Check out repository diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index f90be8deb..d6c4b9b16 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -22,7 +22,7 @@ jobs: matrix: os: - macos-latest - - ubuntu-latest + - ubuntu-20.04 - windows-latest steps: From 2b663e87bdc3d4ba808149bad97bac5c60889b6b Mon Sep 17 00:00:00 2001 From: tali auster <120901234+atalii@users.noreply.github.com> Date: Mon, 18 Mar 2024 10:12:16 -0600 Subject: [PATCH 12/53] dev/build.sh: control build jobs with $ALIRE_BUILD_JOBS (#1651) This is just a minor quality-of-life improvement for packaging. Note that this also quotes the argument to echo so that if (somehow) $ALIRE_OS were set to a malicious value, no harm could be done. --- dev/build.sh | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/dev/build.sh b/dev/build.sh index d5b035a5e..79e79e15d 100755 --- a/dev/build.sh +++ b/dev/build.sh @@ -5,7 +5,8 @@ pushd $( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) > /de . functions.sh popd > /dev/null +ALIRE_BUILD_JOBS="${ALIRE_BUILD_JOBS:-0}" export ALIRE_OS=$(get_OS) -echo Building with ALIRE_OS=$ALIRE_OS... -gprbuild -j0 -r -p -P `dirname $0`/../alr_env.gpr "$@" +echo "Building with ALIRE_OS=$ALIRE_OS..." +gprbuild "-j$ALIRE_BUILD_JOBS" -r -p -P `dirname $0`/../alr_env.gpr "$@" From 8aaefabc5777ce6ebeba8adec761cbc40a171de8 Mon Sep 17 00:00:00 2001 From: Francesc Rocher Date: Mon, 18 Mar 2024 18:59:49 +0100 Subject: [PATCH 13/53] Rename switch arguments to indicate expected type (#1650) In commands with options that admit arguments, like '--prefix' in 'alr install', is useful to provide an argument name in the command help that gives a hint of the expected type. For example, 'alr install -h' shows the message: ==== OPTIONS --prefix=ARG Override installation prefix (default is ...) ==== When changed to: ==== OPTIONS --prefix=DIR Override installation prefix (default is ...) ==== the 'DIR' name of the argument immediately says what is expected in this option. In cases where this is not so obvious, it helps even more. For example, in 'alr build -h': ==== OPTIONS --profiles=LIST Comma-separated list of = values (see description) --stop-after=STAGE Build stage after which to stop (see description) ==== is a quick remainder of what is expected in that options. --- src/alr/alr-commands-build.adb | 6 ++++-- src/alr/alr-commands-edit.adb | 3 ++- src/alr/alr-commands-exec.adb | 3 ++- src/alr/alr-commands-install.adb | 3 ++- src/alr/alr-commands-publish.adb | 3 ++- src/alr/alr-commands-show.adb | 4 ++-- src/alr/alr-commands.adb | 6 ++++-- 7 files changed, 18 insertions(+), 10 deletions(-) diff --git a/src/alr/alr-commands-build.adb b/src/alr/alr-commands-build.adb index 92c8afe28..0e555e58a 100644 --- a/src/alr/alr-commands-build.adb +++ b/src/alr/alr-commands-build.adb @@ -273,13 +273,15 @@ package body Alr.Commands.Build is (Config, Cmd.Profiles'Access, "", Switch_Profiles & "=", - "Comma-separated list of = values (see description)"); + "Comma-separated list of = values (see description)", + Argument => "LIST"); Define_Switch (Config, Cmd.Stop_After'Access, "", Switch_Stop & "=", - "Build stage after which to stop (see description)"); + "Build stage after which to stop (see description)", + Argument => "STAGE"); end Setup_Switches; diff --git a/src/alr/alr-commands-edit.adb b/src/alr/alr-commands-edit.adb index 36758d15a..dbfc1a579 100644 --- a/src/alr/alr-commands-edit.adb +++ b/src/alr/alr-commands-edit.adb @@ -276,7 +276,8 @@ package body Alr.Commands.Edit is Cmd.Prj'Access, "", "--project=", "Select the project file to open if the crate " & - "provides multiple project files, ignored otherwise"); + "provides multiple project files, ignored otherwise", + Argument => "FILE"); Define_Switch (Config, Cmd.Set'Access, "", "--select-editor", diff --git a/src/alr/alr-commands-exec.adb b/src/alr/alr-commands-exec.adb index cf50a2bc2..73ffc9ca4 100644 --- a/src/alr/alr-commands-exec.adb +++ b/src/alr/alr-commands-exec.adb @@ -123,7 +123,8 @@ package body Alr.Commands.Exec is (Config, Cmd.Prj'Access, Switch => "-P?", - Help => "Add ""-P "" to the command switches"); + Help => "Add ""-P "" to the command switches", + Argument => "NUM"); end Setup_Switches; end Alr.Commands.Exec; diff --git a/src/alr/alr-commands-install.adb b/src/alr/alr-commands-install.adb index 974885a6e..2f530135c 100644 --- a/src/alr/alr-commands-install.adb +++ b/src/alr/alr-commands-install.adb @@ -141,7 +141,8 @@ package body Alr.Commands.Install is Cmd.Prefix'Access, "", "--prefix=", "Override installation prefix (default is " - & TTY.URL (Alire.Install.Default_Prefix) & ")"); + & TTY.URL (Alire.Install.Default_Prefix) & ")", + Argument => "DIR"); Define_Switch (Config, Cmd.Info'Access, diff --git a/src/alr/alr-commands-publish.adb b/src/alr/alr-commands-publish.adb index 67c3455d9..018669eb5 100644 --- a/src/alr/alr-commands-publish.adb +++ b/src/alr/alr-commands-publish.adb @@ -151,7 +151,8 @@ package body Alr.Commands.Publish is (Config, Cmd.Manifest'Access, "", "--manifest=", - "Selects a manifest file other than ./alire.toml"); + "Selects a manifest file other than ./alire.toml", + Argument => "FILE"); Define_Switch (Config, diff --git a/src/alr/alr-commands-show.adb b/src/alr/alr-commands-show.adb index e52f38722..9102f7c96 100644 --- a/src/alr/alr-commands-show.adb +++ b/src/alr/alr-commands-show.adb @@ -364,8 +364,8 @@ package body Alr.Commands.Show is Define_Switch (Config, Cmd.Dependents'Access, "", "--dependents?", - "Show dependent crates (ARG=direct|shortest|all)", - Argument => "=ARG"); + "Show dependent crates (WHICH=direct|shortest|all)", + Argument => "=WHICH"); Define_Switch (Config, Cmd.Detail'Access, diff --git a/src/alr/alr-commands.adb b/src/alr/alr-commands.adb index ef7584827..43a50e3fa 100644 --- a/src/alr/alr-commands.adb +++ b/src/alr/alr-commands.adb @@ -153,12 +153,14 @@ package body Alr.Commands is Define_Switch (Config, Command_Line_Config_Path'Access, "-s=", "--settings=", - "Override settings folder location"); + "Override settings folder location", + Argument => "DIR"); Define_Switch (Config, Command_Line_Chdir_Target_Path'Access, "-C=", "--chdir=", - "Run `alr` in the given directory"); + "Run `alr` in the given directory", + Argument => "DIR"); Define_Switch (Config, Alire.Force'Access, From 0fa9239ac94b80b4fa38afa41c9f0950a9526214 Mon Sep 17 00:00:00 2001 From: Alejandro R Mosteo Date: Wed, 20 Mar 2024 12:14:43 +0100 Subject: [PATCH 14/53] Fix installation of binary crates containing softlinks (#1653) * Force creation of missing soft link * More detailed test of softlink installation --- src/alire/alire-directories.adb | 107 ++++++++++-------- src/alire/alire-directories.ads | 4 + .../softlinks/my_index/crate-0.1.0.tgz | Bin 349 -> 20480 bytes .../my_index/index/cr/crate/crate-0.1.0.toml | 2 +- testsuite/tests/install/softlinks/test.py | 62 +++++++++- 5 files changed, 124 insertions(+), 51 deletions(-) diff --git a/src/alire/alire-directories.adb b/src/alire/alire-directories.adb index 5d7905394..ec7741ed1 100644 --- a/src/alire/alire-directories.adb +++ b/src/alire/alire-directories.adb @@ -136,6 +136,28 @@ package body Alire.Directories is End_Search (Search); end Copy; + --------------- + -- Copy_Link -- + --------------- + + procedure Copy_Link (Src, Dst : Any_Path) is + use AAA.Strings; + use all type Platforms.Operating_Systems; + Keep_Links : constant String + := (case Platforms.Current.Operating_System is + when Linux => "-d", + when FreeBSD | MacOS => "-R", + when others => + raise Program_Error with "Unsupported operation"); + begin + -- Given that we are here because Src is indeed a link, we should be in + -- a Unix-like platform able to do this. + OS_Lib.Subprocess.Checked_Spawn + ("cp", + To_Vector (Keep_Links) + & Src & Dst); + end Copy_Link; + ----------------- -- Create_Tree -- ----------------- @@ -812,9 +834,6 @@ package body Alire.Directories is and then Base = Parent (Src) then Trace.Debug (" Merge: Not merging top-level file " & Src); - if Remove_From_Source then - Adirs.Delete_File (Src); - end if; return; end if; @@ -831,20 +850,19 @@ package body Alire.Directories is -- recursion we could more efficiently rename now into place. end if; - -- Copy/Move a file into place + -- Copy file into place - Trace.Debug (" Merge: " - & (if Remove_From_Source then " moving " else " copying ") + Trace.Debug (" Merge: copying " & Adirs.Full_Name (Item) & " into " & Dst); if Adirs.Exists (Dst) then if Fail_On_Existing_File then - Recoverable_User_Error ("Cannot move " & TTY.URL (Src) + Recoverable_User_Error ("Cannot copy " & TTY.URL (Src) & " into place, file already exists: " & TTY.URL (Dst)); elsif Adirs.Kind (Dst) /= Ordinary_File then - Raise_Checked_Error ("Cannot replace " & TTY.URL (Dst) + Raise_Checked_Error ("Cannot overwrite " & TTY.URL (Dst) & " as it is not a regular file"); else Trace.Debug (" Merge: Deleting in preparation to replace: " @@ -853,56 +871,51 @@ package body Alire.Directories is end if; end if; - -- We use GNATCOLL.VFS here as some binary packages contain softlinks + -- We use GNAT.OS_Lib here as some binary packages contain softlinks -- to .so libs that we must copy too, and these are troublesome -- with regular Ada.Directories (that has no concept of softlink). -- Also, some of these softlinks are broken and although they are -- presumably safe to discard, let's just go for an identical copy. - declare - VF : constant VFS.Virtual_File := - VFS.New_Virtual_File (VFS.From_FS (Src)); - OK : Boolean := False; - begin - if VF.Is_Symbolic_Link then - if Remove_From_Source then - VF.Rename (VFS.New_Virtual_File (Dst), OK); - else - VF.Copy (VFS.Filesystem_String (Dst), OK); - end if; - if not OK then - Raise_Checked_Error ("Failed to copy/move softlink: " - & TTY.URL (Src)); - end if; - else - begin - if Remove_From_Source then - Adirs.Rename (Old_Name => Src, - New_Name => Dst); - else - Adirs.Copy_File (Source_Name => Src, - Target_Name => Dst, - Form => "preserve=all_attributes"); - end if; - exception - when E : others => - Trace.Error - ("When " & - (if Remove_From_Source - then "renaming " - else "copying ") - & Src & " --> " & Dst & ": "); - Log_Exception (E, Error); - raise; - end; + if GNAT.OS_Lib.Is_Symbolic_Link (Src) then + Trace.Debug (" Merge (softlink): " & Src); + + Copy_Link (Src, Dst); + if not GNAT.OS_Lib.Is_Symbolic_Link (Dst) then + Raise_Checked_Error ("Failed to copy softlink: " + & TTY.URL (Src) + & " to " & TTY.URL (Dst) + & " (dst not a link)"); end if; - end; + else + begin + Adirs.Copy_File (Source_Name => Src, + Target_Name => Dst, + Form => "preserve=all_attributes"); + exception + when E : others => + Trace.Error + ("When copying " & Src & " --> " & Dst & ": "); + Log_Exception (E, Error); + raise; + end; + end if; end Merge; begin Traverse_Tree (Start => Src, Doing => Merge'Access, Recurse => True); + + -- This is space-inefficient since we use 2x the actual size, but this + -- is the only way we have unless we want to go into platform-dependent + -- details and radical changes due to softlinks . + + -- TODO: remove this limitation on a non-patch release. + + if Remove_From_Source then + Force_Delete (Src); + end if; end Merge_Contents; ------------------- @@ -988,7 +1001,7 @@ package body Alire.Directories is if not Prune and then Recurse and then Kind (Item) = Directory then declare - Normal_Name : constant String + Normal_Name : constant Absolute_Path := String (GNATCOLL.VFS.Full_Name (VFS.New_Virtual_File (Full_Name (Item)), diff --git a/src/alire/alire-directories.ads b/src/alire/alire-directories.ads index 64af2cd39..bbf513751 100644 --- a/src/alire/alire-directories.ads +++ b/src/alire/alire-directories.ads @@ -32,6 +32,10 @@ package Alire.Directories is -- equivalent to "cp -r src/* dst/". Excluding may be a single name that -- will not be copied (if file) or recursed into (if folder). + procedure Copy_Link (Src, Dst : Any_Path) + with Pre => GNAT.OS_Lib.Is_Symbolic_Link (Src); + -- Copy a softlink into a new place preserving its relative path to target + function Current return String renames Ada.Directories.Current_Directory; function Parent (Path : Any_Path) return String diff --git a/testsuite/tests/install/softlinks/my_index/crate-0.1.0.tgz b/testsuite/tests/install/softlinks/my_index/crate-0.1.0.tgz index 11effa04cedfa0880e3d5fefb46130889b860442..233270e6a85bc1eda79e21b41a8f34944129c4f0 100644 GIT binary patch literal 20480 zcmeI24UXC{5QTFTF3{ldPkbK!TG3_6DzMUu(|0D`ZCP+gybP6Ey$A_#V#mDC&z><^ zb&KQXo2jWb^Xfr|;UmUS@f*cp&fn#f92I%*oHdqQQyC)bn`#yw)Spkyz8;Q?ZZ@la zF81xG6Bl2`&R<;quY6sd@$WxBU=#@8asDAT z7+wTyg#Yi=eM2Tw-$yI{_nrdyPvLj(H8=Uc-P?MqFZ`RvkIesl{71%< z@DG9GKcjQ|+pS>!&(Ht7=+1X-`*K^oNoSM)OVZQ32D-uzHkxv=lQ=Z zy3PLh1wpT?l=Bakc>QPo*NthHfPbFE%dUOesHTAB z`~wBg{(s&c4%__?rD2$F2K+Dhx7~WvX@&m1=86B<{$I-f8z=Ff<2(TIe}2H~mR`sI ziT_gZHo|{A{!jb^N&d(AKjMEy0>e!W@n0*>044qp^8WxiP5|KlVQPA3hWTqnf0g9@ z)BWG`_a^ZEFYwi}*heOu01A|G8Z~)rYn=wMGms{3quBo*w}a z|3^cJ|CLc^n;YSuty5Z9F#mh%*Z;=xV}QK>t2zwx-NyQVvHr{a|1Nx9vEtGa{?^L= zPeYt8f`75n%kKjZ(zUrXaM{u2KM z{sS2Nwe&|jPyh-*0Vn_kpa2wr0#E=7KmjNK1)u;FfC5ke3P1rU00p1`6o3Ly017|> MC;$bZ!2hklFS)5@x&QzG literal 349 zcmV-j0iymNiwFP!000001MQcAYJ)HkhVv-Cz-%ri=6P)Aid&~eE$s1ku`Nhjox-KC z?f(!A7`VePmrHhS(U&herBrWJBsAl_3vcpHkKb1mS({}>8_IH}iHyxv;$kH<=G65? zn_+~FafDT{(7z`l>vhKdO~#+${|Vgt-&VD7jI)2gr2a`;o=1)! zm6-0deXZIE8I6Gd8~>M|E*N7){kt&M+ysnA{Xc_A|6FuZYhcy>Z$kgKHq<}-$7k06 zmrRTxIM40E{hxw==lJ}mWx~$`|I_{tO>-28v9SM->fiGU;QpVc8-C6S3FPq*_ subdir/bin ├── broken -> missing +├── lib +│ ├── mock.so -> mock.so.0.0 +│ ├── mock.so.0 -> mock.so.0.0 +│ ├── mock.so.0.0 +│ ├── zzz.so -> mock.so +│ └── zzz.so.0 -> mock.so +├── order +│ ├── ab -> b +│ ├── af -> d/f +│ ├── b +│ ├── cb -> b +│ ├── d +│ │ └── f +│ └── zf -> d/f └── subdir ├── bin │ ├── loop -> ../../subdir │ └── x ├── parent -> .. └── self -> ../subdir + """ +import os +import shutil +import subprocess import sys -from drivers.alr import run_alr -from drivers.helpers import on_windows +from drivers.alr import crate_dirname, run_alr +from drivers.helpers import contents, on_windows + + +def kind(file): + return (os.path.isfile(file), os.path.islink(file), os.path.isdir(file)) + +def ls(path): + out = subprocess.run(["ls", "-alFR", path], capture_output=True, text=True) + return out.stdout # Does not apply to Windows as it does not support softlinks @@ -27,5 +53,35 @@ # This command should succeed normally run_alr("install", "--prefix=install", "crate") +# Contents should be identical. For that, we first untar the crate and then +# directly compare with the destination. + +run_alr("get", "crate") # This merely untars and moves the whole dir in one step, + # so contents are not modified. +cratedir = crate_dirname("crate") +os.chdir(cratedir) +shutil.rmtree("alire") # Created by get +os.remove("alire.toml") # Created by get +items = contents(".") # Contents of the original crate +os.chdir("..") +os.chdir("install") # Contents of the install prefix + +for item in items: + orig = f"../{cratedir}/{item}" + whatis = kind(orig) + if os.path.islink(orig) and os.path.isdir(orig): + continue # We aren't yet able to copy those + if not os.path.exists (orig): + continue # Broken links, we don't copy them at all + assert os.path.exists(item), \ + f"Missing expected entry {item}: {whatis}') in " + \ + f"contents (dst):\n{ls('.')}" + \ + f"contents (src):\n{ls('../' + cratedir)}" + assert kind(item) == kind(orig), \ + f"Unexpected kind for {item}: {kind(item)} != {kind(orig)}" + +# Cleanup +os.chdir("..") +shutil.rmtree(cratedir) print('SUCCESS') From b83fc48bfbe3867819759f97f1cf6c4c452d62ca Mon Sep 17 00:00:00 2001 From: Alejandro R Mosteo Date: Wed, 20 Mar 2024 15:19:23 +0100 Subject: [PATCH 15/53] Early error if toolchain unavailable for testsuite (#1655) --- testsuite/run.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/testsuite/run.py b/testsuite/run.py index 15f9370c4..e20b2df9f 100755 --- a/testsuite/run.py +++ b/testsuite/run.py @@ -10,6 +10,7 @@ from __future__ import absolute_import, print_function import os.path +import shutil import sys from argparse import ArgumentTypeError @@ -35,6 +36,13 @@ def add_options(self, parser): dest='alr_path', metavar='FILE', help='''Set `alr` binary to run the testsuite against. Defaults to `alr` from project's `bin` directory.''') + def require_executable(self, name): + path = shutil.which(name) + if path is None: + raise FileNotFoundError(f"{name} not found in PATH") + else: + print(f"Using {name} at {path}") + def set_up(self): super().set_up() os.environ['ALR_PATH'] = self.main.args.alr_path @@ -51,6 +59,13 @@ def set_up(self): # during the tests (e.g. submitting a release by accident) os.environ["ALR_TESTSUITE"] = "TRUE" + # Ensure toolchain is in scope, or err early instead of during tests + # Locate gnat and gprbuild in path and report their location + required_executables = ['gnat', 'gprbuild'] + for exe in required_executables: + self.require_executable(exe) + print() + def _alr_path(self, alr_file): alr_path = os.path.abspath(alr_file) if not os.path.isfile(alr_path): From ab0e0274daa34d26e7cb3dc84b9cb7a647091455 Mon Sep 17 00:00:00 2001 From: Alejandro R Mosteo Date: Wed, 20 Mar 2024 15:20:38 +0100 Subject: [PATCH 16/53] Fix completion script to avoid unwanted index updates (#1656) --- scripts/alr-completion.bash | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/scripts/alr-completion.bash b/scripts/alr-completion.bash index d9a97cefb..9337e6b71 100755 --- a/scripts/alr-completion.bash +++ b/scripts/alr-completion.bash @@ -5,6 +5,14 @@ if ! builtin type -P alr &>/dev/null; then return fi +# Disable index auto-update to avoid interference with commands below +if alr settings --global | grep -q index.auto_update= ; then + update_period=$(alr settings --global | grep index.auto_update= | cut -f2 -d=) +else + update_period=unset +fi +alr settings --global --set index.auto_update 0 + # Commands/Topics: all line-first words not starting with capital letter, after # COMMANDS _alr_commands=$(alr | grep COMMANDS -A 99 | awk '{print $1}' | grep -v '[[:upper:]]' | xargs) @@ -80,3 +88,8 @@ function _alr_completion() { # Bind the function that performs context-aware completion complete -F _alr_completion alr + +# Re-enable index auto-update to avoid interference with commands below +if [ "$update_period" != "unset" ]; then + alr settings --global --set index.auto_update $update_period +fi From 789b5f3632653df4b99811d7c9c341e8881bc765 Mon Sep 17 00:00:00 2001 From: Alejandro R Mosteo Date: Wed, 20 Mar 2024 16:57:36 +0100 Subject: [PATCH 17/53] Testsuite control condition for Unix-only (#1657) * Test control condition for Unix-only * Fix test post-merge --- testsuite/drivers/driver/base_driver.py | 1 + testsuite/skels/global-index/test.yaml | 1 + testsuite/skels/local-index/test.yaml | 1 + testsuite/skels/no-index/test.yaml | 2 ++ testsuite/tests/install/softlinks/test.py | 5 +---- testsuite/tests/install/softlinks/test.yaml | 2 ++ 6 files changed, 8 insertions(+), 4 deletions(-) diff --git a/testsuite/drivers/driver/base_driver.py b/testsuite/drivers/driver/base_driver.py index 209252ceb..e87c15f38 100644 --- a/testsuite/drivers/driver/base_driver.py +++ b/testsuite/drivers/driver/base_driver.py @@ -47,6 +47,7 @@ def __init__(self, env: e3.env.Env, test_env: Dict[str, Any]): # Hardcode OSes for osname in ["windows", "linux", "macos"]: self.skip[f"skip_{osname}"] = not getattr(drivers.helpers, f"on_{osname}")() + self.skip[f"skip_unix"] = drivers.helpers.on_windows() @property def test_control_creator(self): diff --git a/testsuite/skels/global-index/test.yaml b/testsuite/skels/global-index/test.yaml index 1a71b1b05..dbc089b83 100644 --- a/testsuite/skels/global-index/test.yaml +++ b/testsuite/skels/global-index/test.yaml @@ -1,5 +1,6 @@ driver: python-script build_mode: both # one of shared, sandboxed, both (default) +control: # Used to disable test depending on one of: (see no-skel/test.yaml) indexes: basic_index: in_fixtures: true diff --git a/testsuite/skels/local-index/test.yaml b/testsuite/skels/local-index/test.yaml index a0ce9ba5e..f791268e6 100644 --- a/testsuite/skels/local-index/test.yaml +++ b/testsuite/skels/local-index/test.yaml @@ -1,5 +1,6 @@ driver: python-script build_mode: both # one of shared, sandboxed, both (default) +control: # Used to disable test depending on one of: (see no-skel/test.yaml) indexes: my_index: in_fixtures: false diff --git a/testsuite/skels/no-index/test.yaml b/testsuite/skels/no-index/test.yaml index 97d4f3d5c..317e4f5a1 100644 --- a/testsuite/skels/no-index/test.yaml +++ b/testsuite/skels/no-index/test.yaml @@ -7,7 +7,9 @@ control: # Used to disable test depending on one of: - [SKIP, "skip_network", "Network-requiring tests disabled"] - [SKIP, "skip_linux", "Test is Linux-only"] - [SKIP, "skip_macos", "Test is macOS-only"] + - [SKIP, "skip_unix", "Test is Unix-only"] - [SKIP, "skip_windows", "Test is Windows-only"] + # These have to be interpreted as: "skip (if not running under) condition" indexes: compiler_only_index: {} # Note that shared builds require a detected compiler to be able to compute diff --git a/testsuite/tests/install/softlinks/test.py b/testsuite/tests/install/softlinks/test.py index 54a23b002..a0f881d6f 100644 --- a/testsuite/tests/install/softlinks/test.py +++ b/testsuite/tests/install/softlinks/test.py @@ -30,10 +30,7 @@ import os import shutil -import subprocess -import sys - -from drivers.alr import crate_dirname, run_alr +from drivers.alr import run_alr, crate_dirname from drivers.helpers import contents, on_windows diff --git a/testsuite/tests/install/softlinks/test.yaml b/testsuite/tests/install/softlinks/test.yaml index 0a859639c..1f89021f2 100644 --- a/testsuite/tests/install/softlinks/test.yaml +++ b/testsuite/tests/install/softlinks/test.yaml @@ -1,4 +1,6 @@ driver: python-script +control: + - [SKIP, "skip_unix", "Test is Unix-only"] indexes: my_index: in_fixtures: false From 5dcc28ed57d5849221b0128df0e774af1f3baa53 Mon Sep 17 00:00:00 2001 From: Alejandro R Mosteo Date: Thu, 21 Mar 2024 12:26:58 +0100 Subject: [PATCH 18/53] Use latest FSF GNAT for nightly alr builds (#1659) --- .github/workflows/nightly.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index d6c4b9b16..d28257491 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -32,7 +32,9 @@ jobs: submodules: true - name: Install FSF toolchain - uses: alire-project/setup-alire@v3 + uses: alire-project/alr-install@v1 + with: + crates: gnat_native gprbuild - name: Install Python 3.x (required for the testsuite) uses: actions/setup-python@v2 From a016de24c16715e41fc72d51b4a190481ac9d138 Mon Sep 17 00:00:00 2001 From: Alejandro R Mosteo Date: Mon, 25 Mar 2024 11:54:54 +0100 Subject: [PATCH 19/53] Fix non-ASCII char used when --no-color in effect (#1661) --- src/alire/alire-solutions-diffs.adb | 22 +++++++++++----------- testsuite/tests/pin/change-path/test.py | 2 +- testsuite/tests/with/changes-info/test.py | 4 ++-- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/alire/alire-solutions-diffs.adb b/src/alire/alire-solutions-diffs.adb index 5cdb6d812..dc74563da 100644 --- a/src/alire/alire-solutions-diffs.adb +++ b/src/alire/alire-solutions-diffs.adb @@ -45,18 +45,18 @@ package body Alire.Solutions.Diffs is when Missing => TTY.Error (U ("â—")), -- alts: âš ï¸â—â€¼ï¸ when Binary => TTY.Warn (U ("📦")), when Info => TTY.Emph (U ("🛈"))) - else + else "" & (case Change is - when Added => U ("+"), - when Removed => U ("-"), - when Upgraded => U ("^"), - when Downgraded => U ("v"), - when Pinned => U ("·"), - when Unpinned => U ("o"), - when Unchanged => U ("="), - when Missing => U ("!"), - when Binary => U ("b"), - when Info => U ("i") + when Added => '+', + when Removed => '-', + when Upgraded => '^', + when Downgraded => 'v', + when Pinned => '.', + when Unpinned => 'o', + when Unchanged => '=', + when Missing => '!', + when Binary => 'b', + when Info => 'i' )); -- This type is used to summarize every detected change diff --git a/testsuite/tests/pin/change-path/test.py b/testsuite/tests/pin/change-path/test.py index 48ea5ce7b..bdad2e046 100644 --- a/testsuite/tests/pin/change-path/test.py +++ b/testsuite/tests/pin/change-path/test.py @@ -38,7 +38,7 @@ def create_dep(nest): assert_eq("""Note: Synchronizing workspace... Dependencies automatically updated as follows: - · yyy 0.1.0-dev (path=../nest2/yyy) + . yyy 0.1.0-dev (path=../nest2/yyy) yyy file:../nest2/yyy\n""", p.out) diff --git a/testsuite/tests/with/changes-info/test.py b/testsuite/tests/with/changes-info/test.py index 10f4f91f9..261b71a31 100644 --- a/testsuite/tests/with/changes-info/test.py +++ b/testsuite/tests/with/changes-info/test.py @@ -61,7 +61,7 @@ Changes to dependency solution: - +· local_crate unknown (new,path=local_crate)""") + ".*", + +. local_crate unknown (new,path=local_crate)""") + ".*", p.out, flags=re.S) ############################################################################### @@ -70,7 +70,7 @@ assert_match(".*" + re.escape("""Changes to dependency solution: - · libhello 2.0.0 (pin=2.0.0)""") + ".*", + . libhello 2.0.0 (pin=2.0.0)""") + ".*", p.out, flags=re.S) ############################################################################### From b4044b930d7eb807907d0ef78f5820811fcebe74 Mon Sep 17 00:00:00 2001 From: Alejandro R Mosteo Date: Fri, 24 May 2024 11:25:37 +0200 Subject: [PATCH 20/53] Fix macOS workflows (user macos-12 runner) (#1685) --- .github/workflows/ci-macos.yml | 2 +- .github/workflows/ci-toolchain.yml | 2 +- .github/workflows/nightly.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci-macos.yml b/.github/workflows/ci-macos.yml index 33b7c78aa..15025e11b 100644 --- a/.github/workflows/ci-macos.yml +++ b/.github/workflows/ci-macos.yml @@ -16,7 +16,7 @@ jobs: build: name: CI on macOS - runs-on: macos-latest + runs-on: macos-12 steps: - name: Check out repository diff --git a/.github/workflows/ci-toolchain.yml b/.github/workflows/ci-toolchain.yml index 7db742c4e..0d9852acd 100644 --- a/.github/workflows/ci-toolchain.yml +++ b/.github/workflows/ci-toolchain.yml @@ -22,7 +22,7 @@ jobs: fail-fast: false matrix: os: - - macos-latest + - macos-12 - ubuntu-latest - windows-latest gcc_version: diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index d28257491..6ac6231c1 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -21,7 +21,7 @@ jobs: fail-fast: false # Attempt to generate as many of them as possible matrix: os: - - macos-latest + - macos-12 - ubuntu-20.04 - windows-latest From a52f87021f78520370845a62063caa6d9b2cf3b2 Mon Sep 17 00:00:00 2001 From: Alejandro R Mosteo Date: Fri, 24 May 2024 14:42:54 +0200 Subject: [PATCH 21/53] new dev/clean.sh (#1686) --- dev/clean.sh | 10 ++++++++++ dev/edit.sh | 7 +++++++ 2 files changed, 17 insertions(+) create mode 100755 dev/clean.sh diff --git a/dev/clean.sh b/dev/clean.sh new file mode 100755 index 000000000..9da51bfb0 --- /dev/null +++ b/dev/clean.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash + +# Import reusable bits +pushd $( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) > /dev/null || exit 1 + . functions.sh +popd > /dev/null || exit 1 + +export ALIRE_OS=$(get_OS) + +gprclean -f -r -Palr_env.gpr diff --git a/dev/edit.sh b/dev/edit.sh index 66ca7ca90..a429fe80e 100755 --- a/dev/edit.sh +++ b/dev/edit.sh @@ -1 +1,8 @@ +# Import reusable bits +pushd $( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) > /dev/null || exit 1 + . functions.sh +popd > /dev/null || exit 1 + +export ALIRE_OS=$(get_OS) + gnatstudio -P alr_env & >/dev/null 2>&1 From 119e690458d3a8ec3cbb3b07cff934e2849d2300 Mon Sep 17 00:00:00 2001 From: Alejandro R Mosteo Date: Thu, 30 May 2024 13:14:29 +0200 Subject: [PATCH 22/53] Fix monorepo bug wrt location of generated manifest (#1684) * Fix monorepo bug wrt location of generated manifest When deploying crates, to avoid confusion, we replace the packaged manifest (if any) with the one from the index, which is the one used anyway. For monorepos, the replacement was placed at the repo root instead of at the crate location. * Use macOS 12 to avoid linking problems * Self-review --- src/alire/alire-releases.adb | 31 ++++++----- .../my_index/index/index.toml | 1 + .../tests/monorepo/manifest-in-place/test.py | 54 +++++++++++++++++++ .../monorepo/manifest-in-place/test.yaml | 5 ++ 4 files changed, 78 insertions(+), 13 deletions(-) create mode 100644 testsuite/tests/monorepo/manifest-in-place/my_index/index/index.toml create mode 100644 testsuite/tests/monorepo/manifest-in-place/test.py create mode 100644 testsuite/tests/monorepo/manifest-in-place/test.yaml diff --git a/src/alire/alire-releases.adb b/src/alire/alire-releases.adb index 1858480fe..f616c4e0d 100644 --- a/src/alire/alire-releases.adb +++ b/src/alire/alire-releases.adb @@ -251,20 +251,24 @@ package body Alire.Releases is Mark_Completion : Boolean := True) is use Alire.Directories; - Folder : constant Any_Path := Parent_Folder / This.Deployment_Folder; - Completed : Flags.Flag := Flags.Complete_Copy (Folder); + Repo_Folder : constant Any_Path := + Parent_Folder / This.Deployment_Folder; + Rel_Folder : constant Any_Path := + Parent_Folder / This.Base_Folder; + Completed : Flags.Flag := Flags.Complete_Copy (Repo_Folder); ------------------------------ -- Backup_Upstream_Manifest -- ------------------------------ procedure Backup_Upstream_Manifest is - Working_Dir : Guard (Enter (Folder)) with Unreferenced; + Working_Dir : Guard (Enter (Rel_Folder)) with Unreferenced; begin Ada.Directories.Create_Path (Paths.Working_Folder_Inside_Root); if GNAT.OS_Lib.Is_Regular_File (Paths.Crate_File_Name) then - Trace.Debug ("Backing up bundled manifest file as *.upstream"); + Trace.Debug ("Backing up bundled manifest file at " + & Adirs.Current_Directory & " as *.upstream"); declare Upstream_File : constant String := Paths.Working_Folder_Inside_Root @@ -295,16 +299,17 @@ package body Alire.Releases is begin Trace.Debug ("Generating manifest file for " & This.Milestone.TTY_Image & " with" - & This.Dependencies.Leaf_Count'Img & " dependencies"); + & This.Dependencies.Leaf_Count'Img & " dependencies " + & " at " & (Rel_Folder / Paths.Crate_File_Name)); - This.Whenever (Env).To_File (Folder / Paths.Crate_File_Name, + This.Whenever (Env).To_File (Rel_Folder / Paths.Crate_File_Name, Kind); end Create_Authoritative_Manifest; begin Trace.Debug ("Deploying " & This.Milestone.TTY_Image - & " into " & TTY.URL (Folder)); + & " into " & TTY.URL (Repo_Folder)); -- Deploy if the target dir is not already there. We only skip for -- releases that require a folder to be deployed; system releases @@ -331,14 +336,14 @@ package body Alire.Releases is else Was_There := False; Put_Info ("Deploying " & This.Milestone.TTY_Image & "..."); - Alire.Origins.Deployers.Deploy (This, Folder).Assert; + Alire.Origins.Deployers.Deploy (This, Repo_Folder).Assert; end if; -- For deployers that do nothing, we ensure the folder exists so all -- dependencies leave a trace in the cache/dependencies folder, and -- a place from where to run their actions by default. - Ada.Directories.Create_Path (Folder); + Ada.Directories.Create_Path (Repo_Folder); -- Backup a potentially packaged manifest, so our authoritative -- manifest from the index is always used. @@ -349,8 +354,8 @@ package body Alire.Releases is if Create_Manifest then Create_Authoritative_Manifest (if Include_Origin - then Manifest.Index - else Manifest.Local); + then Manifest.Index + else Manifest.Local); end if; if Mark_Completion then @@ -363,10 +368,10 @@ package body Alire.Releases is -- during an action). Log_Exception (E); - if Ada.Directories.Exists (Folder) then + if Ada.Directories.Exists (Repo_Folder) then Trace.Debug ("Cleaning up failed release deployment of " & This.Milestone.TTY_Image); - Directories.Force_Delete (Folder); + Directories.Force_Delete (Repo_Folder); end if; raise; diff --git a/testsuite/tests/monorepo/manifest-in-place/my_index/index/index.toml b/testsuite/tests/monorepo/manifest-in-place/my_index/index/index.toml new file mode 100644 index 000000000..bad265e4f --- /dev/null +++ b/testsuite/tests/monorepo/manifest-in-place/my_index/index/index.toml @@ -0,0 +1 @@ +version = "1.1" diff --git a/testsuite/tests/monorepo/manifest-in-place/test.py b/testsuite/tests/monorepo/manifest-in-place/test.py new file mode 100644 index 000000000..b6c2e129f --- /dev/null +++ b/testsuite/tests/monorepo/manifest-in-place/test.py @@ -0,0 +1,54 @@ +""" +Verify that the index manifest is copied in the proper place instead of the one +possibly packed by upstream, like it is done for regular crates. +""" + +from glob import glob +import os +from subprocess import run + +from drivers.alr import alr_manifest, alr_publish, init_local_crate, run_alr +from drivers.asserts import assert_contents +from drivers.helpers import init_git_repo + +# We create a repository with a nested crate that will act as the upstream +# remote repository: + +start_dir = os.getcwd() +os.mkdir("monoproject.upstream") +os.chdir("monoproject.upstream") +init_local_crate("crate1", enter=False) +os.chdir(start_dir) +commit1 = init_git_repo("monoproject.upstream") + +# We clone the project to obtain our local copy + +assert run(["git", "clone", + "monoproject.upstream", "monoproject"]).returncode == 0 + +# We enter the crate nested inside and publish. + +os.chdir("monoproject") +os.chdir("crate1") +alr_publish("crate1", "0.1.0-dev", + index_path=os.path.join(start_dir, "my_index")) + +# Verify that we can `alr get` the nested crate + +os.chdir(start_dir) +run_alr("get", "crate1") + +# Enter and verify the only manifest is at the expected location (in the nested +# crate), and the only backup is too there. + +os.chdir(glob("monoproject_*")[0]) + +assert not os.path.isfile(alr_manifest()), \ + "Unexpected manifest at the root of the repository" + +assert_contents(".", + ['./crate1/alire.toml', + './crate1/alire/alire.toml.upstream'], + regex=".*alire.*toml") + +print('SUCCESS') diff --git a/testsuite/tests/monorepo/manifest-in-place/test.yaml b/testsuite/tests/monorepo/manifest-in-place/test.yaml new file mode 100644 index 000000000..d6aa8a36d --- /dev/null +++ b/testsuite/tests/monorepo/manifest-in-place/test.yaml @@ -0,0 +1,5 @@ +driver: python-script +indexes: + my_index: + in_fixtures: false + compiler_only_index: {} From 61ea262ddc02eaf78cd4c4a96a4c5c0a640bc1e8 Mon Sep 17 00:00:00 2001 From: Yannick Moy Date: Thu, 6 Jun 2024 17:19:10 +0200 Subject: [PATCH 23/53] Fix typos and hyperlinks in doc --- doc/getting-started.md | 11 ++++++----- doc/publishing.md | 20 ++++++++++---------- doc/toolchains.md | 7 ++++--- 3 files changed, 20 insertions(+), 18 deletions(-) diff --git a/doc/getting-started.md b/doc/getting-started.md index 3044bdc6d..40b2441c9 100644 --- a/doc/getting-started.md +++ b/doc/getting-started.md @@ -289,12 +289,12 @@ enough to get an idea of the root of the problem. Additionally, `-d` will show tracebacks of exceptions. Subprocess output is shown by default (you can silence it, and anything else -not an error) with `-q`, which enables quiet mode. Any subprocess that exist +not an error) with `-q`, which enables quiet mode. Any subprocess that exits abnormally will be reported, including its invocation arguments. If you suspect your settings may be the source of some problem, please check -our section on [Settings](setting), and in particular how to use a [default -pristine settings](settings#Relocating-your-settings) +our section on [Settings](settings), and in particular how to use a [default +pristine settings](settings#relocating-your-settings) ## Running tests @@ -393,8 +393,9 @@ you have to add a project-files = ["project_file.gpr"] ``` -Although this is not recommended (see Best practices), you can have multiple -GPR project files: +Although this is not recommended (see +[best practices](policies#best-practices)), you can have multiple GPR project files: + ```toml project-files = ["project_file_1.gpr", "project_file_2.gpr"] ``` diff --git a/doc/publishing.md b/doc/publishing.md index 108f73465..b44d2e56c 100644 --- a/doc/publishing.md +++ b/doc/publishing.md @@ -22,7 +22,7 @@ index on GitHub on your behalf. Read on for the details underlying these automated steps, or in case you need to perform further tweaking. -## Creating a Github Personal Access Token +## Creating a GitHub Personal Access Token A Personal Access Token (PAT) allows Alire to act on your behalf to fork the community index, push the new release manifest to a new branch in your own fork, @@ -31,7 +31,7 @@ and finally open a pull-request against the community repository. The PAT, once created, is a plain string. You can either export the environment variable `GH_TOKEN` set to this string, or provide it when Alire asks for it. -There are two kinds of PATs on Github: classic and fine-grained. The latter are +There are two kinds of PATs on GitHub: classic and fine-grained. The latter are in beta and not documented here yet. Follow these steps to create a classic PAT: 1. On the main https://github.com page, after having logged in, click on your @@ -112,20 +112,20 @@ methods to prepare your release submission: For this common use case, you need: -- A git repository that is clean an up-to-date with its remote. +- A git repository that is clean and up-to-date with its remote. - The repository already contains the release you want to publish. - The commit with the release must exist both locally and at the remote. - The repository must also be an Alire-enabled workspace: - It contains a top-level `alire.toml` manifest describing the release. - The remote host must be one of a few trusted major open-source sites. - This requirement is motivated by vulnerabilities identified with SHA1, - whose migration to a stronger hash is [not yet complete] - (https://git-scm.com/docs/hash-function-transition/) in `git`. + whose migration to a stronger hash is + [not yet complete](https://git-scm.com/docs/hash-function-transition/) in `git`. - `alr` will inform you if your host is not supported. Please contact us if you think a site should be allowed. The complete list can be consulted by running `alr publish --trusted-sites`. - This is a temporary measure until more sophisticated publishing automation - is supported. See the [Remote Source Archive](#remote-source-archive) case + is supported. See the [Starting with a remote source archive](#starting-with-a-remote-source-archive) case for alternatives to this scenario (you are not forced to change your code hosting, or even have an online repository). @@ -153,7 +153,7 @@ offer to create the pull request for you, unless you specify `--skip-submit`. If so, a link for conveniently creating this PR will also be provided by `alr`: - Upload the generated index manifest file (`crate-version.toml`) to the - supplied page link on github and create a pull-request. + supplied page link on GitHub and create a pull-request. ### Starting with a remote repository, without local clone @@ -217,7 +217,7 @@ must be manually uploaded by the user to a publicly accessible hosting service. After the upload, the user can supply the URL to fetch this archive to the publishing assistant (which will be waiting for this information), and the assistant will resume as if it had been invoked with `alr publish ` -(see #starting-with-a-remote-source-archive). +(see [Starting with a remote source archive](#starting-with-a-remote-source-archive)). ### Support for complex projects whose sources become multiple Alire crates @@ -261,8 +261,8 @@ workflows. ### Creating the PR via cloning. -Instead of uploading the generated index manifest file via the github upload -link, you can follow the usual procedure to submit a PR to a github repository: +Instead of uploading the generated index manifest file via the GitHub upload +link, you can follow the usual procedure to submit a PR to a GitHub repository: 1. Fork the community index to your GitHub account. 1. Clone your fork locally and place generated manifest at the intended folder. diff --git a/doc/toolchains.md b/doc/toolchains.md index 2ab1e35c2..f0c3aef99 100644 --- a/doc/toolchains.md +++ b/doc/toolchains.md @@ -8,7 +8,7 @@ The user can now select a preferred compiler via `alr toolchain --select`. Releases may still override this selection (for example to use a cross-compiler). -There are two kind of dependencies on GNAT compilers: generic dependencies on +There are two kinds of dependencies on GNAT compilers: generic dependencies on the `gnat` crate, which apply to every compiler, and dependencies on a precise native or cross-compiler. @@ -19,8 +19,9 @@ the host platform. ## Identifying available compilers -Compilers available for download can be listed with `alr search --full ---external-detect gnat_`. They will also be shown by the selection assistant, +Compilers available for download can be listed with +`alr search --full --external-detect gnat_`. +They will also be shown by the selection assistant, `alr toolchain --select`. Running `alr toolchain` without arguments will show the installed compilers and From 72ebc9ab33d608b4f229aef55adadb6d062f069c Mon Sep 17 00:00:00 2001 From: pjljvandelaar Date: Mon, 10 Jun 2024 20:38:16 +0200 Subject: [PATCH 24/53] Prefer usage of quantified expressions (#1238) Co-authored-by: Alejandro R. Mosteo --- src/alire/alire-crate_configuration.adb | 13 +++++-------- src/alire/alire-dependencies-graphs.adb | 8 +------- src/alire/alire-origins-deployers-system-apt.adb | 9 ++------- src/alire/alire-origins-deployers-system-zypper.adb | 10 +++------- src/alire/alire-properties-configurations.adb | 9 +++------ src/alire/alire-utils.adb | 10 +++------- src/alr/alr-utils.adb | 8 +------- 7 files changed, 18 insertions(+), 49 deletions(-) diff --git a/src/alire/alire-crate_configuration.adb b/src/alire/alire-crate_configuration.adb index c886c523b..ba8eb43dd 100644 --- a/src/alire/alire-crate_configuration.adb +++ b/src/alire/alire-crate_configuration.adb @@ -367,14 +367,11 @@ package body Alire.Crate_Configuration is is use Config_Maps; begin - for C in This.Var_Map.Iterate loop - if (Crate = "" or else Name (C).As_String = Crate) - and then Element (C).Set_By = Must_Be_Set - then - return False; - end if; - end loop; - return True; + return + (for all C in This.Var_Map.Iterate => + not + ((Crate = "" or else Name (C).As_String = Crate) + and then Element (C).Set_By = Must_Be_Set)); end Is_Config_Complete; --------------------- diff --git a/src/alire/alire-dependencies-graphs.adb b/src/alire/alire-dependencies-graphs.adb index 22b0dced7..fa5a90273 100644 --- a/src/alire/alire-dependencies-graphs.adb +++ b/src/alire/alire-dependencies-graphs.adb @@ -72,13 +72,7 @@ package body Alire.Dependencies.Graphs is return Boolean is begin - for Dep of This loop - if +Dep.Dependent = Crate then - return True; - end if; - end loop; - - return False; + return (for some Dep of This => +Dep.Dependent = Crate); end Has_Dependencies; ----------- diff --git a/src/alire/alire-origins-deployers-system-apt.adb b/src/alire/alire-origins-deployers-system-apt.adb index 4f4db6ff5..8406e7d76 100644 --- a/src/alire/alire-origins-deployers-system-apt.adb +++ b/src/alire/alire-origins-deployers-system-apt.adb @@ -25,13 +25,8 @@ package body Alire.Origins.Deployers.System.Apt is Valid_Exit_Codes => (0, 1), -- returned when not found Err_To_Out => True); begin - for Line of Output loop - if Line = "Status: install ok installed" then - return True; - end if; - end loop; - - return False; + return + (for some Line of Output => Line = "Status: install ok installed"); end Already_Installed; ------------ diff --git a/src/alire/alire-origins-deployers-system-zypper.adb b/src/alire/alire-origins-deployers-system-zypper.adb index fca28d6b2..716ee622b 100644 --- a/src/alire/alire-origins-deployers-system-zypper.adb +++ b/src/alire/alire-origins-deployers-system-zypper.adb @@ -30,13 +30,9 @@ package body Alire.Origins.Deployers.System.Zypper is Valid_Exit_Codes => (0, 104), -- returned when not found Err_To_Out => True); begin - for Line of Output loop - if Has_Prefix (Line, " + Has_Prefix (Line, " + This.Values.Item (Index).As_String = Str); end; when Real => diff --git a/src/alire/alire-utils.adb b/src/alire/alire-utils.adb index 1b48243d8..fae78c296 100644 --- a/src/alire/alire-utils.adb +++ b/src/alire/alire-utils.adb @@ -16,13 +16,9 @@ package body Alire.Utils is function Command_Line_Contains (Prefix : String) return Boolean is begin - for I in 1 .. Ada.Command_Line.Argument_Count loop - if Has_Prefix (Ada.Command_Line.Argument (I), Prefix) then - return True; - end if; - end loop; - - return False; + return + (for some I in 1 .. Ada.Command_Line.Argument_Count => + Has_Prefix (Ada.Command_Line.Argument (I), Prefix)); end Command_Line_Contains; ------------- diff --git a/src/alr/alr-utils.adb b/src/alr/alr-utils.adb index 61f88396f..cec9c53b3 100644 --- a/src/alr/alr-utils.adb +++ b/src/alr/alr-utils.adb @@ -6,13 +6,7 @@ package body Alr.Utils is function Contains (V : AAA.Strings.Vector; Subst : String) return Boolean is begin - for Str of V loop - if AAA.Strings.Contains (Str, Subst) then - return True; - end if; - end loop; - - return False; + return (for some Str of V => AAA.Strings.Contains (Str, Subst)); end Contains; end Alr.Utils; From 85360b4f04d0a745aea166bb6d1b0c7f52c594e7 Mon Sep 17 00:00:00 2001 From: Alejandro R Mosteo Date: Wed, 12 Jun 2024 11:59:57 +0200 Subject: [PATCH 25/53] Add GNAT 14 to tested versions (#1695) --- .github/workflows/ci-toolchain.yml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/.github/workflows/ci-toolchain.yml b/.github/workflows/ci-toolchain.yml index 0d9852acd..0c02a6be4 100644 --- a/.github/workflows/ci-toolchain.yml +++ b/.github/workflows/ci-toolchain.yml @@ -25,11 +25,7 @@ jobs: - macos-12 - ubuntu-latest - windows-latest - gcc_version: - - 10 - - 11 - - 12 - - 13 + gcc_version: [10, 11, 12, 13, 14] steps: - name: Check out From de6c9faabbf7e61defac013a377186b4ae24da46 Mon Sep 17 00:00:00 2001 From: pjljvandelaar Date: Wed, 12 Jun 2024 13:57:08 +0200 Subject: [PATCH 26/53] Refactor: prefer membership tests (#1243) Co-authored-by: Alejandro R. Mosteo --- src/alire/alire-expressions-maps.adb | 7 ++++--- src/alire/alire-expressions.ads | 9 ++++----- src/alire/alire-properties-actions-runners.ads | 2 +- src/alire/alire-vcss-git.adb | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/alire/alire-expressions-maps.adb b/src/alire/alire-expressions-maps.adb index 3069c2e25..0b7b44737 100644 --- a/src/alire/alire-expressions-maps.adb +++ b/src/alire/alire-expressions-maps.adb @@ -7,12 +7,13 @@ package body Alire.Expressions.Maps is procedure Insert (M : in out Map; V : String; E : Elements) is begin - if V = TOML_Keys.Case_Others or else V = "others" then + if V in TOML_Keys.Case_Others | "others" then M.Set_Others (E); else if not M.Base.Is_Valid (V) and then Force then - Trace.Debug ("Storing unknown value '" & V & "' for enumeration '" - & Key (M.Base) & "'"); + Trace.Debug + ("Storing unknown value '" & V & "' for enumeration '" & + Key (M.Base) & "'"); end if; M.Entries.Insert (V, E); end if; diff --git a/src/alire/alire-expressions.ads b/src/alire/alire-expressions.ads index ee1d5e927..7abcf9889 100644 --- a/src/alire/alire-expressions.ads +++ b/src/alire/alire-expressions.ads @@ -30,11 +30,10 @@ package Alire.Expressions with Preelaborate is function Name (This : Variable) return String; -- The Ada-like name of this variable - function Satisfies (Property : Properties.Property'Class; - Var_Key : String; - Value : String) return Boolean - with Pre => Value /= TOML_Keys.Case_Others and then - Value /= "others"; + function Satisfies + (Property : Properties.Property'Class; Var_Key : String; Value : String) + return Boolean with + Pre => Value not in TOML_Keys.Case_Others | "others"; -- Say if a property is satisfied by the value, which must match the -- Variable and property type (keys must match). Doesn't accept defaults. diff --git a/src/alire/alire-properties-actions-runners.ads b/src/alire/alire-properties-actions-runners.ads index 5dd745e42..cfada5ed3 100644 --- a/src/alire/alire-properties-actions-runners.ads +++ b/src/alire/alire-properties-actions-runners.ads @@ -37,7 +37,7 @@ private Working_Folder : Any_Path (1 .. Folder_Len); end record with Type_Invariant => - (Name = "" or else Name in Action_Name) + (Name in "" | Action_Name) and then (if Moment = On_Demand then Name /= ""); diff --git a/src/alire/alire-vcss-git.adb b/src/alire/alire-vcss-git.adb index 890374b04..f819ac365 100644 --- a/src/alire/alire-vcss-git.adb +++ b/src/alire/alire-vcss-git.adb @@ -484,7 +484,7 @@ package body Alire.VCSs.Git is -- Prepare Ref to make it less ambiguous - if Ref = "HEAD" or else Ref = "" then + if Ref in "HEAD" | "" then return This.Remote_Commit (From, ASCII.HT & "HEAD"); elsif Ref (Ref'First) not in '/' | ASCII.HT then return This.Remote_Commit (From, '/' & Ref); From bd25511f5a4629bad3fbce8c0aafa41d6d73554a Mon Sep 17 00:00:00 2001 From: Alejandro R Mosteo Date: Fri, 14 Jun 2024 09:34:09 +0200 Subject: [PATCH 27/53] Tag builds done through `alr build` with unique build string (#1530) * Replace version build with current commit hash * Detect dirtiness in . to flag it too in version * Fix for line terminators on Windows * Self-review * Ada version * Windows dispatcher script * Patch from the dev/build.sh script too --- alire.toml | 23 ++++ dev/build.sh | 12 +- scripts/ci-github.sh | 17 ++- scripts/version-patcher.ps1 | 25 ++++ scripts/version-patcher.py | 55 ++++++++ scripts/version-patcher.sh | 31 +++++ src/alire/alire-version.ads | 4 + support/version_patcher/.gitignore | 4 + support/version_patcher/alire.toml | 12 ++ .../config/version_patcher_config.ads | 20 +++ .../config/version_patcher_config.gpr | 50 ++++++++ .../config/version_patcher_config.h | 20 +++ .../version_patcher/src/version_patcher.adb | 117 ++++++++++++++++++ support/version_patcher/version_patcher.gpr | 22 ++++ 14 files changed, 404 insertions(+), 8 deletions(-) create mode 100644 scripts/version-patcher.ps1 create mode 100644 scripts/version-patcher.py create mode 100755 scripts/version-patcher.sh create mode 100644 support/version_patcher/.gitignore create mode 100644 support/version_patcher/alire.toml create mode 100644 support/version_patcher/config/version_patcher_config.ads create mode 100644 support/version_patcher/config/version_patcher_config.gpr create mode 100644 support/version_patcher/config/version_patcher_config.h create mode 100644 support/version_patcher/src/version_patcher.adb create mode 100644 support/version_patcher/version_patcher.gpr diff --git a/alire.toml b/alire.toml index 81deb0d6a..7cee2636d 100644 --- a/alire.toml +++ b/alire.toml @@ -94,3 +94,26 @@ commit = "f607a63b714f09bbf6126de9851cbc21cf8666c9" [pins.toml_slicer] url = "https://github.com/mosteo/toml_slicer" branch = "alire" + +# To disable version updating, export ALR_VERSION_DONT_PATCH with any value + +# Before building, we add the commit to the version, for unique identification: +[[actions]] +[actions.'case(os)'.windows] +type = "pre-build" +command = ["pwsh", "scripts/version-patcher.ps1"] + +[actions.'case(os)'.'...'] +type = "pre-build" +command = ["scripts/version-patcher.sh"] + +# Afterwards we leave an extra note, so people manually building don't use a +# misleading commit. +[[actions]] +[actions.'case(os)'.windows] +type = "post-build" +command = ["pwsh", "scripts/version-patcher.ps1", "_or_later"] + +[actions.'case(os)'.'...'] +type = "post-build" +command = ["scripts/version-patcher.sh", "_or_later"] \ No newline at end of file diff --git a/dev/build.sh b/dev/build.sh index 79e79e15d..934986401 100755 --- a/dev/build.sh +++ b/dev/build.sh @@ -1,12 +1,16 @@ #!/usr/bin/env bash # Import reusable bits -pushd $( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) > /dev/null +pushd "$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" > /dev/null || exit 1 . functions.sh -popd > /dev/null +popd > /dev/null || exit 1 ALIRE_BUILD_JOBS="${ALIRE_BUILD_JOBS:-0}" -export ALIRE_OS=$(get_OS) +ALIRE_OS=$(get_OS); export ALIRE_OS + +scripts/version-patcher.sh echo "Building with ALIRE_OS=$ALIRE_OS..." -gprbuild "-j$ALIRE_BUILD_JOBS" -r -p -P `dirname $0`/../alr_env.gpr "$@" +gprbuild "-j$ALIRE_BUILD_JOBS" -r -p -P "$(dirname $0)/../alr_env.gpr" "$@" + +scripts/version-patcher.sh _or_later \ No newline at end of file diff --git a/scripts/ci-github.sh b/scripts/ci-github.sh index 400905ab4..845cd96d0 100755 --- a/scripts/ci-github.sh +++ b/scripts/ci-github.sh @@ -9,10 +9,19 @@ set -o nounset export PATH+=:${PWD}/bin # Import reusable bits -pushd $( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) +pushd "$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" . ../dev/functions.sh popd +# Mark location safe to assuage git if necessary (happens in some distros) +if git status 2>&1 | grep -q "dubious ownership"; then + echo "Marking $PWD as safe for git" + git config --global --add safe.directory "$PWD" +fi + +# Patch version +scripts/version-patcher.sh + # Build alr export ALIRE_OS=$(get_OS) gprbuild -j0 -p -P alr_env @@ -37,7 +46,7 @@ echo ............................ # Set up index if not default: if [ "${INDEX:-}" != "" ]; then - echo Setting default index to: $INDEX + echo Setting default index to: "$INDEX" alr index --name default --add "$INDEX" fi @@ -65,8 +74,8 @@ fi echo PYTHON installing testsuite dependencies... -echo Python version: $($run_python --version) -echo Pip version: $($run_pip --version) +echo "Python version: $($run_python --version)" +echo "Pip version: $($run_pip --version)" $run_pip install --upgrade -r requirements.txt echo Python search paths: diff --git a/scripts/version-patcher.ps1 b/scripts/version-patcher.ps1 new file mode 100644 index 000000000..1d1408bae --- /dev/null +++ b/scripts/version-patcher.ps1 @@ -0,0 +1,25 @@ +# This script dispatches to the Ada patcher, after building it. + +# Set strict mode for PowerShell to exit on error +$ErrorActionPreference = "Stop" + +$bin = "support/version_patcher/bin/version_patcher.exe" + +# If the binary is already in place, do nothing +if (Test-Path $bin) { + Write-Output "Patcher already built." +} elseif (Get-Command gprbuild -ErrorAction SilentlyContinue) { + Write-Output "Building patcher with gprbuild..." + gprbuild -P support/version_patcher/version_patcher.gpr +} elseif (Get-Command alr -ErrorAction SilentlyContinue) { + Write-Output "Building patcher with alr..." + alr -C (Split-Path $bin) build +} else { + Write-Output "WARNING: No Ada tool available to build patcher, skipping." + exit 0 +} + +& $bin @args + +Write-Output "Resulting version file:" +Get-Content src/alire/alire-version.ads | Select-String "Current_Str" diff --git a/scripts/version-patcher.py b/scripts/version-patcher.py new file mode 100644 index 000000000..7088bb313 --- /dev/null +++ b/scripts/version-patcher.py @@ -0,0 +1,55 @@ +import os +import re +import subprocess +import sys + +def replace_version(filename, build_info): + pattern = r'(Current : constant String := "[^+]+\+)([^"]*)(";)' + + # Depending on the context in which this is run, there may be mix-ups with + # line terminators between the environment detected, github runner, etc... + # So just keep them as they are and that should always work. + + with open(filename, 'rb') as file: + content = file.read().decode() + + # The pattern captures the part between '+' and '";', replacing it with our + # new build information + + new_content = re.sub(pattern, r'\g<1>' + build_info + r'\3', content) + + # A few sanity checks and write if needed + + if new_content == content: + if build_info in content: + print(f"Note: version in {filename} already up to date") + else: + print(f"WARNING: failed to update version in {filename}") + else: + # Ensure the content line terminators are not changed + with open(filename, 'wb') as file: + file.write(new_content.encode()) + + +# If a flag exists, skip any updating, just print a message and exit +if "ALR_VERSION_DONT_PATCH" in os.environ: + print("Note: skipping version update") + sys.exit(0) + +# If there is an argument to the script, retrieve it here and use it as the new +# dirty flag after the commit +if len(sys.argv) > 1: + dirty = sys.argv[1] +else: + # Detect whether the current directory contains changes + if subprocess.call(['git', 'diff-index', '--quiet', 'HEAD', '--']) != 0: + dirty = "_dirty" + else: + dirty = "" + +# Find the short git commit of the repository in the current directory +commit = subprocess.check_output(['git', 'rev-parse', '--short', 'HEAD']).decode('utf-8').strip() + +# Replace the build version part with the short commit hash plus any extra info +print(f"Updating version in src/alire/alire-version.ads to commit {commit}{dirty}...") +replace_version('src/alire/alire-version.ads', commit+dirty) diff --git a/scripts/version-patcher.sh b/scripts/version-patcher.sh new file mode 100755 index 000000000..1b0c07895 --- /dev/null +++ b/scripts/version-patcher.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +# This script dispatches to the Ada patcher, after building it. + +set -o errexit + +# Exit already if the ALR_VERSION_DONT_PATCH flag is defined +if [ "${ALR_VERSION_DONT_PATCH:-unset}" != "unset" ]; then + echo "Skipping version patching..." + exit 0 +fi + +bin=support/version_patcher/bin/version_patcher + +# If the binary is already in place, do nothing +if [ -f $bin ]; then + echo "Patcher already built." +elif (which gprbuild &>/dev/null); then + echo "Building patcher with gprbuild..." + gprbuild -P support/version_patcher/version_patcher.gpr +elif (which alr &>/dev/null); then + echo "Building patcher with alr..." + alr -C "$(dirname $bin)" build +else + echo "WARNING: No Ada tool available to build patcher, skipping." + exit 0 +fi + +$bin "$@" + +echo "Resulting version file:" +cat src/alire/alire-version.ads | grep Current_Str \ No newline at end of file diff --git a/src/alire/alire-version.ads b/src/alire/alire-version.ads index 6a0e4f70d..441a826dc 100644 --- a/src/alire/alire-version.ads +++ b/src/alire/alire-version.ads @@ -12,6 +12,10 @@ private -- Remember to update Alire.Index branch if needed too + -- NOTE: in the following version string, the build part (after '+') will + -- be replaced by `alr build` with the current commit, and appended with + -- "_or_later" after build. + Current_Str : constant String := "2.1-dev"; -- 2.0.0: alr settings refactor and minor fixes -- 2.0.0-rc1: release candidate for 2.0 diff --git a/support/version_patcher/.gitignore b/support/version_patcher/.gitignore new file mode 100644 index 000000000..5866d7bfa --- /dev/null +++ b/support/version_patcher/.gitignore @@ -0,0 +1,4 @@ +/obj/ +/bin/ +/alire/ +/config/ diff --git a/support/version_patcher/alire.toml b/support/version_patcher/alire.toml new file mode 100644 index 000000000..22000e222 --- /dev/null +++ b/support/version_patcher/alire.toml @@ -0,0 +1,12 @@ +name = "version_patcher" +description = "Patches current commit into alire-version.ads" +version = "0.1.0-dev" + +authors = ["Alejandro R. Mosteo"] +maintainers = ["Alejandro R. Mosteo "] +maintainers-logins = ["mosteo"] +licenses = "MIT OR Apache-2.0 WITH LLVM-exception" +website = "" +tags = [] + +executables = ["version_patcher"] diff --git a/support/version_patcher/config/version_patcher_config.ads b/support/version_patcher/config/version_patcher_config.ads new file mode 100644 index 000000000..440dfe5a6 --- /dev/null +++ b/support/version_patcher/config/version_patcher_config.ads @@ -0,0 +1,20 @@ +-- Configuration for version_patcher generated by Alire +pragma Restrictions (No_Elaboration_Code); +pragma Style_Checks (Off); + +package Version_Patcher_Config is + pragma Pure; + + Crate_Version : constant String := "0.1.0-dev"; + Crate_Name : constant String := "version_patcher"; + + Alire_Host_OS : constant String := "linux"; + + Alire_Host_Arch : constant String := "x86_64"; + + Alire_Host_Distro : constant String := "ubuntu"; + + type Build_Profile_Kind is (release, validation, development); + Build_Profile : constant Build_Profile_Kind := development; + +end Version_Patcher_Config; diff --git a/support/version_patcher/config/version_patcher_config.gpr b/support/version_patcher/config/version_patcher_config.gpr new file mode 100644 index 000000000..df412e7cb --- /dev/null +++ b/support/version_patcher/config/version_patcher_config.gpr @@ -0,0 +1,50 @@ +-- Configuration for version_patcher generated by Alire +abstract project Version_Patcher_Config is + Crate_Version := "0.1.0-dev"; + Crate_Name := "version_patcher"; + + Alire_Host_OS := "linux"; + + Alire_Host_Arch := "x86_64"; + + Alire_Host_Distro := "ubuntu"; + Ada_Compiler_Switches := External_As_List ("ADAFLAGS", " "); + Ada_Compiler_Switches := Ada_Compiler_Switches & + ( + "-Og" -- Optimize for debug + ,"-ffunction-sections" -- Separate ELF section for each function + ,"-fdata-sections" -- Separate ELF section for each variable + ,"-g" -- Generate debug info + ,"-gnatwa" -- Enable all warnings + ,"-gnatw.X" -- Disable warnings for No_Exception_Propagation + ,"-gnatVa" -- All validity checks + ,"-gnaty3" -- Specify indentation level of 3 + ,"-gnatya" -- Check attribute casing + ,"-gnatyA" -- Use of array index numbers in array attributes + ,"-gnatyB" -- Check Boolean operators + ,"-gnatyb" -- Blanks not allowed at statement end + ,"-gnatyc" -- Check comments + ,"-gnaty-d" -- Disable check no DOS line terminators present + ,"-gnatye" -- Check end/exit labels + ,"-gnatyf" -- No form feeds or vertical tabs + ,"-gnatyh" -- No horizontal tabs + ,"-gnatyi" -- Check if-then layout + ,"-gnatyI" -- check mode IN keywords + ,"-gnatyk" -- Check keyword casing + ,"-gnatyl" -- Check layout + ,"-gnatym" -- Check maximum line length + ,"-gnatyn" -- Check casing of entities in Standard + ,"-gnatyO" -- Check that overriding subprograms are explicitly marked as such + ,"-gnatyp" -- Check pragma casing + ,"-gnatyr" -- Check identifier references casing + ,"-gnatyS" -- Check no statements after THEN/ELSE + ,"-gnatyt" -- Check token spacing + ,"-gnatyu" -- Check unnecessary blank lines + ,"-gnatyx" -- Check extra parentheses + ,"-gnatW8" -- UTF-8 encoding for wide characters + ); + + type Build_Profile_Kind is ("release", "validation", "development"); + Build_Profile : Build_Profile_Kind := "development"; + +end Version_Patcher_Config; diff --git a/support/version_patcher/config/version_patcher_config.h b/support/version_patcher/config/version_patcher_config.h new file mode 100644 index 000000000..638f7552b --- /dev/null +++ b/support/version_patcher/config/version_patcher_config.h @@ -0,0 +1,20 @@ +/* Configuration for version_patcher generated by Alire */ +#ifndef VERSION_PATCHER_CONFIG_H +#define VERSION_PATCHER_CONFIG_H + +#define CRATE_VERSION "0.1.0-dev" +#define CRATE_NAME "version_patcher" + +#define ALIRE_HOST_OS "linux" + +#define ALIRE_HOST_ARCH "x86_64" + +#define ALIRE_HOST_DISTRO "ubuntu" + +#define BUILD_PROFILE_RELEASE 1 +#define BUILD_PROFILE_VALIDATION 2 +#define BUILD_PROFILE_DEVELOPMENT 3 + +#define BUILD_PROFILE 3 + +#endif diff --git a/support/version_patcher/src/version_patcher.adb b/support/version_patcher/src/version_patcher.adb new file mode 100644 index 000000000..b91768447 --- /dev/null +++ b/support/version_patcher/src/version_patcher.adb @@ -0,0 +1,117 @@ +with Ada.Command_Line; use Ada.Command_Line; +with Ada.Directories; +with Ada.Environment_Variables; use Ada.Environment_Variables; +with Ada.Strings.Unbounded; use Ada.Strings.Unbounded; +with Ada.Text_IO; + +with GNAT.Expect; +with GNAT.OS_Lib; + +--------------------- +-- Version_Patcher -- +--------------------- + +procedure Version_Patcher is + + --------------------- + -- Replace_Version -- + --------------------- + + procedure Replace_Version (Filename : String; Build_Info : String) is + F : Ada.Text_IO.File_Type; + O : Ada.Text_IO.File_Type; + use Ada.Text_IO; + + Target : constant String := "Current_Str : constant String :="; + begin + Open (F, In_File, Filename); + Create (O, Out_File, Filename & ".new"); + while not End_Of_File (F) loop + declare + Line : constant String := Get_Line (F); + begin + if (for some I in Line'Range => + I + Target'Length - 1 <= Line'Last and then + Line (I .. I + Target'Length - 1) = Target) + then + declare + Quotes_Seen : Boolean := False; + begin + for Char of Line loop + if Char = '"' and then not Quotes_Seen then + Quotes_Seen := True; + Put (O, Char); + elsif (Char = '"' and then Quotes_Seen) + or else Char = '+' + then + Put_Line (O, "+" & Build_Info & '"' & ";"); + exit; + else + Put (O, Char); + end if; + end loop; + end; + else + Put_Line (O, Line); + end if; + end; + end loop; + + Close (F); + Close (O); + + Ada.Directories.Delete_File (Filename); + Ada.Directories.Rename (Filename & ".new", Filename); + + end Replace_Version; + + ----------------- + -- Git_Command -- + ----------------- + + type Result is record + Output : Unbounded_String; + Code : Integer; + end record; + + function Git_Command (Args : String) return Result is + use GNAT.OS_Lib; + Arg_List : constant Argument_List_Access := + Argument_String_To_List (Args); + Code : aliased Integer; + Output : constant String + := GNAT.Expect.Get_Command_Output + ("git", Arg_List.all, "", Code'Access, True); + begin + return (To_Unbounded_String (Output), Code); + end Git_Command; + +begin + if Exists ("ALR_VERSION_DONT_PATCH") then + Ada.Text_IO.Put_Line ("Note: skipping version update"); + return; + end if; + + declare + Dirty : constant String + := (if Argument_Count > 0 then + Argument (1) + elsif Git_Command ("diff-index --quiet HEAD --").Code /= 0 then + "_dirty" + else + ""); + Commit_Result : constant Result := + Git_Command ("rev-parse --short HEAD"); + Commit : constant String := To_String (Commit_Result.Output); + begin + if Commit_Result.Code /= 0 then + raise Constraint_Error with + "Git error while trying to get commit:" + & Commit_Result.Code'Image; + end if; + Ada.Text_IO.Put_Line + ("Updating version in src/alire/alire-version.ads to commit " + & Commit & Dirty & "..."); + Replace_Version ("src/alire/alire-version.ads", Commit & Dirty); + end; +end Version_Patcher; diff --git a/support/version_patcher/version_patcher.gpr b/support/version_patcher/version_patcher.gpr new file mode 100644 index 000000000..3a496900e --- /dev/null +++ b/support/version_patcher/version_patcher.gpr @@ -0,0 +1,22 @@ +with "config/version_patcher_config.gpr"; +project Version_Patcher is + + for Source_Dirs use ("src/", "config/"); + for Object_Dir use "obj/" & Version_Patcher_Config.Build_Profile; + for Create_Missing_Dirs use "True"; + for Exec_Dir use "bin"; + for Main use ("version_patcher.adb"); + + package Compiler is + for Default_Switches ("Ada") use Version_Patcher_Config.Ada_Compiler_Switches; + end Compiler; + + package Binder is + for Switches ("Ada") use ("-Es"); -- Symbolic traceback + end Binder; + + package Install is + for Artifacts (".") use ("share"); + end Install; + +end Version_Patcher; From dc1bb934301e0bd0d5217928be4cb61f54def480 Mon Sep 17 00:00:00 2001 From: Alejandro R Mosteo Date: Mon, 17 Jun 2024 17:25:34 +0200 Subject: [PATCH 28/53] bugfix: improvements to temp file name creation (#1700) * bugfix: improvements to temp file name creation * fix: harmonize fake GNAT external versions in testsuite * Self-review --- src/alire/alire-directories.adb | 153 ++++++++++-------- testsuite/drivers/builds.py | 6 +- .../gnat_external/gnat_external-external.toml | 6 +- .../gnat_external/gnat_external-external.toml | 6 +- .../gnat_external/gnat_external-external.toml | 1 + testsuite/tests/pin/dir-mismatch/test.py | 1 - 6 files changed, 97 insertions(+), 76 deletions(-) diff --git a/src/alire/alire-directories.adb b/src/alire/alire-directories.adb index ec7741ed1..4c8142ce6 100644 --- a/src/alire/alire-directories.adb +++ b/src/alire/alire-directories.adb @@ -553,91 +553,98 @@ package body Alire.Directories is Epoch : constant Ada.Real_Time.Time := Ada.Real_Time.Time_Of (0, Ada.Real_Time.To_Time_Span (0.0)); - ------------- - -- Counter -- - ------------- + ---------------------- + -- Tempfile_Support -- + ---------------------- - protected Counter is - procedure Get (Value : out Interfaces.Unsigned_32); + protected Tempfile_Support is + procedure Next_Name (Name : out String); private - Next : Interfaces.Unsigned_32 := 0; - end Counter; - - protected body Counter is - procedure Get (Value : out Interfaces.Unsigned_32) is + Next_Seed : Interfaces.Unsigned_32 := 0; + Used_Names : AAA.Strings.Set; + end Tempfile_Support; + + protected body Tempfile_Support is + + --------------- + -- Next_Name -- + --------------- + + procedure Next_Name (Name : out String) is + subtype Valid_Character is Character range 'a' .. 'z'; + package Char_Random is new + Ada.Numerics.Discrete_Random (Valid_Character); + Gen : Char_Random.Generator; + + -- The default random seed has a granularity of 1 second, which is + -- not enough when we run our tests with high parallelism. Increasing + -- the resolution to nanoseconds is less collision-prone. On top, we + -- add the current working directory path to the hash input, which + -- should disambiguate even further for our most usual case which is + -- during testsuite execution, and a counter to avoid clashes in the + -- same process. + + -- It would be safer to use an atomic OS call that returns a unique + -- file name, but we would need native versions for all OSes we + -- support and that may be too much hassle? since GNAT.OS_Lib + -- doesn't do it either. + + use Ada.Real_Time; use type Interfaces.Unsigned_32; - begin - Value := Next; - Next := Next + 1; - end Get; - end Counter; - ---------- - -- Next -- - ---------- + Nano : constant String := + AAA.Strings.Replace (To_Duration (Clock - Epoch)'Image, + ".", ""); + -- This gives us an image without loss of precision and without + -- having to be worried about overflows - function Next return String is - Val : Interfaces.Unsigned_32; - begin - Counter.Get (Val); - return Val'Image; - end Next; + type Hash_Type is mod 2 ** 32; + pragma Compile_Time_Error (Hash_Type'Size > Integer'Size, + "Hash_Type is too large"); - --------------- - -- Temp_Name -- - --------------- + function Hash is new GNAT.String_Hash.Hash + (Char_Type => Character, + Key_Type => String, + Hash_Type => Hash_Type); - function Temp_Name (Length : Positive := 8) return String is - subtype Valid_Character is Character range 'a' .. 'z'; - package Char_Random is new - Ada.Numerics.Discrete_Random (Valid_Character); - Gen : Char_Random.Generator; + function To_Integer is + new Ada.Unchecked_Conversion (Hash_Type, Integer); + -- Ensure unsigned -> signed conversion doesn't bite us - -- The default random seed has a granularity of 1 second, which is not - -- enough when we run our tests with high parallelism. Increasing the - -- resolution to nanoseconds is less collision-prone. On top, we add - -- the current working directory path to the hash input, which should - -- disambiguate even further for our most usual case which is during - -- testsuite execution, and a counter to avoid clashes in the same - -- process. + Seed : constant Hash_Type := + Hash (Nano & " at " & Current & "#" & Next_Seed'Image); + begin + Next_Seed := Next_Seed + 1; - -- It would be safer to use an atomic OS call that returns a unique file - -- name, but we would need native versions for all OSes we support and - -- that may be too much hassle? since GNAT.OS_Lib doesn't do it either. + Char_Random.Reset (Gen, To_Integer (Seed)); - use Ada.Real_Time; + loop + for I in Name'Range loop + Name (I) := Char_Random.Random (Gen); + end loop; - Nano : constant String := - AAA.Strings.Replace (To_Duration (Clock - Epoch)'Image, - ".", ""); - -- This gives us an image without loss of precision and without - -- having to be worried about overflows + -- Make totally sure that not even by random chance we are reusing + -- a temporary name. - type Hash_Type is mod 2 ** 32; - pragma Compile_Time_Error (Hash_Type'Size > Integer'Size, - "Hash_Type is too large"); + exit when not Used_Names.Contains (Name); + end loop; - function Hash is new GNAT.String_Hash.Hash - (Char_Type => Character, - Key_Type => String, - Hash_Type => Hash_Type); + Used_Names.Insert (Name); + end Next_Name; - function To_Integer is new Ada.Unchecked_Conversion (Hash_Type, Integer); - -- Ensure unsigned -> signed conversion doesn't bite us + end Tempfile_Support; - Seed : constant Hash_Type := Hash (Nano & " at " & Current & "#" & Next); + --------------- + -- Temp_Name -- + --------------- + function Temp_Name (Length : Positive := 8) return String is + Result : String (1 .. Length + 4); begin - - Char_Random.Reset (Gen, To_Integer (Seed)); - - return Result : String (1 .. Length + 4) do - Result (1 .. 4) := "alr-"; - Result (Length + 1 .. Result'Last) := ".tmp"; - for I in 5 .. Length loop - Result (I) := Char_Random.Random (Gen); - end loop; - end return; + Result (1 .. 4) := "alr-"; + Result (Length + 1 .. Result'Last) := ".tmp"; + Tempfile_Support.Next_Name (Result (5 .. Length)); + return Result; end Temp_Name; ---------------- @@ -682,6 +689,16 @@ package body Alire.Directories is end if; + -- Ensure that for some bizarre reason, the temp name does not exist + -- already. + + if Adirs.Exists (+This.Name) then + Trace.Debug + ("Name clash for tempfile: " & (+This.Name) & ", retrying..."); + This.Initialize; + return; + end if; + Trace.Debug ("Selected name for tempfile: " & (+This.Name) & " when at dir: " & Current); diff --git a/testsuite/drivers/builds.py b/testsuite/drivers/builds.py index f83448a04..48ff323d9 100644 --- a/testsuite/drivers/builds.py +++ b/testsuite/drivers/builds.py @@ -7,6 +7,7 @@ from shutil import rmtree import subprocess from drivers.alr import alr_builds_dir, run_alr +from drivers.helpers import content_of def clear_builds_dir() -> None: @@ -40,7 +41,10 @@ def find_dir(crate_name: str) -> str: forward slashes in the returned folder path. """ if len(found := glob(f"{path()}/{crate_name}*/*")) != 1: - raise AssertionError(f"Unexpected number of dirs for crate {crate_name}: {found}") + raise AssertionError(f"Unexpected number of dirs for crate {crate_name}: {found}" + \ + str(['\nINPUTS:\n' + content_of(os.path.join(f, "alire", "build_hash_inputs")) \ + for f in found]) + ) return glob(f"{path()}/{crate_name}*/*")[0].replace(os.sep, "/") diff --git a/testsuite/fixtures/basic_index/gn/gnat_external/gnat_external-external.toml b/testsuite/fixtures/basic_index/gn/gnat_external/gnat_external-external.toml index f1171a6ba..b52e5b9b9 100644 --- a/testsuite/fixtures/basic_index/gn/gnat_external/gnat_external-external.toml +++ b/testsuite/fixtures/basic_index/gn/gnat_external/gnat_external-external.toml @@ -5,8 +5,8 @@ maintainers = ["alejandro@mosteo.com"] maintainers-logins = ["mosteo"] [[external]] +# Fake GNAT version that cannot conflict with any real one kind = "version-output" -# We look for make instead that should be always installed. -version-command = ["make", "--version"] -version-regexp = ".*Make ([\\d\\.]+).*" +version-command = ["echo", "1.0"] +version-regexp = "([\\d\\.]+).*" provides = "gnat" diff --git a/testsuite/fixtures/compiler_only_index/gn/gnat_external/gnat_external-external.toml b/testsuite/fixtures/compiler_only_index/gn/gnat_external/gnat_external-external.toml index f1171a6ba..b52e5b9b9 100644 --- a/testsuite/fixtures/compiler_only_index/gn/gnat_external/gnat_external-external.toml +++ b/testsuite/fixtures/compiler_only_index/gn/gnat_external/gnat_external-external.toml @@ -5,8 +5,8 @@ maintainers = ["alejandro@mosteo.com"] maintainers-logins = ["mosteo"] [[external]] +# Fake GNAT version that cannot conflict with any real one kind = "version-output" -# We look for make instead that should be always installed. -version-command = ["make", "--version"] -version-regexp = ".*Make ([\\d\\.]+).*" +version-command = ["echo", "1.0"] +version-regexp = "([\\d\\.]+).*" provides = "gnat" diff --git a/testsuite/fixtures/gnat_toolchain_index/gn/gnat_external/gnat_external-external.toml b/testsuite/fixtures/gnat_toolchain_index/gn/gnat_external/gnat_external-external.toml index 6a899f8d1..b52e5b9b9 100644 --- a/testsuite/fixtures/gnat_toolchain_index/gn/gnat_external/gnat_external-external.toml +++ b/testsuite/fixtures/gnat_toolchain_index/gn/gnat_external/gnat_external-external.toml @@ -5,6 +5,7 @@ maintainers = ["alejandro@mosteo.com"] maintainers-logins = ["mosteo"] [[external]] +# Fake GNAT version that cannot conflict with any real one kind = "version-output" version-command = ["echo", "1.0"] version-regexp = "([\\d\\.]+).*" diff --git a/testsuite/tests/pin/dir-mismatch/test.py b/testsuite/tests/pin/dir-mismatch/test.py index bdb61bbf3..43a2c54ca 100644 --- a/testsuite/tests/pin/dir-mismatch/test.py +++ b/testsuite/tests/pin/dir-mismatch/test.py @@ -3,7 +3,6 @@ """ import os -import re from drivers.alr import run_alr from drivers.asserts import assert_match From a68d748c0a3345914e7a6bf9fdc1092b960106f2 Mon Sep 17 00:00:00 2001 From: Alejandro R Mosteo Date: Tue, 18 Jun 2024 13:14:37 +0200 Subject: [PATCH 29/53] Ensure index repos can be deleted on Windows (#1696) * Ensure index repos can be deleted on Windows * Update alire-version.ads --- src/alire/alire-index_on_disk.adb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/alire/alire-index_on_disk.adb b/src/alire/alire-index_on_disk.adb index 4d6258d38..2f9901d4e 100644 --- a/src/alire/alire-index_on_disk.adb +++ b/src/alire/alire-index_on_disk.adb @@ -77,11 +77,11 @@ package body Alire.Index_On_Disk is ------------ function Delete (This : Index'Class) return Outcome is - use Ada.Directories; + package Adirs renames Ada.Directories; begin - if Exists (This.Metadata_Directory) then - if Kind (This.Metadata_Directory) = Ada.Directories.Directory then - Delete_Tree (This.Metadata_Directory); + if Adirs.Exists (This.Metadata_Directory) then + if Adirs.Kind (This.Metadata_Directory) in Adirs.Directory then + Directories.Delete_Tree (This.Metadata_Directory); Trace.Debug ("Metadata dir deleted: " & This.Metadata_Directory); else return Outcome_Failure From 9acac692f2193a2eea95e6d090e12113b2fdb029 Mon Sep 17 00:00:00 2001 From: Alejandro R Mosteo Date: Tue, 18 Jun 2024 16:43:38 +0200 Subject: [PATCH 30/53] bugfix: pin loading from out-of-root paths (#1699) * Fix bug in pin loading from out-of-root paths * Self-review --- src/alire/alire-lockfiles.adb | 10 +++++-- src/alire/alire-lockfiles.ads | 8 ++--- src/alire/alire-manifest.adb | 7 +++-- src/alire/alire-manifest.ads | 9 ++++-- src/alire/alire-publish.adb | 18 +++++++---- src/alire/alire-releases.adb | 10 ++++++- src/alire/alire-releases.ads | 8 +++-- src/alire/alire-roots-optional.adb | 8 +++-- src/alire/alire-roots.adb | 28 +++++++++++++---- src/alire/alire-roots.ads | 3 +- src/alire/alire-user_pins.adb | 6 ++++ src/alr/alr-commands.adb | 2 +- testsuite/tests/printenv/out-of-root/test.py | 30 +++++++++++++++++++ .../tests/printenv/out-of-root/test.yaml | 4 +++ 14 files changed, 121 insertions(+), 30 deletions(-) create mode 100644 testsuite/tests/printenv/out-of-root/test.py create mode 100644 testsuite/tests/printenv/out-of-root/test.yaml diff --git a/src/alire/alire-lockfiles.adb b/src/alire/alire-lockfiles.adb index 20f60109a..3982d1762 100644 --- a/src/alire/alire-lockfiles.adb +++ b/src/alire/alire-lockfiles.adb @@ -57,7 +57,11 @@ package body Alire.Lockfiles is -- Read -- ---------- - function Read (Filename : Absolute_Path) return Contents is + function Read (Root, Filename : Absolute_Path) return Contents is + -- Enter the workspace root for this lockfile, so any + -- relative pin paths can be properly resolved. + use Alire.Directories; + CWD : Guard (Enter (Root)) with Unreferenced; begin Trace.Debug ("Reading persistent contents from " & Filename); @@ -93,7 +97,7 @@ package body Alire.Lockfiles is -- Validity -- -------------- - function Validity (File : Any_Path) return Validities is + function Validity (Root, File : Absolute_Path) return Validities is begin if not GNAT.OS_Lib.Is_Read_Accessible_File (File) then return Missing; @@ -102,7 +106,7 @@ package body Alire.Lockfiles is -- Try to load to assess validity declare - Unused : constant Contents := Read (File); + Unused : constant Contents := Read (Root, File); begin return Valid; end; diff --git a/src/alire/alire-lockfiles.ads b/src/alire/alire-lockfiles.ads index 01a219d62..37dde7730 100644 --- a/src/alire/alire-lockfiles.ads +++ b/src/alire/alire-lockfiles.ads @@ -27,11 +27,11 @@ package Alire.Lockfiles is -- Return the location /path/to/crate/dir/alire.lock, filename included, -- given the root directory where the crate is deployed. - function Read (Filename : Absolute_Path) return Contents; - -- Read contents from the given lockfile + function Read (Root, Filename : Absolute_Path) return Contents; + -- Read contents from the given lockfile, for a crate rooted at Root - function Validity (File : Any_Path) return Validities; - -- Check if given file is a valid lockfile + function Validity (Root, File : Absolute_Path) return Validities; + -- Check if given file is a valid lockfile, for a crate at Root procedure Write (Contents : Lockfiles.Contents; Filename : Absolute_Path); diff --git a/src/alire/alire-manifest.adb b/src/alire/alire-manifest.adb index 3a229294e..19a77fc2b 100644 --- a/src/alire/alire-manifest.adb +++ b/src/alire/alire-manifest.adb @@ -53,11 +53,14 @@ package body Alire.Manifest is -- Is_Valid -- -------------- - function Is_Valid (Name : Any_Path; Source : Sources) return Boolean is + function Is_Valid (Name : Any_Path; + Source : Sources; + Root : Any_Path := "") + return Boolean is begin -- Check we are able to load the manifest file if Releases.From_Manifest - (Name, Source, Strict => False).Version_Image /= "" + (Name, Source, Strict => False, Root_Path => Root).Version_Image /= "" then Trace.Debug ("Checked valid manifest at " & Name); return True; diff --git a/src/alire/alire-manifest.ads b/src/alire/alire-manifest.ads index c7c1f7699..8709195e0 100644 --- a/src/alire/alire-manifest.ads +++ b/src/alire/alire-manifest.ads @@ -27,7 +27,12 @@ package Alire.Manifest is -- As removal of dependencies, but for pins. If the pin is not found, or -- it cannot be safely removed, it will raise. - function Is_Valid (Name : Any_Path; Source : Sources) return Boolean; - -- Check that the given Name is a loadable manifest + function Is_Valid (Name : Any_Path; + Source : Sources; + Root : Any_Path := "") + return Boolean + with Pre => Source /= Local or else Check_Absolute_Path (Root); + -- Check that the given Name is a loadable manifest. For a local manifest + -- that may contain links, we need the root path. end Alire.Manifest; diff --git a/src/alire/alire-publish.adb b/src/alire/alire-publish.adb index 0edcc4b2f..907b751fa 100644 --- a/src/alire/alire-publish.adb +++ b/src/alire/alire-publish.adb @@ -41,6 +41,7 @@ with TOML_Slicer; package body Alire.Publish is + package Adirs renames Ada.Directories; package Semver renames Semantic_Versioning; use all type UString; @@ -390,7 +391,8 @@ package body Alire.Publish is Check_Release (Releases.From_Manifest (Starting_Manifest (Context), Alire.Manifest.Local, - Strict => True)); + Strict => True, + Root_Path => Adirs.Full_Name (+Context.Path))); -- Will have raised if the release is not loadable or incomplete else declare @@ -612,7 +614,8 @@ package body Alire.Publish is procedure Prepare_Archive (Context : in out Data) with Pre => Alire.Manifest.Is_Valid (Context.Options.Manifest, - Alire.Manifest.Local); + Alire.Manifest.Local, + Adirs.Full_Name (+Context.Path)); -- Prepare a tar file either using git archive (if git repo detected) or -- plain tar otherwise. @@ -622,9 +625,11 @@ package body Alire.Publish is Target_Dir : constant Relative_Path := Paths.Working_Folder_Inside_Root / "archives"; Release : constant Releases.Release := - Releases.From_Manifest (Context.Options.Manifest, - Alire.Manifest.Local, - Strict => True); + Releases.From_Manifest + (Context.Options.Manifest, + Alire.Manifest.Local, + Strict => True, + Root_Path => Adirs.Full_Name (+Context.Path)); Milestone : constant String := TOML_Index.Manifest_File (Release.Name, Release.Version, @@ -788,7 +793,8 @@ package body Alire.Publish is .From_Manifest (Packaged_Manifest (Context), Alire.Manifest.Local, - Strict => True) + Strict => True, + Root_Path => Adirs.Full_Name (+Context.Path)) .Replacing (Origin => Context.Origin); begin Check_Release (Release); diff --git a/src/alire/alire-releases.adb b/src/alire/alire-releases.adb index f616c4e0d..9751795c4 100644 --- a/src/alire/alire-releases.adb +++ b/src/alire/alire-releases.adb @@ -966,9 +966,17 @@ package body Alire.Releases is function From_Manifest (File_Name : Any_Path; Source : Manifest.Sources; - Strict : Boolean) + Strict : Boolean; + Root_Path : Any_Path := "") return Release is + -- Move to file base dir, as relative paths in pins are resolved during + -- loading relative to CWD. + CWD : Directories.Guard + (if Root_Path /= "" then + Directories.Enter (Root_Path) + else + Directories.Stay) with Unreferenced; begin return From_TOML (TOML_Adapters.From diff --git a/src/alire/alire-releases.ads b/src/alire/alire-releases.ads index bf679b7fe..f0ed1a32a 100644 --- a/src/alire/alire-releases.ads +++ b/src/alire/alire-releases.ads @@ -323,8 +323,12 @@ package Alire.Releases is function From_Manifest (File_Name : Any_Path; Source : Manifest.Sources; - Strict : Boolean) - return Release; + Strict : Boolean; + Root_Path : Any_Path := "") + return Release + with Pre => Source in Manifest.Index or else Root_Path in Absolute_Path; + -- When loading a manifest for a workspace, it may contain pins that we + -- must resolve relative to Root_Path. function From_TOML (From : TOML_Adapters.Key_Queue; Source : Manifest.Sources; diff --git a/src/alire/alire-roots-optional.adb b/src/alire/alire-roots-optional.adb index 0d3134e41..84d27ba78 100644 --- a/src/alire/alire-roots-optional.adb +++ b/src/alire/alire-roots-optional.adb @@ -51,9 +51,11 @@ package body Alire.Roots.Optional is return This : constant Root := Outcome_Success (Roots.New_Root - (R => Releases.From_Manifest (Crate_File, - Manifest.Local, - Strict => True), + (R => Releases.From_Manifest + (Crate_File, + Manifest.Local, + Strict => True, + Root_Path => Directories.Current), Path => Directories.Current, Env => Alire.Root.Platform_Properties)) do diff --git a/src/alire/alire-roots.adb b/src/alire/alire-roots.adb index be9ea1d43..5b60531da 100644 --- a/src/alire/alire-roots.adb +++ b/src/alire/alire-roots.adb @@ -1177,7 +1177,9 @@ package body Alire.Roots is return "Expected ordinary manifest file but found a: " & Kind (This.Crate_File)'Img; - elsif not Alire.Manifest.Is_Valid (This.Crate_File, Alire.Manifest.Local) + elsif not Alire.Manifest.Is_Valid (This.Crate_File, + Alire.Manifest.Local, + Path (This)) then return "Manifest is not loadable: " & This.Crate_File; end if; @@ -1201,6 +1203,11 @@ package body Alire.Roots is ------------------------------ procedure Export_Build_Environment (This : in out Root) is + CWD : Directories.Guard (Directories.Enter (Path (This))) + with Unreferenced; + -- Required as this function gets called sometimes directly from + -- commands that may not have relocated to the crate root. + Context : Alire.Environment.Context; begin Alire.Environment.Loading.Load (Context, This); @@ -1342,6 +1349,15 @@ package body Alire.Roots is function Solution (This : in out Root) return Solutions.Solution is + -- Enter the lockfile parent dir, which will be the crate root, so any + -- relative pin paths can be properly resolved, if the lockfile is not + -- yet loaded. + use Alire.Directories; + CWD : Guard (if This.Cached_Solution.Has_Element + then Stay + else Enter (Parent (Parent (This.Lock_File)))) + with Unreferenced; + Result : constant Cached_Solutions.Cached_Info := This.Cached_Solution.Element (This.Lock_File); begin @@ -1650,9 +1666,10 @@ package body Alire.Roots is -- speeds up things greatly and both should be in sync if things -- are as they should. or else - (if Check_Valid - then Lockfiles.Validity (This.Lock_File) in Lockfiles.Valid - else Ada.Directories.Exists (This.Lock_File))); + (if Check_Valid then + Lockfiles.Validity (Path (This), This.Lock_File) in Lockfiles.Valid + else + Ada.Directories.Exists (This.Lock_File))); -------------------------- -- Is_Lockfile_Outdated -- @@ -2023,7 +2040,8 @@ package body Alire.Roots is (Releases.From_Manifest (This.Crate_File, Manifest.Local, - Strict => True)); + Strict => True, + Root_Path => Path (This))); -- And our pins diff --git a/src/alire/alire-roots.ads b/src/alire/alire-roots.ads index 47d79308d..65ec0d097 100644 --- a/src/alire/alire-roots.ads +++ b/src/alire/alire-roots.ads @@ -374,7 +374,8 @@ private -- triggered when doing This.Configuration here. function Load_Solution (Lockfile : Absolute_Path) return Solutions.Solution - is (Lockfiles.Read (Lockfile).Solution); + is (Lockfiles.Read (Directories.Current, Lockfile).Solution); + -- Note that this function requires CWD to already be the crate root procedure Write_Solution (Solution : Solutions.Solution; Lockfile : Absolute_Path); diff --git a/src/alire/alire-user_pins.adb b/src/alire/alire-user_pins.adb index ea0c9ffc3..adf22e83d 100644 --- a/src/alire/alire-user_pins.adb +++ b/src/alire/alire-user_pins.adb @@ -408,6 +408,12 @@ package body Alire.User_Pins is Result.Path := +Utils.User_Input.To_Absolute_From_Portable (This.Checked_Pop (Keys.Path, TOML_String).As_String); + + if not GNAT.OS_Lib.Is_Directory (+Result.Path) then + This.Recoverable_Error + ("Pin path is not a valid directory: " + & (+Result.Path)); + end if; end return; end if; end From_Lockfile; diff --git a/src/alr/alr-commands.adb b/src/alr/alr-commands.adb index 43a50e3fa..9ce783564 100644 --- a/src/alr/alr-commands.adb +++ b/src/alr/alr-commands.adb @@ -363,7 +363,7 @@ package body Alr.Commands is -- For workspaces created pre-lockfiles, or with older format, -- recreate: - case Lockfiles.Validity (Checked.Lock_File) is + case Lockfiles.Validity (Checked.Path, Checked.Lock_File) is when Lockfiles.Valid => Trace.Debug ("Lockfile at " & Checked.Lock_File & " is valid"); diff --git a/testsuite/tests/printenv/out-of-root/test.py b/testsuite/tests/printenv/out-of-root/test.py new file mode 100644 index 000000000..90e05de26 --- /dev/null +++ b/testsuite/tests/printenv/out-of-root/test.py @@ -0,0 +1,30 @@ +""" +Verify that printenv output is correct for regular and pinned crates when +invoked not from the root of the workspace. +""" + +import os +import re +from drivers.alr import alr_pin, init_local_crate, run_alr +from drivers.asserts import assert_eq, assert_match + +parent = os.getcwd() + +init_local_crate("base") +init_local_crate("pinned", enter=False) +alr_pin("pinned", path="pinned") + +os.chdir("src") # Enter a subfolder +p = run_alr("printenv") + +# Verify root crate proper path in GPR_PROJECT_PATH +assert_match(r".*GPR_PROJECT_PATH[^\n]+" + + re.escape(os.path.join(parent, "base")) + + "(:|;|\")", p.out) + +# Verify pinned crate proper path in GPR_PROJECT_PATH +assert_match(r".*GPR_PROJECT_PATH[^\n]+" + + re.escape(os.path.join(parent, "base", "pinned")) + + "(:|;|\")", p.out) + +print("SUCCESS") diff --git a/testsuite/tests/printenv/out-of-root/test.yaml b/testsuite/tests/printenv/out-of-root/test.yaml new file mode 100644 index 000000000..702010525 --- /dev/null +++ b/testsuite/tests/printenv/out-of-root/test.yaml @@ -0,0 +1,4 @@ +driver: python-script +build_mode: both +indexes: + compiler_only_index: {} From 2e7b0c6fa5f5cbe4588de9972931ebc7a8f45996 Mon Sep 17 00:00:00 2001 From: Alejandro R Mosteo Date: Sun, 23 Jun 2024 15:22:35 +0200 Subject: [PATCH 31/53] fix: test runs with recent Docker images (#1704) * fix: git warnings during test runs * Fix semver parsing in APT and From_Output * Fix ownership of local git repo used during tests Using the --shared feature of init * Fix toolchain workflow to ensure use of intended toolchain --- .github/workflows/ci-docker.yml | 2 +- .github/workflows/ci-toolchain.yml | 27 ++++++- alire.toml | 10 +-- deps/semantic_versioning | 2 +- dev/build.sh | 4 +- scripts/ci-github.sh | 23 ++++-- scripts/version-patcher.sh | 2 +- src/alire/alire-externals-from_output.adb | 29 ++++--- src/alire/alire-externals-from_system.adb | 71 +++++++++++------- .../alire-origins-deployers-system-apt.adb | 13 ++++ testsuite/fixtures/crates/libfoo_git/HEAD | 2 +- testsuite/fixtures/crates/libfoo_git/config | 3 + .../07/2d593793eda4efbb0af13f3fa422d72f5492d3 | Bin 18 -> 0 bytes .../9a/6aa1471db1a0c821570db875b12f08653f623c | 1 - .../f4/fdb928df3b0be3f896b18be4987ce7338f0a54 | Bin 127 -> 0 bytes .../fixtures/crates/libfoo_git/packed-refs | 2 - testsuite/fixtures/crates/libhello_git | 1 - .../my_index/crates/crate}/.emptydir | 0 .../index/cr/crate1/crate1-external.toml | 10 +++ .../index/cr/crate2/crate2-external.toml | 10 +++ .../bad-versions/my_index/index/index.toml | 1 + .../tests/externals/bad-versions/test.py | 18 +++++ .../tests/externals/bad-versions/test.yaml | 5 ++ 23 files changed, 167 insertions(+), 69 deletions(-) delete mode 100644 testsuite/fixtures/crates/libfoo_git/objects/07/2d593793eda4efbb0af13f3fa422d72f5492d3 delete mode 100644 testsuite/fixtures/crates/libfoo_git/objects/9a/6aa1471db1a0c821570db875b12f08653f623c delete mode 100644 testsuite/fixtures/crates/libfoo_git/objects/f4/fdb928df3b0be3f896b18be4987ce7338f0a54 delete mode 100644 testsuite/fixtures/crates/libfoo_git/packed-refs delete mode 160000 testsuite/fixtures/crates/libhello_git rename testsuite/{fixtures/crates/libfoo_git/refs/heads => tests/externals/bad-versions/my_index/crates/crate}/.emptydir (100%) create mode 100644 testsuite/tests/externals/bad-versions/my_index/index/cr/crate1/crate1-external.toml create mode 100644 testsuite/tests/externals/bad-versions/my_index/index/cr/crate2/crate2-external.toml create mode 100644 testsuite/tests/externals/bad-versions/my_index/index/index.toml create mode 100644 testsuite/tests/externals/bad-versions/test.py create mode 100644 testsuite/tests/externals/bad-versions/test.yaml diff --git a/.github/workflows/ci-docker.yml b/.github/workflows/ci-docker.yml index 6808694c8..a01ad2d05 100644 --- a/.github/workflows/ci-docker.yml +++ b/.github/workflows/ci-docker.yml @@ -28,7 +28,7 @@ jobs: steps: - name: Check out repository - uses: actions/checkout@v2 + uses: actions/checkout@v4 with: submodules: true diff --git a/.github/workflows/ci-toolchain.yml b/.github/workflows/ci-toolchain.yml index 0c02a6be4..803111b91 100644 --- a/.github/workflows/ci-toolchain.yml +++ b/.github/workflows/ci-toolchain.yml @@ -1,6 +1,10 @@ name: CI self+toolchain # Build Alire with `alr build` and using a toolchain installed from Alire # The `alr` being tested is the one which is being submitted in the PR +# Toolchain is tested from `alr install` (1st build) and `alr build` (2nd +# build), so those are two different toolchain installations of the same +# toolchain. This way we not only test that alr builds itself, but that +# toolchain installations via `alr install` work as intented. on: pull_request: @@ -40,12 +44,22 @@ jobs: with: crates: gnat_native^${{matrix.gcc_version}} gprbuild - - name: Build alr with default toolchain + - name: Build alr with toolchain from `alr install` shell: bash run: dev/build.sh # We can start using the alr we just built + - name: Select toolchain GNAT^${{matrix.gcc_version}} + run: ./bin/alr -d -n toolchain --select gnat_native^${{matrix.gcc_version}} gprbuild + + - name: Show builder alr configuration + run: ./bin/alr -d -n version + + - name: Verify proper toolchain used for 1st build + shell: bash + run: ./bin/alr -d -n version | grep 'compiled with version' | grep -q '${{ matrix.gcc_version }}' + - name: Update dependencies run: ./bin/alr -d -n update @@ -55,7 +69,8 @@ jobs: - name: Show build environment, with debug fallback run: ./bin/alr -d -n printenv || ./bin/alr -n -v -d printenv - - shell: bash + - name: Move ./bin to ./bin-old to allow for self-build + shell: bash run: mv ./bin ./bin-old || { sleep 5s && mv ./bin ./bin-old; } # Windows doesn't allow to replace a running exe so the next command # fails otherwise. Also, this mv fails sometimes so we try twice JIC. @@ -67,6 +82,10 @@ jobs: - name: Show built version run: ./bin/alr -d -n version + - name: Verify proper toolchain used for 2nd build + shell: bash + run: ./bin/alr -d -n version | grep 'compiled with version' | grep -q '${{ matrix.gcc_version }}' + # Run the testsuite with the just build alr. The testsuite picks the proper # alr in the ./bin/alr location. @@ -78,6 +97,6 @@ jobs: - name: Install e3 run: pip install --upgrade -r testsuite/requirements.txt - - name: Run testsuite - run: cd testsuite; ./run.py -E + - name: Run testsuite # But ensure a new alr is not build + run: scripts/ci-github.sh build=false shell: bash diff --git a/alire.toml b/alire.toml index 7cee2636d..8821dc68a 100644 --- a/alire.toml +++ b/alire.toml @@ -61,11 +61,11 @@ commit = "56bbdc008e16996b6f76e443fd0165a240de1b13" [pins.dirty_booleans] url = "https://github.com/mosteo/dirty_booleans" -branch = "alire" +commit = "05c40d88ecfe109e575ec8b21dd6ffa2e61df1dc" [pins.diskflags] url = "https://github.com/mosteo/diskflags" -branch = "alire" +commit = "60729edf31816aca0036b13b2794c39a9bd0172e" [pins.gnatcoll] url = "https://github.com/alire-project/gnatcoll-core.git" @@ -77,7 +77,7 @@ commit = "9a9c660f9c6f27f5ef75417e7fac7061dff14d78" [pins.semantic_versioning] url = "https://github.com/alire-project/semantic_versioning" -commit = "cc2148cf9c8934fb557b5ae49a3f7947194fa7ee" +commit = "4861e32bd8a2f0df038d3ecc9a72b6381e7a34cc" [pins.simple_logging] url = "https://github.com/alire-project/simple_logging" @@ -85,7 +85,7 @@ commit = "3505dc645f3eef6799a486aae223d37e88cfc4d5" [pins.si_units] url = "https://github.com/mosteo/si_units" -branch = "alire" +commit = "9329d2591b82440ccc859a53f1380ac07ea4194d" [pins.stopwatch] url = "https://github.com/mosteo/stopwatch" @@ -93,7 +93,7 @@ commit = "f607a63b714f09bbf6126de9851cbc21cf8666c9" [pins.toml_slicer] url = "https://github.com/mosteo/toml_slicer" -branch = "alire" +commit = "3e5cbdb5673b85a1da6344a41764ef1cbafe3289" # To disable version updating, export ALR_VERSION_DONT_PATCH with any value diff --git a/deps/semantic_versioning b/deps/semantic_versioning index cc2148cf9..4861e32bd 160000 --- a/deps/semantic_versioning +++ b/deps/semantic_versioning @@ -1 +1 @@ -Subproject commit cc2148cf9c8934fb557b5ae49a3f7947194fa7ee +Subproject commit 4861e32bd8a2f0df038d3ecc9a72b6381e7a34cc diff --git a/dev/build.sh b/dev/build.sh index 934986401..336f043c0 100755 --- a/dev/build.sh +++ b/dev/build.sh @@ -10,7 +10,7 @@ ALIRE_OS=$(get_OS); export ALIRE_OS scripts/version-patcher.sh -echo "Building with ALIRE_OS=$ALIRE_OS..." +echo "Building with ALIRE_OS=$ALIRE_OS and $(gnat --version | head -1)" gprbuild "-j$ALIRE_BUILD_JOBS" -r -p -P "$(dirname $0)/../alr_env.gpr" "$@" -scripts/version-patcher.sh _or_later \ No newline at end of file +scripts/version-patcher.sh _or_later diff --git a/scripts/ci-github.sh b/scripts/ci-github.sh index 845cd96d0..06800270e 100755 --- a/scripts/ci-github.sh +++ b/scripts/ci-github.sh @@ -13,18 +13,29 @@ pushd "$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" . ../dev/functions.sh popd -# Mark location safe to assuage git if necessary (happens in some distros) +# Mark location safe to assuage git if necessary (happens under docker as we +# run with a different user). if git status 2>&1 | grep -q "dubious ownership"; then - echo "Marking $PWD as safe for git" - git config --global --add safe.directory "$PWD" + echo "Marking $PWD as safe for git" + git config --global --add safe.directory "$PWD" + + # Change ownership and group to current user of everything in the testsuite, + # as we have there some pre-created git repositories that would fail too. + # These are copied to temporary locations by the test runner, so we cannot + # simply use the `git config` trick. + sudo chown -R $(id -u):$(id -g) testsuite fi # Patch version scripts/version-patcher.sh -# Build alr -export ALIRE_OS=$(get_OS) -gprbuild -j0 -p -P alr_env +# Build alr if no argument is "build=false" +if [[ " $* " == *" build=false "* ]]; then + echo "Skipping alr build, explicitly disabled via arguments" +else + export ALIRE_OS=$(get_OS) + gprbuild -j0 -p -P alr_env +fi # Disable distro detection if supported if [ "${ALIRE_DISABLE_DISTRO:-}" == "true" ]; then diff --git a/scripts/version-patcher.sh b/scripts/version-patcher.sh index 1b0c07895..9e666d56e 100755 --- a/scripts/version-patcher.sh +++ b/scripts/version-patcher.sh @@ -28,4 +28,4 @@ fi $bin "$@" echo "Resulting version file:" -cat src/alire/alire-version.ads | grep Current_Str \ No newline at end of file +cat src/alire/alire-version.ads | grep "Current_Str : constant String" \ No newline at end of file diff --git a/src/alire/alire-externals-from_output.adb b/src/alire/alire-externals-from_output.adb index 97c8b7dfc..965b0757b 100644 --- a/src/alire/alire-externals-from_output.adb +++ b/src/alire/alire-externals-from_output.adb @@ -7,6 +7,7 @@ with Alire.OS_Lib.Subprocess; with Alire.Paths; with Alire.Releases; with Alire.TOML_Keys; +with Alire.Utils.Regex; with Alire.Utils.TTY; with Semantic_Versioning; @@ -48,20 +49,13 @@ package body Alire.Externals.From_Output is end if; declare - use GNAT.Regpat; - Matches : Match_Array (1 .. Match_Count'Last); - -- Although we should have at most one match, it turns out that the - -- match won't necessarily be on the first position in the array, as - -- it depends on the number of () expressions. - Lines : AAA.Strings.Vector; Status : constant Integer := OS_Lib.Subprocess.Unchecked_Spawn_And_Capture (This.Command.First_Element, This.Command.Tail, Lines); - Output : constant String := - Lines.Flatten ("" & ASCII.LF); + Output : constant String := Lines.Flatten ("" & ASCII.LF); -- ASCII.LF is used by Regpat for new lines begin if Status /= 0 then @@ -73,13 +67,13 @@ package body Alire.Externals.From_Output is Trace.Debug ("Looking for external in version string '" & Output & "'"); - Match (This.Regexp, Output, Matches); - for I in Matches'Range loop - if Matches (I) /= No_Match then + declare + Version : constant String := + Utils.Regex.First_Match (+This.Regstr, Output); + begin + if Version /= "" then declare - Version : constant String := - Output (Matches (I).First .. Matches (I).Last); Path : constant Any_Path := OS_Lib.Subprocess.Locate_In_Path (This.Command.First_Element); @@ -89,7 +83,8 @@ package body Alire.Externals.From_Output is Result.Insert (Index.Crate (Name, Index.Query_Mem_Only).Base - .Retagging (Semantic_Versioning.Parse (Version)) + .Retagging (Semantic_Versioning.Parse + (Version, Relaxed => True)) .Providing (This.Provides) .Replacing (Origins.New_External ("path " & Path)) .Replacing (Notes => "Detected at " -- length is 12 @@ -97,8 +92,12 @@ package body Alire.Externals.From_Output is (String (Path), Max_Description_Length - 12))); end; + else + Trace.Debug + ("Failed to match a version using regexp '" + & (+This.Regstr) & "' on output: " & Version); end if; - end loop; + end; end; return Result; diff --git a/src/alire/alire-externals-from_system.adb b/src/alire/alire-externals-from_system.adb index b0edceedb..88bdfabc3 100644 --- a/src/alire/alire-externals-from_system.adb +++ b/src/alire/alire-externals-from_system.adb @@ -39,36 +39,49 @@ package body Alire.Externals.From_System is else for Candidate of Origin.Value.Packages loop Trace.Detail ("Looking for system package: " & Candidate); - declare - Detector : constant System.Deployer'Class := - System.Platform_Deployer (Candidate); - Result : constant System.Version_Outcomes.Outcome := - Detector.Detect; begin - if Result.Success then - Trace.Detail ("Success with system package: " - & Candidate); - - -- A system package may be found more than once if - -- transitional and proper package names are given - -- for detection of the same system package. - declare - Release : constant Alire.Releases.Release - := Index.Crate (Name, Index.Query_Mem_Only).Base - .Retagging (Result.Value) - .Providing (This.Provides) - .Replacing (Origins.New_System (Candidate)) - .Replacing (Notes => "Provided by system package:" - & " " & Candidate); - begin - if Releases.Contains (Release) then - Trace.Debug ("Not readding same system package: " - & Release.Milestone.Image); - else - Releases.Insert (Release); - end if; - end; - end if; + declare + Detector : constant System.Deployer'Class := + System.Platform_Deployer (Candidate); + Result : constant System.Version_Outcomes.Outcome := + Detector.Detect; + begin + if Result.Success then + Trace.Detail ("Success with system package: " + & Candidate); + + -- A system package may be found more than once if + -- transitional and proper package names are given + -- for detection of the same system package. + declare + Release : constant Alire.Releases.Release + := Index.Crate (Name, + Index.Query_Mem_Only) + .Base + .Retagging (Result.Value) + .Providing (This.Provides) + .Replacing (Origins.New_System (Candidate)) + .Replacing + (Notes => "Provided by system package:" + & " " & Candidate); + begin + if Releases.Contains (Release) then + Trace.Debug + ("Not readding same system package: " + & Release.Milestone.Image); + else + Releases.Insert (Release); + end if; + end; + end if; + end; + exception + when E : others => + Trace.Debug + ("Error attempting version detection of system " + & "package [" & Candidate & "] for crate: " + & (+Name)); + Log_Exception (E); end; end loop; end if; diff --git a/src/alire/alire-origins-deployers-system-apt.adb b/src/alire/alire-origins-deployers-system-apt.adb index 8406e7d76..f81c63d39 100644 --- a/src/alire/alire-origins-deployers-system-apt.adb +++ b/src/alire/alire-origins-deployers-system-apt.adb @@ -72,6 +72,19 @@ package body Alire.Origins.Deployers.System.Apt is Trace.Debug ("Unexpected version format, could not identify version"); end if; + exception + -- We do not really want to disturb users for a problem + -- introduced externally by some new package version in the + -- underlying distro. This will make the problem harder to + -- detect, but eventually someone should notice that a package + -- is not being detected as intended. + when Constraint_Error | Semantic_Versioning.Malformed_Input => + Trace.Debug + ("Unexpected error while parsing version from: " + & Match & " in line " & Line & " in pkg " + & This.Base.Package_Name); + return Version_Outcomes.Outcome_Failure + ("could not be detected", Report => False); end; end if; end loop; diff --git a/testsuite/fixtures/crates/libfoo_git/HEAD b/testsuite/fixtures/crates/libfoo_git/HEAD index cb089cd89..b870d8262 100644 --- a/testsuite/fixtures/crates/libfoo_git/HEAD +++ b/testsuite/fixtures/crates/libfoo_git/HEAD @@ -1 +1 @@ -ref: refs/heads/master +ref: refs/heads/main diff --git a/testsuite/fixtures/crates/libfoo_git/config b/testsuite/fixtures/crates/libfoo_git/config index 07d359d07..164a3ce26 100644 --- a/testsuite/fixtures/crates/libfoo_git/config +++ b/testsuite/fixtures/crates/libfoo_git/config @@ -2,3 +2,6 @@ repositoryformatversion = 0 filemode = true bare = true + sharedrepository = 2 +[receive] + denyNonFastforwards = true diff --git a/testsuite/fixtures/crates/libfoo_git/objects/07/2d593793eda4efbb0af13f3fa422d72f5492d3 b/testsuite/fixtures/crates/libfoo_git/objects/07/2d593793eda4efbb0af13f3fa422d72f5492d3 deleted file mode 100644 index eae9323f7044afd2a8f7476e1c645694655608bf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 18 ZcmbV$TF_24NNEL;yc01-SqK diff --git a/testsuite/fixtures/crates/libfoo_git/objects/9a/6aa1471db1a0c821570db875b12f08653f623c b/testsuite/fixtures/crates/libfoo_git/objects/9a/6aa1471db1a0c821570db875b12f08653f623c deleted file mode 100644 index 30748e72a..000000000 --- a/testsuite/fixtures/crates/libfoo_git/objects/9a/6aa1471db1a0c821570db875b12f08653f623c +++ /dev/null @@ -1 +0,0 @@ -x¥Í½ @agžâî& Z.Icttpñ ø¹DL„Òŧ7©àv¦ïøšsê Œ<õFQÅà̈!JÇɈfv)ƒÚ“–#·“bvï¯Úà¶ÒÛ–Ð*<xÔ­S…Åæ#®{IÛÚ. &Í*=#œùÈ9óǸÓ»—Ô“]ág±/žÔ?µ \ No newline at end of file diff --git a/testsuite/fixtures/crates/libfoo_git/objects/f4/fdb928df3b0be3f896b18be4987ce7338f0a54 b/testsuite/fixtures/crates/libfoo_git/objects/f4/fdb928df3b0be3f896b18be4987ce7338f0a54 deleted file mode 100644 index 64745dc27b2f42bfd85ff3765f1d55806a6cf37f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 127 zcmV-_0D%8^0V^p=O;s>7G-EI{FfcPQQAlK{m?5c@Xsz`-L6t@S+uruBgEReFO$>lQ zA&KF1px^54uIoSTTq=6z$&p*D)`Y!X57n5=@NDj**_$q2bN24mTvr}- Date: Sun, 23 Jun 2024 18:27:10 +0200 Subject: [PATCH 32/53] feat: improve VSCode launch command (#1703) * Improve VSCode launch command * Refactor replacement patterns to common package --- src/alire/alire-environment-formatting.adb | 72 ++++++++++++++++++---- src/alire/alire-environment-formatting.ads | 43 ++++++++++++- src/alire/alire-environment-loading.adb | 5 +- src/alr/alr-commands-edit.adb | 45 ++++++-------- 4 files changed, 124 insertions(+), 41 deletions(-) diff --git a/src/alire/alire-environment-formatting.adb b/src/alire/alire-environment-formatting.adb index 4f8029f7a..5d7105c4f 100644 --- a/src/alire/alire-environment-formatting.adb +++ b/src/alire/alire-environment-formatting.adb @@ -1,3 +1,5 @@ +with AAA.Enum_Tools; + with Ada.Strings.Unbounded; use Ada.Strings.Unbounded; with Alire.OS_Lib; @@ -5,6 +7,50 @@ with Alire.Platforms.Current; package body Alire.Environment.Formatting is + -------------- + -- Contains -- + -------------- + + overriding + function Contains (This : Replacements; Pattern : Patterns) return Boolean + is (Pattern_String_Maps.Map (This).Contains (Pattern)); + + ----------- + -- Value -- + ----------- + + function Value (This : Replacements; Pattern : Patterns) return String + is (This (Pattern)); + + ------------------------------ + -- For_Manifest_Environment -- + ------------------------------ + + function For_Manifest_Environment (Crate_Root : Any_Path) + return Replacements + is + Result : Replacements; + begin + Result.Insert (Distrib_Root, Alire.Platforms.Current.Distribution_Root); + Result.Insert (Formatting.Crate_Root, Crate_Root); + + return Result; + end For_Manifest_Environment; + + ---------------- + -- For_Editor -- + ---------------- + + function For_Editor (Root : Alire.Roots.Root; + Prj_File : Relative_Path) + return Replacements + is + Result : Replacements := For_Manifest_Environment (Root.Path); + begin + Result.Insert (GPR_File, Prj_File); + return Result; + end For_Editor; + ---------------- -- Find_Start -- ---------------- @@ -52,8 +98,9 @@ package body Alire.Environment.Formatting is -- Format -- ------------ - function Format (Release_Dir : Any_Path; - Value : String) + function Format (Item : String; + Repl : Replacements; + Is_Path : Boolean) return String is ------------- @@ -63,20 +110,16 @@ package body Alire.Environment.Formatting is procedure Replace (Str : in out Unbounded_String; From, To : Positive) is + function Is_Known is new AAA.Enum_Tools.Is_Valid (Patterns); Id : constant String := Slice (Str, From + 2, To - 1); begin - - if Id = "DISTRIB_ROOT" then - Replace_Slice (Str, From, To, Platforms.Current.Distribution_Root); - - elsif Id = "CRATE_ROOT" then - Replace_Slice - (Str, From, To, - Release_Dir); + if Is_Known (Id) then + Replace_Slice (Str, From, To, Repl (Patterns'Value (Id))); elsif Id = "_ALIRE_TEST_" then -- This is used to test the env var formatting feature Replace_Slice (Str, From, To, "TEST"); + else raise Unknown_Formatting_Key; end if; @@ -99,7 +142,7 @@ package body Alire.Environment.Formatting is return AAA.Strings.Replace (S, "/", "" & OS_Lib.Dir_Separator); end To_Native; - Result : Unbounded_String := To_Unbounded_String (Value); + Result : Unbounded_String := To_Unbounded_String (Item); From : Natural := 1; To : Natural; begin @@ -126,7 +169,12 @@ package body Alire.Environment.Formatting is end loop; -- For final usage, we use the native separator - return To_Native (+Result); + + if Is_Path then + return To_Native (+Result); + else + return +Result; + end if; end Format; end Alire.Environment.Formatting; diff --git a/src/alire/alire-environment-formatting.ads b/src/alire/alire-environment-formatting.ads index 0a28f0027..24d3d4b9e 100644 --- a/src/alire/alire-environment-formatting.ads +++ b/src/alire/alire-environment-formatting.ads @@ -1,10 +1,47 @@ +with Alire.Roots; + +private with Ada.Containers.Indefinite_Ordered_Maps; + package Alire.Environment.Formatting is - function Format (Release_Dir : Any_Path; - Value : String) + type Patterns is (Crate_Root, + Distrib_Root, + GPR_File); + -- These correspond directly with ${PATTERNs} that can be replaced + + function Dollar_Image (Pattern : Patterns) return String + is ("${" & Pattern'Image & "}"); + + type Replacements (<>) is tagged private; + + function For_Manifest_Environment (Crate_Root : Any_Path) + return Replacements; + -- Crate_Root can't be an absolute path as this may be called with relative + -- paths during build hashing. + + function For_Editor (Root : Alire.Roots.Root; + Prj_File : Relative_Path) + return Replacements; + + function Contains (This : Replacements; Pattern : Patterns) return Boolean; + + function Value (This : Replacements; Pattern : Patterns) return String + with Pre => This.Contains (Pattern); + + function Format (Item : String; + Repl : Replacements; + Is_Path : Boolean) return String; - -- Format the environment variable value with ${} replacement patterns + -- If Is_Path, a final pass is done to use platform-specific dir separators + -- Format the item with ${} replacement patterns. Unknown_Formatting_Key : exception; +private + + package Pattern_String_Maps is new + Ada.Containers.Indefinite_Ordered_Maps (Patterns, String); + + type Replacements is new Pattern_String_Maps.Map with null record; + end Alire.Environment.Formatting; diff --git a/src/alire/alire-environment-loading.adb b/src/alire/alire-environment-loading.adb index 7c2a72071..16890a0ef 100644 --- a/src/alire/alire-environment-loading.adb +++ b/src/alire/alire-environment-loading.adb @@ -122,7 +122,10 @@ package body Alire.Environment.Loading is begin declare Value : constant String := - Formatting.Format (Release_Base, Act.Value); + Formatting.Format + (Act.Value, + Formatting.For_Manifest_Environment (Release_Base), + Is_Path => True); begin case Act.Action is diff --git a/src/alr/alr-commands-edit.adb b/src/alr/alr-commands-edit.adb index dbfc1a579..95bf41821 100644 --- a/src/alr/alr-commands-edit.adb +++ b/src/alr/alr-commands-edit.adb @@ -1,16 +1,17 @@ with Ada.Containers; with Alire; use Alire; +with Alire.Environment.Formatting; with Alire.Settings.Builtins; with Alire.OS_Lib.Subprocess; with Alire.Platforms.Current; with CLIC.User_Input; -with Ada.Strings.Unbounded; use Ada.Strings.Unbounded; - package body Alr.Commands.Edit is + package Format renames Alire.Environment.Formatting; + Switch_Select : constant String := "--select-editor"; -------------------- @@ -79,8 +80,10 @@ package body Alr.Commands.Edit is function Cmd (E : Editor_With_Command) return String is (case E is - when VScode => "code ${GPR_FILE}", - when GNATstudio => "gnatstudio -P ${GPR_FILE}"); + when VScode => + "code " & Format.Dollar_Image (Format.Crate_Root), + when GNATstudio => + "gnatstudio -P " & Format.Dollar_Image (Format.GPR_File)); Choices : AAA.Strings.Vector; begin @@ -114,8 +117,10 @@ package body Alr.Commands.Edit is when Other => Trace.Always ("In your custom editor command, `alr` will replace " - & TTY.Emph ("${GPR_FILE}") - & " with the corresponding project file."); + & TTY.Emph (Format.Dollar_Image (Format.Crate_Root)) + & " and " & TTY.Emph (Format.Dollar_Image (Format.GPR_File)) + & " patterns with the workspace root or project file path, " + & "respectively."); declare Custom : constant String := Query_String ("Please enter a custom editor command", @@ -131,11 +136,10 @@ package body Alr.Commands.Edit is -- Start_Editor -- ------------------ - procedure Start_Editor (Args : in out AAA.Strings.Vector; + procedure Start_Editor (Root : in out Alire.Roots.Root; + Args : in out AAA.Strings.Vector; Prj : Relative_Path) is - Pattern : constant String := "${GPR_FILE}"; - Cmd : constant String := Args.First_Element; Replaced_Args : AAA.Strings.Vector; @@ -146,20 +150,11 @@ package body Alr.Commands.Edit is for Elt of Args loop -- Replace pattern in Elt, if any - declare - Us : Unbounded_String := +Elt; - Index : Natural; - begin - Index := Ada.Strings.Unbounded.Index (Us, Pattern); - if Index /= 0 then - Replace_Slice (Us, - Low => Index, - High => Index + Pattern'Length - 1, - By => Prj); - end if; - - Replaced_Args.Append (+Us); - end; + Replaced_Args.Append + (Format.Format + (Elt, + Format.For_Editor (Root, Prj), + Is_Path => True)); end loop; Trace.Info ("Editing crate with: ['" & Cmd & "' '" & @@ -230,7 +225,7 @@ package body Alr.Commands.Edit is ("No project file to open for this crate."); elsif Project_Files.Length = 1 then - Start_Editor (Edit_Args, Project_Files.First_Element); + Start_Editor (Cmd.Root, Edit_Args, Project_Files.First_Element); elsif Cmd.Prj = null or else @@ -245,7 +240,7 @@ package body Alr.Commands.Edit is ("Please specify a project file with --project=."); else - Start_Editor (Edit_Args, Cmd.Prj.all); + Start_Editor (Cmd.Root, Edit_Args, Cmd.Prj.all); end if; end; end Execute; From 873fda0b43d007520d9382b9f1c865683605e0e3 Mon Sep 17 00:00:00 2001 From: Lionel Draghi Date: Mon, 1 Jul 2024 13:06:55 +0200 Subject: [PATCH 33/53] Update required PAT permissions in publishing.md (#1707) When generating the PAT, checking "workflow" is needed. --- doc/publishing.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/publishing.md b/doc/publishing.md index b44d2e56c..8ec90c3c9 100644 --- a/doc/publishing.md +++ b/doc/publishing.md @@ -40,8 +40,8 @@ in beta and not documented here yet. Follow these steps to create a classic PAT: 1. Click on "Developer settings" entry at the bottom in your Settings page. 1. Click on "Personal access tokens" and then "Tokens (classic)". 1. Click on "Generate new token" and the select the classic variant. -1. In the "Select scopes" section, under "repo", check "public_repo". This is - the only permission needed for this PAT. +1. In the "Select scopes" section, under "repo", check "public_repo" and "workflow". Those are + the only permissions needed for this PAT. 1. Click on "Generate token" at the bottom. You will get the PAT string after completing the generation. From b5a896a465dc57c5d2bf87d5a583515bd4368571 Mon Sep 17 00:00:00 2001 From: Gautier de Montmollin Date: Mon, 1 Jul 2024 14:40:40 +0200 Subject: [PATCH 34/53] Improve `alr get` feedback on unknown crate name (#1708) * Update alr-commands-get.adb Added a help message for the case the exact crate name was not found. * Tweak tests for new output --------- Co-authored-by: Alejandro R. Mosteo --- src/alr/alr-commands-get.adb | 4 +++- testsuite/tests/get/get-not-found/test.py | 2 +- testsuite/tests/get/provides/test.py | 2 +- testsuite/tests/index/external-hint/test.py | 3 --- 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/alr/alr-commands-get.adb b/src/alr/alr-commands-get.adb index cc040e5cc..1b5f443d4 100644 --- a/src/alr/alr-commands-get.adb +++ b/src/alr/alr-commands-get.adb @@ -351,7 +351,9 @@ package body Alr.Commands.Get is else Reportaise_Command_Failed ("Crate [" & Allowed.Crate.As_String - & "] does not exist in the index."); + & "] does not exist in the index. " + & "Use `alr search " & Allowed.Crate.As_String + & "` for similar names."); end if; end if; diff --git a/testsuite/tests/get/get-not-found/test.py b/testsuite/tests/get/get-not-found/test.py index f59e890b1..82221d88c 100644 --- a/testsuite/tests/get/get-not-found/test.py +++ b/testsuite/tests/get/get-not-found/test.py @@ -10,7 +10,7 @@ p = run_alr('get', 'does_not_exist', complain_on_error=False) assert_eq(1, p.status) -assert_match('.*Crate \[does_not_exist\] does not exist in the index\.\n', +assert_match('.*Crate \[does_not_exist\] does not exist in the index\..*\n', p.out) assert_eq([], glob('does_not_exist*')) diff --git a/testsuite/tests/get/provides/test.py b/testsuite/tests/get/provides/test.py index 86e835e84..b56d9e08a 100644 --- a/testsuite/tests/get/provides/test.py +++ b/testsuite/tests/get/provides/test.py @@ -19,7 +19,7 @@ # Check the error for a truly unknown crate: assert_eq("""\ -ERROR: Crate [unobtanium] does not exist in the index. +ERROR: Crate [unobtanium] does not exist in the index. Use `alr search unobtanium` for similar names. """, run_alr("get", "unobtanium", "--dirname", complain_on_error=False).out) diff --git a/testsuite/tests/index/external-hint/test.py b/testsuite/tests/index/external-hint/test.py index 2436f2a9e..f926d82b9 100644 --- a/testsuite/tests/index/external-hint/test.py +++ b/testsuite/tests/index/external-hint/test.py @@ -2,13 +2,10 @@ Test the hinting with custom text in external definitions """ -from glob import glob - from drivers.alr import distro_is_known, run_alr from drivers.asserts import assert_eq, assert_match import re -import platform # 1st test: directly attempting to retrieve an external (this is doable for # system externals in supported platforms -- never in this test). Depending on From 3ed7f288e88e239d59e2ed24758224f6484dc2bd Mon Sep 17 00:00:00 2001 From: Alejandro R Mosteo Date: Tue, 2 Jul 2024 15:56:46 +0200 Subject: [PATCH 35/53] Create FUNDING.yml (#1712) * Create FUNDING.yml * Update FUNDING.yml --- .github/FUNDING.yml | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .github/FUNDING.yml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 000000000..cd5b9e513 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,3 @@ +github: [mosteo] # Up to 4 GitHub Sponsors-enabled usernames; e.g., [user1, user2] +polar: mosteo +open_collective: alire From a9332c1e604ff554ce02e5f02d99eac2ce7f0258 Mon Sep 17 00:00:00 2001 From: "Alejandro R. Mosteo" Date: Tue, 2 Jul 2024 16:24:27 +0200 Subject: [PATCH 36/53] Add funding badge and alire-project polar link --- .github/FUNDING.yml | 1 + README.md | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index cd5b9e513..760d011a6 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,3 +1,4 @@ github: [mosteo] # Up to 4 GitHub Sponsors-enabled usernames; e.g., [user1, user2] +polar: alire-project polar: mosteo open_collective: alire diff --git a/README.md b/README.md index 5a46ff81c..e4ca6a0c1 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,7 @@ [![MacOS CI](https://github.com/alire-project/alire/workflows/CI%20macOS/badge.svg)](https://github.com/alire-project/alire/actions) [![Gitter chat](https://badges.gitter.im/gitterHQ/gitter.png)](https://gitter.im/ada-lang/Alire) [![Gitpod ready](https://img.shields.io/badge/Gitpod-ready-908a85?logo=gitpod)](https://gitpod.io/#https://github.com/alire-project/alire) +[![Funding](https://polar.sh/embed/seeks-funding-shield.svg?org=alire-project)](https://polar.sh/alire-project) # ALR # From b7bb8d560c839eafbb2a815b08aa05b19e591794 Mon Sep 17 00:00:00 2001 From: Alejandro R Mosteo Date: Fri, 5 Jul 2024 12:54:35 +0200 Subject: [PATCH 37/53] fix: compiler autoselection on 1st run alr get --build (#1706) --- src/alire/alire-roots-optional.adb | 9 +++++++ src/alire/alire-roots-optional.ads | 3 +++ src/alr/alr-commands-get.adb | 18 ++++++++++--- testsuite/drivers/alr.py | 8 ++++++ testsuite/tests/dockerized/get/build/test.py | 26 +++++++++++++++++++ .../tests/dockerized/get/build/test.yaml | 5 ++++ 6 files changed, 65 insertions(+), 4 deletions(-) create mode 100644 testsuite/tests/dockerized/get/build/test.py create mode 100644 testsuite/tests/dockerized/get/build/test.yaml diff --git a/src/alire/alire-roots-optional.adb b/src/alire/alire-roots-optional.adb index 84d27ba78..a233028a3 100644 --- a/src/alire/alire-roots-optional.adb +++ b/src/alire/alire-roots-optional.adb @@ -137,6 +137,15 @@ package body Alire.Roots.Optional is Reference'(Ptr => This.Data.Value'Unrestricted_Access); end Value; + ------------- + -- Discard -- + ------------- + + procedure Discard (This : in out Root) is + begin + This.Data := (Status => Outside); + end Discard; + --------------------- -- Outcome_Failure -- --------------------- diff --git a/src/alire/alire-roots-optional.ads b/src/alire/alire-roots-optional.ads index ee811233e..862d01cbc 100644 --- a/src/alire/alire-roots-optional.ads +++ b/src/alire/alire-roots-optional.ads @@ -42,6 +42,9 @@ package Alire.Roots.Optional is function Value (This : in out Root) return Reference with Pre => This.Is_Valid; + procedure Discard (This : in out Root); + -- Sets the root to Outside + function Brokenness (This : Root) return String with Pre => This.Is_Broken; diff --git a/src/alr/alr-commands-get.adb b/src/alr/alr-commands-get.adb index 1b5f443d4..f1f41faea 100644 --- a/src/alr/alr-commands-get.adb +++ b/src/alr/alr-commands-get.adb @@ -187,14 +187,24 @@ package body Alr.Commands.Get is Build_OK := True; else + -- Require the workspace to ensure the same checks as in a + -- manual `get` followed by `cd` and `build` are performed. + + Cmd.Optional_Root.Discard; + -- It will be reloaded next. This is needed or otherwise the + -- following call would do nothing (it would see a proper root + -- already available so it would just return). + + Cmd.Requires_Workspace; + -- Build in release mode for a `get --build` Cmd.Root.Set_Build_Profile - (Crate => Cmd.Root.Name, - Profile => Alire.Utils.Switches.Release); + (Crate => Cmd.Root.Name, + Profile => Alire.Utils.Switches.Release); Build_OK := Cmd.Root.Build - (Cmd_Args => AAA.Strings.Empty_Vector, - Saved_Profiles => False); + (Cmd_Args => AAA.Strings.Empty_Vector, + Saved_Profiles => False); end if; else Build_OK := True; diff --git a/testsuite/drivers/alr.py b/testsuite/drivers/alr.py index 95da91404..18f52109b 100644 --- a/testsuite/drivers/alr.py +++ b/testsuite/drivers/alr.py @@ -617,3 +617,11 @@ def unselect_compiler(): """ run_alr("settings", "--global", "--unset", "toolchain.use.gnat") run_alr("settings", "--global", "--unset", "toolchain.external.gnat") + + +def set_default_user_settings(): + """ + Set the default alr settings that are undone by the testsuite defaults + """ + run_alr("settings", "--global", "--set", "index.auto_community", "true") + run_alr("settings", "--global", "--set", "toolchain.assistant", "true") diff --git a/testsuite/tests/dockerized/get/build/test.py b/testsuite/tests/dockerized/get/build/test.py new file mode 100644 index 000000000..ab916bd06 --- /dev/null +++ b/testsuite/tests/dockerized/get/build/test.py @@ -0,0 +1,26 @@ +""" +Check the `alr get --build` combo on 1st run with no available compiler. We +test under a pristine environment to avoid re-occurrence of issue #1671, which +only happens when no compiler has been selected yet, and none is available in +the environment. +""" + +import subprocess +from drivers.alr import run_alr, set_default_user_settings + +# First undo any testsuite compiler and index setup +set_default_user_settings() + +# Remove gnat from path so no compiler is available +subprocess.run(['sudo', 'mv', '/usr/bin/gnat', '/usr/bin/gnat.bak']).check_returncode() +try: + assert subprocess.run(['gnat', '--version']).returncode == 0, "Unexpected GNAT found" +except FileNotFoundError: + pass + +# Should succeed, issue in #1671 was that no compiler was available nor +# selected automatically as should have been the case. +p = run_alr('get', '--build', 'hello') + + +print('SUCCESS') diff --git a/testsuite/tests/dockerized/get/build/test.yaml b/testsuite/tests/dockerized/get/build/test.yaml new file mode 100644 index 000000000..f508caebb --- /dev/null +++ b/testsuite/tests/dockerized/get/build/test.yaml @@ -0,0 +1,5 @@ +driver: docker-wrapper +control: + - [SKIP, "skip_docker", "Docker disabled"] + - [SKIP, "skip_network", "Network disabled"] +# To exactly reproduce the issue we use the community index here From 3afe31a9e67a3dc224b8518fc3f7127acfe075a0 Mon Sep 17 00:00:00 2001 From: Brian Callahan Date: Fri, 5 Jul 2024 06:56:25 -0400 Subject: [PATCH 38/53] Add OpenBSD support. (#1705) Co-authored-by: Alejandro R Mosteo --- README.md | 8 ++-- alire.gpr | 4 ++ alire.toml | 3 +- alire_common.gpr | 1 + alr.gpr | 1 + alr_env.gpr | 1 + dev/functions.sh | 6 +++ doc/catalog-format-spec.md | 4 +- src/alire/alire-directories.adb | 2 +- src/alire/alire-platforms.ads | 3 +- .../os_openbsd/alire-check_absolute_path.adb | 7 ++++ .../alire-platforms-current__openbsd.adb | 40 +++++++++++++++++++ .../alire-platforms-folders__openbsd.adb | 34 ++++++++++++++++ 13 files changed, 105 insertions(+), 9 deletions(-) create mode 100644 src/alire/os_openbsd/alire-check_absolute_path.adb create mode 100644 src/alire/os_openbsd/alire-platforms-current__openbsd.adb create mode 100644 src/alire/os_openbsd/alire-platforms-folders__openbsd.adb diff --git a/README.md b/README.md index e4ca6a0c1..d0e6223dc 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ https://alire.ada.dev/ ## TL;DR ## -Available for Linux/macOS/Windows/FreeBSD. +Available for Linux/macOS/Windows/FreeBSD/OpenBSD. Download the latest stable version from the [Releases](https://github.com/alire-project/alire/releases) page. See the [Getting Started](doc/getting-started.md) guide for binary downloads. @@ -35,7 +35,7 @@ See the [Getting Started](doc/getting-started.md) guide. The build process of `alr` is straighforward and depends only on a recent GNAT Ada 2012 compiler. All dependencies are included as submodules. A project file (`alr_env.gpr`) is provided to drive the build with all necessary configuration (which is also valid for editing with GNAT Studio). -The ALIRE_OS environment variable must be set to the OS for which `alr` is being build, taking one of the values in `freebsd`, `linux`, `macos`, `windows`. +The ALIRE_OS environment variable must be set to the OS for which `alr` is being build, taking one of the values in `freebsd`, `openbsd`, `linux`, `macos`, `windows`. Follow these steps: @@ -43,7 +43,7 @@ Follow these steps: 1. Enter the cloned repository folder. 1. Build the executable: * if you have Bash on your system: `dev/build.sh` - * if you don't have Bash on your system: `ALIRE_OS= gprbuild -j0 -p -P alr_env` + * if you don't have Bash on your system: `ALIRE_OS= gprbuild -j0 -p -P alr_env` The binary will be found at `bin/alr`. You can run `alr version` to see version and diagnostics information. @@ -81,7 +81,7 @@ environment `alr` is using with `alr printenv`. ## Supported platforms ## -Alire can be built on Linux, macOS, Windows, and FreeBSD. +Alire can be built on Linux, macOS, Windows, FreeBSD, and OpenBSD. Alire requires a recent Ada 2012 compiler. In practice, this currently means the latest [GNAT Community](https://www.adacore.com/download) or a somewhat diff --git a/alire.gpr b/alire.gpr index 3490e92de..c15a633c6 100644 --- a/alire.gpr +++ b/alire.gpr @@ -28,6 +28,7 @@ library project Alire is case Alire_Common.Host_Os is when "freebsd" => Src_Dirs := Src_Dirs & ("src/alire/os_freebsd"); + when "openbsd" => Src_Dirs := Src_Dirs & ("src/alire/os_openbsd"); when "linux" => Src_Dirs := Src_Dirs & ("src/alire/os_linux"); when "macos" => Src_Dirs := Src_Dirs & ("src/alire/os_macos"); when "windows" => Src_Dirs := Src_Dirs & ("src/alire/os_windows"); @@ -38,6 +39,9 @@ library project Alire is when "freebsd" => for body ("Alire.Platforms.Current") use "alire-platforms-current__freebsd.adb"; for body ("Alire.Platforms.Folders") use "alire-platforms-folders__freebsd.adb"; + when "openbsd" => + for body ("Alire.Platforms.Current") use "alire-platforms-current__openbsd.adb"; + for body ("Alire.Platforms.Folders") use "alire-platforms-folders__openbsd.adb"; when "linux" => for body ("Alire.Platforms.Current") use "alire-platforms-current__linux.adb"; for body ("Alire.Platforms.Folders") use "alire-platforms-folders__linux.adb"; diff --git a/alire.toml b/alire.toml index 8821dc68a..7fb33a2c2 100644 --- a/alire.toml +++ b/alire.toml @@ -40,6 +40,7 @@ CLIC_LIBRARY_TYPE="static" # Building alr requires the explicit setting of this variable [gpr-set-externals."case(os)"] freebsd = { ALIRE_OS = "freebsd" } +openbsd = { ALIRE_OS = "openbsd" } linux = { ALIRE_OS = "linux" } macos = { ALIRE_OS = "macos" } windows = { ALIRE_OS = "windows" } @@ -116,4 +117,4 @@ command = ["pwsh", "scripts/version-patcher.ps1", "_or_later"] [actions.'case(os)'.'...'] type = "post-build" -command = ["scripts/version-patcher.sh", "_or_later"] \ No newline at end of file +command = ["scripts/version-patcher.sh", "_or_later"] diff --git a/alire_common.gpr b/alire_common.gpr index 39dabb983..fe88657b9 100644 --- a/alire_common.gpr +++ b/alire_common.gpr @@ -4,6 +4,7 @@ abstract project Alire_Common is type Host_OSes is ("linux", "freebsd", + "openbsd", "macos", "windows"); diff --git a/alr.gpr b/alr.gpr index f821cd135..00620f5d4 100644 --- a/alr.gpr +++ b/alr.gpr @@ -14,6 +14,7 @@ project Alr is case Alire_Common.Host_Os is when "freebsd" => Src_Dirs := Src_Dirs & ("src/alr/os_linux"); + when "openbsd" => Src_Dirs := Src_Dirs & ("src/alr/os_linux"); when "linux" => Src_Dirs := Src_Dirs & ("src/alr/os_linux"); when "macos" => Src_Dirs := Src_Dirs & ("src/alr/os_macos"); when "windows" => Src_Dirs := Src_Dirs & ("src/alr/os_windows"); diff --git a/alr_env.gpr b/alr_env.gpr index 0fbb9ba12..b391c975e 100644 --- a/alr_env.gpr +++ b/alr_env.gpr @@ -36,6 +36,7 @@ aggregate project Alr_Env is case Alire_Common.Host_Os is when "freebsd" => for External ("GNATCOLL_OS") use "unix"; + when "openbsd" => for External ("GNATCOLL_OS") use "unix"; when "linux" => for External ("GNATCOLL_OS") use "unix"; when "macos" => for External ("GNATCOLL_OS") use "osx"; when "windows" => for External ("GNATCOLL_OS") use "windows"; diff --git a/dev/functions.sh b/dev/functions.sh index 9ded1e895..d76314503 100755 --- a/dev/functions.sh +++ b/dev/functions.sh @@ -13,6 +13,9 @@ function guess_OS() { "freebsd") echo freebsd ;; + "openbsd") + echo openbsd + ;; "darwin"*) # varies with versions: darwin18, darwin19, etc. echo macos ;; @@ -37,6 +40,9 @@ function get_OS() { "FreeBSD") echo freebsd ;; + "OpenBSD") + echo openbsd + ;; "Darwin") echo macos ;; diff --git a/doc/catalog-format-spec.md b/doc/catalog-format-spec.md index 196aa5ca3..5dabab539 100644 --- a/doc/catalog-format-spec.md +++ b/doc/catalog-format-spec.md @@ -878,8 +878,8 @@ available.'case(toolchain)'.user = false ## Parameters - - `os`: name of the OS. Currently supported values are: `freebsd`, `linux`, - `macos`, `windows`, and `os-unknown`. + - `os`: name of the OS. Currently supported values are: `freebsd`, `openbsd`, + `linux`, `macos`, `windows`, and `os-unknown`. - `distribution`: name of the Linux distribution or name of the software distribution platform if running on a different OS. Currently supported diff --git a/src/alire/alire-directories.adb b/src/alire/alire-directories.adb index 4c8142ce6..f9551b4d4 100644 --- a/src/alire/alire-directories.adb +++ b/src/alire/alire-directories.adb @@ -146,7 +146,7 @@ package body Alire.Directories is Keep_Links : constant String := (case Platforms.Current.Operating_System is when Linux => "-d", - when FreeBSD | MacOS => "-R", + when FreeBSD | OpenBSD | MacOS => "-R", when others => raise Program_Error with "Unsupported operation"); begin diff --git a/src/alire/alire-platforms.ads b/src/alire/alire-platforms.ads index f78e08e41..bf879bd7d 100644 --- a/src/alire/alire-platforms.ads +++ b/src/alire/alire-platforms.ads @@ -3,7 +3,7 @@ package Alire.Platforms with Preelaborate is -- Platform information necessary for some releases type Extended_Architectures is - (AMD64, -- Equivalent to X86_64 (FreeBSD) + (AMD64, -- Equivalent to X86_64 (FreeBSD/OpenBSD) ARM64, -- Equivalent to AARCH64 End_Of_Duplicates, -- Up to this point, these are architectures that we want to rename to @@ -21,6 +21,7 @@ package Alire.Platforms with Preelaborate is -- See e.g. https://stackoverflow.com/a/45125525/761390 type Operating_Systems is (FreeBSD, + OpenBSD, Linux, MacOS, Windows, diff --git a/src/alire/os_openbsd/alire-check_absolute_path.adb b/src/alire/os_openbsd/alire-check_absolute_path.adb new file mode 100644 index 000000000..01caa2a3d --- /dev/null +++ b/src/alire/os_openbsd/alire-check_absolute_path.adb @@ -0,0 +1,7 @@ +separate (Alire) +function Check_Absolute_Path (Path : Any_Path) return Boolean is +begin + return (Path'Length >= 1 + and then + Path (Path'First) = GNAT.OS_Lib.Directory_Separator); +end Check_Absolute_Path; diff --git a/src/alire/os_openbsd/alire-platforms-current__openbsd.adb b/src/alire/os_openbsd/alire-platforms-current__openbsd.adb new file mode 100644 index 000000000..415056eea --- /dev/null +++ b/src/alire/os_openbsd/alire-platforms-current__openbsd.adb @@ -0,0 +1,40 @@ + +package body Alire.Platforms.Current is + + -- OpenBSD implementation (very close to Linux/FreeBSD) + + --------------------------- + -- Detected_Distribution -- + --------------------------- + + function Detected_Distribution return Platforms.Distributions + is (Platforms.Distribution_Unknown); + + ----------------------- + -- Distribution_Root -- + ----------------------- + + function Distribution_Root return Absolute_Path + is ("/"); + + ---------------------- + -- Load_Environment -- + ---------------------- + + procedure Load_Environment (Ctx : in out Alire.Environment.Context) + is null; + + ---------------------- + -- Operating_System -- + ---------------------- + + function Operating_System return Alire.Platforms.Operating_Systems + is (Alire.Platforms.OpenBSD); + + ---------------- + -- Initialize -- + ---------------- + + procedure Initialize is null; + +end Alire.Platforms.Current; diff --git a/src/alire/os_openbsd/alire-platforms-folders__openbsd.adb b/src/alire/os_openbsd/alire-platforms-folders__openbsd.adb new file mode 100644 index 000000000..51b4d1be7 --- /dev/null +++ b/src/alire/os_openbsd/alire-platforms-folders__openbsd.adb @@ -0,0 +1,34 @@ +with Ada.Directories; + +with Alire.Platforms.Common; + +package body Alire.Platforms.Folders is + + -- Linux implementation + + ----------- + -- Cache -- + ----------- + + function Cache return Absolute_Path is (Common.XDG_Data_Home); + + ----------- + -- Config-- + ----------- + + function Config return Absolute_Path is (Common.XDG_Config_Home); + + ---------- + -- Home -- + ---------- + + function Home return Absolute_Path is (Common.Unix_Home_Folder); + + ---------- + -- Temp -- + ---------- + + function Temp return Absolute_Path + is (Ada.Directories.Full_Name (Common.Unix_Temp_Folder)); + +end Alire.Platforms.Folders; From 432ae286d2247c5fc05f85af85ab1f36a29d56e7 Mon Sep 17 00:00:00 2001 From: Maxim Reznik Date: Wed, 10 Jul 2024 11:36:54 +0300 Subject: [PATCH 39/53] Enable MacOS X M1 in nightly builds (#1716) --- .github/workflows/nightly.yml | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 6ac6231c1..aed0ac995 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -21,6 +21,7 @@ jobs: fail-fast: false # Attempt to generate as many of them as possible matrix: os: + - macos-14 - macos-12 - ubuntu-20.04 - windows-latest @@ -36,6 +37,16 @@ jobs: with: crates: gnat_native gprbuild + - name: Replace toolchain with aarch64 + if: ${{ runner.arch == 'ARM64' }} + run: | + curl -L https://github.com/alire-project/GNAT-FSF-builds/releases/download/gnat-14.1.0-3/gnat-aarch64-darwin-14.1.0-3.tar.gz \ + | tar xzf - --strip-components=1 -C /Users/runner/work/alire/alire/alire_prefix + curl -L https://github.com/alire-project/GNAT-FSF-builds/releases/download/gprbuild-24.0.0-1/gprbuild-aarch64-darwin-24.0.0-1.tar.gz \ + | tar xzf - --strip-components=1 -C /Users/runner/work/alire/alire/alire_prefix + which gcc + gcc -v + - name: Install Python 3.x (required for the testsuite) uses: actions/setup-python@v2 with: @@ -77,10 +88,14 @@ jobs: if: startsWith(matrix.os, 'ubuntu') run: zip alr-nightly-bin-x86_64-linux.zip bin/alr* LICENSE.txt alr-*.txt - - name: Package binaries (macOS) - if: startsWith(matrix.os, 'macos') + - name: Package binaries (macOS/x64) + if: startsWith(matrix.os, 'macos') && runner.arch == 'X64' run: zip alr-nightly-bin-x86_64-macos.zip bin/alr* LICENSE.txt alr-*.txt + - name: Package binaries (macOS/arm64) + if: startsWith(matrix.os, 'macos') && runner.arch == 'ARM64' + run: zip alr-nightly-bin-aarch64-macos.zip bin/alr* LICENSE.txt alr-*.txt + # There's no zip on windows - name: Install zip (Windows) if: startsWith(matrix.os, 'windows') From 16b060b385f10932df967a7e9be1792842f24975 Mon Sep 17 00:00:00 2001 From: Alejandro R Mosteo Date: Wed, 10 Jul 2024 23:47:50 +0200 Subject: [PATCH 40/53] Remove per-OS code in favor of GNAT.OS_Lib (#1717) --- src/alire/alire.adb | 14 +++++++++++++- src/alire/os_freebsd/alire-check_absolute_path.adb | 7 ------- src/alire/os_linux/alire-check_absolute_path.adb | 7 ------- src/alire/os_macos/alire-check_absolute_path.adb | 7 ------- src/alire/os_openbsd/alire-check_absolute_path.adb | 7 ------- src/alire/os_windows/alire-check_absolute_path.adb | 10 ---------- testsuite/run.sh | 4 ++++ 7 files changed, 17 insertions(+), 39 deletions(-) delete mode 100644 src/alire/os_freebsd/alire-check_absolute_path.adb delete mode 100644 src/alire/os_linux/alire-check_absolute_path.adb delete mode 100644 src/alire/os_macos/alire-check_absolute_path.adb delete mode 100644 src/alire/os_openbsd/alire-check_absolute_path.adb delete mode 100644 src/alire/os_windows/alire-check_absolute_path.adb create mode 100755 testsuite/run.sh diff --git a/src/alire/alire.adb b/src/alire/alire.adb index 88857118b..6a0c23b6e 100644 --- a/src/alire/alire.adb +++ b/src/alire/alire.adb @@ -11,6 +11,8 @@ with GNATCOLL.OS.Constants; package body Alire is + package OS renames GNAT.OS_Lib; + --------- -- "=" -- --------- @@ -37,7 +39,17 @@ package body Alire is -- Check_Absolute_Path -- ------------------------- - function Check_Absolute_Path (Path : Any_Path) return Boolean is separate; + function Check_Absolute_Path (Path : Any_Path) return Boolean + is (OS.Is_Absolute_Path (Path) + and then + -- On Windows, we must ensure the path is not only absolute but + -- also that it has a drive letter. This is not checked by the + -- GNAT.OS_Lib function. + (if OS.Directory_Separator = '\' + then + (Path'Length >= 3 + and then Path (Path'First) in 'a' .. 'z' | 'A' .. 'Z' + and then Path (Path'First + 1) = ':'))); ------------- -- Err_Log -- diff --git a/src/alire/os_freebsd/alire-check_absolute_path.adb b/src/alire/os_freebsd/alire-check_absolute_path.adb deleted file mode 100644 index 01caa2a3d..000000000 --- a/src/alire/os_freebsd/alire-check_absolute_path.adb +++ /dev/null @@ -1,7 +0,0 @@ -separate (Alire) -function Check_Absolute_Path (Path : Any_Path) return Boolean is -begin - return (Path'Length >= 1 - and then - Path (Path'First) = GNAT.OS_Lib.Directory_Separator); -end Check_Absolute_Path; diff --git a/src/alire/os_linux/alire-check_absolute_path.adb b/src/alire/os_linux/alire-check_absolute_path.adb deleted file mode 100644 index 01caa2a3d..000000000 --- a/src/alire/os_linux/alire-check_absolute_path.adb +++ /dev/null @@ -1,7 +0,0 @@ -separate (Alire) -function Check_Absolute_Path (Path : Any_Path) return Boolean is -begin - return (Path'Length >= 1 - and then - Path (Path'First) = GNAT.OS_Lib.Directory_Separator); -end Check_Absolute_Path; diff --git a/src/alire/os_macos/alire-check_absolute_path.adb b/src/alire/os_macos/alire-check_absolute_path.adb deleted file mode 100644 index 01caa2a3d..000000000 --- a/src/alire/os_macos/alire-check_absolute_path.adb +++ /dev/null @@ -1,7 +0,0 @@ -separate (Alire) -function Check_Absolute_Path (Path : Any_Path) return Boolean is -begin - return (Path'Length >= 1 - and then - Path (Path'First) = GNAT.OS_Lib.Directory_Separator); -end Check_Absolute_Path; diff --git a/src/alire/os_openbsd/alire-check_absolute_path.adb b/src/alire/os_openbsd/alire-check_absolute_path.adb deleted file mode 100644 index 01caa2a3d..000000000 --- a/src/alire/os_openbsd/alire-check_absolute_path.adb +++ /dev/null @@ -1,7 +0,0 @@ -separate (Alire) -function Check_Absolute_Path (Path : Any_Path) return Boolean is -begin - return (Path'Length >= 1 - and then - Path (Path'First) = GNAT.OS_Lib.Directory_Separator); -end Check_Absolute_Path; diff --git a/src/alire/os_windows/alire-check_absolute_path.adb b/src/alire/os_windows/alire-check_absolute_path.adb deleted file mode 100644 index fb3f170d2..000000000 --- a/src/alire/os_windows/alire-check_absolute_path.adb +++ /dev/null @@ -1,10 +0,0 @@ -separate (Alire) -function Check_Absolute_Path (Path : Any_Path) return Boolean is -begin - return (Path'Length >= 3 - and then Path (Path'First) in 'A' .. 'Z' | 'a' .. 'z' - and then Path (Path'First + 1) = ':' - and then Path (Path'First + 2) in '\' | '/'); - -- Even strictly speaking, smthg like C:/ can only be an absolute path that - -- comes from some non-Windows native program (git from msys2 or the like). -end Check_Absolute_Path; diff --git a/testsuite/run.sh b/testsuite/run.sh new file mode 100755 index 000000000..4a2162365 --- /dev/null +++ b/testsuite/run.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +clear +python3 run.py -M1 "$@" From 16048667c98adedbf2177f9032c72ef58e0c6acb Mon Sep 17 00:00:00 2001 From: Seb M'Caw Date: Mon, 29 Jul 2024 10:59:05 +0100 Subject: [PATCH 41/53] Fix changing branch of pins with 'git+ssh://' and 'xyz+https://' urls (#1722) --- src/alire/alire-user_pins.adb | 20 ++++ .../tests/pin/branch-remote-protocols/test.py | 98 +++++++++++++++++++ .../pin/branch-remote-protocols/test.yaml | 4 + 3 files changed, 122 insertions(+) create mode 100644 testsuite/tests/pin/branch-remote-protocols/test.py create mode 100644 testsuite/tests/pin/branch-remote-protocols/test.yaml diff --git a/src/alire/alire-user_pins.adb b/src/alire/alire-user_pins.adb index adf22e83d..cc9a4f725 100644 --- a/src/alire/alire-user_pins.adb +++ b/src/alire/alire-user_pins.adb @@ -7,6 +7,9 @@ with Alire.Utils.User_Input; with Alire.Utils.TTY; with Alire.VFS; +with Ada.Strings.Unbounded; +with AAA.Strings; + with GNAT.OS_Lib; package body Alire.User_Pins is @@ -465,6 +468,8 @@ package body Alire.User_Pins is ----------------- function Load_Remote return Pin is + use Ada.Strings.Unbounded; + use AAA.Strings; Result : Pin := (Kind => To_Git, URL => +This.Checked_Pop (Keys.URL, @@ -473,6 +478,21 @@ package body Alire.User_Pins is Commit => <>, Local_Path => <>); begin + -- "git+ssh://"" and "ssh+git://" are deprecated, so treat them as + -- identical to "ssh://" + if Has_Prefix (To_String (Result.URL), "git+ssh://") + or else Has_Prefix (To_String (Result.URL), "ssh+git://") + then + Replace_Slice (Result.URL, 1, 7, "ssh"); + end if; + + -- Likewise, anything of the form "xyz+https://" should be treated + -- as just "https://" + if Contains (To_String (Result.URL), "+http") then + Result.URL := To_Unbounded_String + (Tail (To_String (Result.URL), '+')); + end if; + if This.Contains (Keys.Branch) and then This.Contains (Keys.Commit) then diff --git a/testsuite/tests/pin/branch-remote-protocols/test.py b/testsuite/tests/pin/branch-remote-protocols/test.py new file mode 100644 index 000000000..047e582c1 --- /dev/null +++ b/testsuite/tests/pin/branch-remote-protocols/test.py @@ -0,0 +1,98 @@ +""" +Check pinning to branches with "git+ssh://" and "xyz+https://" urls +""" + +import os +import subprocess + +from drivers.alr import alr_pin, alr_unpin, init_local_crate +from drivers.helpers import init_git_repo, git_branch +from drivers.asserts import assert_eq + + +# Create a crate with differing branches. +init_local_crate(name="remote", enter=False) +LOCAL_REPO_PATH = os.path.join(os.getcwd(), "remote") +# On the default branch, test_file contains "This is the main branch.\n". +test_file_path = os.path.join(LOCAL_REPO_PATH, "test_file") +with open(test_file_path, "w") as f: + f.write("This is the main branch.\n") +init_git_repo("remote") +os.chdir("remote") +default_branch = git_branch() +# On the "other" branch, test_file contains "This is the other branch.\n". +subprocess.run(["git", "checkout", "-b", "other"]).check_returncode() +with open(test_file_path, "w") as f: + f.write("This is the other branch.\n") +subprocess.run(["git", "add", "test_file"]).check_returncode() +subprocess.run(["git", "commit", "-m", "Change test_file"]).check_returncode() +# Return to the default branch +subprocess.run(["git", "checkout", default_branch]).check_returncode() +os.chdir("..") + + +# Prepare a directory on PATH at which to mock git. +ACTUAL_GIT_PATH = ( + subprocess.run(["bash", "-c", "type -p git"], capture_output=True) + .stdout.decode() + .strip() +) +MOCK_PATH = os.path.join(os.getcwd(), "mock_path") +os.mkdir(MOCK_PATH) +os.environ["PATH"] = f'{MOCK_PATH}:{os.environ["PATH"]}' + + +# Perform the actual tests +URLs = [ + "git+ssh://ssh.gitlab.company-name.com/path/to/repo.git", + "xyz+https://github.com/path/to/repo.git", +] +SANITISED_URLS = [ + "ssh://ssh.gitlab.company-name.com/path/to/repo.git", + "https://github.com/path/to/repo.git", +] +CACHE_TEST_FILE_PATH = "alire/cache/pins/remote/test_file" +for URL, S_URL in zip(URLs, SANITISED_URLS): + # Mock git with a wrapper that naively converts the url into the local path + # to the "remote" crate. + wrapper_script = "\n".join( + [ + "#! /usr/bin/env python", + "import subprocess, sys", + 'if sys.argv[1:] == ["config", "--list"]:', + f' print("remote.origin.url={S_URL}\\n")', + "else:", + " args = [", + f' ("{LOCAL_REPO_PATH}" if a == "{S_URL}" else a)', + " for a in sys.argv[1:]", + " ]", + f' subprocess.run(["{ACTUAL_GIT_PATH}"] + args).check_returncode()', + ] + ) + wrapper_descriptor = os.open( + os.path.join(MOCK_PATH, "git"), + flags=(os.O_WRONLY | os.O_CREAT | os.O_TRUNC), + mode=0o764, + ) + with open(wrapper_descriptor, "w") as f: + f.write(wrapper_script) + + # Create an empty crate, and pin the default branch of the test repo + init_local_crate() + alr_pin("remote", url=URL, branch=default_branch) + with open(CACHE_TEST_FILE_PATH) as f: + assert_eq("This is the main branch.\n", f.read()) + + # Edit pin to point to the other branch, and verify the cached copy changes + # as it should + alr_unpin("remote", update=False) + alr_pin("remote", url=URL, branch="other") + with open(CACHE_TEST_FILE_PATH) as f: + assert_eq("This is the other branch.\n", f.read()) + + +# Restore PATH +os.environ["PATH"] = os.environ["PATH"][len(MOCK_PATH) + 1 :] + + +print("SUCCESS") diff --git a/testsuite/tests/pin/branch-remote-protocols/test.yaml b/testsuite/tests/pin/branch-remote-protocols/test.yaml new file mode 100644 index 000000000..ee8ead706 --- /dev/null +++ b/testsuite/tests/pin/branch-remote-protocols/test.yaml @@ -0,0 +1,4 @@ +driver: python-script +control: + - [SKIP, "skip_linux", "Test is Linux-only"] +indexes: {} From 134d1157c18fd711533a5f9763ddc7dd9716ce54 Mon Sep 17 00:00:00 2001 From: Alejandro R Mosteo Date: Wed, 7 Aug 2024 14:40:08 +0200 Subject: [PATCH 42/53] New `alr cache` (#1642) * Basic report * Test debug * Bump ncdu crate * Debug issue with disappearing folder * Refactor ncdu into den * Self-review --- .gitmodules | 6 + alire.gpr | 2 + alire.toml | 13 +- alr_env.gpr | 2 + deps/aaa | 2 +- deps/cstrings | 1 + deps/den | 1 + src/alire/alire-builds.adb | 6 +- src/alire/alire-cache.adb | 126 ++++++++++++++++++ src/alire/alire-cache.ads | 111 +++++++++++++++ src/alire/alire-paths-vault.ads | 4 +- src/alire/alire-settings-edit.adb | 12 -- src/alire/alire-settings-edit.ads | 7 - src/alire/alire-toolchains.adb | 5 +- .../alire-settings-builtins-windows.ads | 3 +- src/alr/alr-commands-cache.adb | 42 ++++++ src/alr/alr-commands-cache.ads | 43 ++++++ src/alr/alr-commands-skeleton.ads | 2 +- src/alr/alr-commands-version.adb | 3 +- src/alr/alr-commands.adb | 2 + testsuite/tests/cache/summary/test.py | 52 ++++++++ testsuite/tests/cache/summary/test.yaml | 5 + 22 files changed, 419 insertions(+), 31 deletions(-) create mode 160000 deps/cstrings create mode 160000 deps/den create mode 100644 src/alire/alire-cache.adb create mode 100644 src/alire/alire-cache.ads create mode 100644 src/alr/alr-commands-cache.adb create mode 100644 src/alr/alr-commands-cache.ads create mode 100644 testsuite/tests/cache/summary/test.py create mode 100644 testsuite/tests/cache/summary/test.yaml diff --git a/.gitmodules b/.gitmodules index fc56fd8ee..ec5a589be 100644 --- a/.gitmodules +++ b/.gitmodules @@ -60,3 +60,9 @@ [submodule "deps/dirty_booleans"] path = deps/dirty_booleans url = https://github.com/mosteo/dirty_booleans +[submodule "deps/den"] + path = deps/den + url = https://github.com/mosteo/den +[submodule "deps/cstrings"] + path = deps/cstrings + url = https://github.com/mosteo/cstrings diff --git a/alire.gpr b/alire.gpr index c15a633c6..a00b73f38 100644 --- a/alire.gpr +++ b/alire.gpr @@ -3,7 +3,9 @@ with "ada_toml"; with "alire_common"; with "ajunitgen"; with "ansiada"; +with "c_strings"; with "clic"; +with "den"; with "dirty_booleans"; with "diskflags"; with "gnatcoll"; diff --git a/alire.toml b/alire.toml index 7fb33a2c2..a2e72c926 100644 --- a/alire.toml +++ b/alire.toml @@ -19,7 +19,9 @@ aaa = "~0.3.0" ada_toml = "~0.3" ajunitgen = "^1.0.1" ansiada = "^1.0" +c_strings = "^1.0" clic = "~0.3" +den = "~0.1" dirty_booleans = "~0.1" diskflags = "~0.1" gnatcoll = "^21" @@ -48,18 +50,27 @@ windows = { ALIRE_OS = "windows" } # Some dependencies require precise versions during the development cycle: [[pins]] + [pins.aaa] url = "https://github.com/mosteo/aaa" -commit = "dff61d2615cc6332fa6205267bae19b4d044b9da" +commit = "0c3b440ac183c450345d4a67d407785678779aae" [pins.ada_toml] url = "https://github.com/mosteo/ada-toml" commit = "da4e59c382ceb0de6733d571ecbab7ea4919b33d" +[pins.c_strings] +url = "https://github.com/mosteo/cstrings" +commit = "e4d58ad90bf32bc44304197e5906a519f5a9a7bf" + [pins.clic] url = "https://github.com/alire-project/clic" commit = "56bbdc008e16996b6f76e443fd0165a240de1b13" +[pins.den] +url = "https://github.com/mosteo/den" +commit = "35d1f38395b93766dd64bca5901ce3b6a416ba1a" + [pins.dirty_booleans] url = "https://github.com/mosteo/dirty_booleans" commit = "05c40d88ecfe109e575ec8b21dd6ffa2e61df1dc" diff --git a/alr_env.gpr b/alr_env.gpr index b391c975e..78592c5e6 100644 --- a/alr_env.gpr +++ b/alr_env.gpr @@ -14,6 +14,8 @@ aggregate project Alr_Env is "deps/ajunitgen", "deps/ansi", "deps/clic", + "deps/cstrings", + "deps/den", "deps/dirty_booleans", "deps/diskflags", "deps/gnatcoll-slim", diff --git a/deps/aaa b/deps/aaa index dff61d261..0c3b440ac 160000 --- a/deps/aaa +++ b/deps/aaa @@ -1 +1 @@ -Subproject commit dff61d2615cc6332fa6205267bae19b4d044b9da +Subproject commit 0c3b440ac183c450345d4a67d407785678779aae diff --git a/deps/cstrings b/deps/cstrings new file mode 160000 index 000000000..e4d58ad90 --- /dev/null +++ b/deps/cstrings @@ -0,0 +1 @@ +Subproject commit e4d58ad90bf32bc44304197e5906a519f5a9a7bf diff --git a/deps/den b/deps/den new file mode 160000 index 000000000..35d1f3839 --- /dev/null +++ b/deps/den @@ -0,0 +1 @@ +Subproject commit 35d1f38395b93766dd64bca5901ce3b6a416ba1a diff --git a/src/alire/alire-builds.adb b/src/alire/alire-builds.adb index f15ea0041..9593c1c77 100644 --- a/src/alire/alire-builds.adb +++ b/src/alire/alire-builds.adb @@ -1,11 +1,11 @@ with AAA.Strings; -with Alire.Settings.Builtins; -with Alire.Settings.Edit; +with Alire.Cache; with Alire.Directories; with Alire.Flags; with Alire.Paths.Vault; with Alire.Roots; +with Alire.Settings.Builtins; with GNATCOLL.VFS; @@ -83,7 +83,7 @@ package body Alire.Builds is ---------- function Path return Absolute_Path - is (Settings.Edit.Cache_Path + is (Cache.Path / Paths.Build_Folder_Inside_Working_Folder); ---------- diff --git a/src/alire/alire-cache.adb b/src/alire/alire-cache.adb new file mode 100644 index 000000000..e6aade61c --- /dev/null +++ b/src/alire/alire-cache.adb @@ -0,0 +1,126 @@ +with Ada.Calendar; + +with Alire.Directories; +with Alire.Paths; +with Alire.Platforms.Folders; +with Alire.Settings.Builtins; +with Alire.Settings.Edit; + +with Den.Du; + +package body Alire.Cache is + + use Alire.Directories.Operators; + + package Adirs renames Ada.Directories; + package Du is new Den.Du; + + ---------- + -- Path -- + ---------- + + function Path return Absolute_Path + is (if Settings.Builtins.Cache_Dir.Get /= "" then + Settings.Builtins.Cache_Dir.Get + elsif not Settings.Edit.Is_At_Default_Dir then + Settings.Edit.Path / Paths.Cache_Folder_Inside_Working_Folder + else + Platforms.Folders.Cache); + + ----------- + -- Usage -- + ----------- + + function Usage return Usages is + + Busy_Top : Simple_Logging.Ongoing := + Simple_Logging.Activity ("Listing"); + + Busy : Simple_Logging.Ongoing := Simple_Logging.Activity (""); + + Last_Check : Ada.Calendar.Time := Ada.Calendar.Clock; + + -------------- + -- Progress -- + -------------- + + procedure Progress (Path : String) is + use Ada.Calendar; + begin + if Clock - Last_Check >= 0.1 + and then Directories.Is_File (Path / Alire.Paths.Crate_File_Name) + then + Busy_Top.Step; + Busy.Step (Adirs.Simple_Name (Path)); + Last_Check := Clock; + end if; + end Progress; + + Tree : constant Du.Tree := Du.List (Path, + Progress => Progress'Access); + + ---------------- + -- Usage_Wrap -- + ---------------- + + procedure Usage_Wrap (Parent : in out Usages; + Children : Du.Tree; + Depth : Depths; + Branch : String := "" + -- Says if toolchains, releases, or builds + ) + is + begin + for Child of Children loop + declare + Branch : constant String + := (if Usage_Wrap.Branch /= "" + then Usage_Wrap.Branch + else Adirs.Simple_Name (Child.Element.Path)); + Wrapped_Children : Usages; + begin + + -- Wrap the children if we still have room to go down + + if Depth < Release or else + (Depth < Build + and then Branch = Paths.Build_Folder_Inside_Working_Folder) + then + Usage_Wrap (Wrapped_Children, + Child.Element.Children, + Depth => Depths'Succ (Depth), + Branch => Branch); + end if; + + -- Create the wrapped node at the current depth + + Parent.Insert + (Item' + (Depth => Depth, + Name => +Adirs.Simple_Name (Child.Element.Path), + Path => +Child.Element.Path, + Size => Child.Tree_Size, + Children => Wrapped_Children)); + end; + end loop; + end Usage_Wrap; + + begin + -- The root node should be the cache dir itself, unless there is still + -- no cache at all. + if Tree.Is_Empty then + return Item_Sets.Empty_Set; + elsif Tree.Length not in 1 then + raise Program_Error + with "Cache tree root length /= 1:" & Tree.Length'Image; + end if; + + -- Iterate the obtained tree wrapping contents as our usage type + return Result : Usages do + Usage_Wrap (Result, + Tree.First_Element.Element.Children, + Depths'First); + end return; + end Usage; + +end Alire.Cache; diff --git a/src/alire/alire-cache.ads b/src/alire/alire-cache.ads new file mode 100644 index 000000000..de930e045 --- /dev/null +++ b/src/alire/alire-cache.ads @@ -0,0 +1,111 @@ +with Ada.Containers.Indefinite_Ordered_Multisets; +with Ada.Directories; + +package Alire.Cache is + + -- Cache inspection and management. The cache is where we store all data + -- that, if not found, is re-downloaded or regenerated. This currently + -- comprises toolchains, pristine releases (the vault), builds, and the + -- user index fork clone when publishing. + + function Path return Absolute_Path; + -- The location for data that will be recreated if missing; its value in + -- precedence order is: + -- 1) Setting builtin 'cache.dir' + -- 2) if Alire.Settings.Path is overridden, Settings.Path/cache + -- 3) Platforms.Folders.Cache + + subtype Sizes is Ada.Directories.File_Size; + -- A size, in bytes + + -- The following builds a tree of items in the cache, that can be queried + -- to present information up to a level of detail. + + type Depths is (Location, Release, Build); + -- Locations are the top-level folders: toolchains, releases, builds. + -- Releases are a unique release milestone plus short commit. + -- Builds are synced copies for a release, named as the release + build id. + + type Base_Item is abstract tagged null record; + + function "<" (L, R : Base_Item'Class) return Boolean; + + function Depth (This : Base_Item'Class) return Depths; + + function Name (This : Base_Item'Class) return String; + + function Path (This : Base_Item'Class) return Absolute_Path; + + function Size (This : Base_Item'Class) return Sizes; + + package Item_Sets is + new Ada.Containers.Indefinite_Ordered_Multisets (Base_Item'Class); + + subtype Usages is Item_Sets.Set; + + function Children (This : Base_Item'Class) return Usages; + + function Usage return Usages; + -- Compute cache usage. First level is locations, second level is releases, + -- third level is builds. Within level, childen are sorted by size. + + type Item is new Base_Item with record + Depth : Depths; + Name : UString; + Path : Unbounded_Absolute_Path; + Size : Sizes; -- Accumulated size below this item + Children : Usages; + end record; + + function Element (This : Base_Item'Class) return Item is (Item (This)) + with Inline; + +private + + use type Sizes; + + -------------- + -- Children -- + -------------- + + function Children (This : Base_Item'Class) return Usages + is (This.Element.Children); + + ----------- + -- Depth -- + ----------- + + function Depth (This : Base_Item'Class) return Depths + is (This.Element.Depth); + + ---------- + -- Name -- + ---------- + + function Name (This : Base_Item'Class) return String + is (UStrings.To_String (This.Element.Name)); + + ---------- + -- Path -- + ---------- + + function Path (This : Base_Item'Class) return Absolute_Path + is (Absolute_Path (UStrings.To_String (This.Element.Path))); + + ---------- + -- Size -- + ---------- + + function Size (This : Base_Item'Class) return Sizes is (This.Element.Size); + + --------- + -- "<" -- + --------- + + function "<" (L, R : Base_Item'Class) return Boolean + is (L.Size > R.Size + or else + (L.Size = R.Size + and then L.Name < R.Name)); + +end Alire.Cache; diff --git a/src/alire/alire-paths-vault.ads b/src/alire/alire-paths-vault.ads index 0c6b1eeb4..274ba14b5 100644 --- a/src/alire/alire-paths-vault.ads +++ b/src/alire/alire-paths-vault.ads @@ -1,4 +1,4 @@ -with Alire.Settings.Edit; +with Alire.Cache; package Alire.Paths.Vault is @@ -10,7 +10,7 @@ package Alire.Paths.Vault is -- are run there (see Alire.Builds). function Path return Absolute_Path - is (Settings.Edit.Cache_Path + is (Cache.Path / Release_Folder_Inside_Working_Folder); end Alire.Paths.Vault; diff --git a/src/alire/alire-settings-edit.adb b/src/alire/alire-settings-edit.adb index 61e5a9c3e..dcc751174 100644 --- a/src/alire/alire-settings-edit.adb +++ b/src/alire/alire-settings-edit.adb @@ -242,18 +242,6 @@ package body Alire.Settings.Edit is end if; end Path; - ---------------- - -- Cache_Path -- - ---------------- - - function Cache_Path return Absolute_Path - is (if Builtins.Cache_Dir.Get /= "" then - Builtins.Cache_Dir.Get - elsif Path /= Default_Config_Path then - Path / Paths.Cache_Folder_Inside_Working_Folder - else - Platforms.Folders.Cache); - -------------- -- Set_Path -- -------------- diff --git a/src/alire/alire-settings-edit.ads b/src/alire/alire-settings-edit.ads index 196a66c9f..495e644eb 100644 --- a/src/alire/alire-settings-edit.ads +++ b/src/alire/alire-settings-edit.ads @@ -46,13 +46,6 @@ package Alire.Settings.Edit is -- * An ALIRE_SETTINGS_DIR env given folder -- * Default per-platform path (see alire-platforms-*) - function Cache_Path return Absolute_Path; - -- The location for data that will be recreated if missing; its value in - -- precedence order is: - -- 1) Setting builtin 'cache.dir' - -- 2) if Path above is overridden, Path/cache - -- 3) Platforms.Folders.Cache - procedure Set_Path (Path : Absolute_Path); -- Override global settings folder path diff --git a/src/alire/alire-toolchains.adb b/src/alire/alire-toolchains.adb index 08a118e43..560b0a499 100644 --- a/src/alire/alire-toolchains.adb +++ b/src/alire/alire-toolchains.adb @@ -3,7 +3,7 @@ with AAA.Text_IO; with Ada.Containers.Indefinite_Vectors; with Ada.Directories; -with Alire.Settings.Edit; +with Alire.Cache; with Alire.Directories; with Alire.Index; with Alire.Manifest; @@ -12,6 +12,7 @@ with Alire.Paths; with Alire.Platforms.Current; with Alire.Properties; with Alire.Root; +with Alire.Settings.Edit; with Alire.Toolchains.Solutions; with Alire.Warnings; @@ -610,7 +611,7 @@ package body Alire.Toolchains is function Path return Absolute_Path is (if Settings.Builtins.Toolchain_Dir.Get /= "" then Settings.Builtins.Toolchain_Dir.Get - else Settings.Edit.Cache_Path / "toolchains"); + else Cache.Path / "toolchains"); ------------ -- Deploy -- diff --git a/src/alire/os_windows/alire-settings-builtins-windows.ads b/src/alire/os_windows/alire-settings-builtins-windows.ads index cc27adc83..abeaccf54 100644 --- a/src/alire/os_windows/alire-settings-builtins-windows.ads +++ b/src/alire/os_windows/alire-settings-builtins-windows.ads @@ -1,4 +1,5 @@ -- Ensure config is loaded for some defaults below +with Alire.Cache; with Alire.Settings.Edit.Early_Load; pragma Unreferenced (Alire.Settings.Edit.Early_Load); @@ -24,7 +25,7 @@ package Alire.Settings.Builtins.Windows is Msys2_Install_Dir : constant Builtin := New_Builtin (Key => "msys2.install_dir", Kind => Stn_Absolute_Path, - Def => Settings.Edit.Cache_Path / "msys64", + Def => Cache.Path / "msys64", Help => "Directory where Alire will detect and/or install" & " msys2 system package manager. (Windows only)"); diff --git a/src/alr/alr-commands-cache.adb b/src/alr/alr-commands-cache.adb new file mode 100644 index 000000000..ee2c04511 --- /dev/null +++ b/src/alr/alr-commands-cache.adb @@ -0,0 +1,42 @@ +with Alire.Cache; +with Alire.Directories; +with Alire.Utils.Tables; + +package body Alr.Commands.Cache is + + ------------- + -- Summary -- + ------------- + + procedure Summary is + use Alire.Directories; + Table : Alire.Utils.Tables.Table; + Usage : constant Alire.Cache.Usages := Alire.Cache.Usage; + begin + Table + .Append ("Path:") + .Append (Alire.Cache.Path) + .New_Row; + + Table + .Append ("Size:") + .Append (TTY_Image (if Usage.Is_Empty + then 0 + else Alire.Cache.Usage.First_Element.Size)); + + Table.Print (Trace.Always); + end Summary; + + ------------- + -- Execute -- + ------------- + + overriding + procedure Execute (Cmd : in out Command; + Args : AAA.Strings.Vector) + is + begin + Summary; + end Execute; + +end Alr.Commands.Cache; diff --git a/src/alr/alr-commands-cache.ads b/src/alr/alr-commands-cache.ads new file mode 100644 index 000000000..2f933908d --- /dev/null +++ b/src/alr/alr-commands-cache.ads @@ -0,0 +1,43 @@ +with AAA.Strings; + +package Alr.Commands.Cache is + + type Command is new Commands.Command with private; + + overriding + function Name (Cmd : Command) return CLIC.Subcommand.Identifier + is ("cache"); + + overriding + procedure Execute (Cmd : in out Command; + Args : AAA.Strings.Vector); + -- This is called once the command-line is parsed. + + overriding + function Long_Description (Cmd : Command) + return AAA.Strings.Vector + is (AAA.Strings.Empty_Vector + .Append ("Inspect and manage Alire's cache.") + .New_Line + .Append ("Cache entries can be deleted to reclaim space and will be " + & "recreated on demand. Beware that deleting toolchains and releases " + & "may cause potentially large redownloads.")); + + overriding + procedure Setup_Switches + (Cmd : in out Command; + Config : in out CLIC.Subcommand.Switches_Configuration) is null; + + overriding + function Short_Description (Cmd : Command) return String + is ("Inspect and manage Alire's cache"); + + overriding + function Usage_Custom_Parameters (Cmd : Command) return String + is (""); + +private + + type Command is new Commands.Command with null record; + +end Alr.Commands.Cache; diff --git a/src/alr/alr-commands-skeleton.ads b/src/alr/alr-commands-skeleton.ads index 83ff18c8f..ff2ec9b8c 100644 --- a/src/alr/alr-commands-skeleton.ads +++ b/src/alr/alr-commands-skeleton.ads @@ -9,7 +9,7 @@ package Alr.Commands.Skeleton is type Command is new Commands.Command with private; overriding - function Name (Cmd : Command) return String + function Name (Cmd : Command) return CLIC.Subcommand.Identifier is ("skeleton"); overriding diff --git a/src/alr/alr-commands-version.adb b/src/alr/alr-commands-version.adb index f4fd73a32..b2d2fa302 100644 --- a/src/alr/alr-commands-version.adb +++ b/src/alr/alr-commands-version.adb @@ -1,4 +1,5 @@ with Alire.Builds; +with Alire.Cache; with Alire.Settings.Edit; with Alire.Directories; with Alire.Index; @@ -77,7 +78,7 @@ package body Alr.Commands.Version is Table.Append ("settings folder:") .Append (Alire.Settings.Edit.Path).New_Row; Table.Append ("cache folder:") - .Append (Alire.Settings.Edit.Cache_Path).New_Row; + .Append (Alire.Cache.Path).New_Row; Table.Append ("vault folder:").Append (Paths.Vault.Path).New_Row; Table.Append ("build folder:").Append (Build_Path).New_Row; Table.Append ("temp folder:") diff --git a/src/alr/alr-commands.adb b/src/alr/alr-commands.adb index 9ce783564..214546eed 100644 --- a/src/alr/alr-commands.adb +++ b/src/alr/alr-commands.adb @@ -23,6 +23,7 @@ with Alire.Toolchains; with Alr.Commands.Action; with Alr.Commands.Build; +with Alr.Commands.Cache; with Alr.Commands.Clean; with Alr.Commands.Config; with Alr.Commands.Dev; @@ -677,6 +678,7 @@ begin -- Commands -- Sub_Cmd.Register ("General", new Sub_Cmd.Builtin_Help); + Sub_Cmd.Register ("General", new Cache.Command); Sub_Cmd.Register ("General", new Settings.Command); Sub_Cmd.Register ("General", new Config.Command); Sub_Cmd.Register ("General", new Install.Command); diff --git a/testsuite/tests/cache/summary/test.py b/testsuite/tests/cache/summary/test.py new file mode 100644 index 000000000..238062e47 --- /dev/null +++ b/testsuite/tests/cache/summary/test.py @@ -0,0 +1,52 @@ +""" +Check the basic report of cache use +""" + +import os +import re +from drivers import builds +from drivers.alr import run_alr, init_local_crate, alr_with +from drivers.asserts import assert_eq, assert_match +from drivers.helpers import contents, dir_separator + +s = re.escape(dir_separator()) + +# Default cache status after clean install + +assert_match(f"""\ +Path:.*alr-config{s}cache +Size: 0.0 B +""", +run_alr("cache").out) + +# Compile something with a dependency and there should be something in the +# cache when builds are shared. + +init_local_crate() +alr_with("libhello") +run_alr("build") +p = run_alr("cache") +if builds.are_shared(): + # Something already in the cache + assert_match(r"Path:.*alr-config[/\\]cache\nSize: (?!0.0 B).*\n", p.out) +else: + # Still nothing if no shared cache + assert_match(r"Path:.*alr-config[/\\]cache\nSize: 0.0 B\n", p.out) + +# After installing some toolchain, for sure there should be something in the +# cache, as binaries always go into the cache. + +run_alr("toolchain", "--select", "gnat_native=1", "gprbuild") +try: + p = run_alr("cache") +except: + # Something strange is happening... + + # Print to stderr the type of file + print(f"EXISTS: {os.path.exists('alr-config/cache/toolchains/gprbuild_1.0.0_e3d52b4a/share/gprbuildalr-config/cache/toolchains/gprbuild_1.0.0_e3d52b4a/share/gprbuild')}", + file=os.stderr) + + assert_eq(run_alr("version").out, contents("../alr-config/cache")) +assert_match(r"Path:.*alr-config[/\\]cache\nSize: (?!0.0 B).*\n", p.out) + +print("SUCCESS") diff --git a/testsuite/tests/cache/summary/test.yaml b/testsuite/tests/cache/summary/test.yaml new file mode 100644 index 000000000..4e949dace --- /dev/null +++ b/testsuite/tests/cache/summary/test.yaml @@ -0,0 +1,5 @@ +driver: python-script +build_mode: both +indexes: + gnat_toolchain_index: {} + basic_index: {} From 66684903ced48ca36a4a46d1f25d6052c0609d15 Mon Sep 17 00:00:00 2001 From: Alejandro R Mosteo Date: Wed, 7 Aug 2024 17:00:57 +0200 Subject: [PATCH 43/53] Fix traversal of dirs containing troublesome softlinks (#1718) * Fix enumeration of files with troublesome softlinks * Self-review * Workaround in `den` for GCC 14 C++ bug * 2nd self-review commit 7d8b2ccd77998bbc0adfea8807b1ecea19a2a332 Author: Alejandro R. Mosteo Date: Fri Aug 2 09:53:22 2024 +0200 Debug trouble with relative path finder commit 09514571571d234a19af3eee5202838e85508c60 Author: Alejandro R. Mosteo Date: Thu Aug 1 22:32:48 2024 +0200 Use simpler relative path from Den commit 76417fa28f0daf0c0244129c9fecc2d6db7abc37 Author: Alejandro R. Mosteo Date: Thu Aug 1 22:32:09 2024 +0200 Revert "Try with gprbuild<24" This reverts commit b6ca84eac4e7af62aedd2e8d73545da0c71b9fb1. commit f6e0a963163a517b56a17243f1e2685263d7e244 Author: Alejandro R. Mosteo Date: Thu Aug 1 00:04:00 2024 +0200 Repair find relative part output commit 6ad595ff4de348b72d894339b7172c8797a7bf64 Author: Alejandro R. Mosteo Date: Wed Jul 31 23:28:48 2024 +0200 Flush testsuite output at start commit a52e5555983b94adb0090869e0e67c40a8bb9b4c Author: Alejandro R. Mosteo Date: Wed Jul 31 18:18:44 2024 +0200 Fix damaged test commit b6ca84eac4e7af62aedd2e8d73545da0c71b9fb1 Author: Alejandro R. Mosteo Date: Wed Jul 31 14:16:11 2024 +0200 Try with gprbuild<24 commit 500021055430ddc56ca8d4bfbfb371d6e1434333 Author: Alejandro R. Mosteo Date: Wed Jul 31 14:04:07 2024 +0200 Show GNAT/GPRBUILD versions prior to testsuite run --- .github/workflows/ci-docker.yml | 12 +- scripts/ci-github.sh | 8 +- src/alire/alire-directories.adb | 204 +++++++------------ src/alire/alire-directories.ads | 2 +- src/alire/alire-install.adb | 2 +- src/alire/alire-os_lib.ads | 6 +- src/alire/alire-roots.adb | 32 ++- src/alire/alire-toml_index.adb | 7 +- src/alire/alire-toolchains.adb | 2 +- src/alr/alr-commands-clean.adb | 2 +- src/alr/alr-commands-test.adb | 2 +- src/alr/alr-files.adb | 24 ++- testsuite/run.py | 6 +- testsuite/tests/install/softlinks/test.py | 13 +- testsuite/tests/install/softlinks/test.yaml | 2 +- testsuite/tests/misc/dir-traversal/test.py | 25 +++ testsuite/tests/misc/dir-traversal/test.yaml | 6 + testsuite/tests/show/nested/test.py | 2 +- 18 files changed, 182 insertions(+), 175 deletions(-) create mode 100644 testsuite/tests/misc/dir-traversal/test.py create mode 100644 testsuite/tests/misc/dir-traversal/test.yaml diff --git a/.github/workflows/ci-docker.yml b/.github/workflows/ci-docker.yml index a01ad2d05..590cb60c6 100644 --- a/.github/workflows/ci-docker.yml +++ b/.github/workflows/ci-docker.yml @@ -32,8 +32,18 @@ jobs: with: submodules: true + - name: OS information for ${{ matrix.tag }} + uses: mosteo-actions/docker-run@v2 + with: + image: ghcr.io/alire-project/docker/gnat:${{matrix.tag}} + command: | + lsb_release -a || \ + cat /etc/os-release || \ + cat /etc/system-release || \ + echo "No lsb_release information" + - name: Run test script (${{ matrix.tag }}) - uses: mosteo-actions/docker-run@v1 + uses: mosteo-actions/docker-run@v2 with: image: ghcr.io/alire-project/docker/gnat:${{matrix.tag}} command: scripts/ci-github.sh diff --git a/scripts/ci-github.sh b/scripts/ci-github.sh index 06800270e..53d36e9dd 100755 --- a/scripts/ci-github.sh +++ b/scripts/ci-github.sh @@ -51,8 +51,8 @@ echo GNAT VERSION: gnatls -v echo ............................ -echo ALR VERSION: -alr version +echo "ALR VERSION (at $(which alr)):" +alr -d version echo ............................ # Set up index if not default: @@ -61,6 +61,10 @@ if [ "${INDEX:-}" != "" ]; then alr index --name default --add "$INDEX" fi +echo "ALR SETTINGS (global):" +alr settings --global +echo ............................ + echo ALR SEARCH: # List releases for the record alr -q -d search --list --external diff --git a/src/alire/alire-directories.adb b/src/alire/alire-directories.adb index f9551b4d4..8824cd6d8 100644 --- a/src/alire/alire-directories.adb +++ b/src/alire/alire-directories.adb @@ -13,6 +13,9 @@ with Alire.Platforms.Folders; with Alire.VFS; with Alire.Utils; +with Den.Filesystem; +with Den.Walk; + with GNAT.String_Hash; with GNATCOLL.VFS; @@ -392,51 +395,35 @@ package body Alire.Directories is Max_Depth : Natural := Natural'Last) return AAA.Strings.Vector is + use all type Den.Kinds; Found : AAA.Strings.Vector; - procedure Locate (Folder : String; - Current_Depth : Natural; - Max_Depth : Natural) + ----------- + -- Check -- + ----------- + + procedure Check (Item : Den.Walk.Item; + Enter : in out Boolean; + Stop : in out Boolean) is - use Ada.Directories; - Search : Search_Type; begin - Start_Search (Search, Folder, "", - Filter => (Ordinary_File => True, - Directory => True, - others => False)); - - while More_Entries (Search) loop - declare - Current : Directory_Entry_Type; - begin - Get_Next_Entry (Search, Current); - if Kind (Current) = Directory then - if Simple_Name (Current) /= "." - and then - Simple_Name (Current) /= ".." - and then - Current_Depth < Max_Depth - then - Locate (Folder / Simple_Name (Current), - Current_Depth + 1, - Max_Depth); - end if; - elsif Kind (Current) = Ordinary_File - and then Simple_Name (Current) = Simple_Name (Name) - then - Found.Append (Folder / Name); - end if; - end; - end loop; + Stop := False; - End_Search (Search); - end Locate; + if Max_Depth < Natural'Last and then Item.Depth > Max_Depth then + Enter := False; + end if; + + if Den.Kind (Item.Path) = File + and then Den.Name (Item.Path) = Den.Name (Name) + then + Found.Append (Item.Path); + end if; + end Check; - use Ada.Directories; begin - if Exists (Folder) and then Kind (Folder) = Directory then - Locate (Folder, 0, Max_Depth); + if Den.Exists (Folder) and then Den.Kind (Folder) = Den.Directory then + Den.Walk.Find (Folder, + Check'Access); end if; return Found; @@ -451,7 +438,9 @@ package body Alire.Directories is return Any_Path is begin - return AAA.Directories.Relative_Path (Parent, Child); + return Result : constant Any_Path := + Den.Filesystem.Relative (Den.Scrub (Parent), + Den.Scrub (Child)); end Find_Relative_Path; ---------------------- @@ -820,33 +809,32 @@ package body Alire.Directories is Remove_From_Source : Boolean) is - Base : constant Absolute_Path := Adirs.Full_Name (Src); - Target : constant Absolute_Path := Adirs.Full_Name (Dst); + Base : constant Absolute_Path := Den.Filesystem.Absolute (Src); + Target : constant Absolute_Path := Den.Filesystem.Absolute (Dst); ----------- -- Merge -- ----------- procedure Merge - (Item : Ada.Directories.Directory_Entry_Type; + (Item : Any_Path; Stop : in out Boolean) is - use all type Adirs.File_Kind; - + use all type Den.Kinds; + Src : constant Absolute_Path := Den.Filesystem.Absolute (Item); Rel_Path : constant Relative_Path := - Find_Relative_Path (Base, Adirs.Full_Name (Item)); + Find_Relative_Path (Base, Src); -- If this proves to be too slow, we should do our own recursion, -- building the relative path along the way, as this is recomputing -- it for every file needlessly. Dst : constant Absolute_Path := Target / Rel_Path; - Src : constant Absolute_Path := Adirs.Full_Name (Item); begin Stop := False; -- Check if we must skip (we delete source file) - if Adirs.Kind (Item) = Ordinary_File + if Den.Kind (Item) /= Directory and then Skip_Top_Level_Files and then Base = Parent (Src) then @@ -856,7 +844,7 @@ package body Alire.Directories is -- Create a new dir if necessary - if Adirs.Kind (Item) = Directory then + if Den.Kind (Item) = Directory then if not Is_Directory (Dst) then Trace.Debug (" Merge: Creating destination dir " & Dst); Create_Tree (Dst); @@ -870,15 +858,15 @@ package body Alire.Directories is -- Copy file into place Trace.Debug (" Merge: copying " - & Adirs.Full_Name (Item) + & Den.Filesystem.Absolute (Item) & " into " & Dst); - if Adirs.Exists (Dst) then + if Den.Exists (Dst) then if Fail_On_Existing_File then Recoverable_User_Error ("Cannot copy " & TTY.URL (Src) & " into place, file already exists: " & TTY.URL (Dst)); - elsif Adirs.Kind (Dst) /= Ordinary_File then + elsif Den.Kind (Dst) /= File then Raise_Checked_Error ("Cannot overwrite " & TTY.URL (Dst) & " as it is not a regular file"); else @@ -912,7 +900,11 @@ package body Alire.Directories is exception when E : others => Trace.Error - ("When copying " & Src & " --> " & Dst & ": "); + ("When copying " & Src & " (" & Den.Kind (Src)'Image + & ") --> " & Dst & ": "); + Trace.Error + ("Src item was: " + & Item & " (" & Den.Kind (Item)'Image & ")"); Log_Exception (E, Error); raise; end; @@ -941,112 +933,60 @@ package body Alire.Directories is procedure Traverse_Tree (Start : Any_Path; Doing : access procedure - (Item : Ada.Directories.Directory_Entry_Type; + (Item : Any_Path; Stop : in out Boolean); Recurse : Boolean := False; Spinner : Boolean := False) is use Ada.Directories; - Visited : AAA.Strings.Set; - -- To avoid infinite recursion in case of softlinks pointed to parent - -- folders - Progress : Simple_Logging.Ongoing := Simple_Logging.Activity (Text => "Exploring " & Start, Level => (if Spinner then Info else Debug)); - procedure Go_Down (Item : Directory_Entry_Type); - - ---------------------------- - -- Traverse_Tree_Internal -- - ---------------------------- - - procedure Traverse_Tree_Internal - (Start : Any_Path; - Doing : access procedure - (Item : Ada.Directories.Directory_Entry_Type; - Stop : in out Boolean); - Recurse : Boolean := False) - is - pragma Unreferenced (Doing, Recurse); - begin - Search (Start, - "", - (Directory => True, Ordinary_File => True, others => False), - Go_Down'Access); - end Traverse_Tree_Internal; - ------------- -- Go_Down -- ------------- - procedure Go_Down (Item : Directory_Entry_Type) is - Stop : Boolean := False; - Prune : Boolean := False; - VF : constant VFS.Virtual_File := - VFS.New_Virtual_File (VFS.From_FS (Full_Name (Item))); - -- We use this later to check whether this is a soft link + procedure Go_Down (This : Den.Walk.Item; + Enter : in out Boolean; + Stop : in out Boolean) + is + use all type Den.Kinds; + Path : constant Any_Path := This.Path; begin + Enter := True; + Stop := False; - -- Ada.Directories reports softlinks not as special files but as the - -- target of the link. This confuses users of Traverse_Tree that may - -- see files within a folder that has never been visited before. - - -- Short of introducing new file kinds for softlinks and reporting - -- them to clients, for now we just ignore softlinks to dirs, and - -- this way only actual folders are traversed. - - if VF.Is_Symbolic_Link and then Kind (Item) = Directory then - Trace.Warning ("Skipping softlink dir during tree traversal: " - & Full_Name (Item)); + begin + Doing (This.Path, Stop); + exception + when Traverse_Tree_Prune_Dir => + Enter := False; + end; + if Stop then return; end if; - if Simple_Name (Item) /= "." and then Simple_Name (Item) /= ".." then - begin - Doing (Item, Stop); - exception - when Traverse_Tree_Prune_Dir => - Prune := True; - end; - if Stop then - return; - end if; - - if not Prune and then Recurse and then Kind (Item) = Directory then - declare - Normal_Name : constant Absolute_Path - := - String (GNATCOLL.VFS.Full_Name - (VFS.New_Virtual_File (Full_Name (Item)), - Normalize => True, - Resolve_Links => True).all); - begin - if Visited.Contains (Normal_Name) then - Trace.Debug ("Not revisiting " & Normal_Name); - else - Visited.Insert (Normal_Name); - if Spinner then - Progress.Step ("Exploring .../" & Simple_Name (Item)); - end if; - Traverse_Tree_Internal (Normal_Name, Doing, Recurse); - end if; - end; - elsif Prune and then Kind (Item) = Directory then - Trace.Debug ("Skipping dir: " & Full_Name (Item)); - elsif Prune and then Kind (Item) /= Directory then - Trace.Warning ("Pruning of non-dir entry has no effect: " - & Full_Name (Item)); + if Enter and then Recurse and then Den.Kind (Path) = Directory then + if Spinner then + Progress.Step ("Exploring .../" & Simple_Name (Path)); end if; + elsif not Enter and then Den.Kind (Path) = Directory then + Trace.Debug ("Skipping dir: " & Full_Name (Path)); + elsif not Enter and then Den.Kind (Path) /= Directory then + Trace.Warning ("Pruning of non-dir entry has no effect: " + & Full_Name (Path)); end if; end Go_Down; begin Trace.Debug ("Traversing folder: " & Adirs.Full_Name (Start)); - Traverse_Tree_Internal (Start, Doing, Recurse); + Den.Walk.Find (Start, + Action => Go_Down'Access, + Options => (Enter_Regular_Dirs => Recurse, others => <>)); end Traverse_Tree; --------------- @@ -1062,7 +1002,7 @@ package body Alire.Directories is -- Accumulate -- ---------------- - procedure Accumulate (Item : Directory_Entry_Type; + procedure Accumulate (Item : Any_Path; Stop : in out Boolean) is begin diff --git a/src/alire/alire-directories.ads b/src/alire/alire-directories.ads index bbf513751..250b4b709 100644 --- a/src/alire/alire-directories.ads +++ b/src/alire/alire-directories.ads @@ -114,7 +114,7 @@ package Alire.Directories is procedure Traverse_Tree (Start : Any_Path; Doing : access procedure - (Item : Ada.Directories.Directory_Entry_Type; + (Item : Any_Path; Stop : in out Boolean); Recurse : Boolean := False; Spinner : Boolean := False); diff --git a/src/alire/alire-install.adb b/src/alire/alire-install.adb index 319237029..133fcda61 100644 --- a/src/alire/alire-install.adb +++ b/src/alire/alire-install.adb @@ -395,7 +395,7 @@ package body Alire.Install is Result : Installed_Milestones; procedure Find - (Item : Ada.Directories.Directory_Entry_Type; + (Item : Any_Path; Stop : in out Boolean) is Name : constant String := Adirs.Simple_Name (Item); diff --git a/src/alire/alire-os_lib.ads b/src/alire/alire-os_lib.ads index b2c88852a..5b0b2b81a 100644 --- a/src/alire/alire-os_lib.ads +++ b/src/alire/alire-os_lib.ads @@ -65,9 +65,7 @@ private function To_Portable (Path : Any_Path) return Portable_Path_Like - is (case GNATCOLL.OS.Constants.OS is - when MacOS | Unix => Path, - when Windows => Replace (Path, "\", "/")); + is (Replace (Path, "\", "/")); -------------------- -- To_Native_Like -- @@ -75,7 +73,7 @@ private function To_Native (Path : Portable_Path_Like) return Native_Path_Like is (case GNATCOLL.OS.Constants.OS is - when MacOS | Unix => Path, + when MacOS | Unix => Replace (String (Path), "\", "/"), when Windows => Replace (String (Path), "/", "\")); end Alire.OS_Lib; diff --git a/src/alire/alire-roots.adb b/src/alire/alire-roots.adb index 5b60531da..fae9448b8 100644 --- a/src/alire/alire-roots.adb +++ b/src/alire/alire-roots.adb @@ -19,6 +19,9 @@ with Alire.Toolchains.Solutions; with Alire.User_Pins.Maps; with Alire.Utils.TTY; with Alire.Utils.User_Input; +with Alire.VFS; + +with Den.Filesystem; with GNAT.OS_Lib; with GNAT.SHA256; @@ -1229,17 +1232,17 @@ package body Alire.Roots is Found : AAA.Strings.Set; -- Milestone --> Description procedure Check_Dir - (Item : Ada.Directories.Directory_Entry_Type; - Stop : in out Boolean) + (Item : Any_Path; + Stop : in out Boolean) is pragma Unreferenced (Stop); - use Ada.Directories; + use all type Den.Kinds; begin - if Kind (Item) /= Directory then + if Den.Kind (Item) /= Directory then return; end if; - if Simple_Name (Item) = Paths.Working_Folder_Inside_Root + if Den.Name (Item) = Paths.Working_Folder_Inside_Root then -- This is an alire metadata folder, don't go in. It could also be -- a crate named "alire" but that seems like a bad idea anyway. @@ -1250,14 +1253,23 @@ package body Alire.Roots is declare Opt : Optional.Root := - Optional.Detect_Root (Full_Name (Item)); + Optional.Detect_Root (Den.Filesystem.Full_Name (Item)); begin if Opt.Is_Valid then Found.Insert - (TTY.URL (Directories.Find_Relative_Path - (Starting_Path, Full_Name (Item))) & "/" - & Opt.Value.Release.Constant_Reference.Milestone.TTY_Image - & ": " & TTY.Emph + (TTY.URL + (String + (VFS.To_Portable + (Directories.Find_Relative_Path + (Den.Filesystem.Full_Name (Starting_Path), + Den.Filesystem.Full_Name (Item)))) + -- We use both full names because on Windows we see + -- mixed short/long names for the same path if we + -- apply Full_Name to only one of them. + & "/" + & Opt.Value.Release.Constant_Reference.Milestone.TTY_Image) + & ": " + & TTY.Emph (if Opt.Value.Release.Constant_Reference.Description /= "" then Opt.Value.Release.Constant_Reference.Description else "(no description)")); diff --git a/src/alire/alire-toml_index.adb b/src/alire/alire-toml_index.adb index b60e714b0..8534d77e1 100644 --- a/src/alire/alire-toml_index.adb +++ b/src/alire/alire-toml_index.adb @@ -58,7 +58,7 @@ package body Alire.TOML_Index is -- describes a supported index, and that the file tree follows the proper -- naming conventions, without extraneous files being present. - procedure Load_Manifest (Item : Ada.Directories.Directory_Entry_Type; + procedure Load_Manifest (Item : Any_Path; Stop : in out Boolean); -- Check if entry is a candidate to manifest file, and in that case load -- its contents. May raise Checked_Error. @@ -273,7 +273,8 @@ package body Alire.TOML_Index is end return; when others => Result := - Outcome_Failure ("Several index.toml files found in index"); + Outcome_Failure ("Several index.toml files found in index: " + & Repo_Version_Files.Flatten (";")); return ""; end case; end Locate_Root; @@ -357,7 +358,7 @@ package body Alire.TOML_Index is -- Load_Manifest -- ------------------- - procedure Load_Manifest (Item : Ada.Directories.Directory_Entry_Type; + procedure Load_Manifest (Item : Any_Path; Stop : in out Boolean) is pragma Unreferenced (Stop); diff --git a/src/alire/alire-toolchains.adb b/src/alire/alire-toolchains.adb index 560b0a499..2e70dba39 100644 --- a/src/alire/alire-toolchains.adb +++ b/src/alire/alire-toolchains.adb @@ -528,7 +528,7 @@ package body Alire.Toolchains is -- Detect -- ------------ - procedure Detect (Item : Ada.Directories.Directory_Entry_Type; + procedure Detect (Item : Any_Path; Stop : in out Boolean) is use Ada.Directories; diff --git a/src/alr/alr-commands-clean.adb b/src/alr/alr-commands-clean.adb index d22918736..1e58bc496 100644 --- a/src/alr/alr-commands-clean.adb +++ b/src/alr/alr-commands-clean.adb @@ -42,7 +42,7 @@ package body Alr.Commands.Clean is -- Add_Target -- ---------------- - procedure Add_Target (Item : Ada.Directories.Directory_Entry_Type; + procedure Add_Target (Item : Alire.Any_Path; Unused_Stop : in out Boolean) is use Ada.Directories; diff --git a/src/alr/alr-commands-test.adb b/src/alr/alr-commands-test.adb index cfe4c35dd..d16d31705 100644 --- a/src/alr/alr-commands-test.adb +++ b/src/alr/alr-commands-test.adb @@ -436,7 +436,7 @@ package body Alr.Commands.Test is -- Not_Empty -- --------------- - procedure Not_Empty (Item : Ada.Directories.Directory_Entry_Type; + procedure Not_Empty (Item : Alire.Any_Path; Stop : in out Boolean) is pragma Unreferenced (Item, Stop); diff --git a/src/alr/alr-files.adb b/src/alr/alr-files.adb index f728fa1c5..855580bec 100644 --- a/src/alr/alr-files.adb +++ b/src/alr/alr-files.adb @@ -1,4 +1,4 @@ -with Ada.Directories; +with Den.Filesystem; package body Alr.Files is @@ -7,19 +7,27 @@ package body Alr.Files is ------------------------- function Locate_Any_GPR_File return Natural is - use Ada.Directories; Candidates : AAA.Strings.Vector; - procedure Check (File : Directory_Entry_Type) is + ----------- + -- Check -- + ----------- + + procedure Check (File : Alire.Any_Path; Stop : in out Boolean) is + use AAA.Strings; begin - Candidates.Append (Full_Name (File)); + Stop := False; + if Den.Kind (File) in Den.File + and then Has_Suffix (To_Lower_Case (File), ".gpr") + then + Candidates.Append (Den.Filesystem.Full (File)); + end if; end Check; begin - Search (Current_Directory, - "*.gpr", - (Ordinary_File => True, others => False), - Check'Access); + Alire.Directories.Traverse_Tree + (Alire.Directories.Current, + Check'Access); return Natural (Candidates.Length); end Locate_Any_GPR_File; diff --git a/testsuite/run.py b/testsuite/run.py index e20b2df9f..90b8eccef 100755 --- a/testsuite/run.py +++ b/testsuite/run.py @@ -11,6 +11,7 @@ import os.path import shutil +import subprocess import sys from argparse import ArgumentTypeError @@ -41,7 +42,10 @@ def require_executable(self, name): if path is None: raise FileNotFoundError(f"{name} not found in PATH") else: - print(f"Using {name} at {path}") + print(f"Testsuite using {name} at {path} with version:", ) + print(subprocess.run([name, '--version'], + stdout=subprocess.PIPE).stdout.decode()) + sys.stdout.flush() def set_up(self): super().set_up() diff --git a/testsuite/tests/install/softlinks/test.py b/testsuite/tests/install/softlinks/test.py index a0f881d6f..6a06b6460 100644 --- a/testsuite/tests/install/softlinks/test.py +++ b/testsuite/tests/install/softlinks/test.py @@ -1,6 +1,8 @@ """ Test that binary files containing softlinks can be installed properly. The test -crate contains all kinds of pernicious links (broken, recursive, etc.): +crate contains all kinds of pernicious links (broken, recursive, etc.). + +This test is Unix-only, as Windows' tar cannot recreate the broken links: crate/ ├── bin -> subdir/bin @@ -30,8 +32,9 @@ import os import shutil +import subprocess from drivers.alr import run_alr, crate_dirname -from drivers.helpers import contents, on_windows +from drivers.helpers import contents def kind(file): @@ -42,11 +45,6 @@ def ls(path): return out.stdout -# Does not apply to Windows as it does not support softlinks -if on_windows(): - print('SKIP: on Windows, unapplicable') - sys.exit(0) - # This command should succeed normally run_alr("install", "--prefix=install", "crate") @@ -80,5 +78,6 @@ def ls(path): # Cleanup os.chdir("..") shutil.rmtree(cratedir) +shutil.rmtree("install") print('SUCCESS') diff --git a/testsuite/tests/install/softlinks/test.yaml b/testsuite/tests/install/softlinks/test.yaml index 1f89021f2..58bf7be7d 100644 --- a/testsuite/tests/install/softlinks/test.yaml +++ b/testsuite/tests/install/softlinks/test.yaml @@ -1,6 +1,6 @@ driver: python-script control: - - [SKIP, "skip_unix", "Test is Unix-only"] + - [SKIP, "skip_unix", "Test is Unix-only"] indexes: my_index: in_fixtures: false diff --git a/testsuite/tests/misc/dir-traversal/test.py b/testsuite/tests/misc/dir-traversal/test.py new file mode 100644 index 000000000..408943982 --- /dev/null +++ b/testsuite/tests/misc/dir-traversal/test.py @@ -0,0 +1,25 @@ +""" +Check that broken/recursive symlinks don't cause alr to fail +""" + +import os +from drivers.alr import run_alr, init_local_crate +# from drivers.asserts import assert_eq, assert_match + +init_local_crate() + +# Create a symbolic link to itself. This used to cause alr to fail. +os.symlink("self", "self") + +# Commands that traverse looking for things (crates, executables) shouldn't +# fail. + +run_alr("clean", "--temp") +run_alr("run") +run_alr("run", "--list") +run_alr("show", "--nested") + +# Remove the symlink, otherwise it breaks the testsuite driver +os.unlink("self") + +print("SUCCESS") \ No newline at end of file diff --git a/testsuite/tests/misc/dir-traversal/test.yaml b/testsuite/tests/misc/dir-traversal/test.yaml new file mode 100644 index 000000000..9a541fdd1 --- /dev/null +++ b/testsuite/tests/misc/dir-traversal/test.yaml @@ -0,0 +1,6 @@ +driver: python-script +build_mode: both +control: + - [SKIP, "skip_unix", "Test is Unix-only"] +indexes: + compiler_only_index: {} diff --git a/testsuite/tests/show/nested/test.py b/testsuite/tests/show/nested/test.py index 90de669f3..98744e3eb 100644 --- a/testsuite/tests/show/nested/test.py +++ b/testsuite/tests/show/nested/test.py @@ -17,7 +17,7 @@ # After entering the crate, it is no longer nested and shouldn't be detected os.chdir("xxx") -assert_match("\s*", +assert_match(r"\s*", run_alr("show", "--nested", quiet=False).out) # If we initialize another crate without entering it, it should again be From 493d0c18a6cf4f1916b79a60a88c27d7e586c421 Mon Sep 17 00:00:00 2001 From: Maxim Reznik Date: Mon, 12 Aug 2024 17:07:37 +0300 Subject: [PATCH 44/53] Build on Mac OS X ARM64 (#1731) --- .github/workflows/ci-macos.yml | 28 ++++++++++++++++++++++++---- scripts/version-patcher.sh | 2 +- 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci-macos.yml b/.github/workflows/ci-macos.yml index 15025e11b..6903a4462 100644 --- a/.github/workflows/ci-macos.yml +++ b/.github/workflows/ci-macos.yml @@ -11,12 +11,20 @@ on: types: [published] workflow_dispatch: +env: + ARCH: x86_64 + jobs: build: name: CI on macOS - runs-on: macos-12 + strategy: + fail-fast: false + matrix: + os: [macos-12, macos-14] + + runs-on: ${{ matrix.os }} steps: - name: Check out repository @@ -24,11 +32,23 @@ jobs: with: submodules: true - - name: Install FSF toolchain + - name: Install FSF toolchain (x86_64) + if: ${{ matrix.os != 'macos-14' }} uses: alire-project/alr-install@v1 with: crates: gnat_native gprbuild + - name: Install FSF toolchain (AArch64) + if: ${{ matrix.os == 'macos-14' }} + run: | + curl -L -O https://github.com/alire-project/alire/releases/download/nightly/alr-nightly-bin-aarch64-macos.zip + unzip alr-nightly-bin-aarch64-macos.zip bin/alr + bin/alr index --reset-community + bin/alr install gnat_native gprbuild --prefix alire_prefix + echo $PWD/bin >> $GITHUB_PATH + echo $PWD/alire_prefix/bin >> $GITHUB_PATH + echo "ARCH=aarch64" >> $GITHUB_ENV + - name: Install Python 3.x (required for the testsuite) uses: actions/setup-python@v2 with: @@ -44,7 +64,7 @@ jobs: - name: Upload binaries uses: actions/upload-artifact@v2 with: - name: alr-bin-macos.zip + name: alr-bin-${{ env.ARCH }}-macos.zip path: | bin/alr LICENSE.txt @@ -82,5 +102,5 @@ jobs: with: upload_url: ${{ steps.get_release.outputs.upload_url }} asset_path: alr-bin-macos.zip - asset_name: alr-${{ steps.get_version.outputs.version-without-v }}-bin-x86_64-macos.zip + asset_name: alr-${{ steps.get_version.outputs.version-without-v }}-bin-${{ env.ARCH }}-macos.zip asset_content_type: application/zip diff --git a/scripts/version-patcher.sh b/scripts/version-patcher.sh index 9e666d56e..d377825b0 100755 --- a/scripts/version-patcher.sh +++ b/scripts/version-patcher.sh @@ -19,7 +19,7 @@ elif (which gprbuild &>/dev/null); then gprbuild -P support/version_patcher/version_patcher.gpr elif (which alr &>/dev/null); then echo "Building patcher with alr..." - alr -C "$(dirname $bin)" build + alr -C "$(dirname $(dirname $bin))" build else echo "WARNING: No Ada tool available to build patcher, skipping." exit 0 From d296745691926f3c32217b59f93236281593132b Mon Sep 17 00:00:00 2001 From: Seb M'Caw Date: Mon, 19 Aug 2024 15:21:15 +0100 Subject: [PATCH 45/53] Add check that commit ID is valid hexadecimal (#1740) --- src/alire/alire-origins.adb | 4 ++-- src/alire/alire-origins.ads | 4 ++++ testsuite/tests/publish/bad-arguments/test.py | 10 ++++++++++ 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/alire/alire-origins.adb b/src/alire/alire-origins.adb index d838ffd8f..658791d6e 100644 --- a/src/alire/alire-origins.adb +++ b/src/alire/alire-origins.adb @@ -427,14 +427,14 @@ package body Alire.Origins is begin case Scheme is when Pure_Git | Git | HTTP => - if Commit'Length /= Git_Commit'Length then + if not Is_Valid_Commit (Commit) then Raise_Checked_Error ("invalid git commit id, " & "40 digits hexadecimal expected"); end if; return New_Git (VCS_URL, Commit, Subdir); when Hg => - if Commit'Length /= Hg_Commit'Length then + if not Is_Valid_Mercurial_Commit (Commit) then Raise_Checked_Error ("invalid mercurial commit id, " & "40 digits hexadecimal expected"); diff --git a/src/alire/alire-origins.ads b/src/alire/alire-origins.ads index b24327cb8..cf67cd64d 100644 --- a/src/alire/alire-origins.ads +++ b/src/alire/alire-origins.ads @@ -130,6 +130,10 @@ package Alire.Origins is is (S'Length = Git_Commit'Length and then (for all Char of S => Char in Alire.Utils.Hexadecimal_Character)); + function Is_Valid_Mercurial_Commit (S : String) return Boolean + is (S'Length = Hg_Commit'Length and then + (for all Char of S => Char in Alire.Utils.Hexadecimal_Character)); + function Short_Commit (Commit : String) return String; -- First characters in the commit diff --git a/testsuite/tests/publish/bad-arguments/test.py b/testsuite/tests/publish/bad-arguments/test.py index 4c6f13b7b..723013038 100644 --- a/testsuite/tests/publish/bad-arguments/test.py +++ b/testsuite/tests/publish/bad-arguments/test.py @@ -34,6 +34,16 @@ assert_match(".*invalid git commit id, 40 digits hexadecimal expected.*", p.out) +# Bad commit characters +p = run_alr("publish", "git+http://github.com/repo", "_"*40, + complain_on_error=False) +assert_match(".*invalid git commit id, 40 digits hexadecimal expected.*", + p.out) +p = run_alr("publish", "hg+http://host.name/repo", "_"*40, + complain_on_error=False) +assert_match(".*invalid mercurial commit id, 40 digits hexadecimal expected.*", + p.out) + # VCS without transport or extension p = run_alr("publish", "http://somehost.com/badrepo", "deadbeef", complain_on_error=False) From df1d561136190987550b49faeabce881ed08137a Mon Sep 17 00:00:00 2001 From: Alejandro R Mosteo Date: Fri, 30 Aug 2024 11:35:22 +0200 Subject: [PATCH 46/53] Rename tests with old 'config' naming to 'settings' (#1746) --- .../my_index/he/hello/hello-1.0.0.toml | 0 .../my_index/he/hello/hello-1.0.1.toml | 0 .../my_index/index.toml | 0 .../my_index/li/libhello/libhello-1.0.0.toml | 0 .../{missing-config-default => missing-settings-default}/test.py | 0 .../test.yaml | 0 .../missing-settings-path}/test.py | 0 .../missing-settings-path}/test.yaml | 0 .../{relative_config_path => relative-settings-path}/test.py | 0 .../{relative_config_path => relative-settings-path}/test.yaml | 0 10 files changed, 0 insertions(+), 0 deletions(-) rename testsuite/tests/settings/{missing-config-default => missing-settings-default}/my_index/he/hello/hello-1.0.0.toml (100%) rename testsuite/tests/settings/{missing-config-default => missing-settings-default}/my_index/he/hello/hello-1.0.1.toml (100%) rename testsuite/tests/settings/{missing-config-default => missing-settings-default}/my_index/index.toml (100%) rename testsuite/tests/settings/{missing-config-default => missing-settings-default}/my_index/li/libhello/libhello-1.0.0.toml (100%) rename testsuite/tests/settings/{missing-config-default => missing-settings-default}/test.py (100%) rename testsuite/tests/settings/{missing-config-default => missing-settings-default}/test.yaml (100%) rename testsuite/tests/{config/missing-config-path => settings/missing-settings-path}/test.py (100%) rename testsuite/tests/{config/missing-config-path => settings/missing-settings-path}/test.yaml (100%) rename testsuite/tests/settings/{relative_config_path => relative-settings-path}/test.py (100%) rename testsuite/tests/settings/{relative_config_path => relative-settings-path}/test.yaml (100%) diff --git a/testsuite/tests/settings/missing-config-default/my_index/he/hello/hello-1.0.0.toml b/testsuite/tests/settings/missing-settings-default/my_index/he/hello/hello-1.0.0.toml similarity index 100% rename from testsuite/tests/settings/missing-config-default/my_index/he/hello/hello-1.0.0.toml rename to testsuite/tests/settings/missing-settings-default/my_index/he/hello/hello-1.0.0.toml diff --git a/testsuite/tests/settings/missing-config-default/my_index/he/hello/hello-1.0.1.toml b/testsuite/tests/settings/missing-settings-default/my_index/he/hello/hello-1.0.1.toml similarity index 100% rename from testsuite/tests/settings/missing-config-default/my_index/he/hello/hello-1.0.1.toml rename to testsuite/tests/settings/missing-settings-default/my_index/he/hello/hello-1.0.1.toml diff --git a/testsuite/tests/settings/missing-config-default/my_index/index.toml b/testsuite/tests/settings/missing-settings-default/my_index/index.toml similarity index 100% rename from testsuite/tests/settings/missing-config-default/my_index/index.toml rename to testsuite/tests/settings/missing-settings-default/my_index/index.toml diff --git a/testsuite/tests/settings/missing-config-default/my_index/li/libhello/libhello-1.0.0.toml b/testsuite/tests/settings/missing-settings-default/my_index/li/libhello/libhello-1.0.0.toml similarity index 100% rename from testsuite/tests/settings/missing-config-default/my_index/li/libhello/libhello-1.0.0.toml rename to testsuite/tests/settings/missing-settings-default/my_index/li/libhello/libhello-1.0.0.toml diff --git a/testsuite/tests/settings/missing-config-default/test.py b/testsuite/tests/settings/missing-settings-default/test.py similarity index 100% rename from testsuite/tests/settings/missing-config-default/test.py rename to testsuite/tests/settings/missing-settings-default/test.py diff --git a/testsuite/tests/settings/missing-config-default/test.yaml b/testsuite/tests/settings/missing-settings-default/test.yaml similarity index 100% rename from testsuite/tests/settings/missing-config-default/test.yaml rename to testsuite/tests/settings/missing-settings-default/test.yaml diff --git a/testsuite/tests/config/missing-config-path/test.py b/testsuite/tests/settings/missing-settings-path/test.py similarity index 100% rename from testsuite/tests/config/missing-config-path/test.py rename to testsuite/tests/settings/missing-settings-path/test.py diff --git a/testsuite/tests/config/missing-config-path/test.yaml b/testsuite/tests/settings/missing-settings-path/test.yaml similarity index 100% rename from testsuite/tests/config/missing-config-path/test.yaml rename to testsuite/tests/settings/missing-settings-path/test.yaml diff --git a/testsuite/tests/settings/relative_config_path/test.py b/testsuite/tests/settings/relative-settings-path/test.py similarity index 100% rename from testsuite/tests/settings/relative_config_path/test.py rename to testsuite/tests/settings/relative-settings-path/test.py diff --git a/testsuite/tests/settings/relative_config_path/test.yaml b/testsuite/tests/settings/relative-settings-path/test.yaml similarity index 100% rename from testsuite/tests/settings/relative_config_path/test.yaml rename to testsuite/tests/settings/relative-settings-path/test.yaml From 604477a24ddbcb8acaa18c341c090da8d0137c0d Mon Sep 17 00:00:00 2001 From: Alejandro R Mosteo Date: Tue, 3 Sep 2024 12:51:25 +0200 Subject: [PATCH 47/53] Fix syncing of releases containing broken softlinks (#1751) * WIP: fix failure syncing softlinks * Test for fix --- alire.toml | 2 +- deps/den | 2 +- src/alire/alire-builds.adb | 18 +++--- testsuite/drivers/builds.py | 11 +--- .../cache/softlinks/my_index/crate-0.1.0.tgz | Bin 0 -> 760 bytes .../my_index/index/cr/crate/crate-0.1.0.toml | 11 ++++ .../cache/softlinks/my_index/index/index.toml | 1 + testsuite/tests/cache/softlinks/test.py | 60 ++++++++++++++++++ testsuite/tests/cache/softlinks/test.yaml | 8 +++ 9 files changed, 93 insertions(+), 20 deletions(-) create mode 100644 testsuite/tests/cache/softlinks/my_index/crate-0.1.0.tgz create mode 100644 testsuite/tests/cache/softlinks/my_index/index/cr/crate/crate-0.1.0.toml create mode 100644 testsuite/tests/cache/softlinks/my_index/index/index.toml create mode 100644 testsuite/tests/cache/softlinks/test.py create mode 100644 testsuite/tests/cache/softlinks/test.yaml diff --git a/alire.toml b/alire.toml index 4dc401d7f..282d9d152 100644 --- a/alire.toml +++ b/alire.toml @@ -66,7 +66,7 @@ url = "https://github.com/alire-project/clic" commit = "56bbdc008e16996b6f76e443fd0165a240de1b13" [pins.den] url = "https://github.com/mosteo/den" -commit = "35d1f38395b93766dd64bca5901ce3b6a416ba1a" +commit = "681ab5ca522585953f2e1d70763731df34c96012" [pins.dirty_booleans] url = "https://github.com/mosteo/dirty_booleans" diff --git a/deps/den b/deps/den index 35d1f3839..681ab5ca5 160000 --- a/deps/den +++ b/deps/den @@ -1 +1 @@ -Subproject commit 35d1f38395b93766dd64bca5901ce3b6a416ba1a +Subproject commit 681ab5ca522585953f2e1d70763731df34c96012 diff --git a/src/alire/alire-builds.adb b/src/alire/alire-builds.adb index 9593c1c77..373bcbff2 100644 --- a/src/alire/alire-builds.adb +++ b/src/alire/alire-builds.adb @@ -7,7 +7,7 @@ with Alire.Paths.Vault; with Alire.Roots; with Alire.Settings.Builtins; -with GNATCOLL.VFS; +with Den.Filesystem; package body Alire.Builds is @@ -56,16 +56,14 @@ package body Alire.Builds is Simple_Logging.Activity ("Syncing " & Release.Milestone.TTY_Image) with Unreferenced; - Success : Boolean := False; - use GNATCOLL.VFS; begin - GNATCOLL.VFS.Copy - (Create (+Src), - +Dst, - Success); - - Assert (Success, - "Could not sync build dir from " & Src & " to " & Dst); + Den.Filesystem.Create_Directory (Dst); + Den.Filesystem.Copy (Src, Dst); + exception + when E : others => + Log_Exception (E); + Raise_Checked_Error + ("Could not sync build dir from " & Src & " to " & Dst); end; -- At this point we can generate the final crate configuration diff --git a/testsuite/drivers/builds.py b/testsuite/drivers/builds.py index 48ff323d9..05e6359c7 100644 --- a/testsuite/drivers/builds.py +++ b/testsuite/drivers/builds.py @@ -83,14 +83,9 @@ def sync() -> None: """ Sync the shared build directory """ - # We force the sync by running a build, no matter if it succeeds or not - try: - subprocess.run(["alr", "-q", "-d", "build"] - , stdout=subprocess.DEVNULL - , stderr=subprocess.DEVNULL - ) - except: - pass + run_alr("build", "--stop-after=generation") + return + def sync_builds() -> None: sync() diff --git a/testsuite/tests/cache/softlinks/my_index/crate-0.1.0.tgz b/testsuite/tests/cache/softlinks/my_index/crate-0.1.0.tgz new file mode 100644 index 0000000000000000000000000000000000000000..4e90424e30d169be7a5f25d147ff9d14ddc67a3b GIT binary patch literal 760 zcmV9PbRthD9ywG-MR5<|S2 zc-8j*gai^uW99S2GmhD`)oJ%dlvIkRVj+abITPN=8FBsoi-NRPN(xEJ7J^7>Tdd4d z1G?s|J5}v!ReY}w&As0}`u;CvbLGGPyud7&=igeRy?&#;f2pj1{~V0{uj>PIjKRNq z;NLh)N+})k-w36OmF(Wu+qzv~L|4H7jsMFh`WML;^_e-5JlebRBw zfdv2B$&mkAlY##n-1Yy?6C((=R?n&bNN5WFjq&xL;0b>p_>cSFH_eeDM*8~i$Vk8b zm*tax4Fh5r$0YDS?Z4aYzcR&0_iz0|F!*0qegXe6|HrD`9ZoDUK2ZNl8vL`wzYF}| z_J3{jvSV$yKH#5>_Wob%uB#8`la2me-u`cL^j0QG+kV*b~R zG3M3(gMUMI=>JjwXCdZ)%XkM!$$w+|{U3+^KM!<-ASK{`da1FbesfgP8w{Pk&PV zXGOpNr(D?o=bVHw;D6Hp`IG#caGxNp!~Q?~FHbc884JK9|5673r2P6XIg8L3@IUE) z^J)G~?_W#!&%xOLzFsq*0ZFL;wbUxq|0GfWXJOj^YqNPNyQUN+M-84(|7)T?|D7Pz z|2cTzpCv}Z{a;hR{#)M&g#BOEX*do1$Ne+KNbxVF=%4>N2md*k_J2N~`O-hlzYh6t zoS{Gdc}5Wd{-gf6Vx;*G^`Eh%;Xelx&;R)jeq5O6|8oAD`u<|A*&)^7LGHyh9lS|0Dn07d_3t8}|Q1@1J`rj)DK%^}lJ3yc subdir/bin + ├── broken -> missing + ├── lib + │ ├── mock.so -> mock.so.0.0 + │ ├── mock.so.0 -> mock.so.0.0 + │ ├── mock.so.0.0 + │ ├── zzz.so -> mock.so + │ └── zzz.so.0 -> mock.so + ├── loop + │ ├── x -> z + │ ├── y -> x + │ └── z -> y + ├── order + │ ├── ab -> b + │ ├── af -> d/f + │ ├── b + │ ├── cb -> b + │ ├── d + │ │ └── f + │ └── zf -> d/f + ├── self -> self + ├── subdir + │ ├── bin + │ │ ├── loop -> ../../subdir + │ │ └── x + │ ├── parent -> .. + │ └── self -> ../subdir + ├── that -> this + └── this -> that + +""" + +import os +import shutil +import drivers.builds as builds +from drivers.alr import alr_with, init_local_crate, run_alr + + +init_local_crate() + +# Make the crate depend on our troublesome crate and ensure syncing +alr_with("crate") +builds.sync() + +# Ensure that a copy has been made to the cache +if builds.are_shared(): + assert os.path.exists(builds.find_dir("crate")) + +# Cleanup +os.chdir("..") +shutil.rmtree("xxx") + +print('SUCCESS') diff --git a/testsuite/tests/cache/softlinks/test.yaml b/testsuite/tests/cache/softlinks/test.yaml new file mode 100644 index 000000000..f0325a232 --- /dev/null +++ b/testsuite/tests/cache/softlinks/test.yaml @@ -0,0 +1,8 @@ +driver: python-script +build_mode: both +control: + - [SKIP, "skip_unix", "Test is Unix-only"] +indexes: + my_index: + in_fixtures: false + compiler_only_index: {} From bc2fb0137e2b67f86dac2cf14247308497a76659 Mon Sep 17 00:00:00 2001 From: Alejandro R Mosteo Date: Tue, 3 Sep 2024 15:33:17 +0200 Subject: [PATCH 48/53] Fix early error on Windows with msys2 disabled (#1747) * Fix non-absolute path returned when msys2 disabled * Add test for the fix * Wrong platform build friendlier message --- src/alire/alire-platforms-common.ads | 9 ++++++++- .../alire-platforms-current__windows.adb | 15 ++++++++++++++- .../alire-platforms-folders__windows.adb | 15 ++++++++++++++- testsuite/tests/settings/distro-disable/test.py | 15 ++++++++++++++- testsuite/tests/settings/distro-disable/test.yaml | 2 +- 5 files changed, 51 insertions(+), 5 deletions(-) diff --git a/src/alire/alire-platforms-common.ads b/src/alire/alire-platforms-common.ads index 488f87041..707176934 100644 --- a/src/alire/alire-platforms-common.ads +++ b/src/alire/alire-platforms-common.ads @@ -20,7 +20,14 @@ private package Alire.Platforms.Common is --------------------- function Unix_Home_Folder return String - is (OS_Lib.Getenv ("HOME", Default => "/tmp")); + is (if OS_Lib.Getenv ("HOME", "unset") = "unset" and then + GNAT.OS_Lib.Directory_Separator = '\' + then + raise Checked_Error with + "$HOME is not set, you might be running an" + & " `alr` built for a non-Windows OS" + else + OS_Lib.Getenv ("HOME", Default => "/tmp")); ---------------------- -- Unix_Temp_Folder -- diff --git a/src/alire/os_windows/alire-platforms-current__windows.adb b/src/alire/os_windows/alire-platforms-current__windows.adb index d4ccb3140..135519306 100644 --- a/src/alire/os_windows/alire-platforms-current__windows.adb +++ b/src/alire/os_windows/alire-platforms-current__windows.adb @@ -88,7 +88,20 @@ package body Alire.Platforms.Current is return Detect_Msys2_Root; when others => - return OS_Lib.Getenv ("HOMEDRIVE"); + declare + Root : constant String := OS_Lib.Getenv ("HOMEDRIVE", "C:\"); + begin + if Root'Length not in 2 | 3 then + Raise_Checked_Error + ("$HOMEDRIVE is not a proper drive: " & Root); + end if; + + if Root (Root'Last) not in '/' | '\' then + return Root & '\'; + else + return Root; + end if; + end; end case; end Distribution_Root; diff --git a/src/alire/os_windows/alire-platforms-folders__windows.adb b/src/alire/os_windows/alire-platforms-folders__windows.adb index f2f2dc7bd..ea9f50fb9 100644 --- a/src/alire/os_windows/alire-platforms-folders__windows.adb +++ b/src/alire/os_windows/alire-platforms-folders__windows.adb @@ -2,6 +2,8 @@ with Ada.Directories; with Alire.OS_Lib; +with GNAT.OS_Lib; + package body Alire.Platforms.Folders is use OS_Lib.Operators; @@ -11,7 +13,18 @@ package body Alire.Platforms.Folders is ---------- function Home return Absolute_Path - is (OS_Lib.Getenv ("USERPROFILE")); + is + begin + if OS_Lib.Getenv ("USERPROFILE", "unset") = "unset" and then + GNAT.OS_Lib.Directory_Separator = '/' + then + Raise_Checked_Error + ("$USERPROFILE not set " + & "(might you be running an `alr` built for Windows?)"); + else + return OS_Lib.Getenv ("USERPROFILE"); + end if; + end Home; ----------- -- Cache -- diff --git a/testsuite/tests/settings/distro-disable/test.py b/testsuite/tests/settings/distro-disable/test.py index 1e45faa25..3d4912bd8 100644 --- a/testsuite/tests/settings/distro-disable/test.py +++ b/testsuite/tests/settings/distro-disable/test.py @@ -2,11 +2,24 @@ Verify that disabling distro detection works as intended """ -from drivers.alr import run_alr, distro_is_known +from drivers.alr import run_alr, distro_is_known, init_local_crate, alr_with, alr_manifest run_alr("settings", "--global", "--set", "distribution.disable_detection", "true") +# Inspect output of `alr version` assert not distro_is_known(), "Unexpected distro detection" +# Ensure that basic environment can be printed for crates that use +# $DISTRIB_ROOT + +init_local_crate() +# Append usage of DISTRIB_ROOT to the manifest +with open(alr_manifest(), "a") as f: + f.write(""" + [environment] + PATH.append = "${DISTRIB_ROOT}" + """) +run_alr("printenv") + print('SUCCESS') diff --git a/testsuite/tests/settings/distro-disable/test.yaml b/testsuite/tests/settings/distro-disable/test.yaml index 872fc1274..fa855459b 100644 --- a/testsuite/tests/settings/distro-disable/test.yaml +++ b/testsuite/tests/settings/distro-disable/test.yaml @@ -1,3 +1,3 @@ driver: python-script indexes: - basic_index: {} + compiler_only_index: {} From 5b438e6f4f0e142a5484faeaf10f49917e346062 Mon Sep 17 00:00:00 2001 From: "Alejandro R. Mosteo" Date: Thu, 5 Sep 2024 12:21:31 +0200 Subject: [PATCH 49/53] Add PR cleaner workflow --- cleaner.yml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 cleaner.yml diff --git a/cleaner.yml b/cleaner.yml new file mode 100644 index 000000000..a4ceee694 --- /dev/null +++ b/cleaner.yml @@ -0,0 +1,19 @@ +name: 'Close stale PRs' +on: + workflow_dispatch: + schedule: + - cron: '11 1 * * *' + +jobs: + stale: + runs-on: ubuntu-latest + permissions: + pull-requests: write + steps: + - uses: actions/stale@v7 + with: + debug-only : false # Set to true to work in dry-run mode + stale-pr-message: 'This PR is stale because it has been open 60 days with no activity. Remove stale label or comment or this will be closed in 30 days.' + close-pr-message: 'This PR was closed because it has been stalled for 90 days with no activity.' + days-before-stale: 60 + days-before-close: 30 From de0b750404c6a4b3ab56384e5318048179609c77 Mon Sep 17 00:00:00 2001 From: Seb M'Caw Date: Tue, 10 Sep 2024 09:30:11 +0100 Subject: [PATCH 50/53] Fix CI using deprecated version of actions/upload-artifact (#1756) * Fix CI on Linux * Fix others * Use v4 tag --- .github/workflows/ci-appimage.yml | 4 ++-- .github/workflows/ci-docker.yml | 2 +- .github/workflows/ci-linux.yml | 4 ++-- .github/workflows/ci-macos.yml | 4 ++-- .github/workflows/ci-unsupported.yml | 2 +- .github/workflows/ci-windows.yml | 8 ++++---- .github/workflows/nightly.yml | 4 ++-- 7 files changed, 14 insertions(+), 14 deletions(-) diff --git a/.github/workflows/ci-appimage.yml b/.github/workflows/ci-appimage.yml index 023b05bbb..f7e3ac4e9 100644 --- a/.github/workflows/ci-appimage.yml +++ b/.github/workflows/ci-appimage.yml @@ -48,7 +48,7 @@ jobs: - name: Upload logs (if failed) if: failure() - uses: actions/upload-artifact@master + uses: actions/upload-artifact@v4 with: name: e3-log-linux.zip path: testsuite/out @@ -114,7 +114,7 @@ jobs: # regular PRs for easy testing. - name: Upload as artifact (when not a release) if: (github.event_name != 'release') - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: alr-${{ steps.get_ref.outputs.short_sha }}-x86_64.AppImage.zip path: alr.AppImage diff --git a/.github/workflows/ci-docker.yml b/.github/workflows/ci-docker.yml index 590cb60c6..09e3ed1e2 100644 --- a/.github/workflows/ci-docker.yml +++ b/.github/workflows/ci-docker.yml @@ -51,7 +51,7 @@ jobs: - name: Upload logs (if failed) if: failure() - uses: actions/upload-artifact@master + uses: actions/upload-artifact@v4 with: name: e3-log-docker-${{ matrix.tag }}.zip path: testsuite/out diff --git a/.github/workflows/ci-linux.yml b/.github/workflows/ci-linux.yml index 7992d2199..e14e64d9a 100644 --- a/.github/workflows/ci-linux.yml +++ b/.github/workflows/ci-linux.yml @@ -48,7 +48,7 @@ jobs: INDEX: "" - name: Upload binaries - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: alr-bin-linux.zip path: | @@ -57,7 +57,7 @@ jobs: - name: Upload logs (if failed) if: failure() - uses: actions/upload-artifact@master + uses: actions/upload-artifact@v4 with: name: e3-log-linux.zip path: testsuite/out diff --git a/.github/workflows/ci-macos.yml b/.github/workflows/ci-macos.yml index 6903a4462..e87ea3571 100644 --- a/.github/workflows/ci-macos.yml +++ b/.github/workflows/ci-macos.yml @@ -62,7 +62,7 @@ jobs: INDEX: "" - name: Upload binaries - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: alr-bin-${{ env.ARCH }}-macos.zip path: | @@ -71,7 +71,7 @@ jobs: - name: Upload logs (if failed) if: failure() - uses: actions/upload-artifact@master + uses: actions/upload-artifact@v4 with: name: testsuite-log-macos.zip path: testsuite/out diff --git a/.github/workflows/ci-unsupported.yml b/.github/workflows/ci-unsupported.yml index 5d19dc271..73252f42f 100644 --- a/.github/workflows/ci-unsupported.yml +++ b/.github/workflows/ci-unsupported.yml @@ -43,7 +43,7 @@ jobs: - name: Upload logs (if failed) if: failure() - uses: actions/upload-artifact@master + uses: actions/upload-artifact@v4 with: name: e3-log-unsupported.zip path: testsuite/out diff --git a/.github/workflows/ci-windows.yml b/.github/workflows/ci-windows.yml index a909227d7..1193bbbad 100644 --- a/.github/workflows/ci-windows.yml +++ b/.github/workflows/ci-windows.yml @@ -89,26 +89,26 @@ jobs: ALR_INSTALL_OS: ${{ runner.os }} - name: Upload installer - uses: actions/upload-artifact@main + uses: actions/upload-artifact@v4 with: name: installer-release-package path: scripts/installer/alire-*.exe - name: Upload zip archive - uses: actions/upload-artifact@main + uses: actions/upload-artifact@v4 with: name: zip-release-package path: scripts/installer/alire-*.zip - name: Upload tar archive - uses: actions/upload-artifact@main + uses: actions/upload-artifact@v4 with: name: tar-release-package path: scripts/installer/alire-*.tar.xz - name: Upload logs (if failed) if: failure() - uses: actions/upload-artifact@master + uses: actions/upload-artifact@v4 with: name: testsuite-log-windows.zip path: testsuite/out diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 0c0944e8f..d44685008 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -58,13 +58,13 @@ jobs: - name: Upload logs (if failed) if: failure() - uses: actions/upload-artifact@master + uses: actions/upload-artifact@v4 with: name: e3-log-linux.zip path: testsuite/out - name: Upload artifact - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: alr-bin-${{ matrix.os }}.zip path: | From 62b73c9aadc3db7ab1fbb7a1c9b992f581296313 Mon Sep 17 00:00:00 2001 From: Alejandro R Mosteo Date: Mon, 23 Sep 2024 20:12:19 +0200 Subject: [PATCH 51/53] JSON, TOML output for tables (#1759) * Headers * Global option --format * fix tables without headers * Testsuite test * Testsuite-found fixes * Bump submodules, testsuite dependencies --- .gitmodules | 3 + alire.gpr | 1 + alire.toml | 18 +- alr_env.gpr | 1 + deps/aaa | 2 +- deps/ansi | 2 +- deps/den | 2 +- deps/lml | 1 + src/alire/alire-index-search.adb | 7 +- src/alire/alire-solutions-diffs.adb | 2 +- src/alire/alire-solutions.adb | 11 +- src/alire/alire-utils-did_you_mean.adb | 6 +- src/alire/alire-utils-tables.adb | 37 +++- src/alire/alire-utils-tables.ads | 30 ++- src/alr/alr-commands-index.adb | 16 +- src/alr/alr-commands-search.adb | 12 +- src/alr/alr-commands-toolchain.adb | 18 +- src/alr/alr-commands-version.adb | 186 +++++++++--------- src/alr/alr-commands.adb | 64 ++++++ testsuite/requirements.txt | 1 + .../tests/misc/structured-tables/test.py | 72 +++++++ .../tests/misc/structured-tables/test.yaml | 4 + testsuite/tests/pin/branch/test.py | 6 +- 23 files changed, 356 insertions(+), 146 deletions(-) create mode 160000 deps/lml create mode 100644 testsuite/tests/misc/structured-tables/test.py create mode 100644 testsuite/tests/misc/structured-tables/test.yaml diff --git a/.gitmodules b/.gitmodules index ec5a589be..3054b0373 100644 --- a/.gitmodules +++ b/.gitmodules @@ -66,3 +66,6 @@ [submodule "deps/cstrings"] path = deps/cstrings url = https://github.com/mosteo/cstrings +[submodule "deps/lml"] + path = deps/lml + url = https://github.com/mosteo/lml_ada.git diff --git a/alire.gpr b/alire.gpr index a00b73f38..052067841 100644 --- a/alire.gpr +++ b/alire.gpr @@ -9,6 +9,7 @@ with "den"; with "dirty_booleans"; with "diskflags"; with "gnatcoll"; +with "lml"; with "minirest"; with "optional"; with "semantic_versioning"; diff --git a/alire.toml b/alire.toml index 282d9d152..b853e5fa8 100644 --- a/alire.toml +++ b/alire.toml @@ -18,13 +18,14 @@ executables = ["alr"] aaa = "~0.3.0" ada_toml = "~0.3" ajunitgen = "^1.0.1" -ansiada = "^1.0" +ansiada = "^1.1" c_strings = "^1.0" clic = "~0.3" den = "~0.1" dirty_booleans = "~0.1" diskflags = "~0.1" gnatcoll = "^21" +lml = "~0.1" minirest = "~0.3" optional = "~0.1" semantic_versioning = "^3.0" @@ -50,13 +51,19 @@ windows = { ALIRE_OS = "windows" } # Some dependencies require precise versions during the development cycle: [[pins]] + [pins.aaa] url = "https://github.com/mosteo/aaa" -commit = "0c3b440ac183c450345d4a67d407785678779aae" +commit = "ddfeffe2d6c8f9d19161df7b31d16d37bef4ba71" [pins.ada_toml] url = "https://github.com/mosteo/ada-toml" commit = "da4e59c382ceb0de6733d571ecbab7ea4919b33d" + +[pins.ansiada] +url = "https://github.com/mosteo/ansi-ada" +commit = "0772e48d3e1f640829d142745a36b37edcd5470b" + [pins.c_strings] url = "https://github.com/mosteo/cstrings" commit = "e4d58ad90bf32bc44304197e5906a519f5a9a7bf" @@ -64,9 +71,10 @@ commit = "e4d58ad90bf32bc44304197e5906a519f5a9a7bf" [pins.clic] url = "https://github.com/alire-project/clic" commit = "56bbdc008e16996b6f76e443fd0165a240de1b13" + [pins.den] url = "https://github.com/mosteo/den" -commit = "681ab5ca522585953f2e1d70763731df34c96012" +commit = "b12e8461bf41e2cfe199c8196b45fa4fc213a6aa" [pins.dirty_booleans] url = "https://github.com/mosteo/dirty_booleans" @@ -80,6 +88,10 @@ commit = "60729edf31816aca0036b13b2794c39a9bd0172e" url = "https://github.com/alire-project/gnatcoll-core.git" commit = "4e663b87a028252e7e074f054f8f453661397166" +[pins.lml] +url = "https://github.com/mosteo/lml_ada.git" +commit = "ae156ef82a2fedb7e28bb4dcaeb3d5c0a2e046ec" + [pins.minirest] url = "https://github.com/mosteo/minirest.git" commit = "9a9c660f9c6f27f5ef75417e7fac7061dff14d78" diff --git a/alr_env.gpr b/alr_env.gpr index 78592c5e6..c9a6a6b3e 100644 --- a/alr_env.gpr +++ b/alr_env.gpr @@ -19,6 +19,7 @@ aggregate project Alr_Env is "deps/dirty_booleans", "deps/diskflags", "deps/gnatcoll-slim", + "deps/lml", "deps/minirest", "deps/optional", "deps/semantic_versioning", diff --git a/deps/aaa b/deps/aaa index 0c3b440ac..ddfeffe2d 160000 --- a/deps/aaa +++ b/deps/aaa @@ -1 +1 @@ -Subproject commit 0c3b440ac183c450345d4a67d407785678779aae +Subproject commit ddfeffe2d6c8f9d19161df7b31d16d37bef4ba71 diff --git a/deps/ansi b/deps/ansi index dc770a5a6..0772e48d3 160000 --- a/deps/ansi +++ b/deps/ansi @@ -1 +1 @@ -Subproject commit dc770a5a6cdaad8668c32b0cd4625a7d648f8ca2 +Subproject commit 0772e48d3e1f640829d142745a36b37edcd5470b diff --git a/deps/den b/deps/den index 681ab5ca5..b12e8461b 160000 --- a/deps/den +++ b/deps/den @@ -1 +1 @@ -Subproject commit 681ab5ca522585953f2e1d70763731df34c96012 +Subproject commit b12e8461bf41e2cfe199c8196b45fa4fc213a6aa diff --git a/deps/lml b/deps/lml new file mode 160000 index 000000000..ae156ef82 --- /dev/null +++ b/deps/lml @@ -0,0 +1 @@ +Subproject commit ae156ef82a2fedb7e28bb4dcaeb3d5c0a2e046ec diff --git a/src/alire/alire-index-search.adb b/src/alire/alire-index-search.adb index 7e7fb85ff..54adf8914 100644 --- a/src/alire/alire-index-search.adb +++ b/src/alire/alire-index-search.adb @@ -24,6 +24,11 @@ package body Alire.Index.Search is Busy : Simple_Logging.Ongoing := Simple_Logging.Activity ("Searching"); begin + -- Preserve old behavior for human output + if Utils.Tables.Structured_Output then + Table.Header ("NAME").Header ("DESCRIPTION"); + end if; + for Crate of Alire.Index.All_Crates.all loop if Lookup = "" or else Contains (To_Lower_Case (+Crate.Name), Lookup) or else @@ -37,7 +42,7 @@ package body Alire.Index.Search is Busy.Step; end loop; - if Found = 0 then + if Found = 0 and then not Utils.Tables.Structured_Output then Trace.Always ("No hits"); else Table.Print (Always, Separator => " "); diff --git a/src/alire/alire-solutions-diffs.adb b/src/alire/alire-solutions-diffs.adb index dc74563da..169ae6318 100644 --- a/src/alire/alire-solutions-diffs.adb +++ b/src/alire/alire-solutions-diffs.adb @@ -551,7 +551,7 @@ package body Alire.Solutions.Diffs is end loop; if Changed then - Table.Print (Level); + Table.Print (Level, Structured => False); Warn_Toolchain_Download; Warn_Unsatisfiable_GNAT_External; diff --git a/src/alire/alire-solutions.adb b/src/alire/alire-solutions.adb index 8e5228c40..d9609eb46 100644 --- a/src/alire/alire-solutions.adb +++ b/src/alire/alire-solutions.adb @@ -923,8 +923,17 @@ package body Alire.Solutions is Table : Utils.Tables.Table; begin if This.Links.Is_Empty and then Dependency_Map'(This.Pins).Is_Empty then - Trace.Always ("There are no pins"); + if Utils.Tables.Structured_Output then + Table.Print (Always); + else + Trace.Always ("There are no pins"); + end if; else + -- To preserve old behavior with human output + if Utils.Tables.Structured_Output then + Table.Header ("Crate").Header ("Target").Header ("Origin").New_Row; + end if; + for Dep of This.Dependencies loop if Dep.Is_Linked then Table diff --git a/src/alire/alire-utils-did_you_mean.adb b/src/alire/alire-utils-did_you_mean.adb index c7d096979..75c492bec 100644 --- a/src/alire/alire-utils-did_you_mean.adb +++ b/src/alire/alire-utils-did_you_mean.adb @@ -81,10 +81,10 @@ package body Alire.Utils.Did_You_Mean with Preelaborate is for V in Enum loop Possible_Values.Append (case Transform is - when None => V'Img, + when None => V'Img, when Lower_Case => AAA.Strings.To_Lower_Case (V'Img), - when Upper_Case => AAA.Strings.To_Lower_Case (V'Img), - when Tomify => TOML_Adapters.Tomify (V'Img)); + when Upper_Case => AAA.Strings.To_Upper_Case (V'Img), + when Tomify => TOML_Adapters.Tomify (V'Img)); end loop; return Suggestion (Input, Possible_Values); diff --git a/src/alire/alire-utils-tables.adb b/src/alire/alire-utils-tables.adb index a4ea9d76a..243a68442 100644 --- a/src/alire/alire-utils-tables.adb +++ b/src/alire/alire-utils-tables.adb @@ -6,27 +6,39 @@ package body Alire.Utils.Tables is -- Header -- ------------ + overriding procedure Header (T : in out Table; Cell : String) is + Text : constant String := + (if Structured_Output + then AAA.Strings.To_Lower_Case (Cell) + else TTY.Emph (AAA.Strings.To_Upper_Case (Cell))); begin - T.Append (TTY.Emph (AAA.Strings.To_Upper_Case (Cell))); + Parent (T).Header (Text); end Header; + ------------ + -- Header -- + ------------ + + overriding function Header (T : aliased in out Table; Cell : String) - return access Table is + return AAA.Table_IO.Reference + is begin T.Header (Cell); - return T'Access; + return AAA.Table_IO.Reference'(Table => T'Access); end Header; ----------- -- Print -- ----------- - procedure Print (T : Table; - Level : Trace.Levels := Info; - Separator : String := " "; - Align : AAA.Table_IO.Alignments := (1 .. 0 => <>)) + procedure Print (T : Table; + Level : Trace.Levels := Info; + Separator : String := " "; + Align : AAA.Table_IO.Alignments := (1 .. 0 => <>); + Structured : Boolean := Structured_Output) is procedure Print (Line : String) is @@ -37,9 +49,14 @@ package body Alire.Utils.Tables is end Print; begin - T.Print (Separator => Separator, - Align => Align, - Put_Line => Print'Access); + if Structured then + T.Print (Structured_Output_Format, + Put_Line => Print'Access); + else + T.Print (Separator => Separator, + Align => Align, + Put_Line => Print'Access); + end if; end Print; end Alire.Utils.Tables; diff --git a/src/alire/alire-utils-tables.ads b/src/alire/alire-utils-tables.ads index f6f3ef93f..19dd53d53 100644 --- a/src/alire/alire-utils-tables.ads +++ b/src/alire/alire-utils-tables.ads @@ -1,19 +1,33 @@ with AAA.Table_IO; -package Alire.Utils.Tables with Preelaborate is +with LML; - type Table is new AAA.Table_IO.Table with null record; +package Alire.Utils.Tables is + subtype Formats is LML.Formats; + + Structured_Output : Boolean := False; + + Structured_Output_Format : Formats; + + subtype Parent is AAA.Table_IO.Table; + + type Table is new Parent with null record; + + overriding procedure Header (T : in out Table; Cell : String); + overriding function Header (T : aliased in out Table; Cell : String) - return access Table; + return AAA.Table_IO.Reference; - procedure Print (T : Table; - Level : Trace.Levels := Info; - Separator : String := " "; - Align : AAA.Table_IO.Alignments := (1 .. 0 => <>)); - -- Hook so tables use the default output facilities of Alire + procedure Print (T : Table; + Level : Trace.Levels := Info; + Separator : String := " "; + Align : AAA.Table_IO.Alignments := (1 .. 0 => <>); + Structured : Boolean := Structured_Output); + -- Hook so tables use the default output facilities of Alire. When + -- Structured_Output is enabled, formatting information is ignored. end Alire.Utils.Tables; diff --git a/src/alr/alr-commands-index.adb b/src/alr/alr-commands-index.adb index 9f261746e..cb212a177 100644 --- a/src/alr/alr-commands-index.adb +++ b/src/alr/alr-commands-index.adb @@ -1,10 +1,8 @@ -with AAA.Table_IO; - with Alire.Settings.Edit; with Alire.Index; with Alire.Index_On_Disk.Loading; with Alire.Index_On_Disk.Updates; -with Alire.Utils; +with Alire.Utils.Tables; package body Alr.Commands.Index is @@ -155,7 +153,7 @@ package body Alr.Commands.Index is Index_Load.Find_All (Alire.Settings.Edit.Indexes_Directory, Result); - Table : AAA.Table_IO.Table; + Table : Alire.Utils.Tables.Table; Count : Natural := 0; begin if not Result.Success then @@ -163,10 +161,10 @@ package body Alr.Commands.Index is end if; Table - .Append (TTY.Emph ("#")) - .Append (TTY.Emph ("NAME")) - .Append (TTY.Emph ("URL")) - .Append (TTY.Emph ("PATH")); + .Header ("#") + .Header ("NAME") + .Header ("URL") + .Header ("PATH"); if Alire.Log_Level = Alire.Trace.Debug then Table.Append (TTY.Emph ("PRIORITY")); @@ -187,7 +185,7 @@ package body Alr.Commands.Index is end loop; if Count > 0 then - Table.Print; + Table.Print (Always); else Trace.Info ("No index configured."); end if; diff --git a/src/alr/alr-commands-search.adb b/src/alr/alr-commands-search.adb index f5ff17169..61f6feee2 100644 --- a/src/alr/alr-commands-search.adb +++ b/src/alr/alr-commands-search.adb @@ -194,12 +194,12 @@ package body Alr.Commands.Search is -- End of option verification, start of search. First load the index, -- required to look at its entries. - Tab.Append (TTY.Bold ("NAME")); - Tab.Append (TTY.Bold ("STATUS")); - Tab.Append (TTY.Bold ("VERSION")); - Tab.Append (TTY.Bold ("DESCRIPTION")); - Tab.Append (TTY.Bold ("NOTES")); - Tab.Append (TTY.Bold ("MATCHES")); + Tab.Header ("NAME"); + Tab.Header ("STATUS"); + Tab.Header ("VERSION"); + Tab.Header ("DESCRIPTION"); + Tab.Header ("NOTES"); + Tab.Header ("MATCHES"); declare Busy : Simple_Logging.Ongoing := diff --git a/src/alr/alr-commands-toolchain.adb b/src/alr/alr-commands-toolchain.adb index b5f78188b..f84955787 100644 --- a/src/alr/alr-commands-toolchain.adb +++ b/src/alr/alr-commands-toolchain.adb @@ -1,6 +1,4 @@ -with AAA.Table_IO; - with Alire.Settings.Edit; with Alire.Containers; with Alire.Dependencies; @@ -10,7 +8,7 @@ with Alire.Origins.Deployers; with Alire.Releases.Containers; with Alire.Solver; with Alire.Toolchains; -with Alire.Utils; use Alire.Utils; +with Alire.Utils.Tables; with Alire.Utils.TTY; with Alire.Warnings; @@ -18,6 +16,8 @@ with Semantic_Versioning.Extended; package body Alr.Commands.Toolchain is + use Alire.Utils; + package Name_Sets renames Alire.Containers.Crate_Name_Sets; -------------------- @@ -268,7 +268,7 @@ package body Alr.Commands.Toolchain is pragma Unreferenced (Cmd); use Alire; use type Dependencies.Dependency; - Table : AAA.Table_IO.Table; + Table : Tables.Table; begin Alire.Toolchains.Detect_Externals; -- Even if we have selected a non-external toolchain, in this case we @@ -281,10 +281,10 @@ package body Alr.Commands.Toolchain is end if; Table - .Append (TTY.Emph ("CRATE")) - .Append (TTY.Emph ("VERSION")) - .Append (TTY.Emph ("STATUS")) - .Append (TTY.Emph ("NOTES")) + .Header ("CRATE") + .Header ("VERSION") + .Header ("STATUS") + .Header ("NOTES") .New_Row; for Dep of Alire.Toolchains.Available loop @@ -311,7 +311,7 @@ package body Alr.Commands.Toolchain is end if; end loop; - Table.Print; + Table.Print (Always); end List; ------------- diff --git a/src/alr/alr-commands-version.adb b/src/alr/alr-commands-version.adb index b2d2fa302..a89057d9e 100644 --- a/src/alr/alr-commands-version.adb +++ b/src/alr/alr-commands-version.adb @@ -38,8 +38,9 @@ package body Alr.Commands.Version is Args : AAA.Strings.Vector) is use Alire; + use Alire.Utils; use all type Alire.Roots.Optional.States; - Table : Alire.Utils.Tables.Table; + Table : Tables.Table; Index_Outcome : Alire.Outcome; Indexes : constant Alire.Index_On_Disk.Loading.Set := Alire.Index_On_Disk.Loading.Find_All @@ -57,66 +58,82 @@ package body Alr.Commands.Version is / Paths.Cache_Folder_Inside_Working_Folder / Paths.Deps_Folder_Inside_Cache_Folder) else Builds.Path); + + --------- + -- Add -- + --------- + + procedure Add (Key : String; Val : String := "") is + use AAA.Strings; + begin + if (Key = "" or else Val = "") and then Tables.Structured_Output then + return; -- Skip cosmetic rows in structured output + end if; + + if Tables.Structured_Output and then Contains (Key, ":") then + Add (Replace (Key, ":", ""), Val); + return; + end if; + + Table.Append (Key).Append (Val).New_Row; + end Add; + begin if Args.Count /= 0 then Reportaise_Wrong_Arguments (Cmd.Name & " doesn't take arguments"); end if; - Table.Append ("APPLICATION").Append ("").New_Row; - Table.Append ("alr version:") - .Append (Alire.Version.Current.Image).New_Row; - Table.Append ("libalire version:") - .Append (Alire.Version.Current.Image).New_Row; - Table.Append ("compilation date:") - .Append (GNAT.Source_Info.Compilation_ISO_Date & " " - & GNAT.Source_Info.Compilation_Time).New_Row; - Table.Append ("compiled with version:") - .Append (GNAT_Version.Version).New_Row; - - Table.Append ("").New_Row; - Table.Append ("CONFIGURATION").New_Row; - Table.Append ("settings folder:") - .Append (Alire.Settings.Edit.Path).New_Row; - Table.Append ("cache folder:") - .Append (Alire.Cache.Path).New_Row; - Table.Append ("vault folder:").Append (Paths.Vault.Path).New_Row; - Table.Append ("build folder:").Append (Build_Path).New_Row; - Table.Append ("temp folder:") - .Append (Alire.Platforms.Folders.Temp).New_Row; - Table.Append ("force flag:").Append (Alire.Force'Image).New_Row; - Table.Append ("non-interactive flag:") - .Append (CLIC.User_Input.Not_Interactive'Image).New_Row; - Table.Append ("community index branch:") - .Append (Alire.Index.Community_Branch).New_Row; - Table.Append ("compatible index versions:") - .Append (Alire.Index.Valid_Versions.Image).New_Row; - Table.Append ("indexes folder:") - .Append (Alire.Settings.Edit.Indexes_Directory).New_Row; - Table.Append ("indexes metadata:") - .Append (if Index_Outcome.Success - then "OK" - else "ERROR: " & Index_Outcome.Message).New_Row; + -- Enrich output when using a structured format only + if Alire.Utils.Tables.Structured_Output then + Table.Header ("key").Header ("Value").New_Row; + end if; + + Add ("APPLICATION", ""); + Add ("alr version:", Alire.Version.Current.Image); + Add ("libalire version:", Alire.Version.Current.Image); + Add ("compilation date:", + GNAT.Source_Info.Compilation_ISO_Date & " " + & GNAT.Source_Info.Compilation_Time); + Add ("compiled with version:", GNAT_Version.Version); + + Add (""); + Add ("CONFIGURATION"); + Add ("settings folder:", Alire.Settings.Edit.Path); + Add ("cache folder:", Alire.Cache.Path); + Add ("vault folder:", Paths.Vault.Path); + Add ("build folder:", Build_Path); + Add ("temp folder:", Alire.Platforms.Folders.Temp); + Add ("force flag:", Alire.Force'Image); + Add ("non-interactive flag:", + CLIC.User_Input.Not_Interactive'Image); + Add ("community index branch:", Alire.Index.Community_Branch); + Add ("compatible index versions:", + Alire.Index.Valid_Versions.Image); + Add ("indexes folder:", + Alire.Settings.Edit.Indexes_Directory); + Add ("indexes metadata:", + (if Index_Outcome.Success + then "OK" + else "ERROR: " & Index_Outcome.Message)); for Index of Indexes loop - Table.Append ("index #" - & AAA.Strings.Trim (Index.Priority'Image) & ":") - .Append ("(" & Index.Name & ") " & Index.Origin).New_Row; + Add ("index #" + & AAA.Strings.Trim (Index.Priority'Image) & ":", + "(" & Index.Name & ") " & Index.Origin); end loop; - Table.Append ("toolchain folder:") - .Append (Alire.Toolchains.Path).New_Row; - Table.Append ("toolchain assistant:") - .Append (if Alire.Toolchains.Assistant_Enabled - then "enabled" - else "disabled").New_Row; + Add ("toolchain folder:", Alire.Toolchains.Path); + Add ("toolchain assistant:", + (if Alire.Toolchains.Assistant_Enabled + then "enabled" + else "disabled")); declare I : Positive := 1; begin for Tool of Alire.Toolchains.Tools loop - Table - .Append ("tool #" & AAA.Strings.Trim (I'Image) - & " " & Tool.As_String & ":") - .Append (if Alire.Toolchains.Tool_Is_Configured (Tool) - then Alire.Toolchains.Tool_Milestone (Tool).Image - else "not configured").New_Row; + Add ("tool #" & AAA.Strings.Trim (I'Image) + & " " & Tool.As_String & ":", + (if Alire.Toolchains.Tool_Is_Configured (Tool) + then Alire.Toolchains.Tool_Milestone (Tool).Image + else "not configured")); I := I + 1; end loop; end; @@ -125,47 +142,40 @@ package body Alr.Commands.Version is System_Manager : constant String := Origins.Deployers.System.Executable_Path; begin - Table - .Append ("system package manager:") - .Append (if System_Manager /= "" - then System_Manager - else "not found: " - & (if Origins.Deployers.System.Executable_Name /= "" - then "`" & Origins.Deployers.System.Executable_Name & "`" - else "unknown package manager")) - .New_Row; - Table - .Append ("distro detection disabled:") - .Append (Platforms.Current.Disable_Distribution_Detection'Image) - .New_Row; + Add ("system package manager:", + (if System_Manager /= "" + then System_Manager + else "not found: " + & (if Origins.Deployers.System.Executable_Name /= "" + then "`" & Origins.Deployers.System.Executable_Name & "`" + else "unknown package manager"))); + Add ("distro detection disabled:", + Platforms.Current.Disable_Distribution_Detection'Image); end; - Table.Append ("").New_Row; - Table.Append ("WORKSPACE").New_Row; - - Table.Append ("root status:") - .Append (Root.Status'Image).New_Row; - Table.Append ("root release:") - .Append (case Root.Status is - when Valid => Root.Value.Release.Milestone.Image, - when others => "N/A").New_Row; - Table.Append ("root load error:") - .Append (case Root.Status is - when Broken => Cmd.Optional_Root.Message, - when Valid => "none", - when Outside => "N/A").New_Row; - Table.Append ("root folder:") - .Append (case Root.Status is - when Outside => "N/A", - when Broken => "N/A", - when Valid => Root.Value.Path).New_Row; - Table.Append ("current folder:").Append (Alire.Directories.Current) - .New_Row; - - Table.Append ("").New_Row; - Table.Append ("SYSTEM").New_Row; + Add (""); + Add ("WORKSPACE"); + Add ("root status:", Root.Status'Image); + Add ("root release:", + (case Root.Status is + when Valid => Root.Value.Release.Milestone.Image, + when others => "N/A")); + Add ("root load error:", + (case Root.Status is + when Broken => Cmd.Optional_Root.Message, + when Valid => "none", + when Outside => "N/A")); + Add ("root folder:", + (case Root.Status is + when Outside => "N/A", + when Broken => "N/A", + when Valid => Root.Value.Path)); + Add ("current folder:", Alire.Directories.Current); + + Add (""); + Add ("SYSTEM"); for Prop of Platform.Properties loop - Table.Append (Prop.Key & ":").Append (Prop.Image).New_Row; + Add (Prop.Key & ":", Prop.Image); end loop; Table.Print (Level => Always); diff --git a/src/alr/alr-commands.adb b/src/alr/alr-commands.adb index 214546eed..acaf016a1 100644 --- a/src/alr/alr-commands.adb +++ b/src/alr/alr-commands.adb @@ -20,6 +20,8 @@ with Alire.Platforms.Current; with Alire.Root; with Alire.Solutions; with Alire.Toolchains; +with Alire.Utils.Did_You_Mean; +with Alire.Utils.Tables; with Alr.Commands.Action; with Alr.Commands.Build; @@ -83,6 +85,9 @@ package body Alr.Commands is No_TTY : aliased Boolean := False; -- Used to disable control characters in output + Structured_Format : aliased GNAT.OS_Lib.String_Access + := new String'("unset"); + Version_Only : aliased Boolean := False; -- Just display the current version and exit @@ -173,6 +178,13 @@ package body Alr.Commands is "-n", "--non-interactive", "Assume default answers for all user prompts"); + Define_Switch (Config, + Structured_Format'Access, + Long_Switch => "--format?", + Argument => "FORMAT", + Help => + "Use structured output for tables (JSON, TOML)"); + Define_Switch (Config, No_Color'Access, Long_Switch => "--no-color", @@ -468,6 +480,46 @@ package body Alr.Commands is Trace.Debug ("End command line."); end Log_Command_Line; + --------------------------- + -- Set_Structured_Output -- + --------------------------- + + procedure Set_Structured_Output is + use Alire.Utils; + use all type Tables.Formats; + + Format_Str : constant String + := AAA.Strings.Replace (Structured_Format.all, "=", ""); + + function Is_Valid is + new AAA.Enum_Tools.Is_Valid (Tables.Formats); + + function Suggest is + new Alire.Utils.Did_You_Mean.Enum_Suggestion + (Tables.Formats, + Alire.Utils.Did_You_Mean.Upper_Case); + + begin + if Structured_Format.all /= "unset" then + Alire.Utils.Tables.Structured_Output := True; + else + return; + end if; + + if Format_Str /= "" and then not Is_Valid (Format_Str) then + Reportaise_Wrong_Arguments + ("Unknown argument in --format" & Structured_Format.all + & "." & Suggest (Format_Str)); + end if; + + if Format_Str /= "" then + Alire.Utils.Tables.Structured_Output_Format + := Alire.Utils.Tables.Formats'Value (Format_Str); + else + Alire.Utils.Tables.Structured_Output_Format := JSON; + end if; + end Set_Structured_Output; + use all type Alire.Platforms.Operating_Systems; begin @@ -529,6 +581,10 @@ package body Alr.Commands is Ada.Directories.Set_Directory (Command_Line_Chdir_Target_Path.all); end if; + Set_Structured_Output; + + -- End of global switches + Create_Alire_Folders; begin @@ -586,6 +642,14 @@ package body Alr.Commands is OS_Lib.Bailout (1); end if; end; + exception + when Wrong_Command_Arguments => + Trace.Detail ("alr global switches are invalid"); + if Alire.Log_Level = Debug then + raise; + else + OS_Lib.Bailout (1); + end if; end Execute; ------------------------ diff --git a/testsuite/requirements.txt b/testsuite/requirements.txt index dcaf78ee3..4293dd909 100644 --- a/testsuite/requirements.txt +++ b/testsuite/requirements.txt @@ -1,3 +1,4 @@ docker e3-testsuite pexpect +toml diff --git a/testsuite/tests/misc/structured-tables/test.py b/testsuite/tests/misc/structured-tables/test.py new file mode 100644 index 000000000..e4d7da955 --- /dev/null +++ b/testsuite/tests/misc/structured-tables/test.py @@ -0,0 +1,72 @@ +""" +Verify structured output of tables +""" + +import json + +import toml +from drivers.alr import run_alr, init_local_crate +from drivers.asserts import assert_eq, assert_match + +# Check a few commands that output tables + +# Listing of crates +assert_eq("""\ +[ + { + "name": "gnat_external", + "description": "GNAT is a compiler for the Ada programming language" + }, + { + "name": "gprbuild", + "description": "Fake gprbuild external" + } +] +""", + run_alr("--format=JSON", "search", "--crates").out) + +assert_eq("""\ +[[data]] +description = "GNAT is a compiler for the Ada programming language" +name = "gnat_external" +[[data]] +description = "Fake gprbuild external" +name = "gprbuild" + +""", + run_alr("--format=TOML", "search", "--crates").out) + +# Empty pin list + +init_local_crate() +assert_eq("""\ +[ +] +""", + run_alr("--format=JSON", "pin").out) + +assert_eq("""\ + +""", + run_alr("--format=TOML", "pin").out) + +# Check that objects can be reconstructed and queried from the output + +for fmt in ["JSON", "TOML"]: + # List of releases + out = run_alr(f"--format={fmt}", "-q", "search", "--list", "--external").out + # Load and adjust according to format + if fmt == "TOML": + releases = toml.loads(out)["data"] + # In the TOML case, top-level is always a table with a single key "data" + else: + releases = json.loads(out) + + # Check that it is a list of the expected length + assert_eq(list, type(releases)) + assert_eq(2, len(releases)) + # Check some data + assert_eq("gnat_external", releases[0]["name"]) + + +print("SUCCESS") diff --git a/testsuite/tests/misc/structured-tables/test.yaml b/testsuite/tests/misc/structured-tables/test.yaml new file mode 100644 index 000000000..702010525 --- /dev/null +++ b/testsuite/tests/misc/structured-tables/test.yaml @@ -0,0 +1,4 @@ +driver: python-script +build_mode: both +indexes: + compiler_only_index: {} diff --git a/testsuite/tests/pin/branch/test.py b/testsuite/tests/pin/branch/test.py index 53d2806c9..ad580c1f6 100644 --- a/testsuite/tests/pin/branch/test.py +++ b/testsuite/tests/pin/branch/test.py @@ -3,14 +3,12 @@ """ from drivers.alr import run_alr, alr_pin, alr_unpin, init_local_crate -from drivers.asserts import assert_eq, assert_match -from drivers.helpers import git_branch, git_head, init_git_repo +from drivers.asserts import assert_match +from drivers.helpers import git_branch, init_git_repo from e3.os.fs import touch -from re import escape import re import os -import shutil import subprocess # "remote" is going to be the remote crate From 9742e81b641a53754c9119d96b43521765266b9c Mon Sep 17 00:00:00 2001 From: Seb M'Caw Date: Tue, 24 Sep 2024 19:26:15 +0100 Subject: [PATCH 52/53] test: building on an air-gapped system (#1760) * Add test for offline build * Move constraints to YAML file * Remove redundant build check * Change crates from git repos to .tgz archives --- .../air-gapping/my_index/crates/hello.tgz | Bin 0 -> 334 bytes .../air-gapping/my_index/crates/libhello.tgz | Bin 0 -> 428 bytes .../my_index/index/he/hello/hello-1.0.1.toml | 11 +++ .../air-gapping/my_index/index/index.toml | 1 + .../index/li/libhello/libhello-1.0.0.toml | 8 ++ testsuite/tests/workflows/air-gapping/test.py | 83 ++++++++++++++++++ .../tests/workflows/air-gapping/test.yaml | 10 +++ 7 files changed, 113 insertions(+) create mode 100644 testsuite/tests/workflows/air-gapping/my_index/crates/hello.tgz create mode 100644 testsuite/tests/workflows/air-gapping/my_index/crates/libhello.tgz create mode 100644 testsuite/tests/workflows/air-gapping/my_index/index/he/hello/hello-1.0.1.toml create mode 100644 testsuite/tests/workflows/air-gapping/my_index/index/index.toml create mode 100644 testsuite/tests/workflows/air-gapping/my_index/index/li/libhello/libhello-1.0.0.toml create mode 100644 testsuite/tests/workflows/air-gapping/test.py create mode 100644 testsuite/tests/workflows/air-gapping/test.yaml diff --git a/testsuite/tests/workflows/air-gapping/my_index/crates/hello.tgz b/testsuite/tests/workflows/air-gapping/my_index/crates/hello.tgz new file mode 100644 index 0000000000000000000000000000000000000000..a39010d27cf1902c1153f4a9ddc7cd3d7570ed4c GIT binary patch literal 334 zcmV-U0kQrciwFP!000001MStlZi6rs1z=`Bg~ik^DK=mR)(#z7b?DGB;>Qg{6e0u4 z>(@4fMp4@+RY+9zd<%$sEkK8B3RzXnoii&>i=xmRSri82QP}GvhXo=EI2XRqeasWO zjfV3+)}U{@L{aLzXUX=bzGL-2s<8RDDx2{eI4S>->HJyX3*>(ZpZVLfMUtmyzlRA} zh~V4&850Nj``n8h>Ye>bzVOfU-)*`>wIXA$+h1GPBqN`km_oqX1T|)`RAM+%s&r8%>PB$`FE?`VCzr79d?1W z%(}fjKu`Z$qv%W1t4uEMOV!f8mGs8dzYg~{-qoK|v(D6Q*=^FbI~AXjvL07WKO3`l ge+R)90ssI20000000000008hiA3Kd7F90Y20A9796#xJL literal 0 HcmV?d00001 diff --git a/testsuite/tests/workflows/air-gapping/my_index/crates/libhello.tgz b/testsuite/tests/workflows/air-gapping/my_index/crates/libhello.tgz new file mode 100644 index 0000000000000000000000000000000000000000..c5882e61a64bcdbdd93a0e082d77f0fedb4cad96 GIT binary patch literal 428 zcmV;d0aN}TiwFP!000001MQVhPlGTR#yRsTUOCM~=YUcUoJ>4uvN#h>Jdy%+W3sUn z$lSNLWk4JYE{kl&`2H@y`=@R4^m$3*V8)Xqz0pp`NaDJVdYZ07swXYu6`Dz?ODv1J z@_l5QwoNo}PI#tJA*EKc%+R-;x&q^*;wY`qv*r z7G*u|p#-LsbnEZZmi~lTrUs)P&(O&~*MG&rcQ)lvOAm3T>j1D4X~?5OaJW}t|1}j! zc#X|&m! z3)gM7!LL&S>R`LFbFQexp`^(8?rCH!;L~_n<)4b&AID1$1EcJ}g0))U!niC;)pWvn zbqtOF8}Cn7qUSvh`QIW|%l|ed6#ah=q#x%z%wgQ}`CBUBIW0uU{kvFXr7;JF{7F Date: Tue, 24 Sep 2024 19:42:10 +0100 Subject: [PATCH 53/53] feat: support for private indexes with `alr publish --for-private-index` (#1745) * Add support for private indexes to "alr publish" * Fix tests * Support "git@" remotes * Fix test * Bugfix * Update 'config' to 'settings' in 'alr init' * Update 'config' to 'settings' elsewhere * Rewrite documentation * Clarify upload instructions --- doc/catalog-format-spec.md | 10 +- doc/publishing.md | 46 ++- scripts/ci-github.sh | 2 +- src/alire/alire-origins.adb | 5 +- src/alire/alire-properties-from_toml.ads | 2 - src/alire/alire-properties-labeled.adb | 8 +- src/alire/alire-publish-submit.adb | 2 +- src/alire/alire-publish.adb | 150 +++++-- src/alire/alire-publish.ads | 14 +- src/alire/alire-releases.ads | 6 + src/alire/alire-toml_index.adb | 9 +- src/alire/alire-toml_index.ads | 5 + src/alire/alire-uri.adb | 30 ++ src/alire/alire-uri.ads | 8 + .../alire-utils-user_input-query_config.adb | 16 +- .../alire-utils-user_input-query_config.ads | 4 +- .../alire-platforms-current__windows.adb | 2 +- src/alr/alr-commands-init.adb | 63 ++- src/alr/alr-commands-publish.adb | 16 +- src/alr/alr-commands-publish.ads | 9 +- testsuite/drivers/alr.py | 72 +++- testsuite/drivers/asserts.py | 8 + testsuite/drivers/helpers.py | 78 ++++ .../he/hello_world/hello_world-0.1.0.toml | 2 +- testsuite/tests/index/maint-bad-login/test.py | 4 +- testsuite/tests/init/github-login/test.py | 103 +++++ testsuite/tests/init/github-login/test.yaml | 1 + testsuite/tests/monorepo/basic/test.py | 2 +- .../tests/monorepo/doubly-nested/test.py | 4 +- .../tests/monorepo/manifest-in-place/test.py | 2 +- testsuite/tests/monorepo/multi-commit/test.py | 4 +- .../tests/monorepo/subdir-in-tar/test.py | 2 +- .../tests/pin/branch-remote-protocols/test.py | 78 ++-- .../pin/branch-remote-protocols/test.yaml | 3 - .../publish/check-pre-release-version/test.py | 2 +- .../tests/publish/local-repo-branched/test.py | 11 +- .../tests/publish/local-repo-nonstd/test.py | 2 +- testsuite/tests/publish/local-repo/test.py | 2 +- testsuite/tests/publish/pin-removal/test.py | 2 +- .../my_index/crates/crate/alire.toml | 9 + .../my_index/crates/crate/crate.gpr | 22 + .../my_index/crates/crate/src/crate.adb | 4 + .../my_index/index/cr/crate/crate-1.0.0.toml | 13 + .../private-indexes/my_index/index/index.toml | 1 + .../tests/publish/private-indexes/test.py | 391 ++++++++++++++++++ .../tests/publish/private-indexes/test.yaml | 4 + .../publish/remote-origin-nonstd/test.py | 4 +- testsuite/tests/publish/remote-origin/test.py | 4 +- .../tests/publish/ssh-remote-origin/test.py | 34 ++ .../tests/publish/ssh-remote-origin/test.yaml | 1 + .../publish/tarball-plaindir-nonstd/test.py | 2 +- .../tests/publish/tarball-plaindir/test.py | 2 +- .../tests/publish/tarball-repo-nonstd/test.py | 14 +- testsuite/tests/publish/tarball-repo/test.py | 14 +- 54 files changed, 1103 insertions(+), 205 deletions(-) create mode 100644 testsuite/tests/init/github-login/test.py create mode 100644 testsuite/tests/init/github-login/test.yaml create mode 100644 testsuite/tests/publish/private-indexes/my_index/crates/crate/alire.toml create mode 100644 testsuite/tests/publish/private-indexes/my_index/crates/crate/crate.gpr create mode 100644 testsuite/tests/publish/private-indexes/my_index/crates/crate/src/crate.adb create mode 100644 testsuite/tests/publish/private-indexes/my_index/index/cr/crate/crate-1.0.0.toml create mode 100644 testsuite/tests/publish/private-indexes/my_index/index/index.toml create mode 100644 testsuite/tests/publish/private-indexes/test.py create mode 100644 testsuite/tests/publish/private-indexes/test.yaml create mode 100644 testsuite/tests/publish/ssh-remote-origin/test.py create mode 100644 testsuite/tests/publish/ssh-remote-origin/test.yaml diff --git a/doc/catalog-format-spec.md b/doc/catalog-format-spec.md index 5dabab539..2df346ffa 100644 --- a/doc/catalog-format-spec.md +++ b/doc/catalog-format-spec.md @@ -182,14 +182,18 @@ static, i.e. they cannot depend on the context. "Bob For Instance "] ``` - - `maintainers-logins`: mandatory (for indexing) array of strings. Flat - list of github login usernames used by the maintainers of the crate. This - information is used to authorize crate modifications. For instance: + - `maintainers-logins`: optional array of non-empty strings. + For crates submitted to the community index, this is a mandatory flat list of + the GitHub login usernames authorized to modify the crate. + For instance: ```toml maintainers-logins = ["alicehacks", "bobcoder"] ``` + Private indexes may use whichever logins are appropriate for their + hosting arrangement, or none at all. + - `licenses`: mandatory (for indexing) string. A valid [SPDX expression](https://spdx.org/licenses/). Custom license identifiers are accepted with the format: `custom-[0-9a-zA-Z.-]+` diff --git a/doc/publishing.md b/doc/publishing.md index 8ec90c3c9..23ab40ca7 100644 --- a/doc/publishing.md +++ b/doc/publishing.md @@ -313,15 +313,37 @@ This will be shown as: ## Publishing to a local/private index -Having a local index may be useful sometimes, be it for local testing, or for -private crates not intended for publication. - -There is no practical difference between the community index that is cloned -locally and a private local index stored on disk. Hence, after obtaining the -manifest file with `alr publish`, it is a matter of placing it at the expected -location within the index: `/path/to/index/cr/crate_name/crate_name-x.x.x.toml` - -If the crate being published locally contains `"provides"` definitions, it is -necessary to call `alr index --update-all` once to ensure it is properly used -by the dependency solver. This is only necessary for the first release in a -crate that uses the `"provides"` feature. +Having a local or private index may be useful sometimes, be it for local +testing, or for private crates not intended for publication. + +There is no practical difference between the community index and a private index +stored locally on disk or on your own infrastructure. An index must be located +in a first level subdirectory of an accessible git repository or local +filesystem location (or optionally at the top level in the case of a local +filesystem index). This subdirectory should contain only an `index.toml` +file and one or more `cr/crate_name` subdirectories within which the crate +manifests themselves are located. The `index.toml` file contains one line with +the form `version = "x.x.x"`, specifying the index format used. The range of +versions Alire is compatible with can be found by running `alr version`, and +breaking changes are listed in +[BREAKING.md](https://github.com/alire-project/alire/blob/master/BREAKING.md). + +To start using such an index, run + +`alr index --add= --name=`, + +where `` is a human-friendly label that `alr` will use to refer to it. + +To publish a crate to a private index, run + +`alr publish --for-private-index [ ]` + +as described in the sections above, then place the manifest file it generates at +the indicated path (relative to the location of `index.toml`). + +Additions to indexes stored locally on the disk will take effect immediately, +unless the crate being published contains `"provides"` definitions, in which +case an index update will be required (either with `alr index --update-all`, or +through a scheduled auto-update) to ensure it is properly used by the dependency +solver. An index update will always be required when publishing to a git +repository index. diff --git a/scripts/ci-github.sh b/scripts/ci-github.sh index 53d36e9dd..0b28b2dcb 100755 --- a/scripts/ci-github.sh +++ b/scripts/ci-github.sh @@ -39,7 +39,7 @@ fi # Disable distro detection if supported if [ "${ALIRE_DISABLE_DISTRO:-}" == "true" ]; then - alr config --global --set distribution.disable_detection true + alr settings --global --set distribution.disable_detection true fi # For the record diff --git a/src/alire/alire-origins.adb b/src/alire/alire-origins.adb index 658791d6e..15f130356 100644 --- a/src/alire/alire-origins.adb +++ b/src/alire/alire-origins.adb @@ -769,7 +769,10 @@ package body Alire.Origins is when VCS_Kinds => Table.Set (Keys.URL, - +(Prefixes (This.Kind).all + +((if This.Kind in Git + and then AAA.Strings.Has_Prefix (This.URL, "git@") + then "" + else Prefixes (This.Kind).all) & (if URI.Scheme (This.URL) in URI.None -- not needed for remote repos, but for testing -- ones used locally: diff --git a/src/alire/alire-properties-from_toml.ads b/src/alire/alire-properties-from_toml.ads index 00bed23b6..517335b6c 100644 --- a/src/alire/alire-properties-from_toml.ads +++ b/src/alire/alire-properties-from_toml.ads @@ -53,14 +53,12 @@ package Alire.Properties.From_TOML is Crates.External_Shared_Section => (Description | Maintainers | - Maintainers_Logins | Name => True, others => False), Crates.Index_Release => (Description | Maintainers | - Maintainers_Logins | Name | Version => True, others => False), diff --git a/src/alire/alire-properties-labeled.adb b/src/alire/alire-properties-labeled.adb index 4de28a105..d61d619c2 100644 --- a/src/alire/alire-properties-labeled.adb +++ b/src/alire/alire-properties-labeled.adb @@ -131,10 +131,12 @@ package body Alire.Properties.Labeled is end if; when Maintainers_Logins => - if not Utils.Is_Valid_GitHub_Username (L.Value) then + -- The crate may be published through a private index, so we don't + -- know the requirements for a valid username; reject only an + -- empty string. + if L.Value'Length = 0 then From.Checked_Error - ("maintainers-logins must be a valid GitHub login, but got: " - & L.Value); + ("maintainers-logins values must be non-empty"); end if; when Tag => diff --git a/src/alire/alire-publish-submit.adb b/src/alire/alire-publish-submit.adb index 122a5ec0e..dcfc96885 100644 --- a/src/alire/alire-publish-submit.adb +++ b/src/alire/alire-publish-submit.adb @@ -298,7 +298,7 @@ package body Alire.Publish.Submit is Target : constant Absolute_Path := Local_Repo_Path / VFS.To_Native - (TOML_Index.Manifest_Path (Context.Root.Value.Name)) + (TOML_Index.Community_Manifest_Path (Context.Root.Value.Name)) / Filename; begin Directories.Create_Tree (Directories.Parent (Target)); diff --git a/src/alire/alire-publish.adb b/src/alire/alire-publish.adb index 907b751fa..b62352360 100644 --- a/src/alire/alire-publish.adb +++ b/src/alire/alire-publish.adb @@ -173,13 +173,15 @@ package body Alire.Publish is -- New_Options -- ----------------- - function New_Options (Skip_Build : Boolean := False; - Skip_Submit : Boolean := False; - Manifest : String := Roots.Crate_File_Name) + function New_Options (Skip_Build : Boolean := False; + Skip_Submit : Boolean := False; + For_Private_Index : Boolean := False; + Manifest : String := Roots.Crate_File_Name) return All_Options - is (Manifest_File => +Manifest, - Skip_Build => Skip_Build, - Skip_Submit => Skip_Submit); + is (Manifest_File => +Manifest, + Skip_Build => Skip_Build, + Skip_Submit => Skip_Submit, + For_Private_Index => For_Private_Index); --------------- -- Git_Error -- @@ -228,8 +230,9 @@ package body Alire.Publish is ------------------- -- Check_Release -- ------------------- - -- Checks the presence of recommended/mandatory fileds in the release - procedure Check_Release (Release : Releases.Release) is + -- Checks the presence of recommended/mandatory fields in the release + procedure Check_Release (Release : Releases.Release; Context : in out Data) + is use CLIC.User_Input; Recommend : AAA.Strings.Vector; -- Optional @@ -294,6 +297,20 @@ package body Alire.Publish is end if; end loop; + -- The maintainers-logins field is mandatory only if publishing to the + -- community index + + if not Context.Options.For_Private_Index then + declare + Key_String : constant String := Tomify + (Properties.From_TOML.Maintainers_Logins'Image); + begin + if not Release.Has_Property (Key_String) then + Missing.Append (Key_String); + end if; + end; + end if; + Caret_Pre_1 := Release.Check_Caret_Warning; if not Missing.Is_Empty then @@ -312,6 +329,26 @@ package body Alire.Publish is & " not be pre-release versions."); end if; + -- If we are submitting to the community index, the maintainers-logins + -- values must be valid GitHub usernames + if not Context.Options.For_Private_Index then + for Property of Release.Maint_Logins loop + declare + Maint_Login : constant String := Property.To_TOML.As_String; + begin + if not Utils.Is_Valid_GitHub_Username (Maint_Login) then + Raise_Checked_Error ("The maintainer login '" + & Maint_Login + & "' is not a valid GitHub username"); + end if; + + -- We could also check GitHub.User_Exists at this point, but it + -- isn't worth the GitHub API call (running the testsuite a + -- couple of times would trigger GitHub's rate limits) + end; + end loop; + end if; + -- Final confirmation. We default to Yes if no recommended missing or -- Force. @@ -392,7 +429,8 @@ package body Alire.Publish is (Starting_Manifest (Context), Alire.Manifest.Local, Strict => True, - Root_Path => Adirs.Full_Name (+Context.Path))); + Root_Path => Adirs.Full_Name (+Context.Path)), + Context); -- Will have raised if the release is not loadable or incomplete else declare @@ -408,7 +446,7 @@ package body Alire.Publish is ("Invalid metadata found at " & Root.Value.Path, Root.Brokenness)); when Valid => - Check_Release (Root.Value.Release); + Check_Release (Root.Value.Release, Context); end case; end; end if; @@ -557,8 +595,8 @@ package body Alire.Publish is ("Your index manifest file has been generated at " & TTY.URL (Index_Manifest)); - -- Ask to submit, or show the upload URL if submission skipped, or a - -- more generic message otherwise (when lacking a github login). + -- Ask to submit, or provide submission instructions if submission + -- skipped. if not Context.Options.Skip_Submit then -- Safeguard to avoid tests creating a live pull request, unless @@ -580,23 +618,40 @@ package body Alire.Publish is then raise Early_Stop; end if; + elsif Context.Options.For_Private_Index then + -- We are publishing to a private index, the location of which is + -- unknown, so we can only give generic instructions on where to + -- place the file. + Put_Info + ("Please upload this file to the index in the " + & TTY.URL (String (TOML_Index.Manifest_Path (Name)) & "/") + & " subdirectory."); elsif not Settings.Builtins.User_Github_Login.Is_Empty then + -- The user has provided a GitHub login, so provide an upload URL + -- to create a pull request. Put_Info - ("Please upload this file to " + ("If you haven't already, please fork " + & TTY.URL (Tail (Index.Community_Repo, '+')) + & " to your GitHub."); + Put_Info + ("This file can then be uploaded to " & TTY.URL (Index.Community_Host & "/" & Settings.Builtins.User_Github_Login.Get & "/" & Index.Community_Repo_Name & "/upload/" & Index.Community_Branch & "/" - & String (TOML_Index.Manifest_Path (Name))) + & String (TOML_Index.Community_Manifest_Path (Name))) & " to create a pull request against the community index."); else + -- We don't have the user's GitHub username, so show a more + -- generic message. Put_Info ("Please create a pull request against the community index at " & TTY.URL (Tail (Index.Community_Repo, '+')) & " including this file at " - & TTY.URL (String (TOML_Index.Manifest_Path (Name)))); + & TTY.URL + (String (TOML_Index.Community_Manifest_Path (Name)) & "/")); end if; exception @@ -797,7 +852,7 @@ package body Alire.Publish is Root_Path => Adirs.Full_Name (+Context.Path)) .Replacing (Origin => Context.Origin); begin - Check_Release (Release); + Check_Release (Release, Context); end Show_And_Confirm; ------------------- @@ -867,18 +922,20 @@ package body Alire.Publish is if URI.Scheme (URL) not in URI.HTTP then -- A git@ URL is private to the user and should not be used for - -- packaging: + -- packaging via the the community index if AAA.Strings.Has_Prefix (URL, "git@") then - Raise_Checked_Error - ("The origin cannot use a private git remote: " & URL); - end if; + if not Context.Options.For_Private_Index then + Raise_Checked_Error + ("The origin cannot use a private remote: " & URL); + end if; -- Otherwise we assume this is a local path - - Recoverable_User_Error - ("The origin must be a definitive remote location, but is " & URL); - -- For testing we may want to allow local URLs, or may be for - -- internal use with network drives? So allow forcing it. + else + Recoverable_User_Error + ("The origin must be a definitive remote location, but is " & URL); + -- For testing we may want to allow local URLs, or may be for + -- internal use with network drives? So allow forcing it. + end if; end if; Put_Success ("Origin is of supported kind: " & Context.Origin.Kind'Img); @@ -897,10 +954,10 @@ package body Alire.Publish is Is_Trusted (URL) then Put_Success ("Origin is hosted on trusted site: " - & URI.Authority_Without_Credentials (URL)); + & URI.Host (URL)); else Raise_Checked_Error ("Origin is hosted on unknown site: " - & URI.Authority_Without_Credentials (URL)); + & URI.Host (URL)); end if; end if; @@ -1017,9 +1074,13 @@ package body Alire.Publish is Run_Steps (Context, (Step_Check_User_Manifest, Step_Prepare_Archive, - Step_Verify_Origin, - Step_Verify_Github, - Step_Deploy_Sources, + Step_Verify_Origin) + & + (if Options.Skip_Submit + then No_Steps + else (1 => Step_Verify_Github)) + & + (Step_Deploy_Sources, Step_Check_Build, Step_Show_And_Confirm, Step_Generate_Index_Manifest) @@ -1034,10 +1095,8 @@ package body Alire.Publish is ---------------- function Is_Trusted (URL : Alire.URL) return Boolean - is (for some Site of Trusted_Sites => - URI.Authority_Without_Credentials (URL) = Site - or else - Has_Suffix (URI.Authority (URL), "." & Site)); + is (for some Site of Trusted_Sites => URI.Host (URL) = Site + or else Has_Suffix (URI.Host (URL), "." & Site)); ---------------------- -- Local_Repository -- @@ -1164,9 +1223,16 @@ package body Alire.Publish is -- requires the owner keys. case URI.Scheme (Fetch_URL) is when URI.VCS_Schemes => - Raise_Checked_Error - ("The remote URL seems to require repository ownership: " - & Fetch_URL); + if Options.For_Private_Index then + Publish.Remote_Origin (URL => Raw_URL, + Commit => Commit, + Subdir => +Subdir, + Options => Options); + else + Raise_Checked_Error + ("The remote URL seems to require repository " + & "ownership: " & Fetch_URL); + end if; when URI.None | URI.Unknown => Publish.Remote_Origin (URL => "git+file:" & Raw_URL, Commit => Commit, @@ -1249,9 +1315,13 @@ package body Alire.Publish is Token => <>); begin Run_Steps (Context, - (Step_Verify_Origin, - Step_Verify_Github, - Step_Deploy_Sources, + (Step_Verify_Origin) + & + (if Options.Skip_Submit + then No_Steps + else (1 => Step_Verify_Github)) + & + (Step_Deploy_Sources, Step_Check_Build, Step_Show_And_Confirm, Step_Generate_Index_Manifest) diff --git a/src/alire/alire-publish.ads b/src/alire/alire-publish.ads index 1b256fe9b..9c892a4c9 100644 --- a/src/alire/alire-publish.ads +++ b/src/alire/alire-publish.ads @@ -8,9 +8,10 @@ package Alire.Publish is type All_Options is private; - function New_Options (Skip_Build : Boolean := False; - Skip_Submit : Boolean := False; - Manifest : String := Roots.Crate_File_Name) + function New_Options (Skip_Build : Boolean := False; + Skip_Submit : Boolean := False; + For_Private_Index : Boolean := False; + Manifest : String := Roots.Crate_File_Name) return All_Options; procedure Directory_Tar (Path : Any_Path := "."; @@ -55,9 +56,10 @@ package Alire.Publish is private type All_Options is tagged record - Manifest_File : UString; - Skip_Build : Boolean := False; - Skip_Submit : Boolean := False; + Manifest_File : UString; + Skip_Build : Boolean := False; + Skip_Submit : Boolean := False; + For_Private_Index : Boolean := False; end record; function Manifest (Options : All_Options) return Any_Path diff --git a/src/alire/alire-releases.ads b/src/alire/alire-releases.ads index f0ed1a32a..8e8ec152b 100644 --- a/src/alire/alire-releases.ads +++ b/src/alire/alire-releases.ads @@ -287,6 +287,8 @@ package Alire.Releases is function Maintainer (R : Release) return Alire.Properties.Vector; + function Maint_Logins (R : Release) return Alire.Properties.Vector; + function Milestone (R : Release) return Milestones.Milestone; function Website (R : Release) return Alire.Properties.Vector with @@ -521,6 +523,10 @@ private is (Conditional.Enumerate (R.Properties).Filter (Alire.TOML_Keys.Maintainer)); + function Maint_Logins (R : Release) return Alire.Properties.Vector + is (Conditional.Enumerate (R.Properties).Filter + (Alire.TOML_Keys.Maint_Logins)); + function Website (R : Release) return Alire.Properties.Vector is (Conditional.Enumerate (R.Properties).Filter (Alire.TOML_Keys.Website)); diff --git a/src/alire/alire-toml_index.adb b/src/alire/alire-toml_index.adb index 8534d77e1..e22a7096d 100644 --- a/src/alire/alire-toml_index.adb +++ b/src/alire/alire-toml_index.adb @@ -584,7 +584,14 @@ package body Alire.TOML_Index is Name : constant String := +Crate; begin return Portable_Path - ("index/" & Name (Name'First .. Name'First + 1) & "/" & Name); + (Name (Name'First .. Name'First + 1) & "/" & Name); end Manifest_Path; + ----------------------------- + -- Community_Manifest_Path -- + ----------------------------- + + function Community_Manifest_Path (Crate : Crate_Name) return Portable_Path + is ("index/" & Manifest_Path (Crate)); + end Alire.TOML_Index; diff --git a/src/alire/alire-toml_index.ads b/src/alire/alire-toml_index.ads index 7a481ca5a..4de76865d 100644 --- a/src/alire/alire-toml_index.ads +++ b/src/alire/alire-toml_index.ads @@ -18,6 +18,11 @@ package Alire.TOML_Index is -- Get the expected location of a crate manifest in an index. The result is -- portable; that is, always uses forward slashes. + function Community_Manifest_Path (Crate : Crate_Name) return Portable_Path; + -- Get the expected location of a crate manifest according to the community + -- index's convention (i.e. with everything under the "index/" directory). + -- The result is portable; that is, always uses forward slashes. + procedure Load (Index : Index_On_Disk.Index'Class; Strict : Boolean; diff --git a/src/alire/alire-uri.adb b/src/alire/alire-uri.adb index 1c8e47a48..afd90830f 100644 --- a/src/alire/alire-uri.adb +++ b/src/alire/alire-uri.adb @@ -49,6 +49,36 @@ package body Alire.URI is Unknown); end Scheme; + ---------- + -- Host -- + ---------- + + function Host (This : URL) return String is + use AAA.Strings; + Auth : constant String := Authority_Without_Credentials (This); + begin + if Scheme (This) in File_Schemes then + return ""; + elsif Has_Prefix (This, "git@") + and then not Contains (Head (This, ":"), "/") + then + -- This has the form git@X:Y, so return X + return Head (Tail (This, "@"), ":"); + else + -- This is a normal URI, so return with any trailing port removed + -- (note that the host may be an IPv6 address in square brackets) + if Has_Prefix (Auth, "[") then + if Contains (Auth, "]:") then + return Head (Auth, "]:") & "]"; + else + return Auth; + end if; + else + return Head (Auth, ":"); + end if; + end if; + end Host; + package body Operators is --------- diff --git a/src/alire/alire-uri.ads b/src/alire/alire-uri.ads index d7db14cf7..4265896d7 100644 --- a/src/alire/alire-uri.ads +++ b/src/alire/alire-uri.ads @@ -77,6 +77,14 @@ package Alire.URI with Preelaborate is function Authority_Without_Credentials (This : URL) return String; -- Only the part after @ in an authority + function Host (This : URL) return String; + -- The host part of a remote URI + -- + -- Remotes of the form 'git@host.name:/some/path' (which are not valid + -- URIs) return the 'host.name' part. + -- + -- Returns an empty string for local URIs. + function Local_Path (This : URL) return String with Pre => Scheme (This) in None | File or else raise Checked_Error with Errors.Set diff --git a/src/alire/alire-utils-user_input-query_config.adb b/src/alire/alire-utils-user_input-query_config.adb index 50e110c3c..2f0928710 100644 --- a/src/alire/alire-utils-user_input-query_config.adb +++ b/src/alire/alire-utils-user_input-query_config.adb @@ -41,15 +41,23 @@ package body Alire.Utils.User_Input.Query_Config is Default => "Your Name", Validation => null)); + --------------------------------------- + -- Is_Empty_Or_Valid_GitHub_Username -- + --------------------------------------- + + function Is_Empty_Or_Valid_GitHub_Username (Str : String) return Boolean + is (Str = "" or else Is_Valid_GitHub_Username (Str)); + ----------------------- -- User_GitHub_Login -- ----------------------- function User_GitHub_Login return String - is (Config_Or_Query_String (Config_Key => "user.github_login", - Question => "Please enter your GitHub login:", - Default => "github-username", - Validation => Is_Valid_GitHub_Username'Access)); + is (Config_Or_Query_String + (Config_Key => "user.github_login", + Question => "Please enter your GitHub login:", + Default => "", + Validation => Is_Empty_Or_Valid_GitHub_Username'Access)); ----------------- -- Check_Email -- diff --git a/src/alire/alire-utils-user_input-query_config.ads b/src/alire/alire-utils-user_input-query_config.ads index 79aede3ab..5ce459b5c 100644 --- a/src/alire/alire-utils-user_input-query_config.ads +++ b/src/alire/alire-utils-user_input-query_config.ads @@ -26,7 +26,9 @@ package Alire.Utils.User_Input.Query_Config is function User_Name return String; function User_GitHub_Login return String - with Post => (Is_Valid_GitHub_Username (User_GitHub_Login'Result)); + with Post => + (User_GitHub_Login'Result = "" + or else Is_Valid_GitHub_Username (User_GitHub_Login'Result)); function User_Email return String with Post => Could_Be_An_Email (User_Email'Result, With_Name => False); diff --git a/src/alire/os_windows/alire-platforms-current__windows.adb b/src/alire/os_windows/alire-platforms-current__windows.adb index 135519306..8ec2f3fd6 100644 --- a/src/alire/os_windows/alire-platforms-current__windows.adb +++ b/src/alire/os_windows/alire-platforms-current__windows.adb @@ -173,7 +173,7 @@ package body Alire.Platforms.Current is Trace.Detail ("Alire is configured not to install msys2."); Trace.Detail - ("Run 'alr config --global --set msys2.do_not_install false'" & + ("Run 'alr settings --global --set msys2.do_not_install false'" & " if you want Alire to install msys2."); return False; end if; diff --git a/src/alr/alr-commands-init.adb b/src/alr/alr-commands-init.adb index 0895c203f..102c2f349 100644 --- a/src/alr/alr-commands-init.adb +++ b/src/alr/alr-commands-init.adb @@ -269,7 +269,9 @@ package body Alr.Commands.Init is Put_Line ("authors = " & Arr (Q (Username))); Put_Line ("maintainers = " & Arr (Q (Username & " <" & Email & ">"))); - Put_Line ("maintainers-logins = " & Arr (Q (Login))); + if Login /= "" then + Put_Line ("maintainers-logins = " & Arr (Q (Login))); + end if; Put_Line ("licenses = " & Q (Info.Licenses)); Put_Line ("website = " & Q (Info.Website)); Put_Line ("tags = " & Q_Arr (Info.Tags)); @@ -473,6 +475,23 @@ package body Alr.Commands.Init is end if; end Query_License; + ------------------------ + -- Query_GitHub_Login -- + ------------------------ + + procedure Query_GitHub_Login (Info : in out Crate_Init_Info) is + begin + if Alire.Settings.Builtins.User_Github_Login.Is_Empty then + AAA.Text_IO.Put_Paragraph + ("If you intend to publish this crate to the community index, you " + & "will need a GitHub account with which to submit a pull " + & "request, which can optionally be configured now (leave blank " + & "to skip)."); + end if; + Info.GitHub_Login := To_Unbounded_String + (UI.Query_Config.User_GitHub_Login); + end Query_GitHub_Login; + ---------------------- -- Query_Crate_Kind -- ---------------------- @@ -582,27 +601,16 @@ package body Alr.Commands.Init is is use Alire.Settings; Info : Crate_Init_Info; + User_Not_Already_Configured : constant Boolean := + Builtins.User_Email.Is_Empty + or else Builtins.User_Name.Is_Empty + or else Builtins.User_Github_Login.Is_Empty; begin if Cmd.Bin and then Cmd.Lib then Reportaise_Wrong_Arguments ("Please provide either --bin or --lib"); end if; - if Builtins.User_Email.Is_Empty or else - Builtins.User_Name.Is_Empty or else - Builtins.User_Github_Login.Is_Empty - then - AAA.Text_IO.Put_Paragraph - ("Alire needs some user information to initialize the crate" - & " author and maintainer, for eventual submission to" - & " the Alire community index. This information will be" - & " interactively requested now."); - TIO.New_Line; - TIO.Put_Line - ("You can edit this information at any time with 'alr config'"); - TIO.New_Line; - end if; - Query_Crate_Name (Args, Info); if Cmd.Bin then @@ -616,11 +624,30 @@ package body Alr.Commands.Init is Query_Description (Info); -- Query User info + if User_Not_Already_Configured then + TIO.New_Line; + AAA.Text_IO.Put_Paragraph + ("Alire needs some user information to prepare the crate for " + & "eventual submission to an index, which will be interactively " + & "requested now."); + TIO.New_Line; + TIO.Put_Line + ("You can edit this information at any time with 'alr settings'"); + TIO.New_Line; + end if; Info.Username := To_Unbounded_String (UI.Query_Config.User_Name); - Info.GitHub_Login := To_Unbounded_String - (UI.Query_Config.User_GitHub_Login); + Query_GitHub_Login (Info); Info.Email := To_Unbounded_String (UI.Query_Config.User_Email); + -- Make it clear that the remainder can't be changed with `alr settings` + TIO.New_Line; + if User_Not_Already_Configured then + AAA.Text_IO.Put_Paragraph + ("Alire needs some further crate-specific information to help " + & "other people who want to use your crate."); + end if; + TIO.New_Line; + Query_License (Info); Query_Tags (Info); diff --git a/src/alr/alr-commands-publish.adb b/src/alr/alr-commands-publish.adb index 018669eb5..f94a7369f 100644 --- a/src/alr/alr-commands-publish.adb +++ b/src/alr/alr-commands-publish.adb @@ -29,12 +29,15 @@ package body Alr.Commands.Publish is Options : constant Alire.Publish.All_Options := Alire.Publish.New_Options - (Manifest => + (Manifest => (if Cmd.Manifest.all /= "" then Cmd.Manifest.all else Alire.Roots.Crate_File_Name), - Skip_Build => Cmd.Skip_Build, - Skip_Submit => Cmd.Skip_Submit); + Skip_Build => Cmd.Skip_Build, + Skip_Submit => + -- "--for-private-index" implies "--skip-submit" + Cmd.Skip_Submit or else Cmd.For_Private_Index, + For_Private_Index => Cmd.For_Private_Index); begin if Alire.Utils.Count_True @@ -166,6 +169,13 @@ package body Alr.Commands.Publish is "", "--skip-submit", "Do not create the online pull request onto the community index"); + Define_Switch + (Config, + Cmd.For_Private_Index'Access, + "", "--for-private-index", + "The same as --skip-submit, but additionally disable checks which " + & "are specific to the community index and may not apply to others"); + Define_Switch (Config, Cmd.Cancel'Access, diff --git a/src/alr/alr-commands-publish.ads b/src/alr/alr-commands-publish.ads index 3ca816a4b..9b743c072 100644 --- a/src/alr/alr-commands-publish.ads +++ b/src/alr/alr-commands-publish.ads @@ -55,7 +55,7 @@ package Alr.Commands.Publish is overriding function Usage_Custom_Parameters (Cmd : Command) return String - is ("[--skip-build] [--skip-submit] [--tar] " + is ("[--skip-build] [--skip-submit|--for-private-index] [--tar] " & "[--manifest ] [ [commit]]] [--request-review NUM]"); private @@ -70,7 +70,12 @@ private -- Skip the build check Skip_Submit : aliased Boolean := False; - -- Stop after generation instead of asking the user to continue + -- Skip checking user's GitHub account, and stop after manifest + -- generation instead of asking the user to continue + + For_Private_Index : aliased Boolean := False; + -- Skip_Submit, and also disable checks which only apply to the + -- community index Cancel : aliased GNAT.Strings.String_Access := new String'(Unset); -- Number of a PR to prematurely close diff --git a/testsuite/drivers/alr.py b/testsuite/drivers/alr.py index 18f52109b..c2ac9ae63 100644 --- a/testsuite/drivers/alr.py +++ b/testsuite/drivers/alr.py @@ -123,17 +123,7 @@ def run_alr(*args, **kwargs): argv.insert(1, '-q') argv.extend(args) p = Run(argv) - if (p.status != 0 and complain_on_error) or (p.status == 0 and not complain_on_error): - print('The following command:') - print(' {}'.format(' '.join(quote_arg(arg) for arg in argv))) - print('Exited with status code {}'.format(p.status)) - print('Output:') - print(p.out) - if complain_on_error: - raise CalledProcessError('alr returned non-zero status code') - else: - raise CalledProcessError('alr returned zero status code but ' - 'an error was expected') + _report_unexpected_exit_status(p.status, complain_on_error, argv, p.out) # Convert CRLF line endings (Windows-style) to LF (Unix-style). This # canonicalization is necessary to make output comparison work on all @@ -141,7 +131,8 @@ def run_alr(*args, **kwargs): return ProcessResult(p.status, p.out.replace('\r\n', '\n')) -def run_alr_interactive(args: [str], output: [str], input: [str], timeout=5) -> str: +def run_alr_interactive(args: list[str], output: list[str], input: list[str], + timeout=5, complain_on_error=True) -> str: """ NON-WINDOWS-ONLY Run "alr" with the given arguments, feeding it the given input. No other @@ -151,7 +142,13 @@ def run_alr_interactive(args: [str], output: [str], input: [str], timeout=5) -> :param output: List of strings expected to be output by the subprocess. :param input: List of strings to feed to the subprocess's standard input. :param timeout: Timeout in seconds for the subprocess to complete. + :param complain_on_error: If True and the subprocess exits with a non-zero + status code, print information on the standard output (for debugging) + and raise a CalledProcessError (to abort the test). + Conversely if False and the process ends without error, it's presumed + an error was expected and CalledProcessError is raised too. """ + # Check whether on Windows to fail early (revisit if pexpect is updated?) if platform.system() == "Windows": print('SKIP: pexpect unavailable on Windows') @@ -177,12 +174,37 @@ def run_alr_interactive(args: [str], output: [str], input: [str], timeout=5) -> f"{child.before.decode('utf-8')}") # Assert proper output code - assert child.exitstatus == 0, \ - f"Unexpected exit status: {child.exitstatus}\n" + \ - f"Output: {child.before.decode('utf-8')}" + output = child.before.decode('utf-8') + _report_unexpected_exit_status( + child.exitstatus, complain_on_error, ["alr"] + args, output + ) # Return command output with CRLF replaced by LF (as does run_alr) - return child.before.decode('utf-8').replace('\r\n', '\n') + return output.replace('\r\n', '\n') + + +def _report_unexpected_exit_status(exit_status, complain_on_error, args, output): + """ + Report if a command yielded an unexpected exit status. + + If complain_on_error is True and exit_status is non-zero, or if it is False + and exit_status is zero, print the command and its output, then raise a + CalledProcessError. Otherwise, do nothing. + """ + error_occured = (exit_status != 0) + if (error_occured == complain_on_error): + command = " ".join(quote_arg(arg) for arg in args) + print('The following command:') + print(f' {command}') + print(f'Exited with status code {exit_status}') + print('Output:') + print(output) + if complain_on_error: + raise CalledProcessError('alr returned non-zero status code') + else: + raise CalledProcessError( + 'alr returned zero status code but an error was expected' + ) def fixtures_path(*args): @@ -282,7 +304,8 @@ def index_version(): return index_branch().split('-')[1] -def init_local_crate(name="xxx", binary=True, enter=True, update=True): +def init_local_crate(name="xxx", binary=True, enter=True, update=True, + with_maintainer_login=False): """ Initialize a local crate and enter its folder for further testing. @@ -291,16 +314,23 @@ def init_local_crate(name="xxx", binary=True, enter=True, update=True): :param bool binary: Initialize as --bin or --lib :param bool enter: Enter the created crate directory + + :param bool with_maintainer_login: Set the value of the `maintainers-logins` + field of the manifest to `["github-username"]` so that the crate is + valid for submission to the community index. """ run_alr("init", name, "--bin" if binary else "--lib") + os.chdir(name) if update: - os.chdir(name) run_alr("update") - os.chdir("..") - if enter: - os.chdir(name) + if with_maintainer_login: + with open("alire.toml", "a") as f: + f.write('maintainers-logins = ["github-username"]\n') + + if not enter: + os.chdir("..") def alr_workspace_cache(): diff --git a/testsuite/drivers/asserts.py b/testsuite/drivers/asserts.py index 7947044c4..9b6215c90 100644 --- a/testsuite/drivers/asserts.py +++ b/testsuite/drivers/asserts.py @@ -143,3 +143,11 @@ def assert_substring(target: str, text: str): """ assert target in text, \ f"Missing expected string '{target}' in text:\n{text}" + + +def assert_not_substring(target: str, text: str): + """ + Check that a string is not contained in another string + """ + assert target not in text, \ + f"Unexpected string '{target}' in text:\n{text}" diff --git a/testsuite/drivers/helpers.py b/testsuite/drivers/helpers.py index b61403216..b13877498 100644 --- a/testsuite/drivers/helpers.py +++ b/testsuite/drivers/helpers.py @@ -8,6 +8,7 @@ import re import shutil import stat +import sys from subprocess import run from zipfile import ZipFile @@ -293,3 +294,80 @@ def __exit__(self, exc_type, exc_val, exc_tb): import fcntl fcntl.flock(self.lock_file.fileno(), fcntl.LOCK_UN) self.lock_file.close() + + +GIT_WRAPPER_TEMPLATE = """\ +#! /usr/bin/env python +import subprocess, sys +substitution_dict = {substitution_dict} +# Argument substitutions +args = sys.argv[1:] +for key in substitution_dict: + args = [arg.replace(key, substitution_dict[key]) for arg in args] +# Run git +p = subprocess.run(['{actual_git_path}'] + args, capture_output=True) +# Output substitutions +stdout, stderr = p.stdout.decode(), p.stderr.decode() +for key in substitution_dict: + stdout = stdout.replace(substitution_dict[key], key) + stderr = stderr.replace(substitution_dict[key], key) +print(stdout, end="") +print(stderr, file=sys.stderr, end="") +# Exit with appropriate error code +sys.exit(p.returncode) +""" + +class MockGit: + """ + NON-WINDOWS-ONLY + A context manager which mocks the git command with string substitutions. + + The string substitutions are specified by the dictionary substitution_dict. + Every non-overlapping occurrence of each of its keys in a command line + argument is replaced with its corresponding value before being passed to + git. The reverse substitution is applied to git's output. The substitutions + are applied in the order in which they appear in substitution_dict. + + The mocked version of git will be placed in mock_git_dir, which will be + temporarily added to PATH. + """ + + def __init__(self, substitution_dict, mock_git_dir): + self._substitution_dict = substitution_dict + self._mock_git_dir = mock_git_dir + + def __enter__(self): + # Mocking on Windows would require git.exe wrapper + if on_windows(): + print('SKIP: git mocking unavailable on Windows') + sys.exit(0) + + # Create a wrapper script for git + wrapper_script = GIT_WRAPPER_TEMPLATE.format( + substitution_dict=self._substitution_dict, + actual_git_path=shutil.which("git") + ) + # Add the directory to PATH + try: + os.mkdir(self._mock_git_dir) + except FileExistsError: + pass + os.environ["PATH"] = ( + f'{self._mock_git_dir}{os.pathsep}{os.environ["PATH"]}' + ) + # Write the script to the directory + wrapper_descriptor = os.open( + os.path.join(self._mock_git_dir, "git"), + flags=(os.O_WRONLY | os.O_CREAT | os.O_EXCL), + mode=0o764, + ) + with open(wrapper_descriptor, "w") as f: + f.write(wrapper_script) + + def __exit__(self, type, value, traceback): + # Restore PATH + os.environ["PATH"] = os.environ["PATH"].replace( + f'{self._mock_git_dir}{os.pathsep}', '', 1 + ) + # Delete the wrapper script + os.remove(os.path.join(self._mock_git_dir, "git")) diff --git a/testsuite/tests/index/maint-bad-login/my_index/index/he/hello_world/hello_world-0.1.0.toml b/testsuite/tests/index/maint-bad-login/my_index/index/he/hello_world/hello_world-0.1.0.toml index df813e411..4cd12a989 100644 --- a/testsuite/tests/index/maint-bad-login/my_index/index/he/hello_world/hello_world-0.1.0.toml +++ b/testsuite/tests/index/maint-bad-login/my_index/index/he/hello_world/hello_world-0.1.0.toml @@ -3,7 +3,7 @@ name = "hello_world" version = "0.1.0" licenses = "GPL-3.0-only" maintainers = ["Mr. User "] -maintainers-logins = ["mr.user"] +maintainers-logins = [""] [origin] url = "file:." diff --git a/testsuite/tests/index/maint-bad-login/test.py b/testsuite/tests/index/maint-bad-login/test.py index 2e37dd915..38d6acba3 100644 --- a/testsuite/tests/index/maint-bad-login/test.py +++ b/testsuite/tests/index/maint-bad-login/test.py @@ -1,5 +1,5 @@ """ -Test that maintainers provide a plausible GitHub login +Test that maintainers-logins values can't be empty strings """ from drivers.alr import run_alr @@ -10,7 +10,7 @@ complain_on_error=False, debug=False, quiet=True) assert_match( '.*Loading .*hello_world-0.1.0.toml:.*maintainers-logins:.*' - 'maintainers-logins must be a valid GitHub login, but got: mr.user\n', + 'maintainers-logins values must be non-empty\n', p.out) print('SUCCESS') diff --git a/testsuite/tests/init/github-login/test.py b/testsuite/tests/init/github-login/test.py new file mode 100644 index 000000000..90a865c62 --- /dev/null +++ b/testsuite/tests/init/github-login/test.py @@ -0,0 +1,103 @@ +""" +Check optional input of user.github_login setting and maintainers-logins field +""" + +import os +import shutil + +from drivers.alr import run_alr, run_alr_interactive +from drivers.asserts import assert_eq, assert_substring, assert_not_substring + + +USERNAME_PROMPT = ( + r"If you intend to publish this crate to the community index, you will " + r"need a (\r\n|\r|\n)GitHub account with which to submit a pull request, " + r"which can optionally be (\r\n|\r|\n)configured now \(leave blank to " + r"skip\)\.(\r\n|\r|\n)Please enter your GitHub login: \(default: ''\)" +) + + +# `alr init` a crate without specifying a login. The resulting manifest should +# not contain a `maintainers-logins` field, and user.github_login should remain +# unset. +outputs, inputs = zip(*[ + ("Select the kind of crate you want to create", ""), + ("Enter a short description of the crate", ""), + ("Please enter your full name", ""), + (USERNAME_PROMPT, ""), + ("Please enter your email address", ""), + ("Select a software license for the crate", ""), + ("Enter a comma \(','\) separated list of tags", ""), + ("Enter an optional Website URL for the crate", ""), +]) +run_alr_interactive( + ['init', 'xxx'], + output=outputs, + input=inputs, + timeout=3 +) +assert_eq( + "\n", + run_alr("settings", "--global", "user.github_login").out +) +with open(os.path.join("xxx", "alire.toml")) as f: + assert_not_substring('maintainers-logins', f.read()) + +# Clean up for next test +shutil.rmtree("xxx") + +# Check inputs which aren't valid GitHub logins are rejected, then check +# configuring a valid login. The configured login should appear in the manifest +# file, and in the output of `alr settings` under `user.github_login`. +outputs, inputs = zip(*[ + ("Select the kind of crate you want to create", "" ), + ("Enter a short description of the crate", "" ), + ("Please enter your full name", "" ), + (USERNAME_PROMPT, "invalid_for_GitHub"), + (r"Invalid answer.[\r\n]+Please enter your GitHub", "valid-user-name" ), + ("Please enter your email address", "" ), + ("Select a software license for the crate", "" ), + ("Enter a comma \(','\) separated list of tags", "" ), + ("Enter an optional Website URL for the crate", "" ), +]) +run_alr_interactive( + ['init', 'xxx'], + output=outputs, + input=inputs, + timeout=3 +) +assert_eq( + "user.github_login=valid-user-name\n", + run_alr("settings", "--global", "user.github_login").out +) +with open(os.path.join("xxx", "alire.toml")) as f: + assert_substring('maintainers-logins = ["valid-user-name"]', f.read()) +shutil.rmtree("xxx") + +# Now that a username has been configured, check that the prompt is skipped +# and user.github_login is used instead. +outputs, inputs = zip(*[ + ("Select the kind of crate you want to create", ""), + ("Enter a short description of the crate", ""), + ("Please enter your full name", ""), + ("Please enter your email address", ""), + ("Select a software license for the crate", ""), + ("Enter a comma \(','\) separated list of tags", ""), + ("Enter an optional Website URL for the crate", ""), +]) +run_alr_interactive( + ['init', 'xxx'], + output=outputs, + input=inputs, + timeout=3 +) +assert_eq( + "user.github_login=valid-user-name\n", + run_alr("settings", "--global", "user.github_login").out +) +with open(os.path.join("xxx", "alire.toml")) as f: + assert_substring('maintainers-logins = ["valid-user-name"]', f.read()) +shutil.rmtree("xxx") + + +print('SUCCESS') diff --git a/testsuite/tests/init/github-login/test.yaml b/testsuite/tests/init/github-login/test.yaml new file mode 100644 index 000000000..32c747b3f --- /dev/null +++ b/testsuite/tests/init/github-login/test.yaml @@ -0,0 +1 @@ +driver: python-script diff --git a/testsuite/tests/monorepo/basic/test.py b/testsuite/tests/monorepo/basic/test.py index 4d5137170..e3c1bc0d7 100644 --- a/testsuite/tests/monorepo/basic/test.py +++ b/testsuite/tests/monorepo/basic/test.py @@ -16,7 +16,7 @@ start_dir = os.getcwd() os.mkdir("monoproject.upstream") os.chdir("monoproject.upstream") -init_local_crate("mycrate", enter=False) +init_local_crate("mycrate", enter=False, with_maintainer_login=True) os.chdir(start_dir) commit = init_git_repo("monoproject.upstream") diff --git a/testsuite/tests/monorepo/doubly-nested/test.py b/testsuite/tests/monorepo/doubly-nested/test.py index 6995e796a..5f646800e 100644 --- a/testsuite/tests/monorepo/doubly-nested/test.py +++ b/testsuite/tests/monorepo/doubly-nested/test.py @@ -15,8 +15,8 @@ index_dir = os.path.join(os.getcwd(), "my_index") os.mkdir("monoproject.upstream") os.chdir("monoproject.upstream") -init_local_crate("myparent") -init_local_crate("mychild") +init_local_crate("myparent", with_maintainer_login=True) +init_local_crate("mychild", with_maintainer_login=True) os.chdir(start_dir) commit = init_git_repo("monoproject.upstream") diff --git a/testsuite/tests/monorepo/manifest-in-place/test.py b/testsuite/tests/monorepo/manifest-in-place/test.py index b6c2e129f..59e2c7388 100644 --- a/testsuite/tests/monorepo/manifest-in-place/test.py +++ b/testsuite/tests/monorepo/manifest-in-place/test.py @@ -17,7 +17,7 @@ start_dir = os.getcwd() os.mkdir("monoproject.upstream") os.chdir("monoproject.upstream") -init_local_crate("crate1", enter=False) +init_local_crate("crate1", enter=False, with_maintainer_login=True) os.chdir(start_dir) commit1 = init_git_repo("monoproject.upstream") diff --git a/testsuite/tests/monorepo/multi-commit/test.py b/testsuite/tests/monorepo/multi-commit/test.py index 344948c85..3ad5a61c9 100644 --- a/testsuite/tests/monorepo/multi-commit/test.py +++ b/testsuite/tests/monorepo/multi-commit/test.py @@ -16,7 +16,7 @@ start_dir = os.getcwd() os.mkdir("monoproject.upstream") os.chdir("monoproject.upstream") -init_local_crate("crate1", enter=False) +init_local_crate("crate1", enter=False, with_maintainer_login=True) os.chdir(start_dir) commit1 = init_git_repo("monoproject.upstream") @@ -32,7 +32,7 @@ # We create a second crate at another commit os.chdir(os.path.join(start_dir, "monoproject.upstream")) -init_local_crate("crate2", enter=False) +init_local_crate("crate2", enter=False, with_maintainer_login=True) os.chdir(start_dir) commit2 = commit_all("monoproject.upstream") diff --git a/testsuite/tests/monorepo/subdir-in-tar/test.py b/testsuite/tests/monorepo/subdir-in-tar/test.py index 686a2bba5..460449499 100644 --- a/testsuite/tests/monorepo/subdir-in-tar/test.py +++ b/testsuite/tests/monorepo/subdir-in-tar/test.py @@ -11,7 +11,7 @@ import os # Prepare our "remote" repo -init_local_crate("xxx", enter=True) +init_local_crate("xxx", enter=True, with_maintainer_login=True) # Publish it. We need to give input to alr, so we directly call it. We use the # generated location as the "online" location, and this works because we are diff --git a/testsuite/tests/pin/branch-remote-protocols/test.py b/testsuite/tests/pin/branch-remote-protocols/test.py index 047e582c1..702bd22b3 100644 --- a/testsuite/tests/pin/branch-remote-protocols/test.py +++ b/testsuite/tests/pin/branch-remote-protocols/test.py @@ -3,18 +3,19 @@ """ import os +import shutil import subprocess from drivers.alr import alr_pin, alr_unpin, init_local_crate -from drivers.helpers import init_git_repo, git_branch +from drivers.helpers import init_git_repo, git_branch, MockGit from drivers.asserts import assert_eq # Create a crate with differing branches. init_local_crate(name="remote", enter=False) -LOCAL_REPO_PATH = os.path.join(os.getcwd(), "remote") +remote_path = os.path.join(os.getcwd(), "remote") # On the default branch, test_file contains "This is the main branch.\n". -test_file_path = os.path.join(LOCAL_REPO_PATH, "test_file") +test_file_path = os.path.join(remote_path, "test_file") with open(test_file_path, "w") as f: f.write("This is the main branch.\n") init_git_repo("remote") @@ -31,68 +32,37 @@ os.chdir("..") -# Prepare a directory on PATH at which to mock git. -ACTUAL_GIT_PATH = ( - subprocess.run(["bash", "-c", "type -p git"], capture_output=True) - .stdout.decode() - .strip() -) -MOCK_PATH = os.path.join(os.getcwd(), "mock_path") -os.mkdir(MOCK_PATH) -os.environ["PATH"] = f'{MOCK_PATH}:{os.environ["PATH"]}' - - # Perform the actual tests -URLs = [ +urls = [ "git+ssh://ssh.gitlab.company-name.com/path/to/repo.git", "xyz+https://github.com/path/to/repo.git", ] -SANITISED_URLS = [ +sanitised_urls = [ "ssh://ssh.gitlab.company-name.com/path/to/repo.git", "https://github.com/path/to/repo.git", ] -CACHE_TEST_FILE_PATH = "alire/cache/pins/remote/test_file" -for URL, S_URL in zip(URLs, SANITISED_URLS): +cache_test_file_path = "alire/cache/pins/remote/test_file" +mocked_git_dir = os.path.join(os.getcwd(), "mock_path") +for url, s_url in zip(urls, sanitised_urls): # Mock git with a wrapper that naively converts the url into the local path # to the "remote" crate. - wrapper_script = "\n".join( - [ - "#! /usr/bin/env python", - "import subprocess, sys", - 'if sys.argv[1:] == ["config", "--list"]:', - f' print("remote.origin.url={S_URL}\\n")', - "else:", - " args = [", - f' ("{LOCAL_REPO_PATH}" if a == "{S_URL}" else a)', - " for a in sys.argv[1:]", - " ]", - f' subprocess.run(["{ACTUAL_GIT_PATH}"] + args).check_returncode()', - ] - ) - wrapper_descriptor = os.open( - os.path.join(MOCK_PATH, "git"), - flags=(os.O_WRONLY | os.O_CREAT | os.O_TRUNC), - mode=0o764, - ) - with open(wrapper_descriptor, "w") as f: - f.write(wrapper_script) - - # Create an empty crate, and pin the default branch of the test repo - init_local_crate() - alr_pin("remote", url=URL, branch=default_branch) - with open(CACHE_TEST_FILE_PATH) as f: - assert_eq("This is the main branch.\n", f.read()) - - # Edit pin to point to the other branch, and verify the cached copy changes - # as it should - alr_unpin("remote", update=False) - alr_pin("remote", url=URL, branch="other") - with open(CACHE_TEST_FILE_PATH) as f: - assert_eq("This is the other branch.\n", f.read()) + with MockGit({s_url: remote_path}, mocked_git_dir): + # Create an empty crate, and pin the default branch of the test repo + init_local_crate() + alr_pin("remote", url=url, branch=default_branch) + with open(cache_test_file_path) as f: + assert_eq("This is the main branch.\n", f.read()) + # Edit pin to point to the other branch, and verify the cached copy changes + # as it should + alr_unpin("remote", update=False) + alr_pin("remote", url=url, branch="other") + with open(cache_test_file_path) as f: + assert_eq("This is the other branch.\n", f.read()) -# Restore PATH -os.environ["PATH"] = os.environ["PATH"][len(MOCK_PATH) + 1 :] + # Clean up for next test + os.chdir("..") + shutil.rmtree("xxx") print("SUCCESS") diff --git a/testsuite/tests/pin/branch-remote-protocols/test.yaml b/testsuite/tests/pin/branch-remote-protocols/test.yaml index ee8ead706..32c747b3f 100644 --- a/testsuite/tests/pin/branch-remote-protocols/test.yaml +++ b/testsuite/tests/pin/branch-remote-protocols/test.yaml @@ -1,4 +1 @@ driver: python-script -control: - - [SKIP, "skip_linux", "Test is Linux-only"] -indexes: {} diff --git a/testsuite/tests/publish/check-pre-release-version/test.py b/testsuite/tests/publish/check-pre-release-version/test.py index 58f223f4e..1b257f626 100644 --- a/testsuite/tests/publish/check-pre-release-version/test.py +++ b/testsuite/tests/publish/check-pre-release-version/test.py @@ -6,7 +6,7 @@ from drivers.asserts import assert_match from drivers.helpers import init_git_repo -init_local_crate("my_crate") +init_local_crate("my_crate", with_maintainer_login=True) p = run_alr("publish", "--tar", complain_on_error=False, quiet=False) diff --git a/testsuite/tests/publish/local-repo-branched/test.py b/testsuite/tests/publish/local-repo-branched/test.py index b2d09efcc..340c85dd9 100644 --- a/testsuite/tests/publish/local-repo-branched/test.py +++ b/testsuite/tests/publish/local-repo-branched/test.py @@ -3,6 +3,7 @@ """ from drivers.alr import init_local_crate, run_alr +from drivers.asserts import assert_match from drivers.helpers import init_git_repo from shutil import copyfile from subprocess import run @@ -10,7 +11,7 @@ import os # Prepare our "remote" repo -init_local_crate("xxx", enter=False) +init_local_crate("xxx", enter=False, with_maintainer_login=True) head_commit = init_git_repo("xxx") # Clone to a "local" repo and set minimal config @@ -27,6 +28,12 @@ assert run(["git", "push", "-u", "origin", "devel"]).returncode == 0 # Check that the publishing assistant completes without complaining -run_alr("--force", "publish", "--skip-submit") +p = run_alr("--force", "publish", "--skip-submit", quiet=False) + +# Check the user is warned that the origin URL is a local path +assert_match( + r".*The origin must be a definitive remote location, but is .*", + p.out +) print('SUCCESS') diff --git a/testsuite/tests/publish/local-repo-nonstd/test.py b/testsuite/tests/publish/local-repo-nonstd/test.py index 355cd7fd4..157ccd19e 100644 --- a/testsuite/tests/publish/local-repo-nonstd/test.py +++ b/testsuite/tests/publish/local-repo-nonstd/test.py @@ -20,7 +20,7 @@ def verify_manifest(): # Prepare our "remote" repo, changing the manifest name to "xxx.toml" -init_local_crate("xxx") +init_local_crate("xxx", with_maintainer_login=True) os.rename("alire.toml", "xxx.toml") os.chdir("..") head_commit = init_git_repo("xxx") diff --git a/testsuite/tests/publish/local-repo/test.py b/testsuite/tests/publish/local-repo/test.py index 41d27e43b..6ff3b40cd 100644 --- a/testsuite/tests/publish/local-repo/test.py +++ b/testsuite/tests/publish/local-repo/test.py @@ -20,7 +20,7 @@ def verify_manifest(): # Prepare our "remote" repo -init_local_crate("xxx", enter=False) +init_local_crate("xxx", enter=False, with_maintainer_login=True) head_commit = init_git_repo("xxx") # Clone to a "local" repo and set minimal config diff --git a/testsuite/tests/publish/pin-removal/test.py b/testsuite/tests/publish/pin-removal/test.py index 9956fdad1..f7ee1df27 100644 --- a/testsuite/tests/publish/pin-removal/test.py +++ b/testsuite/tests/publish/pin-removal/test.py @@ -16,7 +16,7 @@ # We create a repository with the nested crate that will act as the upstream # remote repository: start_dir = os.getcwd() -init_local_crate(crate) +init_local_crate(crate, with_maintainer_login=True) # And add the pin directly in the remote alr_pin("unobtanium", path="/") diff --git a/testsuite/tests/publish/private-indexes/my_index/crates/crate/alire.toml b/testsuite/tests/publish/private-indexes/my_index/crates/crate/alire.toml new file mode 100644 index 000000000..f52a77781 --- /dev/null +++ b/testsuite/tests/publish/private-indexes/my_index/crates/crate/alire.toml @@ -0,0 +1,9 @@ +name = "crate" +description = "Dummy crate" +version = "0.0.0" + +authors = ["Your Name"] +maintainers = ["Your Name "] +maintainers-logins = ["github-username"] +website = "" +tags = [] diff --git a/testsuite/tests/publish/private-indexes/my_index/crates/crate/crate.gpr b/testsuite/tests/publish/private-indexes/my_index/crates/crate/crate.gpr new file mode 100644 index 000000000..29bbd90b8 --- /dev/null +++ b/testsuite/tests/publish/private-indexes/my_index/crates/crate/crate.gpr @@ -0,0 +1,22 @@ +with "config/crate_config.gpr"; +project Crate is + + for Source_Dirs use ("src/", "config/"); + for Object_Dir use "obj/" & Crate_Config.Build_Profile; + for Create_Missing_Dirs use "True"; + for Exec_Dir use "bin"; + for Main use ("crate.adb"); + + package Compiler is + for Default_Switches ("Ada") use Crate_Config.Ada_Compiler_Switches; + end Compiler; + + package Binder is + for Switches ("Ada") use ("-Es"); -- Symbolic traceback + end Binder; + + package Install is + for Artifacts (".") use ("share"); + end Install; + +end Crate; diff --git a/testsuite/tests/publish/private-indexes/my_index/crates/crate/src/crate.adb b/testsuite/tests/publish/private-indexes/my_index/crates/crate/src/crate.adb new file mode 100644 index 000000000..27b9f460a --- /dev/null +++ b/testsuite/tests/publish/private-indexes/my_index/crates/crate/src/crate.adb @@ -0,0 +1,4 @@ +procedure Crate is +begin + null; +end Crate; diff --git a/testsuite/tests/publish/private-indexes/my_index/index/cr/crate/crate-1.0.0.toml b/testsuite/tests/publish/private-indexes/my_index/index/cr/crate/crate-1.0.0.toml new file mode 100644 index 000000000..623a83b91 --- /dev/null +++ b/testsuite/tests/publish/private-indexes/my_index/index/cr/crate/crate-1.0.0.toml @@ -0,0 +1,13 @@ +# NOTE: this crate is not used in the test, but we need at least one crate in +# the index or the community index will get cloned due to empty in-memory +# catalog. + +description = "Dummy 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/publish/private-indexes/my_index/index/index.toml b/testsuite/tests/publish/private-indexes/my_index/index/index.toml new file mode 100644 index 000000000..bad265e4f --- /dev/null +++ b/testsuite/tests/publish/private-indexes/my_index/index/index.toml @@ -0,0 +1 @@ +version = "1.1" diff --git a/testsuite/tests/publish/private-indexes/test.py b/testsuite/tests/publish/private-indexes/test.py new file mode 100644 index 000000000..a21265f43 --- /dev/null +++ b/testsuite/tests/publish/private-indexes/test.py @@ -0,0 +1,391 @@ +""" +Check "alr publish --for-private-index" supports private indexes +""" + + +import os +import shutil +import subprocess + +from drivers.alr import run_alr, run_alr_interactive +from drivers.helpers import init_git_repo, MockGit +from drivers.asserts import assert_match, assert_file_exists + + +INDEX_PATH = os.path.join(os.getcwd(), "my_index", "index") + + +def run(*args): + subprocess.run(*args).check_returncode() + +def test( + args, + url, + num_confirms, + output, + gen_manifest=None, + maint_logins=None, + github_user=None, + expect_success=True +): + """ + Perform the general test procedure. + + - Create a mock remote repo which appears to have a remote URL, and a local + clone thereof. + - `alr init` a crate in this repo + - Run `alr` with the specified arguments, responding `y` to the prompt + `Do you want to proceed with this information?` a specified number of + times + - Assert that `alr`'s final output matches zero or more regex patterns + - Optionally, assert that an index manifest was generated and matches zero + or more regex patterns + + :param list(str) args: The arguments to pass to `alr` (`--no-color` will be + added) + :param str url: The URL at which (as far as Alire is concerned) the remote + repository is located + :param int num_confirms: The number of times to respond `y` to the prompt + `Do you want to proceed with this information?` + :param list(str) output: Zero or more regex patterns which must match the + final output (i.e. that which follows the last confirmation prompt) of + `alr` + :param list(str) gen_manifest: Zero or more regex patterns which must match + the content of the generated manifest. If None, expects no manifest to + be generated. + :param str maint_logins: If not `None`, the value to set for the + `maintainers-logins` field in the crate's manifest before calling `alr` + :param str github_user: If not `None`, the value to set as + `user.github_login` before calling `alr` + :param bool expect_success: If True, the test will fail if `alr` returns a + non-zero exit code. If False, fail on a zero exit code. + """ + # Create an alire workspace to act as a "remote" + os.makedirs("remote") + os.chdir("remote") + run_alr("init", "--bin", "xxx") + os.chdir("xxx") + # Adjust the values of maintainers-logins and user.github_login if required + if github_user is not None: + run(["alr", "settings", "--set", "user.github_login", github_user]) + if maint_logins is not None: + with open("alire.toml", "a") as f: + f.write(f"maintainers-logins = {maint_logins}\n") + # Initialise as a git repo + init_git_repo(".") + remote_path = os.getcwd() + + # Mock git with a wrapper that naively converts the url into the local path + # to the "remote" crate. + mocked_git_dir = os.path.abspath(os.path.join("..", "..", "mocked_git")) + with MockGit({url: remote_path}, mocked_git_dir): + # Create a "local" clone of the "remote" + local_path = os.path.abspath(os.path.join("..", "..", "local", "xxx")) + os.makedirs(local_path) + os.chdir(local_path) + run(["git", "clone", url, local_path]) + + # Run alr + p = run_alr_interactive( + args, + output=num_confirms * [ + "Do you want to proceed with this information?" + ], + input=num_confirms * ["y"], + complain_on_error=expect_success, + timeout=60, + ) + + # Check output matches + for pattern in output: + assert_match(pattern, p) + + # Check the generated manifest file + gen_manifest_path = os.path.join( + os.getcwd(), "alire", "releases", "xxx-0.1.0-dev.toml" + ) + idx_manifest_dir = os.path.join(INDEX_PATH, "xx", "xxx") + os.chdir(os.path.join("..", "..")) + if gen_manifest is None: + assert_file_exists(gen_manifest_path, wanted=False) + else: + # Check existence + assert_file_exists(gen_manifest_path, wanted=True) + + # Check regex matches + with open(gen_manifest_path) as f: + manifest = f.read() + for pattern in gen_manifest: + assert_match(pattern, manifest) + + # Add this manifest to our local index + os.makedirs(idx_manifest_dir) + shutil.copyfile( + gen_manifest_path, + os.path.join(idx_manifest_dir, "xxx-0.1.0-dev.toml") + ) + + # Check that the crate can be retrieved and built without error + p = run_alr("get", "--build", "xxx", quiet=False) + assert_match( + r".*xxx=0\.1\.0-dev successfully retrieved and built.*", + p.out + ) + + # Clean up for next test + shutil.rmtree("local") + shutil.rmtree("remote") + shutil.rmtree(idx_manifest_dir, ignore_errors=True) # may not exist + + +# All tests should behave the same with and without "--force" +for force_arg in ([], ["--force"]): + # A crate suitable for the community index: + # + # Publication should succeed, with either "--for-private-index" or + # "--skip-submit" circumventing the requirement for the user to provide a + # GitHub account with a fork of the community index. + test( + args=force_arg + ["publish", "--skip-submit"], + url="https://github.com/some_user/repo-name.git", + maint_logins='["github-username"]', + num_confirms=2, + output=[ + r".*Success: Your index manifest file has been generated.*", + # Even though the automatic pull request has been skipped, alr + # should provide instructions for submission to the community index. + ( + r".*Please create a pull request against the community index " + r"at https://github.com/alire-project/alire-index including " + r"this file at index/xx/xxx/.*" + ), + ], + gen_manifest=[ + # "git+" should be prepended to avoid ambiguity + r'.*url = "git\+https://github\.com/some_user/repo-name\.git".*', + ], + expect_success=True + ) + test( + args=force_arg + ["publish", "--for-private-index"], + url="https://github.com/some_user/repo-name.git", + maint_logins='["github-username"]', + num_confirms=2, + output=[ + r".*Success: Your index manifest file has been generated.*", + # alr should provide instructions again, but they should be more + # generic, since we don't know where the private index is located. + r".*Please upload this file to the index in the xx/xxx/ subdirectory", + ], + gen_manifest=[ + r'.*url = "git\+https://github\.com/some_user/repo-name\.git".*', + ], + expect_success=True + ) + + # A crate suitable for the community index, with a GitHub user configured: + test( + args=force_arg + ["publish", "--skip-submit"], + url="https://github.com/some_user/repo-name.git", + maint_logins='["github-username"]', + github_user="github-username", + num_confirms=2, + output=[ + r".*Success: Your index manifest file has been generated.*", + # The user has configured a GitHub username, so a specific upload + # URL should be provided + ( + r".*If you haven't already, please fork " + r"https://github.com/alire-project/alire-index to your GitHub.*" + ), + ( + r".*This file can then be uploaded to " + r"https://github\.com/github-username/alire-index/upload/" + r"stable-1\.3\.0/index/xx/xxx to create a pull request against" + r" the community index.*" + ), + ], + gen_manifest=[ + r'.*url = "git\+https://github\.com/some_user/repo-name\.git".*', + ], + expect_success=True + ) + + # A crate unsuitable for the community index because its origin is private: + # + # "alr publish" should fail, because the origin URL looks private (it will + # also fail if the user does not provide a GitHub account with a fork of the + # community index, but that check comes later). + test( + args=force_arg + ["publish"], + url="git@bitbucket.org:/some_user/repo-name.git", + maint_logins='["github-username"]', + num_confirms=1, + output=[ + r".*The remote URL seems to require repository ownership: .*", + ], + gen_manifest=None, + expect_success=False + ) + # "alr publish --skip-submit" will fail for the same reason. + test( + args=force_arg + ["publish", "--skip-submit"], + url="git@bitbucket.org:/some_user/repo-name.git", + maint_logins='["github-username"]', + num_confirms=1, + output=[ + r".*The remote URL seems to require repository ownership: .*", + ], + gen_manifest=None, + expect_success=False + ) + # "alr publish --for-private-index" will succeed. + test( + args=force_arg + ["publish", "--for-private-index"], + url="git@bitbucket.org:/some_user/repo-name.git", + maint_logins='["github-username"]', + num_confirms=2, + output=[ + r".*Success: Your index manifest file has been generated.*", + r".*Please upload this file to the index in the xx/xxx/ subdirectory", + ], + gen_manifest=[ + r'.*url = "git@bitbucket\.org:/some_user/repo-name\.git".*', + ], + expect_success=True + ) + + # A crate unsuitable for the community index because it has a + # "maintainers-logins" value which is invalid for GitHub: + # + # "alr publish" and "alr publish --skip-submit" should fail. + test( + args=force_arg + ["publish"], + url="https://github.com/some_user/repo-name.git", + maint_logins='["valid-for-GitHub", "invalid_for_GitHub"]', + num_confirms=0, # (fails before first confirmation) + output=[ + ( + r".*The maintainer login 'invalid_for_GitHub' " + r"is not a valid GitHub username.*" + ), + ], + gen_manifest=None, + expect_success=False + ) + test( + args=force_arg + ["publish", "--skip-submit"], + url="https://github.com/some_user/repo-name.git", + maint_logins='["valid-for-GitHub", "invalid_for_GitHub"]', + num_confirms=0, + output=[ + ( + r".*The maintainer login 'invalid_for_GitHub' " + r"is not a valid GitHub username.*" + ), + ], + gen_manifest=None, + expect_success=False + ) + # "alr publish --for-private-index" will succeed. + test( + args=force_arg + ["publish", "--for-private-index"], + url="https://github.com/some_user/repo-name.git", + maint_logins='["valid-for-GitHub", "invalid_for_GitHub"]', + num_confirms=2, + output=[ + r".*Success: Your index manifest file has been generated.*", + r".*Please upload this file to the index in the xx/xxx/ subdirectory", + ], + gen_manifest=[ + r'.*url = "git\+https://github\.com/some_user/repo-name\.git".*', + ], + expect_success=True + ) + + # A crate unsuitable for the community index because it has no + # "maintainers-logins" value: + # + # "alr publish" and "alr publish --skip-submit" should fail. + test( + args=force_arg + ["publish"], + url="https://github.com/some_user/repo-name.git", + maint_logins=None, + num_confirms=0, # (fails before first confirmation) + output=[ + r".*Missing required properties: maintainers-logins.*", + ], + gen_manifest=None, + expect_success=False + ) + test( + args=force_arg + ["publish", "--skip-submit"], + url="https://github.com/some_user/repo-name.git", + maint_logins=None, + num_confirms=0, + output=[ + r".*Missing required properties: maintainers-logins.*", + ], + gen_manifest=None, + expect_success=False + ) + # "alr publish --for-private-index" will succeed. + test( + args=force_arg + ["publish", "--for-private-index"], + url="https://github.com/some_user/repo-name.git", + maint_logins=None, + num_confirms=2, + output=[ + r".*Success: Your index manifest file has been generated.*", + r".*Please upload this file to the index in the xx/xxx/ subdirectory", + ], + gen_manifest=[ + r'.*url = "git\+https://github\.com/some_user/repo-name\.git".*', + ], + expect_success=True + ) + + # A crate unsuitable for the community index because "maintainers-logins" + # is an empty list: + # + # This should be identical to there being no "maintainers-logins" field at + # all. + test( + args=force_arg + ["publish"], + url="https://github.com/some_user/repo-name.git", + maint_logins="[]", + num_confirms=0, + output=[ + r".*Missing required properties: maintainers-logins.*", + ], + gen_manifest=None, + expect_success=False + ) + test( + args=force_arg + ["publish", "--skip-submit"], + url="https://github.com/some_user/repo-name.git", + maint_logins="[]", + num_confirms=0, + output=[ + r".*Missing required properties: maintainers-logins.*", + ], + gen_manifest=None, + expect_success=False + ) + test( + args=force_arg + ["publish", "--for-private-index"], + url="https://github.com/some_user/repo-name.git", + maint_logins="[]", + num_confirms=2, + output=[ + r".*Success: Your index manifest file has been generated.*", + r".*Please upload this file to the index in the xx/xxx/ subdirectory", + ], + gen_manifest=[ + r'.*url = "git\+https://github\.com/some_user/repo-name\.git".*', + ], + expect_success=True + ) + + +print("SUCCESS") diff --git a/testsuite/tests/publish/private-indexes/test.yaml b/testsuite/tests/publish/private-indexes/test.yaml new file mode 100644 index 000000000..0a859639c --- /dev/null +++ b/testsuite/tests/publish/private-indexes/test.yaml @@ -0,0 +1,4 @@ +driver: python-script +indexes: + my_index: + in_fixtures: false diff --git a/testsuite/tests/publish/remote-origin-nonstd/test.py b/testsuite/tests/publish/remote-origin-nonstd/test.py index 2651efc2d..b4f6d1f0c 100644 --- a/testsuite/tests/publish/remote-origin-nonstd/test.py +++ b/testsuite/tests/publish/remote-origin-nonstd/test.py @@ -2,7 +2,7 @@ Test proper publishing of a ready remote origin with custom manifest location """ -from drivers.alr import run_alr, index_version +from drivers.alr import init_local_crate, run_alr, index_version from drivers.asserts import assert_match from drivers.helpers import contents, content_of, init_git_repo, zip_dir from shutil import copyfile, rmtree @@ -26,7 +26,7 @@ def verify_manifest(): run_alr("index", "--add", "my_index", "--name", "my_index") # Prepare a repo and a zipball to be used as "remote" targets for publishing -run_alr("init", "--bin", "xxx") +init_local_crate("xxx", enter=False, with_maintainer_login=True) # Rename the manifest location os.rename(os.path.join("xxx", "alire.toml"), os.path.join("xxx", "xxx.toml")) diff --git a/testsuite/tests/publish/remote-origin/test.py b/testsuite/tests/publish/remote-origin/test.py index f35c29510..bdd9ba65f 100644 --- a/testsuite/tests/publish/remote-origin/test.py +++ b/testsuite/tests/publish/remote-origin/test.py @@ -2,7 +2,7 @@ Tests for proper publishing of a ready remote origin """ -from drivers.alr import run_alr, index_version +from drivers.alr import init_local_crate, run_alr, index_version from drivers.asserts import assert_match from drivers.helpers import contents, content_of, init_git_repo, zip_dir from shutil import copyfile, rmtree @@ -26,7 +26,7 @@ def verify_manifest(): run_alr("index", "--add", "my_index", "--name", "my_index") # Prepare a repo and a zipball to be used as "remote" targets for publishing -run_alr("init", "--bin", "xxx") +init_local_crate("xxx", enter=False, with_maintainer_login=True) # Create the zip zip_dir("xxx", "xxx.zip") diff --git a/testsuite/tests/publish/ssh-remote-origin/test.py b/testsuite/tests/publish/ssh-remote-origin/test.py new file mode 100644 index 000000000..3dc33a82a --- /dev/null +++ b/testsuite/tests/publish/ssh-remote-origin/test.py @@ -0,0 +1,34 @@ +""" +Check "alr publish " only allows private origins with --for-private-index +""" + + +from drivers.alr import run_alr +from drivers.asserts import assert_match + + +urls = [ + "git@host.invalid:/path/to/repo.git", +] +commit = "0" * 40 + +# We expect attempts to publish from these origins to fail, because they are +# obviously private. +for url in urls: + p = run_alr("publish", url, commit, complain_on_error=False) + assert_match(r".*The origin cannot use a private remote:.*", p.out) + p = run_alr( + "publish", "--skip-submit", url, commit, complain_on_error=False + ) + assert_match(r".*The origin cannot use a private remote:.*", p.out) + +# Publishing will still fail with "--for-private-index", but it should be due to +# the untrusted host, not because the URLs appear private. +for url in urls: + p = run_alr( + "publish", "--for-private-index", url, commit,complain_on_error=False + ) + assert_match(r".*Origin is hosted on unknown site: host\.invalid.*", p.out) + + +print("SUCCESS") diff --git a/testsuite/tests/publish/ssh-remote-origin/test.yaml b/testsuite/tests/publish/ssh-remote-origin/test.yaml new file mode 100644 index 000000000..32c747b3f --- /dev/null +++ b/testsuite/tests/publish/ssh-remote-origin/test.yaml @@ -0,0 +1 @@ +driver: python-script diff --git a/testsuite/tests/publish/tarball-plaindir-nonstd/test.py b/testsuite/tests/publish/tarball-plaindir-nonstd/test.py index 0b54ebc52..3f48c71f8 100644 --- a/testsuite/tests/publish/tarball-plaindir-nonstd/test.py +++ b/testsuite/tests/publish/tarball-plaindir-nonstd/test.py @@ -11,7 +11,7 @@ import os # Prepare our "remote" repo -init_local_crate("xxx", enter=True) +init_local_crate("xxx", enter=True, with_maintainer_login=True) # with custom manifest location os.rename("alire.toml", "xxx.toml") diff --git a/testsuite/tests/publish/tarball-plaindir/test.py b/testsuite/tests/publish/tarball-plaindir/test.py index a3b910d33..46c956946 100644 --- a/testsuite/tests/publish/tarball-plaindir/test.py +++ b/testsuite/tests/publish/tarball-plaindir/test.py @@ -11,7 +11,7 @@ import os # Prepare our "remote" repo -init_local_crate("xxx", enter=True) +init_local_crate("xxx", enter=True, with_maintainer_login=True) canary = "canary.txt" diff --git a/testsuite/tests/publish/tarball-repo-nonstd/test.py b/testsuite/tests/publish/tarball-repo-nonstd/test.py index ad482832e..93578e115 100644 --- a/testsuite/tests/publish/tarball-repo-nonstd/test.py +++ b/testsuite/tests/publish/tarball-repo-nonstd/test.py @@ -3,6 +3,7 @@ """ from drivers.alr import init_local_crate, run_alr +from drivers.asserts import assert_match from drivers.helpers import init_git_repo from shutil import copyfile from subprocess import run @@ -10,7 +11,7 @@ import os # Prepare our "remote" repo -init_local_crate("xxx", enter=True) +init_local_crate("xxx", enter=True, with_maintainer_login=True) os.rename("alire.toml", "xxx.toml") # Initialize a repo right here @@ -25,11 +26,18 @@ # Publish it. We need to give input to alr, so we directly call it. We use the # generated location as the "online" location, and this works because we are # forcing. ".tgz" is used, as bzip2 is not supported by `git archive`. -p = run(["alr", "-q", "-f", "-n", "publish", "--skip-build", "--skip-submit", "--tar", +p = run(["alr", "-f", "-n", "publish", "--skip-build", "--skip-submit", "--tar", "--manifest", "xxx.toml"], - input=f"file:{os.getcwd()}/alire/archives/xxx-0.1.0-dev.tgz\n".encode()) + input=f"file:{os.getcwd()}/alire/archives/xxx-0.1.0-dev.tgz\n".encode(), + capture_output=True) p.check_returncode() +# Check user is warned that the origin URL is a local path +assert_match( + r".*The origin must be a definitive remote location, but is .*", + p.stderr.decode() +) + # Verify the index manifest has been generated assert os.path.isfile("./alire/releases/xxx-0.1.0-dev.toml") diff --git a/testsuite/tests/publish/tarball-repo/test.py b/testsuite/tests/publish/tarball-repo/test.py index 2dc953bce..aa69bd465 100644 --- a/testsuite/tests/publish/tarball-repo/test.py +++ b/testsuite/tests/publish/tarball-repo/test.py @@ -3,6 +3,7 @@ """ from drivers.alr import init_local_crate, run_alr +from drivers.asserts import assert_match from drivers.helpers import init_git_repo from shutil import copyfile from subprocess import run @@ -10,7 +11,7 @@ import os # Prepare our "remote" repo -init_local_crate("xxx", enter=True) +init_local_crate("xxx", enter=True, with_maintainer_login=True) # Initialize a repo right here init_git_repo(".") @@ -24,10 +25,17 @@ # Publish it. We need to give input to alr, so we directly call it. We use the # generated location as the "online" location, and this works because we are # forcing. ".tgz" is used, as bzip2 is not supported by `git archive`. -p = run(["alr", "-q", "-f", "-n", "publish", "--skip-build", "--skip-submit", "--tar"], - input=f"file:{os.getcwd()}/alire/archives/xxx-0.1.0-dev.tgz\n".encode()) +p = run(["alr", "-f", "-n", "publish", "--skip-build", "--skip-submit", "--tar"], + input=f"file:{os.getcwd()}/alire/archives/xxx-0.1.0-dev.tgz\n".encode(), + capture_output=True) p.check_returncode() +# Check user is warned that the origin URL is a local path +assert_match( + r".*The origin must be a definitive remote location, but is .*", + p.stderr.decode() +) + # Verify the index manifest has been generated assert os.path.isfile("./alire/releases/xxx-0.1.0-dev.toml")