blob: f294caf539970d7dcd42e2b0538fc96e36cfe41e [file] [log] [blame]
Jim Hebert91c052c2011-03-11 11:00:53 -08001# Copyright (c) 2010 The Chromium OS Authors. All rights reserved.
2# 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
Mike Frysingera942aee2020-03-20 03:53:37 -040012import sys
Don Garrettf8bf7842014-03-20 17:03:42 -070013
Chris McDonalda4fb7fe2019-08-19 15:16:57 -060014from chromite.lib.depgraph import DepGraphGenerator
Don Garrettf8bf7842014-03-20 17:03:42 -070015
Mike Frysinger06a51c82021-04-06 11:39:17 -040016from chromite.lib import build_target_lib
Jim Hebertcf870d72013-06-12 15:33:34 -070017from chromite.lib import commandline
18from chromite.lib import cros_build_lib
Ralph Nathan446aee92015-03-23 14:44:56 -070019from chromite.lib import cros_logging as logging
Mike Frysingerd0960812020-06-09 01:53:32 -040020from chromite.lib import pformat
Alex Klein5cdd57f2020-11-23 11:53:28 -070021from chromite.lib import sysroot_lib
Alex Klein18a60af2020-06-11 12:08:47 -060022from chromite.lib.parser import package_info
Mike Frysingercc838832014-05-24 13:10:30 -040023
Chris McDonaldd8a7f112019-11-01 10:35:07 -060024
Yu-Ju Hong7f01e9a2014-10-23 11:01:57 -070025def FlattenDepTree(deptree, pkgtable=None, parentcpv=None, get_cpe=False):
Don Garrett25f309a2014-03-19 14:02:12 -070026 """Simplify dependency json.
27
28Turn something like this (the parallel_emerge DepsTree format):
Jim Hebert91c052c2011-03-11 11:00:53 -080029{
30 "app-admin/eselect-1.2.9": {
31 "action": "merge",
32 "deps": {
33 "sys-apps/coreutils-7.5-r1": {
34 "action": "merge",
35 "deps": {},
36 "deptype": "runtime"
37 },
38 ...
39 }
40 }
41}
42 ...into something like this (the cros_extract_deps format):
43{
44 "app-admin/eselect-1.2.9": {
45 "deps": ["coreutils-7.5-r1"],
46 "rev_deps": [],
47 "name": "eselect",
48 "category": "app-admin",
49 "version": "1.2.9",
50 "full_name": "app-admin/eselect-1.2.9",
51 "action": "merge"
52 },
53 "sys-apps/coreutils-7.5-r1": {
54 "deps": [],
55 "rev_deps": ["app-admin/eselect-1.2.9"],
56 "name": "coreutils",
57 "category": "sys-apps",
58 "version": "7.5-r1",
59 "full_name": "sys-apps/coreutils-7.5-r1",
60 "action": "merge"
61 }
62}
Yu-Ju Hong7f01e9a2014-10-23 11:01:57 -070063
64 Args:
65 deptree: The dependency tree.
66 pkgtable: The package table to update. If None, create a new one.
67 parentcpv: The parent CPV.
68 get_cpe: If set True, include CPE in the flattened dependency tree.
69
70 Returns:
71 A flattened dependency tree.
Jim Hebert91c052c2011-03-11 11:00:53 -080072 """
David James1b363582012-12-17 11:53:11 -080073 if pkgtable is None:
74 pkgtable = {}
Mike Frysinger0bdbc102019-06-13 15:27:29 -040075 for cpv, record in deptree.items():
Jim Hebert91c052c2011-03-11 11:00:53 -080076 if cpv not in pkgtable:
Alex Klein18a60af2020-06-11 12:08:47 -060077 split = package_info.SplitCPV(cpv)
Yu-Ju Hong7f01e9a2014-10-23 11:01:57 -070078 pkgtable[cpv] = {'deps': [],
79 'rev_deps': [],
Xuewei Zhang656f9932017-09-15 16:15:05 -070080 'name': split.package,
81 'category': split.category,
82 'version': '%s' % split.version,
Yu-Ju Hong7f01e9a2014-10-23 11:01:57 -070083 'full_name': cpv,
84 'cpes': [],
85 'action': record['action']}
86 if get_cpe:
Xuewei Zhang656f9932017-09-15 16:15:05 -070087 pkgtable[cpv]['cpes'].extend(GetCPEFromCPV(
88 split.category, split.package, split.version_no_rev))
Yu-Ju Hong7f01e9a2014-10-23 11:01:57 -070089
Jim Hebert91c052c2011-03-11 11:00:53 -080090 # If we have a parent, that is a rev_dep for the current package.
91 if parentcpv:
Yu-Ju Hong7f01e9a2014-10-23 11:01:57 -070092 pkgtable[cpv]['rev_deps'].append(parentcpv)
Jim Hebert91c052c2011-03-11 11:00:53 -080093 # If current package has any deps, record those.
Yu-Ju Hong7f01e9a2014-10-23 11:01:57 -070094 for childcpv in record['deps']:
95 pkgtable[cpv]['deps'].append(childcpv)
Jim Hebert91c052c2011-03-11 11:00:53 -080096 # Visit the subtree recursively as well.
Yu-Ju Hong7f01e9a2014-10-23 11:01:57 -070097 FlattenDepTree(record['deps'], pkgtable=pkgtable, parentcpv=cpv,
98 get_cpe=get_cpe)
Ned Nguyen3bbd2072019-01-28 19:33:41 -070099 # Sort 'deps' & 'rev_deps' alphabetically to make them more readable.
100 pkgtable[cpv]['deps'].sort()
101 pkgtable[cpv]['rev_deps'].sort()
Jim Hebert91c052c2011-03-11 11:00:53 -0800102 return pkgtable
103
104
Jim Hebertcf870d72013-06-12 15:33:34 -0700105def GetCPEFromCPV(category, package, version):
106 """Look up the CPE for a specified Portage package.
Jim Hebert91c052c2011-03-11 11:00:53 -0800107
Jim Hebertcf870d72013-06-12 15:33:34 -0700108 Args:
109 category: The Portage package's category, e.g. "net-misc"
110 package: The Portage package's name, e.g. "curl"
111 version: The Portage version, e.g. "7.30.0"
112
Mike Frysinger02e1e072013-11-10 22:11:34 -0500113 Returns:
114 A list of CPE Name strings, e.g.
115 ["cpe:/a:curl:curl:7.30.0", "cpe:/a:curl:libcurl:7.30.0"]
Jim Hebertcf870d72013-06-12 15:33:34 -0700116 """
Yu-Ju Hong7f01e9a2014-10-23 11:01:57 -0700117 equery_cmd = ['equery', 'm', '-U', '%s/%s' % (category, package)]
Mike Frysingerf5a3b2d2019-12-12 14:36:17 -0500118 lines = cros_build_lib.run(equery_cmd, check=False, print_cmd=False,
Mike Frysinger44b83812019-12-10 00:09:30 -0500119 stdout=True, encoding='utf-8').stdout.splitlines()
Jim Hebertcf870d72013-06-12 15:33:34 -0700120 # Look for lines like "Remote-ID: cpe:/a:kernel:linux-pam ID: cpe"
121 # and extract the cpe URI.
122 cpes = []
123 for line in lines:
Yu-Ju Hong7f01e9a2014-10-23 11:01:57 -0700124 if 'ID: cpe' not in line:
Jim Hebertcf870d72013-06-12 15:33:34 -0700125 continue
Yu-Ju Hong7f01e9a2014-10-23 11:01:57 -0700126 cpes.append('%s:%s' % (line.split()[1], version.replace('_', '')))
Jim Hebertcf870d72013-06-12 15:33:34 -0700127 # Note that we're assuming we can combine the root of the CPE, taken
128 # from metadata.xml, and tack on the version number as used by
129 # Portage, and come up with a legitimate CPE. This works so long as
130 # Portage and CPE agree on the precise formatting of the version
Jim Hebert96aff9c2013-07-16 15:43:17 -0700131 # number, which they almost always do. The major exception we've
132 # identified thus far is that our ebuilds have a pattern of inserting
133 # underscores prior to patchlevels, that neither upstream nor CPE
134 # use. For example, our code will decide we have
Jim Hebertcf870d72013-06-12 15:33:34 -0700135 # cpe:/a:todd_miller:sudo:1.8.6_p7 yet the advisories use a format
136 # like cpe:/a:todd_miller:sudo:1.8.6p7, without the underscore. (CPE
137 # is "right" in this example, in that it matches www.sudo.ws.)
138 #
Jim Hebert96aff9c2013-07-16 15:43:17 -0700139 # Removing underscores seems to improve our chances of correctly
140 # arriving at the CPE used by NVD. However, at the end of the day,
141 # ebuild version numbers are rev'd by people who don't have "try to
142 # match NVD" as one of their goals, and there is always going to be
143 # some risk of minor formatting disagreements at the version number
144 # level, if not from stray underscores then from something else.
145 #
Jim Hebertcf870d72013-06-12 15:33:34 -0700146 # This is livable so long as you do some fuzzy version number
147 # comparison in your vulnerability monitoring, between what-we-have
148 # and what-the-advisory-says-is-affected.
149 return cpes
150
151
Chris McDonalde69db662018-11-15 12:50:18 -0700152def GenerateCPEList(deps_list, sysroot):
Xuewei Zhang656f9932017-09-15 16:15:05 -0700153 """Generate all CPEs for the packages included in deps_list and SDK packages
154
155 Args:
156 deps_list: A flattened dependency tree (cros_extract_deps format).
Chris McDonalde69db662018-11-15 12:50:18 -0700157 sysroot: The board directory to use when finding SDK packages.
Xuewei Zhang656f9932017-09-15 16:15:05 -0700158
159 Returns:
160 A list of CPE info for packages in deps_list and SDK packages, e.g.
161 [
162 {
163 "ComponentName": "app-admin/sudo",
164 "Repository": "cros",
165 "Targets": [
166 "cpe:/a:todd_miller:sudo:1.8.19p2"
167 ]
168 },
169 {
170 "ComponentName": "sys-libs/glibc",
171 "Repository": "cros",
172 "Targets": [
173 "cpe:/a:gnu:glibc:2.23"
174 ]
175 }
176 ]
177 """
Jim Hebertcf870d72013-06-12 15:33:34 -0700178 cpe_dump = []
Xuewei Zhang656f9932017-09-15 16:15:05 -0700179
Alex Klein5cdd57f2020-11-23 11:53:28 -0700180 # Generate CPEs for SDK packages.
181 for pkg_info in sorted(
182 sysroot_lib.get_sdk_provided_packages(sysroot), key=lambda x: x.cpvr):
Xuewei Zhang656f9932017-09-15 16:15:05 -0700183 # Only add CPE for SDK CPVs missing in deps_list.
Alex Klein5cdd57f2020-11-23 11:53:28 -0700184 if deps_list.get(pkg_info.cpvr) is not None:
Xuewei Zhang656f9932017-09-15 16:15:05 -0700185 continue
186
Alex Klein5cdd57f2020-11-23 11:53:28 -0700187 cpes = GetCPEFromCPV(pkg_info.category, pkg_info.package, pkg_info.version)
Xuewei Zhang656f9932017-09-15 16:15:05 -0700188 if cpes:
Alex Klein5cdd57f2020-11-23 11:53:28 -0700189 cpe_dump.append({'ComponentName': '%s' % pkg_info.atom,
Xuewei Zhang656f9932017-09-15 16:15:05 -0700190 'Repository': 'cros',
191 'Targets': sorted(cpes)})
192 else:
Alex Klein5cdd57f2020-11-23 11:53:28 -0700193 logging.warning('No CPE entry for %s', pkg_info.cpvr)
Xuewei Zhang656f9932017-09-15 16:15:05 -0700194
195 # Generage CPEs for packages in deps_list.
Mike Frysinger1457e482019-01-03 04:04:02 -0500196 for cpv, record in sorted(deps_list.items()):
Yu-Ju Hong7f01e9a2014-10-23 11:01:57 -0700197 if record['cpes']:
198 name = '%s/%s' % (record['category'], record['name'])
199 cpe_dump.append({'ComponentName': name,
200 'Repository': 'cros',
201 'Targets': sorted(record['cpes'])})
Jim Hebertcf870d72013-06-12 15:33:34 -0700202 else:
Ralph Nathan446aee92015-03-23 14:44:56 -0700203 logging.warning('No CPE entry for %s', cpv)
Yu-Ju Hong7f01e9a2014-10-23 11:01:57 -0700204 return sorted(cpe_dump, key=lambda k: k['ComponentName'])
Jim Hebert91c052c2011-03-11 11:00:53 -0800205
206
Chris McDonalde69db662018-11-15 12:50:18 -0700207def ParseArgs(argv):
208 """Parse command line arguments."""
Mike Frysinger012371e2019-01-03 03:42:32 -0500209 parser = commandline.ArgumentParser(description=__doc__)
Chris McDonalde69db662018-11-15 12:50:18 -0700210 target = parser.add_mutually_exclusive_group()
211 target.add_argument('--sysroot', type='path', help='Path to the sysroot.')
212 target.add_argument('--board', help='Board name.')
213
Yu-Ju Hong7f01e9a2014-10-23 11:01:57 -0700214 parser.add_argument('--format', default='deps',
215 choices=['deps', 'cpe'],
Yu-Ju Hongf48d3982014-10-30 16:12:16 -0700216 help='Output either traditional deps or CPE-only JSON.')
217 parser.add_argument('--output-path', default=None,
218 help='Write output to the given path.')
Mike Frysinger012371e2019-01-03 03:42:32 -0500219 parser.add_argument('pkgs', nargs='*')
220 opts = parser.parse_args(argv)
221 opts.Freeze()
222 return opts
Jim Hebert91c052c2011-03-11 11:00:53 -0800223
Chris McDonalde69db662018-11-15 12:50:18 -0700224
Ned Nguyene16dcfb2019-03-22 10:36:05 -0600225def FilterObsoleteDeps(package_deps):
226 """Remove all the packages that are to be uninstalled from |package_deps|.
227
228 Returns:
229 None since this method mutates |package_deps| directly.
230 """
231 obsolete_package_deps = []
Mike Frysinger0bdbc102019-06-13 15:27:29 -0400232 for k, v in package_deps.items():
Ned Nguyene16dcfb2019-03-22 10:36:05 -0600233 if v['action'] in ('merge', 'nomerge'):
234 continue
235 elif v['action'] == 'uninstall':
236 obsolete_package_deps.append(k)
237 else:
238 assert False, (
239 'Unrecognized action. Package dep data: %s' % v)
240 for p in obsolete_package_deps:
241 del package_deps[p]
242
243
Chris McDonaldd8a7f112019-11-01 10:35:07 -0600244def ExtractDeps(sysroot,
245 package_list,
246 formatting='deps',
Chris McDonald76608422020-06-15 11:42:20 -0600247 include_bdepend=True,
248 backtrack=True):
Ned Nguyendd3e09f2019-03-14 18:54:03 -0600249 """Returns the set of dependencies for the packages in package_list.
250
251 For calculating dependencies graph, this should only consider packages
Chris McDonaldebc7ae72019-10-03 14:58:46 -0600252 that are DEPENDS, RDEPENDS, or BDEPENDS. Essentially, this should answer the
253 question "which are all the packages which changing them may change the
254 execution of any binaries produced by packages in |package_list|."
Ned Nguyendd3e09f2019-03-14 18:54:03 -0600255
256 Args:
257 sysroot: the path (string) to the root directory into which the package is
258 pretend to be merged. This value is also used for setting
259 PORTAGE_CONFIGROOT.
260 package_list: the list of packages (CP string) to extract their dependencies
261 from.
262 formatting: can either be 'deps' or 'cpe'. For 'deps', see the return
263 format in docstring of FlattenDepTree, for 'cpe', see the return format in
264 docstring of GenerateCPEList.
Chris McDonaldd8a7f112019-11-01 10:35:07 -0600265 include_bdepend: Controls whether BDEPEND packages that would be installed
266 to BROOT (usually "/" instead of ROOT) are included in the output.
Chris McDonald76608422020-06-15 11:42:20 -0600267 backtrack: Setting to False disables backtracking in Portage's dependency
268 solver. If the highest available version of dependencies doesn't produce
269 a solvable graph Portage will give up and return an error instead of
270 trying other candidates.
Ned Nguyendd3e09f2019-03-14 18:54:03 -0600271
272 Returns:
273 A JSON-izable object that either follows 'deps' or 'cpe' format.
274 """
Chris McDonaldd8a7f112019-11-01 10:35:07 -0600275 lib_argv = ['--quiet', '--pretend', '--emptytree']
276 if include_bdepend:
277 lib_argv += ['--include-bdepend']
Chris McDonald76608422020-06-15 11:42:20 -0600278 if not backtrack:
279 lib_argv += ['--backtrack=0']
Chris McDonalde69db662018-11-15 12:50:18 -0700280 lib_argv += ['--sysroot=%s' % sysroot]
281 lib_argv.extend(package_list)
Jim Hebert91c052c2011-03-11 11:00:53 -0800282
283 deps = DepGraphGenerator()
Jim Hebertcf870d72013-06-12 15:33:34 -0700284 deps.Initialize(lib_argv)
Chris McDonaldd8a7f112019-11-01 10:35:07 -0600285
286 deps_tree, _deps_info, bdeps_tree = deps.GenDependencyTree()
287 trees = (deps_tree, bdeps_tree)
288
289 flattened_trees = tuple(
290 FlattenDepTree(tree, get_cpe=(formatting == 'cpe')) for tree in trees)
Ned Nguyene16dcfb2019-03-22 10:36:05 -0600291
292 # Workaround: since emerge doesn't honor the --emptytree flag, for now we need
293 # to manually filter out packages that are obsolete (meant to be
294 # uninstalled by emerge)
295 # TODO(crbug.com/938605): remove this work around once
296 # https://bugs.gentoo.org/681308 is addressed.
Chris McDonaldd8a7f112019-11-01 10:35:07 -0600297 for tree in flattened_trees:
298 FilterObsoleteDeps(tree)
Ned Nguyene16dcfb2019-03-22 10:36:05 -0600299
Chris McDonalde69db662018-11-15 12:50:18 -0700300 if formatting == 'cpe':
Chris McDonaldd8a7f112019-11-01 10:35:07 -0600301 flattened_trees = tuple(
302 GenerateCPEList(tree, sysroot) for tree in flattened_trees)
303 return flattened_trees
Chris McDonalde69db662018-11-15 12:50:18 -0700304
305
306def main(argv):
Mike Frysinger012371e2019-01-03 03:42:32 -0500307 opts = ParseArgs(argv)
Chris McDonalde69db662018-11-15 12:50:18 -0700308
Mike Frysinger06a51c82021-04-06 11:39:17 -0400309 sysroot = (opts.sysroot or
310 build_target_lib.get_default_sysroot_path(opts.board))
Chris McDonald486508d2019-11-04 09:20:07 -0700311 deps_list, _ = ExtractDeps(sysroot, opts.pkgs, opts.format)
Yu-Ju Hongf48d3982014-10-30 16:12:16 -0700312
Mike Frysingerd0960812020-06-09 01:53:32 -0400313 pformat.json(deps_list,
314 fp=opts.output_path if opts.output_path else sys.stdout)