portage_util: Fix HasPrebuilt check.
The prebuilt check had been using -K to avoid needing to parse the
output, but -K does not consider local changes, so eclass deps
changes could cause it to report binpkgs that actually needed to
be rebuilt. Refactor to use the depgraph lib instead.
BUG=chromium:1083435
TEST=manual
Change-Id: Id7a644390af09b90f6f486bdc1be8b4fc590b2b4
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/chromite/+/2207897
Tested-by: Alex Klein <saklein@chromium.org>
Commit-Queue: Alex Klein <saklein@chromium.org>
Reviewed-by: Mike Frysinger <vapier@chromium.org>
diff --git a/lib/depgraph.py b/lib/depgraph.py
index db16574..05728a7 100644
--- a/lib/depgraph.py
+++ b/lib/depgraph.py
@@ -288,6 +288,24 @@
vardb.counter_tick()
vardb.flush_cache()
+ def HasPrebuilt(self, pkg_cpf: str):
+ """Check if the given package cpf has a prebuilt.
+
+ Args:
+ pkg_cpf: The fully qualified category/package-version-revision.
+
+ Returns:
+ bool: True if there is a prebuilt available, False otherwise.
+ """
+ if not self.package_db:
+ self.GenDependencyTree()
+
+ package = self.package_db.get(pkg_cpf)
+ if package:
+ return package.type_name == 'binary'
+
+ return False
+
def GenDependencyTree(self):
"""Get dependency tree info from emerge.
@@ -408,6 +426,7 @@
if isinstance(pkg, Package):
# Save off info about the package
deps_info[str(pkg.cpv)] = {'idx': len(deps_info)}
+ self.package_db[pkg.cpv] = pkg
seconds = time.time() - start
if '--quiet' not in emerge.opts:
diff --git a/lib/portage_util.py b/lib/portage_util.py
index 7d0d639..b7bed46 100644
--- a/lib/portage_util.py
+++ b/lib/portage_util.py
@@ -11,6 +11,7 @@
import errno
import glob
import itertools
+import json
import multiprocessing
import os
import re
@@ -2228,22 +2229,27 @@
def HasPrebuilt(atom, board=None, extra_env=None):
"""Check if the atom's best visible version has a prebuilt available."""
- best = PortageqBestVisible(atom, board=board)
+ cmd = [
+ os.path.join(constants.CHROOT_SOURCE_ROOT, 'chromite', 'scripts',
+ 'has_prebuilt'),
+ ]
+ if board:
+ cmd += ['--build-target', board]
+ cmd += [atom]
+ result = cros_build_lib.run(cmd, enter_chroot=True, extra_env=extra_env,
+ capture_output=True, encoding='utf-8')
- emerge = 'emerge-%s' % board if board else 'emerge'
- # Emerge args: binpkg only, no deps, pretend, quiet. --binpkg-respect-use is
- # disabled by default when you use -K, so turn it back on.
- cmd = [emerge, '-gKOpq', '--binpkg-respect-use=y', '=%s' % best.cpf]
- logging.debug('Checking %s for %s.', board or 'sdk', best.cpf)
- result = cros_build_lib.run(
- cmd,
- print_cmd=True,
- enter_chroot=True,
- extra_env=extra_env,
- check=False,
- debug_level=logging.DEBUG)
+ if result.returncode:
+ logging.warning('Error when checking for prebuilts: %s', result.stderr)
+ return False
- return not result.returncode
+ # Script produces {atom:has-prebuilt} mapping.
+ prebuilts = json.loads(result.stdout)
+ if atom not in prebuilts:
+ logging.warning('%s not found in has_prebuilt output.', atom)
+ return False
+
+ return prebuilts[atom]
class PortageqError(Error):
diff --git a/lib/portage_util_unittest.py b/lib/portage_util_unittest.py
index f262954..4c788c6 100644
--- a/lib/portage_util_unittest.py
+++ b/lib/portage_util_unittest.py
@@ -7,6 +7,7 @@
from __future__ import print_function
+import json
import os
import sys
@@ -1486,34 +1487,21 @@
"""HasPrebuilt tests."""
def setUp(self):
- self.atom = 'chromeos-base/chromeos-chrome'
- self.r1_cpf = 'chromeos-base/chromeos-chrome-78.0.3900.0_rc-r1'
- self.r2_cpf = 'chromeos-base/chromeos-chrome-78.0.3900.0_rc-r2'
- self.r1_cpv = portage_util.SplitCPV(self.r1_cpf)
- self.r2_cpv = portage_util.SplitCPV(self.r2_cpf)
-
- def _check_pkg(cmd, *_args, **_kwargs):
- rc = 0 if '=%s' % self.r1_cpf in cmd else 1
- return cros_build_lib.CommandResult(cmd=cmd, returncode=rc)
- self.rc.SetDefaultCmdResult(side_effect=_check_pkg)
+ self.atom = constants.CHROME_CP
def testHasPrebuilt(self):
"""Test a package with a matching prebuilt."""
- self.PatchObject(portage_util, 'PortageqBestVisible',
- return_value=self.r1_cpv)
- self.rc.SetDefaultCmdResult(returncode=0)
+ self.rc.SetDefaultCmdResult(
+ returncode=0, stdout=json.dumps({self.atom: True}))
self.assertTrue(portage_util.HasPrebuilt(self.atom))
- self.rc.assertCommandContains(('=%s' % self.r1_cpf,))
def testNoPrebuilt(self):
"""Test a package without a matching prebuilt."""
- self.PatchObject(portage_util, 'PortageqBestVisible',
- return_value=self.r2_cpv)
- self.rc.SetDefaultCmdResult(returncode=1)
+ self.rc.SetDefaultCmdResult(returncode=0,
+ stdout=json.dumps({self.atom: False}))
self.assertFalse(portage_util.HasPrebuilt(self.atom))
- self.rc.assertCommandContains(('=%s' % self.r2_cpf,))
class PortageqBestVisibleTest(cros_test_lib.MockTestCase):
diff --git a/scripts/has_prebuilt b/scripts/has_prebuilt
new file mode 120000
index 0000000..b68b744
--- /dev/null
+++ b/scripts/has_prebuilt
@@ -0,0 +1 @@
+wrapper3.py
\ No newline at end of file
diff --git a/scripts/has_prebuilt.py b/scripts/has_prebuilt.py
new file mode 100644
index 0000000..8dbacbe
--- /dev/null
+++ b/scripts/has_prebuilt.py
@@ -0,0 +1,90 @@
+# -*- coding: utf-8 -*-
+# Copyright 2020 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.
+
+"""Script to check if the package(s) have prebuilts.
+
+The script must be run inside the chroot. The output is a json dict mapping the
+package atoms to a boolean for whether a prebuilt exists.
+"""
+
+from __future__ import print_function
+
+import json
+
+from chromite.lib import commandline
+from chromite.lib import cros_build_lib
+from chromite.lib import portage_util
+
+if cros_build_lib.IsInsideChroot():
+ from chromite.lib import depgraph
+
+
+def GetParser():
+ """Build the argument parser."""
+ parser = commandline.ArgumentParser(description=__doc__)
+
+ parser.add_argument(
+ '-b',
+ '--build-target',
+ type='build_target',
+ help='The build target that is being checked.')
+ parser.add_argument(
+ 'packages',
+ nargs='+',
+ help='The package atoms that are being checked.')
+
+ return parser
+
+
+def _ParseArguments(argv):
+ """Parse and validate arguments."""
+ parser = GetParser()
+ opts = parser.parse_args(argv)
+
+ # Manually parse the packages as CPVs.
+ packages = []
+ for pkg in opts.packages:
+ cpv = portage_util.SplitCPV(pkg, strict=False)
+ if not cpv.category or not cpv.package:
+ parser.error('Invalid package atom: %s' % pkg)
+
+ packages.append(cpv)
+ opts.packages = packages
+
+ opts.Freeze()
+ return opts
+
+
+def main(argv):
+ opts = _ParseArguments(argv)
+ cros_build_lib.AssertInsideChroot()
+
+ board = opts.build_target.name if opts.build_target else None
+ bests = {}
+ for cpv in opts.packages:
+ bests[cpv.cp] = portage_util.PortageqBestVisible(cpv.cp, board=board)
+
+ # Emerge args:
+ # g: use binpkgs (needed to find if we have one)
+ # u: update packages to latest version (want updates to invalidate binpkgs)
+ # D: deep -- consider full tree rather that just immediate deps
+ # (changes in dependencies and transitive deps can invalidate a binpkg)
+ # N: Packages with changed use flags should be considered
+ # (changes in dependencies and transitive deps can invalidate a binpkg)
+ # q: quiet (simplifies output)
+ # p: pretend (don't actually install it)
+ args = ['-guDNqp', '--with-bdeps=y', '--color=n']
+ if board:
+ args.append('--board=%s' % board)
+ args.extend('=%s' % best.cpf for best in bests.values())
+
+ generator = depgraph.DepGraphGenerator()
+ generator.Initialize(args)
+
+ results = {}
+ for atom, best in bests.items():
+ results[atom] = generator.HasPrebuilt(best.cpf)
+
+ print(json.dumps(results))