blob: 62fa84d2d39beedb8ae6567493c1893234a28710 [file] [log] [blame]
# Copyright 2015 The ChromiumOS Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Configuration options for various cbuildbot builders."""
import copy
import itertools
import json
import os
from chromite.lib import constants
from chromite.lib import osutils
from chromite.utils import memoize
from chromite.utils import pformat
GS_PATH_DEFAULT = "default" # Means gs://chromeos-image-archive/ + bot_id
# Contains the valid build config suffixes.
CONFIG_TYPE_RELEASE = "release"
CONFIG_TYPE_FULL = "full"
CONFIG_TYPE_FACTORY = "factory"
CONFIG_TYPE_PUBLIC = "public"
# DISPLAY labels are used to group related builds together in the GE UI.
DISPLAY_LABEL_TRYJOB = "tryjob"
DISPLAY_LABEL_INCREMENATAL = "incremental"
DISPLAY_LABEL_FULL = "full"
DISPLAY_LABEL_CHROME_INFORMATIONAL = "chrome_informational"
DISPLAY_LABEL_INFORMATIONAL = "informational"
DISPLAY_LABEL_RELEASE = "release"
DISPLAY_LABEL_CHROME_PFQ = "chrome_pfq"
DISPLAY_LABEL_MST_ANDROID_PFQ = "mst_android_pfq"
DISPLAY_LABEL_PI_ANDROID_PFQ = "pi_android_pfq"
DISPLAY_LABEL_QT_ANDROID_PFQ = "qt_android_pfq"
DISPLAY_LABEL_RVC_ANDROID_PFQ = "rvc_android_pfq"
DISPLAY_LABEL_VMRVC_ANDROID_PFQ = "vmrvc_android_pfq"
DISPLAY_LABEL_VMSC_ANDROID_PFQ = "vmsc_android_pfq"
DISPLAY_LABEL_VMT_ANDROID_PFQ = "vmt_android_pfq"
DISPLAY_LABEL_FACTORY = "factory"
DISPLAY_LABEL_TOOLCHAIN = "toolchain"
DISPLAY_LABEL_UTILITY = "utility"
DISPLAY_LABEL_PRODUCTION_TRYJOB = "production_tryjob"
# This list of constants should be kept in sync with GoldenEye code.
ALL_DISPLAY_LABEL = {
DISPLAY_LABEL_TRYJOB,
DISPLAY_LABEL_INCREMENATAL,
DISPLAY_LABEL_FULL,
DISPLAY_LABEL_CHROME_INFORMATIONAL,
DISPLAY_LABEL_INFORMATIONAL,
DISPLAY_LABEL_RELEASE,
DISPLAY_LABEL_CHROME_PFQ,
DISPLAY_LABEL_MST_ANDROID_PFQ,
DISPLAY_LABEL_PI_ANDROID_PFQ,
DISPLAY_LABEL_QT_ANDROID_PFQ,
DISPLAY_LABEL_RVC_ANDROID_PFQ,
DISPLAY_LABEL_VMRVC_ANDROID_PFQ,
DISPLAY_LABEL_VMSC_ANDROID_PFQ,
DISPLAY_LABEL_VMT_ANDROID_PFQ,
DISPLAY_LABEL_FACTORY,
DISPLAY_LABEL_TOOLCHAIN,
DISPLAY_LABEL_UTILITY,
DISPLAY_LABEL_PRODUCTION_TRYJOB,
}
# These values must be kept in sync with the ChromeOS LUCI builders.
#
# https://chrome-internal.googlesource.com/chromeos/infra/config/+/HEAD/luci/cr-buildbucket.cfg
LUCI_BUILDER_FACTORY = "Factory"
LUCI_BUILDER_FULL = "Full"
LUCI_BUILDER_INCREMENTAL = "Incremental"
LUCI_BUILDER_INFORMATIONAL = "Informational"
LUCI_BUILDER_INFRA = "Infra"
LUCI_BUILDER_LEGACY_RELEASE = "LegacyRelease"
LUCI_BUILDER_PFQ = "PFQ"
LUCI_BUILDER_RAPID = "Rapid"
LUCI_BUILDER_RELEASE = "Release"
LUCI_BUILDER_STAGING = "Staging"
LUCI_BUILDER_TRY = "Try"
ALL_LUCI_BUILDER = {
LUCI_BUILDER_FACTORY,
LUCI_BUILDER_FULL,
LUCI_BUILDER_INCREMENTAL,
LUCI_BUILDER_INFORMATIONAL,
LUCI_BUILDER_INFRA,
LUCI_BUILDER_LEGACY_RELEASE,
LUCI_BUILDER_PFQ,
LUCI_BUILDER_RAPID,
LUCI_BUILDER_RELEASE,
LUCI_BUILDER_STAGING,
LUCI_BUILDER_TRY,
}
GOLDENEYE_IGNORED_BOARDS = [
"capri",
"capri-zfpga",
"cobblepot",
"gonzo",
"lakitu",
"lasilla-ground",
"lasilla-sky",
"macchiato-ground",
"octavius",
"romer",
"wooten",
]
def isTryjobConfig(build_config):
"""Is a given build config a tryjob config, or a production config?
Args:
build_config: A fully populated instance of BuildConfig.
Returns:
Boolean. True if it's a tryjob config.
"""
return build_config.luci_builder in [LUCI_BUILDER_RAPID, LUCI_BUILDER_TRY]
# In the Json, this special build config holds the default values for all
# other configs.
DEFAULT_BUILD_CONFIG = "_default"
# Constants for config template file
CONFIG_TEMPLATE_BOARDS = "boards"
CONFIG_TEMPLATE_NAME = "name"
CONFIG_TEMPLATE_EXPERIMENTAL = "experimental"
CONFIG_TEMPLATE_BUILDER = "builder"
CONFIG_TEMPLATE_RELEASE = "RELEASE"
CONFIG_TEMPLATE_CONFIGS = "configs"
CONFIG_TEMPLATE_ARCH = "arch"
CONFIG_TEMPLATE_REFERENCE_BOARD_NAME = "reference_board_name"
CONFIG_X86_INTERNAL = "X86_INTERNAL"
CONFIG_X86_EXTERNAL = "X86_EXTERNAL"
CONFIG_ARM_INTERNAL = "ARM_INTERNAL"
CONFIG_ARM_EXTERNAL = "ARM_EXTERNAL"
def IsCanaryMaster(builder_run):
"""Returns True if this build type is master-release"""
return (
builder_run.config.build_type == constants.CANARY_TYPE
and builder_run.config.master
and builder_run.manifest_branch in ("main", "master")
)
def IsPFQType(b_type):
"""Returns True if this build type is a PFQ."""
return b_type in (constants.PFQ_TYPE,)
def IsCanaryType(b_type):
"""Returns True if this build type is a Canary."""
return b_type == constants.CANARY_TYPE
class AttrDict(dict):
"""Dictionary with 'attribute' access.
This is identical to a dictionary, except that string keys can be addressed
as read-only attributes.
"""
def __getattr__(self, name: str):
"""Support attribute-like access to each dict entry."""
if name in self:
return self[name]
# Super class (dict) has no __getattr__ method, so use __getattribute__.
return super().__getattribute__(name)
class BuildConfig(AttrDict):
"""Dictionary of explicit configuration settings for a cbuildbot config
Each dictionary entry is in turn a dictionary of config_param->value.
See DefaultSettings for details on known configurations, and their
documentation.
"""
def deepcopy(self):
"""Create a deep copy of this object.
This is a specialized version of copy.deepcopy() for BuildConfig
objects. It speeds up deep copies by 10x because we know in advance what
is stored inside a BuildConfig object and don't have to do as much
introspection. This function is called a lot during setup of the config
objects so optimizing it makes a big difference. (It saves seconds off
the load time of this module!)
"""
result = BuildConfig(self)
# Here is where we handle all values that need deepcopy instead of
# shallow.
for k, v in result.items():
if v is not None:
# type(v) is faster than isinstance.
if type(v) is list: # pylint: disable=unidiomatic-typecheck
result[k] = v[:]
return result
def apply(self, *args, **kwargs):
"""Apply changes to this BuildConfig.
Note: If an override is callable, it will be called and passed the prior
value for the given key (or None) to compute the new value.
Args:
*args: Dictionaries or templates to update this config with.
**kwargs: Settings to inject; see DefaultSettings for valid values.
Returns:
self after changes are applied.
"""
inherits = list(args)
inherits.append(kwargs)
for update_config in inherits:
for name, value in update_config.items():
if callable(value):
# If we are applying to a fixed value, we resolve to a fixed
# value. Otherwise, we save off a callable to apply later,
# perhaps with nested callables (IE: we curry them). This
# allows us to use callables in templates, and apply
# templates to each other and still get the expected result
# when we use them later on.
#
# Delaying the resolution of callables is safe, because
# "Add()" always applies against the default, which has
# fixed values for everything.
if name in self:
# apply it to the current value.
if callable(self[name]):
# If we have no fixed value to resolve with, stack
# the callables.
def stack(new_callable, old_callable):
"""Helper to isolate namespace for closure."""
return lambda fixed: new_callable(
old_callable(fixed)
)
self[name] = stack(value, self[name])
else:
# If the current value was a fixed value, apply the
# callable.
self[name] = value(self[name])
else:
# If we had no value to apply it to, save it for later.
self[name] = value
elif name == "_template":
# We never apply _template. You have to set it through Add.
pass
else:
# Simple values overwrite whatever we do or don't have.
self[name] = value
return self
def derive(self, *args, **kwargs):
"""Create a new config derived from this one.
Note: If an override is callable, it will be called and passed the prior
value for the given key (or None) to compute the new value.
Args:
*args: Mapping instances to mixin.
**kwargs: Settings to inject; see DefaultSettings for valid values.
Returns:
A new _config instance.
"""
return self.deepcopy().apply(*args, **kwargs)
def AddSlave(self, slave) -> None:
"""Assign slave config(s) to a build master.
A helper for adding slave configs to a master config.
"""
assert self.master
if self["slave_configs"] is None:
self["slave_configs"] = []
self.slave_configs.append(slave.name)
self.slave_configs.sort()
def AddSlaves(self, slaves) -> None:
"""Assign slave config(s) to a build master.
A helper for adding slave configs to a master config.
"""
assert self.master
if self["slave_configs"] is None:
self["slave_configs"] = []
self.slave_configs.extend(slave_config.name for slave_config in slaves)
self.slave_configs.sort()
def DefaultSettings():
# Enumeration of valid settings; any/all config settings must be in this.
# All settings must be documented.
return dict(
# The name of the template we inherit settings from.
_template=None,
# The name of the config.
name=None,
# A list of boards to build.
boards=None,
# This value defines what part of the Golden Eye UI is responsible for
# displaying builds of this build config. The value is required, and
# must be in ALL_DISPLAY_LABEL.
# TODO: Make the value required after crbug.com/776955 is finished.
display_label=None,
# This defines which LUCI Builder to use. It must match an entry in:
#
# https://chrome-internal.git.corp.google.com/chromeos/
# manifest-internal/+/infra/config/cr-buildbucket.cfg
#
luci_builder=LUCI_BUILDER_LEGACY_RELEASE,
# The profile of the variant to set up and build.
profile=None,
# This bot pushes changes to the overlays.
master=False,
# If this bot triggers slave builds, this will contain a list of
# slave config names.
slave_configs=None,
# If False, this flag indicates that the CQ should not check whether
# this bot passed or failed. Set this to False if you are setting up a
# new bot. Once the bot is on the waterfall and is consistently green,
# mark the builder as important=True.
important=True,
# If True, build config should always be run as if --debug was set
# on the cbuildbot command line. This is different from 'important'
# and is usually correlated with tryjob build configs.
debug=False,
# If True, use the debug instance of CIDB instead of prod.
debug_cidb=False,
# Timeout for the build as a whole (in seconds).
build_timeout=(5 * 60 + 30) * 60,
# Whether this is an internal build config.
internal=False,
# Whether this is a branched build config. Used for pfq logic.
branch=False,
# The name of the manifest to use. E.g., to use the buildtools manifest,
# specify 'buildtools'.
manifest=constants.DEFAULT_MANIFEST,
# emerge use flags to use while setting up the board, building packages,
# making images, etc.
useflags=[],
# Set the variable CHROMEOS_OFFICIAL for the build. Known to affect
# parallel_emerge, cros_set_lsb_release, and chromeos_version.sh. See
# bug chromium-os:14649
chromeos_official=False,
# Use binary packages for build_packages and setup_board.
usepkg_build_packages=True,
# Does this profile need to sync chrome? If None, we guess based on
# other factors. If True/False, we always do that.
sync_chrome=None,
# Use the newest ebuilds for all the toolchain packages.
latest_toolchain=False,
# Wipe and replace the board inside the chroot.
board_replace=False,
# Wipe and replace chroot, but not source.
chroot_replace=True,
# Uprevs the local ebuilds to build new changes since last stable.
# build. If master then also pushes these changes on success. Note that
# we uprev on just about every bot config because it gives us a more
# deterministic build system (the tradeoff being that some bots build
# from source more frequently than if they never did an uprev). This way
# the release/factory/etc... builders will pick up changes that devs
# pushed before it runs, but after the correspoding PFQ bot ran (which
# is what creates+uploads binpkgs). The incremental bots are about the
# only ones that don't uprev because they mimic the flow a developer
# goes through on their own local systems.
uprev=True,
# Select what overlays to look at for revving and prebuilts. This can be
# any constants.VALID_OVERLAYS.
overlays=constants.PUBLIC_OVERLAYS,
# Select what overlays to push at. This should be a subset of overlays
# for the particular builder. Must be None if not a master. There
# should only be one master bot pushing changes to each overlay per
# branch.
push_overlays=None,
# Uprev Chrome, values of 'tot', 'stable_release', or None.
chrome_rev=None,
# Runs unittests for packages.
unittests=True,
# If true, uploads artifacts for hw testing. Upload payloads for test
# image if the image is built. If not, dev image is used and then base
# image.
upload_hw_test_artifacts=True,
# If true, uploads individual image tarballs.
upload_standalone_images=True,
# Whether to run BuildConfigsExport stage. This stage generates build
# configs (see crbug.com/974795 project). Only release builders should
# run this stage.
run_build_configs_export=False,
# List of patterns for portage packages for which stripped binpackages
# should be uploaded to GS. The patterns are used to search for packages
# via `equery list`.
upload_stripped_packages=[
# Used by SimpleChrome workflow.
"chromeos-base/chromeos-chrome",
"sys-kernel/*kernel*",
],
# Google Storage path to offload files to.
# None - No upload
# GS_PATH_DEFAULT - 'gs://chromeos-image-archive/' + bot_id
# value - Upload to explicit path
gs_path=GS_PATH_DEFAULT,
# TODO(sosa): Deprecate binary.
# Type of builder. Check constants.VALID_BUILD_TYPES.
build_type=constants.PFQ_TYPE,
# Whether to schedule test suites by suite_scheduler. Generally only
# True for "release" builders.
suite_scheduling=False,
# The class name used to build this config. See the modules in
# cbuildbot / builders/*_builders.py for possible values. This should
# be the name in string form -- e.g. "simple_builders.SimpleBuilder" to
# get the SimpleBuilder class in the simple_builders module. If not
# specified, we'll fallback to legacy probing behavior until everyone
# has been converted (see the scripts/cbuildbot.py file for details).
builder_class_name=None,
# List of images we want to build -- see `cros build-image --help`.
images=["test"],
# Whether to build a netboot image.
factory_install_netboot=True,
# Whether to build the factory toolkit.
factory_toolkit=True,
# Whether to build factory packages in BuildPackages.
factory=True,
# Flag to control if all packages for the target are built. If disabled
# and unittests are enabled, the unit tests and their dependencies
# will still be built during the testing stage.
build_packages=True,
# Tuple of specific packages we want to build. Most configs won't
# specify anything here and instead let build_packages calculate.
packages=[],
# Do we push a final release image to chromeos-images.
push_image=False,
# Do we upload debug symbols.
upload_symbols=False,
# Run a stage that generates and uploads debug symbols.
debug_symbols=True,
# Include *.debug files for debugging core files with gdb in debug.tgz.
# These are very large. This option only has an effect if debug_symbols
# and archive are set.
archive_build_debug=False,
# Run a stage that archives build and test artifacts for developer
# consumption.
archive=True,
# Git repository URL for our manifests.
# https://chromium.googlesource.com/chromiumos/manifest
# https://chrome-internal.googlesource.com/chromeos/manifest-internal
manifest_repo_url=None,
# Whether we are using the manifest_version repo that stores per-build
# manifests.
manifest_version=False,
# Use a different branch of the project manifest for the build.
manifest_branch=None,
# Upload prebuilts for this build. Valid values are PUBLIC, PRIVATE, or
# False.
prebuilts=False,
# Use SDK as opposed to building the chroot from source.
use_sdk=True,
# The description string to print out for config when user runs --list.
description=None,
# Boolean that enables parameter --git-sync for upload_prebuilts.
git_sync=False,
# Whether this config belongs to a config group.
grouped=False,
# If enabled, run the PatchChanges stage. Enabled by default. Can be
# overridden by the --nopatch flag.
postsync_patch=True,
# Reexec into the buildroot after syncing. Enabled by default.
postsync_reexec=True,
# If specified, it is passed on to the PushImage script as
# '--sign-types' commandline argument. Must be either None or a list of
# image types.
sign_types=None,
# TODO(sosa): Collapse to one option.
# ========== Dev installer prebuilts options =======================
# Upload prebuilts for this build to this bucket. If it equals None the
# default buckets are used.
binhost_bucket=None,
# Parameter --key for upload_prebuilts. If it equals None, the default
# values are used, which depend on the build type.
binhost_key=None,
# Parameter --binhost-base-url for upload_prebuilts. If it equals None,
# the default value is used.
binhost_base_url=None,
# Enable rootfs verification on the image.
rootfs_verification=True,
# ==================================================================
# Workspace related options.
# Which branch should WorkspaceSyncStage checkout, if run.
workspace_branch=None,
# ==================================================================
# The documentation associated with the config.
doc=None,
# This is a LUCI Scheduler schedule string. Setting this will create
# a LUCI Scheduler for this build on swarming (not buildbot).
# See: https://goo.gl/VxSzFf
schedule=None,
# This is the list of git repos which can trigger this build in
# swarming. Implies that schedule is set, to "triggered".
# The format is of the form:
# [ (<git repo url>, (<ref1>, <ref2>, …)),
# …]
triggered_gitiles=None,
# If true, skip package retries in BuildPackages step.
nobuildretry=False,
)
def GerritInstanceParameters(name, instance):
param_names = [
"_GOB_INSTANCE",
"_GERRIT_INSTANCE",
"_GOB_HOST",
"_GERRIT_HOST",
"_GOB_URL",
"_GERRIT_URL",
]
gob_instance = instance
gerrit_instance = "%s-review" % instance
gob_host = constants.GOB_HOST % gob_instance
gerrit_host = constants.GOB_HOST % gerrit_instance
gob_url = "https://%s" % gob_host
gerrit_url = "https://%s" % gerrit_host
params = [
gob_instance,
gerrit_instance,
gob_host,
gerrit_host,
gob_url,
gerrit_url,
]
return {f"{name}{pn}": p for pn, p in zip(param_names, params)}
def DefaultSiteParameters():
# Enumeration of valid site parameters; any/all site parameters must be
# here. All site parameters should be documented.
default_site_params = {}
manifest_project = "chromiumos/manifest"
manifest_int_project = "chromeos/manifest-internal"
external_remote = "cros"
internal_remote = "cros-internal"
chromium_remote = "chromium"
chrome_remote = "chrome"
aosp_remote = "aosp"
weave_remote = "weave"
internal_change_prefix = "chrome-internal:"
external_change_prefix = "chromium:"
# Gerrit instance site parameters.
default_site_params.update(GerritInstanceParameters("EXTERNAL", "chromium"))
default_site_params.update(
GerritInstanceParameters("INTERNAL", "chrome-internal")
)
default_site_params.update(GerritInstanceParameters("AOSP", "android"))
default_site_params.update(GerritInstanceParameters("WEAVE", "weave"))
default_site_params.update(
# Parameters to define which manifests to use.
MANIFEST_PROJECT=manifest_project,
MANIFEST_INT_PROJECT=manifest_int_project,
MANIFEST_PROJECTS=(manifest_project, manifest_int_project),
MANIFEST_URL=os.path.join(
default_site_params["EXTERNAL_GOB_URL"], manifest_project
),
MANIFEST_INT_URL=os.path.join(
default_site_params["INTERNAL_GERRIT_URL"], manifest_int_project
),
# CrOS remotes specified in the manifests.
EXTERNAL_REMOTE=external_remote,
INTERNAL_REMOTE=internal_remote,
GOB_REMOTES={
default_site_params["EXTERNAL_GOB_INSTANCE"]: external_remote,
default_site_params["INTERNAL_GOB_INSTANCE"]: internal_remote,
},
CHROMIUM_REMOTE=chromium_remote,
CHROME_REMOTE=chrome_remote,
AOSP_REMOTE=aosp_remote,
WEAVE_REMOTE=weave_remote,
# Only remotes listed in CROS_REMOTES are considered branchable.
# CROS_REMOTES and BRANCHABLE_PROJECTS must be kept in sync.
GERRIT_HOSTS={
external_remote: default_site_params["EXTERNAL_GERRIT_HOST"],
internal_remote: default_site_params["INTERNAL_GERRIT_HOST"],
aosp_remote: default_site_params["AOSP_GERRIT_HOST"],
weave_remote: default_site_params["WEAVE_GERRIT_HOST"],
},
CROS_REMOTES={
external_remote: default_site_params["EXTERNAL_GOB_URL"],
internal_remote: default_site_params["INTERNAL_GOB_URL"],
aosp_remote: default_site_params["AOSP_GOB_URL"],
weave_remote: default_site_params["WEAVE_GOB_URL"],
},
GIT_REMOTES={
chromium_remote: default_site_params["EXTERNAL_GOB_URL"],
chrome_remote: default_site_params["INTERNAL_GOB_URL"],
external_remote: default_site_params["EXTERNAL_GOB_URL"],
internal_remote: default_site_params["INTERNAL_GOB_URL"],
aosp_remote: default_site_params["AOSP_GOB_URL"],
weave_remote: default_site_params["WEAVE_GOB_URL"],
},
# Prefix to distinguish internal and external changes. This is used
# when a user specifies a patch with "-g", when generating a key for
# a patch to use in our PatchCache, and when displaying a custom
# string for the patch.
INTERNAL_CHANGE_PREFIX=internal_change_prefix,
EXTERNAL_CHANGE_PREFIX=external_change_prefix,
CHANGE_PREFIX={
external_remote: external_change_prefix,
internal_remote: internal_change_prefix,
},
# List of remotes that are okay to include in the external manifest.
EXTERNAL_REMOTES=(
external_remote,
chromium_remote,
aosp_remote,
weave_remote,
),
# Mapping 'remote name' -> regexp that matches names of repositories on
# that remote that can be branched when creating CrOS branch. Branching
# script will actually create a new git ref when branching these
# projects. It won't attempt to create a git ref for other projects that
# may be mentioned in a manifest. If a remote is missing from this
# dictionary, all projects on that remote are considered to not be
# branchable.
BRANCHABLE_PROJECTS={
external_remote: r"(chromiumos|aosp)/(.+)",
internal_remote: r"chromeos/(.+)",
},
# Additional parameters used to filter manifests, create modified
# manifests, and to branch manifests.
MANIFEST_VERSIONS_GOB_URL=(
"%s/chromiumos/manifest-versions"
% default_site_params["EXTERNAL_GOB_URL"]
),
MANIFEST_VERSIONS_INT_GOB_URL=(
"%s/chromeos/manifest-versions"
% default_site_params["INTERNAL_GOB_URL"]
),
MANIFEST_VERSIONS_GS_URL="gs://chromeos-manifest-versions",
# Standard directories under buildroot for cloning these repos.
EXTERNAL_MANIFEST_VERSIONS_PATH="manifest-versions",
INTERNAL_MANIFEST_VERSIONS_PATH="manifest-versions-internal",
# GS URL in which to archive build artifacts.
ARCHIVE_URL="gs://chromeos-image-archive",
)
return default_site_params
class SiteConfig(dict):
"""This holds a set of named BuildConfig values."""
def __init__(self, defaults=None, templates=None) -> None:
"""Init.
Args:
defaults: Dictionary of key value pairs to use as BuildConfig
values. All BuildConfig values should be defined here. If None,
the DefaultSettings() is used. Most sites should use
DefaultSettings(), and then update to add any site specific
values needed.
templates: Dictionary of template names to partial BuildConfigs
other BuildConfigs can be based on. Mostly used to reduce
verbosity of the config dump file format.
"""
super().__init__()
self._defaults = DefaultSettings()
if defaults:
self._defaults.update(defaults)
self._templates = (
AttrDict() if templates is None else AttrDict(templates)
)
def GetDefault(self):
"""Create the canonical default build configuration."""
# Enumeration of valid settings; any/all config settings must be in
# this. All settings must be documented.
return BuildConfig(**self._defaults)
def GetTemplates(self):
"""Get the templates of the build configs"""
return self._templates
@property
def templates(self):
return self._templates
#
# Methods for searching a SiteConfig's contents.
#
def GetBoards(self):
"""Return an iterable of all boards in the SiteConfig."""
return set(
itertools.chain.from_iterable(
x.boards for x in self.values() if x.boards
)
)
def FindFullConfigsForBoard(self, board=None):
"""Returns full builder configs for a board.
Args:
board: The board to match. By default, match all boards.
Returns:
A tuple containing a list of matching external configs and a list of
matching internal release configs for a board.
"""
ext_cfgs = []
int_cfgs = []
for name, c in self.items():
possible_names = []
if board:
possible_names = [
board + "-" + CONFIG_TYPE_RELEASE,
board + "-" + CONFIG_TYPE_FULL,
]
if c["boards"] and (
board is None or board in c["boards"] or name in possible_names
):
if name.endswith("-%s" % CONFIG_TYPE_RELEASE) and c["internal"]:
int_cfgs.append(c.deepcopy())
elif (
name.endswith("-%s" % CONFIG_TYPE_FULL)
and not c["internal"]
):
ext_cfgs.append(c.deepcopy())
return ext_cfgs, int_cfgs
def FindCanonicalConfigForBoard(self, board, allow_internal=True):
"""Get the canonical cbuildbot builder config for a board."""
ext_cfgs, int_cfgs = self.FindFullConfigsForBoard(board)
# If both external and internal builds exist for this board, prefer the
# internal one unless instructed otherwise.
both = (int_cfgs if allow_internal else []) + ext_cfgs
if not both:
raise ValueError("Invalid board specified: %s." % board)
return both[0]
def GetSlaveConfigMapForMaster(
self, master_config, options=None, important_only=True
):
"""Gets the slave builds triggered by a master config.
If a master builder also performs a build, it can (incorrectly) return
itself.
Args:
master_config: A build config for a master builder.
options: The options passed on the commandline. This argument is
required for normal operation, but we accept None to assist with
testing.
important_only: If True, only get the important slaves.
Returns:
A slave_name to slave_config map, corresponding to the slaves for
the master represented by master_config.
Raises:
AssertionError if the given config is not a master config or it does
not have a manifest_version.
"""
assert master_config.master
assert master_config.slave_configs is not None
slave_name_config_map = {}
if options is not None and options.remote_trybot:
return {}
# Look up the build configs for all slaves named by the master.
slave_name_config_map = {
name: self[name] for name in master_config.slave_configs
}
if important_only:
# Remove unimportant configs from the result.
slave_name_config_map = {
k: v for k, v in slave_name_config_map.items() if v.important
}
return slave_name_config_map
def GetSlavesForMaster(
self, master_config, options=None, important_only=True
):
"""Get a list of qualified build slave configs given the master_config.
Args:
master_config: A build config for a master builder.
options: The options passed on the commandline. This argument is
optional, and only makes sense when called from cbuildbot.
important_only: If True, only get the important slaves.
"""
slave_map = self.GetSlaveConfigMapForMaster(
master_config, options=options, important_only=important_only
)
return list(slave_map.values())
#
# Methods used when creating a Config programatically.
#
def Add(self, name, template=None, *args, **kwargs):
"""Add a new BuildConfig to the SiteConfig.
Examples:
# Creates default build named foo.
site_config.Add('foo')
# Creates default build with board 'foo_board'
site_config.Add('foo', boards=['foo_board'])
# Creates build based on template_build for 'foo_board'.
site_config.Add(
'foo', template_build, boards=['foo_board']
)
# Creates build based on template for 'foo_board'. with mixin.
# Inheritance order is default, template, mixin, arguments.
site_config.Add(
'foo', template_build, mixin_build_config, boards=['foo_board']
)
# Creates build without a template but with mixin.
# Inheritance order is default, template, mixin, arguments.
site_config.Add(
'foo', None, mixin_build_config, boards=['foo_board']
)
Args:
name: The name to label this configuration; this is what cbuildbot
would see.
template: BuildConfig to use as a template for this build.
*args: BuildConfigs to patch into this config. First one (if
present) is considered the template. See AddTemplate for help on
templates.
**kwargs: BuildConfig values to explicitly set on this config.
Returns:
The BuildConfig just added to the SiteConfig.
"""
assert name not in self, "%s already exists." % name
inherits, overrides = args, kwargs
if template:
inherits = (template,) + inherits
# Make sure we don't ignore that argument silently.
if "_template" in overrides:
raise ValueError("_template cannot be explicitly set.")
result = self.GetDefault()
result.apply(*inherits, **overrides)
# Select the template name based on template argument, or nothing.
resolved_template = template.get("_template") if template else None
assert (
not resolved_template or resolved_template in self.templates
), "%s inherits from non-template %s" % (name, resolved_template)
# Our name is passed as an explicit argument. We use the first build
# config as our template, or nothing.
result["name"] = name
result["_template"] = resolved_template
self[name] = result
return result
def AddWithoutTemplate(self, name, *args, **kwargs) -> None:
"""Add config containing only explicitly listed values (no defaults)."""
self.Add(name, None, *args, **kwargs)
def AddForBoards(
self, suffix, boards, per_board=None, template=None, *args, **kwargs
):
"""Create configs for all boards in |boards|.
Args:
suffix: Config name is <board>-<suffix>.
boards: A list of board names as strings.
per_board: A dictionary of board names to BuildConfigs, or None.
template: The template to use for all configs created.
*args: Mixin templates to apply.
**kwargs: Additional keyword arguments to be used in AddConfig.
Returns:
List of the configs created.
"""
result = []
for board in boards:
config_name = "%s-%s" % (board, suffix)
# Insert the per_board value as the last mixin, if it exists.
mixins = args + (dict(boards=[board]),)
if per_board and board in per_board:
mixins = mixins + (per_board[board],)
# Create the new config for this board.
result.append(self.Add(config_name, template, *mixins, **kwargs))
return result
def ApplyForBoards(self, suffix, boards, *args, **kwargs):
"""Update configs for all boards in |boards|.
Args:
suffix: Config name is <board>-<suffix>.
boards: A list of board names as strings.
*args: Mixin templates to apply.
**kwargs: Additional keyword arguments to be used in AddConfig.
Returns:
List of the configs updated.
"""
result = []
for board in boards:
config_name = "%s-%s" % (board, suffix)
assert config_name in self, "%s does not exist." % config_name
# Update the config for this board.
result.append(self[config_name].apply(*args, **kwargs))
return result
def AddTemplate(self, name, *args, **kwargs):
"""Create a template named |name|.
Templates are used to define common settings that are shared across
types of builders. They help reduce duplication in config_dump.json,
because we only define the template and its settings once.
Args:
name: The name of the template.
*args: See the docstring of BuildConfig.derive.
**kwargs: See the docstring of BuildConfig.derive.
"""
assert name not in self._templates, "Template %s already exists." % name
template = BuildConfig()
template.apply(*args, **kwargs)
template["_template"] = name
self._templates[name] = template
return template
def _MarshalBuildConfig(self, name, config):
"""Hide the defaults from a given config entry.
Args:
name: Default build name (usually dictionary key).
config: A config entry.
Returns:
The same config entry, but without any defaults.
"""
defaults = self.GetDefault()
defaults["name"] = name
template = config.get("_template")
if template:
defaults.apply(self._templates[template])
defaults["_template"] = None
result = {}
for k, v in config.items():
if defaults.get(k) != v:
result[k] = v
return result
def _MarshalTemplates(self) -> dict:
"""Return a version of self._templates with only used templates.
Templates have callables/delete keys resolved against GetDefault() to
ensure they can be safely saved to json.
Returns:
Dict copy of self._templates with all unreferenced templates
removed.
"""
defaults = self.GetDefault()
# All templates used. We ignore child configs since they
# should exist at top level.
used = set(c.get("_template", None) for c in self.values())
used.discard(None)
result = {}
for name in used:
# Expand any special values (callables, etc)
expanded = defaults.derive(self._templates[name])
# Recover the '_template' value which is filtered out by derive.
expanded["_template"] = name
# Hide anything that matches the default.
save = {k: v for k, v in expanded.items() if defaults.get(k) != v}
result[name] = save
return result
def SaveConfigToString(self):
"""Save this Config object to a Json format string."""
default = self.GetDefault()
config_dict = {}
config_dict["_default"] = default
config_dict["_templates"] = self._MarshalTemplates()
for k, v in self.items():
config_dict[k] = self._MarshalBuildConfig(k, v)
return PrettyJsonDict(config_dict)
def SaveConfigToFile(self, config_file) -> None:
"""Save this Config to a Json file.
Args:
config_file: The file to write too.
"""
json_string = self.SaveConfigToString()
osutils.WriteFile(config_file, json_string)
def DumpExpandedConfigToString(self):
"""Dump the SiteConfig to Json with all configs full expanded.
This is intended for debugging default/template behavior. The dumped
JSON can't be reloaded (at least not reliably).
"""
return PrettyJsonDict(self)
def DumpConfigCsv(self):
"""Dump the SiteConfig to CSV with all configs fully expanded.
This supports configuration analysis and debugging.
"""
raw_config = json.loads(self.DumpExpandedConfigToString())
header_keys = {"builder_name", "test_type", "device"}
csv_rows = []
for builder_name, values in raw_config.items():
row = {"builder_name": builder_name}
tests = {}
raw_devices = []
for key, value in values.items():
header_keys.add(key)
if value:
if isinstance(value, list):
if "_tests" in key:
tests[key] = value
elif key == "models":
raw_devices = value
else:
row[key] = " | ".join(
str(array_val) for array_val in value
)
else:
row[key] = value
if tests:
for test_type, test_entries in tests.items():
for test_entry in test_entries:
test_row = copy.deepcopy(row)
test_row["test_type"] = test_type
raw_test = json.loads(test_entry)
for test_key, test_value in raw_test.items():
if test_value:
header_keys.add(test_key)
test_row[test_key] = test_value
csv_rows.append(test_row)
if raw_devices:
for raw_device in raw_devices:
device = json.loads(raw_device)
test_suite = test_row.get("suite", "")
test_suites = device.get("test_suites", [])
if (
test_suite
and test_suites
and test_suite in test_suites
):
device_row = copy.deepcopy(test_row)
device_row["device"] = device["name"]
csv_rows.append(device_row)
else:
csv_rows.append(row)
csv_result = [",".join(header_keys)]
for csv_row in csv_rows:
row_values = []
for header_key in header_keys:
row_values.append('"%s"' % str(csv_row.get(header_key, "")))
csv_result.append(",".join(row_values))
return "\n".join(csv_result)
#
# Functions related to working with GE Data.
#
def LoadGEBuildConfigFromFile(
build_settings_file=constants.GE_BUILD_CONFIG_FILE,
):
"""Load template config dict from a Json encoded file."""
json_string = osutils.ReadFile(build_settings_file)
ret = json.loads(json_string)
i = 0
configs = ret["boards"]
while i < len(configs):
if configs[i]["name"] in {
"betty",
"betty-arc-r",
"betty-arc-t",
"betty-arc-u",
"betty-kernelnext",
"betty-pi-arc",
"guado-macrophage",
"novato",
"novato-arc64",
"reven",
"reven-vmtest",
}:
configs.pop(i)
else:
i += 1
i = 0
configs = ret["reference_board_unified_builds"]
while i < len(configs):
if configs[i]["name"] in {
"aurora-borealis",
"fizz-moblab",
"puff-moblab",
}:
configs.pop(i)
else:
i += 1
return ret
def GeBuildConfigAllBoards(ge_build_config):
"""Extract a list of board names from the GE Build Config.
Args:
ge_build_config: Dictionary containing the decoded GE configuration
file.
Returns:
A list of board names as strings.
"""
return [b["name"] for b in ge_build_config["boards"]]
def GetUnifiedBuildConfigAllBuilds(ge_build_config):
"""Extract a list of all unified build configurations.
This dictionary is based on the JSON defined by the proto generated from
GoldenEye. See cs/crosbuilds.proto
Args:
ge_build_config: Dictionary containing the decoded GE configuration
file.
Returns:
A list of unified build configurations (json configs)
"""
return ge_build_config.get("reference_board_unified_builds", [])
def GroupBoardsByBuilder(board_list):
"""Group boards by the 'builder' flag."""
builder_to_boards_dict = {}
for b in board_list:
# Until Lakitu is removed from GE, skip the board
# http://b/180437658
if b["name"] in GOLDENEYE_IGNORED_BOARDS:
continue
# Invalid build configs being written out with no configs array, thus
# the default. See https://crbug.com/1005803.
for config in b.get(CONFIG_TEMPLATE_CONFIGS, []):
builder = config[CONFIG_TEMPLATE_BUILDER]
if builder not in builder_to_boards_dict:
builder_to_boards_dict[builder] = set()
builder_to_boards_dict[builder].add(b[CONFIG_TEMPLATE_NAME])
return builder_to_boards_dict
def GetArchBoardDict(ge_build_config):
"""Get a dict mapping arch types to board names.
Args:
ge_build_config: Dictionary containing the decoded GE configuration
file.
Returns:
A dict mapping arch types to board names.
"""
arch_board_dict = {}
for b in ge_build_config[CONFIG_TEMPLATE_BOARDS]:
board_name = b[CONFIG_TEMPLATE_NAME]
# Invalid build configs being written out with no configs array, thus
# the default. See https://crbug.com/947712.
for config in b.get(CONFIG_TEMPLATE_CONFIGS, []):
arch = config[CONFIG_TEMPLATE_ARCH]
arch_board_dict.setdefault(arch, set()).add(board_name)
for b in GetUnifiedBuildConfigAllBuilds(ge_build_config):
board_name = b[CONFIG_TEMPLATE_REFERENCE_BOARD_NAME]
arch = b[CONFIG_TEMPLATE_ARCH]
arch_board_dict.setdefault(arch, set()).add(board_name)
return arch_board_dict
#
# Functions related to loading/saving Json.
#
class ObjectJSONEncoder(json.JSONEncoder):
"""Json Encoder that encodes objects as their dictionaries."""
# pylint: disable=method-hidden
def default(self, o):
return self.encode(o.__dict__)
def PrettyJsonDict(dictionary):
"""Returns a pretty-ified json dump of a dictionary."""
return pformat.json(dictionary, cls=ObjectJSONEncoder)
def LoadConfigFromFile(config_file=constants.CHROMEOS_CONFIG_FILE):
"""Load a Config a Json encoded file."""
json_string = osutils.ReadFile(config_file)
return LoadConfigFromString(json_string)
def LoadConfigFromString(json_string):
"""Load a cbuildbot config from it's Json encoded string."""
config_dict = json.loads(json_string)
# Use standard defaults, but allow the config to override.
defaults = DefaultSettings()
defaults.update(config_dict.pop(DEFAULT_BUILD_CONFIG))
templates = config_dict.pop("_templates", {})
defaultBuildConfig = BuildConfig(**defaults)
builds = {
n: _CreateBuildConfig(n, defaultBuildConfig, v, templates)
for n, v in config_dict.items()
}
# config is the struct that holds the complete cbuildbot config.
result = SiteConfig(defaults=defaults, templates=templates)
result.update(builds)
return result
def _CreateBuildConfig(name, default, build_dict, templates):
"""Create a BuildConfig object from it's parsed JSON dictionary encoding."""
# These build config values need special handling.
template = build_dict.get("_template")
# Use the name passed in as the default build name.
build_dict.setdefault("name", name)
result = default.deepcopy()
# Use update to explicitly avoid apply's special handing.
if template:
result.update(templates[template])
result.update(build_dict)
return result
@memoize.Memoize
def GetConfig():
"""Load the current SiteConfig.
Returns:
SiteConfig instance to use for this build.
"""
return LoadConfigFromFile(constants.CHROMEOS_CONFIG_FILE)
@memoize.Memoize
def GetSiteParams():
"""Get the site parameter configs.
This is the new, preferred method of accessing the site parameters, instead
of SiteConfig.params.
Returns:
AttrDict of site parameters
"""
site_params = AttrDict()
site_params.update(DefaultSiteParameters())
return site_params
def append_useflags(useflags):
"""Used to append a set of useflags to existing useflags.
Useflags that shadow prior use flags will cause the prior flag to be
removed. (e.g. appending '-foo' to 'foo' will cause 'foo' to be removed)
Examples:
new_config = base_config.derive(
useflags=append_useflags(['foo', '-bar'])
)
Args:
useflags: List of string useflags to append.
"""
assert isinstance(useflags, (list, set))
shadowed_useflags = {
"-" + flag for flag in useflags if not flag.startswith("-")
}
shadowed_useflags.update(
{flag[1:] for flag in useflags if flag.startswith("-")}
)
def handler(old_useflags):
new_useflags = set(old_useflags or [])
new_useflags.update(useflags)
new_useflags.difference_update(shadowed_useflags)
return sorted(list(new_useflags))
return handler