blob: 01f874b2535849688f65c89743fdf8ca17b27cca [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
Mike Frysinger012371e2019-01-03 03:42:32 -05005"""Command to extract the dependancy tree for a given package.
6
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 Klein1699fab2022-09-08 08:46:06 -060063 Args:
64 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 Klein1699fab2022-09-08 08:46:06 -060069 Returns:
70 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:
114 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:
119 A list of CPE Name strings, e.g.
120 ["cpe:/a:curl:curl:7.30.0", "cpe:/a:curl:libcurl:7.30.0"]
121 """
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:
162 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:
166 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 ]
183 """
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 Klein1699fab2022-09-08 08:46:06 -0600208 # Generage CPEs for packages in deps_list.
209 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:
250 None since this method mutates |package_deps| directly.
251 """
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:
279 sysroot: the path (string) to the root directory into which the package is
280 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 dependencies
283 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 format in
286 docstring of GenerateCPEList.
287 include_bdepend: Controls whether BDEPEND packages that would be installed
288 to BROOT (usually "/" instead of ROOT) are included in the output.
289 backtrack: Setting to False disables backtracking in Portage's dependency
290 solver. If the highest available version of dependencies doesn't produce
291 a solvable graph Portage will give up and return an error instead of
292 trying other candidates.
Ned Nguyendd3e09f2019-03-14 18:54:03 -0600293
Alex Klein1699fab2022-09-08 08:46:06 -0600294 Returns:
295 A JSON-izable object that either follows 'deps' or 'cpe' format.
296 """
297 lib_argv = ["--quiet", "--pretend", "--emptytree"]
298 if include_bdepend:
299 lib_argv += ["--include-bdepend"]
300 if not backtrack:
301 lib_argv += ["--backtrack=0"]
302 lib_argv += ["--sysroot=%s" % sysroot]
303 lib_argv.extend(package_list)
Jim Hebert91c052c2011-03-11 11:00:53 -0800304
Alex Klein1699fab2022-09-08 08:46:06 -0600305 deps = DepGraphGenerator()
306 deps.Initialize(lib_argv)
Chris McDonaldd8a7f112019-11-01 10:35:07 -0600307
Alex Klein1699fab2022-09-08 08:46:06 -0600308 deps_tree, _deps_info, bdeps_tree = deps.GenDependencyTree()
309 trees = (deps_tree, bdeps_tree)
Chris McDonaldd8a7f112019-11-01 10:35:07 -0600310
Chris McDonaldd8a7f112019-11-01 10:35:07 -0600311 flattened_trees = tuple(
Alex Klein1699fab2022-09-08 08:46:06 -0600312 FlattenDepTree(tree, get_cpe=(formatting == "cpe")) for tree in trees
313 )
314
315 # Workaround: since emerge doesn't honor the --emptytree flag, for now we need
316 # to manually filter out packages that are obsolete (meant to be
317 # uninstalled by emerge)
318 # TODO(crbug.com/938605): remove this work around once
319 # https://bugs.gentoo.org/681308 is addressed.
320 for tree in flattened_trees:
321 FilterObsoleteDeps(tree)
322
323 if formatting == "cpe":
324 flattened_trees = tuple(
325 GenerateCPEList(tree, sysroot) for tree in flattened_trees
326 )
327 return flattened_trees
Chris McDonalde69db662018-11-15 12:50:18 -0700328
329
330def main(argv):
Alex Klein1699fab2022-09-08 08:46:06 -0600331 opts = ParseArgs(argv)
Chris McDonalde69db662018-11-15 12:50:18 -0700332
Alex Klein1699fab2022-09-08 08:46:06 -0600333 sysroot = opts.sysroot or build_target_lib.get_default_sysroot_path(
334 opts.board
335 )
336 deps_list, _ = ExtractDeps(sysroot, opts.pkgs, opts.format)
Yu-Ju Hongf48d3982014-10-30 16:12:16 -0700337
Alex Klein1699fab2022-09-08 08:46:06 -0600338 pformat.json(
339 deps_list, fp=opts.output_path if opts.output_path else sys.stdout
340 )