blob: cfc5bd422464cc6604ea64e95c9045f6c06269c5 [file] [log] [blame]
Mike Frysingere58c0e22017-10-04 15:43:30 -04001# -*- coding: utf-8 -*-
Jim Hebert91c052c2011-03-11 11:00:53 -08002# Copyright (c) 2010 The Chromium OS Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
Mike Frysinger012371e2019-01-03 03:42:32 -05006"""Command to extract the dependancy tree for a given package.
7
8This produces JSON output for other tools to process.
9"""
Don Garrett25f309a2014-03-19 14:02:12 -070010
Mike Frysinger383367e2014-09-16 15:06:17 -040011from __future__ import print_function
Chris McDonaldd2fa6162019-07-30 15:30:58 -060012from __future__ import absolute_import
Mike Frysinger383367e2014-09-16 15:06:17 -040013
Xuewei Zhang656f9932017-09-15 16:15:05 -070014import os
Mike Frysingera942aee2020-03-20 03:53:37 -040015import sys
Don Garrettf8bf7842014-03-20 17:03:42 -070016
Chris McDonalda4fb7fe2019-08-19 15:16:57 -060017from chromite.lib.depgraph import DepGraphGenerator
Don Garrettf8bf7842014-03-20 17:03:42 -070018
Jim Hebertcf870d72013-06-12 15:33:34 -070019from chromite.lib import commandline
20from chromite.lib import cros_build_lib
Ralph Nathan446aee92015-03-23 14:44:56 -070021from chromite.lib import cros_logging as logging
Xuewei Zhang656f9932017-09-15 16:15:05 -070022from chromite.lib import osutils
Mike Frysingerd0960812020-06-09 01:53:32 -040023from chromite.lib import pformat
Xuewei Zhang656f9932017-09-15 16:15:05 -070024from chromite.lib import portage_util
Mike Frysingercc838832014-05-24 13:10:30 -040025
Chris McDonaldd8a7f112019-11-01 10:35:07 -060026
Mike Frysingera942aee2020-03-20 03:53:37 -040027assert sys.version_info >= (3, 6), 'This module requires Python 3.6+'
28
29
Yu-Ju Hong7f01e9a2014-10-23 11:01:57 -070030def FlattenDepTree(deptree, pkgtable=None, parentcpv=None, get_cpe=False):
Don Garrett25f309a2014-03-19 14:02:12 -070031 """Simplify dependency json.
32
33Turn something like this (the parallel_emerge DepsTree format):
Jim Hebert91c052c2011-03-11 11:00:53 -080034{
35 "app-admin/eselect-1.2.9": {
36 "action": "merge",
37 "deps": {
38 "sys-apps/coreutils-7.5-r1": {
39 "action": "merge",
40 "deps": {},
41 "deptype": "runtime"
42 },
43 ...
44 }
45 }
46}
47 ...into something like this (the cros_extract_deps format):
48{
49 "app-admin/eselect-1.2.9": {
50 "deps": ["coreutils-7.5-r1"],
51 "rev_deps": [],
52 "name": "eselect",
53 "category": "app-admin",
54 "version": "1.2.9",
55 "full_name": "app-admin/eselect-1.2.9",
56 "action": "merge"
57 },
58 "sys-apps/coreutils-7.5-r1": {
59 "deps": [],
60 "rev_deps": ["app-admin/eselect-1.2.9"],
61 "name": "coreutils",
62 "category": "sys-apps",
63 "version": "7.5-r1",
64 "full_name": "sys-apps/coreutils-7.5-r1",
65 "action": "merge"
66 }
67}
Yu-Ju Hong7f01e9a2014-10-23 11:01:57 -070068
69 Args:
70 deptree: The dependency tree.
71 pkgtable: The package table to update. If None, create a new one.
72 parentcpv: The parent CPV.
73 get_cpe: If set True, include CPE in the flattened dependency tree.
74
75 Returns:
76 A flattened dependency tree.
Jim Hebert91c052c2011-03-11 11:00:53 -080077 """
David James1b363582012-12-17 11:53:11 -080078 if pkgtable is None:
79 pkgtable = {}
Mike Frysinger0bdbc102019-06-13 15:27:29 -040080 for cpv, record in deptree.items():
Jim Hebert91c052c2011-03-11 11:00:53 -080081 if cpv not in pkgtable:
Xuewei Zhang656f9932017-09-15 16:15:05 -070082 split = portage_util.SplitCPV(cpv)
Yu-Ju Hong7f01e9a2014-10-23 11:01:57 -070083 pkgtable[cpv] = {'deps': [],
84 'rev_deps': [],
Xuewei Zhang656f9932017-09-15 16:15:05 -070085 'name': split.package,
86 'category': split.category,
87 'version': '%s' % split.version,
Yu-Ju Hong7f01e9a2014-10-23 11:01:57 -070088 'full_name': cpv,
89 'cpes': [],
90 'action': record['action']}
91 if get_cpe:
Xuewei Zhang656f9932017-09-15 16:15:05 -070092 pkgtable[cpv]['cpes'].extend(GetCPEFromCPV(
93 split.category, split.package, split.version_no_rev))
Yu-Ju Hong7f01e9a2014-10-23 11:01:57 -070094
Jim Hebert91c052c2011-03-11 11:00:53 -080095 # If we have a parent, that is a rev_dep for the current package.
96 if parentcpv:
Yu-Ju Hong7f01e9a2014-10-23 11:01:57 -070097 pkgtable[cpv]['rev_deps'].append(parentcpv)
Jim Hebert91c052c2011-03-11 11:00:53 -080098 # If current package has any deps, record those.
Yu-Ju Hong7f01e9a2014-10-23 11:01:57 -070099 for childcpv in record['deps']:
100 pkgtable[cpv]['deps'].append(childcpv)
Jim Hebert91c052c2011-03-11 11:00:53 -0800101 # Visit the subtree recursively as well.
Yu-Ju Hong7f01e9a2014-10-23 11:01:57 -0700102 FlattenDepTree(record['deps'], pkgtable=pkgtable, parentcpv=cpv,
103 get_cpe=get_cpe)
Ned Nguyen3bbd2072019-01-28 19:33:41 -0700104 # Sort 'deps' & 'rev_deps' alphabetically to make them more readable.
105 pkgtable[cpv]['deps'].sort()
106 pkgtable[cpv]['rev_deps'].sort()
Jim Hebert91c052c2011-03-11 11:00:53 -0800107 return pkgtable
108
109
Jim Hebertcf870d72013-06-12 15:33:34 -0700110def GetCPEFromCPV(category, package, version):
111 """Look up the CPE for a specified Portage package.
Jim Hebert91c052c2011-03-11 11:00:53 -0800112
Jim Hebertcf870d72013-06-12 15:33:34 -0700113 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"
117
Mike Frysinger02e1e072013-11-10 22:11:34 -0500118 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"]
Jim Hebertcf870d72013-06-12 15:33:34 -0700121 """
Yu-Ju Hong7f01e9a2014-10-23 11:01:57 -0700122 equery_cmd = ['equery', 'm', '-U', '%s/%s' % (category, package)]
Mike Frysingerf5a3b2d2019-12-12 14:36:17 -0500123 lines = cros_build_lib.run(equery_cmd, check=False, print_cmd=False,
Mike Frysinger44b83812019-12-10 00:09:30 -0500124 stdout=True, encoding='utf-8').stdout.splitlines()
Jim Hebertcf870d72013-06-12 15:33:34 -0700125 # Look for lines like "Remote-ID: cpe:/a:kernel:linux-pam ID: cpe"
126 # and extract the cpe URI.
127 cpes = []
128 for line in lines:
Yu-Ju Hong7f01e9a2014-10-23 11:01:57 -0700129 if 'ID: cpe' not in line:
Jim Hebertcf870d72013-06-12 15:33:34 -0700130 continue
Yu-Ju Hong7f01e9a2014-10-23 11:01:57 -0700131 cpes.append('%s:%s' % (line.split()[1], version.replace('_', '')))
Jim Hebertcf870d72013-06-12 15:33:34 -0700132 # Note that we're assuming we can combine the root of the CPE, taken
133 # from metadata.xml, and tack on the version number as used by
134 # Portage, and come up with a legitimate CPE. This works so long as
135 # Portage and CPE agree on the precise formatting of the version
Jim Hebert96aff9c2013-07-16 15:43:17 -0700136 # number, which they almost always do. The major exception we've
137 # identified thus far is that our ebuilds have a pattern of inserting
138 # underscores prior to patchlevels, that neither upstream nor CPE
139 # use. For example, our code will decide we have
Jim Hebertcf870d72013-06-12 15:33:34 -0700140 # cpe:/a:todd_miller:sudo:1.8.6_p7 yet the advisories use a format
141 # like cpe:/a:todd_miller:sudo:1.8.6p7, without the underscore. (CPE
142 # is "right" in this example, in that it matches www.sudo.ws.)
143 #
Jim Hebert96aff9c2013-07-16 15:43:17 -0700144 # Removing underscores seems to improve our chances of correctly
145 # arriving at the CPE used by NVD. However, at the end of the day,
146 # ebuild version numbers are rev'd by people who don't have "try to
147 # match NVD" as one of their goals, and there is always going to be
148 # some risk of minor formatting disagreements at the version number
149 # level, if not from stray underscores then from something else.
150 #
Jim Hebertcf870d72013-06-12 15:33:34 -0700151 # This is livable so long as you do some fuzzy version number
152 # comparison in your vulnerability monitoring, between what-we-have
153 # and what-the-advisory-says-is-affected.
154 return cpes
155
156
Chris McDonalde69db662018-11-15 12:50:18 -0700157def GenerateSDKCPVList(sysroot):
Xuewei Zhang656f9932017-09-15 16:15:05 -0700158 """Find all SDK packages from package.provided
159
160 Args:
Chris McDonalde69db662018-11-15 12:50:18 -0700161 sysroot: The board directory to use when finding SDK packages.
Xuewei Zhang656f9932017-09-15 16:15:05 -0700162
163 Returns:
164 A list of CPV Name strings, e.g.
165 ["sys-libs/glibc-2.23-r9", "dev-lang/go-1.8.3-r1"]
166 """
167 # Look at packages in package.provided.
Chris McDonalde69db662018-11-15 12:50:18 -0700168 sdk_file_path = os.path.join(sysroot, 'etc', 'portage',
Xuewei Zhang656f9932017-09-15 16:15:05 -0700169 'profile', 'package.provided')
170 for line in osutils.ReadFile(sdk_file_path).splitlines():
171 # Skip comments and empty lines.
172 line = line.split('#', 1)[0].strip()
173 if not line:
174 continue
175 yield line
176
177
Chris McDonalde69db662018-11-15 12:50:18 -0700178def GenerateCPEList(deps_list, sysroot):
Xuewei Zhang656f9932017-09-15 16:15:05 -0700179 """Generate all CPEs for the packages included in deps_list and SDK packages
180
181 Args:
182 deps_list: A flattened dependency tree (cros_extract_deps format).
Chris McDonalde69db662018-11-15 12:50:18 -0700183 sysroot: The board directory to use when finding SDK packages.
Xuewei Zhang656f9932017-09-15 16:15:05 -0700184
185 Returns:
186 A list of CPE info for packages in deps_list and SDK packages, e.g.
187 [
188 {
189 "ComponentName": "app-admin/sudo",
190 "Repository": "cros",
191 "Targets": [
192 "cpe:/a:todd_miller:sudo:1.8.19p2"
193 ]
194 },
195 {
196 "ComponentName": "sys-libs/glibc",
197 "Repository": "cros",
198 "Targets": [
199 "cpe:/a:gnu:glibc:2.23"
200 ]
201 }
202 ]
203 """
Jim Hebertcf870d72013-06-12 15:33:34 -0700204 cpe_dump = []
Xuewei Zhang656f9932017-09-15 16:15:05 -0700205
206 # Generage CPEs for SDK packages.
Mike Frysinger1457e482019-01-03 04:04:02 -0500207 for sdk_cpv in sorted(GenerateSDKCPVList(sysroot)):
Xuewei Zhang656f9932017-09-15 16:15:05 -0700208 # Only add CPE for SDK CPVs missing in deps_list.
209 if deps_list.get(sdk_cpv) is not None:
210 continue
211
212 split = portage_util.SplitCPV(sdk_cpv)
213 cpes = GetCPEFromCPV(split.category, split.package, split.version_no_rev)
214 if cpes:
Alex Klein9f93b482018-10-01 09:26:51 -0600215 cpe_dump.append({'ComponentName': '%s' % split.cp,
Xuewei Zhang656f9932017-09-15 16:15:05 -0700216 'Repository': 'cros',
217 'Targets': sorted(cpes)})
218 else:
219 logging.warning('No CPE entry for %s', sdk_cpv)
220
221 # Generage CPEs for packages in deps_list.
Mike Frysinger1457e482019-01-03 04:04:02 -0500222 for cpv, record in sorted(deps_list.items()):
Yu-Ju Hong7f01e9a2014-10-23 11:01:57 -0700223 if record['cpes']:
224 name = '%s/%s' % (record['category'], record['name'])
225 cpe_dump.append({'ComponentName': name,
226 'Repository': 'cros',
227 'Targets': sorted(record['cpes'])})
Jim Hebertcf870d72013-06-12 15:33:34 -0700228 else:
Ralph Nathan446aee92015-03-23 14:44:56 -0700229 logging.warning('No CPE entry for %s', cpv)
Yu-Ju Hong7f01e9a2014-10-23 11:01:57 -0700230 return sorted(cpe_dump, key=lambda k: k['ComponentName'])
Jim Hebert91c052c2011-03-11 11:00:53 -0800231
232
Chris McDonalde69db662018-11-15 12:50:18 -0700233def ParseArgs(argv):
234 """Parse command line arguments."""
Mike Frysinger012371e2019-01-03 03:42:32 -0500235 parser = commandline.ArgumentParser(description=__doc__)
Chris McDonalde69db662018-11-15 12:50:18 -0700236 target = parser.add_mutually_exclusive_group()
237 target.add_argument('--sysroot', type='path', help='Path to the sysroot.')
238 target.add_argument('--board', help='Board name.')
239
Yu-Ju Hong7f01e9a2014-10-23 11:01:57 -0700240 parser.add_argument('--format', default='deps',
241 choices=['deps', 'cpe'],
Yu-Ju Hongf48d3982014-10-30 16:12:16 -0700242 help='Output either traditional deps or CPE-only JSON.')
243 parser.add_argument('--output-path', default=None,
244 help='Write output to the given path.')
Mike Frysinger012371e2019-01-03 03:42:32 -0500245 parser.add_argument('pkgs', nargs='*')
246 opts = parser.parse_args(argv)
247 opts.Freeze()
248 return opts
Jim Hebert91c052c2011-03-11 11:00:53 -0800249
Chris McDonalde69db662018-11-15 12:50:18 -0700250
Ned Nguyene16dcfb2019-03-22 10:36:05 -0600251def FilterObsoleteDeps(package_deps):
252 """Remove all the packages that are to be uninstalled from |package_deps|.
253
254 Returns:
255 None since this method mutates |package_deps| directly.
256 """
257 obsolete_package_deps = []
Mike Frysinger0bdbc102019-06-13 15:27:29 -0400258 for k, v in package_deps.items():
Ned Nguyene16dcfb2019-03-22 10:36:05 -0600259 if v['action'] in ('merge', 'nomerge'):
260 continue
261 elif v['action'] == 'uninstall':
262 obsolete_package_deps.append(k)
263 else:
264 assert False, (
265 'Unrecognized action. Package dep data: %s' % v)
266 for p in obsolete_package_deps:
267 del package_deps[p]
268
269
Chris McDonaldd8a7f112019-11-01 10:35:07 -0600270def ExtractDeps(sysroot,
271 package_list,
272 formatting='deps',
273 include_bdepend=True):
Ned Nguyendd3e09f2019-03-14 18:54:03 -0600274 """Returns the set of dependencies for the packages in package_list.
275
276 For calculating dependencies graph, this should only consider packages
Chris McDonaldebc7ae72019-10-03 14:58:46 -0600277 that are DEPENDS, RDEPENDS, or BDEPENDS. Essentially, this should answer the
278 question "which are all the packages which changing them may change the
279 execution of any binaries produced by packages in |package_list|."
Ned Nguyendd3e09f2019-03-14 18:54:03 -0600280
281 Args:
282 sysroot: the path (string) to the root directory into which the package is
283 pretend to be merged. This value is also used for setting
284 PORTAGE_CONFIGROOT.
285 package_list: the list of packages (CP string) to extract their dependencies
286 from.
287 formatting: can either be 'deps' or 'cpe'. For 'deps', see the return
288 format in docstring of FlattenDepTree, for 'cpe', see the return format in
289 docstring of GenerateCPEList.
Chris McDonaldd8a7f112019-11-01 10:35:07 -0600290 include_bdepend: Controls whether BDEPEND packages that would be installed
291 to BROOT (usually "/" instead of ROOT) are included in the output.
Ned Nguyendd3e09f2019-03-14 18:54:03 -0600292
293 Returns:
294 A JSON-izable object that either follows 'deps' or 'cpe' format.
295 """
Chris McDonaldd8a7f112019-11-01 10:35:07 -0600296 lib_argv = ['--quiet', '--pretend', '--emptytree']
297 if include_bdepend:
298 lib_argv += ['--include-bdepend']
Chris McDonalde69db662018-11-15 12:50:18 -0700299 lib_argv += ['--sysroot=%s' % sysroot]
300 lib_argv.extend(package_list)
Jim Hebert91c052c2011-03-11 11:00:53 -0800301
302 deps = DepGraphGenerator()
Jim Hebertcf870d72013-06-12 15:33:34 -0700303 deps.Initialize(lib_argv)
Chris McDonaldd8a7f112019-11-01 10:35:07 -0600304
305 deps_tree, _deps_info, bdeps_tree = deps.GenDependencyTree()
306 trees = (deps_tree, bdeps_tree)
307
308 flattened_trees = tuple(
309 FlattenDepTree(tree, get_cpe=(formatting == 'cpe')) for tree in trees)
Ned Nguyene16dcfb2019-03-22 10:36:05 -0600310
311 # Workaround: since emerge doesn't honor the --emptytree flag, for now we need
312 # to manually filter out packages that are obsolete (meant to be
313 # uninstalled by emerge)
314 # TODO(crbug.com/938605): remove this work around once
315 # https://bugs.gentoo.org/681308 is addressed.
Chris McDonaldd8a7f112019-11-01 10:35:07 -0600316 for tree in flattened_trees:
317 FilterObsoleteDeps(tree)
Ned Nguyene16dcfb2019-03-22 10:36:05 -0600318
Chris McDonalde69db662018-11-15 12:50:18 -0700319 if formatting == 'cpe':
Chris McDonaldd8a7f112019-11-01 10:35:07 -0600320 flattened_trees = tuple(
321 GenerateCPEList(tree, sysroot) for tree in flattened_trees)
322 return flattened_trees
Chris McDonalde69db662018-11-15 12:50:18 -0700323
324
325def main(argv):
Mike Frysinger012371e2019-01-03 03:42:32 -0500326 opts = ParseArgs(argv)
Chris McDonalde69db662018-11-15 12:50:18 -0700327
Mike Frysinger012371e2019-01-03 03:42:32 -0500328 sysroot = opts.sysroot or cros_build_lib.GetSysroot(opts.board)
Chris McDonald486508d2019-11-04 09:20:07 -0700329 deps_list, _ = ExtractDeps(sysroot, opts.pkgs, opts.format)
Yu-Ju Hongf48d3982014-10-30 16:12:16 -0700330
Mike Frysingerd0960812020-06-09 01:53:32 -0400331 pformat.json(deps_list,
332 fp=opts.output_path if opts.output_path else sys.stdout)