# Copyright (c) 2012 The Chromium OS Authors. All rights reserved. # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. """Command to list patches applies to a repository.""" from __future__ import print_function import functools import json import os import parallel_emerge import portage # pylint: disable=import-error import re import shutil import sys import tempfile from chromite.lib import cros_build_lib from chromite.lib import osutils class PatchReporter(object): """Help discover patches being applied by ebuilds. The patches can be compared to a set of expected patches. They can also be sorted into categories like 'needs_upstreaming', etc. Use of this can help ensure that critical (e.g. security) patches are not inadvertently dropped, and help surface forgotten-about patches that are yet-to-be upstreamed. """ PATCH_TYPES = ('upstreamed', 'needs_upstreaming', 'not_for_upstream', 'uncategorized') def __init__(self, config, overlay_dir, ebuild_cmd, equery_cmd, sudo=False): """Initialize. The 'config' dictionary should look like this: { "ignored_packages": ["chromeos-base/chromeos-chrome"], "upstreamed": [], "needs_upstreaming": [], "not_for_upstream": [], "uncategorized": [ "net-misc/htpdate htpdate-1.0.4-checkagainstbuildtime.patch", "net-misc/htpdate htpdate-1.0.4-errorcheckhttpresp.patch" ] } """ self.overlay_dir = os.path.realpath(overlay_dir) self.ebuild_cmd = ebuild_cmd self.equery_cmd = equery_cmd self._invoke_command = cros_build_lib.RunCommand if sudo: self._invoke_command = functools.partial(cros_build_lib.SudoRunCommand, strict=False) self.ignored_packages = config['ignored_packages'] self.package_count = 0 # The config format is stored as category: [ list of patches ] # for ease of maintenance. But it's actually more useful to us # in the code if kept as a map of patch:patch_type. self.patches = {} for cat in self.PATCH_TYPES: for patch in config[cat]: self.patches[patch] = cat def Ignored(self, package_name): """See if |package_name| should be ignored. Args: package_name: A package name (e.g. 'chromeos-base/chromeos-chrome') Returns: True if this package should be skipped in the analysis. False otherwise. """ return package_name in self.ignored_packages def ObservePatches(self, deps_map): """Observe the patches being applied by ebuilds in |deps_map|. Args: deps_map: The packages to analyze. Returns: A list of patches being applied. """ original = os.environ.get('PORT_LOGDIR', None) temp_space = None try: temp_space = tempfile.mkdtemp(prefix='check_patches') os.environ['PORT_LOGDIR'] = temp_space return self._ObservePatches(temp_space, deps_map) finally: if temp_space: shutil.rmtree(os.environ['PORT_LOGDIR']) if original: os.environ['PORT_LOGDIR'] = original else: os.environ.pop('PORT_LOGDIR') def _ObservePatches(self, temp_space, deps_map): for cpv in deps_map: cat, name, _, _ = portage.versions.catpkgsplit(cpv) if self.Ignored('%s/%s' % (cat, name)): continue cmd = self.equery_cmd[:] cmd.extend(['which', cpv]) ebuild_path = self._invoke_command(cmd, print_cmd=False, redirect_stdout=True).output.rstrip() # Some of these packages will be from other portdirs. Since we are # only interested in extracting the patches from one particular # overlay, we skip ebuilds not from that overlay. if self.overlay_dir != os.path.commonprefix([self.overlay_dir, ebuild_path]): continue # By running 'ebuild blah.ebuild prepare', we get logs in PORT_LOGDIR # of what patches were applied. We clean first, to ensure we get a # complete log, and clean again afterwards to avoid leaving a mess. cmd = self.ebuild_cmd[:] cmd.extend([ebuild_path, 'clean', 'prepare', 'clean']) self._invoke_command(cmd, print_cmd=False, redirect_stdout=True) self.package_count += 1 # Done with ebuild. Now just harvest the logs and we're finished. # This regex is tuned intentionally to ignore a few unhelpful cases. # E.g. elibtoolize repetitively applies a set of sed/portage related # patches. And media-libs/jpeg says it is applying # "various patches (bugfixes/updates)", which isn't very useful for us. # So, if you noticed these omissions, it was intentional, not a bug. :-) patch_regex = r'^ [*] Applying ([^ ]*) [.][.][.].*' output = cros_build_lib.RunCommand( ['egrep', '-r', patch_regex, temp_space], print_cmd=False, redirect_stdout=True).output lines = output.splitlines() patches = [] patch_regex = re.compile(patch_regex) for line in lines: cat, pkg, _, patchmsg = line.split(':') cat = os.path.basename(cat) _, pkg, _, _ = portage.versions.catpkgsplit('x-x/%s' % pkg) patch_name = re.sub(patch_regex, r'\1', patchmsg) patches.append('%s/%s %s' % (cat, pkg, patch_name)) return patches def ReportDiffs(self, observed_patches): """Prints a report on any differences to stdout. Returns: An int representing the total number of discrepancies found. """ expected_patches = set(self.patches.keys()) observed_patches = set(observed_patches) missing_patches = sorted(list(expected_patches - observed_patches)) unexpected_patches = sorted(list(observed_patches - expected_patches)) if missing_patches: print('Missing Patches:') for p in missing_patches: print('%s (%s)' % (p, self.patches[p])) if unexpected_patches: print('Unexpected Patches:') print('\n'.join(unexpected_patches)) return len(missing_patches) + len(unexpected_patches) def Usage(): """Print usage.""" print("""Usage: cros_check_patches [--board=BOARD] [emerge args] package overlay-dir config.json Given a package name (e.g. 'virtual/target-os') and an overlay directory (e.g. /usr/local/portage/chromiumos), outputs a list of patches applied by that overlay, in the course of building the specified package and all its dependencies. Additional configuration options are specified in the JSON-format config file named on the command line. First run? Try this for a starter config: { "ignored_packages": ["chromeos-base/chromeos-chrome"], "upstreamed": [], "needs_upstreaming": [], "not_for_upstream": [], "uncategorized": [] } """) def main(argv): if len(argv) < 4: Usage() sys.exit(1) # Avoid parsing most of argv because most of it is destined for # DepGraphGenerator/emerge rather than us. Extract what we need # without disturbing the rest. config_path = argv.pop() config = json.loads(osutils.ReadFile(config_path)) overlay_dir = argv.pop() board = [x.split('=')[1] for x in argv if x.find('--board=') != -1] if board: ebuild_cmd = ['ebuild-%s' % board[0]] equery_cmd = ['equery-%s' % board[0]] else: ebuild_cmd = ['ebuild'] equery_cmd = ['equery'] use_sudo = not board # We want the toolchain to be quiet to avoid interfering with our output. depgraph_argv = ['--quiet', '--pretend', '--emptytree'] # Defaults to rdeps, but allow command-line override. default_rootdeps_arg = ['--root-deps=rdeps'] for arg in argv: if arg.startswith('--root-deps'): default_rootdeps_arg = [] # Now, assemble the overall argv as the concatenation of the # default list + possible rootdeps-default + actual command line. depgraph_argv.extend(default_rootdeps_arg) depgraph_argv.extend(argv) deps = parallel_emerge.DepGraphGenerator() deps.Initialize(depgraph_argv) deps_tree, deps_info = deps.GenDependencyTree() deps_map = deps.GenDependencyGraph(deps_tree, deps_info) reporter = PatchReporter(config, overlay_dir, ebuild_cmd, equery_cmd, sudo=use_sudo) observed = reporter.ObservePatches(deps_map) diff_count = reporter.ReportDiffs(observed) print('Packages analyzed: %d' % reporter.package_count) print('Patches observed: %d' % len(observed)) print('Patches expected: %d' % len(reporter.patches.keys())) sys.exit(diff_count)