blob: 4ccf1073c341d0f06b4e4b5d78cddc311c29695c [file] [log] [blame]
# Copyright 2023 The ChromiumOS Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Logic to handle chromeos-base/protofiles uprev."""
import base64
import dataclasses
import enum
import glob
import logging
from pathlib import Path
import re
import shutil
from typing import Dict, List
import urllib.request
from chromite.lib import cros_build_lib
from chromite.lib import osutils
from chromite.lib import uprev_lib
from chromite.lib.parser import package_info
_REMOTE_BRANCH_CROS_MAIN = "cros/main"
@dataclasses.dataclass(frozen=True)
class ProtofilesModifiedPaths:
"""Paths to files modified by ProtofilesLib.Uprev()"""
version_file_path: Path
new_ebuild_path: Path
old_ebuild_path: Path
class ProtofilesLib:
"""Handles chromeos-base/protofiles uprevs."""
_UPREV_PROJECTS_PATHS = (
"components/policy",
"third_party/private_membership",
"third_party/shell-encryption",
)
_VERSION_URL = (
"https://chromium.googlesource.com/chromium/src.git"
"/+/refs/heads/main/chrome/VERSION?format=TEXT"
)
class _GitObjectType(enum.Enum):
"""Git object types."""
COMMIT = 0
TREE = 1
def Uprev(self, cros_path: Path) -> ProtofilesModifiedPaths:
"""Uprevs chromeos-base/protofiles package.
Uprevs protofiles package with ToT hashes of components/policy,
third_party/private_membership, third_party/shell-encryption.
Args:
cros_path: absolute path to ChromeOS repo checkout
Returns:
ProtofilesModifiedPaths with absolute paths to a version file,
and a new and an old chromeos-base/protofiles ebuild files.
"""
cros_src_path = cros_path / "src"
chromium_src_path = cros_src_path / "chromium/src"
project_full_path_list = [
chromium_src_path / project_path
for project_path in self._UPREV_PROJECTS_PATHS
]
logging.info(
"Fetching latest commit hashes for %s.", self._UPREV_PROJECTS_PATHS
)
commit_hashes = self._FetchLatestCommitHashes(
project_full_path_list, self._GitObjectType.COMMIT
)
tree_hashes = self._FetchLatestCommitHashes(
project_full_path_list, self._GitObjectType.TREE
)
package_name = "protofiles"
package_path = (
cros_src_path
/ "third_party/chromiumos-overlay/chromeos-base"
/ package_name
)
logging.info("Updating ebuild file for %s.", package_path)
modified_ebuilds = self._UpdateEbuildFile(
package_path, package_name, commit_hashes, tree_hashes
)
version_file_path = package_path / "files/VERSION"
version_content = self._FetchChromeVersion()
logging.info("Updating version file %s.", version_file_path)
osutils.WriteFile(version_file_path, version_content, "wb")
return ProtofilesModifiedPaths(
version_file_path=version_file_path,
new_ebuild_path=modified_ebuilds[0],
old_ebuild_path=modified_ebuilds[1],
)
def _FetchLatestCommitHashes(
self, project_full_path_list: List[Path], object_type: _GitObjectType
) -> Dict[Path, str]:
"""Fetches object hashes of the latest commits of passed projects.
Args:
project_full_path_list: list of absolute paths to projects
object_type: _GitObjectType to fetch the hash of
Returns:
A dictionary that maps from |project_full_path| to an object hash
"""
object_type_to_git_format = {
self._GitObjectType.COMMIT: "%H",
self._GitObjectType.TREE: "%T",
}
object_hashes: Dict[Path, str] = {}
git_log_cmd = [
"git",
"log",
"-1",
f"--format={object_type_to_git_format[object_type]}",
_REMOTE_BRANCH_CROS_MAIN,
]
for project_full_path in project_full_path_list:
object_hash = cros_build_lib.run(
git_log_cmd,
capture_output=True,
cwd=project_full_path,
encoding="utf-8",
).stdout.rstrip()
object_hashes[project_full_path] = object_hash
return object_hashes
def _UpdateEbuildFile(
self,
package_path: Path,
package_name: str,
commit_hashes: Dict[Path, str],
tree_hashes: Dict[Path, str],
) -> List[Path]:
"""Updates a stable ebuild file in the |package_path| with new hashes.
Searches for the stable ebuild file with |package_name| prefix
(not *-9999 ebuild) and updates it with the following steps:
1. Rename the ebuild file with incremented index.
2. Updates ebuild's hashes with new |commit_hashes|
and |tree_hashes|.
Args:
package_path: absolute path to the protofiles package
package_name: name of the protofiles package
commit_hashes: dictionary of projects mapped to latest commit hashes
tree_hashes: dictionary of projects mapped to latest tree hashes
Returns:
List with absolute paths to a new and an old
chromeos-base/protofiles ebuild files, respectively.
Raises:
NoEbuildsError: if there are no stable ebuild files found
TooManyStableEbuildsError: if multiple stable ebuild files found
"""
ebuild_full_path_pattern = package_path / f"{package_name}-0.0.*.ebuild"
ebuild_files = glob.glob(str(ebuild_full_path_pattern))
if not ebuild_files:
raise uprev_lib.NoEbuildsError(
f"Have not found a single ebuild file in {package_path}"
)
if len(ebuild_files) > 1:
raise uprev_lib.TooManyStableEbuildsError(
f"Found too many ebuild files in {package_path}"
)
old_filename = Path(ebuild_files[0])
old_package_info = package_info.parse(old_filename)
old_last_version_component = int(
old_package_info.version.split(".")[-1]
)
new_last_version_component = old_last_version_component + 1
new_package_info = package_info.PackageInfo(
old_package_info.category,
old_package_info.package,
f"0.0.{new_last_version_component}",
)
new_ebuild_path = package_path / new_package_info.ebuild
old_ebuild_path = package_path / old_filename
# TODO(python3.9): In python 3.9, shutil.move() accepts
# Path object. Remove the typecast to string, once python
# version moves to 3.9.
shutil.move(str(old_ebuild_path), str(new_ebuild_path))
self._ReplaceHashes(new_ebuild_path, commit_hashes, tree_hashes)
return [new_ebuild_path, old_ebuild_path]
def _ReplaceHashes(
self,
ebuild_path: Path,
commit_hashes: Dict[Path, str],
tree_hashes: Dict[Path, str],
) -> None:
"""Replaces CROS_WORKON_[COMMIT|TREE] hashes in the ebuild file.
Replaces hashes in the file in |ebuild_path| for entries with
the following format:
CROS_WORKON_[COMMIT|TREE] = (
<hash> # <project_name>
<other_hash> # <other_project_name>
)
Args:
ebuild_path: path to ebuild file.
commit_hashes: dictionary of projects with latest commit hashes
tree_hashes: dictionary of projects with latest tree hashes
"""
ebuild_content = osutils.ReadFile(ebuild_path)
assert commit_hashes.keys() == tree_hashes.keys()
for project_full_path in commit_hashes.keys():
project_name = project_full_path.name
commit_hash = commit_hashes[project_full_path]
commit_replacement = rf"\g<1>{commit_hash}\2"
commit_pattern = (
rf"(CROS_WORKON_COMMIT=\([^)]*\")[a-z0-9]*(\" # {project_name})"
)
ebuild_content = re.sub(
commit_pattern, commit_replacement, ebuild_content
)
assert commit_hash in ebuild_content, (
f"commit hash {commit_hash} for {project_name} "
"not found in output"
)
tree_hash = tree_hashes[project_full_path]
tree_replacement = rf"\g<1>{tree_hash}\2"
tree_pattern = (
rf"(CROS_WORKON_TREE=\([^)]*\")[a-z0-9]*(\" # {project_name})"
)
ebuild_content = re.sub(
tree_pattern, tree_replacement, ebuild_content
)
assert (
tree_hash in ebuild_content
), f"tree hash {tree_hash} for {project_name} not found in output"
osutils.WriteFile(ebuild_path, ebuild_content, "w")
def _FetchChromeVersion(self) -> bytes:
"""Fetches the current Chrome version.
Fetches the current Chrome version via an HTTPS request from
the chromium repo.
Currently, the only way to get the contents of the version file
from the repo is to download the base64 encoded contents.
See https://github.com/google/gitiles/issues/7.
Returns:
base64 decoded bytes with the contents of the Chrome version file.
"""
request = urllib.request.Request(self._VERSION_URL)
with urllib.request.urlopen(request) as response:
data = response.read()
version = base64.b64decode(data)
return version