From 676da56eeeec6676c5200529d05d6913c74b9a80 Mon Sep 17 00:00:00 2001 From: Alejandro R Mosteo Date: Mon, 6 Jul 2020 19:38:06 +0200 Subject: [PATCH] Move ASCII graph to `--graph` switch, and add a `--tree` alternative & fallback (#465) * New `alr with --tree`, `alr show --tree` switch Prints dependency graph in tree form, as an alternative to the ascii-art plots that 1) not always will be available, depending in libgraph-easy-perl and 2) quickly become big and broken. * Move ASCII graph to dedicated --graph switch Previously, it was always attempted to be shown with `--solve`, which could be an unnecessary pollution for an otherwise useful command. * Update user-changes document --- doc/user-changes.md | 19 +++ src/alire/alire-solutions.adb | 145 ++++++++++++++++++--- src/alire/alire-solutions.ads | 13 ++ src/alire/alire-utils-tools.adb | 64 ++++++--- src/alire/alire-utils-tools.ads | 11 +- src/alr/alr-commands-show.adb | 42 +++++- src/alr/alr-commands-show.ads | 5 +- src/alr/alr-commands-withing.adb | 27 +++- src/alr/alr-commands-withing.ads | 5 +- testsuite/tests/with/tree-switch/test.py | 38 ++++++ testsuite/tests/with/tree-switch/test.yaml | 3 + 11 files changed, 319 insertions(+), 53 deletions(-) create mode 100644 testsuite/tests/with/tree-switch/test.py create mode 100644 testsuite/tests/with/tree-switch/test.yaml diff --git a/doc/user-changes.md b/doc/user-changes.md index 31bd9d60..3c6f218e 100644 --- a/doc/user-changes.md +++ b/doc/user-changes.md @@ -4,6 +4,25 @@ 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. +### New `alr with --graph` and `alr with --tree` switches + +PR [#465](https://github.com/alire-project/alire/pull/465). + +The ASCII art dependency graph generated with `graph-easy`, that was printed at +the end of `alr with --solve` output, is moved to its own `alr with --graph` +switch. A fallback tree visualization is generated when `graph-easy` is +unavailable. This new tree visualization can also be obtained with `alr with +--tree`: +``` +my_project=0.0.0 +├── hello=1.0.1 (^1) +│ └── libhello=1.0.1 (^1.0) +├── superhello=1.0.0 (*) +│ └── libhello=1.0.1 (~1.0) +├── unobtanium* (direct,missed) (*) +└── wip* (direct,linked,pin=/fake) (*) +``` + ### Automatically 'with' GPR project files from dependencies PR [#458](https://github.com/alire-project/alire/pull/458). diff --git a/src/alire/alire-solutions.adb b/src/alire/alire-solutions.adb index 3f9e573c..ff43eb71 100644 --- a/src/alire/alire-solutions.adb +++ b/src/alire/alire-solutions.adb @@ -1,13 +1,13 @@ with Ada.Containers; with Alire.Crates.With_Releases; +with Alire.Dependencies.Containers; with Alire.Dependencies.Graphs; with Alire.Index; -with Alire.OS_Lib.Subprocess; -with Alire.Paths; with Alire.Root; with Alire.Solutions.Diffs; with Alire.Utils.Tables; +with Alire.Utils.Tools; with Alire.Utils.TTY; with Semantic_Versioning; @@ -283,14 +283,6 @@ package body Alire.Solutions is end case; end Is_Better; - ---------------------------------- - -- Libgraph_Easy_Perl_Installed -- - ---------------------------------- - - function Libgraph_Easy_Perl_Installed return Boolean - is (OS_Lib.Subprocess.Locate_In_Path (Paths.Scripts_Graph_Easy) /= ""); - -- Return whether libgraph_easy_perl_install is in path - ------------------ -- New_Solution -- ------------------ @@ -425,21 +417,41 @@ package body Alire.Solutions is .From_Solution (With_Root, Env); begin Graph.Print (With_Root, Prefix => " "); + end; + end if; + end Print; - -- Optional graphical if possible. TODO: remove this warning once - -- show once. + ----------------- + -- Print_Graph -- + ----------------- - if Libgraph_Easy_Perl_Installed then + procedure Print_Graph (This : Solution; + Root : Alire.Releases.Release; + Env : Properties.Vector) + is + begin + if This.Dependencies.Is_Empty then + Trace.Always ("There are no dependencies."); + else + Utils.Tools.Check_Tool (Utils.Tools.Easy_Graph, Fail => False); + + if Utils.Tools.Available (Utils.Tools.Easy_Graph) then + declare + With_Root : constant Solution := + This.Including + (Root, Env, Add_Dependency => True); + Graph : constant Alire.Dependencies.Graphs.Graph := + Alire.Dependencies.Graphs + .From_Solution (With_Root, Env); + begin Graph.Plot (With_Root); - else - Trace.Log ("Cannot display graphical graph: " & - Paths.Scripts_Graph_Easy & " not in path" & - " (usually packaged as libgraph_easy_perl).", - Level); - end if; - end; + end; + else + Trace.Info ("Defaulting to tree view."); + This.Print_Tree (Root); + end if; end if; - end Print; + end Print_Graph; ----------------- -- Print_Hints -- @@ -497,6 +509,97 @@ package body Alire.Solutions is end if; end Print_Pins; + ---------------- + -- Print_Tree -- + ---------------- + + procedure Print_Tree (This : Solution; + Root : Alire.Releases.Release; + Prefix : String := ""; + Print_Root : Boolean := True) + is + + Mid_Node : constant String := + (if TTY.Color_Enabled then "├── " else "+-- "); + Last_Node : constant String := + (if TTY.Color_Enabled then "└── " else "+-- "); + Branch : constant String := + (if TTY.Color_Enabled then "│ " else "| "); + No_Branch : constant String := " "; + + procedure Print (Deps : Dependencies.Containers.List; + Prefix : String := ""; + Omit : Boolean := False) + -- Omit is used to remove the top-level connectors, for when the tree + -- is printed without the root release. + is + Last : UString; + -- Used to store the last dependency name in a subtree, to be able to + -- use the proper ASCII connector. See just below. + begin + + -- Find last printable dependency. This is related to OR trees, that + -- might cause the last in the enumeration to not really belong to + -- the solution. + + for Dep of Deps loop + if This.Depends_On (Dep.Crate) then + Last := +(+Dep.Crate); + end if; + end loop; + + -- Print each dependency for real + + for Dep of Deps loop + if This.Depends_On (Dep.Crate) then + Trace.Always + (Prefix + -- The prefix is the possible "|" connectors from upper tree + -- levels. + + -- Print the appropriate final connector for the node + & (if Omit -- top-level, no prefix + then "" + else (if +Dep.Crate = +Last + then Last_Node -- A └── connector + else Mid_Node)) -- A ├── connector + + -- For a dependency solved by a release, print exact + -- version. Otherwise print the state of the dependency. + & (if This.State (Dep.Crate).Has_Release + then This.State (Dep.Crate).Release.Milestone.TTY_Image + else This.State (Dep.Crate).TTY_Image) + + -- And dependency that introduces the crate in the solution + & " (" & TTY.Emph (Dep.Versions.Image) & ")"); + + -- Recurse for further releases + + if This.State (Dep.Crate).Has_Release then + Print (Conditional.Enumerate + (This.State (Dep.Crate).Release.Dependencies), + Prefix => + Prefix + -- Indent adding the proper running connector + & (if Omit + then "" + else (if +Dep.Crate = +Last + then No_Branch -- End of this connector + else Branch))); -- "│" over the subtree + end if; + end if; + end loop; + end Print; + + begin + if Print_Root then + Trace.Always (Prefix & Root.Milestone.TTY_Image); + end if; + Print (Conditional.Enumerate (Root.Dependencies), + Prefix, + not Print_Root); + end Print_Tree; + -------------- -- Releases -- -------------- diff --git a/src/alire/alire-solutions.ads b/src/alire/alire-solutions.ads index 4faa91c2..9d2842f7 100644 --- a/src/alire/alire-solutions.ads +++ b/src/alire/alire-solutions.ads @@ -270,6 +270,12 @@ package Alire.Solutions is -- crate not in solution that introduces the direct dependencies. When -- Detailed, extra information about origins is shown. + procedure Print_Graph (This : Solution; + Root : Alire.Releases.Release; + Env : Properties.Vector); + -- Print an ASCII graph of dependencies using libgraph-easy-perl, if + -- installed, or default to Print_Tree. + procedure Print_Hints (This : Solution; Env : Properties.Vector); -- Display hints about any undetected externals in the solutions @@ -277,6 +283,13 @@ package Alire.Solutions is procedure Print_Pins (This : Solution); -- Dump a table with pins in this solution + procedure Print_Tree (This : Solution; + Root : Alire.Releases.Release; + Prefix : String := ""; + Print_Root : Boolean := True); + -- Print the solution in tree form. If Print_Root, Root is printed too; + -- otherwise the tree is a forest starting at Root direct dependencies. + ----------------- -- Persistence -- ----------------- diff --git a/src/alire/alire-utils-tools.adb b/src/alire/alire-utils-tools.adb index 579e97e4..4aa9e851 100644 --- a/src/alire/alire-utils-tools.adb +++ b/src/alire/alire-utils-tools.adb @@ -9,12 +9,33 @@ package body Alire.Utils.Tools is Already_Detected : array (Tool_Kind) of Boolean := (others => False); + function Exec_For_Tool (Tool : Tool_Kind) return String; + + --------------- + -- Available -- + --------------- + + function Available (Tool : Tool_Kind) return Boolean is + begin + if Already_Detected (Tool) then + return True; + end if; + + if Locate_In_Path (Exec_For_Tool (Tool)) /= "" then + -- The tool is available + Already_Detected (Tool) := True; + end if; + + return Already_Detected (Tool); + end Available; + ------------------- -- Exec_For_Tool -- ------------------- function Exec_For_Tool (Tool : Tool_Kind) return String is (case Tool is + when Easy_Graph => "graph-easy", when Git => "git", when Tar => "tar", when Unzip => "unzip", @@ -38,8 +59,12 @@ package body Alire.Utils.Tools is when Msys2 | Debian | Ubuntu => return (case Tool is + when Easy_Graph => + (if Distribution /= Msys2 + then "libgraph-easy-perl" + else ""), when Git | Tar | Unzip | Curl => Exec_For_Tool (Tool), - when Mercurial => "mercurial", + when Mercurial => "mercurial", when Subversion => "subversion"); end case; end System_Package_For_Tool; @@ -48,7 +73,7 @@ package body Alire.Utils.Tools is -- Install_From_Distrib -- -------------------------- - procedure Install_From_Distrib (Tool : Tool_Kind) is + procedure Install_From_Distrib (Tool : Tool_Kind; Fail : Boolean) is use Utils.User_Input; Pck : constant String := System_Package_For_Tool (Tool); @@ -87,16 +112,27 @@ package body Alire.Utils.Tools is end; else -- Error when user rejected installation - Trace.Error ("Cannot proceed."); - Trace.Error ("Please install the tool and retry."); + if Fail then + Trace.Error ("Cannot proceed."); + Trace.Error ("Please install the tool and retry."); + else + Trace.Info ("Tool not installed."); + return; + end if; end if; else -- Error when Alire doesn't know how to install (unknown distro or -- tool not available in distro). - Trace.Error ("Cannot proceed."); - Trace.Error ("Alire is not able to install required tool: '" & - Tool'Img & "'"); - Trace.Error ("Please install the tool and retry."); + if Fail then + Trace.Error ("Cannot proceed."); + Trace.Error ("Alire is not able to install required tool: '" & + Tool'Img & "'"); + Trace.Error ("Please install the tool and retry."); + else + Trace.Warning ("Alire is not able to install tool: '" & + Tool'Img & "'"); + return; + end if; end if; OS_Lib.Bailout (1); @@ -106,22 +142,16 @@ package body Alire.Utils.Tools is -- Check_Tool -- ---------------- - procedure Check_Tool (Tool : Tool_Kind) is + procedure Check_Tool (Tool : Tool_Kind; Fail : Boolean := True) is begin - if Already_Detected (Tool) then - return; - end if; - - if Locate_In_Path (Exec_For_Tool (Tool)) /= "" then - -- The tool is available - Already_Detected (Tool) := True; + if Available (Tool) then return; end if; Trace.Info ("Cannot find required tool: " & Tool'Img); - Install_From_Distrib (Tool); + Install_From_Distrib (Tool, Fail); end Check_Tool; end Alire.Utils.Tools; diff --git a/src/alire/alire-utils-tools.ads b/src/alire/alire-utils-tools.ads index dad880b2..f299932a 100644 --- a/src/alire/alire-utils-tools.ads +++ b/src/alire/alire-utils-tools.ads @@ -1,9 +1,14 @@ package Alire.Utils.Tools is - type Tool_Kind is (Git, Tar, Unzip, Curl, Mercurial, Subversion); + type Tool_Kind is + (Easy_Graph, Git, Tar, Unzip, Curl, Mercurial, Subversion); - procedure Check_Tool (Tool : Tool_Kind); + function Available (Tool : Tool_Kind) return Boolean; + -- Say if tool is already available (attemps detection for the tool, but + -- does not install it if missing). + + procedure Check_Tool (Tool : Tool_Kind; Fail : Boolean := True); -- Check if a required executable tool is available in PATH. - -- If not, try to install it or abort. + -- If not, try to install it. If unable and Fail, abort, otherwise return end Alire.Utils.Tools; diff --git a/src/alr/alr-commands-show.adb b/src/alr/alr-commands-show.adb index b202f611..7ee9a6c0 100644 --- a/src/alr/alr-commands-show.adb +++ b/src/alr/alr-commands-show.adb @@ -57,7 +57,7 @@ package body Alr.Commands.Show is Put_Line ("Platform package: " & Rel.Origin.Package_Name); end if; - if Cmd.Solve then + if Cmd.Graph or else Cmd.Solve or else Cmd.Tree then declare Needed : constant Query.Solution := (if Current @@ -69,10 +69,26 @@ package body Alr.Commands.Show is Options => (Age => Query_Policy, others => <>))); begin - Needed.Print (Rel, - Platform.Properties, - Cmd.Detail, - Always); + if Cmd.Solve then + Needed.Print (Rel, + Platform.Properties, + Cmd.Detail, + Always); + elsif Cmd.Tree then + if Needed.Crates.Length not in 0 then + Trace.Always ("Dependencies (tree):"); + Needed.Print_Tree (Rel, + Prefix => " ", + Print_Root => False); + end if; + elsif Cmd.Graph then + if Needed.Crates.Length not in 0 then + Trace.Always ("Dependencies (graph):"); + Needed.Print_Graph (Rel, + Platform.Properties); + end if; + end if; + if not Needed.Is_Complete then Put_Line ("Dependencies cannot be met"); end if; @@ -200,12 +216,16 @@ package body Alr.Commands.Show is end case; end if; - if Cmd.External and (Cmd.Detect or Cmd.Jekyll or Cmd.Solve) then + if Cmd.External and then + (Cmd.Detect or Cmd.Jekyll or Cmd.Graph or Cmd.Solve or Cmd.Tree) + then Reportaise_Wrong_Arguments ("Switch --external can only be combined with --system"); end if; - if Num_Arguments = 1 or else Cmd.Solve then + if Num_Arguments = 1 or else + Cmd.Graph or else Cmd.Solve or else Cmd.Tree + then Requires_Full_Index; end if; @@ -288,6 +308,10 @@ package body Alr.Commands.Show is "", "--external", "Show info about external definitions for a crate"); + Define_Switch (Config, + Cmd.Graph'Access, + "", "--graph", "Print ASCII graph of dependencies"); + Define_Switch (Config, Cmd.System'Access, "", "--system", @@ -297,6 +321,10 @@ package body Alr.Commands.Show is Cmd.Solve'Access, "", "--solve", "Solve dependencies and report"); + Define_Switch (Config, + Cmd.Tree'Access, + "", "--tree", "Show complete dependency tree"); + Define_Switch (Config, Cmd.Jekyll'Access, "", "--jekyll", "Enable Jekyll output format"); diff --git a/src/alr/alr-commands-show.ads b/src/alr/alr-commands-show.ads index f9981ee0..03096301 100644 --- a/src/alr/alr-commands-show.ads +++ b/src/alr/alr-commands-show.ads @@ -16,7 +16,8 @@ package Alr.Commands.Show is ("See information about a release"); overriding function Usage_Custom_Parameters (Cmd : Command) return String is - ("[allowed versions]"); + ("[[allowed versions]] [--system] [--external[-detect]" + & " | --graph | --jekyll | --solve | --tree"); private @@ -24,8 +25,10 @@ private Detail : aliased Boolean := False; Detect : aliased Boolean := False; External : aliased Boolean := False; + Graph : aliased Boolean := False; Solve : aliased Boolean := False; System : aliased Boolean := False; + Tree : aliased Boolean := False; Jekyll : aliased Boolean := False; end record; diff --git a/src/alr/alr-commands-withing.adb b/src/alr/alr-commands-withing.adb index b4fd35ed..075a12bb 100644 --- a/src/alr/alr-commands-withing.adb +++ b/src/alr/alr-commands-withing.adb @@ -441,12 +441,23 @@ package body Alr.Commands.Withing is Check (Cmd.Del); Check (Cmd.From); + Check (Cmd.Graph); Check (Cmd.Solve); + Check (Cmd.Tree); -- No parameters: give current platform dependencies and BAIL OUT - if Num_Arguments = 0 and then (Flags = 0 or else Cmd.Solve) then - List (Cmd); - return; + if Num_Arguments = 0 then + if Flags = 0 or else Cmd.Solve then + List (Cmd); + return; + elsif Cmd.Tree then + Root.Current.Solution.Print_Tree (Root.Current.Release); + return; + elsif Cmd.Graph then + Root.Current.Solution.Print_Graph + (Root.Current.Release, Platform.Properties); + return; + end if; end if; if Num_Arguments < 1 then @@ -550,6 +561,11 @@ package body Alr.Commands.Withing is "", "--from", "Use dependencies declared within GPR project file"); + Define_Switch (Config, + Cmd.Graph'Access, + "", "--graph", + "Show ASCII graph of dependencies"); + Define_Switch (Config => Config, Output => Cmd.URL'Access, @@ -561,6 +577,11 @@ package body Alr.Commands.Withing is Cmd.Solve'Access, "", "--solve", "Show complete solution to dependencies"); + + Define_Switch (Config, + Cmd.Tree'Access, + "", "--tree", + "Show complete dependency tree"); end Setup_Switches; end Alr.Commands.Withing; diff --git a/src/alr/alr-commands-withing.ads b/src/alr/alr-commands-withing.ads index 38386ccb..dcd3f08f 100644 --- a/src/alr/alr-commands-withing.ads +++ b/src/alr/alr-commands-withing.ads @@ -20,14 +20,17 @@ package Alr.Commands.Withing is overriding function Usage_Custom_Parameters (Cmd : Command) return String is ("[{ [--del] [versions]..." & " | --from ..." - & " | [versions] --use } ]"); + & " | [versions] --use } ]" + & " | --tree"); private type Command is new Commands.Command with record Del : aliased Boolean := False; From : aliased Boolean := False; + Graph : aliased Boolean := False; Solve : aliased Boolean := False; + Tree : aliased Boolean := False; URL : aliased GNAT.Strings.String_Access; end record; diff --git a/testsuite/tests/with/tree-switch/test.py b/testsuite/tests/with/tree-switch/test.py new file mode 100644 index 00000000..c06efae0 --- /dev/null +++ b/testsuite/tests/with/tree-switch/test.py @@ -0,0 +1,38 @@ +""" +Check output of the --tree switch +""" + +import os +import re + +from drivers.alr import run_alr +from drivers.asserts import assert_match + +# Initialize project +run_alr('init', '--bin', 'xxx') +os.chdir('xxx') + +# Add dependency on hello^1. Solution is hello=1.0.1 --> libhello=1.1.0 +run_alr('with', 'hello^1') + +# Add dependency on superhello*. Solution is superhello=1.0 --> libhello=1.0.1 +# This implies a downgrade from libhello=1.1.0 to libhello=1.0.1, which is the +# only possible combination of libhello^1.0 & libhello~1.0 +run_alr('with', 'superhello') + +# Add more dependencies, without a proper release +run_alr('with', 'wip', '--use', '/fake') +run_alr('with', 'unobtanium', '--force') + +# Verify printout (but for test-dependent path) +p = run_alr('with', '--tree') +assert_match(re.escape('''xxx=0.0.0 ++-- hello=1.0.1 (^1) +| +-- libhello=1.0.1 (^1.0) ++-- superhello=1.0.0 (*) +| +-- libhello=1.0.1 (~1.0) ++-- unobtanium* (direct,missed) (*) ++-- wip* (direct,linked,pin=''') + '.*' + re.escape(') (*)'), + p.out, flags=re.S) + +print('SUCCESS') diff --git a/testsuite/tests/with/tree-switch/test.yaml b/testsuite/tests/with/tree-switch/test.yaml new file mode 100644 index 00000000..476e709f --- /dev/null +++ b/testsuite/tests/with/tree-switch/test.yaml @@ -0,0 +1,3 @@ +driver: python-script +indexes: + solver_index: {} -- 2.39.5