From 08a73db72dcc17c43f56fb67913a07716fa831e7 Mon Sep 17 00:00:00 2001 From: Alejandro R Mosteo Date: Thu, 6 Feb 2025 13:09:29 +0100 Subject: [PATCH] fix: install system gprbuild when selected in assistant (#1833) * Install system toolchain if missing * test * self-review --- src/alire/alire-origins-deployers-system.adb | 15 +++ src/alire/alire-origins-deployers-system.ads | 4 + src/alire/alire-origins.adb | 8 ++ src/alire/alire-origins.ads | 3 + src/alire/alire-releases.adb | 9 ++ src/alire/alire-releases.ads | 5 + src/alire/alire-toolchains-solutions.adb | 6 ++ src/alire/alire-toolchains.adb | 36 ++++++- src/alire/alire-toolchains.ads | 12 +++ src/alr/alr-commands-toolchain.adb | 29 ++++++ testsuite/drivers/alr.py | 8 ++ testsuite/drivers/helpers.py | 11 +++ .../gnat_external/gnat_external-external.toml | 17 ++++ .../gp/gprbuild/gprbuild-external.toml | 18 ++++ .../system_toolchain_ubuntu/index.toml | 4 + .../toolchain/install-from-system/test.py | 95 +++++++++++++++++++ .../toolchain/install-from-system/test.yaml | 5 + 17 files changed, 280 insertions(+), 5 deletions(-) create mode 100644 testsuite/fixtures/system_toolchain_ubuntu/gn/gnat_external/gnat_external-external.toml create mode 100644 testsuite/fixtures/system_toolchain_ubuntu/gp/gprbuild/gprbuild-external.toml create mode 100644 testsuite/fixtures/system_toolchain_ubuntu/index.toml create mode 100644 testsuite/tests/dockerized/toolchain/install-from-system/test.py create mode 100644 testsuite/tests/dockerized/toolchain/install-from-system/test.yaml diff --git a/src/alire/alire-origins-deployers-system.adb b/src/alire/alire-origins-deployers-system.adb index 8ef2db99..521e2f3d 100644 --- a/src/alire/alire-origins-deployers-system.adb +++ b/src/alire/alire-origins-deployers-system.adb @@ -6,6 +6,7 @@ with Alire.Origins.Deployers.System.RPM_Wrappers; with Alire.Origins.Deployers.System.Unknown; with Alire.Origins.Deployers.System.Zypper; with Alire.OS_Lib; +with Alire.Toolchains; with CLIC.User_Input; @@ -161,4 +162,18 @@ package body Alire.Origins.Deployers.System is end if; end Executable_Path; + ------------- + -- Install -- + ------------- + + procedure Install (This : Releases.Release) is + begin + if Toolchains.Is_Tool (This) then + Toolchains.Deploy (This); + -- This ensures the cache of tools is properly reset + else + Platform_Deployer (This.Origin).Deploy ("").Assert; + end if; + end Install; + end Alire.Origins.Deployers.System; diff --git a/src/alire/alire-origins-deployers-system.ads b/src/alire/alire-origins-deployers-system.ads index e3769bb7..2a22c12c 100644 --- a/src/alire/alire-origins-deployers-system.ads +++ b/src/alire/alire-origins-deployers-system.ads @@ -73,6 +73,10 @@ package Alire.Origins.Deployers.System is function Already_Installed (This : Origins.Origin) return Boolean with Pre => This.Is_System; + procedure Install (This : Releases.Release) + with Pre => This.Origin.Is_System; + -- Install the system package that provides this release + function Executable_Name return String; -- Returns the simple name of the executable package manager on the system diff --git a/src/alire/alire-origins.adb b/src/alire/alire-origins.adb index d43b25b4..276d340f 100644 --- a/src/alire/alire-origins.adb +++ b/src/alire/alire-origins.adb @@ -4,6 +4,7 @@ with AAA.Strings; with Alire.Features; with Alire.Loading; +with Alire.Origins.Deployers.System; with Alire.Platforms.Current; with Alire.Root; with Alire.URI; @@ -854,4 +855,11 @@ package body Alire.Origins is or else not This.Data.Bin_Archive.Evaluate (Env).Is_Empty); + ------------------ + -- Is_Installed -- + ------------------ + + function Already_Installed (This : Origin) return Boolean + renames Deployers.System.Already_Installed; + end Alire.Origins; diff --git a/src/alire/alire-origins.ads b/src/alire/alire-origins.ads index 9d986a92..9f0a97f5 100644 --- a/src/alire/alire-origins.ads +++ b/src/alire/alire-origins.ads @@ -117,6 +117,9 @@ package Alire.Origins is function Is_System (This : Origin) return Boolean is (This.Kind = System); + function Already_Installed (This : Origin) return Boolean + with Pre => This.Is_System; + function Package_Name (This : Origin) return String with Pre => This.Kind = System; diff --git a/src/alire/alire-releases.adb b/src/alire/alire-releases.adb index 9751795c..91161719 100644 --- a/src/alire/alire-releases.adb +++ b/src/alire/alire-releases.adb @@ -377,6 +377,15 @@ package body Alire.Releases is raise; end Deploy; + ---------------------------- + -- Install_System_Package -- + ---------------------------- + + procedure Install_System_Package (This : Release) is + begin + Origins.Deployers.System.Install (This); + end Install_System_Package; + ---------------- -- Forbidding -- ---------------- diff --git a/src/alire/alire-releases.ads b/src/alire/alire-releases.ads index 8e8ec152..8b5daf27 100644 --- a/src/alire/alire-releases.ads +++ b/src/alire/alire-releases.ads @@ -381,6 +381,11 @@ package Alire.Releases is -- so future inspections of the folder can ensure the operation wasn't -- interrupted. No actions for the release are run at this time. + procedure Install_System_Package (This : Release) + with Pre => This.Origin.Is_System; + -- Install the system package that provides the release, without any + -- additional actions (unlike Deploy). + private use Semantic_Versioning; diff --git a/src/alire/alire-toolchains-solutions.adb b/src/alire/alire-toolchains-solutions.adb index af1e46a4..0000ded8 100644 --- a/src/alire/alire-toolchains-solutions.adb +++ b/src/alire/alire-toolchains-solutions.adb @@ -35,6 +35,12 @@ package body Alire.Toolchains.Solutions is Result : Alire.Solutions.Solution := Solution; begin + -- Last-minute redeployment of any missing toolchain element. This may + -- happen if the user has manually deleted the cache of toolchains, or + -- uninstalled a system package for the external compiler. + + Toolchains.Deploy_Missing; + -- For every tool in the toolchain that does not appear in the solution, -- we will insert the user-configured tool, if any. diff --git a/src/alire/alire-toolchains.adb b/src/alire/alire-toolchains.adb index 2e70dba3..636f0657 100644 --- a/src/alire/alire-toolchains.adb +++ b/src/alire/alire-toolchains.adb @@ -7,7 +7,7 @@ with Alire.Cache; with Alire.Directories; with Alire.Index; with Alire.Manifest; -with Alire.Origins; +with Alire.Origins.Deployers.System; with Alire.Paths; with Alire.Platforms.Current; with Alire.Properties; @@ -205,6 +205,8 @@ package body Alire.Toolchains is if Release.Origin.Is_Index_Provided then Toolchains.Deploy (Release); + elsif Release.Origin.Is_System then + Release.Install_System_Package; else Trace.Debug ("The user selected a external version as default for " @@ -584,9 +586,23 @@ package body Alire.Toolchains is Root.Platform_Properties) loop if not Release.Origin.Is_Index_Provided then - Trace.Debug ("Detected external toolchain release: " - & Release.Milestone.TTY_Image); - Result.Include (Release); + -- For a system external, we must make sure it is installed + case Origins.External_Kinds'(Release.Origin.Kind) is + when Origins.External => + Trace.Debug ("Detected external toolchain release: " + & Release.Milestone.TTY_Image); + Result.Include (Release); + when Origins.System => + if Release.Origin.Already_Installed then + Trace.Debug ("Detected system toolchain release: " + & Release.Milestone.TTY_Image); + Result.Include (Release); + else + Trace.Debug + ("Skipping uninstalled system toolchain release: " + & Release.Milestone.TTY_Image); + end if; + end case; end if; end loop; end loop; @@ -630,13 +646,23 @@ package body Alire.Toolchains is Trace.Detail ("Skipping installation of already available release: " & Release.Milestone.TTY_Image); return; + elsif Release.Origin.Is_System then + if Release.Origin.Already_Installed then + Trace.Debug ("Skipping installation of already available release: " + & Release.Milestone.TTY_Image); + else + Origins.Deployers.System.Platform_Deployer + (Release.Origin).Deploy ("").Assert; + Invalidate_Available_Cache; + end if; + return; elsif Release.Origin.Kind in Origins.External then Trace.Debug ("Skipping installation of external tool release: " & Release.Milestone.TTY_Image); return; end if; - -- Deploy at the install location + -- Deploy a regular binary release at the install location Release.Deploy (Env => Root.Platform_Properties, Parent_Folder => Location, diff --git a/src/alire/alire-toolchains.ads b/src/alire/alire-toolchains.ads index 51b44f3e..1c941ba3 100644 --- a/src/alire/alire-toolchains.ads +++ b/src/alire/alire-toolchains.ads @@ -84,6 +84,11 @@ package Alire.Toolchains is -- release being deployed (e.g. the user messed with files and deleted it -- manually). + function Toolchain_Is_Complete return Boolean; + -- If the user configures only part of the toolchain, this might work if + -- the unconfigured tool is in the environment, but otherwise it might + -- cause troubles, so we may want to warn about it. + procedure Unconfigure (Crate : Crate_Name; Level : Settings.Level; Fail_If_Unset : Boolean := True); @@ -195,4 +200,11 @@ private function Tool_Milestone (Crate : Crate_Name) return Milestones.Milestone is (Milestones.New_Milestone (Settings.DB.Get (Tool_Key (Crate), ""))); + --------------------------- + -- Toolchain_Is_Complete -- + --------------------------- + + function Toolchain_Is_Complete return Boolean + is (for all Tool of Tools => Tool_Is_Configured (Tool)); + end Alire.Toolchains; diff --git a/src/alr/alr-commands-toolchain.adb b/src/alr/alr-commands-toolchain.adb index f8495578..20504074 100644 --- a/src/alr/alr-commands-toolchain.adb +++ b/src/alr/alr-commands-toolchain.adb @@ -314,6 +314,31 @@ package body Alr.Commands.Toolchain is Table.Print (Always); end List; + ------------------------------ + -- Report_Unavailable_Tools -- + ------------------------------ + + procedure Report_Unavailable_Tools is + use Alire; + Complain : Boolean := False; + begin + for Tool of Toolchains.Tools loop + if not Toolchains.Tool_Is_Configured (Tool) and then + not OS_Lib.Exists_In_Path (+Tool) + then + Trace.Warning + ("Unconfigured tool is not in path, builds will likely fail: " + & TTY.Error (+Tool)); + Complain := True; + end if; + end loop; + + if Complain then + Trace.Warning ("Please ensure a complete toolchain is available with " + & TTY.Terminal ("alr toolchain --select")); + end if; + end Report_Unavailable_Tools; + ------------- -- Execute -- ------------- @@ -394,6 +419,10 @@ package body Alr.Commands.Toolchain is & "command line."); end if; + if not Alire.Toolchains.Toolchain_Is_Complete then + Report_Unavailable_Tools; + end if; + elsif not Cmd.Disable then -- When no command is specified, print the list Cmd.List; diff --git a/testsuite/drivers/alr.py b/testsuite/drivers/alr.py index da5a847e..bbe51820 100644 --- a/testsuite/drivers/alr.py +++ b/testsuite/drivers/alr.py @@ -671,6 +671,14 @@ def unselect_compiler(): alr_settings_unset("toolchain.external.gnat") +def unselect_gprbuild(): + """ + Leave gprbuild configuration as if "None" was selected by the user in the + assistant. + """ + alr_settings_unset("toolchain.use.gprbuild") + + def set_default_user_settings(): """ Set the default alr settings that are undone by the testsuite defaults diff --git a/testsuite/drivers/helpers.py b/testsuite/drivers/helpers.py index 53c8d953..70ec398f 100644 --- a/testsuite/drivers/helpers.py +++ b/testsuite/drivers/helpers.py @@ -287,6 +287,17 @@ def neutral_path(path : str) -> str: """ return path.replace('\\', '/') +def which(exec : str) -> str: + """ + Return the full path to an executable if it can be found in PATH, or "" + otherwise. On Windows, ".exe" is automatically appended. + """ + if on_windows() and not exec.endswith(".exe"): + return which(f"{exec}.exe") + + return shutil.which(exec) + + class FileLock(): """ A filesystem-level lock for tests executed from different threads but diff --git a/testsuite/fixtures/system_toolchain_ubuntu/gn/gnat_external/gnat_external-external.toml b/testsuite/fixtures/system_toolchain_ubuntu/gn/gnat_external/gnat_external-external.toml new file mode 100644 index 00000000..61180ec9 --- /dev/null +++ b/testsuite/fixtures/system_toolchain_ubuntu/gn/gnat_external/gnat_external-external.toml @@ -0,0 +1,17 @@ +description = "GNAT is a compiler for the Ada programming language" +name = "gnat_external" + +maintainers = ["alejandro@mosteo.com"] +maintainers-logins = ["mosteo"] + +[[external]] +kind = "version-output" +version-regexp = "^GNAT\\D*([\\d\\.]+).*" +version-command = ["gnat", "--version"] +provides = "gnat" + +# We do not want to have system package definitions because in typical systems +# like Debian/Ubuntu only one version at a time can be installed. Hence using +# different versions in different crates/configurations would imply messing the +# users' system. Let them manually configure the compiler they want when they +# do not want one of the pre-packaged Alire versions. diff --git a/testsuite/fixtures/system_toolchain_ubuntu/gp/gprbuild/gprbuild-external.toml b/testsuite/fixtures/system_toolchain_ubuntu/gp/gprbuild/gprbuild-external.toml new file mode 100644 index 00000000..8b7a2c98 --- /dev/null +++ b/testsuite/fixtures/system_toolchain_ubuntu/gp/gprbuild/gprbuild-external.toml @@ -0,0 +1,18 @@ +description = "The GPRBuild Ada/multilanguage build tool" +name = "gprbuild" + +maintainers = ["alejandro@mosteo.com"] +maintainers-logins = ["mosteo"] + +[[external]] +kind = "version-output" +version-regexp = "^GPRBUILD\\D*([\\d\\.-]+).*" +version-command = ["gprbuild", "--version"] + +# Neither macOS distribution (Homebrew, MacPorts) provides gprbuild. +[[external]] +kind = "system" +[external.origin.'case(os)'] +"freebsd" = ["gprbuild"] +"linux" = ["gprbuild"] +"windows" = ["gprbuild"] diff --git a/testsuite/fixtures/system_toolchain_ubuntu/index.toml b/testsuite/fixtures/system_toolchain_ubuntu/index.toml new file mode 100644 index 00000000..9a87888b --- /dev/null +++ b/testsuite/fixtures/system_toolchain_ubuntu/index.toml @@ -0,0 +1,4 @@ +# This index describes a mock GNAT toolchain, with native and cross compiler +# providing the virtual gnat crate. + +version = "1.1" diff --git a/testsuite/tests/dockerized/toolchain/install-from-system/test.py b/testsuite/tests/dockerized/toolchain/install-from-system/test.py new file mode 100644 index 00000000..e55de916 --- /dev/null +++ b/testsuite/tests/dockerized/toolchain/install-from-system/test.py @@ -0,0 +1,95 @@ +""" +Check that toolchains are correctly installed from the system as needed. Check +also that manually removed tools are reinstalled if still selected. Finally, +check that incomplete toolchain selection is reported whenever the missing tool +is not in PATH. +""" + +import json +from shutil import which +import subprocess +from drivers.alr import run_alr, set_default_user_settings, unselect_compiler, unselect_gprbuild +from drivers.asserts import assert_eq, assert_match, assert_substring + +INSTALL_TELLTALE = "The system package 'gprbuild' is about to be installed" + +def apt_uninstall(pkg:str, exe:str=""): + """ + Uninstall a package and verify that an executable, by default named as the + package, is no longer in PATH. + """ + real_exe = exe if exe != "" else pkg + subprocess.run(["sudo", "apt-get", "remove", "-y", pkg]).check_returncode() + assert which(real_exe) is None, f"Unexpected executable: {which(real_exe)}" + + +# Start by selecting the available external tools +run_alr("toolchain", "--select", "gnat_external", "gprbuild") + +tools = json.loads(run_alr("--format", "toolchain").out) +# Expected output: +# [ +# { +# "crate": "gprbuild", +# "version": "18.0.0", +# "status": "Default", +# "notes": "Detected at /usr/bin/gprbuild" +# }, +# { +# "crate": "gprbuild", +# "version": "2021.0.0+0778", +# "status": "Available", +# "notes": "Provided by system package: gprbuild" +# }, +# { +# "crate": "gnat_external", +# "version": "10.3.0", +# "status": "Default", +# "notes": "Detected at /usr/bin/gnat" +# } +# ] + +# Verify expected toolchain is selected +assert_eq(tools[2]["crate"], "gnat_external") +assert_eq(tools[2]["status"], "Default") +assert_eq(tools[0]["crate"], "gprbuild") +assert_eq(tools[0]["status"], "Default") + +# Remove gprbuild from the system and reselect, this should force gprbuild +# installation. We need to --force because the external gnat wants a likewise +# external gprbuild (instead of the system one). +apt_uninstall("gprbuild") +p = run_alr("--force", "toolchain", "--select", "gprbuild") +assert_substring(INSTALL_TELLTALE, p.out) + +# Remove gprbuild and try to initialize a crate. This should result in +# reinstallation, as a complete toolchain is needed when the crate solution is +# solved. +apt_uninstall("gprbuild") +p = run_alr("init", "--bin", "crate") +assert_substring(INSTALL_TELLTALE, p.out) + +# Unset selected gnat and gprbuild, and re-selecting gprbuild should properly +# reinstall gprbuild. No need to force in this case as no gnat is selected that +# requires a particular gprbuild. +unselect_compiler() +unselect_gprbuild() +apt_uninstall("gprbuild") +p = run_alr("toolchain", "--select", "gprbuild") +assert_substring(INSTALL_TELLTALE, p.out) + +# Finally, we remove both gnat and gprbuild and select gprbuild only. This +# should result in a warning that the toolchain is incomplete and no gnat can +# be found. Note that we cannot force a gnat installation, as there are no +# system package definitions for it (because several versions are available +# through system packages) and we don't want to force switches between them. +apt_uninstall("gnat-10", "gnat") # gnat-10 for our current Dockerfile +apt_uninstall("gprbuild") +p = run_alr("toolchain", "--select", "gprbuild", quiet=False) +assert_substring(INSTALL_TELLTALE, p.out) +assert_substring(""" +Warning: Unconfigured tool is not in path, builds will likely fail: gnat +Warning: Please ensure a complete toolchain is available with alr toolchain --select +""", p.out) + +print('SUCCESS') diff --git a/testsuite/tests/dockerized/toolchain/install-from-system/test.yaml b/testsuite/tests/dockerized/toolchain/install-from-system/test.yaml new file mode 100644 index 00000000..f6ea2798 --- /dev/null +++ b/testsuite/tests/dockerized/toolchain/install-from-system/test.yaml @@ -0,0 +1,5 @@ +driver: docker-wrapper +control: + - [SKIP, "skip_docker", "Docker disabled"] +indexes: + system_toolchain_ubuntu: {} -- 2.39.5