blob: 2cc33c937d37c8768eb87b833a95dcb4de633157 [file] [log] [blame]
Mike Frysingerf1ba7ad2022-09-12 05:42:57 -04001# Copyright 2010 The ChromiumOS Authors
Jim Hebert91c052c2011-03-11 11:00:53 -08002# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
Alex Klein9e7b29e2023-04-11 16:10:31 -06005"""Command to extract the dependency tree for a given package.
Mike Frysinger012371e2019-01-03 03:42:32 -05006
7This produces JSON output for other tools to process.
8"""
Don Garrett25f309a2014-03-19 14:02:12 -07009
Chris McDonaldd2fa6162019-07-30 15:30:58 -060010from __future__ import absolute_import
Mike Frysinger383367e2014-09-16 15:06:17 -040011
Chris McDonald59650c32021-07-20 15:29:28 -060012import logging
Mike Frysingera942aee2020-03-20 03:53:37 -040013import sys
Don Garrettf8bf7842014-03-20 17:03:42 -070014
Mike Frysinger06a51c82021-04-06 11:39:17 -040015from chromite.lib import build_target_lib
Jim Hebertcf870d72013-06-12 15:33:34 -070016from chromite.lib import commandline
17from chromite.lib import cros_build_lib
Alex Klein5cdd57f2020-11-23 11:53:28 -070018from chromite.lib import sysroot_lib
Chris McDonald59650c32021-07-20 15:29:28 -060019from chromite.lib.depgraph import DepGraphGenerator
Alex Klein18a60af2020-06-11 12:08:47 -060020from chromite.lib.parser import package_info
Alex Klein73eba212021-09-09 11:43:33 -060021from chromite.utils import pformat
Mike Frysingercc838832014-05-24 13:10:30 -040022
Chris McDonaldd8a7f112019-11-01 10:35:07 -060023
Yu-Ju Hong7f01e9a2014-10-23 11:01:57 -070024def FlattenDepTree(deptree, pkgtable=None, parentcpv=None, get_cpe=False):
Alex Klein1699fab2022-09-08 08:46:06 -060025 """Simplify dependency json.
Don Garrett25f309a2014-03-19 14:02:12 -070026
Alex Klein1699fab2022-09-08 08:46:06 -060027 Turn something like this (the parallel_emerge DepsTree format):
28 {
29 "app-admin/eselect-1.2.9": {
Jim Hebert91c052c2011-03-11 11:00:53 -080030 "action": "merge",
Alex Klein1699fab2022-09-08 08:46:06 -060031 "deps": {
32 "sys-apps/coreutils-7.5-r1": {
33 "action": "merge",
34 "deps": {},
35 "deptype": "runtime"
36 },
37 ...
38 }
39 }
Jim Hebert91c052c2011-03-11 11:00:53 -080040 }
Alex Klein1699fab2022-09-08 08:46:06 -060041 ...into something like this (the cros_extract_deps format):
42 {
43 "app-admin/eselect-1.2.9": {
44 "deps": ["coreutils-7.5-r1"],
45 "rev_deps": [],
46 "name": "eselect",
47 "category": "app-admin",
48 "version": "1.2.9",
49 "full_name": "app-admin/eselect-1.2.9",
50 "action": "merge"
51 },
52 "sys-apps/coreutils-7.5-r1": {
53 "deps": [],
54 "rev_deps": ["app-admin/eselect-1.2.9"],
55 "name": "coreutils",
56 "category": "sys-apps",
57 "version": "7.5-r1",
58 "full_name": "sys-apps/coreutils-7.5-r1",
59 "action": "merge"
60 }
61 }
Yu-Ju Hong7f01e9a2014-10-23 11:01:57 -070062
Alex Klein9e7b29e2023-04-11 16:10:31 -060063 Args:
Alex Klein1699fab2022-09-08 08:46:06 -060064 deptree: The dependency tree.
65 pkgtable: The package table to update. If None, create a new one.
66 parentcpv: The parent CPV.
67 get_cpe: If set True, include CPE in the flattened dependency tree.
Yu-Ju Hong7f01e9a2014-10-23 11:01:57 -070068
Alex Klein9e7b29e2023-04-11 16:10:31 -060069 Returns:
Alex Klein1699fab2022-09-08 08:46:06 -060070 A flattened dependency tree.
71 """
72 if pkgtable is None:
73 pkgtable = {}
74 for cpv, record in deptree.items():
75 if cpv not in pkgtable:
76 split = package_info.SplitCPV(cpv)
77 pkgtable[cpv] = {
78 "deps": [],
79 "rev_deps": [],
80 "name": split.package,
81 "category": split.category,
82 "version": "%s" % split.version,
83 "full_name": cpv,
84 "cpes": [],
85 "action": record["action"],
86 }
87 if get_cpe:
88 pkgtable[cpv]["cpes"].extend(
89 GetCPEFromCPV(
90 split.category, split.package, split.version_no_rev
91 )
92 )
Yu-Ju Hong7f01e9a2014-10-23 11:01:57 -070093
Alex Klein1699fab2022-09-08 08:46:06 -060094 # If we have a parent, that is a rev_dep for the current package.
95 if parentcpv:
96 pkgtable[cpv]["rev_deps"].append(parentcpv)
97 # If current package has any deps, record those.
98 for childcpv in record["deps"]:
99 pkgtable[cpv]["deps"].append(childcpv)
100 # Visit the subtree recursively as well.
101 FlattenDepTree(
102 record["deps"], pkgtable=pkgtable, parentcpv=cpv, get_cpe=get_cpe
103 )
104 # Sort 'deps' & 'rev_deps' alphabetically to make them more readable.
105 pkgtable[cpv]["deps"].sort()
106 pkgtable[cpv]["rev_deps"].sort()
107 return pkgtable
Jim Hebert91c052c2011-03-11 11:00:53 -0800108
109
Jim Hebertcf870d72013-06-12 15:33:34 -0700110def GetCPEFromCPV(category, package, version):
Alex Klein1699fab2022-09-08 08:46:06 -0600111 """Look up the CPE for a specified Portage package.
Jim Hebert91c052c2011-03-11 11:00:53 -0800112
Alex Klein1699fab2022-09-08 08:46:06 -0600113 Args:
Alex Klein9e7b29e2023-04-11 16:10:31 -0600114 category: The Portage package's category, e.g. "net-misc"
115 package: The Portage package's name, e.g. "curl"
116 version: The Portage version, e.g. "7.30.0"
Jim Hebertcf870d72013-06-12 15:33:34 -0700117
Alex Klein1699fab2022-09-08 08:46:06 -0600118 Returns:
Alex Klein9e7b29e2023-04-11 16:10:31 -0600119 A list of CPE Name strings, e.g.
120 ["cpe:/a:curl:curl:7.30.0", "cpe:/a:curl:libcurl:7.30.0"]
Alex Klein1699fab2022-09-08 08:46:06 -0600121 """
122 equery_cmd = ["equery", "m", "-U", "%s/%s" % (category, package)]
123 lines = cros_build_lib.run(
124 equery_cmd, check=False, print_cmd=False, stdout=True, encoding="utf-8"
125 ).stdout.splitlines()
126 # Look for lines like "Remote-ID: cpe:/a:kernel:linux-pam ID: cpe"
127 # and extract the cpe URI.
128 cpes = []
129 for line in lines:
130 if "ID: cpe" not in line:
131 continue
132 cpes.append("%s:%s" % (line.split()[1], version.replace("_", "")))
133 # Note that we're assuming we can combine the root of the CPE, taken
134 # from metadata.xml, and tack on the version number as used by
135 # Portage, and come up with a legitimate CPE. This works so long as
136 # Portage and CPE agree on the precise formatting of the version
137 # number, which they almost always do. The major exception we've
138 # identified thus far is that our ebuilds have a pattern of inserting
139 # underscores prior to patchlevels, that neither upstream nor CPE
140 # use. For example, our code will decide we have
141 # cpe:/a:todd_miller:sudo:1.8.6_p7 yet the advisories use a format
142 # like cpe:/a:todd_miller:sudo:1.8.6p7, without the underscore. (CPE
143 # is "right" in this example, in that it matches www.sudo.ws.)
144 #
145 # Removing underscores seems to improve our chances of correctly
146 # arriving at the CPE used by NVD. However, at the end of the day,
147 # ebuild version numbers are rev'd by people who don't have "try to
148 # match NVD" as one of their goals, and there is always going to be
149 # some risk of minor formatting disagreements at the version number
150 # level, if not from stray underscores then from something else.
151 #
152 # This is livable so long as you do some fuzzy version number
153 # comparison in your vulnerability monitoring, between what-we-have
154 # and what-the-advisory-says-is-affected.
155 return cpes
Jim Hebertcf870d72013-06-12 15:33:34 -0700156
157
Chris McDonalde69db662018-11-15 12:50:18 -0700158def GenerateCPEList(deps_list, sysroot):
Alex Klein1699fab2022-09-08 08:46:06 -0600159 """Generate all CPEs for the packages included in deps_list and SDK packages
Xuewei Zhang656f9932017-09-15 16:15:05 -0700160
Alex Klein1699fab2022-09-08 08:46:06 -0600161 Args:
Alex Klein9e7b29e2023-04-11 16:10:31 -0600162 deps_list: A flattened dependency tree (cros_extract_deps format).
163 sysroot: The board directory to use when finding SDK packages.
Xuewei Zhang656f9932017-09-15 16:15:05 -0700164
Alex Klein1699fab2022-09-08 08:46:06 -0600165 Returns:
Alex Klein9e7b29e2023-04-11 16:10:31 -0600166 A list of CPE info for packages in deps_list and SDK packages, e.g.
167 [
168 {
169 "ComponentName": "app-admin/sudo",
170 "Repository": "cros",
171 "Targets": [
172 "cpe:/a:todd_miller:sudo:1.8.19p2"
173 ]
174 },
175 {
176 "ComponentName": "sys-libs/glibc",
177 "Repository": "cros",
178 "Targets": [
179 "cpe:/a:gnu:glibc:2.23"
180 ]
181 }
182 ]
Alex Klein1699fab2022-09-08 08:46:06 -0600183 """
184 cpe_dump = []
Xuewei Zhang656f9932017-09-15 16:15:05 -0700185
Alex Klein1699fab2022-09-08 08:46:06 -0600186 # Generate CPEs for SDK packages.
187 for pkg_info in sorted(
188 sysroot_lib.get_sdk_provided_packages(sysroot), key=lambda x: x.cpvr
189 ):
190 # Only add CPE for SDK CPVs missing in deps_list.
191 if deps_list.get(pkg_info.cpvr) is not None:
192 continue
Xuewei Zhang656f9932017-09-15 16:15:05 -0700193
Alex Klein1699fab2022-09-08 08:46:06 -0600194 cpes = GetCPEFromCPV(
195 pkg_info.category, pkg_info.package, pkg_info.version
196 )
197 if cpes:
198 cpe_dump.append(
199 {
200 "ComponentName": "%s" % pkg_info.atom,
201 "Repository": "cros",
202 "Targets": sorted(cpes),
203 }
204 )
205 else:
206 logging.warning("No CPE entry for %s", pkg_info.cpvr)
Xuewei Zhang656f9932017-09-15 16:15:05 -0700207
Alex Klein9e7b29e2023-04-11 16:10:31 -0600208 # Generate CPEs for packages in deps_list.
Alex Klein1699fab2022-09-08 08:46:06 -0600209 for cpv, record in sorted(deps_list.items()):
210 if record["cpes"]:
211 name = "%s/%s" % (record["category"], record["name"])
212 cpe_dump.append(
213 {
214 "ComponentName": name,
215 "Repository": "cros",
216 "Targets": sorted(record["cpes"]),
217 }
218 )
219 else:
220 logging.warning("No CPE entry for %s", cpv)
221 return sorted(cpe_dump, key=lambda k: k["ComponentName"])
Jim Hebert91c052c2011-03-11 11:00:53 -0800222
223
Chris McDonalde69db662018-11-15 12:50:18 -0700224def ParseArgs(argv):
Alex Klein1699fab2022-09-08 08:46:06 -0600225 """Parse command line arguments."""
226 parser = commandline.ArgumentParser(description=__doc__)
227 target = parser.add_mutually_exclusive_group()
228 target.add_argument("--sysroot", type="path", help="Path to the sysroot.")
229 target.add_argument("--board", help="Board name.")
Chris McDonalde69db662018-11-15 12:50:18 -0700230
Alex Klein1699fab2022-09-08 08:46:06 -0600231 parser.add_argument(
232 "--format",
233 default="deps",
234 choices=["deps", "cpe"],
235 help="Output either traditional deps or CPE-only JSON.",
236 )
237 parser.add_argument(
238 "--output-path", default=None, help="Write output to the given path."
239 )
240 parser.add_argument("pkgs", nargs="*")
241 opts = parser.parse_args(argv)
242 opts.Freeze()
243 return opts
Jim Hebert91c052c2011-03-11 11:00:53 -0800244
Chris McDonalde69db662018-11-15 12:50:18 -0700245
Ned Nguyene16dcfb2019-03-22 10:36:05 -0600246def FilterObsoleteDeps(package_deps):
Alex Klein1699fab2022-09-08 08:46:06 -0600247 """Remove all the packages that are to be uninstalled from |package_deps|.
Ned Nguyene16dcfb2019-03-22 10:36:05 -0600248
Alex Klein1699fab2022-09-08 08:46:06 -0600249 Returns:
Alex Klein9e7b29e2023-04-11 16:10:31 -0600250 None since this method mutates |package_deps| directly.
Alex Klein1699fab2022-09-08 08:46:06 -0600251 """
252 obsolete_package_deps = []
253 for k, v in package_deps.items():
254 if v["action"] in ("merge", "nomerge"):
255 continue
256 elif v["action"] == "uninstall":
257 obsolete_package_deps.append(k)
258 else:
259 assert False, "Unrecognized action. Package dep data: %s" % v
260 for p in obsolete_package_deps:
261 del package_deps[p]
Ned Nguyene16dcfb2019-03-22 10:36:05 -0600262
263
Alex Klein1699fab2022-09-08 08:46:06 -0600264def ExtractDeps(
265 sysroot,
266 package_list,
267 formatting="deps",
268 include_bdepend=True,
269 backtrack=True,
270):
271 """Returns the set of dependencies for the packages in package_list.
Ned Nguyendd3e09f2019-03-14 18:54:03 -0600272
Alex Klein1699fab2022-09-08 08:46:06 -0600273 For calculating dependencies graph, this should only consider packages
274 that are DEPENDS, RDEPENDS, or BDEPENDS. Essentially, this should answer the
275 question "which are all the packages which changing them may change the
276 execution of any binaries produced by packages in |package_list|."
Ned Nguyendd3e09f2019-03-14 18:54:03 -0600277
Alex Klein1699fab2022-09-08 08:46:06 -0600278 Args:
Alex Klein9e7b29e2023-04-11 16:10:31 -0600279 sysroot: the path (string) to the root directory into which the package
280 is pretend to be merged. This value is also used for setting
281 PORTAGE_CONFIGROOT.
282 package_list: the list of packages (CP string) to extract their
283 dependencies from.
284 formatting: can either be 'deps' or 'cpe'. For 'deps', see the return
285 format in docstring of FlattenDepTree, for 'cpe', see the return
286 format in docstring of GenerateCPEList.
287 include_bdepend: Controls whether BDEPEND packages that would be
288 installed to BROOT (usually "/" instead of ROOT) are included in the
289 output.
290 backtrack: Setting to False disables backtracking in Portage's
291 dependency solver. If the highest available version of dependencies
292 doesn't produce a solvable graph Portage will give up and return an
293 error instead of trying other candidates.
Ned Nguyendd3e09f2019-03-14 18:54:03 -0600294
Alex Klein1699fab2022-09-08 08:46:06 -0600295 Returns:
Alex Klein9e7b29e2023-04-11 16:10:31 -0600296 A JSON-izable object that either follows 'deps' or 'cpe' format.
Alex Klein1699fab2022-09-08 08:46:06 -0600297 """
298 lib_argv = ["--quiet", "--pretend", "--emptytree"]
299 if include_bdepend:
300 lib_argv += ["--include-bdepend"]
301 if not backtrack:
302 lib_argv += ["--backtrack=0"]
303 lib_argv += ["--sysroot=%s" % sysroot]
304 lib_argv.extend(package_list)
Jim Hebert91c052c2011-03-11 11:00:53 -0800305
Alex Klein1699fab2022-09-08 08:46:06 -0600306 deps = DepGraphGenerator()
307 deps.Initialize(lib_argv)
Chris McDonaldd8a7f112019-11-01 10:35:07 -0600308
Alex Klein1699fab2022-09-08 08:46:06 -0600309 deps_tree, _deps_info, bdeps_tree = deps.GenDependencyTree()
310 trees = (deps_tree, bdeps_tree)
Chris McDonaldd8a7f112019-11-01 10:35:07 -0600311
Chris McDonaldd8a7f112019-11-01 10:35:07 -0600312 flattened_trees = tuple(
Alex Klein1699fab2022-09-08 08:46:06 -0600313 FlattenDepTree(tree, get_cpe=(formatting == "cpe")) for tree in trees
314 )
315
Alex Klein9e7b29e2023-04-11 16:10:31 -0600316 # Workaround: since emerge doesn't honor the --emptytree flag, for now we
317 # need to manually filter out packages that are obsolete (meant to be
Alex Klein1699fab2022-09-08 08:46:06 -0600318 # uninstalled by emerge)
319 # TODO(crbug.com/938605): remove this work around once
320 # https://bugs.gentoo.org/681308 is addressed.
321 for tree in flattened_trees:
322 FilterObsoleteDeps(tree)
323
324 if formatting == "cpe":
325 flattened_trees = tuple(
326 GenerateCPEList(tree, sysroot) for tree in flattened_trees
327 )
328 return flattened_trees
Chris McDonalde69db662018-11-15 12:50:18 -0700329
330
331def main(argv):
Alex Klein1699fab2022-09-08 08:46:06 -0600332 opts = ParseArgs(argv)
Chris McDonalde69db662018-11-15 12:50:18 -0700333
Alex Klein1699fab2022-09-08 08:46:06 -0600334 sysroot = opts.sysroot or build_target_lib.get_default_sysroot_path(
335 opts.board
336 )
337 deps_list, _ = ExtractDeps(sysroot, opts.pkgs, opts.format)
Yu-Ju Hongf48d3982014-10-30 16:12:16 -0700338
Alex Klein1699fab2022-09-08 08:46:06 -0600339 pformat.json(
340 deps_list, fp=opts.output_path if opts.output_path else sys.stdout
341 )