diff mbox series

[2/2] support/scripts/graph-depends allow for forward and reverse depends on same graph

Message ID 20230123231224.3772679-2-me@stevenhay.com
State New
Headers show
Series None | expand

Commit Message

ʎɐH ǝʌǝʇS Jan. 23, 2023, 11:12 p.m. UTC
The current implementation of buildroot depe dependency graphing
either does forward- or reverse-dependency traversal.

This patch enables buildroot to graph forward and reverse dependencies on the
graph for the same package: (Diagram Credit: Yann E. MORRIN)

    $ make pkg-d-graph-both-depends

    pkg A -.            .-> pkg E
            \          /
    pkg B ----> pkg D ----> pkg F
            /          \
    pkg C -'            '-> pkg G

In the above example a single graph shows pkg {A,B,C} are needed
by pkg D, and pkg D is a dependency of pkg {E,F,G}.

The variables DEPTH and RDEPTH can be passed into the graphing functions to
specify the maximum depths of direct and reverse dependencies to control graph
size.

Makefile documentation is also updated.

Signed-off-by: Steve Hay <me@stevenhay.com>

---
Changes v1 -> v2:
  - Enhanced patch description. (Yann)
  - Added Makefile documentation. (Yann)
  - Completed the implementation of graph-both-depends. (Yann)
  - Added ability to control depth of the generated graph.
  - Ensured both forward and reverse dependencies are checked for --check-only case. (Yann)
---
 Makefile                      |  7 ++++
 package/pkg-generic.mk        |  7 +++-
 support/scripts/graph-depends | 69 ++++++++++++++++++++++-------------
 3 files changed, 55 insertions(+), 28 deletions(-)

Comments

ʎɐH ǝʌǝʇS Feb. 6, 2023, 5:27 a.m. UTC | #1
All,

Wanted to bump this email thread to see what we think of the second part 
(the meat) of this patch.

