| # Copyright 2020 The ChromiumOS Authors |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| """Simple script to search a generated depgraph for package relationships. |
| |
| This is meant to more quickly and easily explore a board's depgraph because it |
| can be generated once and reused rather than re-querying portage. |
| |
| The script gives the ancestors common to all the packages (plus lists any of |
| the packages themselves that are in all the other packages' dependencies). |
| The output of that step is simply an ordered list of packages. It also |
| generates all the pair-wise builds-before relationships. |
| |
| The depgraph used is the one generated from the |
| DependencyService/GetBuildDependencyGraph endpoint. Use gen_call_scripts to |
| easily call it. By default, this script will use the default output of the |
| gen_call_scripts script for the endpoint. i.e.: |
| |
| $> cd ~/chromiumos/chromite/api/contrib/ |
| $> ./gen_call_scripts --board=betty |
| $> ./call_scripts/dependency__get_build_dependency_graph |
| $> ./depgraph_common_inheritance dev-libs/libxml2 app-text/docbook-xml-dtd |
| """ |
| |
| import copy |
| import json |
| import logging |
| import os |
| |
| from chromite.lib import commandline |
| from chromite.lib import osutils |
| |
| |
| def GetParser(): |
| """Build the argument parser.""" |
| parser = commandline.ArgumentParser(description=__doc__) |
| |
| default_depgraph = os.path.join( |
| os.path.dirname(__file__), |
| "call_scripts", |
| "dependency__get_build_dependency_graph_output.json", |
| ) |
| |
| parser.add_argument( |
| "packages", |
| nargs="+", |
| help="The packages to search for. They must be in the " |
| '"category/package" format. At least 2 packages are required, but any ' |
| "number can be given.", |
| ) |
| parser.add_argument( |
| "-d", |
| "--depgraph", |
| type="str_path", |
| default=default_depgraph, |
| help="The json output file containing the depgraph. By default uses " |
| "the dependency__get_build_dependency_graph call_scripts output file.", |
| ) |
| parser.add_argument( |
| "-b", |
| "--backtrack", |
| type=int, |
| default=0, |
| help="The number of layers to traverse back up the depgraph from the " |
| "packages. 1 will search their immediate parents only, 2 will go " |
| "through grandparents, and so on. Use 0 for unlimited, the default.", |
| ) |
| return parser |
| |
| |
| def _ParseArguments(argv): |
| """Parse and validate arguments.""" |
| parser = GetParser() |
| opts = parser.parse_args(argv) |
| |
| if not os.path.exists(opts.depgraph): |
| parser.error("depgraph does not exist.") |
| |
| if len(opts.packages) < 2: |
| parser.error("Must specify at least 2 packages.") |
| |
| opts.Freeze() |
| return opts |
| |
| |
| def main(argv) -> None: |
| opts = _ParseArguments(argv) |
| |
| depgraph = json.loads(osutils.ReadFile(opts.depgraph)) |
| |
| # Build out the package: dependencies mapping from the depgraph. |
| # Key depends on each of the values |
| dependencies = {} |
| for package_info in depgraph.get("depGraph", {}).get("packageDeps", []): |
| pi = package_info["packageInfo"] |
| package = "%s/%s" % (pi["category"], pi["packageName"]) |
| |
| packages = [ |
| "%s/%s" % (p["category"], p["packageName"]) |
| for p in package_info.get("dependencyPackages", []) |
| ] |
| dependencies[package] = set(packages) |
| |
| logging.info("Found %d packages.", len(dependencies)) |
| |
| dep_lists = [] |
| for pkg in opts.packages: |
| pkg_deps = set() |
| if pkg not in dependencies: |
| logging.notice("%s not found in depgraph.", pkg) |
| dep_lists.append(pkg_deps) |
| continue |
| |
| unprocessed = set(dependencies.get(pkg, [])) |
| # Include the package itself so that it will appear in the list if the |
| # other packages depend on it. |
| processed = {pkg} |
| unlimited = not opts.backtrack |
| limit = opts.backtrack if opts.backtrack else None |
| while unprocessed and (limit or unlimited): |
| parents = [] |
| for cur in unprocessed: |
| if cur in processed: |
| continue |
| |
| parents.extend(dependencies.get(cur, [])) |
| processed.add(cur) |
| |
| unprocessed = set(parents) |
| if not unlimited: |
| limit -= 1 |
| |
| dep_lists.append(processed) |
| |
| # Combine them all into a single common list. |
| common = copy.deepcopy(dep_lists[0]) |
| for deps in dep_lists[1:]: |
| common &= deps |
| |
| if common: |
| print("Common package dependencies:") |
| print("\n".join(sorted(list(common)))) |
| else: |
| print("No common dependencies found.") |
| |
| # Find guaranteed build ordering pairs. |
| # Try to find each package in the parent sets of the other packages. |
| before = {pkg: [] for pkg in opts.packages} |
| for pkg in opts.packages: |
| others = set(opts.packages) - {pkg} |
| unprocessed = set(dependencies.get(pkg, [])) |
| processed = set() |
| while others and unprocessed: |
| found = set() |
| for other in others: |
| if other in unprocessed: |
| before[other].append(pkg) |
| found.add(other) |
| others = others - found |
| parents = [] |
| for cur in unprocessed: |
| if cur in processed: |
| continue |
| parents.extend(dependencies.get(cur, [])) |
| processed.add(cur) |
| unprocessed = set(parents) |
| |
| # Remove cycles and build strings. |
| before_strs = [] |
| for pkg, built_before in before.items(): |
| for cur in built_before: |
| if pkg in before[cur]: |
| continue |
| before_strs.append("%s builds before %s" % (pkg, cur)) |
| |
| print() |
| if before_strs: |
| print("Build order relations:") |
| print("\n".join(sorted(before_strs))) |
| else: |
| print("No guaranteed build order relations found.") |