blob: 26947304291adf006e7d864358cf2b4b0e8ef521 [file] [log] [blame]
# Copyright 2019 The ChromiumOS Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Operations to work with the SDK chroot."""
import json
import logging
import os
from pathlib import Path
import tempfile
from typing import Dict, List, Optional, Tuple
from chromite.api.gen.chromiumos import common_pb2
from chromite.lib import binpkg
from chromite.lib import chroot_lib
from chromite.lib import constants
from chromite.lib import cros_build_lib
from chromite.lib import cros_sdk_lib
from chromite.lib import gs
from chromite.lib import osutils
from chromite.lib import portage_util
from chromite.lib import sdk_builder_lib
from chromite.utils import key_value_store
# Version of the Manifest file being generated for SDK artifacts. Should be
# incremented for major format changes.
PACKAGE_MANIFEST_VERSION = "1"
class Error(Exception):
"""Base module error."""
class UnmountError(Error):
"""An error raised when unmount fails."""
def __init__(
self,
path: str,
cmd_error: cros_build_lib.RunCommandError,
fs_debug: cros_sdk_lib.FileSystemDebugInfo,
):
super().__init__(path, cmd_error, fs_debug)
self.path = path
self.cmd_error = cmd_error
self.fs_debug = fs_debug
def __str__(self):
return (
f"Umount failed: {self.cmd_error.stdout}.\n"
f"fuser output={self.fs_debug.fuser}\n"
f"lsof output={self.fs_debug.lsof}\n"
f"ps output={self.fs_debug.ps}\n"
)
class CreateArguments(object):
"""Value object to handle the chroot creation arguments."""
def __init__(
self,
replace: bool = False,
bootstrap: bool = False,
chroot: Optional["chroot_lib.Chroot"] = None,
sdk_version: Optional[str] = None,
skip_chroot_upgrade: Optional[bool] = False,
):
"""Create arguments init.
Args:
replace: Whether an existing chroot should be deleted.
bootstrap: Whether to build the SDK from source.
chroot: chroot_lib.Chroot object representing the paths for the
chroot to create.
sdk_version: Specific SDK version to use, e.g. 2022.01.20.073008.
skip_chroot_upgrade: Whether to skip any chroot upgrades (using
the --skip-chroot-upgrade arg to cros_sdk).
"""
self.replace = replace
self.bootstrap = bootstrap
self.chroot = chroot or chroot_lib.Chroot()
self.sdk_version = sdk_version
self.skip_chroot_upgrade = skip_chroot_upgrade
def GetArgList(self) -> List[str]:
"""Get the list of the corresponding command line arguments.
Returns:
The list of the corresponding command line arguments.
"""
args = []
if self.replace:
args.append("--replace")
else:
args.append("--create")
if self.bootstrap:
args.append("--bootstrap")
if self.chroot.cache_dir:
args.extend(["--cache-dir", self.chroot.cache_dir])
args.extend(["--chroot", self.chroot.path])
args.extend(["--out-dir", str(self.chroot.out_path)])
if self.sdk_version:
args.extend(["--sdk-version", self.sdk_version])
if self.skip_chroot_upgrade:
args.append("--skip-chroot-upgrade")
return args
class UpdateArguments(object):
"""Value object to handle the update arguments."""
def __init__(
self,
build_source: bool = False,
toolchain_targets: Optional[List[str]] = None,
toolchain_changed: bool = False,
):
"""Update arguments init.
Args:
build_source: Whether to build the source or use prebuilts.
toolchain_targets: The list of build targets whose toolchains should
be updated.
toolchain_changed: Whether a toolchain change has occurred. Implies
build_source.
"""
self.build_source = build_source or toolchain_changed
self.toolchain_targets = toolchain_targets
def GetArgList(self) -> List[str]:
"""Get the list of the corresponding command line arguments.
Returns:
The list of the corresponding command line arguments.
"""
args = []
if self.build_source:
args.append("--nousepkg")
elif self.toolchain_targets:
args.extend(
["--toolchain_boards", ",".join(self.toolchain_targets)]
)
return args
def Clean(
chroot: Optional["chroot_lib.Chroot"],
images: bool = False,
sysroots: bool = False,
tmp: bool = False,
safe: bool = False,
cache: bool = False,
logs: bool = False,
workdirs: bool = False,
incrementals: bool = False,
) -> None:
"""Clean the chroot.
See:
cros clean -h
Args:
chroot: The chroot to clean.
images: Remove all built images.
sysroots: Remove all of the sysroots.
tmp: Clean the tmp/ directory.
safe: Clean all produced artifacts.
cache: Clean the shared cache.
logs: Clean up various logs.
workdirs: Clean out various package build work directories.
incrementals: Clean out the incremental artifacts.
"""
if not (images or sysroots or tmp or safe or cache or logs or workdirs):
# Nothing specified to clean.
return
cmd = ["cros", "clean", "--debug"]
if chroot:
cmd.extend(["--sdk-path", chroot.path])
cmd.extend(["--out-path", chroot.out_path])
if safe:
cmd.append("--safe")
if images:
cmd.append("--images")
if sysroots:
cmd.append("--sysroots")
if tmp:
cmd.append("--chroot-tmp")
if cache:
cmd.append("--cache")
if logs:
cmd.append("--logs")
if workdirs:
cmd.append("--workdirs")
if incrementals:
cmd.append("--incrementals")
cros_build_lib.run(cmd)
def Create(arguments: CreateArguments) -> Optional[int]:
"""Create or replace the chroot.
Args:
arguments: The various arguments to create a chroot.
Returns:
The version of the resulting chroot.
"""
cros_build_lib.AssertOutsideChroot()
cmd = [constants.CHROMITE_BIN_DIR / "cros_sdk"]
cmd.extend(arguments.GetArgList())
cros_build_lib.run(cmd)
version = GetChrootVersion(arguments.chroot.path)
if not arguments.replace:
# Force replace scenarios. Only needed when we're not already replacing
# it.
if not version:
# Force replace when we can't get a version for a chroot that
# exists, since something must have gone wrong.
logging.notice("Replacing broken chroot.")
arguments.replace = True
return Create(arguments)
elif not cros_sdk_lib.IsChrootVersionValid(arguments.chroot.path):
# Force replace when the version is not valid, i.e. ahead of the
# chroot version hooks.
logging.notice("Replacing chroot ahead of current checkout.")
arguments.replace = True
return Create(arguments)
elif not cros_sdk_lib.IsChrootDirValid(arguments.chroot.path):
# Force replace when the permissions or owner are not correct.
logging.notice("Replacing chroot with invalid permissions.")
arguments.replace = True
return Create(arguments)
return GetChrootVersion(arguments.chroot.path)
def Delete(
chroot: Optional["chroot_lib.Chroot"] = None, force: bool = False
) -> None:
"""Delete the chroot.
Args:
chroot: The chroot being deleted, or None for the default chroot.
force: Whether to apply the --force option.
"""
# Delete the chroot itself.
logging.info("Removing the SDK.")
cmd = [constants.CHROMITE_BIN_DIR / "cros_sdk", "--delete"]
if force:
cmd.extend(["--force"])
if chroot:
cmd.extend(["--chroot", chroot.path])
cmd.extend(["--out-dir", chroot.out_path])
cros_build_lib.run(cmd)
# Remove any images that were built.
logging.info("Removing images.")
Clean(chroot, images=True)
def UnmountPath(path: str) -> None:
"""Unmount the specified path.
Args:
path: The path being unmounted.
"""
logging.info("Unmounting path %s", path)
try:
osutils.UmountTree(path)
except cros_build_lib.RunCommandError as e:
fs_debug = cros_sdk_lib.GetFileSystemDebug(path, run_ps=True)
raise UnmountError(path, e, fs_debug)
def GetChrootVersion(chroot_path: Optional[str] = None) -> Optional[int]:
"""Get the chroot version.
Args:
chroot_path: The chroot path, or None for the default chroot path.
Returns:
The version of the chroot if the chroot is valid, else None.
"""
if chroot_path:
path = chroot_path
elif cros_build_lib.IsInsideChroot():
path = None
else:
path = constants.DEFAULT_CHROOT_PATH
return cros_sdk_lib.GetChrootVersion(path)
def Update(arguments: UpdateArguments) -> Optional[int]:
"""Update the chroot.
Args:
arguments: The various arguments for updating a chroot.
Returns:
The version of the chroot after the update, or None if the chroot is
invalid.
"""
# TODO: This should be able to be run either in or out of the chroot.
cros_build_lib.AssertInsideChroot()
cmd = [constants.CROSUTILS_DIR / "update_chroot"]
cmd.extend(arguments.GetArgList())
# The sdk update uses splitdebug instead of separatedebug. Make sure
# separatedebug is disabled and enable splitdebug.
existing = os.environ.get("FEATURES", "")
features = " ".join((existing, "-separatedebug splitdebug")).strip()
extra_env = {"FEATURES": features}
cros_build_lib.run(cmd, extra_env=extra_env)
return GetChrootVersion()
def _get_remote_latest_file_value(key: str) -> str:
"""Return a value from the remote latest SDK file on GS://, if it exists.
Returns:
The value of the given key in the remote latest file.
Raises:
ValueError: If the given key is not found in the file.
"""
uri = gs.GetGsURL(
constants.SDK_GS_BUCKET,
for_gsutil=True,
suburl="cros-sdk-latest.conf",
)
contents = gs.GSContext().Cat(uri).decode()
contents_dict = key_value_store.LoadData(
contents, source="remote latest SDK file"
)
if key not in contents_dict:
raise ValueError(
f"Unable to find key {key} in latest SDK file ({uri}):\n{contents}"
)
return contents_dict[key]
def get_latest_version() -> str:
"""Return the latest SDK version according to GS://."""
return _get_remote_latest_file_value("LATEST_SDK")
def get_latest_uprev_target_version() -> str:
"""Return the latest-built target version for SDK uprevs form GS://."""
return _get_remote_latest_file_value("LATEST_SDK_UPREV_TARGET")
def _uprev_local_sdk_version_file(
new_sdk_version: str,
new_toolchain_tarball_template: str,
) -> bool:
"""Update the local SDK version file (but don't commit the change).
Args:
new_sdk_version: The SDK version to update to.
new_toolchain_tarball_template: The new value for the TC_PATH
Returns:
True if changes were made, else False.
Raises:
ValueError: If the toolchain tarball template is malformatted.
"""
if "%(target)s" not in new_toolchain_tarball_template:
raise ValueError(
"Toolchain tarball template doesn't contain %(target)s: "
+ new_toolchain_tarball_template
)
logging.info(
"Updating SDK version file (%s)", constants.SDK_VERSION_FILE_FULL_PATH
)
return key_value_store.UpdateKeysInLocalFile(
constants.SDK_VERSION_FILE_FULL_PATH,
{
"SDK_LATEST_VERSION": new_sdk_version,
"TC_PATH": new_toolchain_tarball_template,
},
)
def _uprev_local_host_prebuilts_file(
binhost_gs_bucket: str, target_version: str
) -> bool:
"""Update the local amd64-host prebuilt file (but don't commit the change).
Args:
binhost_gs_bucket: The bucket containing prebuilt files (including
the "gs://" prefix).
target_version: The prebuilts version to sync to. Typically this is
identical to an SDK version (ex. "chroot-2023.03.14.159265"), since
the host prebuilts are typically created during SDK uprevs.
Returns:
True if changes were made, else False.
"""
if not gs.PathIsGs(binhost_gs_bucket):
raise ValueError(
"binhost_gs_bucket doesn't look like a gs path: %s"
% binhost_gs_bucket
)
logging.info(
"Updating amd64-host prebuilt file (%s)",
constants.HOST_PREBUILT_CONF_FILE_FULL_PATH,
)
new_binhost = "%(bucket)s/board/amd64-host/%(version)s/packages/" % {
"bucket": binhost_gs_bucket.rstrip("/"),
"version": target_version,
}
return key_value_store.UpdateKeyInLocalFile(
constants.HOST_PREBUILT_CONF_FILE_FULL_PATH,
"FULL_BINHOST",
new_binhost,
)
def uprev_sdk_and_prebuilts(
binhost_gs_bucket: str, version: str, toolchain_tarball_template: str
) -> List[Path]:
"""Uprev the SDK version and prebuilt conf files on the local filesystem.
Args:
binhost_gs_bucket: The bucket to which prebuilts get uploaded, including
the "gs://" prefix. Example: "gs://chromeos-prebuilt/"
version: The SDK version to uprev to, which is also the prebuilt version
to uprev to. Example: "2023.02.12.144623
toolchain_tarball_template: The new TC_PATH value for the SDK version
file.
Returns:
List of absolute paths to modified files.
"""
modified_paths = []
if _uprev_local_sdk_version_file(version, toolchain_tarball_template):
modified_paths.append(constants.SDK_VERSION_FILE_FULL_PATH)
if _uprev_local_host_prebuilts_file(binhost_gs_bucket, version):
modified_paths.append(constants.HOST_PREBUILT_CONF_FILE_FULL_PATH)
return modified_paths
def BuildPrebuilts(chroot: "chroot_lib.Chroot", board: str = ""):
"""Builds the binary packages that compose the Chromium OS SDK.
Args:
chroot: The chroot in which to run the build.
board: The name of the SDK build target to build packages for.
"""
cmd = ["./build_sdk_board"]
if board:
cmd.append(f"--board={board}")
cros_build_lib.run(
cmd,
enter_chroot=True,
extra_env=chroot.env,
chroot_args=chroot.get_enter_args(),
check=True,
)
def BuildSdkTarball(chroot: "chroot_lib.Chroot") -> Path:
"""Create a tarball previously built (e.g. by BuildPrebuilts) SDK.
Args:
chroot: The chroot that contains the built SDK.
Returns:
The path at which the SDK tarball has been created.
"""
sdk_path = Path(chroot.path) / "build/amd64-host"
return sdk_builder_lib.BuildSdkTarball(sdk_path)
def CreateManifestFromSdk(sdk_path: Path, dest_dir: Path) -> Path:
"""Create a manifest file showing the ebuilds in an SDK.
Args:
sdk_path: The path to the full SDK. (Not a tarball!)
dest_dir: The directory in which the manifest file should be created.
Returns:
The filepath of the generated manifest file.
"""
dest_manifest = dest_dir / f"{constants.SDK_TARBALL_NAME}.Manifest"
# package_data: {"category/package" : [("version", {}), ...]}
package_data: Dict[str, List[Tuple[str, Dict]]] = {}
for package in portage_util.PortageDB(sdk_path).InstalledPackages():
key = f"{package.category}/{package.package}"
package_data.setdefault(key, []).append((package.version, {}))
json_input = dict(version=PACKAGE_MANIFEST_VERSION, packages=package_data)
osutils.WriteFile(dest_manifest, json.dumps(json_input))
return dest_manifest
def CreateBinhostCLs(
prepend_version: str,
version: str,
upload_location: str,
sdk_tarball_template: str,
) -> List[str]:
"""Create CLs that update the binhost to point at uploaded prebuilts.
The CLs are *not* automatically submitted.
Args:
prepend_version: String to prepend to version.
version: The SDK version string.
upload_location: Prefix of the upload path (e.g. 'gs://bucket')
sdk_tarball_template: Template for the path to the SDK tarball.
This will be stored in SDK_VERSION_FILE, and looks something
like '2022/12/%(target)s-2022.12.11.185558.tar.xz'.
Returns:
List of created CLs (in str:num format).
"""
with tempfile.NamedTemporaryFile() as report_file:
cros_build_lib.run(
[
constants.CHROMITE_BIN_DIR / "upload_prebuilts",
"--skip-upload",
"--dry-run",
"--sync-host",
"--git-sync",
"--key",
"FULL_BINHOST",
"--build-path",
constants.SOURCE_ROOT,
"--board",
"amd64-host",
"--set-version",
version,
"--prepend-version",
prepend_version,
"--upload",
upload_location,
"--binhost-conf-dir",
constants.PUBLIC_BINHOST_CONF_DIR,
"--output",
report_file.name,
],
check=True,
)
report = json.load(report_file.file)
sdk_settings = {
"SDK_LATEST_VERSION": version,
"TC_PATH": sdk_tarball_template,
}
# Note: dryrun=True prevents the change from being automatically
# submitted. We only want to create the change, not submit it.
binpkg.UpdateAndSubmitKeyValueFile(
constants.SDK_VERSION_FILE, sdk_settings, report=report, dryrun=True
)
return report["created_cls"]
def UploadPrebuiltPackages(
chroot: "chroot_lib.Chroot",
prepend_version: str,
version: str,
upload_location: str,
):
"""Uploads prebuilt packages (such as built by BuildPrebuilts).
Args:
chroot: The chroot that contains the packages to upload.
prepend_version: String to prepend to version.
version: The SDK version string.
upload_location: Prefix of the upload path (e.g. 'gs://bucket')
"""
cros_build_lib.run(
[
constants.CHROMITE_BIN_DIR / "upload_prebuilts",
"--sync-host",
"--build-path",
constants.SOURCE_ROOT,
"--chroot",
chroot.path,
"--out-dir",
chroot.out_path,
"--board",
"amd64-host",
"--set-version",
version,
"--prepend-version",
prepend_version,
"--upload",
upload_location,
"--binhost-conf-dir",
os.path.join(
constants.SOURCE_ROOT,
"src/third_party/chromiumos-overlay/chromeos/binhost",
),
],
check=True,
)
def BuildSdkToolchain(
chroot: "chroot_lib.Chroot",
extra_env: Optional[Dict[str, str]] = None,
) -> List[common_pb2.Path]:
"""Build cross-compiler toolchain packages for the SDK.
Args:
chroot: The chroot in which the build is being run.
extra_env: Any extra env vars to pass into cros_setup_toolchains.
Returns:
List of generated filepaths.
"""
toolchain_dir = chroot.full_path(constants.SDK_TOOLCHAINS_OUTPUT)
def _SetupToolchains(flags: List[str], include_extra_env: bool):
"""Run the cros_setup_toolchains binary."""
cmd = ["cros_setup_toolchains"] + flags
cros_build_lib.sudo_run(
cmd,
extra_env=extra_env if include_extra_env else None,
enter_chroot=True,
chroot_args=chroot.get_enter_args(),
)
_SetupToolchains(["--nousepkg", "--debug"], True)
osutils.RmDir(
chroot.full_path(constants.SDK_TOOLCHAINS_OUTPUT),
ignore_missing=True,
sudo=True,
)
_SetupToolchains(
[
"--debug",
"--create-packages",
"--output-dir",
os.path.join("/", constants.SDK_TOOLCHAINS_OUTPUT),
],
False,
)
return [
common_pb2.Path(
path=os.path.join("/", constants.SDK_TOOLCHAINS_OUTPUT, filename),
location=common_pb2.Path.INSIDE,
)
for filename in os.listdir(toolchain_dir)
]