uprev_lib: Add function to uprev cros-workon package to specific version
Add a new function `uprev_workon_ebuild_to_version` for use in PUPr
endpoints to uprev a cros-workon package while being able to control the
version of the stable ebuild that gets emitted.
This function differs from `uprev_ebuild_from_pin` in two significant ways:
1. It emits a proper stable cros-workon ebuild with derived CROS_WORKON
variables written out.
2. It doesn't require a stable ebuild to exist yet.
BUG=chromium:1139412, chromium:1071391
TEST=`run_pytest`
Change-Id: I31c7400bf81a060d513813d242cfa7e222b2c2f4
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/chromite/+/2481502
Tested-by: Chris McDonald <cjmcdonald@chromium.org>
Reviewed-by: Michael Mortensen <mmortensen@google.com>
Commit-Queue: Chris McDonald <cjmcdonald@chromium.org>
diff --git a/conftest.py b/conftest.py
index 2a426e9..b1281ab 100644
--- a/conftest.py
+++ b/conftest.py
@@ -76,6 +76,10 @@
# pylint: disable=protected-access
retry_stats._STATS_COLLECTION = None
+@pytest.fixture(autouse=True)
+def set_testing_environment_variable(monkeypatch):
+ """Set an environment marker to relax certain strict checks for test code."""
+ monkeypatch.setenv('CHROMITE_INSIDE_PYTEST', '1')
@pytest.fixture
def singleton_manager(monkeypatch):
diff --git a/lib/portage_util.py b/lib/portage_util.py
index fcdb335..ad8a2b0 100644
--- a/lib/portage_util.py
+++ b/lib/portage_util.py
@@ -835,8 +835,15 @@
# See what git repo the ebuild lives in to make sure the ebuild isn't
# tracking the same repo. https://crbug.com/1050663
+ # We set strict=False if we're running under pytest because it's valid for
+ # ebuilds created in tests to not live in a git tree.
+ under_test = os.environ.get('CHROMITE_INSIDE_PYTEST') == '1'
ebuild_git_tree = manifest.FindCheckoutFromPath(
- self.ebuild_path).get('local_path')
+ self.ebuild_path, strict=not under_test)
+ if ebuild_git_tree:
+ ebuild_git_tree_path = ebuild_git_tree.get('local_path')
+ else:
+ ebuild_git_tree_path = None
subdir_paths = []
subtree_paths = []
@@ -871,7 +878,7 @@
real_project,
project))
- if subdir_path == ebuild_git_tree:
+ if subdir_path == ebuild_git_tree_path:
msg = ('%s: ebuilds may not live in & track the same source '
'repository (%s); use the empty-project instead' %
(self.ebuild_path, subdir_path))
diff --git a/lib/portage_util_unittest.py b/lib/portage_util_unittest.py
index 287db37..fd707f9 100644
--- a/lib/portage_util_unittest.py
+++ b/lib/portage_util_unittest.py
@@ -363,7 +363,7 @@
raise Exception('unhandled path: %s' % path)
- def _FindCheckoutFromPath(path):
+ def _FindCheckoutFromPath(path, strict=True): # pylint: disable=unused-argument
"""Mock function for manifest.FindCheckoutFromPath"""
for project, localname in zip(fake_projects, fake_localnames):
if path == os.path.join(self.tempdir, 'platform', localname):
diff --git a/lib/uprev_lib.py b/lib/uprev_lib.py
index 7ac6f62..5fb2868 100644
--- a/lib/uprev_lib.py
+++ b/lib/uprev_lib.py
@@ -20,6 +20,7 @@
from chromite.lib import constants
from chromite.lib import cros_build_lib
from chromite.lib import cros_logging as logging
+from chromite.lib import git
from chromite.lib import osutils
from chromite.lib import parallel
from chromite.lib import portage_util
@@ -718,3 +719,114 @@
stable_ebuild.ebuild_path,
manifest_src_path])
return result
+
+
+def uprev_workon_ebuild_to_version(
+ package_path: Union[str, 'pathlib.Path'],
+ target_version: str,
+ chroot: Optional['chromite.lib.chroot_lib.Chroot'] = None,
+ *,
+ src_root: str = constants.SOURCE_ROOT,
+ chroot_src_root: str = constants.CHROOT_SOURCE_ROOT) -> UprevResult:
+ """Uprev a cros-workon ebuild to a specified version.
+
+ Args:
+ package_path: The path of the package relative to the src root. This path
+ should contain an unstable 9999 ebuild that inherits from cros-workon.
+ target_version: The version to use for the stable ebuild to be generated.
+ Should not contain a revision number.
+ chroot: The path to the chroot to enter, if not the default.
+ srcroot: Path to the root of the source checkout. Only override for testing.
+ chroot_src_root: Path to the root of the source checkout when inside the
+ chroot. Only override for testing.
+ """
+
+ package_path = str(package_path)
+ package = os.path.basename(package_path)
+
+ package_src_path = os.path.join(src_root, package_path)
+ ebuild_paths = list(portage_util.EBuild.List(package_src_path))
+ stable_ebuild = None
+ unstable_ebuild = None
+ for path in ebuild_paths:
+ ebuild = portage_util.EBuild(path)
+ if ebuild.is_stable:
+ stable_ebuild = ebuild
+ else:
+ unstable_ebuild = ebuild
+
+ outcome = None
+
+ if stable_ebuild is None:
+ outcome = outcome or Outcome.NEW_EBUILD_CREATED
+ if unstable_ebuild is None:
+ raise EbuildUprevError(f'No unstable ebuild found for {package}')
+ if len(ebuild_paths) > 2:
+ raise EbuildUprevError(f'Found too many ebuilds for {package}: '
+ 'expected one stable and one unstable')
+
+ if not unstable_ebuild.is_workon:
+ raise EbuildUprevError('A workon ebuild was expected '
+ f'but {unstable_ebuild.ebuild_path} is not workon.')
+ # If the new version is the same as the old version, bump the revision number,
+ # otherwise reset it to 1
+ if stable_ebuild and target_version == stable_ebuild.version_no_rev:
+ output_version = f'{target_version}-r{stable_ebuild.current_revision + 1}'
+ outcome = outcome or Outcome.REVISION_BUMP
+ else:
+ output_version = f'{target_version}-r1'
+ outcome = outcome or Outcome.VERSION_BUMP
+
+ new_ebuild_path = os.path.join(package_path,
+ f'{package}-{output_version}.ebuild')
+ new_ebuild_src_path = os.path.join(src_root, new_ebuild_path)
+ manifest_src_path = os.path.join(package_src_path, 'Manifest')
+
+ # Go through the normal uprev process for a cros-workon ebuild, by calculating
+ # and writing out the commit & tree IDs for the projects and subtrees
+ # specified in the unstable ebuild.
+ manifest = git.ManifestCheckout.Cached(constants.SOURCE_ROOT)
+ info = unstable_ebuild.GetSourceInfo(
+ os.path.join(constants.SOURCE_ROOT, 'src'), manifest)
+ commit_ids = [unstable_ebuild.GetCommitId(x) for x in info.srcdirs]
+ if not commit_ids:
+ raise EbuildUprevError('No commit_ids found for %s' % info.srcdirs)
+
+ tree_ids = [unstable_ebuild.GetTreeId(x) for x in info.subtrees]
+ tree_ids = [tree_id for tree_id in tree_ids if tree_id]
+ if not tree_ids:
+ raise EbuildUprevError('No tree_ids found for %s' % info.subtrees)
+
+ variables = dict(
+ CROS_WORKON_COMMIT=unstable_ebuild.FormatBashArray(commit_ids),
+ CROS_WORKON_TREE=unstable_ebuild.FormatBashArray(tree_ids))
+
+ portage_util.EBuild.MarkAsStable(unstable_ebuild.ebuild_path,
+ new_ebuild_src_path, variables)
+
+ # If the newly generated stable ebuild is identical to the previous one,
+ # early return without incrementing the revision number.
+ if stable_ebuild and target_version == stable_ebuild.version_no_rev and \
+ filecmp.cmp(
+ new_ebuild_src_path, stable_ebuild.ebuild_path, shallow=False):
+ return UprevResult(outcome=Outcome.SAME_VERSION_EXISTS)
+
+ if stable_ebuild is not None:
+ osutils.SafeUnlink(stable_ebuild.ebuild_path)
+
+ try:
+ # UpdateEbuildManifest runs inside the chroot and therefore needs a
+ # chroot-relative path.
+ new_ebuild_chroot_path = os.path.join(chroot_src_root, new_ebuild_path)
+ portage_util.UpdateEbuildManifest(new_ebuild_chroot_path, chroot=chroot)
+ except cros_build_lib.RunCommandError as e:
+ raise EbuildManifestError(
+ f'Unable to update manifest for {package}: {e.stderr}')
+
+ result = UprevResult(
+ outcome=outcome, changed_files=[new_ebuild_src_path, manifest_src_path])
+
+ if stable_ebuild is not None:
+ result.changed_files.append(stable_ebuild.ebuild_path)
+
+ return result
diff --git a/lib/uprev_lib_unittest.py b/lib/uprev_lib_unittest.py
index 4d0687b..8a52549 100644
--- a/lib/uprev_lib_unittest.py
+++ b/lib/uprev_lib_unittest.py
@@ -9,9 +9,11 @@
from __future__ import print_function
import os
+import pathlib
import sys
import mock
+import pytest
import chromite as cr
from chromite.lib import constants
@@ -21,6 +23,7 @@
from chromite.lib import uprev_lib
from chromite.lib.build_target_lib import BuildTarget
from chromite.lib.chroot_lib import Chroot
+from chromite.lib.parser import package_info
assert sys.version_info >= (3, 6), 'This module requires Python 3.6+'
@@ -461,3 +464,68 @@
'chromeos-base', 'chromeos-chrome', version=f'{NEW_CHROME_VERSION}_rc-r1')
assert stable_chrome.cpv in overlay
+
+
+@pytest.mark.inside_only
+def test_non_workon_fails_uprev_workon_ebuild_to_version(overlay_stack):
+ overlay, = overlay_stack(1)
+ unstable_package = cr.test.Package(
+ 'chromeos-base',
+ 'test-package',
+ version='9999',
+ keywords='~*',
+ )
+
+ overlay.add_package(unstable_package)
+
+ with pytest.raises(uprev_lib.EbuildUprevError):
+ uprev_lib.uprev_workon_ebuild_to_version(
+ pathlib.Path(unstable_package.category) / unstable_package.package,
+ target_version='1',
+ chroot=None,
+ src_root=overlay.path,
+ chroot_src_root=overlay.path,
+ )
+
+ stable_package = package_info.PackageInfo(
+ 'chromeos-base',
+ 'test-package',
+ version='1',
+ revision='1',
+ )
+
+ assert not stable_package in overlay
+
+
+@pytest.mark.inside_only
+def test_simple_uprev_workon_ebuild_to_version(overlay_stack):
+ overlay, = overlay_stack(1)
+ unstable_package = cr.test.Package(
+ 'chromeos-base',
+ 'test-package',
+ version='9999',
+ keywords='~*',
+ inherit='cros-workon',
+ CROS_WORKON_PROJECT='chromiumos/infra/build/empty-project',
+ CROS_WORKON_LOCALNAME='empty-project')
+
+ overlay.add_package(unstable_package)
+
+ res = uprev_lib.uprev_workon_ebuild_to_version(
+ pathlib.Path(unstable_package.category) / unstable_package.package,
+ target_version='1',
+ chroot=None,
+ src_root=overlay.path,
+ chroot_src_root=overlay.path,
+ )
+
+ assert res.outcome is uprev_lib.Outcome.NEW_EBUILD_CREATED
+
+ stable_package = package_info.PackageInfo(
+ 'chromeos-base',
+ 'test-package',
+ version='1',
+ revision='1',
+ )
+
+ assert stable_package in overlay