On 1/23/23 6:12 PM, Steve Hay wrote:
> The current implementation of buildroot depe dependency graphing
> either does forward- or reverse-dependency traversal.
>
> This patch enables buildroot to graph forward and reverse dependencies on the
> graph for the same package: (Diagram Credit: Yann E. MORRIN)
>
>      $ make pkg-d-graph-both-depends
>
>      pkg A -.            .-> pkg E
>              \          /
>      pkg B ----> pkg D ----> pkg F
>              /          \
>      pkg C -'            '-> pkg G
>
> In the above example a single graph shows pkg {A,B,C} are needed
> by pkg D, and pkg D is a dependency of pkg {E,F,G}.
>
> The variables DEPTH and RDEPTH can be passed into the graphing functions to
> specify the maximum depths of direct and reverse dependencies to control graph
> size.
>
> Makefile documentation is also updated.
>
> Signed-off-by: Steve Hay <me@stevenhay.com>
>
> ---
> Changes v1 -> v2:
>    - Enhanced patch description. (Yann)
>    - Added Makefile documentation. (Yann)
>    - Completed the implementation of graph-both-depends. (Yann)
>    - Added ability to control depth of the generated graph.
>    - Ensured both forward and reverse dependencies are checked for --check-only case. (Yann)
> ---
>   Makefile                      |  7 ++++
>   package/pkg-generic.mk        |  7 +++-
>   support/scripts/graph-depends | 69 ++++++++++++++++++++++-------------
>   3 files changed, 55 insertions(+), 28 deletions(-)
>
> diff --git a/Makefile b/Makefile
> index 8517d563d5..e47544fd42 100644
> --- a/Makefile
> +++ b/Makefile
> @@ -1163,7 +1163,14 @@ help:
>   	@echo '  <pkg>-show-recursive-rdepends'
>   	@echo '                         - Recursively list packages which have <pkg> as a dependency'
>   	@echo '  <pkg>-graph-depends    - Generate a graph of <pkg>'\''s dependencies'
> +	@echo '                             DEPTH, if set on the command line, is used to specify maximum'
> +	@echo '                               depth to graph dependencies.'
>   	@echo '  <pkg>-graph-rdepends   - Generate a graph of <pkg>'\''s reverse dependencies'
> +	@echo '                             RDEPTH, if set on the command line, is used to specify maximum'
> +	@echo '                               depth to graph reverse dependencies.'
> +	@echo '  <pkg>-graph-both-depends'
> +	@echo '                         - Generate a graph of both <pkg>'\''s forward and'
> +	@echo '                           reverse dependencies. DEPTH and RDEPTH work as described above.'
>   	@echo '  <pkg>-dirclean         - Remove <pkg> build directory'
>   	@echo '  <pkg>-reconfigure      - Restart the build from the configure step'
>   	@echo '  <pkg>-rebuild          - Restart the build from the build step'
> diff --git a/package/pkg-generic.mk b/package/pkg-generic.mk
> index 2f8ba39edf..6ddb304a12 100644
> --- a/package/pkg-generic.mk
> +++ b/package/pkg-generic.mk
> @@ -1031,10 +1031,13 @@ $(1)-show-info:
>   	$$(info $$(call clean-json,{ $$(call json-info,$(2)) }))
>   
>   $(1)-graph-depends: graph-depends-requirements
> -	$(call pkg-graph-depends,$(1),--direct)
> +	$(call pkg-graph-depends,$(1),--direct --depth $(or $(DEPTH),0))
>   
>   $(1)-graph-rdepends: graph-depends-requirements
> -	$(call pkg-graph-depends,$(1),--reverse)
> +	$(call pkg-graph-depends,$(1),--reverse --rdepth $(or $(RDEPTH),0))
> +
> +$(1)-graph-both-depends: graph-depends-requirements
> +	$(call pkg-graph-depends,$(1),--direct --reverse --depth $(or $(DEPTH),0) --rdepth $(or $(RDEPTH),0))
>   
>   $(1)-all-source:	$(1)-source
>   $(1)-all-source:	$$(foreach p,$$($(2)_FINAL_ALL_DEPENDENCIES),$$(p)-all-source)
> diff --git a/support/scripts/graph-depends b/support/scripts/graph-depends
> index 3e3373950f..0285afb46d 100755
> --- a/support/scripts/graph-depends
> +++ b/support/scripts/graph-depends
> @@ -159,11 +159,11 @@ def check_circular_deps(deps):
>   
>   # This functions trims down the dependency list of all packages.
>   # It applies in sequence all the dependency-elimination methods.
> -def remove_extra_deps(deps, rootpkg, transitive, arrow_dir):
> +def remove_extra_deps(deps, rootpkg, transitive, direct):
>       # For the direct dependencies, find and eliminate mandatory
>       # deps, and add them to the root package. Don't do it for a
>       # reverse graph, because mandatory deps are only direct deps.
> -    if arrow_dir == "forward":
> +    if direct:
>           for pkg in list(deps.keys()):
>               if not pkg == rootpkg:
>                   for d in get_mandatory_deps(pkg, deps):
> @@ -197,14 +197,16 @@ def print_attrs(outfile, pkg, pkg_type, pkg_version, depth, colors):
>       outfile.write("%s [color=%s,style=filled]\n" % (name, color))
>   
>   
> +
>   # Print the dependency graph of a package
>   def print_pkg_deps(outfile, dict_deps, dict_types, dict_versions, stop_list, exclude_list,
> -                   arrow_dir, draw_graph, depth, max_depth, pkg, colors, done_deps=None):
> +                   direct, draw_graph, depth, max_depth, pkg, colors, done_deps=None):
>       if done_deps is None:
>           done_deps = []
>       if pkg in done_deps:
>           return
>       done_deps.append(pkg)
> +
>       if draw_graph:
>           print_attrs(outfile, pkg, dict_types[pkg], dict_versions[pkg], depth, colors)
>       elif depth != 0:
> @@ -231,9 +233,12 @@ def print_pkg_deps(outfile, dict_deps, dict_types, dict_versions, stop_list, exc
>                       break
>               if add:
>                   if draw_graph:
> -                    outfile.write("%s -> %s [dir=%s]\n" % (pkg_node_name(pkg), pkg_node_name(d), arrow_dir))
> +                    if direct:
> +                        outfile.write("%s -> %s [dir=%s]\n" % (pkg_node_name(pkg), pkg_node_name(d), "forward"))
> +                    else:
> +                        outfile.write("%s -> %s [dir=%s]\n" % (pkg_node_name(d), pkg_node_name(pkg), "forward"))
>                   print_pkg_deps(outfile, dict_deps, dict_types, dict_versions, stop_list, exclude_list,
> -                               arrow_dir, draw_graph, depth + 1, max_depth, d, colors, done_deps)
> +                               direct, draw_graph, depth + 1, max_depth, d, colors, done_deps)
>   
>   
>   def parse_args():
> @@ -246,6 +251,8 @@ def parse_args():
>                           help="Graph the dependencies of PACKAGE")
>       parser.add_argument("--depth", '-d', metavar="DEPTH", dest="depth", type=int, default=0,
>                           help="Limit the dependency graph to DEPTH levels; 0 means no limit.")
> +    parser.add_argument("--rdepth", metavar="RDEPTH", dest="rdepth", type=int, default=0,
> +                        help="Limit the dependency graph to DEPTH levels; 0 means no limit.")
>       parser.add_argument("--stop-on", "-s", metavar="PACKAGE", dest="stop_list", action="append",
>                           help="Do not graph past this package (can be given multiple times)." +
>                           " Can be a package name or a glob, " +
> @@ -265,9 +272,9 @@ def parse_args():
>                           default=False)
>       parser.add_argument("--no-transitive", dest="transitive", action='store_false',
>                           help="Draw (do not draw) transitive dependencies")
> -    parser.add_argument("--direct", dest="direct", action='store_true', default=True,
> +    parser.add_argument("--direct", dest="direct", action='store_true', default=False,
>                           help="Draw direct dependencies (the default)")
> -    parser.add_argument("--reverse", dest="direct", action='store_false',
> +    parser.add_argument("--reverse", dest="reverse", action='store_true', default=False,
>                           help="Draw reverse dependencies")
>       parser.add_argument("--quiet", '-q', dest="quiet", action='store_true',
>                           help="Quiet")
> @@ -292,6 +299,9 @@ def main():
>               sys.exit(1)
>           outfile = open(args.outfile, "w")
>   
> +    if not args.direct and not args.reverse: # select default direct if none is specified.
> +        args.direct = True
> +
>       if args.package is None:
>           mode = MODE_FULL
>           rootpkg = 'all'
> @@ -312,13 +322,9 @@ def main():
>       if args.exclude_mandatory:
>           exclude_list += MANDATORY_DEPS
>   
> -    if args.direct:
> -        arrow_dir = "forward"
> -    else:
> -        if mode == MODE_FULL:
> -            logging.error("--reverse needs a package")
> -            sys.exit(1)
> -        arrow_dir = "back"
> +    if args.reverse and mode == MODE_FULL:
> +        logging.error("--reverse needs a package")
> +        sys.exit(1)
>   
>       draw_graph = not args.flat_list
>   
> @@ -330,23 +336,34 @@ def main():
>           logging.error("Error: incorrect color list '%s'" % args.colors)
>           sys.exit(1)
>   
> -    deps, rdeps, dict_types, dict_versions = brpkgutil.get_dependency_tree()
> -    dict_deps = deps if args.direct else rdeps
> -
> -    check_circular_deps(dict_deps)
> -    if check_only:
> -        sys.exit(0)
> -
> -    dict_deps = remove_extra_deps(dict_deps, rootpkg, args.transitive, arrow_dir)
>   
>       # Start printing the graph data
> -    if draw_graph:
> +    if not check_only and draw_graph:
>           outfile.write("digraph G {\n")
>   
> -    print_pkg_deps(outfile, dict_deps, dict_types, dict_versions, stop_list, exclude_list,
> -                   arrow_dir, draw_graph, 0, args.depth, rootpkg, colors)
> +    deps, rdeps, dict_types, dict_versions = brpkgutil.get_dependency_tree()
>   
> -    if draw_graph:
> +    # forward
> +    if args.direct:
> +        dict_deps = deps
> +        direct = True
> +        check_circular_deps(dict_deps)
> +        if not check_only:
> +            dict_deps = remove_extra_deps(dict_deps, rootpkg, args.transitive, direct)
> +            print_pkg_deps(outfile, dict_deps, dict_types, dict_versions, stop_list, exclude_list,
> +                           direct, draw_graph, 0, args.depth, rootpkg, colors)
> +
> +    # reverse
> +    if args.reverse:
> +        dict_deps = rdeps
> +        direct = False
> +        check_circular_deps(dict_deps)
> +        if not check_only:
> +            dict_deps = remove_extra_deps(dict_deps, rootpkg, args.transitive, direct)
> +            print_pkg_deps(outfile, dict_deps, dict_types, dict_versions, stop_list, exclude_list,
> +                           direct, draw_graph, 0, args.rdepth, rootpkg, colors)
> +
> +    if not check_only and draw_graph:
>           outfile.write("}\n")
>       else:
>           outfile.write("\n")
diff mbox series

Patch

diff --git a/Makefile b/Makefile
index 8517d563d5..e47544fd42 100644
--- a/Makefile
+++ b/Makefile
@@ -1163,7 +1163,14 @@  help:
 	@echo '  <pkg>-show-recursive-rdepends'
 	@echo '                         - Recursively list packages which have <pkg> as a dependency'
 	@echo '  <pkg>-graph-depends    - Generate a graph of <pkg>'\''s dependencies'
+	@echo '                             DEPTH, if set on the command line, is used to specify maximum'
+	@echo '                               depth to graph dependencies.'
 	@echo '  <pkg>-graph-rdepends   - Generate a graph of <pkg>'\''s reverse dependencies'
+	@echo '                             RDEPTH, if set on the command line, is used to specify maximum'
+	@echo '                               depth to graph reverse dependencies.'
+	@echo '  <pkg>-graph-both-depends'
+	@echo '                         - Generate a graph of both <pkg>'\''s forward and'
+	@echo '                           reverse dependencies. DEPTH and RDEPTH work as described above.'
 	@echo '  <pkg>-dirclean         - Remove <pkg> build directory'
 	@echo '  <pkg>-reconfigure      - Restart the build from the configure step'
 	@echo '  <pkg>-rebuild          - Restart the build from the build step'
diff --git a/package/pkg-generic.mk b/package/pkg-generic.mk
index 2f8ba39edf..6ddb304a12 100644
--- a/package/pkg-generic.mk
+++ b/package/pkg-generic.mk
@@ -1031,10 +1031,13 @@  $(1)-show-info:
 	$$(info $$(call clean-json,{ $$(call json-info,$(2)) }))
 
 $(1)-graph-depends: graph-depends-requirements
-	$(call pkg-graph-depends,$(1),--direct)
+	$(call pkg-graph-depends,$(1),--direct --depth $(or $(DEPTH),0))
 
 $(1)-graph-rdepends: graph-depends-requirements
-	$(call pkg-graph-depends,$(1),--reverse)
+	$(call pkg-graph-depends,$(1),--reverse --rdepth $(or $(RDEPTH),0))
+
+$(1)-graph-both-depends: graph-depends-requirements
+	$(call pkg-graph-depends,$(1),--direct --reverse --depth $(or $(DEPTH),0) --rdepth $(or $(RDEPTH),0))
 
 $(1)-all-source:	$(1)-source
 $(1)-all-source:	$$(foreach p,$$($(2)_FINAL_ALL_DEPENDENCIES),$$(p)-all-source)
diff --git a/support/scripts/graph-depends b/support/scripts/graph-depends
index 3e3373950f..0285afb46d 100755
--- a/support/scripts/graph-depends
+++ b/support/scripts/graph-depends
@@ -159,11 +159,11 @@  def check_circular_deps(deps):
 
 # This functions trims down the dependency list of all packages.
 # It applies in sequence all the dependency-elimination methods.
-def remove_extra_deps(deps, rootpkg, transitive, arrow_dir):
+def remove_extra_deps(deps, rootpkg, transitive, direct):
     # For the direct dependencies, find and eliminate mandatory
     # deps, and add them to the root package. Don't do it for a
     # reverse graph, because mandatory deps are only direct deps.
-    if arrow_dir == "forward":
+    if direct:
         for pkg in list(deps.keys()):
             if not pkg == rootpkg:
                 for d in get_mandatory_deps(pkg, deps):
@@ -197,14 +197,16 @@  def print_attrs(outfile, pkg, pkg_type, pkg_version, depth, colors):
     outfile.write("%s [color=%s,style=filled]\n" % (name, color))
 
 
+
 # Print the dependency graph of a package
 def print_pkg_deps(outfile, dict_deps, dict_types, dict_versions, stop_list, exclude_list,
-                   arrow_dir, draw_graph, depth, max_depth, pkg, colors, done_deps=None):
+                   direct, draw_graph, depth, max_depth, pkg, colors, done_deps=None):
     if done_deps is None:
         done_deps = []
     if pkg in done_deps:
         return
     done_deps.append(pkg)
+
     if draw_graph:
         print_attrs(outfile, pkg, dict_types[pkg], dict_versions[pkg], depth, colors)
     elif depth != 0:
@@ -231,9 +233,12 @@  def print_pkg_deps(outfile, dict_deps, dict_types, dict_versions, stop_list, exc
                     break
             if add:
                 if draw_graph:
-                    outfile.write("%s -> %s [dir=%s]\n" % (pkg_node_name(pkg), pkg_node_name(d), arrow_dir))
+                    if direct:
+                        outfile.write("%s -> %s [dir=%s]\n" % (pkg_node_name(pkg), pkg_node_name(d), "forward"))
+                    else:
+                        outfile.write("%s -> %s [dir=%s]\n" % (pkg_node_name(d), pkg_node_name(pkg), "forward"))
                 print_pkg_deps(outfile, dict_deps, dict_types, dict_versions, stop_list, exclude_list,
-                               arrow_dir, draw_graph, depth + 1, max_depth, d, colors, done_deps)
+                               direct, draw_graph, depth + 1, max_depth, d, colors, done_deps)
 
 
 def parse_args():
@@ -246,6 +251,8 @@  def parse_args():
                         help="Graph the dependencies of PACKAGE")
     parser.add_argument("--depth", '-d', metavar="DEPTH", dest="depth", type=int, default=0,
                         help="Limit the dependency graph to DEPTH levels; 0 means no limit.")
