From 950b73433677e805af6b9bc5603608484df316c7 Mon Sep 17 00:00:00 2001 From: Alejandro R Mosteo Date: Fri, 19 Jun 2020 19:53:44 +0200 Subject: [PATCH] Allow incomplete solutions in relevant commands (#451) * Allow get with missing deps (with confirmation) Up to now, trying to retrieve a release with non-solvable dependencies resulted in an opaque "can't get a solution", leaving the only resort of doing an `alr get --only`. With this patch, the best partial solution found is used and displayed. The user can accept a partial deployment, understanding that the build will fail unless missing dependencies are manually provided. It would be relatively easy to allow the user to reject the proposal and continue seeing other incomplete solutions (as, e.g., `aptitude` does) until one of them is selected. * Allow pinning to unavailable versions The user may want for some reason to specify versions that are not in the index, for work-in-progress releases or when switching between several indexes. Instead of blocking him entirely, the incomplete solution is displayed and he can accept it regardless. Forcing is required when not interactive. * Allow update to complete with incomplete solution When the solution is already incomplete we emit a warning to remind the user about this, but we need not to err, since there are no changes to the solution, or we might find a less incomplete one. Also added a test for the behavior when trying to update a pinned version * Allow withing dependencies with incomplete solution This is the last of the commands that previously erred out with "Cannot solve dependencies". Now, it can be forced into accepting any dependency as long as the user is OK with having an incomplete solution. * Remove deprecated Alire.Solutions.Valid Instead, .Is_Complete or .Composition should be used for better granularity. Is_Complete is more stringent than before (hints make the solution incomplete), but the new flexibility to work with incomplete solutions makes the difference secondary; the user is less restricted than before. * Minor fixes to tests for Is_Complete change By making solutions with any external (including hints) incomplete, it is necessary to force a few commands in the tests that before succeeded normally. Some tests that checked for exact text were now failing due to the extra information/interaction caused by incomplete solutions. The stderr warning on incomplete solutions in setenv also requires fixing its output. * Document user-visible changes --- doc/user-changes.md | 13 +++ src/alire/alire-pinning.adb | 52 ++++++----- src/alire/alire-pinning.ads | 8 +- src/alire/alire-solutions-diffs.adb | 18 ++-- src/alire/alire-solutions-diffs.ads | 7 +- src/alire/alire-solutions.adb | 2 +- src/alire/alire-solutions.ads | 32 ++++--- src/alire/alire-solver.adb | 2 +- src/alr/alr-build_env.adb | 21 ++++- src/alr/alr-commands-get.adb | 55 ++++++++---- src/alr/alr-commands-pin.adb | 88 +++++++------------ src/alr/alr-commands-show.adb | 2 +- src/alr/alr-commands-update.adb | 55 ++++-------- src/alr/alr-commands-update.ads | 6 -- src/alr/alr-commands-user_input.adb | 6 +- src/alr/alr-commands-user_input.ads | 5 +- src/alr/alr-commands-withing.adb | 44 ++-------- testsuite/fixtures/solver_index/he/hello.toml | 40 +++++++++ testsuite/fixtures/solver_index/index.toml | 1 + .../fixtures/solver_index/li/libhello.toml | 17 ++++ testsuite/fixtures/solver_index/ma/make.toml | 9 ++ testsuite/tests/get/missing-deps/test.py | 28 ++++++ testsuite/tests/get/missing-deps/test.yaml | 3 + testsuite/tests/get/system-hint/test.py | 10 ++- testsuite/tests/index/external-hint/test.py | 12 ++- testsuite/tests/pin/missing-version/test.py | 44 ++++++++++ testsuite/tests/pin/missing-version/test.yaml | 3 + testsuite/tests/setenv/with-external/test.py | 6 +- testsuite/tests/update/missing-deps/test.py | 29 ++++++ testsuite/tests/update/missing-deps/test.yaml | 3 + testsuite/tests/update/pinned/test.py | 36 ++++++++ testsuite/tests/update/pinned/test.yaml | 3 + testsuite/tests/with/external/test.py | 2 +- testsuite/tests/with/missing-deps/test.py | 36 ++++++++ testsuite/tests/with/missing-deps/test.yaml | 3 + 35 files changed, 475 insertions(+), 226 deletions(-) create mode 100644 testsuite/fixtures/solver_index/he/hello.toml create mode 100644 testsuite/fixtures/solver_index/index.toml create mode 100644 testsuite/fixtures/solver_index/li/libhello.toml create mode 100644 testsuite/fixtures/solver_index/ma/make.toml create mode 100644 testsuite/tests/get/missing-deps/test.py create mode 100644 testsuite/tests/get/missing-deps/test.yaml create mode 100644 testsuite/tests/pin/missing-version/test.py create mode 100644 testsuite/tests/pin/missing-version/test.yaml create mode 100644 testsuite/tests/update/missing-deps/test.py create mode 100644 testsuite/tests/update/missing-deps/test.yaml create mode 100644 testsuite/tests/update/pinned/test.py create mode 100644 testsuite/tests/update/pinned/test.yaml create mode 100644 testsuite/tests/with/missing-deps/test.py create mode 100644 testsuite/tests/with/missing-deps/test.yaml diff --git a/doc/user-changes.md b/doc/user-changes.md index 31e3ddfc..8192e3bf 100644 --- a/doc/user-changes.md +++ b/doc/user-changes.md @@ -4,6 +4,19 @@ 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. +### Allow working with incomplete solutions + +PR [#447](https://github.com/alire-project/alire/pull/447). + +Before this patch, any change in dependences that resulted in an incomplete +solution caused a final "invalid solution" error. Now, any incomplete solution +will be presented to the user with details about the unfulfilled dependencies. +This solution can be accepted and worked with normally, although the user is +responsible to provide in the environment any missing project files. + +This change affects all commands that compute a dependency solution, i.e., +`get`, `pin`, `update`, `with`. + ### Use a directory to fulfill a dependency PR [#439](https://github.com/alire-project/alire/pull/439) diff --git a/src/alire/alire-pinning.adb b/src/alire/alire-pinning.adb index f3c7a100..9a860dca 100644 --- a/src/alire/alire-pinning.adb +++ b/src/alire/alire-pinning.adb @@ -17,36 +17,36 @@ package body Alire.Pinning is is -- We solve forcing the new version, while preemptively removing any -- previous pin for the same crate, since two pins to different versions - -- would be unsolvable. - - New_Solution : constant Solutions.Solution := - Solver.Resolve - (Conditional.New_Dependency (Crate, Version) and - Dependencies, - Environment, - Solution.Unpinning (Crate)); + -- would be unsolvable. A link is also preemptively removed to prevent + -- simultaneous pinning and linking. begin - -- If the solution is valid, we enable the pin for the given release - - if New_Solution.Valid then - return New_Solution.Pinning (Crate, Version); - else - return New_Solution; - end if; + return + Solver.Resolve + (Conditional.New_Dependency (Crate, Version) and Dependencies, + Environment, + Solution.Unpinning (Crate).Missing (Crate)) + .Pinning (Crate, Version); end Pin; ------------ -- Pin_To -- ------------ - function Pin_To (URL : String; - Solution : Solutions.Solution; - Crate : Crate_Name) + function Pin_To (Crate : Crate_Name; + URL : String; + Dependencies : Conditional.Dependencies; + Environment : Properties.Vector; + Solution : Solutions.Solution) return Solutions.Solution - is (Solution - .Unpinning (Crate) - .Linking (Crate, URL)); - -- Just in case it was already pinned to a version, we remove that + -- Just in case it was already pinned to a version, we remove that hidden + -- restriction, and re-solve so any old constraints in the dependencies + -- caused by the old pin disappear. + is (Solver.Resolve + (Dependencies, + Environment, + Solution + .Unpinning (Crate) + .Linking (Crate, URL))); ----------- -- Unpin -- @@ -66,11 +66,9 @@ package body Alire.Pinning is return Solver.Resolve (Dependencies, Environment, - Solutions.Solution' - (if Solution.State (Crate).Is_Linked - then Solution.Missing (Solution.Dependency (Crate)) - else Solution) - .Unpinning (Crate)); + Solution + .Missing (Crate) -- Clears any previous link + .Unpinning (Crate)); -- Clears any previous pin end Unpin; end Alire.Pinning; diff --git a/src/alire/alire-pinning.ads b/src/alire/alire-pinning.ads index c8820f3a..b74e239a 100644 --- a/src/alire/alire-pinning.ads +++ b/src/alire/alire-pinning.ads @@ -19,9 +19,11 @@ package Alire.Pinning is -- must exist in the solution. Root dependencies are given, and a previous -- solution with possibly more pins. The resulting solution may be invalid. - function Pin_To (URL : String; - Solution : Solutions.Solution; - Crate : Crate_Name) + function Pin_To (Crate : Crate_Name; + URL : String; + Dependencies : Conditional.Dependencies; + Environment : Properties.Vector; + Solution : Solutions.Solution) return Solutions.Solution with Pre => Solution.Depends_On (Crate) or else raise Checked_Error with diff --git a/src/alire/alire-solutions-diffs.adb b/src/alire/alire-solutions-diffs.adb index c67a89db..ea128f3c 100644 --- a/src/alire/alire-solutions-diffs.adb +++ b/src/alire/alire-solutions-diffs.adb @@ -22,7 +22,7 @@ package body Alire.Solutions.Diffs is when Linked => "file:" & (+Status.Path), when Hinted => Status.Versions.Image, when Unneeded => "unneeded", - when Unsolved => "unsolved"); + when Unsolved => Status.Versions.Image); ------------- -- Between -- @@ -51,7 +51,8 @@ package body Alire.Solutions.Diffs is Versions => Sol.Dependency (Crate).Versions); elsif Sol.Depends_On (Crate) then - return (Status => Unsolved); + return (Status => Unsolved, + Versions => Sol.Dependencies (Crate).Versions); else return (Status => Unneeded); @@ -130,6 +131,13 @@ package body Alire.Solutions.Diffs is (This.Former_Complete /= This.Latter_Complete or else (for some Change of This.Changes => Change.Former /= Change.Latter)); + ------------------------ + -- Latter_Is_Complete -- + ------------------------ + + function Latter_Is_Complete (This : Diff) return Boolean + is (This.Latter_Complete); + ------------------------ -- Pin_Change_Summary -- ------------------------ @@ -176,10 +184,10 @@ package body Alire.Solutions.Diffs is Trace.Log ("", Level); if not This.Latter_Complete then - Trace.Log (Prefix & "New solution is " & TTY.Warn ("invalid."), + Trace.Log (Prefix & "New solution is " & TTY.Warn ("incomplete."), Level); elsif This.Latter_Complete and then not This.Former_Complete then - Trace.Log (Prefix & "New solution is " & TTY.OK ("valid."), + Trace.Log (Prefix & "New solution is " & TTY.OK ("complete."), Level); end if; @@ -236,7 +244,7 @@ package body Alire.Solutions.Diffs is -- Show most precise version available - if Latter.Status in Hinted | Linked | Needed then + if Latter.Status in Unsolved | Hinted | Linked | Needed then Table.Append (TTY.Version (Best_Version (Latter))); else Table.Append (TTY.Version (Best_Version (Former))); diff --git a/src/alire/alire-solutions-diffs.ads b/src/alire/alire-solutions-diffs.ads index 7d1c5b9d..b1b67636 100644 --- a/src/alire/alire-solutions-diffs.ads +++ b/src/alire/alire-solutions-diffs.ads @@ -28,6 +28,9 @@ package Alire.Solutions.Diffs is function Contains_Changes (This : Diff) return Boolean; -- Says if there are, in fact, changes between both solutions + function Latter_Is_Complete (This : Diff) return Boolean; + -- Says if the new solution is complete + procedure Print (This : Diff; Changed_Only : Boolean; Prefix : String := " "; @@ -48,9 +51,9 @@ private Version : Semantic_Versioning.Version; when Linked => Path : UString; - when Hinted => + when Hinted | Unsolved => Versions : Semantic_Versioning.Extended.Version_Set; - when Unneeded | Unsolved => + when Unneeded => null; end case; end record; diff --git a/src/alire/alire-solutions.adb b/src/alire/alire-solutions.adb index 092c06d3..6e989eab 100644 --- a/src/alire/alire-solutions.adb +++ b/src/alire/alire-solutions.adb @@ -362,7 +362,7 @@ package body Alire.Solutions is Alire.Index.Crate (Dep.Crate) .Externals.Hints (Name => Dep.Crate, - Env => Alire.Properties.No_Properties) + Env => Env) loop Trace.Log (TTY.Emph (" Hint: ") & Hint, Level); end loop; diff --git a/src/alire/alire-solutions.ads b/src/alire/alire-solutions.ads index fa87d485..030034f0 100644 --- a/src/alire/alire-solutions.ads +++ b/src/alire/alire-solutions.ads @@ -128,6 +128,11 @@ package Alire.Solutions is return Solution; -- Add/merge dependency as missing in solution + function Missing (This : Solution; + Crate : Crate_Name) + return Solution; + -- Fulfill an existing dependency as missing, or do nothing otherwise + function Pinning (This : Solution; Crate : Crate_Name; Version : Semantic_Versioning.Version) @@ -231,17 +236,6 @@ package Alire.Solutions is with Pre => This.Depends_On (Crate); -- Returns the solving state of a dependency in the solution - function Valid (This : Solution) return Boolean - is (This.Composition <= Hints); - -- Transitional function to limit changes in this patch (Valid was - -- previously a discriminant of Solution). A follow-up patch removes - -- the use of validity all around when it's not strictly necessary. We - -- currently consider hints to result in a valid solution, although this - -- is not a guarantee of buildability. The follow-up makes this distinction - -- moot (the user is better informed about what is available, externally - -- needed, or outright missing. TODO: deprecate this function in favor of - -- Is_Complete. - -------------- -- Mutation -- -------------- @@ -395,7 +389,7 @@ private ----------------- function Is_Complete (This : Solution) return Boolean - is (This.Composition in Empty | Releases); + is (This.Composition <= Releases); ------------- -- Linking -- @@ -448,6 +442,20 @@ private Dependencies => This.Dependencies.Including (States.New_State (Dep).Missing))); + ------------- + -- Missing -- + ------------- + + function Missing (This : Solution; + Crate : Crate_Name) + return Solution + is (if This.Dependencies.Contains (Crate) + then (Solved => True, + Dependencies => + This.Dependencies.Including + (This.Dependencies (Crate).Missing)) + else This); + ------------- -- Pinning -- ------------- diff --git a/src/alire/alire-solver.adb b/src/alire/alire-solver.adb index 606ee892..be5838e8 100644 --- a/src/alire/alire-solver.adb +++ b/src/alire/alire-solver.adb @@ -103,7 +103,7 @@ package body Alire.Solver is Current : Solution; Options : Query_Options := Default_Options) return Boolean - is (Resolve (Deps, Props, Current, Options).Valid); + is (Resolve (Deps, Props, Current, Options).Is_Complete); ------------- -- Resolve -- diff --git a/src/alr/alr-build_env.adb b/src/alr/alr-build_env.adb index 67ceeca1..217d0eff 100644 --- a/src/alr/alr-build_env.adb +++ b/src/alr/alr-build_env.adb @@ -1,12 +1,15 @@ with Ada.Strings.Unbounded; with Ada.Text_IO; +with GNAT.IO; with GNAT.OS_Lib; +with Alire_Early_Elaboration; with Alire.GPR; with Alire.Properties.Scenarios; with Alire.Solutions; with Alire.Solver; +with Alire.Utils.TTY; with Alire.Utils; with Alr.OS_Lib; @@ -15,6 +18,7 @@ with Alr.Platform; package body Alr.Build_Env is package Query renames Alire.Solver; + package TTY renames Alire.Utils.TTY; type Env_Var_Action_Callback is access procedure (Key, Val : String); @@ -64,9 +68,20 @@ package body Alr.Build_Env is Full_Instance : Alire.Solutions.Release_Map; begin - if not Needed.Valid then - Trace.Error ("Cannot generate environment for invalid solution"); - raise Command_Failed; + if not Needed.Is_Complete then + Trace.Debug ("Generating incomplete environment" + & " because of missing dependencies"); + + -- Normally we would generate a warning, but since that will pollute + -- the output making it unusable, for once we write directly to + -- stderr (unless quiet is in effect): + + if not Alire_Early_Elaboration.Switch_Q then + GNAT.IO.Put_Line + (GNAT.IO.Standard_Error, + TTY.Warn ("warn:") & " Generating incomplete environment" + & " because of missing dependencies"); + end if; end if; -- GPR_PROJECT_PATH diff --git a/src/alr/alr-commands-get.adb b/src/alr/alr-commands-get.adb index 6dc5d9dc..176e943a 100644 --- a/src/alr/alr-commands-get.adb +++ b/src/alr/alr-commands-get.adb @@ -10,10 +10,10 @@ with Alire.Platforms; with Alire.Properties.Actions.Executor; with Alire.Solutions.Diffs; with Alire.Solver; +with Alire.Utils.User_Input; with Alire.Workspace; with Alr.Commands.Build; -with Alr.Commands.Update; with Alr.Platform; with Alr.Bootstrap; @@ -44,7 +44,8 @@ package body Alr.Commands.Get is Diff : Alire.Solutions.Diffs.Diff; -- Used to present dependencies to the user - Build_OK : Boolean; + Build_OK : Boolean := False; + Solution : Alire.Solutions.Solution; begin Trace.Detail ("Using " & Rel.Milestone.TTY_Image & " for requested " @@ -87,21 +88,32 @@ package body Alr.Commands.Get is if not Cmd.Only then declare - Solution : constant Alire.Solutions.Solution := - Query.Resolve - (Rel.Dependencies (Platform.Properties), - Platform.Properties, - Alire.Solutions.Empty_Valid_Solution); + use Alire.Utils.User_Input; begin - if Solution.Valid then - Diff := Alire.Solutions.Empty_Valid_Solution.Changes (Solution); - else - Trace.Error ("Could not resolve dependencies for: " & - Alire.Dependencies.New_Dependency - (Name, Versions).Image); - Trace.Error ("You can still retrieve the crate without " - & "dependencies with --only."); - raise Command_Failed; + Solution := Query.Resolve + (Rel.Dependencies (Platform.Properties), + Platform.Properties, + Alire.Solutions.Empty_Valid_Solution); + Diff := Alire.Solutions.New_Solution (Platform.Properties) + .Changes (Solution); + + if not Solution.Is_Complete then + Diff.Print (Changed_Only => False, + Level => Warning); + Trace.Warning (""); + Trace.Warning ("Could not find a complete solution for " + & Rel.Milestone.TTY_Image); + + if Alire.Utils.User_Input.Query + (Question => + "Build will fail unless externals are made available," + & " do you want to continue?", + Valid => (Yes | No => True, others => False), + Default => (if Alire.Force then Yes else No)) = No + then + Trace.Info ("Crate retrieval abandoned."); + raise Command_Failed; + end if; end if; end; end if; @@ -123,6 +135,7 @@ package body Alr.Commands.Get is -- dependencies can still occur, but these are outside of the -- retrieved crate and might be corrected manipulating dependencies -- and updating. + Root_Dir.Keep; end; @@ -137,7 +150,9 @@ package body Alr.Commands.Get is Guard : Folder_Guard (Enter_Folder (Rel.Unique_Folder)) with Unreferenced; begin - Commands.Update.Execute (Interactive => False); + Alire.Workspace.Deploy_Dependencies + (Env => Platform.Properties, + Solution => Solution); -- Execute the checked out release post_fetch actions, now that -- dependencies are in place @@ -157,7 +172,11 @@ package body Alr.Commands.Get is Trace.Info (""); - Trace.Log (Rel.Milestone.TTY_Image & " successfully retrieved" + Trace.Log (Rel.Milestone.TTY_Image + & " successfully retrieved" + & (if Solution.Is_Complete + then "" + else " with missing dependencies") & (if Cmd.Build then (if Build_OK then " and built." diff --git a/src/alr/alr-commands-pin.adb b/src/alr/alr-commands-pin.adb index 2e13b3c6..c02ecd16 100644 --- a/src/alr/alr-commands-pin.adb +++ b/src/alr/alr-commands-pin.adb @@ -1,11 +1,10 @@ with Alire.Dependencies; -with Alire.Lockfiles; with Alire.Releases; with Alire.Solutions.Diffs; with Alire.Pinning; with Alire.Utils.TTY; +with Alire.Workspace; -with Alr.Commands.Update; with Alr.Commands.User_Input; with Alr.Platform; with Alr.Root; @@ -40,25 +39,14 @@ package body Alr.Commands.Pin is Requires_Full_Index; - declare - New_Solution : constant Alire.Solutions.Solution := - Alire.Pinning.Pin - (Crate => Name, - Version => Version, - Dependencies => - Root.Current.Release.Dependencies, - Environment => Platform.Properties, - Solution => - Solution.Missing -- Remove a possible link - (Solution.Dependency (Name))); - begin - if New_Solution.Valid then - Solution := New_Solution; - else - Reportaise_Command_Failed - ("Cannot find a solution with the requested pin version"); - end if; - end; + Solution := Alire.Pinning.Pin + (Crate => Name, + Version => Version, + Dependencies => + Root.Current.Release.Dependencies, + Environment => Platform.Properties, + Solution => Solution); + end Pin; ----------- @@ -75,22 +63,12 @@ package body Alr.Commands.Pin is Requires_Full_Index; - declare - New_Solution : constant Alire.Solutions.Solution := - Alire.Pinning.Unpin - (Crate => Name, - Dependencies => - Root.Current.Release.Dependencies, - Environment => Platform.Properties, - Solution => Solution); - begin - if New_Solution.Valid then - Solution := New_Solution; - else - Reportaise_Command_Failed - ("Cannot find a solution without the pinned release"); - end if; - end; + Solution := Alire.Pinning.Unpin + (Crate => Name, + Dependencies => + Root.Current.Release.Dependencies, + Environment => Platform.Properties, + Solution => Solution); end Unpin; begin @@ -98,7 +76,7 @@ package body Alr.Commands.Pin is -- Sanity checks if not Solution.Depends_On (Name) then - Reportaise_Command_Failed ("Cannot pin crate not in dependencies: " + Reportaise_Command_Failed ("Cannot pin dependency not in solution: " & (+Name)); end if; @@ -114,11 +92,12 @@ package body Alr.Commands.Pin is Relaxed => False); Trace.Debug ("Pin requested for exact version: " & Version.Image); - elsif Solution.Releases.Contains (Name) then - Version := Solution.Releases.Element (Name).Version; + elsif Solution.State (Name).Is_Solved then + Version := Solution.State (Name).Release.Version; elsif not Cmd.Unpin then - Trace.Warning ("Cannot pin crate with no release" - & " in current solution: " & TTY.Name (Name)); + Trace.Warning ("An explicit version is required to pin a crate with" + & " no release in the current solution: " + & TTY.Name (Name)); return; end if; @@ -147,17 +126,10 @@ package body Alr.Commands.Pin is Old_Sol.Changes (New_Sol); begin if Diff.Contains_Changes then - if Commands.User_Input.Confirm_Solution_Changes - (Diff, Changed_Only => not Alire.Detailed) - then - Alire.Lockfiles.Write (Solution => New_Sol, - Filename => Root.Current.Lock_File); - - -- We force the update because we have just stored the new - -- solution, so Update won't detect any changes. - - Update.Execute (Interactive => False, - Force => True); + if Commands.User_Input.Confirm_Solution_Changes (Diff) then + Alire.Workspace.Deploy_Dependencies + (Env => Platform.Properties, + Solution => New_Sol); end if; else Trace.Info ("No changes to apply."); @@ -200,10 +172,6 @@ package body Alr.Commands.Pin is -- Change all pins - if not New_Sol.Valid then - Reportaise_Command_Failed ("Cannot pin an invalid solution"); - end if; - for Crate of New_Sol.Crates loop if New_Sol.State (Crate).Is_Pinned = Cmd.Unpin then Change_One_Pin (Cmd, New_Sol, +Crate); @@ -215,7 +183,11 @@ package body Alr.Commands.Pin is -- Pin to dir New_Sol := Alire.Pinning.Pin_To - (Cmd.URL.all, Old_Sol, +Argument (1)); + (+Argument (1), + Cmd.URL.all, + Root.Current.Release.Dependencies, + Platform.Properties, + Old_Sol); else diff --git a/src/alr/alr-commands-show.adb b/src/alr/alr-commands-show.adb index e5bf29e0..b202f611 100644 --- a/src/alr/alr-commands-show.adb +++ b/src/alr/alr-commands-show.adb @@ -73,7 +73,7 @@ package body Alr.Commands.Show is Platform.Properties, Cmd.Detail, Always); - if not Needed.Valid then + if not Needed.Is_Complete then Put_Line ("Dependencies cannot be met"); end if; end; diff --git a/src/alr/alr-commands-update.adb b/src/alr/alr-commands-update.adb index 57fa04bd..a1e55407 100644 --- a/src/alr/alr-commands-update.adb +++ b/src/alr/alr-commands-update.adb @@ -18,16 +18,14 @@ package body Alr.Commands.Update is -- Upgrade -- ------------- - procedure Upgrade (Interactive : Boolean; - Force : Boolean := False; - Allowed : Alire.Containers.Crate_Name_Sets.Set := + procedure Upgrade (Allowed : Alire.Containers.Crate_Name_Sets.Set := Alire.Containers.Crate_Name_Sets.Empty_Set) is Old : constant Query.Solution := Root.Current.Solution; begin - -- Ensure requested crates are in solution first + -- Ensure requested crates are in solution first. for Crate of Allowed loop if not Old.Depends_On (Crate) then @@ -36,7 +34,9 @@ package body Alr.Commands.Update is end if; if Old.Pins.Contains (Crate) then - Reportaise_Wrong_Arguments + -- The solver will never update a pinned crate, so we may allow + -- this to be attempted but it will have no effect. + Alire.Recoverable_Error ("Requested crate is pinned and cannot be updated: " & Alire.Utils.TTY.Name (Crate)); end if; @@ -51,34 +51,27 @@ package body Alr.Commands.Update is Allowed, Options => (Age => Query_Policy, others => <>)); - Diff : constant Alire.Solutions.Diffs.Diff := - Old.Changes (Needed); + Diff : constant Alire.Solutions.Diffs.Diff := Old.Changes (Needed); begin - if not Needed.Valid then - Reportaise_Command_Failed - ("Could not solve dependencies, update failed"); - end if; -- Early exit when there are no changes - if not Force and not Diff.Contains_Changes then - if Interactive then - Trace.Info ("Nothing to update."); + if not Alire.Force and not Diff.Contains_Changes then + if not Needed.Is_Complete then + Trace.Warning + ("There are missing dependencies" + & " (use `alr with --solve` for details)."); end if; + Trace.Info ("Nothing to update."); return; end if; -- Show changes and ask user to apply them - if Interactive then - if not User_Input.Confirm_Solution_Changes - (Diff, - Changed_Only => not Alire.Detailed) - then - Trace.Detail ("Update abandoned."); - return; - end if; + if not User_Input.Confirm_Solution_Changes (Diff) then + Trace.Detail ("Update abandoned."); + return; end if; -- Apply the update @@ -124,23 +117,7 @@ package body Alr.Commands.Update is Index.Update_All; end if; - Upgrade (Interactive => True, - Force => False, - Allowed => Parse_Allowed); - end Execute; - - ------------- - -- Execute -- - ------------- - - procedure Execute (Interactive : Boolean; - Force : Boolean := False) - is - begin - Requires_Valid_Session; - - Upgrade (Interactive => Interactive, - Force => Force); + Upgrade (Allowed => Parse_Allowed); end Execute; ---------------------- diff --git a/src/alr/alr-commands-update.ads b/src/alr/alr-commands-update.ads index eabce87c..3da46451 100644 --- a/src/alr/alr-commands-update.ads +++ b/src/alr/alr-commands-update.ads @@ -22,12 +22,6 @@ package Alr.Commands.Update is function Usage_Custom_Parameters (Cmd : Command) return String is ("[crate]..."); - procedure Execute (Interactive : Boolean; - Force : Boolean := False); - -- Interactive serves to flag that the update is requested from somewhere - -- else within Alire, and is already confirmed by the user. So, when False, - -- not output of differences or confirmation will be presented. - private type Command is new Commands.Command with record diff --git a/src/alr/alr-commands-user_input.adb b/src/alr/alr-commands-user_input.adb index 9fd7d438..2c4ddf5e 100644 --- a/src/alr/alr-commands-user_input.adb +++ b/src/alr/alr-commands-user_input.adb @@ -8,7 +8,7 @@ package body Alr.Commands.User_Input is function Confirm_Solution_Changes (Changes : Alire.Solutions.Diffs.Diff; - Changed_Only : Boolean; + Changed_Only : Boolean := not Alire.Detailed; Level : Alire.Trace.Levels := Info) return Boolean is @@ -32,7 +32,9 @@ package body Alr.Commands.User_Input is (Question => "Do you want to proceed?", Valid => (Yes | No => True, others => False), - Default => Yes) = Yes; + Default => (if Changes.Latter_Is_Complete or else Alire.Force + then Yes + else No)) = Yes; end Confirm_Solution_Changes; end Alr.Commands.User_Input; diff --git a/src/alr/alr-commands-user_input.ads b/src/alr/alr-commands-user_input.ads index a63586e5..ea43075d 100644 --- a/src/alr/alr-commands-user_input.ads +++ b/src/alr/alr-commands-user_input.ads @@ -6,10 +6,11 @@ package Alr.Commands.User_Input is function Confirm_Solution_Changes (Changes : Alire.Solutions.Diffs.Diff; - Changed_Only : Boolean; + Changed_Only : Boolean := not Alire.Detailed; Level : Alire.Trace.Levels := Info) return Boolean; -- Present a summary of changes and ask the user for confirmation. Returns - -- True when the user answers positively. + -- True when the user answers positively. Defaults to Yes when the new + -- solution is complete, or when Alire.Force. end Alr.Commands.User_Input; diff --git a/src/alr/alr-commands-withing.adb b/src/alr/alr-commands-withing.adb index 47c4fd05..6eeb9cfc 100644 --- a/src/alr/alr-commands-withing.adb +++ b/src/alr/alr-commands-withing.adb @@ -25,8 +25,6 @@ package body Alr.Commands.Withing is Switch_URL : constant String := "--use"; - package Query renames Alire.Solver; - procedure Replace_Current (Old_Deps, New_Deps : Alire.Conditional.Dependencies; @@ -36,8 +34,8 @@ package body Alr.Commands.Withing is -- Add -- --------- - function Add (Deps : Alire.Conditional.Dependencies; - New_Dep : String) + function Add (Deps : Alire.Conditional.Dependencies; + New_Dep : String) return Alire.Conditional.Dependencies is use all type Alire.Conditional.Dependencies; @@ -48,8 +46,8 @@ package body Alr.Commands.Withing is -- Check that the requested dependency exists if not Alire.Index.Exists (Requested.Crate) then - Reportaise_Command_Failed - ("The requested crate was not found in the catalog: " & + Trace.Warning + ("The requested crate does not exist in the catalog: " & (+Requested.Crate)); end if; @@ -64,23 +62,11 @@ package body Alr.Commands.Withing is end if; end loop; - -- Merge the dependency and ensure there is a solution - - return Result : constant Alire.Conditional.Dependencies := - Deps and Alire.Conditional.New_Dependency (Requested.Crate, - Requested.Versions) - do - if not Query.Is_Resolvable (Result.Evaluate (Platform.Properties), - Platform.Properties, - Root.Current.Solution) - then - Reportaise_Command_Failed ("Adding " & New_Dep & - " has no dependency solution"); - else - Trace.Detail ("Dependency " & New_Dep & " can be added"); - end if; + -- Merge the dependency. Completeness of the solution will be presented + -- as a whole after all changes have been processed, in Replace_Current. - end return; + return Deps and Alire.Conditional.New_Dependency (Requested.Crate, + Requested.Versions); end Add; ------------------ @@ -98,11 +84,6 @@ package body Alr.Commands.Withing is ("Exactly one crate needed for external pinning."); end if; - if not Root.Current.Solution.Valid then - Reportaise_Command_Failed - ("Cannot add pinned crates to already unsolvable dependencies"); - end if; - declare use Alire; use type Conditional.Dependencies; @@ -204,15 +185,6 @@ package body Alr.Commands.Withing is Trace.Info (""); Alire.Dependencies.Diffs.Between (Old_Deps, New_Deps).Print; - -- In the event of a new invalid solution (this should not happen, - -- but as a safeguard we ensure it cannot be committed to disk) bail - -- out already. - - if not New_Solution.Valid then - Reportaise_Command_Failed - ("No solution for the requested changes"); - end if; - -- Show the effects on the solution if not User_Input.Confirm_Solution_Changes diff --git a/testsuite/fixtures/solver_index/he/hello.toml b/testsuite/fixtures/solver_index/he/hello.toml new file mode 100644 index 00000000..aac32a5e --- /dev/null +++ b/testsuite/fixtures/solver_index/he/hello.toml @@ -0,0 +1,40 @@ +[general] +description = """"Hello, world!" demonstration project""" +licenses = ["GPL 3.0", "MIT"] +website = "example.com" +maintainers = ["bob@example.com"] +maintainers-logins = ["mylogin"] +authors = ["Bob", "Alice"] + + +['4.0'] # with missing dependencies + missing external +origin = "file://." + + ['4.0'.depends-on] + libhello = "^4.0" + make = "*" + +['3.0'] # with missing dependencies +origin = "file://." + + ['3.0'.depends-on] + libhello = "^3.0" + +['2.0'] # with regular and unavailable external (hint) +origin = "file://." + + ['2.0'.depends-on] + libhello = "^2.0" + make = "*" + +['1.0.1'] # with plain dependencies +origin = "file://." + + ['1.0.1'.depends-on] + libhello = "^1.0" + +['1.0.0'] +origin = "file://." + + ['1.0.0'.depends-on] + libhello = "^1.0" diff --git a/testsuite/fixtures/solver_index/index.toml b/testsuite/fixtures/solver_index/index.toml new file mode 100644 index 00000000..7c969026 --- /dev/null +++ b/testsuite/fixtures/solver_index/index.toml @@ -0,0 +1 @@ +version = "0.2" diff --git a/testsuite/fixtures/solver_index/li/libhello.toml b/testsuite/fixtures/solver_index/li/libhello.toml new file mode 100644 index 00000000..c41322f9 --- /dev/null +++ b/testsuite/fixtures/solver_index/li/libhello.toml @@ -0,0 +1,17 @@ +[general] +description = """"Hello, world!" demonstration project support library""" +licenses = [] +maintainers = ["john@doe.com"] +maintainers-logins = ["mylogin"] + +['2.0'] +origin = "file://." + +['1.1'] +origin = "file://." + +['1.0.1'] +origin = "file://." + +['1.0'] +origin = "file://." diff --git a/testsuite/fixtures/solver_index/ma/make.toml b/testsuite/fixtures/solver_index/ma/make.toml new file mode 100644 index 00000000..5b2629c4 --- /dev/null +++ b/testsuite/fixtures/solver_index/ma/make.toml @@ -0,0 +1,9 @@ +[general] +description = "Utility for directing compilation" +maintainers = ["alejandro@mosteo.com"] +maintainers-logins = ["mylogin"] +licenses = [] + +[[external]] +kind = "system" +origin = [] # No package name since we require it unavailable in tests diff --git a/testsuite/tests/get/missing-deps/test.py b/testsuite/tests/get/missing-deps/test.py new file mode 100644 index 00000000..26528317 --- /dev/null +++ b/testsuite/tests/get/missing-deps/test.py @@ -0,0 +1,28 @@ +""" +Retrieve a crate with a partial solution available +""" + +import re +import os + +from drivers.alr import run_alr +from drivers.asserts import assert_match +from glob import glob + + +# Get the "hello" crate with version 3, which has missing libhello=3 dep +run_alr('get', 'hello=3', '--force') +os.chdir(glob('hello*')[0]) + +# Check missing dependency is shown as external +p = run_alr('with', '--solve') +assert_match('.*Dependencies \(external\):\n' + ' libhello\^3\.0.*', + p.out, flags=re.S) + +# Double-check that build fails +p = run_alr('build', complain_on_error=False) +assert p.status != 0, "Build should have failed" + + +print('SUCCESS') diff --git a/testsuite/tests/get/missing-deps/test.yaml b/testsuite/tests/get/missing-deps/test.yaml new file mode 100644 index 00000000..476e709f --- /dev/null +++ b/testsuite/tests/get/missing-deps/test.yaml @@ -0,0 +1,3 @@ +driver: python-script +indexes: + solver_index: {} diff --git a/testsuite/tests/get/system-hint/test.py b/testsuite/tests/get/system-hint/test.py index e3733c3a..aef8ecf0 100644 --- a/testsuite/tests/get/system-hint/test.py +++ b/testsuite/tests/get/system-hint/test.py @@ -9,12 +9,16 @@ from drivers.asserts import assert_match import re -p = run_alr('get', 'libhello=0.9-test_unav_native', +p = run_alr('get', 'libhello=0.9-test_unav_native', '--force', complain_on_error=True, quiet=False) -assert_match('Warning: The following external dependencies are unavailable within Alire:\n' +assert_match('.*' # Skip user interaction and solution changes + 'Warning: The following external dependencies' + ' are unavailable within Alire:\n' 'Warning: make\*\n' - 'Warning: They should be made available in the environment by the user.\n', + 'Warning: They should be made available in the' + ' environment by the user.\n' + '.*', # Skip final solution summary p.out, flags=re.S) print('SUCCESS') diff --git a/testsuite/tests/index/external-hint/test.py b/testsuite/tests/index/external-hint/test.py index 9e25c437..828667e8 100644 --- a/testsuite/tests/index/external-hint/test.py +++ b/testsuite/tests/index/external-hint/test.py @@ -25,10 +25,11 @@ else: # 2nd test: hint is displayed when the hint belongs to a dependency, on get -p = run_alr('get', 'crate_master', quiet=False) +p = run_alr('get', 'crate_master', '--force', quiet=False) assert_match -("Warning: The following native dependencies are unavailable within Alire:\n" +(".*" # Skip previous user interaction and warning about incomplete solution + "Warning: The following native dependencies are unavailable within Alire:\n" "Warning: crate\*\n" "Warning: Hint: This is a custom hint\n" "Warning: They should be made available in the environment by the user.\n", @@ -38,9 +39,12 @@ assert_match p = run_alr('show', 'crate_master', '--solve', '--system', quiet=False) -assert_match(".*Dependencies \(external\):\n" +assert_match(".*" # Skip previous crate info + "Dependencies \(external\):\n" " crate\* \(direct,hinted\)\n" - " Hint: This is a custom hint\n.*", + " Hint: This is a custom hint\n" + "Dependencies \(graph\):\n" + ".*", # Skip solution graph p.out, flags=re.S) print('SUCCESS') diff --git a/testsuite/tests/pin/missing-version/test.py b/testsuite/tests/pin/missing-version/test.py new file mode 100644 index 00000000..df7c654a --- /dev/null +++ b/testsuite/tests/pin/missing-version/test.py @@ -0,0 +1,44 @@ +""" +Pin forcibly a dependencies that cause missing dependencies +""" + +import re +import os + +from drivers.alr import run_alr +from drivers.asserts import assert_match +from glob import glob + + +# Initialize a new crate and add the "hello*" dependency. This is solved as: +# xxx=0.0.0 -> hello=1.0.1 --> libhello=1.1.0 +run_alr('init', '--bin', 'xxx') +os.chdir('xxx') +run_alr('with', 'hello') + +# 1st test: pin to an existing version that brings in missing dependencies. +# Pinning hello=3 brings in a libhello^3 dependency that is unavailable, so: +# xxx=0.0.0 -> hello=3.0.0 --> libhello^3 (missing) +run_alr('pin', '--force', 'hello=3') + +# Check solution is as expected +p = run_alr('with', '--solve') +assert_match('.*Dependencies \(solution\):\n' + ' hello=3\.0\.0 \(pinned\).*\n' # skip irrelevant origin info + '.*Dependencies \(external\):\n' + ' libhello\^3\.0.*', + p.out, flags=re.S) + +# 2nd test: directly pin to a missing version (hello=5). This causes libhello +# to disappear from the solution, since hello's dependencies are now unknown: +# xxx=0.0.0 -> hello=5 (missing) +run_alr('pin', '--force', 'hello=5') + +# Check solution is as expected +p = run_alr('with', '--solve') +assert_match('.*Dependencies \(external\):\n' + ' hello=5\.0\.0.*', + p.out, flags=re.S) + + +print('SUCCESS') diff --git a/testsuite/tests/pin/missing-version/test.yaml b/testsuite/tests/pin/missing-version/test.yaml new file mode 100644 index 00000000..476e709f --- /dev/null +++ b/testsuite/tests/pin/missing-version/test.yaml @@ -0,0 +1,3 @@ +driver: python-script +indexes: + solver_index: {} diff --git a/testsuite/tests/setenv/with-external/test.py b/testsuite/tests/setenv/with-external/test.py index c8d75f5d..81178c8f 100644 --- a/testsuite/tests/setenv/with-external/test.py +++ b/testsuite/tests/setenv/with-external/test.py @@ -13,7 +13,7 @@ import platform # Retrieve a crate with a external dependency -run_alr('get', 'libhello=0.9-test_unav_native') +run_alr('get', 'libhello=0.9-test_unav_native', '--force') os.chdir('libhello_0.9.0_filesystem') # Run it not quietly to ensure that at normal level @@ -22,7 +22,9 @@ p = run_alr('setenv', quiet=False) assert_eq(0, p.status) # Check the setenv output -assert_match('export GPR_PROJECT_PATH=""\n' +assert_match('warn: Generating incomplete environment' # Note: this warning is + ' because of missing dependencies\n' # via stderr so it's OK + 'export GPR_PROJECT_PATH=""\n' 'export ALIRE="True"\n', p.out) diff --git a/testsuite/tests/update/missing-deps/test.py b/testsuite/tests/update/missing-deps/test.py new file mode 100644 index 00000000..8100892a --- /dev/null +++ b/testsuite/tests/update/missing-deps/test.py @@ -0,0 +1,29 @@ +""" +Check that updating an incomplete solution is doable resulting in no changes +""" + +import re +import os + +from drivers.alr import run_alr +from drivers.asserts import assert_match +from glob import glob + + +# Add a dependency and force it missing by pinning it to non-existing version +run_alr('init', '--bin', 'xxx') +os.chdir('xxx') +run_alr('with', 'libhello') +run_alr('pin', '--force', 'libhello=3') + +# See that updating succeeds +run_alr('update') + +# Check that the solution is still the expected one +p = run_alr('with', '--solve') +assert_match('.*Dependencies \(external\):\n' + ' libhello=3.0.0.*', + p.out, flags=re.S) + + +print('SUCCESS') diff --git a/testsuite/tests/update/missing-deps/test.yaml b/testsuite/tests/update/missing-deps/test.yaml new file mode 100644 index 00000000..476e709f --- /dev/null +++ b/testsuite/tests/update/missing-deps/test.yaml @@ -0,0 +1,3 @@ +driver: python-script +indexes: + solver_index: {} diff --git a/testsuite/tests/update/pinned/test.py b/testsuite/tests/update/pinned/test.py new file mode 100644 index 00000000..e5b794fa --- /dev/null +++ b/testsuite/tests/update/pinned/test.py @@ -0,0 +1,36 @@ +""" +Check that updating a pinned crate results in a recoverable error +""" + +import re +import os + +from drivers.alr import run_alr +from drivers.asserts import assert_match +from glob import glob + + +# Add a dependency and force it missing by pinning it to non-existing version +run_alr('init', '--bin', 'xxx') +os.chdir('xxx') +run_alr('with', 'libhello') # This causes libhello=1.1 +run_alr('pin', 'libhello=1') # Downgrade to 1.0 + +# Check that updating without specific crate does not err +run_alr('update') + +# See that updating the pinned crate errs +p = run_alr('update', 'libhello', complain_on_error=False) +assert p.status != 0, "should have erred" + +# Check that force updating the pinned crate does not err +run_alr('update', '--force', 'libhello') + +# Check that the solution is still the expected one +p = run_alr('with', '--solve') +assert_match('.*Dependencies \(solution\):\n' + ' libhello=1.0.0 \(pinned\).*', + p.out, flags=re.S) + + +print('SUCCESS') diff --git a/testsuite/tests/update/pinned/test.yaml b/testsuite/tests/update/pinned/test.yaml new file mode 100644 index 00000000..476e709f --- /dev/null +++ b/testsuite/tests/update/pinned/test.yaml @@ -0,0 +1,3 @@ +driver: python-script +indexes: + solver_index: {} diff --git a/testsuite/tests/with/external/test.py b/testsuite/tests/with/external/test.py index 6ead3123..b455e481 100644 --- a/testsuite/tests/with/external/test.py +++ b/testsuite/tests/with/external/test.py @@ -16,7 +16,7 @@ run_alr('init', '--bin', 'xxx') os.chdir('xxx') # Add a dependency on 'make', defined in the index as only a hint -run_alr('with', 'make') +run_alr('with', 'make', '--force') # Verify that it appears in the solution as unavailable external p = run_alr('with', '--solve') diff --git a/testsuite/tests/with/missing-deps/test.py b/testsuite/tests/with/missing-deps/test.py new file mode 100644 index 00000000..baa22209 --- /dev/null +++ b/testsuite/tests/with/missing-deps/test.py @@ -0,0 +1,36 @@ +""" +Check that `with` works with missing dependencies +""" + +import re +import os + +from drivers.alr import run_alr +from drivers.asserts import assert_match +from glob import glob + + +# Initialize test crate +run_alr('init', '--bin', 'xxx') +os.chdir('xxx') + +# 1st test, adding an entirely inexistent crate +run_alr('with', 'unobtanium', '--force') + +# 2nd test, adding a dependency that exists but with missing version +run_alr('with', 'libhello^3', '--force') + +# 3rd test, adding a dependency that has missing dependencies +run_alr('with', 'hello^3', '--force') + +# Check that the solution contains the requested dependencies +p = run_alr('with', '--solve') +assert_match('.*Dependencies \(solution\):\n' + ' hello=3\.0\.0.*' # skip origin + 'Dependencies \(external\):\n' + ' libhello\^3.*' # skip flags + ' unobtanium\*.*', + p.out, flags=re.S) + + +print('SUCCESS') diff --git a/testsuite/tests/with/missing-deps/test.yaml b/testsuite/tests/with/missing-deps/test.yaml new file mode 100644 index 00000000..476e709f --- /dev/null +++ b/testsuite/tests/with/missing-deps/test.yaml @@ -0,0 +1,3 @@ +driver: python-script +indexes: + solver_index: {} -- 2.39.5