blob: 5ef2acc01e550413c580607a5f100f860dd51423 [file] [log] [blame]
# Copyright 2013 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import re
from recipe_engine import recipe_api
class ChromiteApi(recipe_api.RecipeApi):
chromite_url = 'https://chromium.googlesource.com/chromiumos/chromite.git'
depot_tools_url = (
'https://chromium.googlesource.com/chromium/tools/depot_tools.git')
# Keep this pin in sync with manifest pin in:
# https://chromium.googlesource.com/chromiumos/manifest/+/master/full.xml
depot_tools_pin = '7639f1999ac1d8992fc4fbb169d164b156d9a7f7'
# Only used by the internal goma recipe.
manifest_url = 'https://chromium.googlesource.com/chromiumos/manifest.git'
repo_url = 'https://chromium.googlesource.com/external/repo.git'
# The number of Gitiles attempts to make before giving up.
_GITILES_ATTEMPTS = 10
_MANIFEST_CMD_RE = re.compile(r'Automatic:\s+Start\s+([^\s]+)\s+([^\s]+)')
_BUILD_ID_RE = re.compile(r'CrOS-Build-Id: (.+)')
@property
def chromite_path(self):
return self.m.path['start_dir'].join('chromite')
@property
def depot_tools_path(self):
return self.m.path['start_dir'].join('depot_tools')
def get_config_defaults(self):
defaults = {
'CBB_CONFIG': self.m.properties.get('cbb_config'),
'CBB_BRANCH': self.m.properties.get('cbb_branch'),
'CBB_MASTER_BUILD_ID': self.m.properties.get('cbb_master_build_id'),
'CBB_DEBUG': self.m.properties.get('cbb_debug') is not None,
'CBB_CLOBBER': 'clobber' in self.m.properties,
}
if 'buildnumber' in self.m.properties:
defaults['CBB_BUILD_NUMBER'] = int(self.m.properties['buildnumber'])
build_id = self.m.buildbucket.build.id
if build_id:
defaults['CBB_BUILDBUCKET_ID'] = build_id
return defaults
def check_repository(self, repo_type_key, value):
"""Scans through registered repositories for a specified value.
Args:
repo_type_key (str): The key in the 'repositories' config to scan through.
value (str): The value to scan for.
Returns (bool): True if the value was found.
"""
def remove_tail(v, tail):
if v.endswith(tail):
v = v[:-len(tail)]
return v
for v in self.c.repositories.get(repo_type_key, ()):
if remove_tail(v, '.git') == remove_tail(value, '.git'):
return True
return False
def load_manifest_config(self, repository, revision):
"""Loads manifest-specified parameters from the manifest commit.
This method parses the commit log for the following information:
- The branch to build (From the "Automatic": tag).
- The build ID (from the CrOS-Build-Id: tag).
Args:
repository (str): The URL of the repository hosting the change.
revision (str): The revision hash to load the build ID from.
"""
if all((self.c.chromite_branch, self.c.cbb.build_id)):
# They have all already been populated, so we're done (BuildBucket).
return
# Load our manifest fields from the formatted Gitiles commit message that
# scheduled this build.
#
# First, check that we are actually in a known manifest Gitiles repository.
if not self.check_repository('cros_manifest', repository):
return
commit_log = self.m.gitiles.commit_log(
repository, revision, step_name='Fetch manifest config',
attempts=self._GITILES_ATTEMPTS)
result = self.m.step.active_result
# Handle missing/invalid response.
if not (commit_log and commit_log.get('message')):
self.m.python.failing_step('Fetch manifest config failure',
'Failed to fetch manifest config.')
loaded = []
for line in reversed(commit_log['message'].splitlines()):
# Automatic command?
match = self._MANIFEST_CMD_RE.match(line)
if match:
self.c.chromite_branch = match.group(2)
loaded.append('Chromite branch: %s' % (self.c.chromite_branch,))
continue
# Build ID?
match = self._BUILD_ID_RE.match(line)
if match:
self.c.cbb.build_id = match.group(1)
continue
if loaded:
loaded.insert(0, '')
result.presentation.step_text += '<br/>'.join(loaded)
def gclient_config(self):
"""Generate a 'gclient' configuration to check out Chromite.
Return: (config) A 'gclient' recipe module configuration.
"""
cfg = self.m.gclient.make_config()
soln = cfg.solutions.add()
soln.name = 'chromite'
soln.url = self.chromite_url
soln.revision = 'master'
soln = cfg.solutions.add()
soln.name = 'depot_tools'
soln.url = self.depot_tools_url
soln.revision = self.depot_tools_pin
return cfg
def cbuildbot(self, name, config, args=None, **kwargs):
"""Runs the cbuildbot command defined by the arguments.
Args:
name: (str) The name of the command step.
config: (str) The name of the 'cbuildbot' configuration to invoke.
args: (list) If not None, addition arguments to pass to 'cbuildbot'.
Returns: (Step) The step that was run.
"""
args = (args or [])[:]
args.append(config)
cmd = [self.chromite_path.join('scripts', 'cbuildbot_launch')] + args
return self.m.step(name, cmd, allow_subannotations=True, **kwargs)
# Only used by the internal goma recipe.
def checkout(self, manifest_url=None, repo_url=None, branch=None,
repo_sync_args=None):
if repo_sync_args is None:
repo_sync_args = []
manifest_url = manifest_url or self.manifest_url
repo_url = repo_url or self.repo_url
if branch:
self.m.repo.init(manifest_url, '--repo-url', repo_url, '-b', branch)
else:
self.m.repo.init(manifest_url, '--repo-url', repo_url)
self.m.repo.sync(*repo_sync_args)
# Only used by the internal goma recipe.
def cros_sdk(self, name, cmd, args=None, environ=None, chroot_cmd=None,
**kwargs):
"""Return a step to run a command inside the cros_sdk.
Used by the internal goma recipe.
"""
if not chroot_cmd:
chroot_cmd = self.chromite_path.join('bin', 'cros_sdk')
arg_list = (args or [])[:]
for t in sorted((environ or {}).items()):
arg_list.append('%s=%s' % t)
arg_list.append('--')
arg_list.extend(cmd)
self.m.python(name, chroot_cmd, arg_list, **kwargs)
# Only used by the internal goma recipe.
def setup_board(self, board, args=None, **kwargs):
"""Run the setup_board script inside the chroot.
Used by the internal goma recipe.
"""
self.cros_sdk('setup board',
['./setup_board', '--board', board],
args, **kwargs)
# Only used by the internal goma recipe.
def build_packages(self, board, args=None, **kwargs):
"""Run the build_packages script inside the chroot.
Used by the internal goma recipe.
"""
self.cros_sdk('build packages',
['./build_packages', '--board', board],
args, **kwargs)
def configure(self, properties, config_map, **KWARGS):
"""Loads configuration from build properties into this recipe config.
Args:
properties (Properties): The build properties object.
config_map (dict): The configuration map to use.
KWARGS: Additional keyword arguments to forward to the configuration.
"""
master = properties.get('mastername')
builder = properties.get('buildername')
if master is None:
self.set_config('master_swarming', **KWARGS)
return
# Set the master's base configuration.
config_map = config_map.get(master, {})
master_config = config_map.get('master_config')
assert master_config, (
"No 'master_config' configuration for '%s'" % (master,))
self.set_config(master_config, **KWARGS)
# Set per-builder configuration if any.
builder_config = config_map.get('builder_config', {}).get(builder, None)
if builder_config:
self.set_config(builder_config, **KWARGS)
def run_cbuildbot(self, args=None, goma_canary=False):
"""Performs a Chromite repository checkout, then runs cbuildbot.
Args:
args (list): Initial argument list, see run() for details.
goma_canary (bool): Use canary version of goma if True.
"""
# Fetch chromite and depot_tools.
self.checkout_chromite()
# Update or install goma client via cipd.
client_type = None
if goma_canary:
client_type = 'candidate'
self.m.goma.ensure_goma(client_type=client_type)
self.run(args)
def checkout_chromite(self):
"""Checks out the configured Chromite branch.
"""
self.m.bot_update.ensure_checkout(
gclient_config=self.gclient_config(),
update_presentation=False,
ignore_input_commit=True)
return self.chromite_path
def with_system_python(self):
"""Prepare a directory with the system python binary available.
This is designed to make it possible to mask "bundled python" out of the
standard path without hiding any other binaries.
Returns: (context manager) A context manager that inserts system python
into the front of PATH.
"""
with self.m.step.nest('system_python'):
# Create a directory to hold a symlink to the system python binary.
python_bin = self.m.path['start_dir'].join('python_bin')
self.m.file.ensure_directory('create_dir', python_bin)
# Remove any old symlinks.
self.m.file.remove('remove_link', python_bin.join('python'))
self.m.file.remove('remove_link', python_bin.join('python2'))
# Create a symlink to the system python binary in that directory.
self.m.file.symlink('create_link',
'/usr/bin/python',
python_bin.join('python'))
self.m.file.symlink('create_link',
'/usr/bin/python2',
python_bin.join('python2'))
# python2 a context manager to insert that directory at the front of PATH.
return self.m.context(env_prefixes={'PATH': [python_bin]})
def run(self, args=None):
"""Runs the configured 'cbuildbot' build.
This workflow uses the registered configuration dictionary to make master-
and builder-specific changes to the standard workflow.
The specific workflow paths that are taken are also influenced by several
build properties.
TODO(dnj): When CrOS migrates away from BuildBot, replace property
inferences with command-line parameters.
This workflow:
- Checks out the specified 'cbuildbot' repository.
- Pulls information based on the configured change's repository/revision
to pass to 'cbuildbot'.
- Executes the 'cbuildbot' command.
Args:
args (list): Initial argument list, expanded based on other values.
Returns: (Step) the 'cbuildbot' execution step.
"""
# Assert correct configuration.
assert self.c.cbb.config, 'An empty configuration was specified.'
# Load properties from the commit being processed. This requires both a
# repository and revision to proceed.
repository = self.m.properties.get('repository')
revision = self.m.properties.get('revision')
if repository and revision:
# Pull more information from the commit if it came from certain known
# repositories.
if (self.c.use_chrome_version and
self.check_repository('chromium', repository)):
# If our change comes from a Chromium repository, add the
# '--chrome_version' flag.
self.c.cbb.chrome_version = self.m.properties['revision']
# This change comes from a manifest repository. Load configuration
# parameters from the manifest command.
self.load_manifest_config(repository, revision)
cbb_args = [
'--buildroot', self.m.path['cache'].join('cbuild'),
]
if args:
cbb_args.extend(args)
if self.c.chromite_branch:
cbb_args.extend(['--branch', self.c.chromite_branch])
if self.c.cbb.build_number is not None:
cbb_args.extend(['--buildnumber', self.c.cbb.build_number])
if self.c.cbb.debug:
cbb_args.extend(['--debug'])
if self.c.cbb.clobber:
cbb_args.extend(['--clobber'])
if self.c.cbb.chrome_version:
cbb_args.extend(['--chrome_version', self.c.cbb.chrome_version])
if self.c.cbb.buildbucket_id:
cbb_args.extend(['--buildbucket-id', self.c.cbb.buildbucket_id])
# Set the CIDB master build ID, if specified.
if self.c.cbb.build_id:
cbb_args.extend(['--master-build-id', self.c.cbb.build_id])
cbb_args.extend(['--git-cache-dir', self.m.path['cache'].join('git')])
cbb_args.extend([
'--goma_dir', self.m.goma.goma_dir,
'--goma_client_json', self.m.goma.service_account_json_path])
# Add custom args, if there are any.
cbb_args.extend(self.c.cbb.extra_args)
# Run cbuildbot.
# TODO(dgarrett): stop adjusting path here, and pass into cbuildbot_launcher
# instead.
#
# Set "DEPOT_TOOLS_UPDATE" to prevent any invocations of "depot_tools"
# scripts that call "//update_depot_tools" (e.g., "gclient") from trying
# to self-update from their pinned version (crbug.com/736890).
context_key = 'env_suffixes' if self.m.runtime.is_luci else 'env_prefixes'
with self.m.context(**{
'cwd': self.m.path['start_dir'],
context_key: {'PATH': [self.depot_tools_path]},
'env': {'DEPOT_TOOLS_UPDATE': '0'}}):
return self.cbuildbot(str('cbuildbot_launch [%s]' % (self.c.cbb.config,)),
self.c.cbb.config,
args=cbb_args)