blob: e5d02f346d8d48cec580749ff3819ee0197df439 [file] [log] [blame]
# -*- coding: utf-8 -*-
# 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.
"""Module containing the various individual commands a builder can run."""
from __future__ import print_function
import base64
import collections
import datetime
import fnmatch
import glob
import json
import multiprocessing
import os
import re
import shutil
import sys
import tempfile
from google.protobuf import json_format
import six
from chromite.api.gen.chromite.api import android_pb2
from chromite.cbuildbot import swarming_lib
from chromite.cbuildbot import topology
from chromite.lib import buildbot_annotations
from chromite.lib import chroot_lib
from chromite.lib import cipd
from chromite.lib import config_lib
from chromite.lib import constants
from chromite.lib import cros_build_lib
from chromite.lib import cros_logging as logging
from chromite.lib import failures_lib
from chromite.lib import gob_util
from chromite.lib import gs
from chromite.lib import image_lib
from chromite.lib import metrics
from chromite.lib import osutils
from chromite.lib import parallel
from chromite.lib import path_util
from chromite.lib import portage_util
from chromite.lib import retry_util
from chromite.lib import sysroot_lib
from chromite.lib import timeout_util
from chromite.lib.paygen import filelib
from chromite.scripts import pushimage
from chromite.service import artifacts as artifacts_service
_PACKAGE_FILE = '%(buildroot)s/src/scripts/cbuildbot_package.list'
CHROME_KEYWORDS_FILE = ('/build/%(board)s/etc/portage/package.keywords/chrome')
CHROME_UNMASK_FILE = ('/build/%(board)s/etc/portage/package.unmask/chrome')
_CROS_ARCHIVE_URL = 'CROS_ARCHIVE_URL'
_FACTORY_SHIM = 'factory_shim'
_AUTOTEST_RPC_CLIENT = ('/b/build_internal/scripts/slave-internal/autotest_rpc/'
'autotest_rpc_client.py')
_AUTOTEST_RPC_HOSTNAME = 'master2'
LOCAL_BUILD_FLAGS = ['--nousepkg', '--reuse_pkgs_from_local_boards']
UPLOADED_LIST_FILENAME = 'UPLOADED'
STATEFUL_FILE = 'stateful.tgz'
# For swarming proxy
_SWARMING_ADDITIONAL_TIMEOUT = 60 * 60
_DEFAULT_HWTEST_TIMEOUT_MINS = 1440
_SWARMING_EXPIRATION = 20 * 60
RUN_SUITE_PATH = '/usr/local/autotest/site_utils/run_suite.py'
SKYLAB_RUN_SUITE_PATH = '/usr/local/autotest/bin/run_suite_skylab'
_ABORT_SUITE_PATH = '/usr/local/autotest/site_utils/abort_suite.py'
_SKYLAB_ABORT_SUITE_PATH = '/usr/local/autotest/bin/abort_suite_skylab'
_MAX_HWTEST_CMD_RETRY = 10
SKYLAB_SUITE_BOT_POOL = 'ChromeOSSkylab-suite'
SUITE_BOT_POOL = 'default'
# Be very careful about retrying suite creation command as
# it may create multiple suites.
_MAX_HWTEST_START_CMD_RETRY = 1
# For json_dump json dictionary marker
JSON_DICT_START = '#JSON_START#'
JSON_DICT_END = '#JSON_END#'
# Filename for tarball containing Autotest server files needed for Server-Side
# Packaging.
AUTOTEST_SERVER_PACKAGE = 'autotest_server_package.tar.bz2'
# Directory within AUTOTEST_SERVER_PACKAGE where Tast files needed to run with
# Server-Side Packaging are stored.
_TAST_SSP_SUBDIR = 'tast'
# Tast files and directories to include in AUTOTEST_SERVER_PACKAGE relative to
# the build root. Public so it can be used by commands_unittest.py.
TAST_SSP_FILES = [
'chroot/usr/bin/tast', # Main Tast executable.
'chroot/usr/bin/remote_test_runner', # Runs remote tests.
'chroot/usr/libexec/tast/bundles', # Dir containing test bundles.
'chroot/usr/share/tast/data', # Dir containing test data.
'src/platform/tast/tools/run_tast.sh', # Helper script to run SSP tast.
]
# =========================== Command Helpers =================================
def RunBuildScript(buildroot, cmd, chromite_cmd=False, **kwargs):
"""Run a build script, wrapping exceptions as needed.
This wraps run(cmd, cwd=buildroot, **kwargs), adding extra logic to help
determine the cause of command failures.
- If a package fails to build, a PackageBuildFailure exception is thrown,
which lists exactly which packages failed to build.
- If the command fails for a different reason, a BuildScriptFailure
exception is thrown.
We detect what packages failed to build by creating a temporary status file,
and passing that status file to parallel_emerge via the
PARALLEL_EMERGE_STATUS_FILE variable.
Args:
buildroot: The root of the build directory.
cmd: The command to run.
chromite_cmd: Whether the command should be evaluated relative to the
chromite/bin subdir of the |buildroot|.
kwargs: Optional args passed to run; see cros_build_lib.run for specifics.
In addition, if 'sudo' kwarg is True, sudo_run will be used.
"""
assert not kwargs.get('shell', False), 'Cannot execute shell commands'
kwargs.setdefault('cwd', buildroot)
enter_chroot = kwargs.get('enter_chroot', False)
sudo = kwargs.pop('sudo', False)
if chromite_cmd:
cmd = cmd[:]
cmd[0] = os.path.join(buildroot, constants.CHROMITE_BIN_SUBDIR, cmd[0])
if enter_chroot:
cmd[0] = path_util.ToChrootPath(cmd[0])
# If we are entering the chroot, create status file for tracking what
# packages failed to build.
chroot_tmp = os.path.join(buildroot, 'chroot', 'tmp')
status_file = None
with cros_build_lib.ContextManagerStack() as stack:
if enter_chroot and os.path.exists(chroot_tmp):
kwargs['extra_env'] = (kwargs.get('extra_env') or {}).copy()
status_file = stack.Add(tempfile.NamedTemporaryFile, dir=chroot_tmp)
kwargs['extra_env'][constants.PARALLEL_EMERGE_STATUS_FILE_ENVVAR] = \
path_util.ToChrootPath(status_file.name)
runcmd = cros_build_lib.run
if sudo:
runcmd = cros_build_lib.sudo_run
try:
return runcmd(cmd, **kwargs)
except cros_build_lib.RunCommandError as ex:
# Print the original exception.
logging.error('\n%s', ex)
# Check whether a specific package failed. If so, wrap the exception
# appropriately. These failures are usually caused by a recent CL, so we
# don't ever treat these failures as flaky.
if status_file is not None:
status_file.seek(0)
failed_packages = status_file.read().split()
if failed_packages:
raise failures_lib.PackageBuildFailure(ex, cmd[0], failed_packages)
# Looks like a generic failure. Raise a BuildScriptFailure.
raise failures_lib.BuildScriptFailure(ex, cmd[0])
def ValidateClobber(buildroot):
"""Do due diligence if user wants to clobber buildroot.
Args:
buildroot: buildroot that's potentially clobbered.
Returns:
True if the clobber is ok.
"""
cwd = os.path.dirname(os.path.realpath(__file__))
if cwd.startswith(buildroot):
cros_build_lib.Die('You are trying to clobber this chromite checkout!')
if buildroot == '/':
cros_build_lib.Die('Refusing to clobber your system!')
if os.path.exists(buildroot):
return cros_build_lib.BooleanPrompt(default=False)
return True
# =========================== Main Commands ===================================
def WipeOldOutput(buildroot):
"""Wipes out build output directory.
Args:
buildroot: Root directory where build occurs.
board: Delete image directories for this board name.
"""
image_dir = os.path.join(buildroot, 'src', 'build', 'images')
osutils.RmDir(image_dir, ignore_missing=True, sudo=True)
def MakeChroot(buildroot,
replace,
use_sdk,
chrome_root=None,
extra_env=None,
use_image=False,
cache_dir=None):
"""Wrapper around make_chroot."""
cmd = ['cros_sdk', '--buildbot-log-version']
if not use_image:
cmd.append('--nouse-image')
if use_sdk:
cmd.append('--create')
else:
cmd.append('--bootstrap')
if replace:
cmd.append('--replace')
if chrome_root:
cmd.append('--chrome_root=%s' % chrome_root)
if cache_dir:
cmd += ['--cache-dir', cache_dir]
RunBuildScript(buildroot, cmd, chromite_cmd=True, extra_env=extra_env)
def ListChrootSnapshots(buildroot):
"""Wrapper around cros_sdk --snapshot-list."""
cmd = ['cros_sdk', '--snapshot-list']
cmd_snapshots = RunBuildScript(
buildroot, cmd, chromite_cmd=True, redirect_stdout=True)
return cmd_snapshots.output.splitlines()
def RevertChrootToSnapshot(buildroot, snapshot_name):
"""Wrapper around cros_sdk --snapshot-restore."""
cmd = ['cros_sdk', '--snapshot-restore', snapshot_name]
result = RunBuildScript(buildroot, cmd, chromite_cmd=True, error_code_ok=True)
return result.returncode == 0
def CreateChrootSnapshot(buildroot, snapshot_name):
"""Wrapper around cros_sdk --snapshot-create."""
cmd = ['cros_sdk', '--snapshot-create', snapshot_name]
result = RunBuildScript(buildroot, cmd, chromite_cmd=True, error_code_ok=True)
return result.returncode == 0
def DeleteChrootSnapshot(buildroot, snapshot_name):
"""Wrapper around cros_sdk --snapshot-delete."""
cmd = ['cros_sdk', '--snapshot-delete', snapshot_name]
result = RunBuildScript(buildroot, cmd, chromite_cmd=True, error_code_ok=True)
return result.returncode == 0
def UpdateChroot(buildroot,
usepkg,
toolchain_boards=None,
extra_env=None,
chroot_args=None):
"""Wrapper around update_chroot.
Args:
buildroot: The buildroot of the current build.
usepkg: Whether to use binary packages when setting up the toolchain.
toolchain_boards: List of boards to always include.
extra_env: A dictionary of environmental variables to set during generation.
chroot_args: The args to the chroot.
"""
cmd = ['./update_chroot']
if not usepkg:
cmd.extend(['--nousepkg'])
if toolchain_boards:
cmd.extend(['--toolchain_boards', ','.join(toolchain_boards)])
# workaround https://crbug.com/225509
# Building with FEATURES=separatedebug will create a dedicated tarball with
# the debug files, and the debug files won't be in the glibc.tbz2, which is
# where the build scripts expect them.
extra_env_local = extra_env.copy()
extra_env_local.setdefault('FEATURES', '')
extra_env_local['FEATURES'] += ' -separatedebug splitdebug'
RunBuildScript(
buildroot,
cmd,
enter_chroot=True,
chroot_args=chroot_args,
extra_env=extra_env_local)
def SetupBoard(buildroot,
board,
usepkg,
extra_env=None,
force=False,
profile=None,
chroot_upgrade=True,
chroot_args=None):
"""Wrapper around setup_board.
Args:
buildroot: The buildroot of the current build.
board: The board to set up.
usepkg: Whether to use binary packages when setting up the board.
extra_env: A dictionary of environmental variables to set during generation.
force: Whether to remove the board prior to setting it up.
profile: The profile to use with this board.
chroot_upgrade: Whether to update the chroot. If the chroot is already up to
date, you can specify chroot_upgrade=False.
chroot_args: The args to the chroot.
"""
cmd = ['setup_board', '--board=%s' % board, '--accept-licenses=@CHROMEOS']
# This isn't the greatest thing, but emerge's dependency calculation
# isn't the speediest thing, so let callers skip this step when they
# know the system is up-to-date already.
if not chroot_upgrade:
cmd.append('--skip-chroot-upgrade')
if profile:
cmd.append('--profile=%s' % profile)
if not usepkg:
# TODO(crbug.com/922144): Uses the underscore variant of an argument, will
# require updating tests when the arguments are cleaned up.
cmd.extend(LOCAL_BUILD_FLAGS)
if force:
cmd.append('--force')
RunBuildScript(
buildroot,
cmd,
chromite_cmd=True,
extra_env=extra_env,
enter_chroot=True,
chroot_args=chroot_args)
def LegacySetupBoard(buildroot,
board,
usepkg,
extra_env=None,
force=False,
profile=None,
chroot_upgrade=True,
chroot_args=None):
"""Wrapper around setup_board for the workspace stage only.
This wrapper supports the old version of setup_board, and is only meant to be
used for the workspace builders so they can support old firmware/factory
branches.
This function should not need to be changed until it's deleted.
Args:
buildroot: The buildroot of the current build.
board: The board to set up.
usepkg: Whether to use binary packages when setting up the board.
extra_env: A dictionary of environmental variables to set during generation.
force: Whether to remove the board prior to setting it up.
profile: The profile to use with this board.
chroot_upgrade: Whether to update the chroot. If the chroot is already up to
date, you can specify chroot_upgrade=False.
chroot_args: The args to the chroot.
"""
cmd = ['./setup_board', '--board=%s' % board, '--accept_licenses=@CHROMEOS']
# This isn't the greatest thing, but emerge's dependency calculation
# isn't the speediest thing, so let callers skip this step when they
# know the system is up-to-date already.
if not chroot_upgrade:
cmd.append('--skip_chroot_upgrade')
if profile:
cmd.append('--profile=%s' % profile)
if not usepkg:
cmd.extend(LOCAL_BUILD_FLAGS)
if force:
cmd.append('--force')
RunBuildScript(
buildroot,
cmd,
extra_env=extra_env,
enter_chroot=True,
chroot_args=chroot_args)
def BuildSDKBoard(buildroot,
board,
force=False,
extra_env=None,
chroot_args=None):
"""Wrapper around setup_host_board.
Args:
board (str): The name of the board.
buildroot (str): The buildroot of the current build.
force (bool): Whether to remove existing sysroot if it exists.
extra_env (dict): A dictionary of environment variables to set.
chroot_args (dict): The args to the chroot.
"""
cmd = ['./build_sdk_board', '--board', board]
if force:
cmd.append('--force')
RunBuildScript(
buildroot,
cmd,
extra_env=extra_env,
enter_chroot=True,
chroot_args=chroot_args)
def SetupToolchains(buildroot,
usepkg=True,
create_packages=False,
targets=None,
sysroot=None,
boards=None,
output_dir=None,
**kwargs):
"""Install or update toolchains.
See cros_setup_toolchains for more documentation about the arguments other
than buildroot.
Args:
buildroot: str - The buildroot of the current build.
usepkg: bool - Whether to use prebuilt packages.
create_packages: bool - Whether to build redistributable packages.
targets: str - Type of target for the toolchain install, e.g. 'boards'.
sysroot: str - The sysroot in which to install the toolchains.
boards: str|list - The board(s) whose toolchain should be installed.
output_dir: str - The output directory.
"""
kwargs.setdefault('chromite_cmd', True)
kwargs.setdefault('enter_chroot', True)
kwargs.setdefault('sudo', True)
cmd = ['cros_setup_toolchains']
if not usepkg:
cmd.append('--nousepkg')
if create_packages:
cmd.append('--create-packages')
if targets:
cmd += ['--targets', targets]
if sysroot:
cmd += ['--sysroot', sysroot]
if boards:
boards_str = (boards if isinstance(boards, six.string_types)
else ','.join(boards))
cmd += ['--include-boards', boards_str]
if output_dir:
cmd += ['--output-dir', output_dir]
RunBuildScript(buildroot, cmd, **kwargs)
class MissingBinpkg(failures_lib.StepFailure):
"""Error class for when we are missing an essential binpkg."""
def VerifyBinpkg(buildroot, board, pkg, packages, extra_env=None):
"""Verify that an appropriate binary package exists for |pkg|.
Using the depgraph from |packages|, check to see if |pkg| would be pulled in
as a binary or from source. If |pkg| isn't installed at all, then ignore it.
Args:
buildroot: The buildroot of the current build.
board: The board to set up.
pkg: The package to look for.
packages: The list of packages that get installed on |board|.
extra_env: A dictionary of environmental variables to set.
Raises:
If the package is found and is built from source, raise MissingBinpkg.
If the package is not found, or it is installed from a binpkg, do nothing.
"""
cmd = ['emerge-%s' % board, '-pegNuvq', '--with-bdeps=y', '--color=n'
] + list(packages)
result = RunBuildScript(
buildroot,
cmd,
capture_output=True,
enter_chroot=True,
extra_env=extra_env)
pattern = r'^\[(ebuild|binary).*%s' % re.escape(pkg)
m = re.search(pattern, result.output, re.MULTILINE)
if m and m.group(1) == 'ebuild':
logging.info('(output):\n%s', result.output)
msg = 'Cannot find prebuilts for %s on %s' % (pkg, board)
raise MissingBinpkg(msg)
def RunLocalTryjob(buildroot, build_config, args=None, target_buildroot=None):
"""Run a local tryjob.
Setting --git-cache-dir (probably to self._run.options.git_cache_dir) can
have a huge impact on performance.
This function will use cbuildbot_launch of the current checkout, which
whatever patches are applied, but cbuildbot from the requested build.
Args:
buildroot: The buildroot of the current build.
build_config: The name of the build config to build.
args: List of strings giving additional command line arguments.
target_buildroot: Buildroot for the tryjob. None for a tmpdir.
"""
with osutils.TempDir() as tempdir:
if not target_buildroot:
target_buildroot = tempdir
cmd = [
'cros',
'tryjob',
'--local',
'--yes',
'--buildroot',
target_buildroot,
build_config,
]
if args:
cmd.extend(args)
RunBuildScript(buildroot, cmd, chromite_cmd=True)
def UpdateBinhostJson(buildroot):
"""Test prebuilts for all boards, making sure everybody gets Chrome prebuilts.
Args:
buildroot: The buildroot of the current build.
"""
cmd = ['../cbuildbot/update_binhost_json']
RunBuildScript(buildroot, cmd, chromite_cmd=True, enter_chroot=True)
def Build(buildroot,
board,
build_autotest,
usepkg,
packages=(),
skip_chroot_upgrade=True,
extra_env=None,
chrome_root=None,
noretry=False,
chroot_args=None,
event_file=None,
run_goma=False,
build_all_with_goma=False,
disable_revdep_logic=False):
"""Wrapper around build_packages.
Args:
buildroot: The buildroot of the current build.
board: The board to set up.
build_autotest: Whether to build autotest-related packages.
usepkg: Whether to use binary packages.
packages: Tuple of specific packages we want to build. If empty,
build_packages will calculate a list of packages automatically.
skip_chroot_upgrade: Whether to skip the chroot update. If the chroot is
not yet up to date, you should specify skip_chroot_upgrade=False.
extra_env: A dictionary of environmental variables to set during generation.
chrome_root: The directory where chrome is stored.
noretry: Do not retry package failures.
chroot_args: The args to the chroot.
event_file: File name that events will be logged to.
build_all_with_goma: Use goma to build all board packages.
run_goma: Set ./build_package --run_goma option, which starts and stops
goma server in chroot while building packages.
disable_revdep_logic: Pass --nowithrevdeps to build_packages, disabling the
reverse dependency calculation step.
"""
cmd = [
'./build_packages',
'--board=%s' % board, '--accept_licenses=@CHROMEOS', '--withdebugsymbols'
]
if not build_autotest:
cmd.append('--nowithautotest')
if skip_chroot_upgrade:
cmd.append('--skip_chroot_upgrade')
if not usepkg:
cmd.extend(LOCAL_BUILD_FLAGS)
if noretry:
cmd.append('--nobuildretry')
if disable_revdep_logic:
cmd.append('--nowithrevdeps')
if run_goma:
cmd.append('--run_goma')
if build_all_with_goma:
cmd.append('--build_all_with_goma')
if not chroot_args:
chroot_args = []
if chrome_root:
chroot_args.append('--chrome_root=%s' % chrome_root)
if event_file:
cmd.append('--withevents')
cmd.append('--eventfile=%s' % event_file)
cmd.extend(packages)
RunBuildScript(
buildroot,
cmd,
extra_env=extra_env,
chroot_args=chroot_args,
enter_chroot=True)
FirmwareVersions = collections.namedtuple(
'FirmwareVersions', ['model', 'main', 'main_rw', 'ec', 'ec_rw'])
def GetFirmwareVersionCmdResult(buildroot, board):
"""Gets the raw result output of the firmware updater version command.
Args:
buildroot: The buildroot of the current build.
board: The board the firmware is for.
Returns:
Command execution result.
"""
updater = os.path.join(buildroot, constants.DEFAULT_CHROOT_DIR,
cros_build_lib.GetSysroot(board).lstrip(os.path.sep),
'usr', 'sbin', 'chromeos-firmwareupdate')
if not os.path.isfile(updater):
return ''
updater = path_util.ToChrootPath(updater)
return cros_build_lib.run([updater, '-V'], enter_chroot=True,
capture_output=True, log_output=True,
cwd=buildroot).output
def FindFirmwareVersions(cmd_output):
"""Finds firmware version output via regex matches against the cmd_output.
Args:
cmd_output: The raw output to search against.
Returns:
FirmwareVersions namedtuple with results.
Each element will either be set to the string output by the firmware
updater shellball, or None if there is no match.
"""
# Sometimes a firmware bundle includes a special combination of RO+RW
# firmware. In this case, the RW firmware version is indicated with a "(RW)
# version" field. In other cases, the "(RW) version" field is not present.
# Therefore, search for the "(RW)" fields first and if they aren't present,
# fallback to the other format. e.g. just "BIOS version:".
# TODO(aaboagye): Use JSON once the firmware updater supports it.
main = None
main_rw = None
ec = None
ec_rw = None
model = None
match = re.search(r'BIOS version:\s*(?P<version>.*)', cmd_output)
if match:
main = match.group('version')
match = re.search(r'BIOS \(RW\) version:\s*(?P<version>.*)', cmd_output)
if match:
main_rw = match.group('version')
match = re.search(r'EC version:\s*(?P<version>.*)', cmd_output)
if match:
ec = match.group('version')
match = re.search(r'EC \(RW\) version:\s*(?P<version>.*)', cmd_output)
if match:
ec_rw = match.group('version')
match = re.search(r'Model:\s*(?P<model>.*)', cmd_output)
if match:
model = match.group('model')
return FirmwareVersions(model, main, main_rw, ec, ec_rw)
def GetAllFirmwareVersions(buildroot, board):
"""Extract firmware version for all models present.
Args:
buildroot: The buildroot of the current build.
board: The board the firmware is for.
Returns:
A dict of FirmwareVersions namedtuple instances by model.
Each element will be populated based on whether it was present in the
command output.
"""
result = {}
cmd_result = GetFirmwareVersionCmdResult(buildroot, board)
# There is a blank line between the version info for each model.
firmware_version_payloads = cmd_result.split('\n\n')
for firmware_version_payload in firmware_version_payloads:
if 'BIOS' in firmware_version_payload:
firmware_version = FindFirmwareVersions(firmware_version_payload)
result[firmware_version.model] = firmware_version
return result
def GetFirmwareVersions(buildroot, board):
"""Extract version information from the firmware updater, if one exists.
Args:
buildroot: The buildroot of the current build.
board: The board the firmware is for.
Returns:
A FirmwareVersions namedtuple instance.
Each element will either be set to the string output by the firmware
updater shellball, or None if there is no firmware updater.
"""
cmd_result = GetFirmwareVersionCmdResult(buildroot, board)
if cmd_result:
return FindFirmwareVersions(cmd_result)
else:
return FirmwareVersions(None, None, None, None, None)
def RunCrosConfigHost(buildroot, board, args, log_output=True):
"""Run the cros_config_host tool in the buildroot
Args:
buildroot: The buildroot of the current build.
board: The board the build is for.
args: List of arguments to pass.
log_output: Whether to log the output of the cros_config_host.
Returns:
Output of the tool
"""
tool = os.path.join(buildroot, constants.DEFAULT_CHROOT_DIR, 'usr', 'bin',
'cros_config_host')
if not os.path.isfile(tool):
return None
tool = path_util.ToChrootPath(tool)
config_fname = os.path.join(
cros_build_lib.GetSysroot(board), 'usr', 'share', 'chromeos-config',
'yaml', 'config.yaml')
result = cros_build_lib.run(
[tool, '-c', config_fname] + args,
enter_chroot=True,
capture_output=True,
log_output=log_output,
cwd=buildroot,
error_code_ok=True)
if result.returncode:
# Show the output for debugging purposes.
if 'No such file or directory' not in result.error:
print('cros_config_host failed: %s\n' % result.error, file=sys.stderr)
return None
return result.output.strip().splitlines()
def GetModels(buildroot, board, log_output=True):
"""Obtain a list of models supported by a unified board
This ignored whitelabel models since GoldenEye has no specific support for
these at present.
Args:
buildroot: The buildroot of the current build.
board: The board the build is for.
log_output: Whether to log the output of the cros_config_host invocation.
Returns:
A list of models supported by this board, if it is a unified build; None,
if it is not a unified build.
"""
return RunCrosConfigHost(
buildroot, board, ['list-models'], log_output=log_output)
def BuildImage(buildroot,
board,
images_to_build,
version=None,
builder_path=None,
rootfs_verification=True,
extra_env=None,
disk_layout=None,
chroot_args=None):
"""Run the script which builds images.
Args:
buildroot: The buildroot of the current build.
board: The board of the image.
images_to_build: The images to be built.
version: The version of image.
builder_path: The path of the builder to build the image.
rootfs_verification: Whether to enable the rootfs verification.
extra_env: A dictionary of environmental variables to set during generation.
disk_layout: The disk layout.
chroot_args: The args to the chroot.
"""
# Default to base if images_to_build is passed empty.
if not images_to_build:
images_to_build = ['base']
version_str = '--version=%s' % (version or '')
builder_path_str = '--builder_path=%s' % (builder_path or '')
cmd = [
'./build_image',
'--board=%s' % board, '--replace', version_str, builder_path_str
]
if not rootfs_verification:
cmd += ['--noenable_rootfs_verification']
if disk_layout:
cmd += ['--disk_layout=%s' % disk_layout]
cmd += images_to_build
RunBuildScript(
buildroot,
cmd,
enter_chroot=True,
extra_env=extra_env,
chroot_args=chroot_args)
def BuildVMImageForTesting(buildroot, board, extra_env=None, disk_layout=None):
cmd = ['./image_to_vm.sh', '--board=%s' % board, '--test_image']
if disk_layout:
cmd += ['--disk_layout=%s' % disk_layout]
RunBuildScript(buildroot, cmd, extra_env=extra_env, enter_chroot=True)
def RunTestImage(buildroot, board, image_dir, results_dir):
"""Executes test_image on the produced image in |image_dir|.
The "test_image" script will be run as root in chroot. Running the script as
root will allow the tests to read normally-forbidden files such as those
owned by root. Running tests inside the chroot allows us to control
dependencies better.
Args:
buildroot: The buildroot of the current build.
board: The board the image was built for.
image_dir: The directory in which to find the image.
results_dir: The directory to store result files.
Raises:
failures_lib.BuildScriptFailure if the test script fails.
"""
cmd = [
'test_image',
'--board',
board,
'--test_results_root',
path_util.ToChrootPath(results_dir),
path_util.ToChrootPath(image_dir),
]
RunBuildScript(
buildroot, cmd, enter_chroot=True, chromite_cmd=True, sudo=True)
def RunSignerTests(_buildroot, board):
image_lib.SecurityTest(board=board)
def RunUnitTests(buildroot,
board,
blacklist=None,
extra_env=None,
build_stage=True,
chroot_args=None):
cmd = ['cros_run_unit_tests', '--board=%s' % board]
if blacklist:
cmd += ['--blacklist_packages=%s' % ' '.join(blacklist)]
if not build_stage:
cmd += ['--assume-empty-sysroot']
RunBuildScript(
buildroot,
cmd,
chromite_cmd=True,
enter_chroot=True,
extra_env=extra_env or {},
chroot_args=chroot_args)
def BuildAndArchiveTestResultsTarball(src_dir, buildroot):
"""Create a compressed tarball of test results.
Args:
src_dir: The directory containing the test results.
buildroot: Build root directory.
Returns:
The name of the tarball.
"""
target = '%s.tgz' % src_dir.rstrip(os.path.sep)
chroot = os.path.join(buildroot, constants.DEFAULT_CHROOT_DIR)
cros_build_lib.CreateTarball(
target, src_dir, compression=cros_build_lib.COMP_GZIP, chroot=chroot)
return os.path.basename(target)
# TODO(akeshet): Deprecate this undocumented type.
HWTestSuiteResult = collections.namedtuple('HWTestSuiteResult',
['to_raise', 'json_dump_result'])
@failures_lib.SetFailureType(failures_lib.SuiteTimedOut,
timeout_util.TimeoutError)
def RunHWTestSuite(build,
suite,
board,
model=None,
pool=None,
file_bugs=None,
wait_for_results=None,
priority=None,
timeout_mins=None,
max_runtime_mins=None,
retry=None,
max_retries=None,
minimum_duts=0,
suite_min_duts=0,
suite_args=None,
offload_failures_only=None,
debug=True,
skip_duts_check=False,
job_keyvals=None,
test_args=None):
"""Run the test suite in the Autotest lab.
Args:
build: The build is described as the bot_id and the build version.
e.g. x86-mario-release/R18-1655.0.0-a1-b1584.
suite: Name of the Autotest suite.
board: The board the test suite should be scheduled against.
model: A specific model to schedule the test suite against.
pool: The pool of machines we should use to run the hw tests on.
file_bugs: File bugs on test failures for this suite run.
wait_for_results: If True, wait for autotest results before returning.
priority: Priority of this suite run.
timeout_mins: Timeout in minutes for the suite job and its sub-jobs.
max_runtime_mins: Maximum runtime for the suite job and its sub-jobs.
retry: If True, will enable job-level retry. Only works when
wait_for_results is True.
max_retries: Integer, maximum job retries allowed at suite level.
None for no max.
minimum_duts: The minimum number of DUTs should be available in lab for the
suite job to be created. If it's set to 0, the check will be
skipped.
suite_min_duts: Preferred minimum duts, lab will prioritize on getting
such many duts even if the suite is competing with
a suite that has higher priority.
suite_args: Arguments passed to the suite. This should be a dict
representing keyword arguments. The value is marshalled
using repr(), so the dict values should be basic types.
offload_failures_only: Only offload failed tests to Google Storage.
debug: Whether we are in debug mode.
skip_duts_check: If True, skip minimum available DUTs check.
job_keyvals: A dict of job keyvals to be inject to suite control file.
test_args: A dict of test parameters to be inject to suite control file.
Returns:
An instance of named tuple HWTestSuiteResult, the first element is the
exception to be raised; the second element is the json dump cmd result,
if json_dump cmd is not called, None will be returned.
"""
try:
cmd = [RUN_SUITE_PATH]
cmd += _GetRunSuiteArgs(
build,
suite,
board,
model=model,
pool=pool,
file_bugs=file_bugs,
priority=priority,
timeout_mins=timeout_mins,
max_runtime_mins=max_runtime_mins,
retry=retry,
max_retries=max_retries,
minimum_duts=minimum_duts,
suite_min_duts=suite_min_duts,
suite_args=suite_args,
offload_failures_only=offload_failures_only,
subsystems=set([]),
skip_duts_check=skip_duts_check,
job_keyvals=job_keyvals,
test_args=test_args)
swarming_args = _CreateSwarmingArgs(build, suite, board, priority,
timeout_mins)
running_json_dump_flag = False
json_dump_result = None
job_id = _HWTestCreate(cmd, debug, **swarming_args)
if job_id:
if wait_for_results:
pass_hwtest = _HWTestWait(cmd, job_id, **swarming_args)
if wait_for_results:
# Only dump the json output when tests don't pass, since the json
# output is used to decide whether we can do subsystem based partial
# submission.
running_json_dump_flag = not pass_hwtest
if running_json_dump_flag:
json_dump_result = retry_util.RetryException(
ValueError, 3, _HWTestDumpJson, cmd, job_id, **swarming_args)
return HWTestSuiteResult(None, json_dump_result)
except cros_build_lib.RunCommandError as e:
result = e.result
if not result.HasValidSummary():
# swarming client has failed.
logging.error('No valid task summary json generated, output:%s',
result.output)
if result.task_summary_json:
logging.error('Invalid task summary json:\n%s',
json.dumps(result.task_summary_json, indent=2))
to_raise = failures_lib.SwarmingProxyFailure(
'** Failed to fullfill request with proxy server, code(%d) **' %
result.returncode)
elif result.GetValue('internal_failure'):
logging.error(
'Encountered swarming internal error:\n'
'stdout: \n%s\n'
'summary json content:\n%s', result.output,
str(result.task_summary_json))
to_raise = failures_lib.SwarmingProxyFailure(
'** Failed to fullfill request with proxy server, code(%d) **' %
result.returncode)
else:
logging.debug(
'swarming info: name: %s, bot_id: %s, task_id: %s, '
'created_ts: %s', result.GetValue('name'), result.GetValue('bot_id'),
result.GetValue('id'), result.GetValue('created_ts'))
# If running json_dump cmd, write the pass/fail subsys dict into console,
# otherwise, write the cmd output to the console.
outputs = result.GetValue('outputs', '')
if running_json_dump_flag:
s = ''.join(outputs)
sys.stdout.write(s)
sys.stdout.write('\n')
try:
# If we can't parse the JSON dump, subsystem based partial submission
# will be skipped due to missing information about which individual
# tests passed. This can happen for example when the JSON dump step
# fails due to connectivity issues in which case we'll have no output
# to parse. It's OK to just assume complete test failure in this case
# though: since we don't know better anyways, we need to err on the
# safe side and not submit any change. So we just log an error below
# instead of raising an exception and allow the subsequent logic to
# decide which failure condition to report. This is in the hope that
# providing more information from the RunCommandError we encountered
# will be useful in diagnosing the root cause of the failure.
json_dump_result = _HWTestParseJSONDump(s)
except ValueError as e:
logging.error(
'Failed to parse HWTest JSON dump string, ' +
'subsystem based partial submission will be skipped: %s', e)
else:
for output in outputs:
sys.stdout.write(output)
sys.stdout.flush()
# swarming client has submitted task and returned task information.
lab_warning_codes = (2,)
infra_error_codes = (3,)
timeout_codes = (4,)
board_not_available_codes = (5,)
proxy_failure_codes = (241,)
if result.returncode in lab_warning_codes:
to_raise = failures_lib.TestWarning(
'** Suite passed with a warning code **')
elif (result.returncode in infra_error_codes or
result.returncode in proxy_failure_codes):
to_raise = failures_lib.TestLabFailure(
'** HWTest did not complete due to infrastructure issues '
'(code %d) **' % result.returncode)
elif result.returncode in timeout_codes:
to_raise = failures_lib.SuiteTimedOut(
'** Suite timed out before completion **')
elif result.returncode in board_not_available_codes:
to_raise = failures_lib.BoardNotAvailable(
'** Board was not availble in the lab **')
elif result.returncode != 0:
to_raise = failures_lib.TestFailure(
'** HWTest failed (code %d) **' % result.returncode)
elif json_dump_result is None:
to_raise = failures_lib.TestFailure('** Failed to decode JSON dump **')
return HWTestSuiteResult(to_raise, json_dump_result)
def _GetSkylabWaitTaskArgs(task_id, timeout_mins=None):
"""Get arguments for the `skylab wait-task` command.
Args:
timeout_mins: maximum number of minutes to wait for task completion.
task_id: id of the task to wait for.
Returns:
List of args for `skylab wait-task`, not including the subcommand itself.
"""
args = ['-service-account-json', constants.CHROMEOS_SERVICE_ACCOUNT]
if timeout_mins is not None:
args += ['-timeout-mins', str(timeout_mins)]
args += [task_id]
return args
def _GetSkylabCreateSuiteArgs(build,
suite,
board,
pool,
model=None,
priority=None,
timeout_mins=None,
max_retries=None,
job_keyvals=None,
quota_account=None):
"""Get arguments for the `skylab create-suite` command.
Command will run in -json mode.
Args:
build: string image name to use, e.g. elm-paladin/R99-12345
suite: suite name to run
board: board name to run suite for
pool: pool to run the suite in
model: (optional) model name to run suite for
priority: (optional) integer priority for the suite. Higher number is a
lower priority
timeout_mins: (optional) suite timeout
max_retries: (optional) max retries allowed across all child tasks
job_keyvals: (optional) dictionary of {'key': 'value'} keyvals to be
injected into all children of suite.
quota_account: (optional) quotascheduler account to use for child tasks
Returns:
A list of args for the `skylab create-suite` subcommand (not including)
the subcommand itself.
"""
args = ['-image', build, '-board', board]
if model:
args += ['-model', model]
if pool is not None:
args += ['-pool', pool]
if priority is not None:
args += ['-priority', str(priority)]
if timeout_mins is not None:
args += ['-timeout-mins', str(timeout_mins)]
if max_retries is not None:
args += ['-max-retries', str(max_retries)]
if job_keyvals is not None:
for key, value in job_keyvals.items():
args += ['-keyval', '%s:%s' % (key, value)]
if quota_account is not None:
args += ['-qs-account', quota_account]
args += ['-json']
args += ['-service-account-json', constants.CHROMEOS_SERVICE_ACCOUNT]
args += [suite]
return args
def _InstallSkylabTool():
"""Install skylab tool.
Returns:
the path of installed skylab tool.
"""
instance_id = cipd.GetInstanceID(cipd.GetCIPDFromCache(),
constants.CIPD_SKYLAB_PACKAGE, 'latest')
cache_dir = os.path.join(path_util.GetCacheDir(), 'cipd/packages')
path = cipd.InstallPackage(cipd.GetCIPDFromCache(),
constants.CIPD_SKYLAB_PACKAGE, instance_id,
cache_dir)
return os.path.join(path, 'skylab')
# pylint: disable=docstring-missing-args
def _SkylabCreateTestArgs(build,
pool,
test_name,
board=None,
model=None,
timeout_mins=None,
tags=None,
keyvals=None,
test_args=None):
"""Get a list of args for skylab create-test.
Args:
See args of RunSkylabHWTEST.
Returns:
A list of string args for skylab create-test.
"""
args = ['-image', build]
args += ['-pool', 'DUT_POOL_%s' % pool.upper()]
if board is None and model is None:
raise ValueError('Need to specify either board or model.')
if board is not None:
args += ['-board', board]
if model is not None:
args += ['-model', model]
if timeout_mins is not None:
args += ['-timeout-mins', str(timeout_mins)]
if tags is not None:
for tag in tags:
args += ['-tag', tag]
if keyvals is not None:
for k in keyvals:
args += ['-keyval', k]
if test_args is not None:
args += ['-test-args', test_args]
args += ['-service-account-json', constants.CHROMEOS_SERVICE_ACCOUNT]
return args + [test_name]
# TODO(crbug.com/1008919): Remove 120 default once config is properly set
# with default timeouts.
def RunSkylabHWTest(build,
pool,
test_name,
shown_test_name=None,
board=None,
model=None,
timeout_mins=120,
tags=None,
keyvals=None,
test_args=None):
"""Run a skylab test in the Autotest lab using skylab tool.
Args:
build: A string full image name.
pool: A string pool to run the test on.
test_name: A string testname to run.
shown_test_name: A string to print the test in cbuildbot. Usually it's the
same as test_name. But for parameterized tests, it could
vary due to different test_args, e.g. for paygen au tests:
test_name: autoupdate_EndToEndTest
shown_test_name:
autoupdate_EndToEndTest_paygen_au_beta_full_11316.66.0
board: A string board to run the test on.
model: A string model to run the test on.
timeout_mins: An integer to indicate the test's timeout.
tags: A list of strings to tag the task in swarming.
keyvals: A list of strings to be passed to the test as job_keyvals.
test_args: A string to be passed to the test as arguments. The format is:
'X1=Y1 X2=Y2 X3=Y3 ...'
Returns:
A swarming task link if the test is created successfully.
"""
if shown_test_name is None:
shown_test_name = test_name
skylab_path = _InstallSkylabTool()
args = _SkylabCreateTestArgs(build, pool, test_name, board, model,
timeout_mins, tags, keyvals, test_args)
try:
result = cros_build_lib.run(
[skylab_path, 'create-test'] + args, redirect_stdout=True)
return HWTestSuiteResult(None, None)
except cros_build_lib.RunCommandError as e:
result = e.result
to_raise = failures_lib.TestFailure(
'** HWTest failed (code %d) **' % result.returncode)
return HWTestSuiteResult(to_raise, None)
finally:
# This is required to output buildbot annotations, e.g. 'STEP_LINKS'.
output = result.output.strip()
# The format of output is:
# Created Swarming task https://chromeos-swarming.appspot.com/task?id=XX
sys.stdout.write('%s \n' % output)
sys.stdout.write('######## Output for buildbot annotations ######## \n')
sys.stdout.write('%s \n' % str(
buildbot_annotations.StepLink(shown_test_name,
output.split()[-1])))
sys.stdout.write('######## END Output for buildbot annotations ######## \n')
sys.stdout.flush()
# pylint: disable=docstring-missing-args
@failures_lib.SetFailureType(failures_lib.SuiteTimedOut,
timeout_util.TimeoutError)
def RunSkylabHWTestSuite(
build,
suite,
board,
model=None,
# TODO(akeshet): Make this required argument a positional arg.
pool=None,
wait_for_results=False,
priority=None,
timeout_mins=120,
# TODO(akeshet): Delete this ignored argument.
retry=None,
max_retries=None,
# TODO(akeshet): Delete this ignored argument.
suite_args=None,
job_keyvals=None,
quota_account=None):
"""Run the test suite in the Autotest lab using Skylab.
Args:
See args of RunHWTestSuite.
Returns:
See returns of RunHWTestSuite.
"""
if suite_args:
logging.warning(
'Skylab suites do not support suite_args, although they '
'were specified: %s', suite_args)
if retry:
logging.warning(
'Skylab suites do not support retry arg, although it '
'was specified: %s', retry)
if not pool:
raise ValueError('|pool| argument is required in Skylab, but was not '
'supplied.')
if priority:
priority = constants.SKYLAB_HWTEST_PRIORITIES_MAP[str(priority)]
skylab_tool = _InstallSkylabTool()
cmd = [skylab_tool, 'create-suite']
cmd += _GetSkylabCreateSuiteArgs(
build,
suite,
board,
pool,
model=model,
priority=priority,
timeout_mins=timeout_mins,
max_retries=max_retries,
job_keyvals=job_keyvals,
quota_account=quota_account)
try:
output = cros_build_lib.run(cmd, redirect_stdout=True)
report = json.loads(output.output)
task_id = report['task_id']
task_url = report['task_url']
logging.info('Launched suite task %s', task_url)
logging.PrintBuildbotLink('Suite task: %s' % suite, task_url)
if not wait_for_results:
return HWTestSuiteResult(None, None)
wait_cmd = [skylab_tool, 'wait-task'] + _GetSkylabWaitTaskArgs(
task_id, timeout_mins=timeout_mins)
output = cros_build_lib.run(wait_cmd, redirect_stdout=True)
try:
report = json.loads(output.output)
except:
logging.error('Error when json parsing:\n%s', output.output)
raise
logging.info(
'Suite ended in state %s (success=%s) with output:\n%s',
report['task-result']['state'],
report['task-result']['success'],
report['stdout'],
)
error = None
if not report['task-result']['success']:
error = failures_lib.TestFailure('Suite failed.')
return HWTestSuiteResult(error, None)
except cros_build_lib.RunCommandError as e:
result = e.result
to_raise = failures_lib.TestFailure(
'** HWTest failed (code %d) **' % result.returncode)
return HWTestSuiteResult(to_raise, None)
def RunSkylabHWTestPlan(test_plan=None,
build=None,
legacy_suite=None,
pool=None,
board=None,
model=None,
timeout_mins=120,
tags=None,
keyvals=None):
"""Run a skylab test in the Autotest lab using skylab tool.
Args:
test_plan: A JSONpb string containing a TestPlan object.
build: A string full image name.
legacy_suite: A string suite name, if non-empty it overrides the test plan
on the autotest backend.
pool: A string pool to run the test on.
board: A string board to run the test on.
model: A string model to run the test on.
timeout_mins: An integer to indicate the test's timeout.
tags: A list of strings to tag the task in swarming.
keyvals: A list of strings to be passed to the test as job_keyvals.
"""
if not test_plan:
raise ValueError('Need to specify test plan.')
if not build:
raise ValueError('Need to specify build.')
if not (board or model):
raise ValueError('Need to specify either board or model.')
args = ['-image', build]
if legacy_suite:
args += ['-legacy-suite', legacy_suite]
if pool:
args += ['-pool', pool]
if board:
args += ['-board', board]
if model:
args += ['-model', model]
if timeout_mins:
args += ['-timeout-mins', str(timeout_mins)]
if tags:
for tag in tags:
args += ['-tag', tag]
if keyvals is not None:
for k, v in keyvals.items():
args += ['-keyval', k+':'+v]
args += ['-service-account-json', constants.CHROMEOS_SERVICE_ACCOUNT]
args += ['-plan-file', '/dev/stdin']
skylab_path = _InstallSkylabTool()
try:
result = cros_build_lib.run(
[skylab_path, 'create-testplan'] + args,
redirect_stdout=True,
input=test_plan)
task_url = ''
try:
report = json.loads(result.output)
task_url = report.get('task_url')
logging.info('Launched test plan task %s', task_url)
logging.PrintBuildbotLink('Test plan task', task_url)
except ValueError:
logging.warning('Unable to parse output:\n%s', result.output)
return HWTestSuiteResult(None, None)
except cros_build_lib.RunCommandError as e:
result = e.result
to_raise = failures_lib.TestFailure(
'** HWTest failed (code %d) **' % result.returncode)
return HWTestSuiteResult(to_raise, None)
# pylint: disable=docstring-missing-args
def _GetRunSuiteArgs(build,
suite,
board,
model=None,
pool=None,
file_bugs=None,
priority=None,
timeout_mins=None,
max_runtime_mins=None,
retry=None,
max_retries=None,
minimum_duts=0,
suite_min_duts=0,
suite_args=None,
offload_failures_only=None,
subsystems=frozenset(),
skip_duts_check=False,
job_keyvals=None,
test_args=None):
"""Get a list of args for run_suite.
Args:
See RunHWTestSuite.
Returns:
A list of args for run_suite
"""
args = ['--build', build, '--board', board]
if model:
args += ['--model', model]
if subsystems:
args += ['--suite_name', 'suite_attr_wrapper']
else:
args += ['--suite_name', suite]
# Add optional arguments to command, if present.
if pool is not None:
args += ['--pool', pool]
if file_bugs is not None:
args += ['--file_bugs', str(file_bugs)]
if priority is not None:
args += ['--priority', str(priority)]
if timeout_mins is not None:
args += ['--timeout_mins', str(timeout_mins)]
# The default for max_runtime_mins is one day. We increase this if longer
# timeouts are requested to avoid premature (and unexpected) aborts.
if not max_runtime_mins and timeout_mins > 1440:
args += ['--max_runtime_mins', str(timeout_mins)]
if max_runtime_mins is not None:
args += ['--max_runtime_mins', str(max_runtime_mins)]
if retry is not None:
args += ['--retry', str(retry)]
if max_retries is not None:
args += ['--max_retries', str(max_retries)]
if minimum_duts != 0:
args += ['--minimum_duts', str(minimum_duts)]
if suite_min_duts != 0:
args += ['--suite_min_duts', str(suite_min_duts)]
if suite_args is not None:
args += ['--suite_args', repr(suite_args)]
if offload_failures_only is not None:
args += ['--offload_failures_only', str(offload_failures_only)]
if subsystems:
subsystem_attr = ['subsystem:%s' % x for x in subsystems]
subsystems_attr_str = ' or '.join(subsystem_attr)
if suite != 'suite_attr_wrapper':
suite_attr_str = 'suite:%s' % suite
attr_value = '(%s) and (%s)' % (suite_attr_str, subsystems_attr_str)
else:
attr_value = subsystems_attr_str
suite_args_dict = repr({'attr_filter': attr_value})
args += ['--suite_args', suite_args_dict]
if skip_duts_check:
args += ['--skip_duts_check']
if job_keyvals:
args += ['--job_keyvals', repr(job_keyvals)]
if test_args:
args += ['--test_args', repr(test_args)]
return args
# pylint: enable=docstring-missing-args
def _CreateSwarmingArgs(build,
suite,
board,
priority,
timeout_mins=None,
run_skylab=False):
"""Create args for swarming client.
Args:
build: Name of the build, will be part of the swarming task name.
suite: Name of the suite, will be part of the swarming task name.
timeout_mins: run_suite timeout mins, will be used to figure out
timeouts for swarming task.
board: Name of the board.
priority: A String, e.g. CQ, representing the priority of this call.
run_skylab: Indicate whether to create a swarming cmd for Skylab HWTest.
Returns:
A dictionary of args for swarming client.
"""
swarming_timeout = timeout_mins or _DEFAULT_HWTEST_TIMEOUT_MINS
swarming_timeout = swarming_timeout * 60 + _SWARMING_ADDITIONAL_TIMEOUT
priority_num = None
if run_skylab:
swarming_server = topology.topology.get(
topology.CHROME_SWARMING_PROXY_HOST_KEY)
pool = SKYLAB_SUITE_BOT_POOL
if priority:
priority_num = constants.SKYLAB_HWTEST_PRIORITIES_MAP[str(priority)]
else:
swarming_server = topology.topology.get(topology.SWARMING_PROXY_HOST_KEY)
pool = SUITE_BOT_POOL
tags = {
'task_name': '-'.join([build, suite]),
'build': build,
'suite': suite,
'board': board,
'priority': priority,
}
if run_skylab:
tags['luci_project'] = 'chromeos'
tags['skylab'] = 'run_suite'
return {
'swarming_server': swarming_server,
'task_name': '-'.join([build, suite]),
'priority': priority_num,
'dimensions': [('os', 'Ubuntu-14.04'), ('pool', pool)],
'print_status_updates': True,
'timeout_secs': swarming_timeout,
'io_timeout_secs': swarming_timeout,
'hard_timeout_secs': swarming_timeout,
'expiration_secs': _SWARMING_EXPIRATION,
'tags': tags,
'service_account_json': constants.CHROMEOS_SERVICE_ACCOUNT,
}
def _HWTestCreate(cmd, debug=False, **kwargs):
"""Start a suite in the HWTest lab, and return its id.
This method runs a command to create the suite. Since we are using
swarming client, which contiuously send request to swarming server
to poll task result, there is no need to retry on any network
related failures.
Args:
cmd: Proxied run_suite command.
debug: If True, log command rather than running it.
kwargs: args to be passed to RunSwarmingCommand.
Returns:
Job id of created suite. Returned id will be None if no job id was created.
"""
# Start the suite.
start_cmd = list(cmd) + ['-c']
if debug:
logging.info('RunHWTestSuite would run: %s',
cros_build_lib.CmdToStr(start_cmd))
else:
result = swarming_lib.RunSwarmingCommandWithRetries(
max_retry=_MAX_HWTEST_START_CMD_RETRY,
error_check=swarming_lib.SwarmingRetriableErrorCheck,
cmd=start_cmd,
capture_output=True,
combine_stdout_stderr=True,
**kwargs)
# If the command succeeds, result.task_summary_json
# should have the right content.
for output in result.GetValue('outputs', ''):
sys.stdout.write(output)
sys.stdout.flush()
m = re.search(r'Created task id: (?P<job_id>.*)', result.output)
if m:
return m.group('job_id')
return None
def _HWTestWait(cmd, job_id, **kwargs):
"""Wait for HWTest suite to complete.
Wait for the HWTest suite to complete. All the test related exceptions will
be mute(swarming client side exception), only the swarming server side
exception will be raised, e.g. swarming internal failure. The test related
exception will be raised in _HWTestDumpJson step.
Args:
cmd: Proxied run_suite command.
job_id: The job id of the suite that was created.
kwargs: args to be passed to RunSwarmingCommand.
Returns:
True if all tests pass.
"""
# Wait on the suite
wait_cmd = list(cmd) + ['-m', str(job_id)]
pass_hwtest = False
try:
result = swarming_lib.RunSwarmingCommandWithRetries(
max_retry=_MAX_HWTEST_CMD_RETRY,
error_check=swarming_lib.SwarmingRetriableErrorCheck,
cmd=wait_cmd,
capture_output=True,
combine_stdout_stderr=True,
**kwargs)
pass_hwtest = True
except cros_build_lib.RunCommandError as e:
result = e.result
# Delay the lab-related exceptions, since those will be raised in the next
# json_dump cmd run.
if (not result.task_summary_json or not result.GetValue('outputs') or
result.GetValue('internal_failure')):
raise
else:
logging.error(
'wait_cmd has lab failures: %s.\nException will be raised '
'in the next json_dump run.', e.msg)
for output in result.GetValue('outputs', ''):
sys.stdout.write(output)
sys.stdout.flush()
return pass_hwtest
def _HWTestParseJSONDump(dump_output):
"""Parses JSON dump output and returns the parsed JSON dict.
Args:
dump_output: The string containing the HWTest result JSON dictionary to
parse, marked up with #JSON_START# and #JSON_END# start/end delimiters.
Returns:
Decoded JSON dict. May raise ValueError upon failure to pass the embedded
JSON object.
"""
i = dump_output.find(JSON_DICT_START) + len(JSON_DICT_START)
j = dump_output.find(JSON_DICT_END)
if i == -1 or j == -1 or i > j:
raise ValueError('Invalid swarming output: %s' % dump_output)
return json.loads(dump_output[i:j])
def _HWTestDumpJson(cmd, job_id, **kwargs):
"""Consume HWTest suite json output and return passed/failed subsystems dict.
Args:
cmd: Proxied run_suite command.
job_id: The job id of the suite that was created.
kwargs: args to be passed to RunSwarmingCommand.
Returns:
The parsed json_dump dictionary.
"""
dump_json_cmd = list(cmd) + ['--json_dump', '-m', str(job_id)]
result = swarming_lib.RunSwarmingCommandWithRetries(
max_retry=_MAX_HWTEST_CMD_RETRY,
error_check=swarming_lib.SwarmingRetriableErrorCheck,
cmd=dump_json_cmd,
capture_output=True,
combine_stdout_stderr=True,
**kwargs)
for output in result.GetValue('outputs', ''):
sys.stdout.write(output)
sys.stdout.write('\n')
sys.stdout.flush()
return _HWTestParseJSONDump(''.join(result.GetValue('outputs', '')))
def AbortHWTests(config_type_or_name, version, debug, suite=''):
"""Abort the specified hardware tests for the given bot(s).
Args:
config_type_or_name: Either the name of the builder (e.g. link-paladin) or
the config type if you want to abort all HWTests for
that config (e.g. config_lib.CONFIG_TYPE_FULL).
version: The version of the current build. E.g. R18-1655.0.0-rc1
debug: Whether we are in debug mode.
suite: Name of the Autotest suite. If empty, abort all suites.
"""
# Abort all jobs for the given config and version.
# Example for a specific config: link-paladin/R35-5542.0.0-rc1
# Example for a config type: paladin/R35-5542.0.0-rc1
substr = '%s/%s' % (config_type_or_name, version)
abort_args = ['-i', substr, '-s', suite]
try:
cmd = [_ABORT_SUITE_PATH] + abort_args
swarming_args = {
'swarming_server':
topology.topology.get(topology.SWARMING_PROXY_HOST_KEY),
'task_name':
'-'.join(['abort', substr, suite]),
'dimensions': [('os', 'Ubuntu-14.04'), ('pool', 'default')],
'print_status_updates':
True,
'expiration_secs':
_SWARMING_EXPIRATION
}
if debug:
logging.info(
'AbortHWTests would run the cmd via '
'swarming, cmd: %s, swarming_args: %s', cros_build_lib.CmdToStr(cmd),
str(swarming_args))
else:
swarming_lib.RunSwarmingCommand(cmd, **swarming_args)
except cros_build_lib.RunCommandError:
logging.warning('AbortHWTests failed', exc_info=True)
def AbortSkylabHWTests(build, board, debug, suite):
"""Abort the specified hardware tests for the given bot(s).
Args:
build: A string build name, like 'link-paladin/R18-1655.0.0-rc1'.
board: The name of the board.
debug: Whether we are in debug mode.
suite: Name of the Autotest suite.
"""
abort_args = ['--board', board, '--suite_name', suite, '--build', build]
try:
cmd = [_SKYLAB_ABORT_SUITE_PATH] + abort_args
swarming_args = {
'swarming_server':
topology.topology.get(topology.CHROME_SWARMING_PROXY_HOST_KEY),
'task_name':
'-'.join(['abort', build, suite]),
'dimensions': [('os', 'Ubuntu-14.04'), ('pool', SKYLAB_SUITE_BOT_POOL)],
'print_status_updates':
True,
'expiration_secs':
_SWARMING_EXPIRATION,
'tags': {
'luci_project': 'chromeos',
'skylab': 'abort_suite'
}
}
if debug:
logging.info(
'AbortSkylabHWTests would run the cmd via '
'swarming, cmd: %s, swarming_args: %s', cros_build_lib.CmdToStr(cmd),
str(swarming_args))
else:
swarming_lib.RunSwarmingCommand(cmd, **swarming_args)
except cros_build_lib.RunCommandError:
logging.warning('AbortHWTests failed', exc_info=True)
def GenerateStackTraces(buildroot, board, test_results_dir, archive_dir,
got_symbols):
"""Generates stack traces for logs in |gzipped_test_tarball|
Args:
buildroot: Root directory where build occurs.
board: Name of the board being worked on.
test_results_dir: Directory of the test results.
archive_dir: Local directory for archiving.
got_symbols: True if breakpad symbols have been generated.
Returns:
List of stack trace file names.
"""
stack_trace_filenames = []
asan_log_signaled = False
board_path = cros_build_lib.GetSysroot(board=board)
symbol_dir = os.path.join(board_path, 'usr', 'lib', 'debug', 'breakpad')
for curr_dir, _subdirs, files in os.walk(test_results_dir):
for curr_file in files:
full_file_path = os.path.join(curr_dir, curr_file)
processed_file_path = '%s.txt' % full_file_path
# Distinguish whether the current file is a minidump or asan_log.
if curr_file.endswith('.dmp'):
# Skip crash files that were purposely generated or if
# breakpad symbols are absent.
if not got_symbols or curr_file.find('crasher_nobreakpad') == 0:
continue
# Precess the minidump from within chroot.
minidump = path_util.ToChrootPath(full_file_path)
cwd = os.path.join(buildroot, 'src', 'scripts')
cros_build_lib.run(['minidump_stackwalk', minidump, symbol_dir],
cwd=cwd,
enter_chroot=True,
error_code_ok=True,
redirect_stderr=True,
debug_level=logging.DEBUG,
log_stdout_to_file=processed_file_path)
# Process asan log.
else:
# Prepend '/chrome/$board' path to the stack trace in log.
log_content = ''
with open(full_file_path) as f:
for line in f:
# Stack frame line example to be matched here:
# #0 0x721d1831 (/opt/google/chrome/chrome+0xb837831)
stackline_match = re.search(r'^ *#[0-9]* 0x.* \(', line)
if stackline_match:
frame_end = stackline_match.span()[1]
line = line[:frame_end] + board_path + line[frame_end:]
log_content += line
# Symbolize and demangle it.
raw = cros_build_lib.run(
['asan_symbolize.py'],
input=log_content,
enter_chroot=True,
debug_level=logging.DEBUG,
capture_output=True,
extra_env={'LLVM_SYMBOLIZER_PATH': '/usr/bin/llvm-symbolizer'})
cros_build_lib.run(['c++filt'],
input=raw.output,
debug_level=logging.DEBUG,
cwd=buildroot,
redirect_stderr=True,
log_stdout_to_file=processed_file_path)
# Break the bot if asan_log found. This is because some asan
# crashes may not fail any test so the bot stays green.
# Ex: crbug.com/167497
if not asan_log_signaled:
asan_log_signaled = True
logging.error('Asan crash occurred. See asan_logs in Artifacts.')
logging.PrintBuildbotStepFailure()
# Append the processed file to archive.
filename = ArchiveFile(processed_file_path, archive_dir)
stack_trace_filenames.append(filename)
return stack_trace_filenames
@failures_lib.SetFailureType(failures_lib.BuilderFailure)
def ArchiveFile(file_to_archive, archive_dir):
"""Archives the specified file.
Args:
file_to_archive: Full path to file to archive.
archive_dir: Local directory for archiving.
Returns:
The base name of the archived file.
"""
filename = os.path.basename(file_to_archive)
if archive_dir:
archived_file = os.path.join(archive_dir, filename)
shutil.copy(file_to_archive, archived_file)
os.chmod(archived_file, 0o644)
return filename
class AndroidIsPinnedUprevError(failures_lib.InfrastructureFailure):
"""Raised when we try to uprev while Android is pinned."""
def __init__(self, new_android_atom):
"""Initialize a AndroidIsPinnedUprevError.
Args:
new_android_atom: The Android atom that we failed to
uprev to, due to Android being pinned.
"""
assert new_android_atom
msg = ('Failed up uprev to Android version %s as Android was pinned.' %
new_android_atom)
super(AndroidIsPinnedUprevError, self).__init__(msg)
self.new_android_atom = new_android_atom
class ChromeIsPinnedUprevError(failures_lib.InfrastructureFailure):
"""Raised when we try to uprev while chrome is pinned."""
def __init__(self, new_chrome_atom):
"""Initialize a ChromeIsPinnedUprevError.
Args:
new_chrome_atom: The chrome atom that we failed to
uprev to, due to chrome being pinned.
"""
msg = ('Failed up uprev to chrome version %s as chrome was pinned.' %
new_chrome_atom)
super(ChromeIsPinnedUprevError, self).__init__(msg)
self.new_chrome_atom = new_chrome_atom
def MarkAndroidAsStable(buildroot,
tracking_branch,
android_package,
android_build_branch,
boards=None,
android_version=None,
android_gts_build_branch=None):
"""Returns the portage atom for the revved Android ebuild - see man emerge."""
input_msg = android_pb2.MarkStableRequest()
input_msg.tracking_branch = tracking_branch
input_msg.package_name = android_package
input_msg.android_build_branch = android_build_branch
if android_version:
input_msg.android_version = android_version
if android_gts_build_branch:
input_msg.android_gts_build_branch = android_gts_build_branch
if boards:
for board in boards:
input_msg.build_targets.add().name = board
result = CallBuildApiWithInputProto(
buildroot, 'chromite.api.AndroidService/MarkStable',
json_format.MessageToDict(input_msg))
if result['status'] == android_pb2.MARK_STABLE_STATUS_EARLY_EXIT:
# Early exit (nothing to uprev).
return None
# The result comes back with camelCase names rather than the underscore
# separated names we use for the response objects. For now, adding support
# for both just in case. Will need to fix that so we can use response
# objects in the long term.
atom = result.get('androidAtom') or result.get('android_atom')
if not atom:
logging.info('Found nothing to rev.')
return None
category = atom['category']
package = atom.get('packageName') or atom.get('package_name')
version = atom['version']
android_atom = '%s/%s-%s' % (category, package, version)
if result['status'] == android_pb2.MARK_STABLE_STATUS_PINNED:
# Failed to emerge the new package, probably pinned.
raise AndroidIsPinnedUprevError(android_atom)
return android_atom
def MarkChromeAsStable(buildroot,
tracking_branch,
chrome_rev,
boards,
chrome_version=None):
"""Returns the portage atom for the revved chrome ebuild - see man emerge."""
cwd = os.path.join(buildroot, 'src', 'scripts')
extra_env = None
chroot_args = None
command = [
'../../chromite/bin/cros_mark_chrome_as_stable',
'--tracking_branch=%s' % tracking_branch
]
if boards:
command.append('--boards=%s' % ':'.join(boards))
if chrome_version:
command.append('--force_version=%s' % chrome_version)
portage_atom_string = cros_build_lib.run(
command + [chrome_rev],
cwd=cwd,
redirect_stdout=True,
enter_chroot=True,
chroot_args=chroot_args,
extra_env=extra_env).output.rstrip()
chrome_atom = None
if portage_atom_string:
chrome_atom = portage_atom_string.splitlines()[-1].partition('=')[-1]
if not chrome_atom:
logging.info('Found nothing to rev.')
return None
for board in boards:
# If we're using a version of Chrome other than the latest one, we need
# to unmask it manually.
if chrome_rev != constants.CHROME_REV_LATEST:
keywords_file = CHROME_KEYWORDS_FILE % {'board': board}
for keywords_file in (CHROME_KEYWORDS_FILE % {
'board': board
}, CHROME_UNMASK_FILE % {
'board': board
}):
cros_build_lib.sudo_run(
['mkdir', '-p', os.path.dirname(keywords_file)],
enter_chroot=True,
cwd=cwd)
cros_build_lib.sudo_run(['tee', keywords_file],
input='=%s\n' % chrome_atom,
enter_chroot=True, cwd=cwd)
# Sanity check: We should always be able to merge the version of
# Chrome we just unmasked.
try:
cros_build_lib.run(
['emerge-%s' % board, '-p', '--quiet',
'=%s' % chrome_atom],
enter_chroot=True)
except cros_build_lib.RunCommandError:
logging.error(
'Cannot emerge-%s =%s\nIs Chrome pinned to an older '
'version?', board, chrome_atom)
raise ChromeIsPinnedUprevError(chrome_atom)
return chrome_atom
def CleanupChromeKeywordsFile(boards, buildroot):
"""Cleans chrome uprev artifact if it exists."""
for board in boards:
keywords_path_in_chroot = CHROME_KEYWORDS_FILE % {'board': board}
keywords_file = '%s/chroot%s' % (buildroot, keywords_path_in_chroot)
if os.path.exists(keywords_file):
cros_build_lib.sudo_run(['rm', '-f', keywords_file])
def UprevPackages(buildroot, boards, overlay_type, workspace=None):
"""Uprevs non-browser chromium os packages that have changed.
Args:
buildroot: Root directory where build occurs.
boards: List of boards to uprev.
overlay_type: A value from constants.VALID_OVERLAYS.
workspace: Alternative buildroot directory to uprev.
"""
assert overlay_type
drop_file = _PACKAGE_FILE % {'buildroot': buildroot}
cmd = [
'cros_mark_as_stable',
'commit',
'--all',
'--boards=%s' % ':'.join(boards),
'--drop_file=%s' % drop_file,
'--buildroot',
workspace or buildroot,
'--overlay-type',
overlay_type,
]
RunBuildScript(buildroot, cmd, chromite_cmd=True)
def RegenPortageCache(overlays):
"""Regenerate portage cache for the list of overlays."""
task_inputs = [[o] for o in overlays if os.path.isdir(o)]
parallel.RunTasksInProcessPool(portage_util.RegenCache, task_inputs)
def UprevPush(buildroot,
overlay_type,
dryrun=True,
staging_branch=None,
workspace=None):
"""Pushes uprev changes to the main line.
Args:
buildroot: Root directory where build occurs.
dryrun: If True, do not actually push.
staging_branch: If not None, push uprev commits to this
staging_branch.
overlay_type: A value from constants.VALID_OVERLAYS.
workspace: Alternative buildroot directory to uprev.
"""
assert overlay_type
cmd = [
'cros_mark_as_stable',
'push',
'--buildroot',
workspace or buildroot,
'--overlay-type',
overlay_type,
]
if staging_branch is not None:
cmd.append('--staging_branch=%s' % staging_branch)
if dryrun:
cmd.append('--dryrun')
RunBuildScript(buildroot, cmd, chromite_cmd=True)
def ExtractDependencies(buildroot,
packages,
board=None,
useflags=None,
cpe_format=False,
raw_cmd_result=False):
"""Extracts dependencies for |packages|.
Args:
buildroot: The root directory where the build occurs.
packages: A list of packages for which to extract dependencies.
board: Board type that was built on this machine.
useflags: A list of useflags for this build.
cpe_format: Set output format to CPE-only JSON; otherwise,
output traditional deps.
raw_cmd_result: If set True, returns the CommandResult object.
Otherwise, returns the dependencies as a dictionary.
Returns:
Returns the CommandResult object if |raw_cmd_result| is set; returns
the dependencies in a dictionary otherwise.
"""
cmd = ['cros_extract_deps']
if board:
cmd += ['--board', board]
if cpe_format:
cmd += ['--format=cpe']
else:
cmd += ['--format=deps']
cmd += packages
env = {}
if useflags:
env['USE'] = ' '.join(useflags)
if raw_cmd_result:
return RunBuildScript(
buildroot,
cmd,
enter_chroot=True,
chromite_cmd=True,
capture_output=True,
extra_env=env)
# The stdout of cros_extract_deps may contain undesirable
# output. Avoid that by instructing the script to explicitly dump
# the deps into a file.
with tempfile.NamedTemporaryFile(
dir=os.path.join(buildroot, 'chroot', 'tmp')) as f:
cmd += ['--output-path', path_util.ToChrootPath(f.name)]
RunBuildScript(
buildroot,
cmd,
enter_chroot=True,
chromite_cmd=True,
capture_output=True,
extra_env=env)
return json.loads(f.read())
def ExtractBuildDepsGraph(buildroot, board):
"""Extract the build deps graph for |board| using build_api proto service.
Args:
buildroot: The root directory where the build occurs.
board: Board type that was built on this machine.
"""
input_proto = {
'build_target': {
'name': board,
},
}
json_output = CallBuildApiWithInputProto(
buildroot, 'chromite.api.DependencyService/GetBuildDependencyGraph',
input_proto)
return json_output['depGraph']
def GenerateBuildConfigs(board, config_useflags):
"""Generate build configs..
Args:
board: Board type that was built on this machine.
config_useflags: A list of useflags for this build set by the cbuildbot
configs.
Returns:
A jsonizable object which is the combination of config.yaml (for unibuild)
and use flags.
"""
config_chroot_path = os.path.join(
cros_build_lib.GetSysroot(board), 'usr', 'share', 'chromeos-config',
'yaml', 'config.yaml')
config_fname = path_util.FromChrootPath(config_chroot_path)
results = {}
if os.path.exists(config_fname):
with open(config_fname) as f:
results = json.load(f)
else:
logging.warning('Cannot find config.yaml file in %s', config_fname)
results['board'] = board
results['useflags'] = portage_util.PortageqEnvvar(
variable='USE', board=board, allow_undefined=False)
if config_useflags:
results['config_useflags'] = config_useflags
return results
def GenerateCPEExport(buildroot, board, useflags=None):
"""Generate CPE export.
Args:
buildroot: The root directory where the build occurs.
board: Board type that was built on this machine.
useflags: A list of useflags for this build.
Returns:
A CommandResult object with the results of running the CPE
export command.
"""
return ExtractDependencies(
buildroot, ['virtual/target-os'],
board=board,
useflags=useflags,
cpe_format=True,
raw_cmd_result=True)
def GenerateBreakpadSymbols(buildroot,
board,
debug,
extra_env=None,
chroot_args=None):
"""Generate breakpad symbols.
Args:
buildroot: The root directory where the build occurs.
board: Board type that was built on this machine.
debug: Include extra debugging output.
extra_env: A dictionary of environmental variables to set during generation.
chroot_args: The args to the chroot.
"""
# We don't care about firmware symbols.
# See https://crbug.com/213670.
exclude_dirs = ['firmware']
cmd = [
'cros_generate_breakpad_symbols',
'--board=%s' % board, '--jobs',
str(max([1, multiprocessing.cpu_count() // 2]))
]
cmd += ['--exclude-dir=%s' % x for x in exclude_dirs]
if debug:
cmd += ['--debug']
RunBuildScript(
buildroot,
cmd,
enter_chroot=True,
chromite_cmd=True,
chroot_args=chroot_args,
extra_env=extra_env)
def GenerateAndroidBreakpadSymbols(buildroot,
board,
symbols_file,
extra_env=None,
chroot_args=None):
"""Generate breakpad symbols of Android binaries.
Args:
buildroot: The root directory where the build occurs.
board: Board type that was built on this machine.
symbols_file: Path to a symbol archive file.
extra_env: A dictionary of environmental variables to set during generation.
chroot_args: The args to the chroot.
"""
board_path = cros_build_lib.GetSysroot(board=board)
breakpad_dir = os.path.join(board_path, 'usr', 'lib', 'debug', 'breakpad')
cmd = [
'cros_generate_android_breakpad_symbols',
'--symbols_file=%s' % path_util.ToChrootPath(symbols_file),
'--breakpad_dir=%s' % breakpad_dir
]
RunBuildScript(
buildroot,
cmd,
enter_chroot=True,
chromite_cmd=True,
chroot_args=chroot_args,
extra_env=extra_env)
def GenerateDebugTarball(buildroot,
board,
archive_path,
gdb_symbols,
archive_name='debug.tgz',
chroot_compression=True):
"""Generates a debug tarball in the archive_dir.
Args:
buildroot: The root directory where the build occurs.
board: Board type that was built on this machine
archive_path: Directory where tarball should be stored.
gdb_symbols: Include *.debug files for debugging core files with gdb.
archive_name: Name of the tarball to generate.
chroot_compression: Whether to use compression tools in the chroot if
they're available.
Returns:
The filename of the created debug tarball.
"""
# Generate debug tarball. This needs to run as root because some of the
# symbols are only readable by root.
chroot = os.path.join(buildroot, 'chroot')
board_dir = os.path.join(chroot, 'build', board, 'usr', 'lib')
debug_tarball = os.path.join(archive_path, archive_name)
extra_args = None
inputs = None
if gdb_symbols:
extra_args = [
'--exclude',
os.path.join('debug', constants.AUTOTEST_BUILD_PATH), '--exclude',
'debug/tests'
]
inputs = ['debug']
else:
inputs = ['debug/breakpad']
compression_chroot = None
if chroot_compression:
compression_chroot = chroot
compression = cros_build_lib.CompressionExtToType(debug_tarball)
cros_build_lib.CreateTarball(
debug_tarball,
board_dir,
sudo=True,
compression=compression,
chroot=compression_chroot,
inputs=inputs,
extra_args=extra_args)
# Fix permissions and ownership on debug tarball.
cros_build_lib.sudo_run(['chown', str(os.getuid()), debug_tarball])
os.chmod(debug_tarball, 0o644)
return os.path.basename(debug_tarball)
def GenerateUploadJSON(filepath, archive_path, uploaded):
"""Generate upload.json file given a set of filenames.
The JSON is a dictionary keyed by filename, with entries for size, and hashes.
Args:
filepath: complete output filepath as string.
archive_path: location of files.
uploaded: file with list of uploaded filepaths, relative to archive_path.
"""
utcnow = datetime.datetime.utcnow
start = utcnow()
result = {}
files = osutils.ReadFile(uploaded).splitlines()
for f in files:
path = os.path.join(archive_path, f)
# Ignore directories.
if os.path.isdir(path):
continue
size = os.path.getsize(path)
sha1, sha256 = filelib.ShaSums(path)
result[f] = {'size': size, 'sha1': sha1, 'sha256': sha256}
osutils.WriteFile(filepath, json.dumps(result, indent=2, sort_keys=True))
logging.info('GenerateUploadJSON completed in %s.', utcnow() - start)
def GenerateHtmlIndex(index, files, title='Index', url_base=None):
"""Generate a simple index.html file given a set of filenames
Args:
index: The file to write the html index to.
files: The list of files to create the index of. If a string, then it
may be a path to a file (with one file per line), or a directory
(which will be listed).
title: Title string for the HTML file.
url_base: The URL to prefix to all elements (otherwise they'll be relative).
"""
def GenLink(target, name=None):
if name == '':
return ''
return ('<li><a href="%s%s">%s</a></li>' % (url_base, target,
name if name else target))
if isinstance(files, six.string_types):
if os.path.isdir(files):
files = os.listdir(files)
else:
files = osutils.ReadFile(files).splitlines()
url_base = url_base + '/' if url_base else ''
# Head + open list.
html = '<html>'
html += '<head><title>%s</title></head>' % title
html += '<body><h2>%s</h2><ul>' % title
# List members.
dot = ('.',)
dot_dot = ('..',)
links = []
for a in sorted(set(files)):
a = a.split('|')
if a[0] == '.':
dot = a
elif a[0] == '..':
dot_dot = a
else:
links.append(GenLink(*a))
links.insert(0, GenLink(*dot_dot))
links.insert(0, GenLink(*dot))
html += '\n'.join(links)
# Close list and file.
html += '</ul></body></html>'
osutils.WriteFile(index, html)
def GenerateHtmlTimeline(timeline, rows, title):
"""Generate a simple timeline.html file given a list of timings.
Args:
timeline: The file to write the html index to.
rows: The list of rows to generate a timeline of. Each row should be
tuple of (entry, start_time, end_time)
title: Title of the timeline.
"""
_HTML = """<html>
<head>
<title>%(title)s</title>
%(javascript)s
</head>
<body>
<h2>%(title)s</h2>
<!--Div that will hold the timeline-->
<div id="chart_div"></div>
</body>
</html>
"""
_JAVASCRIPT_HEAD = """
<!--Load the AJAX API-->
<script type="text/javascript" src="https://www.gstatic.com/charts/loader.js"></script>
<script type="text/javascript">
// Load the Visualization API and the timeline package.
google.charts.load('current', {'packages':['timeline']});
// Set a callback to run when the Google Visualization API is loaded.
google.charts.setOnLoadCallback(drawChart);
// Callback that creates and populates a data table,
// instantiates the timeline, passes in the data and
// draws it.
function drawChart() {
// Create the data table.
var container = document.getElementById('chart_div');
var data = new google.visualization.DataTable();
data.addColumn('string', 'Entry');
data.addColumn('datetime', 'Start Time');
data.addColumn('datetime', 'Finish Time');
"""
_JAVASCRIPT_TAIL = """
// Set chart options
var height = data.getNumberOfRows() * 50 + 30;
var options = {'title':'timings',
'width':'100%',
'height':height};
// Instantiate and draw our chart, passing in some options.
var chart = new google.visualization.Timeline(container);
chart.draw(data, options);
}
</script>
"""
def GenRow(entry, start, end):
def GenDate(time):
# Javascript months are 0..11 instead of 1..12
return ('new Date(%d, %d, %d, %d, %d, %d)' %
(time.year, time.month - 1, time.day, time.hour, time.minute,
time.second))
return ('data.addRow(["%s", %s, %s]);\n' % (entry, GenDate(start),
GenDate(end)))
now = datetime.datetime.utcnow()
rows = [(entry, start, end or now)
for (entry, start, end) in rows
if entry and start]
javascript = [_JAVASCRIPT_HEAD]
javascript += [GenRow(entry, start, end) for (entry, start, end) in rows]
javascript.append(_JAVASCRIPT_TAIL)
data = {'javascript': ''.join(javascript), 'title': title if title else ''}
html = _HTML % data
osutils.WriteFile(timeline, html)
@failures_lib.SetFailureType(failures_lib.GSUploadFailure)
def _UploadPathToGS(local_path, upload_urls, debug, timeout, acl=None):
"""Upload |local_path| to Google Storage.
Args:
local_path: Local path to upload.
upload_urls: Iterable of GS locations to upload to.
debug: Whether we are in debug mode.
filename: Filename of the file to upload.
timeout: Timeout in seconds.
acl: Canned gsutil acl to use.
"""
gs_context = gs.GSContext(acl=acl, dry_run=debug)
for upload_url in upload_urls:
with timeout_util.Timeout(timeout):
gs_context.CopyInto(local_path, upload_url, parallel=True, recursive=True)
@failures_lib.SetFailureType(failures_lib.InfrastructureFailure)
def ExportToGCloud(build_root,
creds_file,
filename,
namespace=None,
parent_key=None,
project_id=None,
caller=None):
"""Export the given file to gCloud Datastore using export_to_gcloud
Args:
build_root: The root of the chromium os checkout.
creds_file: Filename of gcloud credential file
filename: Name of file to export.
namespace: Optional, namespace to store entities. Defaults to
datastore credentials
parent_key: Optional, Key of parent entity to insert into, expects tuple.
project_id: Optional, project_id of datastore to write to. Defaults to
datastore credentials
caller: Optional, name of the caller. We emit a metric for each run with
this value in the metric:caller field.
Returns:
If command was successfully run or not
"""
export_cmd = os.path.join(build_root, 'chromite', 'bin', 'export_to_gcloud')
cmd = [export_cmd, creds_file, filename]
if namespace:
cmd.extend(['--namespace', namespace])
if parent_key:
cmd.extend(['--parent_key', repr(parent_key)])
if project_id:
cmd.extend(['--project_id', project_id])
try:
cros_build_lib.run(cmd)
success = True
except cros_build_lib.RunCommandError as e:
logging.warn('Unable to export to datastore: %s', e)
success = False
metrics.Counter(constants.MON_EXPORT_TO_GCLOUD).increment(fields={
'caller': str(caller),
'success': success
})
return success
@failures_lib.SetFailureType(failures_lib.InfrastructureFailure)
def UploadArchivedFile(archive_dir,
upload_urls,
filename,
debug,
update_list=False,
timeout=2 * 60 * 60,
acl=None):
"""Uploads |filename| in |archive_dir| to Google Storage.
Args:
archive_dir: Path to the archive directory.
upload_urls: Iterable of GS locations to upload to.
debug: Whether we are in debug mode.
filename: Name of the file to upload.
update_list: Flag to update the list of uploaded files.
timeout: Timeout in seconds.
acl: Canned gsutil acl to use.
"""
# Upload the file.
file_path = os.path.join(archive_dir, filename)
_UploadPathToGS(file_path, upload_urls, debug, timeout, acl=acl)
if update_list:
# Append |filename| to the local list of uploaded files and archive
# the list to Google Storage. As long as the |filename| string is
# less than PIPE_BUF (> 512 bytes), the append is atomic.
uploaded_file_path = os.path.join(archive_dir, UPLOADED_LIST_FILENAME)
osutils.WriteFile(uploaded_file_path, filename + '\n', mode='a')
_UploadPathToGS(uploaded_file_path, upload_urls, debug, timeout)
def UploadSymbols(buildroot,
board=None,
official=False,
cnt=None,
failed_list=None,
breakpad_root=None,
product_name=None,
extra_env=None,
chroot_args=None):
"""Upload debug symbols for this build."""
cmd = ['upload_symbols', '--yes', '--dedupe']
if board is not None:
# Board requires both root and board to be set to be useful.
cmd += [
'--root',
os.path.join(buildroot, constants.DEFAULT_CHROOT_DIR), '--board', board
]
if official:
cmd.append('--official_build')
if cnt is not None:
cmd += ['--upload-limit', str(cnt)]
if failed_list is not None:
cmd += ['--failed-list', str(failed_list)]
if breakpad_root is not None:
cmd += ['--breakpad_root', breakpad_root]
if product_name is not None:
cmd += ['--product_name', product_name]
# We don't want to import upload_symbols directly because it uses the
# swarming module which itself imports a _lot_ of stuff. It has also
# been known to hang. We want to keep cbuildbot isolated & robust.
RunBuildScript(
buildroot,
cmd,
chromite_cmd=True,
chroot_args=chroot_args,
extra_env=extra_env)
def PushImages(board,
archive_url,
dryrun,
profile,
sign_types=(),
buildroot=None):
"""Push the generated image to the release bucket for signing."""
# Log the equivalent command for debugging purposes.
log_cmd = ['pushimage', '--board=%s' % board]
if dryrun:
log_cmd.append('-n')
if profile:
log_cmd.append('--profile=%s' % profile)
if sign_types:
log_cmd.append('--sign-types=%s' % ' '.join(sign_types))
if buildroot:
log_cmd.append('--buildroot=%s' % buildroot)
log_cmd.append(archive_url)
logging.info('Running: %s', cros_build_lib.CmdToStr(log_cmd))
try:
return pushimage.PushImage(
archive_url,
board,
profile=profile,
sign_types=sign_types,
dry_run=dryrun,
buildroot=buildroot)
except pushimage.PushError as e:
logging.PrintBuildbotStepFailure()
return e.args[1]
def BuildFactoryInstallImage(buildroot, board, extra_env):
"""Build a factory install image.
Args:
buildroot: Root directory where build occurs.
board: Board type that was built on this machine
extra_env: Flags to be added to the environment for the new process.
Returns:
The basename of the symlink created for the image.
"""
# We use build_attempt=3 here to ensure that this image uses a different
# output directory from our regular image and the factory test image.
alias = _FACTORY_SHIM
cmd = [
'./build_image',
'--board=%s' % board, '--replace',
'--symlink=%s' % alias, '--build_attempt=3', 'factory_install'
]
RunBuildScript(
buildroot,
cmd,
extra_env=extra_env,
capture_output=True,
enter_chroot=True)
return alias
def MakeNetboot(buildroot, board, image_dir):
"""Build a netboot image.
Args:
buildroot: Root directory where build occurs.
board: Board type that was built on this machine.
image_dir: Directory containing factory install shim.
"""
cmd = [
'./make_netboot.sh',
'--board=%s' % board,
'--image_dir=%s' % path_util.ToChrootPath(image_dir)
]
RunBuildScript(buildroot, cmd, capture_output=True, enter_chroot=True)
def BuildRecoveryImage(buildroot, board, image_dir, extra_env):
"""Build a recovery image.
Args:
buildroot: Root directory where build occurs.
board: Board type that was built on this machine.
image_dir: Directory containing base image.
extra_env: Flags to be added to the environment for the new process.
"""
base_image = os.path.join(image_dir, constants.BASE_IMAGE_BIN)
# mod_image_for_recovery leaves behind some artifacts in the source directory
# that we don't care about. So, use a tempdir as the working directory.
# This tempdir needs to be at a chroot accessible path.
with osutils.TempDir(base_dir=image_dir) as tempdir:
tempdir_base_image = os.path.join(tempdir, constants.BASE_IMAGE_BIN)
tempdir_recovery_image = os.path.join(tempdir, constants.RECOVERY_IMAGE_BIN)
# Copy the base image. Symlinking isn't enough because image building
# scripts follow symlinks by design.
shutil.copyfile(base_image, tempdir_base_image)
cmd = [
'./mod_image_for_recovery.sh',
'--board=%s' % board,
'--image=%s' % path_util.ToChrootPath(tempdir_base_image)
]
RunBuildScript(
buildroot,
cmd,
extra_env=extra_env,
capture_output=True,
enter_chroot=True)
shutil.move(tempdir_recovery_image, image_dir)
def BuildTarball(buildroot,
input_list,
tarball_output,
cwd=None,
compressed=True,
**kwargs):
"""Tars and zips files and directories from input_list to tarball_output.
Args:
buildroot: Root directory where build occurs.
input_list: A list of files and directories to be archived.
tarball_output: Path of output tar archive file.
cwd: Current working directory when tar command is executed.
compressed: Whether or not the tarball should be compressed with pbzip2.
**kwargs: Keyword arguments to pass to CreateTarball.
Returns:
Return value of cros_build_lib.CreateTarball.
"""
compressor = cros_build_lib.COMP_NONE
chroot = None
if compressed:
compressor = cros_build_lib.COMP_BZIP2
chroot = os.path.join(buildroot, 'chroot')
return cros_build_lib.CreateTarball(
tarball_output,
cwd,
compression=compressor,
chroot=chroot,
inputs=input_list,
**kwargs)
def FindFilesWithPattern(pattern, target='./', cwd=os.curdir, exclude_dirs=()):
"""Search the root directory recursively for matching filenames.
Args:
pattern: the pattern used to match the filenames.
target: the target directory to search.
cwd: current working directory.
exclude_dirs: Directories to not include when searching.
Returns:
A list of paths of the matched files.
"""
# Backup the current working directory before changing it
old_cwd = os.getcwd()
os.chdir(cwd)
matches = []
for root, _, filenames in os.walk(target):
if not any(root.startswith(e) for e in exclude_dirs):
for filename in fnmatch.filter(filenames, pattern):
matches.append(os.path.join(root, filename))
# Restore the working directory
os.chdir(old_cwd)
return matches
def BuildAutotestControlFilesTarball(buildroot, cwd, tarball_dir):
"""Tar up the autotest control files.
Args:
buildroot: Root directory where build occurs.
cwd: Current working directory.
tarball_dir: Location for storing autotest tarball.
Returns:
Path of the partial autotest control files tarball.
"""
# Find the control files in autotest/
control_files = FindFilesWithPattern(
'control*',
target='autotest',
cwd=cwd,
exclude_dirs=['autotest/test_suites'])
control_files_tarball = os.path.join(tarball_dir, 'control_files.tar')
BuildTarball(
buildroot,
control_files,
control_files_tarball,
cwd=cwd,
compressed=False)
return control_files_tarball
def BuildAutotestPackagesTarball(buildroot, cwd, tarball_dir):
"""Tar up the autotest packages.
Args:
buildroot: Root directory where build occurs.
cwd: Current working directory.
tarball_dir: Location for storing autotest tarball.
Returns:
Path of the partial autotest packages tarball.
"""
input_list = ['autotest/packages']
packages_tarball = os.path.join(tarball_dir, 'autotest_packages.tar')
BuildTarball(
buildroot, input_list, packages_tarball, cwd=cwd, compressed=False)
return packages_tarball
def BuildAutotestTestSuitesTarball(buildroot, cwd, tarball_dir):
"""Tar up the autotest test suite control files.
Args:
buildroot: Root directory where build occurs.
cwd: Current working directory.
tarball_dir: Location for storing autotest tarball.
Returns:
Path of the autotest test suites tarball.
"""
test_suites_tarball = os.path.join(tarball_dir, 'test_suites.tar.bz2')
BuildTarball(
buildroot, ['autotest/test_suites'], test_suites_tarball, cwd=cwd)
return test_suites_tarball
def BuildAutotestServerPackageTarball(buildroot, cwd, tarball_dir):
"""Tar up the autotest files required by the server package.
Args:
buildroot: Root directory where build occurs.
cwd: Current working directory.
tarball_dir: Location for storing autotest tarballs.
Returns:
The path of the autotest server package tarball.
"""
# Find all files in autotest excluding certain directories.
autotest_files = FindFilesWithPattern(
'*',
target='autotest',
cwd=cwd,
exclude_dirs=('autotest/packages', 'autotest/client/deps/',
'autotest/client/tests', 'autotest/client/site_tests'))
tast_files, transforms = _GetTastServerFilesAndTarTransforms(buildroot)
tarball = os.path.join(tarball_dir, AUTOTEST_SERVER_PACKAGE)
BuildTarball(
buildroot,
autotest_files + tast_files,
tarball,
cwd=cwd,
extra_args=transforms,
error_code_ok=True)
return tarball
def _GetTastServerFilesAndTarTransforms(buildroot):
"""Returns Tast server files and corresponding tar transform flags.
The returned paths should be included in AUTOTEST_SERVER_PACKAGE. The
--transform arguments should be passed to GNU tar to convert the paths to
appropriate destinations in the tarball.
Args:
buildroot: Absolute path to root build directory.
Returns:
(files, transforms), where files is a list of absolute paths to Tast server
files/directories and transforms is a list of --transform arguments to
pass to GNU tar when archiving those files.
"""
files = []
transforms = []
for p in TAST_SSP_FILES:
path = os.path.join(buildroot, p)
if os.path.exists(path):
files.append(path)
dest = os.path.join(_TAST_SSP_SUBDIR, os.path.basename(path))
transforms.append(
'--transform=s|^%s|%s|' % (os.path.relpath(path, '/'), dest))
return files, transforms
def BuildAutotestTarballsForHWTest(buildroot, cwd, tarball_dir):
"""Generate the "usual" autotest tarballs required for running HWTests.
These tarballs are created in multiple places wherever they need to be staged
for running HWTests.
Args:
buildroot: Root directory where build occurs.
cwd: Current working directory.
tarball_dir: Location for storing autotest tarballs.
Returns:
A list of paths of the generated tarballs.
TODO(crbug.com/924655): Has been ported to a build API endpoint. Remove this
function and any unused child functions when the stages have been updated to
use the API call.
"""
return [
BuildAutotestControlFilesTarball(buildroot, cwd, tarball_dir),
BuildAutotestPackagesTarball(buildroot, cwd, tarball_dir),
BuildAutotestTestSuitesTarball(buildroot, cwd, tarball_dir),
BuildAutotestServerPackageTarball(buildroot, cwd, tarball_dir),
]
def BuildTastBundleTarball(buildroot, cwd, tarball_dir):
"""Tar up the Tast private test bundles.
Args:
buildroot: Root directory where build occurs.
cwd: Current working directory pointing /build/$board/build.
tarball_dir: Location for storing the tarball.
Returns:
Path of the generated tarball, or None if there is no private test bundles.
"""
chroot = chroot_lib.Chroot(os.path.join(buildroot, 'chroot'))
sysroot_path = cwd.replace(chroot.path, '', 1)
sysroot_path = sysroot_path.rstrip('build').rstrip(os.sep)
sysroot = sysroot_lib.Sysroot(sysroot_path)
return artifacts_service.BundleTastFiles(chroot, sysroot, tarball_dir)
def BuildPinnedGuestImagesTarball(buildroot, board, tarball_dir):
"""Create a tarball of Guest VM and Container images for testing.
Args:
buildroot: Root directory where build occurs.
board: Board name of build target.
tarball_dir: Location for storing the images and tarball.
Returns:
Path of the generated tarball, or None if there are no images.
"""
pins_root = os.path.abspath(
os.path.join(buildroot, 'chroot', 'build', board,
constants.GUEST_IMAGES_PINS_PATH))
gs_context = gs.GSContext()
files = []
for pin_file in sorted(glob.iglob(os.path.join(pins_root, '*.json'))):
with open(pin_file) as f:
pin = json.load(f)
try:
filename = os.path.join(tarball_dir, pin[constants.PIN_KEY_FILENAME])
gs_context.Copy(pin[constants.PIN_KEY_GSURI], filename)
files.append(pin[constants.PIN_KEY_FILENAME])
except KeyError as e:
logging.warning("Skipping invalid pin file '%s': %r", pin_file, e)
if not files:
return None
tarball = os.path.join(tarball_dir, 'guest_images.tar')
BuildTarball(buildroot, files, tarball, cwd=tarball_dir, compressed=False)
return tarball
def BuildFullAutotestTarball(buildroot, board, tarball_dir):
"""Tar up the full autotest directory into image_dir.
Args:
buildroot: Root directory where build occurs.
board: Board type that was built on this machine.
tarball_dir: Location for storing autotest tarballs.
Returns:
A tuple the path of the full autotest tarball.
"""
tarball = os.path.join(tarball_dir, 'autotest.tar.bz2')
cwd = os.path.abspath(
os.path.join(buildroot, 'chroot', 'build', board,
constants.AUTOTEST_BUILD_PATH, '..'))
result = BuildTarball(
buildroot, ['autotest'], tarball, cwd=cwd, error_code_ok=True)
# Emerging the autotest package to the factory test image while this is
# running modifies the timestamp on /build/autotest/server by
# adding a tmp directory underneath it.
# When tar spots this, it flags this and returns
# status code 1. The tarball is still OK, although there might be a few
# unneeded (and garbled) tmp files. If tar fails in a different way, it'll
# return an error code other than 1.
# TODO: Fix the autotest ebuild. See https://crbug.com/237537
if result.returncode not in (0, 1):
raise Exception('Autotest tarball creation failed with exit code %s' %
(result.returncode))
return tarball
def BuildUnitTestTarball(buildroot, board, tarball_dir):
"""Tar up the UnitTest binaries."""
tarball = 'unit_tests.tar'
tarball_path = os.path.join(tarball_dir, tarball)
cwd = os.path.abspath(
os.path.join(buildroot, 'chroot', 'build', board,
constants.UNITTEST_PKG_PATH))
# UnitTest binaries are already compressed so just create a tar file.
BuildTarball(
buildroot, ['.'],
tarball_path,
cwd=cwd,
compressed=False,
error_code_ok=True)
return tarball
def BuildImageZip(archive_dir, image_dir):
"""Build image.zip in archive_dir from contents of image_dir.
Exclude the dev image from the zipfile.
Args:
archive_dir: Directory to store image.zip.
image_dir: Directory to zip up.
Returns:
The basename of the zipfile.
"""
filename = 'image.zip'
zipfile = os.path.join(archive_dir, filename)
cros_build_lib.run(['zip', zipfile, '-r', '.'],
cwd=image_dir, capture_output=True)
return filename
def BuildStandaloneArchive(archive_dir, image_dir, artifact_info):
"""Create a compressed archive from the specified image information.
The artifact info is derived from a JSON file in the board overlay. It
should be in the following format:
{
"artifacts": [
{ artifact },
{ artifact },
...
]
}
Each artifact can contain the following keys:
input - Required. A list of paths and globs that expands to
the list of files to archive.
output - the name of the archive to be created. If omitted,
it will default to the first filename, stripped of
extensions, plus the appropriate .tar.gz or other suffix.
archive - "tar" or "zip". If omitted, files will be uploaded
directly, without being archived together.
compress - a value cros_build_lib.CompressionStrToType knows about. Only
useful for tar. If omitted, an uncompressed tar will be created.
Args:
archive_dir: Directory to store image zip.
image_dir: Base path for all inputs.
artifact_info: Extended archive configuration dictionary containing:
- paths - required, list of files to archive.
- output, archive & compress entries from the JSON file.
Returns:
The base name of the archive.
Raises:
A ValueError if the compression or archive values are unknown.
A KeyError is a required field is missing from artifact_info.
"""
if 'archive' not in artifact_info:
# Copy the file in 'paths' as is to the archive directory.
if len(artifact_info['paths']) > 1:
raise ValueError('default archive type does not support multiple inputs')
src_path = os.path.join(image_dir, artifact_info['paths'][0])
tgt_path = os.path.join(archive_dir, artifact_info['paths'][0])
if not os.path.exists(tgt_path):
# The image may have already been copied into place. If so, overwriting it
# can affect parallel processes.
if os.path.isdir(src_path):
shutil.copytree(src_path, tgt_path)
else:
shutil.copy(src_path, tgt_path)
return artifact_info['paths']
inputs = artifact_info['paths']
archive = artifact_info['archive']
compress = artifact_info.get('compress')
compress_type = cros_build_lib.CompressionStrToType(compress)
if compress_type is None:
raise ValueError('unknown compression type: %s' % compress)
# If the output is fixed, use that. Otherwise, construct it
# from the name of the first archived file, stripping extensions.
filename = artifact_info.get(
'output', '%s.%s' % (os.path.splitext(inputs[0])[0], archive))
if archive == 'tar':
# Add the .compress extension if we don't have a fixed name.
if 'output' not in artifact_info and compress:
filename = '%s.%s' % (filename, compress)
extra_env = {'XZ_OPT': '-1'}
cros_build_lib.CreateTarball(
os.path.join(archive_dir, filename),
image_dir,
inputs=inputs,
compression=compress_type,
extra_env=extra_env)
elif archive == 'zip':
cros_build_lib.run(
['zip', os.path.join(archive_dir, filename), '-r'] + inputs,
cwd=image_dir,
capture_output=True)
else:
raise ValueError('unknown archive type: %s' % archive)
return [filename]
def BuildStrippedPackagesTarball(buildroot, board, package_globs, archive_dir):
"""Builds a tarball containing stripped packages.
Args:
buildroot: Root directory where build occurs.
board: The board for which packages should be tarred up.
package_globs: List of package search patterns. Each pattern is used to
search for packages via `equery list`.
archive_dir: The directory to drop the tarball in.
Returns:
The file name of the output tarball, None if no package found.
"""
chroot_path = os.path.join(buildroot, constants.DEFAULT_CHROOT_DIR)
board_path = os.path.join(chroot_path, 'build', board)
stripped_pkg_dir = os.path.join(board_path, 'stripped-packages')
tarball_paths = []
for pattern in package_globs:
packages = portage_util.FindPackageNameMatches(
pattern, board, buildroot=buildroot)
for cpv in packages:
cmd = ['strip_package', '--board', board, cpv.cpf]
cros_build_lib.run(cmd, cwd=buildroot, enter_chroot=True)
# Find the stripped package.
files = glob.glob(os.path.join(stripped_pkg_dir, cpv.cpf) + '.*')
if not files:
raise AssertionError('Silent failure to strip binary %s? '
'Failed to find stripped files at %s.' %
(cpv.cpf, os.path.join(stripped_pkg_dir, cpv.cpf)))
if len(files) > 1:
logging.PrintBuildbotStepWarnings()
logging.warning('Expected one stripped package for %s, found %d',
cpv.cpf, len(files))
tarball = sorted(files)[-1]
tarball_paths.append(os.path.abspath(tarball))
if not tarball_paths:
# tar barfs on an empty list of files, so skip tarring completely.
return None
tarball_output = os.path.join(archive_dir, 'stripped-packages.tar')
BuildTarball(buildroot, tarball_paths, tarball_output, compressed=False)
return os.path.basename(tarball_output)
def BuildEbuildLogsTarball(buildroot, board, archive_dir):
"""Builds a tarball containing ebuild logs.
Args:
buildroot: Root directory where build occurs.
board: The board for which packages should be tarred up.
archive_dir: The directory to drop the tarball in.
Returns:
The file name of the output tarball, None if no package found.
"""
sysroot = sysroot_lib.Sysroot(os.path.join('build', board))
chroot = chroot_lib.Chroot(path=os.path.join(buildroot, 'chroot'))
return artifacts_service.BundleEBuildLogsTarball(chroot, sysroot, archive_dir)
def BuildChromeOSConfig(buildroot, board, archive_dir):
"""Builds the ChromeOS Config JSON payload.
Args:
buildroot: Root directory where the build occurs.
board: The board for which ChromeOS Configs payload should be built.
archive_dir: The directory to drop the payload in.
Returns:
The file name of the output payload.
"""
sysroot = sysroot_lib.Sysroot(os.path.join('build', board))
chroot = chroot_lib.Chroot(path=os.path.join(buildroot, 'chroot'))
return artifacts_service.BundleChromeOSConfig(chroot, sysroot, archive_dir)
def BuildGceTarball(archive_dir, image_dir, image):
"""Builds a tarball that can be converted into a GCE image.
GCE has some very specific requirements about the format of VM
images. The full list can be found at
https://cloud.google.com/compute/docs/tutorials/building-images#requirements
Args:
archive_dir: Directory to store the output tarball.
image_dir: Directory where raw disk file can be found.
image: Name of raw disk file.
Returns:
The file name of the output tarball.
"""
with osutils.TempDir() as tempdir:
temp_disk_raw = os.path.join(tempdir, 'disk.raw')
output = constants.ImageBinToGceTar(image)
output_file = os.path.join(archive_dir, output)
os.symlink(os.path.join(image_dir, image), temp_disk_raw)
cros_build_lib.CreateTarball(
output_file,
tempdir,
inputs=['disk.raw'],
compression=cros_build_lib.COMP_GZIP,
extra_args=['--dereference'])
return os.path.basename(output_file)
def BuildFirmwareArchive(buildroot,
board,
archive_dir,
archive_name=constants.FIRMWARE_ARCHIVE_NAME):
"""Build firmware_from_source.tar.bz2 in archive_dir from build root.
Args:
buildroot: Root directory where build occurs.
board: Board name of build target.
archive_dir: Directory to store output file.
archive_name: Name of file to create in archive_dir.
Returns:
The basename of the archived file, or None if the target board does
not have firmware from source.
"""
sysroot = sysroot_lib.Sysroot(os.path.join('build', board))
chroot = chroot_lib.Chroot(path=os.path.join(buildroot, 'chroot'))
archive_file = artifacts_service.BuildFirmwareArchive(
chroot, sysroot, archive_dir
)
if archive_file is None:
return None
else:
shutil.move(archive_file, os.path.join(archive_dir, archive_name))
return archive_name
def CallBuildApiWithInputProto(buildroot, build_api_command, input_proto):
"""Call BuildApi with the input_proto and buildroot.
Args:
buildroot: Root directory where build occurs.
build_api_command: Service (command) to execute.
input_proto (dict): The input proto as a dict.
Returns:
The json-encoded output proto.
"""
cmd = ['build_api', build_api_command]
with osutils.TempDir() as tmpdir:
input_proto_file = os.path.join(tmpdir, 'input.json')
output_proto_file = os.path.join(tmpdir, 'output.json')
with open(input_proto_file, 'w') as f:
json.dump(input_proto, f)
cmd += [
'--input-json', input_proto_file, '--output-json', output_proto_file
]
RunBuildScript(buildroot, cmd, chromite_cmd=True, redirect_stdout=True)
return json.loads(osutils.ReadFile(output_proto_file))
def BuildFactoryZip(buildroot,
board,
archive_dir,
factory_shim_dir,
version=None):
"""Build factory_image.zip in archive_dir.
Args:
buildroot: Root directory where build occurs.
board: Board name of build target.
archive_dir: Directory to store factory_image.zip.
factory_shim_dir: Directory containing factory shim.
version: The version string to be included in the factory image.zip.
Returns:
The basename of the zipfile.
"""
filename = 'factory_image.zip'
# Creates a staging temporary folder.
temp_dir = tempfile.mkdtemp(prefix='cbuildbot_factory')
zipfile = os.path.join(archive_dir, filename)
cmd = ['zip', '-r', zipfile, '.']
# Rules for archive: { folder: pattern }
rules = {
factory_shim_dir: [
'*factory_install*.bin', '*partition*',
os.path.join('netboot', '*')
],
}
for folder, patterns in rules.items():
if not folder or not os.path.exists(folder):
continue
basename = os.path.basename(folder)
target = os.path.join(temp_dir, basename)
os.symlink(folder, target)
for pattern in patterns:
cmd.extend(['--include', os.path.join(basename, pattern)])
# Everything in /usr/local/factory/bundle gets overlaid into the
# bundle.
bundle_src_dir = os.path.join(buildroot, 'chroot', 'build', board, 'usr',
'local', 'factory', 'bundle')
if os.path.exists(bundle_src_dir):
for f in os.listdir(bundle_src_dir):
src_path = os.path.join(bundle_src_dir, f)
os.symlink(src_path, os.path.join(temp_dir, f))
cmd.extend([
'--include', f if os.path.isfile(src_path) else os.path.join(f, '*')
])
# Add a version file in the zip file.
if version is not None:
version_file = os.path.join(temp_dir, 'BUILD_VERSION')
osutils.WriteFile(version_file, version)
cmd.extend(['--include', version_file])
cros_build_lib.run(cmd, cwd=temp_dir, capture_output=True)
osutils.RmDir(temp_dir)
return filename
def ArchiveHWQual(buildroot, hwqual_name, archive_dir, image_dir):
"""Create a hwqual tarball in archive_dir.
Args:
buildroot: Root directory where build occurs.
hwqual_name: Name for tarball.
archive_dir: Local directory for hwqual tarball.
image_dir: Directory containing test image.
"""
script_dir = os.path.join(buildroot, 'src', 'platform', 'crostestutils')
ssh_private_key = os.path.join(image_dir, constants.TEST_KEY_PRIVATE)
cmd = [
os.path.join(script_dir, 'archive_hwqual'), '--from', archive_dir,
'--image_dir', image_dir, '--ssh_private_key', ssh_private_key,
'--output_tag', hwqual_name
]
cros_build_lib.run(cmd, capture_output=True)
return '%s.tar.bz2' % hwqual_name
def CreateTestRoot(build_root):
"""Returns a temporary directory for test results in chroot.
Returns:
The path inside the chroot rather than whole path.
"""
# Create test directory within tmp in chroot.
chroot = os.path.join(build_root, 'chroot')
chroot_tmp = os.path.join(chroot, 'tmp')
test_root = tempfile.mkdtemp(prefix='cbuildbot', dir=chroot_tmp)
# Path inside chroot.
return os.path.sep + os.path.relpath(test_root, start=chroot)
def GenerateQuickProvisionPayloads(target_image_path, archive_dir):
"""Generates payloads needed for quick_provision script.
Args:
target_image_path: The path to the image to extract the partitions.
archive_dir: Where to store partitions when generated.
"""
# TODO(saklein): Remove this function entirely in favor of a combined call to
# the endpoint with GeneratePayloads when its arguments have been added to
# the API.
artifacts_service.GenerateQuickProvisionPayloads(target_image_path,
archive_dir)
def GeneratePayloads(target_image_path,
archive_dir,
full=False,
delta=False,
stateful=False):
"""Generates the payloads for hw testing.
Args:
target_image_path: The path to the image to generate payloads to.
archive_dir: Where to store payloads we generated.
full: Generate full payloads.
delta: Generate delta payloads.
stateful: Generate stateful payload.
"""
# TODO(saklein): Change to a combined call to the endpoint with
# GenerateQuickProvisionPayloads after the payload type arguments have been
# added.
artifacts_service.GenerateTestPayloads(target_image_path, archive_dir,
full=full, delta=delta,
stateful=stateful)
def GetChromeLKGM(revision):
"""Returns the ChromeOS LKGM from Chrome given the git revision."""
revision = revision or 'refs/heads/master'
lkgm_url_path = '%s/+/%s/%s?format=text' % (
constants.CHROMIUM_SRC_PROJECT, revision, constants.PATH_TO_CHROME_LKGM)
contents_b64 = gob_util.FetchUrl(config_lib.GetSiteParams().EXTERNAL_GOB_HOST,
lkgm_url_path)
# TODO(crbug.com/997354): for Python 3 support, it probably makes
# sense to return a string here, not bytes.
return base64.b64decode(contents_b64).strip()
def SyncChrome(build_root,
chrome_root,
useflags,
tag=None,
revision=None,
git_cache_dir=None):
"""Sync chrome.
Args:
build_root: The root of the chromium os checkout.
chrome_root: The directory where chrome is stored.
useflags: Array of use flags.
tag: If supplied, the Chrome tag to sync.
revision: If supplied, the Chrome revision to sync.
git_cache_dir: Directory to use for git-cache.
"""
sync_chrome = os.path.join(build_root, 'chromite', 'bin', 'sync_chrome')
internal = constants.USE_CHROME_INTERNAL in useflags
# --reset tells sync_chrome to blow away local changes and to feel
# free to delete any directories that get in the way of syncing. This
# is needed for unattended operation.
# --ignore-locks tells sync_chrome to ignore git-cache locks.
cmd = [sync_chrome, '--reset', '--ignore_locks']
if internal:
cmd += ['--internal']
if tag:
cmd += ['--tag', tag]
if revision:
cmd += ['--revision', revision]
if git_cache_dir:
cmd += ['--git_cache_dir', git_cache_dir]
cmd += [chrome_root]
retry_util.RunCommandWithRetries(constants.SYNC_RETRIES, cmd, cwd=build_root)
class ChromeSDK(object):
"""Wrapper for the 'cros chrome-sdk' command."""
DEFAULT_GOMA_JOBS = '80'
def __init__(self,
cwd,
board,
extra_args=None,
chrome_src=None,
goma=False,
debug_log=True,
cache_dir=None,
target_tc=None,
toolchain_url=None):
"""Initialization.
Args:
cwd: Where to invoke 'cros chrome-sdk'.
board: The board to run chrome-sdk for.
extra_args: Extra args to pass in on the command line.
chrome_src: Path to pass in with --chrome-src.
goma: If True, run using goma.
debug_log: If set, run with debug log-level.
cache_dir: Specify non-default cache directory.
target_tc: Override target toolchain.
toolchain_url: Override toolchain url pattern.
"""
self.cwd = cwd
self.board = board
self.extra_args = extra_args or []
if chrome_src:
self.extra_args += ['--chrome-src', chrome_src]
self.goma = goma
if not self.goma:
self.extra_args.append('--nogoma')
self.debug_log = debug_log
self.cache_dir = cache_dir
self.target_tc = target_tc
self.toolchain_url = toolchain_url
def Run(self, cmd, extra_args=None, run_args=None):
"""Run a command inside the chrome-sdk context.
Args:
cmd: Command (list) to run inside 'cros chrome-sdk'.
extra_args: Extra arguments for 'cros chorme-sdk'.
run_args: If set (dict), pass to run as kwargs.
Returns:
A CommandResult object.
"""
if run_args is None:
run_args = {}
cros_cmd = ['cros']
if self.debug_log:
cros_cmd += ['--log-level', 'debug']
if self.cache_dir:
cros_cmd += ['--cache-dir', self.cache_dir]
if self.target_tc:
self.extra_args += ['--target-tc', self.target_tc]
if self.toolchain_url:
self.extra_args += ['--toolchain-url', self.toolchain_url]
cros_cmd += ['chrome-sdk', '--board', self.board] + self.extra_args
cros_cmd += (extra_args or []) + ['--'] + cmd
return cros_build_lib.run(cros_cmd, cwd=self.cwd, **run_args)
def Ninja(self, debug=False, run_args=None):
"""Run 'ninja' inside a chrome-sdk context.
Args:
debug: Whether to do a Debug build (defaults to Release).
run_args: If set (dict), pass to run as kwargs.
Returns:
A CommandResult object.
"""
return self.Run(self.GetNinjaCommand(debug=debug), run_args=run_args)
def GetNinjaCommand(self, debug=False):
"""Returns a command line to run "ninja".
Args:
debug: Whether to do a Debug build (defaults to Release).
Returns:
Command line to run "ninja".
"""
cmd = ['autoninja']
if self.goma:
cmd += ['-j', self.DEFAULT_GOMA_JOBS]
cmd += ['-C', self._GetOutDirectory(debug=debug), 'chromiumos_preflight']
return cmd
def VMTest(self, image_path, debug=False):
"""Run cros_run_test in a VM.
Only run tests for boards where we build a VM.
Args:
image_path: VM image path.
debug: True if this is a debug build.
Returns:
A CommandResult object.
"""
return self.Run([
'cros_run_test',
'--copy-on-write',
'--deploy',
'--board=%s' % self.board,
'--image-path=%s' % image_path,
'--build-dir=%s' % self._GetOutDirectory(debug=debug),
])
def _GetOutDirectory(self, debug=False):
"""Returns the path to the output directory.
Args:
debug: Whether to do a Debug build (defaults to Release).
Returns:
Path to the output directory.
"""
flavor = 'Debug' if debug else 'Release'
return 'out_%s/%s' % (self.board, flavor)
def GetNinjaLogPath(self, debug=False):
"""Returns the path to the .ninja_log file.
Args:
debug: Whether to do a Debug build (defaults to Release).
Returns:
Path to the .ninja_log file.
"""
return os.path.join(self._GetOutDirectory(debug=debug), '.ninja_log')
def GenerateAFDOArtifacts(buildroot, chrome_root, board, output_path, target):
"""Command to generate AFDO artifacts.
This is only a wrapper of the build API. It doesn't validate the inputs.
Args:
buildroot: The path to build root.
chrome_root: The path to Chrome root.
board: Name of the board.
output_path: The path to save output.
target: A valid toolchain_pb2.AFDOArtifactType.
Returns:
List of artifact names.
"""
input_proto = {
'chroot': {
'path': os.path.join(buildroot, 'chroot'),
'chrome_dir': chrome_root,
},
'build_target': {
'name': board,
},
'output_dir': output_path,
'artifact_type': target
}
output = CallBuildApiWithInputProto(
buildroot,
'chromite.api.ArtifactsService/BundleAFDOGenerationArtifacts',
input_proto
)
return [artifact['path'] for artifact in output['artifacts']]
def VerifyAFDOArtifacts(buildroot, board, target, build_api):
"""Command to verify AFDO artifacts.
This is only a wrapper of the build API. It doesn't validate the inputs.
Args:
buildroot: The path to build root.
board: Name of the board.
target: A valid toolchain_pb2.AFDOArtifactType.
build_api: Full path of the build API. Only applies to APIs that returns
a single field containing the status.
Returns:
True of False: The status of the build API.
"""
input_proto = {
'build_target': {
'name': board,
},
'artifact_type': target
}
output = CallBuildApiWithInputProto(buildroot, build_api, input_proto)
return output['status']
class ApiMismatchError(Exception):
"""Raised by GetTargetChromiteApiVersion."""
class NoChromiteError(Exception):
"""Raised when an expected chromite installation was missing."""
def GetTargetChromiteApiVersion(buildroot, validate_version=True):
"""Get the re-exec API version of the target chromite.
Args:
buildroot: The directory containing the chromite to check.
validate_version: If set to true, checks the target chromite for
compatibility, and raises an ApiMismatchError when there is an
incompatibility.
Returns:
The version number in (major, minor) tuple.
Raises:
May raise an ApiMismatchError if validate_version is set.
"""
try:
api = cros_build_lib.run(
[constants.PATH_TO_CBUILDBOT, '--reexec-api-version'],
cwd=buildroot, error_code_ok=True, capture_output=True)
except cros_build_lib.RunCommandError:
# Although error_code_ok=True was used, this exception will still be raised
# if the executible did not exist.
full_cbuildbot_path = os.path.join(buildroot, constants.PATH_TO_CBUILDBOT)
if not os.path.exists(full_cbuildbot_path):
raise NoChromiteError('No cbuildbot found in buildroot %s, expected to '
'find %s. ' % (buildroot, full_cbuildbot_path))
raise
# If the command failed, then we're targeting a cbuildbot that lacks the
# option; assume 0:0 (ie, initial state).
major = minor = 0
if api.returncode == 0:
major, minor = (int(x) for x in api.output.strip().split('.', 1))
if validate_version and major != constants.REEXEC_API_MAJOR:
raise ApiMismatchError(
'The targeted version of chromite in buildroot %s requires '
'api version %i, but we are api version %i. We cannot proceed.'
% (buildroot, major, constants.REEXEC_API_MAJOR))
return major, minor