blob: 39a85de275960a21a74610b41ca55c0daf01cd9f [file] [log] [blame]
#!/usr/bin/python
# 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.
"""Main builder code for Chromium OS.
Used by Chromium OS buildbot configuration for all Chromium OS builds including
full and pre-flight-queue builds.
"""
import distutils.version
import glob
import logging
import multiprocessing
import optparse
import os
import pprint
import sys
import time
from chromite.buildbot import builderstage as bs
from chromite.buildbot import cbuildbot_background as background
from chromite.buildbot import cbuildbot_config
from chromite.buildbot import cbuildbot_stages as stages
from chromite.buildbot import cbuildbot_results as results_lib
from chromite.buildbot import constants
from chromite.buildbot import gerrit_helper
from chromite.buildbot import patch as cros_patch
from chromite.buildbot import remote_try
from chromite.buildbot import repository
from chromite.buildbot import tee
from chromite.buildbot import trybot_patch_pool
from chromite.lib import cgroups
from chromite.lib import cleanup
from chromite.lib import cros_build_lib
from chromite.lib import osutils
from chromite.lib import sudo
cros_build_lib.STRICT_SUDO = True
_DEFAULT_LOG_DIR = 'cbuildbot_logs'
_BUILDBOT_LOG_FILE = 'cbuildbot.log'
_DEFAULT_EXT_BUILDROOT = 'trybot'
_DEFAULT_INT_BUILDROOT = 'trybot-internal'
_DISTRIBUTED_TYPES = [constants.COMMIT_QUEUE_TYPE, constants.PFQ_TYPE,
constants.CANARY_TYPE, constants.CHROME_PFQ_TYPE,
constants.PALADIN_TYPE]
_BUILDBOT_REQUIRED_BINARIES = ('pbzip2',)
# Used by --resume and --bootstrap to decipher which options they
# can pass to the target cbuildbot (since it may not have that
# option).
# Format is Major:Minor. Minor is used for tracking new options added
# that aren't critical to the older version if it's not ran.
# Major is used for tracking heavy API breakage- for example, no longer
# supporting the --resume option.
_REEXEC_API_MAJOR = 0
_REEXEC_API_MINOR = 1
_REEXEC_API_VERSION = '%i.%i' % (_REEXEC_API_MAJOR, _REEXEC_API_MINOR)
def _PrintValidConfigs(display_all=False):
"""Print a list of valid buildbot configs.
Arguments:
display_all: Print all configs. Otherwise, prints only configs with
trybot_list=True.
"""
def _GetSortKey(config_name):
config_dict = cbuildbot_config.config[config_name]
return (not config_dict['trybot_list'], config_dict['description'],
config_name)
COLUMN_WIDTH = 45
print 'config'.ljust(COLUMN_WIDTH), 'description'
print '------'.ljust(COLUMN_WIDTH), '-----------'
config_names = cbuildbot_config.config.keys()
config_names.sort(key=_GetSortKey)
for name in config_names:
if display_all or cbuildbot_config.config[name]['trybot_list']:
desc = cbuildbot_config.config[name].get('description')
desc = desc if desc else ''
print name.ljust(COLUMN_WIDTH), desc
def _GetConfig(config_name):
"""Gets the configuration for the build"""
if not cbuildbot_config.config.has_key(config_name):
print 'Non-existent configuration %s specified.' % config_name
print 'Please specify one of:'
_PrintValidConfigs()
sys.exit(1)
result = cbuildbot_config.config[config_name]
return result
def AcquirePoolFromOptions(options):
"""Generate patch objects from passed in options.
Args:
options: The options object generated by optparse.
Returns:
trybot_patch_pool.TrybotPatchPool object.
Raises:
gerrit_helper.GerritException, cros_patch.PatchException
"""
gerrit_patches = []
local_patches = []
remote_patches = []
if options.gerrit_patches:
gerrit_patches = gerrit_helper.GetGerritPatchInfo(
options.gerrit_patches)
for patch in gerrit_patches:
if patch.IsAlreadyMerged():
cros_build_lib.Warning('Patch %s has already been merged.' % str(patch))
if options.local_patches:
manifest = cros_build_lib.ManifestCheckout.Cached(options.sourceroot)
local_patches = cros_patch.PrepareLocalPatches(manifest,
options.local_patches)
if options.remote_patches:
remote_patches = cros_patch.PrepareRemotePatches(
options.remote_patches)
return trybot_patch_pool.TrybotPatchPool(gerrit_patches, local_patches,
remote_patches)
class Builder(object):
"""Parent class for all builder types.
This class functions as a parent class for various build types. It's intended
use is builder_instance.Run().
Vars:
build_config: The configuration dictionary from cbuildbot_config.
options: The options provided from optparse in main().
archive_urls: Where our artifacts for this builder will be archived.
release_tag: The associated "chrome os version" of this build.
"""
def __init__(self, options, build_config):
"""Initializes instance variables. Must be called by all subclasses."""
self.build_config = build_config
self.options = options
# TODO, Remove here and in config after bug chromium-os:14649 is fixed.
if self.build_config['chromeos_official']:
os.environ['CHROMEOS_OFFICIAL'] = '1'
self.archive_stages = {}
self.archive_urls = {}
self.release_tag = None
self.patch_pool = trybot_patch_pool.TrybotPatchPool()
bs.BuilderStage.SetManifestBranch(self.options.branch)
def Initialize(self):
"""Runs through the initialization steps of an actual build."""
if self.options.resume:
results_lib.LoadCheckpoint(self.options.buildroot)
self._RunStage(stages.CleanUpStage)
def _GetStageInstance(self, stage, *args, **kwargs):
"""Helper function to get an instance given the args.
Useful as almost all stages just take in options and build_config.
"""
config = kwargs.pop('config', self.build_config)
return stage(self.options, config, *args, **kwargs)
def _SetReleaseTag(self):
"""Sets the release tag from the manifest_manager.
Must be run after sync stage as syncing enables us to have a release tag.
"""
# Extract version we have decided to build into self.release_tag.
manifest_manager = stages.ManifestVersionedSyncStage.manifest_manager
if manifest_manager:
self.release_tag = manifest_manager.current_version
def _RunStage(self, stage, *args, **kwargs):
"""Wrapper to run a stage."""
stage_instance = self._GetStageInstance(stage, *args, **kwargs)
return stage_instance.Run()
def GetSyncInstance(self):
"""Returns an instance of a SyncStage that should be run.
Subclasses must override this method.
"""
raise NotImplementedError()
def RunStages(self):
"""Subclasses must override this method. Runs the appropriate code."""
raise NotImplementedError()
def _ShouldReExecuteInBuildRoot(self):
"""Returns True if this build should be re-executed in the buildroot."""
abs_buildroot = os.path.abspath(self.options.buildroot)
return not os.path.abspath(__file__).startswith(abs_buildroot)
def _ReExecuteInBuildroot(self, sync_instance):
"""Reexecutes self in buildroot and returns True if build succeeds.
This allows the buildbot code to test itself when changes are patched for
buildbot-related code. This is a no-op if the buildroot == buildroot
of the running chromite checkout.
Args:
sync_instance: Instance of the sync stage that was run to sync.
Returns:
True if the Build succeeded.
"""
if not self.options.resume:
results_lib.WriteCheckpoint(self.options.buildroot)
# Get the re-exec API version of the target chromite; if it's incompatible
# with us, bail now.
api = cros_build_lib.RunCommandCaptureOutput(
[constants.PATH_TO_CBUILDBOT] + ['--reexec-api-version'],
cwd=self.options.buildroot, error_code_ok=True)
# If the command failed, then we're targeting a cbuildbot that lacks the
# option; assume 0:0 (ie, initial state).
major, minor = 0, 0
if api.returncode == 0:
major, minor = map(int, api.output.strip().split('.', 1))
if major != _REEXEC_API_MAJOR:
cros_build_lib.Die(
'The targeted version of chromite in buildroot %s requires '
'api version %i, but we are api version %i. We cannot proceed.'
% (self.options.buildroot, major, _REEXEC_API_MAJOR))
# Re-write paths to use absolute paths.
# Suppress any timeout options given from the commandline in the
# invoked cbuildbot; our timeout will enforce it instead.
args_to_append = ['--resume', '--timeout', '0', '--notee', '--nocgroups',
'--buildroot', os.path.abspath(self.options.buildroot)]
if self.options.chrome_root:
args_to_append += ['--chrome_root',
os.path.abspath(self.options.chrome_root)]
if stages.ManifestVersionedSyncStage.manifest_manager:
ver = stages.ManifestVersionedSyncStage.manifest_manager.current_version
args_to_append += ['--version', ver]
if isinstance(sync_instance, stages.CommitQueueSyncStage):
vp_file = sync_instance.SaveValidationPool()
args_to_append += ['--validation_pool', vp_file]
# Re-run the command in the buildroot.
# Finally, be generous and give the invoked cbuildbot 30s to shutdown
# when something occurs. It should exit quicker, but the sigterm may
# hit while the system is particularly busy.
return_obj = cros_build_lib.RunCommand(
[constants.PATH_TO_CBUILDBOT] + sys.argv[1:] + args_to_append,
cwd=self.options.buildroot, error_code_ok=True, kill_timeout=30)
return return_obj.returncode == 0
def _InitializeTrybotPatchPool(self):
"""Generate patch pool from patches specified on the command line.
Do this only if we need to patch changes later on.
"""
changes_stage = stages.PatchChangesStage.StageNamePrefix()
check_func = results_lib.Results.PreviouslyCompletedRecord
if not check_func(changes_stage) or self.options.bootstrap:
self.patch_pool = AcquirePoolFromOptions(self.options)
def _GetBootstrapStage(self):
"""Constructs and returns the BootStrapStage object.
We return None when there are no chromite patches to test, and
--test-bootstrap wasn't passed in.
"""
stage = None
chromite_pool = self.patch_pool.Filter(project=constants.CHROMITE_PROJECT)
manifest_pool = self.patch_pool.FilterManifest()
chromite_branch = cros_build_lib.GetChromiteTrackingBranch()
if (chromite_pool or manifest_pool or self.options.test_bootstrap
or chromite_branch != self.options.branch):
stage = stages.BootstrapStage(self.options, self.build_config,
chromite_pool, manifest_pool)
return stage
def Run(self):
"""Main runner for this builder class. Runs build and prints summary.
Returns:
Whether the build succeeded.
"""
self._InitializeTrybotPatchPool()
if self.options.bootstrap:
bootstrap_stage = self._GetBootstrapStage()
if bootstrap_stage:
# BootstrapStage blocks on re-execution of cbuildbot.
bootstrap_stage.Run()
return bootstrap_stage.returncode == 0
print_report = True
exception_thrown = False
success = True
try:
self.Initialize()
sync_instance = self.GetSyncInstance()
sync_instance.Run()
self._SetReleaseTag()
# Filter out patches to manifest, since PatchChangesStage can't handle
# them. Manifest patches are patched in the BootstrapStage.
non_manifest_patches = self.patch_pool.FilterManifest(negate=True)
if non_manifest_patches:
self._RunStage(stages.PatchChangesStage, non_manifest_patches)
if self._ShouldReExecuteInBuildRoot():
print_report = False
success = self._ReExecuteInBuildroot(sync_instance)
else:
self.RunStages()
except results_lib.StepFailure:
# StepFailure exceptions are already recorded in the report, so there
# is no need to print these tracebacks twice.
exception_thrown = True
if not print_report:
raise
except Exception:
exception_thrown = True
raise
finally:
if print_report:
results_lib.WriteCheckpoint(self.options.buildroot)
print '\n\n\n@@@BUILD_STEP Report@@@\n'
results_lib.Results.Report(sys.stdout, self.archive_urls,
self.release_tag)
success = results_lib.Results.BuildSucceededSoFar()
if exception_thrown and success:
success = False
cros_build_lib.PrintBuildbotStepWarnings()
print """\
Exception thrown, but all stages marked successful. This is an internal error,
because the stage that threw the exception should be marked as failing."""
return success
class SimpleBuilder(Builder):
"""Builder that performs basic vetting operations."""
def GetSyncInstance(self):
"""Sync to lkgm or TOT as necessary.
Returns: the instance of the sync stage that was run.
"""
if self.options.lkgm or self.build_config['use_lkgm']:
sync_stage = self._GetStageInstance(stages.LKGMSyncStage)
else:
sync_stage = self._GetStageInstance(stages.SyncStage)
return sync_stage
def _RunBackgroundStagesForBoard(self, board):
"""Run background board-specific stages for the specified board."""
archive_stage = self.archive_stages[board]
configs = self.build_config['board_specific_configs']
config = configs.get(board, self.build_config)
stage_list = [[stages.VMTestStage, board, archive_stage],
[stages.ChromeTestStage, board, archive_stage],
[stages.UnitTestStage, board],
[stages.UploadPrebuiltsStage, board, archive_stage]]
# We can not run hw tests without archiving the payloads.
if self.options.archive:
for suite in config['hw_tests']:
stage_list.append([stages.HWTestStage, board, archive_stage, suite])
for suite in config['async_hw_tests']:
stage_list.append([stages.ASyncHWTestStage, board, archive_stage,
suite])
steps = [self._GetStageInstance(*x, config=config).Run for x in stage_list]
background.RunParallelSteps(steps + [archive_stage.Run])
def RunStages(self):
"""Runs through build process."""
self._RunStage(stages.BuildBoardStage)
# TODO(sosa): Split these out into classes.
if self.build_config['build_type'] == constants.CHROOT_BUILDER_TYPE:
self._RunStage(stages.SDKTestStage)
self._RunStage(stages.UploadPrebuiltsStage,
constants.CHROOT_BUILDER_BOARD, None)
elif self.build_config['build_type'] == constants.REFRESH_PACKAGES_TYPE:
self._RunStage(stages.RefreshPackageStatusStage)
else:
self._RunStage(stages.UprevStage)
configs = self.build_config['board_specific_configs']
for board in self.build_config['boards']:
config = configs.get(board, self.build_config)
archive_stage = self._GetStageInstance(stages.ArchiveStage, board,
config=config)
self.archive_stages[board] = archive_stage
# Set up a process pool to run test/archive stages in the background.
# This process runs task(board) for each board added to the queue.
queue = multiprocessing.Queue()
task = self._RunBackgroundStagesForBoard
with background.BackgroundTaskRunner(queue, task):
for board in self.build_config['boards']:
# Run BuildTarget in the foreground.
archive_stage = self.archive_stages[board]
config = configs.get(board, self.build_config)
self._RunStage(stages.BuildTargetStage, board, archive_stage,
self.release_tag, config=config)
self.archive_urls[board] = archive_stage.GetDownloadUrl()
# Kick off task(board) in the background.
queue.put([board])
class DistributedBuilder(SimpleBuilder):
"""Build class that has special logic to handle distributed builds.
These builds sync using git/manifest logic in manifest_versions. In general
they use a non-distributed builder code for the bulk of the work.
"""
def __init__(self, *args, **kwargs):
"""Initializes a buildbot builder.
Extra variables:
completion_stage_class: Stage used to complete a build. Set in the Sync
stage.
"""
super(DistributedBuilder, self).__init__(*args, **kwargs)
self.completion_stage_class = None
def GetSyncInstance(self):
"""Syncs the tree using one of the distributed sync logic paths.
Returns: the instance of the sync stage that was run.
"""
# Determine sync class to use. CQ overrides PFQ bits so should check it
# first.
if cbuildbot_config.IsCQType(self.build_config['build_type']):
sync_stage = self._GetStageInstance(stages.CommitQueueSyncStage)
self.completion_stage_class = stages.CommitQueueCompletionStage
elif cbuildbot_config.IsPFQType(self.build_config['build_type']):
sync_stage = self._GetStageInstance(stages.LKGMCandidateSyncStage)
self.completion_stage_class = stages.LKGMCandidateSyncCompletionStage
else:
sync_stage = self._GetStageInstance(stages.ManifestVersionedSyncStage)
self.completion_stage_class = stages.ManifestVersionedSyncCompletionStage
return sync_stage
def Publish(self, was_build_successful):
"""Completes build by publishing any required information."""
completion_stage = self._GetStageInstance(self.completion_stage_class,
was_build_successful)
completion_stage.Run()
name = completion_stage.name
if not results_lib.Results.WasStageSuccessful(name):
should_publish_changes = False
else:
should_publish_changes = (self.build_config['master'] and
was_build_successful)
if should_publish_changes:
self._RunStage(stages.PublishUprevChangesStage)
def RunStages(self):
"""Runs simple builder logic and publishes information to overlays."""
was_build_successful = False
try:
super(DistributedBuilder, self).RunStages()
was_build_successful = results_lib.Results.BuildSucceededSoFar()
except SystemExit as ex:
# If a stage calls sys.exit(0), it's exiting with success, so that means
# we should mark ourselves as successful.
if ex.code == 0:
was_build_successful = True
raise
finally:
self.Publish(was_build_successful)
def _ConfirmBuildRoot(buildroot):
"""Confirm with user the inferred buildroot, and mark it as confirmed."""
warning = 'Using default directory %s as buildroot' % buildroot
response = cros_build_lib.YesNoPrompt(
default=cros_build_lib.NO, warning=warning, full=True)
if response == cros_build_lib.NO:
print('Please specify a buildroot with the --buildroot option.')
sys.exit(0)
if not os.path.exists(buildroot):
os.mkdir(buildroot)
repository.CreateTrybotMarker(buildroot)
def _ConfirmRemoteBuildbotRun():
"""Confirm user wants to run with --buildbot --remote."""
warning = ('You are about to launch a PRODUCTION job! This is *NOT* a '
'trybot run! Are you sure?')
response = cros_build_lib.YesNoPrompt(
default=cros_build_lib.NO, warning=warning, full=True)
if response == cros_build_lib.NO:
print('Please specify --pass-through="--debug".')
sys.exit(0)
def _DetermineDefaultBuildRoot(sourceroot, internal_build):
"""Default buildroot to be under the directory that contains current checkout.
Arguments:
internal_build: Whether the build is an internal build
sourceroot: Use specified sourceroot.
"""
if not repository.IsARepoRoot(sourceroot):
cros_build_lib.Die(
'Could not find root of local checkout at %s. Please specify '
'using the --sourceroot option.' % sourceroot)
# Place trybot buildroot under the directory containing current checkout.
top_level = os.path.dirname(os.path.realpath(sourceroot))
if internal_build:
buildroot = os.path.join(top_level, _DEFAULT_INT_BUILDROOT)
else:
buildroot = os.path.join(top_level, _DEFAULT_EXT_BUILDROOT)
return buildroot
def _BackupPreviousLog(log_file, backup_limit=25):
"""Rename previous log.
Args:
log_file: The absolute path to the previous log.
"""
if os.path.exists(log_file):
old_logs = sorted(glob.glob(log_file + '.*'),
key=distutils.version.LooseVersion)
if len(old_logs) >= backup_limit:
os.remove(old_logs[0])
last = 0
if old_logs:
last = int(old_logs.pop().rpartition('.')[2])
os.rename(log_file, log_file + '.' + str(last + 1))
def _RunBuildStagesWrapper(options, build_config):
"""Helper function that wraps RunBuildStages()."""
def IsDistributedBuilder():
"""Determines whether the build_config should be a DistributedBuilder."""
if not options.buildbot:
return False
elif build_config['build_type'] in _DISTRIBUTED_TYPES:
chrome_rev = build_config['chrome_rev']
if options.chrome_rev: chrome_rev = options.chrome_rev
# We don't do distributed logic to TOT Chrome PFQ's, nor local
# chrome roots (e.g. chrome try bots)
if chrome_rev not in [constants.CHROME_REV_TOT,
constants.CHROME_REV_LOCAL,
constants.CHROME_REV_SPEC]:
return True
return False
cros_build_lib.Info("cbuildbot executed with args %s"
% ' '.join(map(repr, sys.argv)))
target = DistributedBuilder if IsDistributedBuilder() else SimpleBuilder
buildbot = target(options, build_config)
if not buildbot.Run():
sys.exit(1)
# Parser related functions
def _CheckLocalPatches(sourceroot, local_patches):
"""Do an early quick check of the passed-in patches.
If the branch of a project is not specified we append the current branch the
project is on.
Args:
sourceroot: The checkout where patches are coming from.
"""
verified_patches = []
manifest = cros_build_lib.ManifestCheckout.Cached(sourceroot)
for patch in local_patches:
components = patch.split(':')
if len(components) > 2:
cros_build_lib.Die(
'Specify local patches in project[:branch] format. Got %s' % patch)
# validate project
project = components[0]
try:
project_dir = manifest.GetProjectPath(project, True)
except KeyError:
cros_build_lib.Die('Project %s does not exist.' % project)
# If no branch was specified, we use the project's current branch.
if len(components) == 1:
branch = cros_build_lib.GetCurrentBranch(project_dir)
if not branch:
cros_build_lib.Die('Project %s is not on a branch!' % project)
else:
branch = components[1]
if not cros_build_lib.DoesLocalBranchExist(project_dir, branch):
cros_build_lib.Die('Project %s does not have branch %s'
% (project, branch))
verified_patches.append('%s:%s' % (project, branch))
return verified_patches
def _CheckChromeVersionOption(_option, _opt_str, value, parser):
"""Upgrade other options based on chrome_version being passed."""
value = value.strip()
if parser.values.chrome_rev is None and value:
parser.values.chrome_rev = constants.CHROME_REV_SPEC
parser.values.chrome_version = value
def _CheckChromeRootOption(_option, _opt_str, value, parser):
"""Validate and convert chrome_root to full-path form."""
if parser.values.chrome_rev is None:
parser.values.chrome_rev = constants.CHROME_REV_LOCAL
parser.values.chrome_root = value
def _CheckChromeRevOption(_option, _opt_str, value, parser):
"""Validate the chrome_rev option."""
value = value.strip()
if value not in constants.VALID_CHROME_REVISIONS:
raise optparse.OptionValueError('Invalid chrome rev specified')
parser.values.chrome_rev = value
def _CheckGerritChromeOption(_option, _opt_str, value, parser):
"""Validate the chrome_rev option."""
if parser.values.chrome_rev is None:
parser.values.chrome_rev = constants.CHROME_REV_TOT
parser.values.gerrit_chrome = True
class CustomParser(optparse.OptionParser):
def add_remote_option(self, *args, **kwargs):
"""For arguments that are passed-through to remote trybot."""
return optparse.OptionParser.add_option(self, *args,
remote_pass_through=True,
**kwargs)
class CustomGroup(optparse.OptionGroup):
def add_remote_option(self, *args, **kwargs):
"""For arguments that are passed-through to remote trybot."""
return optparse.OptionGroup.add_option(self, *args,
remote_pass_through=True,
**kwargs)
# pylint: disable=W0613
def check_path(option, opt, value):
"""Expand paths and make them absolute."""
expanded = osutils.ExpandPath(value)
if expanded == '/':
raise optparse.OptionValueError('Invalid path %s specified for %s'
% (expanded, opt))
return expanded
# pylint: disable=W0613
def check_gs_path(option, opt, value):
"""Expand paths and make them absolute."""
value = value.strip().rstrip('/')
if not value.startswith('gs://'):
raise optparse.OptionValueError('Invalid gs path %s specified for %s'
% (value, opt))
return value
class CustomOption(optparse.Option):
"""Subclass Option class to implement pass-through and path evaluation."""
TYPES = optparse.Option.TYPES + ('path', 'gs_path')
TYPE_CHECKER = optparse.Option.TYPE_CHECKER.copy()
TYPE_CHECKER['path'] = check_path
TYPE_CHECKER['gs_path'] = check_gs_path
ACTIONS = optparse.Option.ACTIONS + ('extend',)
STORE_ACTIONS = optparse.Option.STORE_ACTIONS + ('extend',)
TYPED_ACTIONS = optparse.Option.TYPED_ACTIONS + ('extend',)
ALWAYS_TYPED_ACTIONS = optparse.Option.ALWAYS_TYPED_ACTIONS + ('extend',)
def __init__(self, *args, **kwargs):
# The remote_pass_through argument specifies whether we should directly
# pass the argument (with its value) onto the remote trybot.
self.pass_through = kwargs.pop('remote_pass_through', False)
optparse.Option.__init__(self, *args, **kwargs)
def take_action(self, action, dest, opt, value, values, parser):
if action == 'extend':
# If there is extra spaces between each argument, we get '' which later
# code barfs on, so skip those. e.g. We see this with the forms:
# cbuildbot -p 'proj:branch ' ...
# cbuildbot -p ' proj:branch' ...
# cbuildbot -p 'proj:branch proj2:branch' ...
lvalue = value.split()
values.ensure_value(dest, []).extend(lvalue)
else:
optparse.Option.take_action(self, action, dest, opt, value, values,
parser)
if self.pass_through:
parser.values.pass_through_args.append(opt)
if self.nargs and self.nargs > 1:
# value is a tuple if nargs > 1
string_list = [str(val) for val in list(value)]
parser.values.pass_through_args.extend(string_list)
elif value:
parser.values.pass_through_args.append(str(value))
def _CreateParser():
"""Generate and return the parser with all the options."""
# Parse options
usage = "usage: %prog [options] buildbot_config"
parser = CustomParser(usage=usage, option_class=CustomOption)
# Main options
# The remote_pass_through parameter to add_option is implemented by the
# CustomOption class. See CustomOption for more information.
parser.add_option('-a', '--all', action='store_true', dest='print_all',
default=False,
help=('List all of the buildbot configs available. Use '
'with the --list option'))
parser.add_remote_option('-b', '--branch',
help='The manifest branch to test. The branch to '
'check the buildroot out to.')
parser.add_option('-r', '--buildroot', dest='buildroot', type='path',
help='Root directory where source is checked out to, and '
'where the build occurs. For external build configs, '
"defaults to 'trybot' directory at top level of your "
'repo-managed checkout.')
parser.add_remote_option('--chrome_rev', default=None, type='string',
action='callback', dest='chrome_rev',
callback=_CheckChromeRevOption,
help=('Revision of Chrome to use, of type [%s]'
% '|'.join(constants.VALID_CHROME_REVISIONS)))
parser.add_remote_option('-g', '--gerrit-patches', action='extend',
default=[], type='string',
metavar="'Id1 *int_Id2...IdN'",
help=("Space-separated list of short-form Gerrit "
"Change-Id's or change numbers to patch. "
"Please prepend '*' to internal Change-Id's"))
parser.add_option('-l', '--list', action='store_true', dest='list',
default=False,
help=('List the suggested trybot configs to use. Use '
'--all to list all of the available configs.'))
parser.add_option('--local', default=False, action='store_true',
help=('Specifies that this tryjob should be run locally.'))
parser.add_option('-p', '--local-patches', action='extend', default=[],
metavar="'<project1>[:<branch1>]...<projectN>[:<branchN>]'",
help=('Space-separated list of project branches with '
'patches to apply. Projects are specified by name. '
'If no branch is specified the current branch of the '
'project will be used.'))
parser.add_remote_option('--profile', default=None, type='string',
action='store', dest='profile',
help='Name of profile to sub-specify board variant.')
parser.add_option('--remote', default=False, action='store_true',
help=('Specifies that this tryjob should be run remotely.'))
parser.add_option('--remote-description', default=None,
help=('Attach an optional description to a --remote run '
'to make it easier to identify the results when it '
'finishes.'))
#
# Advanced options
#
group = CustomGroup(
parser,
'Advanced Options',
'Caution: use these options at your own risk.')
group.add_remote_option('--buildbot', dest='buildbot', action='store_true',
default=False, help='This is running on a buildbot')
group.add_remote_option('--buildnumber', help='build number', type='int',
default=0)
group.add_option('--chrome_root', default=None, type='path',
action='callback', callback=_CheckChromeRootOption,
dest='chrome_root', help='Local checkout of Chrome to use.')
group.add_remote_option('--chrome_version', default=None, type='string',
action='callback', dest='chrome_version',
callback=_CheckChromeVersionOption,
help='Used with SPEC logic to force a particular SVN '
'revision of chrome rather than the latest.')
group.add_remote_option('--clobber', action='store_true', dest='clobber',
default=False,
help='Clears an old checkout before syncing')
group.add_remote_option('--hwtest', dest='hwtest', action='store_true',
default=False,
help='This adds HW test for remote trybot')
group.add_remote_option('--lkgm', action='store_true', dest='lkgm',
default=False,
help='Sync to last known good manifest blessed by '
'PFQ')
parser.add_option('--log_dir', dest='log_dir', type='path',
help=('Directory where logs are stored.'))
group.add_remote_option('--maxarchives', dest='max_archive_builds',
default=3, type='int',
help="Change the local saved build count limit.")
parser.add_remote_option('--manifest-repo-url',
help=('Overrides the default manifest repo url.'))
group.add_remote_option('--noarchive', action='store_false', dest='archive',
default=True, help="Don't run archive stage.")
group.add_remote_option('--nobootstrap', action='store_false',
dest='bootstrap', default=True,
help="Don't checkout and run from a standalone "
"chromite repo.")
group.add_remote_option('--nobuild', action='store_false', dest='build',
default=True,
help="Don't actually build (for cbuildbot dev)")
group.add_remote_option('--noclean', action='store_false', dest='clean',
default=True, help="Don't clean the buildroot")
group.add_remote_option('--nocgroups', action='store_false', dest='cgroups',
default=True,
help='Disable cbuildbots usage of cgroups.')
group.add_remote_option('--noprebuilts', action='store_false',
dest='prebuilts', default=True,
help="Don't upload prebuilts.")
group.add_remote_option('--nosync', action='store_false', dest='sync',
default=True, help="Don't sync before building.")
group.add_remote_option('--notests', action='store_false', dest='tests',
default=True,
help='Override values from buildconfig and run no '
'tests.')
group.add_remote_option('--nouprev', action='store_false', dest='uprev',
default=True,
help='Override values from buildconfig and never '
'uprev.')
group.add_option('--reference-repo', action='store', default=None,
dest='reference_repo',
help='Reuse git data stored in an existing repo '
'checkout. This can drastically reduce the network '
'time spent setting up the trybot checkout. By '
"default, if this option isn't given but cbuildbot "
'is invoked from a repo checkout, cbuildbot will '
'use the repo root.')
group.add_option('--resume', action='store_true', default=False,
help='Skip stages already successfully completed.')
group.add_remote_option('--timeout', action='store', type='int', default=0,
help='Specify the maximum amount of time this job '
'can run for, at which point the build will be '
'aborted. If set to zero, then there is no '
'timeout.')
group.add_option('--test-tryjob', action='store_true',
default=False,
help='Submit a tryjob to the test repository. Will not '
'show up on the production trybot waterfall.')
group.add_remote_option('--validation_pool', default=None,
help='Path to a pickled validation pool. Intended '
'for use only with the commit queue.')
group.add_remote_option('--version', dest='force_version', default=None,
help='Used with manifest logic. Forces use of this '
'version rather than create or get latest.')
group.add_remote_option('--cq-gerrit-query', dest='cq_gerrit_override',
default=None,
help=
"If given, this gerrit query will be used to find what patches to test, "
"rather than the normal 'CommitReady=2 AND Verified=1 AND CodeReview=2' "
"query it defaults to. Use with care- note additionally this setting "
"only has an effect if the buildbot target is a cq target, and we're "
"in buildbot mode.")
parser.add_option_group(group)
#
# Hidden options.
#
# The base GS URL (gs://<bucket_name>/<path>) to archive artifacts to.
parser.add_remote_option('--archive-base', type='gs_path',
help=optparse.SUPPRESS_HELP)
# bootstrap-args are not verified by the bootstrap code. It gets passed
# direcly to the bootstrap re-execution.
parser.add_remote_option('--bootstrap-args', action='append',
default=[], help=optparse.SUPPRESS_HELP)
# Specify to use Gerrit Source for building Chrome. Implies
# --chrome_rev=CHROME_REV_TOT.
parser.add_option('--gerrit-chrome', action='callback', default=False,
callback=_CheckGerritChromeOption, dest='gerrit_chrome',
help=optparse.SUPPRESS_HELP)
parser.add_option('--pass-through', dest='pass_through_args', action='append',
type='string', default=[], help=optparse.SUPPRESS_HELP)
# Used for handling forwards/backwards compatibility for --resume and
# --bootstrap.
parser.add_option('--reexec-api-version', dest='output_api_version',
action='store_true', default=False,
help=optparse.SUPPRESS_HELP)
# Indicates this is running on a remote trybot machine.
parser.add_option('--remote-trybot', dest='remote_trybot',
action='store_true', default=False,
help=optparse.SUPPRESS_HELP)
# Patches uploaded by trybot client when run using the -p option.
parser.add_remote_option('--remote-patches', action='extend', default=[],
help=optparse.SUPPRESS_HELP)
# Specify specific remote tryslaves to run on.
parser.add_option('--slaves', action='extend', default=[],
help=optparse.SUPPRESS_HELP)
parser.add_option('--sourceroot', type='path', default=constants.SOURCE_ROOT,
help=optparse.SUPPRESS_HELP)
# Causes cbuildbot to bootstrap itself twice, in the sequence A->B->C.
# A(unpatched) patches and bootstraps B. B patches and bootstraps C.
parser.add_remote_option('--test-bootstrap', action='store_true',
default=False, help=optparse.SUPPRESS_HELP)
# Note the default here needs to be hardcoded to 3; that is the last version
# that lacked this functionality.
# This is used so that cbuildbot when processing tryjobs from
# older chromite instances, we can use it for handling compatibility.
parser.add_option('--remote-version', default=3, type=int, action='store',
help=optparse.SUPPRESS_HELP)
parser.add_option('--ts-mon-task-num', type='int', default=0,
help='The task number of this process. Defaults to 0. '
'This argument is useful for running multiple copies '
'of cbuildbot without their metrics colliding.')
#
# Debug options
#
group = CustomGroup(parser, "Debug Options")
group.add_remote_option('--debug', action='store_true', default=None,
help='Override some options to run as a developer.')
group.add_option('--dump_config', action='store_true', dest='dump_config',
default=False,
help='Dump out build config options, and exit.')
group.add_option('--notee', action='store_false', dest='tee', default=True,
help="Disable logging and internal tee process. Primarily "
"used for debugging cbuildbot itself.")
parser.add_option_group(group)
return parser
def _FinishParsing(options, args):
"""Perform some parsing tasks that need to take place after optparse.
This function needs to be easily testable! Keep it free of
environment-dependent code. Put more detailed usage validation in
_PostParseCheck().
Args:
options, args: The options/args object returned by optparse
"""
# Setup logging levels first so any parsing triggered log messages
# are appropriately filtered.
logging.getLogger().setLevel(
logging.DEBUG if options.debug else logging.INFO)
if options.chrome_root:
if options.chrome_rev != constants.CHROME_REV_LOCAL:
cros_build_lib.Die('Chrome rev must be %s if chrome_root is set.' %
constants.CHROME_REV_LOCAL)
else:
if options.chrome_rev == constants.CHROME_REV_LOCAL:
cros_build_lib.Die('Chrome root must be set if chrome_rev is %s.' %
constants.CHROME_REV_LOCAL)
if options.chrome_version:
if options.chrome_rev != constants.CHROME_REV_SPEC:
cros_build_lib.Die('Chrome rev must be %s if chrome_version is set.' %
constants.CHROME_REV_SPEC)
else:
if options.chrome_rev == constants.CHROME_REV_SPEC:
cros_build_lib.Die(
'Chrome rev must not be %s if chrome_version is not set.'
% constants.CHROME_REV_SPEC)
if options.gerrit_chrome:
if options.remote_trybot or options.remote:
cros_build_lib.Die('Cannot use --gerrit-chrome with remote trybots!')
elif options.chrome_rev != constants.CHROME_REV_TOT:
cros_build_lib.Die('Chrome rev must be %s if chrome_root is set.' %
constants.CHROME_REV_TOT)
patches = bool(options.gerrit_patches or options.local_patches)
if options.remote:
if options.local:
cros_build_lib.Die('Cannot specify both --remote and --local')
if not options.buildbot and not patches:
cros_build_lib.Die('Must provide patches when running with --remote.')
# --debug needs to be explicitly passed through for remote invocations.
release_mode_with_patches = (options.buildbot and patches and
'--debug' not in options.pass_through_args)
else:
if len(args) > 1:
cros_build_lib.Die('Multiple configs not supported if not running with '
'--remote. Got %r', args)
if options.slaves:
cros_build_lib.Die('Cannot use --slaves if not running with --remote.')
release_mode_with_patches = (options.buildbot and patches and
not options.debug)
# When running in release mode, make sure we are running with checked-in code.
# We want checked-in cbuildbot/scripts to prevent errors, and we want to build
# a release image with checked-in code for CrOS packages.
if release_mode_with_patches:
cros_build_lib.Die(
'Cannot provide patches when running with --buildbot!')
if options.buildbot and options.remote_trybot:
cros_build_lib.Die(
'--buildbot and --remote-trybot cannot be used together.')
# Record whether --debug was set explicitly vs. it was inferred.
options.debug_forced = False
if options.debug:
options.debug_forced = True
else:
# We don't set debug by default for
# 1. --buildbot invocations.
# 2. --remote invocations, because it needs to push changes to the tryjob
# repo.
options.debug = not options.buildbot and not options.remote
# pylint: disable=W0613
def _PostParseCheck(options, args):
"""Perform some usage validation after we've parsed the arguments
Args:
options/args: The options/args object returned by optparse
"""
if not options.branch:
options.branch = cros_build_lib.GetChromiteTrackingBranch()
if options.local_patches and not repository.IsARepoRoot(options.sourceroot):
raise Exception('Could not find repo checkout at %s!'
% options.sourceroot)
if options.local_patches:
options.local_patches = _CheckLocalPatches(
options.sourceroot, options.local_patches)
default = os.environ.get('CBUILDBOT_DEFAULT_MODE')
if (default and not any([options.local, options.buildbot,
options.remote, options.remote_trybot])):
cros_build_lib.Info("CBUILDBOT_DEFAULT_MODE=%s env var detected, using it."
% default)
default = default.lower()
if default == 'local':
options.local = True
elif default == 'remote':
options.remote = True
elif default == 'buildbot':
options.buildbot = True
else:
cros_build_lib.Die("CBUILDBOT_DEFAULT_MODE value %s isn't supported. "
% default)
def _ParseCommandLine(parser, argv):
"""Completely parse the commandline arguments"""
(options, args) = parser.parse_args(argv)
if options.output_api_version:
print _REEXEC_API_VERSION
sys.exit(0)
if options.list:
_PrintValidConfigs(options.print_all)
sys.exit(0)
# Strip out null arguments.
# TODO(rcui): Remove when buildbot is fixed
args = [arg for arg in args if arg]
if not args:
parser.error('Invalid usage. Use -h to see usage. Use -l to list '
'supported configs.')
_FinishParsing(options, args)
return options, args
def main(argv):
# Set umask to 022 so files created by buildbot are readable.
os.umask(022)
if cros_build_lib.IsInsideChroot():
cros_build_lib.Die('Please run cbuildbot from outside the chroot.')
parser = _CreateParser()
(options, args) = _ParseCommandLine(parser, argv)
_PostParseCheck(options, args)
if options.remote:
cros_build_lib.logger.setLevel(logging.WARNING)
# Verify configs are valid.
for bot in args:
_GetConfig(bot)
# Verify gerrit patches are valid.
print 'Verifying patches...'
patch_pool = AcquirePoolFromOptions(options)
# --debug need to be explicitly passed through for remote invocations.
if options.buildbot and '--debug' not in options.pass_through_args:
_ConfirmRemoteBuildbotRun()
print 'Submitting tryjob...'
tryjob = remote_try.RemoteTryJob(options, args, patch_pool.local_patches)
tryjob.Submit(testjob=options.test_tryjob, dryrun=False)
print 'Tryjob submitted!'
print ('Go to %s to view the status of your job.'
% tryjob.GetTrybotWaterfallLink())
sys.exit(0)
elif (not options.buildbot and not options.remote_trybot
and not options.resume and not options.local):
cros_build_lib.Warning(
'Running in LOCAL TRYBOT mode! Use --remote to submit REMOTE '
'tryjobs. Use --local to suppress this message.')
cros_build_lib.Warning(
'In the future, --local will be required to run the local '
'trybot.')
time.sleep(5)
# Only expecting one config
bot_id = args[-1]
build_config = _GetConfig(bot_id)
if options.reference_repo is None:
repo_path = os.path.join(options.sourceroot, '.repo')
# If we're being run from a repo checkout, reuse the repo's git pool to
# cut down on sync time.
if os.path.exists(repo_path):
options.reference_repo = options.sourceroot
elif options.reference_repo:
if not os.path.exists(options.reference_repo):
parser.error('Reference path %s does not exist'
% (options.reference_repo,))
elif not os.path.exists(os.path.join(options.reference_repo, '.repo')):
parser.error('Reference path %s does not look to be the base of a '
'repo checkout; no .repo exists in the root.'
% (options.reference_repo,))
if (options.buildbot or options.remote_trybot) and not options.resume:
if not options.cgroups:
parser.error('Options --buildbot/--remote-trybot and --nocgroups cannot '
'be used together. Cgroup support is required for '
'buildbot/remote-trybot mode.')
if not cgroups.Cgroup.CgroupsSupported():
parser.error('Option --buildbot/--remote-trybot was given, but this '
'system does not support cgroups. Failing.')
missing = []
for program in _BUILDBOT_REQUIRED_BINARIES:
ret = cros_build_lib.RunCommand(
'which %s' % program, shell=True, redirect_stderr=True,
redirect_stdout=True, error_code_ok=True, print_cmd=False)
if ret.returncode != 0:
missing.append(program)
if missing:
parser.error("Option --buildbot/--remote-trybot requires the following "
"binaries which couldn't be found in $PATH: %s"
% (', '.join(missing)))
if options.reference_repo:
options.reference_repo = os.path.abspath(options.reference_repo)
if options.dump_config:
# This works, but option ordering is bad...
print 'Configuration %s:' % bot_id
pretty_printer = pprint.PrettyPrinter(indent=2)
pretty_printer.pprint(build_config)
sys.exit(0)
if not options.buildroot:
if options.buildbot:
parser.error('Please specify a buildroot with the --buildroot option.')
options.buildroot = _DetermineDefaultBuildRoot(options.sourceroot,
build_config['internal'])
# We use a marker file in the buildroot to indicate the user has
# consented to using this directory.
if not os.path.exists(repository.GetTrybotMarkerPath(options.buildroot)):
_ConfirmBuildRoot(options.buildroot)
# Sanity check of buildroot- specifically that it's not pointing into the
# midst of an existing repo since git-repo doesn't support nesting.
if (not repository.IsARepoRoot(options.buildroot) and
repository.InARepoRepository(options.buildroot)):
parser.error('Configured buildroot %s points into a repository checkout, '
'rather than the root of it. This is not supported.'
% options.buildroot)
log_file = None
if options.tee:
default_dir = os.path.join(options.buildroot, _DEFAULT_LOG_DIR)
dirname = options.log_dir or default_dir
log_file = os.path.join(dirname, _BUILDBOT_LOG_FILE)
osutils.SafeMakedirs(dirname)
_BackupPreviousLog(log_file)
with cros_build_lib.ContextManagerStack() as stack:
critical_section = stack.Add(cleanup.EnforcedCleanupSection)
stack.Add(sudo.SudoKeepAlive)
if not options.resume:
# If we're in resume mode, use our parents tempdir rather than
# nesting another layer.
stack.Add(osutils.TempDirContextManager, 'cbuildbot-tmp')
logging.debug("Cbuildbot tempdir is %r.", os.environ.get('TMP'))
# TODO(ferringb): update this once https://gerrit.chromium.org/gerrit/25359
# is landed- it's sensitive to the manifest-versions cache path.
options.preserve_paths = set(['manifest-versions',
'manifest-versions-internal'])
if log_file is not None:
stack.Add(tee.Tee, log_file)
options.preserve_paths.add(_DEFAULT_LOG_DIR)
if options.cgroups:
stack.Add(cgroups.SimpleContainChildren, 'cbuildbot')
# Mark everything between EnforcedCleanupSection and here as having to
# be rolled back via the contextmanager cleanup handlers. This
# ensures that sudo bits cannot outlive cbuildbot, that anything
# cgroups would kill gets killed, etc.
critical_section.ForkWatchdog()
if options.timeout > 0:
stack.Add(cros_build_lib.Timeout, options.timeout)
if not options.buildbot:
build_config = cbuildbot_config.OverrideConfigForTrybot(
build_config,
options.remote_trybot)
_RunBuildStagesWrapper(options, build_config)