blob: 39af0a2dff2579ce1f15f3c3acbcb580f00124b9 [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
12
Jim Hebert91c052c2011-03-11 11:00:53 -080013import json
Xuewei Zhang656f9932017-09-15 16:15:05 -070014import os
Don Garrettf8bf7842014-03-20 17:03:42 -070015
Jim Hebert91c052c2011-03-11 11:00:53 -080016from parallel_emerge import DepGraphGenerator
Don Garrettf8bf7842014-03-20 17:03:42 -070017
Jim Hebertcf870d72013-06-12 15:33:34 -070018from chromite.lib import commandline
19from chromite.lib import cros_build_lib
Ralph Nathan446aee92015-03-23 14:44:56 -070020from chromite.lib import cros_logging as logging
Xuewei Zhang656f9932017-09-15 16:15:05 -070021from chromite.lib import osutils
22from chromite.lib import portage_util
Mike Frysingercc838832014-05-24 13:10:30 -040023
Yu-Ju Hong7f01e9a2014-10-23 11:01:57 -070024def FlattenDepTree(deptree, pkgtable=None, parentcpv=None, get_cpe=False):
Don Garrett25f309a2014-03-19 14:02:12 -070025 """Simplify dependency json.
26
27Turn something like this (the parallel_emerge DepsTree format):
Jim Hebert91c052c2011-03-11 11:00:53 -080028{
29 "app-admin/eselect-1.2.9": {
30 "action": "merge",
31 "deps": {
32 "sys-apps/coreutils-7.5-r1": {
33 "action": "merge",
34 "deps": {},
35 "deptype": "runtime"
36 },
37 ...
38 }
39 }
40}
41 ...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
63 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.
68
69 Returns:
70 A flattened dependency tree.
Jim Hebert91c052c2011-03-11 11:00:53 -080071 """
David James1b363582012-12-17 11:53:11 -080072 if pkgtable is None:
73 pkgtable = {}
Yu-Ju Hong7f01e9a2014-10-23 11:01:57 -070074 for cpv, record in deptree.iteritems():
Jim Hebert91c052c2011-03-11 11:00:53 -080075 if cpv not in pkgtable:
Xuewei Zhang656f9932017-09-15 16:15:05 -070076 split = portage_util.SplitCPV(cpv)
Yu-Ju Hong7f01e9a2014-10-23 11:01:57 -070077 pkgtable[cpv] = {'deps': [],
78 'rev_deps': [],
Xuewei Zhang656f9932017-09-15 16:15:05 -070079 'name': split.package,
80 'category': split.category,
81 'version': '%s' % split.version,
Yu-Ju Hong7f01e9a2014-10-23 11:01:57 -070082 'full_name': cpv,
83 'cpes': [],
84 'action': record['action']}
85 if get_cpe:
Xuewei Zhang656f9932017-09-15 16:15:05 -070086 pkgtable[cpv]['cpes'].extend(GetCPEFromCPV(
87 split.category, split.package, split.version_no_rev))
Yu-Ju Hong7f01e9a2014-10-23 11:01:57 -070088
Jim Hebert91c052c2011-03-11 11:00:53 -080089 # If we have a parent, that is a rev_dep for the current package.
90 if parentcpv:
Yu-Ju Hong7f01e9a2014-10-23 11:01:57 -070091 pkgtable[cpv]['rev_deps'].append(parentcpv)
Jim Hebert91c052c2011-03-11 11:00:53 -080092 # If current package has any deps, record those.
Yu-Ju Hong7f01e9a2014-10-23 11:01:57 -070093 for childcpv in record['deps']:
94 pkgtable[cpv]['deps'].append(childcpv)
Jim Hebert91c052c2011-03-11 11:00:53 -080095 # Visit the subtree recursively as well.
Yu-Ju Hong7f01e9a2014-10-23 11:01:57 -070096 FlattenDepTree(record['deps'], pkgtable=pkgtable, parentcpv=cpv,
97 get_cpe=get_cpe)
Ned Nguyen3bbd2072019-01-28 19:33:41 -070098 # Sort 'deps' & 'rev_deps' alphabetically to make them more readable.
99 pkgtable[cpv]['deps'].sort()
100 pkgtable[cpv]['rev_deps'].sort()
Jim Hebert91c052c2011-03-11 11:00:53 -0800101 return pkgtable
102
103
Jim Hebertcf870d72013-06-12 15:33:34 -0700104def GetCPEFromCPV(category, package, version):
105 """Look up the CPE for a specified Portage package.
Jim Hebert91c052c2011-03-11 11:00:53 -0800106
Jim Hebertcf870d72013-06-12 15:33:34 -0700107 Args:
108 category: The Portage package's category, e.g. "net-misc"
109 package: The Portage package's name, e.g. "curl"
110 version: The Portage version, e.g. "7.30.0"
111
Mike Frysinger02e1e072013-11-10 22:11:34 -0500112 Returns:
113 A list of CPE Name strings, e.g.
114 ["cpe:/a:curl:curl:7.30.0", "cpe:/a:curl:libcurl:7.30.0"]
Jim Hebertcf870d72013-06-12 15:33:34 -0700115 """
Yu-Ju Hong7f01e9a2014-10-23 11:01:57 -0700116 equery_cmd = ['equery', 'm', '-U', '%s/%s' % (category, package)]
Jim Hebertcf870d72013-06-12 15:33:34 -0700117 lines = cros_build_lib.RunCommand(equery_cmd, error_code_ok=True,
118 print_cmd=False,
119 redirect_stdout=True).output.splitlines()
120 # 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 GenerateSDKCPVList(sysroot):
Xuewei Zhang656f9932017-09-15 16:15:05 -0700153 """Find all SDK packages from package.provided
154
155 Args:
Chris McDonalde69db662018-11-15 12:50:18 -0700156 sysroot: The board directory to use when finding SDK packages.
Xuewei Zhang656f9932017-09-15 16:15:05 -0700157
158 Returns:
159 A list of CPV Name strings, e.g.
160 ["sys-libs/glibc-2.23-r9", "dev-lang/go-1.8.3-r1"]
161 """
162 # Look at packages in package.provided.
Chris McDonalde69db662018-11-15 12:50:18 -0700163 sdk_file_path = os.path.join(sysroot, 'etc', 'portage',
Xuewei Zhang656f9932017-09-15 16:15:05 -0700164 'profile', 'package.provided')
165 for line in osutils.ReadFile(sdk_file_path).splitlines():
166 # Skip comments and empty lines.
167 line = line.split('#', 1)[0].strip()
168 if not line:
169 continue
170 yield line
171
172
Chris McDonalde69db662018-11-15 12:50:18 -0700173def GenerateCPEList(deps_list, sysroot):
Xuewei Zhang656f9932017-09-15 16:15:05 -0700174 """Generate all CPEs for the packages included in deps_list and SDK packages
175
176 Args:
177 deps_list: A flattened dependency tree (cros_extract_deps format).
Chris McDonalde69db662018-11-15 12:50:18 -0700178 sysroot: The board directory to use when finding SDK packages.
Xuewei Zhang656f9932017-09-15 16:15:05 -0700179
180 Returns:
181 A list of CPE info for packages in deps_list and SDK packages, e.g.
182 [
183 {
184 "ComponentName": "app-admin/sudo",
185 "Repository": "cros",
186 "Targets": [
187 "cpe:/a:todd_miller:sudo:1.8.19p2"
188 ]
189 },
190 {
191 "ComponentName": "sys-libs/glibc",
192 "Repository": "cros",
193 "Targets": [
194 "cpe:/a:gnu:glibc:2.23"
195 ]
196 }
197 ]
198 """
Jim Hebertcf870d72013-06-12 15:33:34 -0700199 cpe_dump = []
Xuewei Zhang656f9932017-09-15 16:15:05 -0700200
201 # Generage CPEs for SDK packages.
Mike Frysinger1457e482019-01-03 04:04:02 -0500202 for sdk_cpv in sorted(GenerateSDKCPVList(sysroot)):
Xuewei Zhang656f9932017-09-15 16:15:05 -0700203 # Only add CPE for SDK CPVs missing in deps_list.
204 if deps_list.get(sdk_cpv) is not None:
205 continue
206
207 split = portage_util.SplitCPV(sdk_cpv)
208 cpes = GetCPEFromCPV(split.category, split.package, split.version_no_rev)
209 if cpes:
Alex Klein9f93b482018-10-01 09:26:51 -0600210 cpe_dump.append({'ComponentName': '%s' % split.cp,
Xuewei Zhang656f9932017-09-15 16:15:05 -0700211 'Repository': 'cros',
212 'Targets': sorted(cpes)})
213 else:
214 logging.warning('No CPE entry for %s', sdk_cpv)
215
216 # Generage CPEs for packages in deps_list.
Mike Frysinger1457e482019-01-03 04:04:02 -0500217 for cpv, record in sorted(deps_list.items()):
Yu-Ju Hong7f01e9a2014-10-23 11:01:57 -0700218 if record['cpes']:
219 name = '%s/%s' % (record['category'], record['name'])
220 cpe_dump.append({'ComponentName': name,
221 'Repository': 'cros',
222 'Targets': sorted(record['cpes'])})
Jim Hebertcf870d72013-06-12 15:33:34 -0700223 else:
Ralph Nathan446aee92015-03-23 14:44:56 -0700224 logging.warning('No CPE entry for %s', cpv)
Yu-Ju Hong7f01e9a2014-10-23 11:01:57 -0700225 return sorted(cpe_dump, key=lambda k: k['ComponentName'])
Jim Hebert91c052c2011-03-11 11:00:53 -0800226
227
Chris McDonalde69db662018-11-15 12:50:18 -0700228def ParseArgs(argv):
229 """Parse command line arguments."""
Mike Frysinger012371e2019-01-03 03:42:32 -0500230 parser = commandline.ArgumentParser(description=__doc__)
Chris McDonalde69db662018-11-15 12:50:18 -0700231 target = parser.add_mutually_exclusive_group()
232 target.add_argument('--sysroot', type='path', help='Path to the sysroot.')
233 target.add_argument('--board', help='Board name.')
234
Yu-Ju Hong7f01e9a2014-10-23 11:01:57 -0700235 parser.add_argument('--format', default='deps',
236 choices=['deps', 'cpe'],
Yu-Ju Hongf48d3982014-10-30 16:12:16 -0700237 help='Output either traditional deps or CPE-only JSON.')
238 parser.add_argument('--output-path', default=None,
239 help='Write output to the given path.')
Mike Frysinger012371e2019-01-03 03:42:32 -0500240 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):
247 """Remove all the packages that are to be uninstalled from |package_deps|.
248
249 Returns:
250 None since this method mutates |package_deps| directly.
251 """
252 obsolete_package_deps = []
253 for k, v in package_deps.iteritems():
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, (
260 'Unrecognized action. Package dep data: %s' % v)
261 for p in obsolete_package_deps:
262 del package_deps[p]
263
264
Chris McDonalde69db662018-11-15 12:50:18 -0700265def ExtractDeps(sysroot, package_list, formatting='deps'):
Ned Nguyendd3e09f2019-03-14 18:54:03 -0600266 """Returns the set of dependencies for the packages in package_list.
267
268 For calculating dependencies graph, this should only consider packages
269 that are DEPENDS or RDEPENDS. Essentially, this should answer the question
270 "which are all the packages which changing them may change the execution
271 of any binaries produced by packages in |package_list|."
272
273 Args:
274 sysroot: the path (string) to the root directory into which the package is
275 pretend to be merged. This value is also used for setting
276 PORTAGE_CONFIGROOT.
277 package_list: the list of packages (CP string) to extract their dependencies
278 from.
279 formatting: can either be 'deps' or 'cpe'. For 'deps', see the return
280 format in docstring of FlattenDepTree, for 'cpe', see the return format in
281 docstring of GenerateCPEList.
282
283 Returns:
284 A JSON-izable object that either follows 'deps' or 'cpe' format.
285 """
Chris McDonalde69db662018-11-15 12:50:18 -0700286 lib_argv = ['--quiet', '--pretend', '--emptytree']
287 lib_argv += ['--sysroot=%s' % sysroot]
288 lib_argv.extend(package_list)
Jim Hebert91c052c2011-03-11 11:00:53 -0800289
290 deps = DepGraphGenerator()
Jim Hebertcf870d72013-06-12 15:33:34 -0700291 deps.Initialize(lib_argv)
David James1b363582012-12-17 11:53:11 -0800292 deps_tree, _deps_info = deps.GenDependencyTree()
Chris McDonalde69db662018-11-15 12:50:18 -0700293 flattened_deps = FlattenDepTree(deps_tree, get_cpe=(formatting == 'cpe'))
Ned Nguyene16dcfb2019-03-22 10:36:05 -0600294
295 # Workaround: since emerge doesn't honor the --emptytree flag, for now we need
296 # to manually filter out packages that are obsolete (meant to be
297 # uninstalled by emerge)
298 # TODO(crbug.com/938605): remove this work around once
299 # https://bugs.gentoo.org/681308 is addressed.
300 FilterObsoleteDeps(flattened_deps)
301
Chris McDonalde69db662018-11-15 12:50:18 -0700302 if formatting == 'cpe':
303 flattened_deps = GenerateCPEList(flattened_deps, sysroot)
304 return flattened_deps
305
306
307def main(argv):
Mike Frysinger012371e2019-01-03 03:42:32 -0500308 opts = ParseArgs(argv)
Chris McDonalde69db662018-11-15 12:50:18 -0700309
Mike Frysinger012371e2019-01-03 03:42:32 -0500310 sysroot = opts.sysroot or cros_build_lib.GetSysroot(opts.board)
311 deps_list = ExtractDeps(sysroot, opts.pkgs, opts.format)
Yu-Ju Hongf48d3982014-10-30 16:12:16 -0700312
313 deps_output = json.dumps(deps_list, sort_keys=True, indent=2)
Mike Frysinger012371e2019-01-03 03:42:32 -0500314 if opts.output_path:
315 with open(opts.output_path, 'w') as f:
Yu-Ju Hongf48d3982014-10-30 16:12:16 -0700316 f.write(deps_output)
317 else:
318 print(deps_output)