+    parser.add_argument("--rdepth", metavar="RDEPTH", dest="rdepth", type=int, default=0,
+                        help="Limit the dependency graph to DEPTH levels; 0 means no limit.")
     parser.add_argument("--stop-on", "-s", metavar="PACKAGE", dest="stop_list", action="append",
                         help="Do not graph past this package (can be given multiple times)." +
                         " Can be a package name or a glob, " +
@@ -265,9 +272,9 @@  def parse_args():
                         default=False)
     parser.add_argument("--no-transitive", dest="transitive", action='store_false',
                         help="Draw (do not draw) transitive dependencies")
-    parser.add_argument("--direct", dest="direct", action='store_true', default=True,
+    parser.add_argument("--direct", dest="direct", action='store_true', default=False,
                         help="Draw direct dependencies (the default)")
-    parser.add_argument("--reverse", dest="direct", action='store_false',
+    parser.add_argument("--reverse", dest="reverse", action='store_true', default=False,
                         help="Draw reverse dependencies")
     parser.add_argument("--quiet", '-q', dest="quiet", action='store_true',
                         help="Quiet")
@@ -292,6 +299,9 @@  def main():
             sys.exit(1)
         outfile = open(args.outfile, "w")
 
+    if not args.direct and not args.reverse: # select default direct if none is specified.
+        args.direct = True
+ 
     if args.package is None:
         mode = MODE_FULL
         rootpkg = 'all'
@@ -312,13 +322,9 @@  def main():
     if args.exclude_mandatory:
         exclude_list += MANDATORY_DEPS
 
-    if args.direct:
-        arrow_dir = "forward"
-    else:
-        if mode == MODE_FULL:
-            logging.error("--reverse needs a package")
-            sys.exit(1)
-        arrow_dir = "back"
+    if args.reverse and mode == MODE_FULL:
+        logging.error("--reverse needs a package")
+        sys.exit(1)
 
     draw_graph = not args.flat_list
 
@@ -330,23 +336,34 @@  def main():
         logging.error("Error: incorrect color list '%s'" % args.colors)
         sys.exit(1)
 
-    deps, rdeps, dict_types, dict_versions = brpkgutil.get_dependency_tree()
-    dict_deps = deps if args.direct else rdeps
-
-    check_circular_deps(dict_deps)
-    if check_only:
-        sys.exit(0)
-
-    dict_deps = remove_extra_deps(dict_deps, rootpkg, args.transitive, arrow_dir)
 
     # Start printing the graph data
-    if draw_graph:
+    if not check_only and draw_graph:
         outfile.write("digraph G {\n")
 
-    print_pkg_deps(outfile, dict_deps, dict_types, dict_versions, stop_list, exclude_list,
-                   arrow_dir, draw_graph, 0, args.depth, rootpkg, colors)
+    deps, rdeps, dict_types, dict_versions = brpkgutil.get_dependency_tree()
 
-    if draw_graph:
+    # forward
+    if args.direct:
+        dict_deps = deps
+        direct = True
+        check_circular_deps(dict_deps)
+        if not check_only:
+            dict_deps = remove_extra_deps(dict_deps, rootpkg, args.transitive, direct)
+            print_pkg_deps(outfile, dict_deps, dict_types, dict_versions, stop_list, exclude_list,
+                           direct, draw_graph, 0, args.depth, rootpkg, colors)
+
+    # reverse
+    if args.reverse:
+        dict_deps = rdeps
+        direct = False
+        check_circular_deps(dict_deps)
+        if not check_only:
+            dict_deps = remove_extra_deps(dict_deps, rootpkg, args.transitive, direct)
+            print_pkg_deps(outfile, dict_deps, dict_types, dict_versions, stop_list, exclude_list,
+                           direct, draw_graph, 0, args.rdepth, rootpkg, colors)
+
+    if not check_only and draw_graph:
         outfile.write("}\n")
     else:
         outfile.write("\n")