blob: 0bcdb2b9c730faacd98ac96315295c27825b8192 [file] [log] [blame]
#!/usr/bin/env python
# Copyright (c) 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 argparse
import json
import logging
import os
import subprocess
import sys
# Install Infra build environment.
BUILD_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(
os.path.abspath(__file__))))
sys.path.insert(0, os.path.join(BUILD_ROOT, 'scripts'))
from common import annotator
from common import chromium_utils
from common import env
from common import master_cfg_utils
from slave import logdog_bootstrap
from slave import monitoring_utils
from slave import remote_run
from slave import robust_tempdir
from slave import update_scripts
# Logging instance.
LOGGER = logging.getLogger('annotated_run')
# /b/build/slave/<slavename>/build/
BUILD_DIR = os.getcwd()
# /b/build/slave/<slavename>/
BUILDER_DIR = os.path.dirname(BUILD_DIR)
# /b
B_DIR = os.path.dirname(os.path.dirname(os.path.dirname(BUILDER_DIR)))
# ENGINE_FLAGS is a mapping of master name to a engine flags. This can be used
# to test new recipe engine flags on a select few masters.
_ENGINE_FLAGS = {
# None is the default set of engine flags, and is used if nothing else
# matches. It MUST be defined.
None: {
'use_result_proto': True,
},
'chromium.fyi': {
'use_result_proto': True,
},
}
def _get_engine_flags(mastername):
return _ENGINE_FLAGS.get(mastername, _ENGINE_FLAGS[None])
# List of bots that automatically get run through "remote_run".
_REMOTE_RUN_PASSTHROUGH_ALL = '*'
_REMOTE_RUN_PASSTHROUGH = {
'chromium.fyi': [
'ios-simulator',
],
'client.flutter': '*',
}
def _is_remote_run_passthrough(properties):
builders = _REMOTE_RUN_PASSTHROUGH.get(properties.get('mastername'))
return (builders is _REMOTE_RUN_PASSTHROUGH_ALL or
builders and properties.get('buildername') in builders)
def _build_dir():
return BUILD_DIR
def _builder_dir():
return BUILDER_DIR
def _ensure_directory(*path):
path = os.path.join(*path)
if not os.path.isdir(path):
os.makedirs(path)
return path
def _run_command(cmd, **kwargs):
if kwargs.pop('dry_run', False):
LOGGER.info('(Dry Run) Would have executed command: %s', cmd)
return 0, ''
LOGGER.debug('Executing command: %s', cmd)
kwargs.setdefault('stderr', subprocess.STDOUT)
proc = subprocess.Popen(cmd, **kwargs)
stdout, _ = proc.communicate()
LOGGER.debug('Process [%s] returned [%d] with output:\n%s',
cmd, proc.returncode, stdout)
return proc.returncode, stdout
def _check_command(cmd, **kwargs):
rv, stdout = _run_command(cmd, **kwargs)
if rv != 0:
raise subprocess.CalledProcessError(rv, cmd, output=stdout)
return stdout
def get_recipe_properties(stream, workdir, build_properties,
use_factory_properties_from_disk):
"""Constructs the recipe's properties from buildbot's properties.
This retrieves the current factory properties from the master_config
in the slave's checkout (no factory properties are handed to us from the
master), and merges in the build properties.
Using the values from the checkout allows us to do things like change
the recipe and other factory properties for a builder without needing
a master restart.
As the build properties doesn't include the factory properties, we would:
1. Load factory properties from checkout on the slave.
2. Override the factory properties with the build properties.
3. Set the factory-only properties as build properties using annotation so
that they will show up on the build page.
"""
if not use_factory_properties_from_disk:
return build_properties
with stream.step('setup_properties') as s:
factory_properties = {}
mastername = build_properties.get('mastername')
buildername = build_properties.get('buildername')
if mastername and buildername:
# Load factory properties from tip-of-tree checkout on the slave builder.
factory_properties = get_factory_properties_from_disk(
workdir, mastername, buildername)
# Check conflicts between factory properties and build properties.
conflicting_properties = {}
for name, value in factory_properties.items():
if not build_properties.has_key(name) or build_properties[name] == value:
continue
conflicting_properties[name] = (value, build_properties[name])
if conflicting_properties:
s.step_text(
'<br/>detected %d conflict[s] between factory and build properties'
% len(conflicting_properties))
conflicts = [' "%s": factory: "%s", build: "%s"' % (
name,
'<unset>' if (fv is None) else fv,
'<unset>' if (bv is None) else bv)
for name, (fv, bv) in conflicting_properties.items()]
LOGGER.warning('Conflicting factory and build properties:\n%s',
'\n'.join(conflicts))
LOGGER.warning("Will use the values from build properties.")
# Figure out the factory-only properties and set them as build properties so
# that they will show up on the build page.
for name, value in factory_properties.items():
if not build_properties.has_key(name):
s.set_build_property(name, json.dumps(value))
# Build properties override factory properties.
properties = factory_properties.copy()
properties.update(build_properties)
# Unhack buildbot-hacked blamelist (iannucci).
if ('blamelist_real' in properties and 'blamelist' in properties):
properties['blamelist'] = properties['blamelist_real']
del properties['blamelist_real']
return properties
def get_factory_properties_from_disk(workdir, mastername, buildername):
master_list = master_cfg_utils.GetMasters()
master_path = None
for name, path in master_list:
if name == mastername:
master_path = path
if not master_path:
raise LookupError('master "%s" not found.' % mastername)
script_path = os.path.join(env.Build, 'scripts', 'tools',
'dump_master_cfg.py')
master_json = os.path.join(workdir, 'dump_master_cfg.json')
dump_cmd = [sys.executable,
script_path,
master_path, master_json]
proc = subprocess.Popen(dump_cmd, cwd=env.Build,
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
out, err = proc.communicate()
if proc.returncode:
raise LookupError('Failed to get the master config; running %r in %r '
'returned exit code %d\nstdout: %s\nstderr: %s'% (
dump_cmd, env.Build, proc.returncode, out, err))
with open(master_json, 'rU') as f:
config = json.load(f)
# Now extract just the factory properties for the requested builder
# from the master config.
props = {}
found = False
for builder_dict in config['builders']:
if builder_dict['name'] == buildername:
found = True
factory_properties = builder_dict['factory']['properties']
for name, (value, _) in factory_properties.items():
props[name] = value
if not found:
raise LookupError('builder "%s" not found on in master "%s"' %
(buildername, mastername))
if 'recipe' not in props:
raise LookupError('Cannot find recipe for %s on %s' %
(buildername, mastername))
return props
def get_args(argv):
"""Process command-line arguments."""
parser = argparse.ArgumentParser(
description='Entry point for annotated builds.')
parser.add_argument('-v', '--verbose',
action='count', default=0,
help='Increase verbosity. This can be specified multiple times.')
parser.add_argument('-d', '--dry-run', action='store_true',
help='Perform the setup, but refrain from executing the recipe.')
parser.add_argument('-l', '--leak', action='store_true',
help="Refrain from cleaning up generated artifacts.")
parser.add_argument('--build-properties',
type=json.loads, default={},
help='build properties in JSON format')
parser.add_argument('--factory-properties',
type=json.loads, default={},
help='factory properties in JSON format')
parser.add_argument('--build-properties-gz', dest='build_properties',
type=chromium_utils.convert_gz_json_type, default={},
help='build properties in b64 gz JSON format')
parser.add_argument('--factory-properties-gz', dest='factory_properties',
type=chromium_utils.convert_gz_json_type, default={},
help='factory properties in b64 gz JSON format')
parser.add_argument('--keep-stdin', action='store_true', default=False,
help='don\'t close stdin when running recipe steps')
parser.add_argument('--master-overrides-slave', action='store_true',
help='use the property values given on the command line from the master, '
'not the ones looked up on the slave')
parser.add_argument('--use-factory-properties-from-disk',
action='store_true', default=False,
help='use factory properties loaded from disk on the slave')
parser.add_argument('--remote-run-passthrough', action='store_true',
help='pass through and use remote_run')
group = parser.add_argument_group('LogDog Bootstrap')
logdog_bootstrap.add_arguments(group)
return parser.parse_args(argv)
def _locate_recipe(recipe):
# Find out if the recipe we intend to run is in build_internal's recipes. If
# so, use recipes.py from there, otherwise use the one from build.
recipe_file = recipe.replace('/', os.path.sep) + '.py'
# Use the standard recipe runner unless the recipes are explicitly in the
# "build_limited" repository.
if env.BuildInternal:
build_limited = os.path.join(env.BuildInternal, 'scripts', 'slave')
if os.path.exists(os.path.join(build_limited, 'recipes', recipe_file)):
return (
os.path.join(build_limited, 'recipes.py'),
('https://chrome-internal.googlesource.com/chrome/tools/'
'build_limited/scripts/slave.git'))
# Public recipe (this repository).
return (
os.path.join(env.Build, 'scripts', 'slave', 'recipes.py'),
'https://chromium.googlesource.com/chromium/tools/build.git')
def _remote_run_passthrough(opts, properties, stream):
recipe = properties.pop('recipe')
_, recipe_repository = _locate_recipe(recipe)
with stream.step('remote_run passthrough') as s:
s.step_text('<br>'.join([
'recipe: %s' % (recipe,),
'recipe_repository: %s' % (recipe_repository,),
]))
cmd = [
'annotated_run', # argv[0]
'--repository', recipe_repository,
'--recipe', recipe,
'--build-properties-gz', chromium_utils.b64_gz_json_encode(properties),
'--canary',
'--kitchen', remote_run._CANARY_CIPD_PINS.kitchen,
]
if opts.verbose > 0:
cmd += ['--verbose']
if opts.leak:
cmd += ['--leak']
LOGGER.info('Configured for "remote_run" passthrough w/ translated argv: %s',
cmd)
return remote_run.main(cmd, stream, passthrough=True)
def _exec_recipe(rt, opts, stream, basedir, tdir, properties):
recipe_runner, _ = _locate_recipe(properties['recipe'])
# Dump properties to JSON and build recipe command.
props_file = os.path.join(tdir, 'recipe_properties.json')
with open(props_file, 'w') as fh:
json.dump(properties, fh)
engine_flags = _get_engine_flags(properties.get('mastername'))
recipe_result_path = os.path.join(tdir, 'recipe_result.json')
engine_args = []
if engine_flags:
engine_flags_path = os.path.join(tdir, 'op_args.json')
with open(engine_flags_path, 'w') as f:
json.dump({
'engine_flags': engine_flags
}, f)
engine_args = ['--operational-args-path', engine_flags_path]
recipe_cmd = [
remote_run.find_python(), '-u', recipe_runner,
] + engine_args + [
'--verbose',
'run',
'--workdir=%s' % _build_dir(),
'--properties-file=%s' % props_file,
'--output-result-json',
recipe_result_path,
properties['recipe'],
]
environ = os.environ.copy()
environ['VPYTHON_CLEAR_PYTHONPATH'] = '1'
# Default to return code != 0 is for the benefit of buildbot, which uses
# return code to decide if a step failed or not.
recipe_return_code = 1
try:
bs = logdog_bootstrap.bootstrap(rt, opts, basedir, tdir, properties,
recipe_cmd)
LOGGER.info('Bootstrapping through LogDog: %s', bs.cmd)
bs.annotate(stream)
_, _ = _run_command(bs.cmd, dry_run=opts.dry_run, env=environ)
recipe_return_code = bs.get_result()
except logdog_bootstrap.NotBootstrapped as e:
LOGGER.info('Not using LogDog. Invoking `recipes.py` directly: %s', e)
recipe_return_code, _ = _run_command(recipe_cmd, dry_run=opts.dry_run,
env=environ)
# Try to open recipe result JSON. Any failure will result in an exception
# and an infra failure.
with open(recipe_result_path, 'r') as f:
return_value = json.load(f)
if engine_flags.get('use_result_proto'):
# If we failed, but aren't a step failure, we assume it was an
# exception.
f = return_value.get('failure')
if f is not None and not f.get('step_failure'):
# The recipe engine used to return -1, which got interpreted as 255
# by os.exit in python, since process exit codes are a single byte.
recipe_return_code = 255
return recipe_return_code
def main(argv):
# We always want everything to be unbuffered so that we can see the logs on
# buildbot/logdog as soon as they're available.
os.environ['PYTHONUNBUFFERED'] = '1'
opts = get_args(argv)
if opts.verbose == 0:
level = logging.INFO
else:
level = logging.DEBUG
logging.getLogger().setLevel(level)
# Enter our runtime environment.
basedir = _builder_dir()
with robust_tempdir.RobustTempdir(
prefix='.recipe_runtime', leak=opts.leak) as rt:
tdir = rt.tempdir(base=basedir)
LOGGER.debug('Using temporary directory: [%s].', tdir)
# Load factory properties and configuration.
# TODO(crbug.com/551165): remove flag "factory_properties".
use_factory_properties_from_disk = (opts.use_factory_properties_from_disk or
bool(opts.factory_properties))
stream = annotator.StructuredAnnotationStream()
properties = get_recipe_properties(
stream, tdir, opts.build_properties, use_factory_properties_from_disk)
LOGGER.debug('Loaded properties: %s', properties)
# If this is an opt-in run, or if this builder is configured for
# passthrough, use "remote_run"!
if (_is_remote_run_passthrough(properties) or
remote_run.get_is_opt_in(properties) or
opts.remote_run_passthrough):
return _remote_run_passthrough(opts, properties, stream)
# Ensure that the CIPD client and base tooling is installed and available on
# PATH.
from slave import cipd_bootstrap_v2
cipd_bootstrap_v2.high_level_ensure_cipd_client(
B_DIR, properties.get('mastername'))
# Setup monitoring directory and send a monitoring event.
build_data_dir = _ensure_directory(tdir, 'build_data')
properties['build_data_dir'] = build_data_dir
# path_config property defines what paths a build uses for checkout, git
# cache, goma cache, etc.
# Unless it is explicitly specified by a builder, use paths for buildbot
# environment.
properties['path_config'] = properties.get('path_config', 'buildbot')
properties['bot_id'] = properties['slavename']
properties['builder_id'] = (
'master.{mastername}:{buildername}'.format(**properties))
properties['build_id'] = (
'buildbot/{mastername}/{buildername}/{buildnumber}'.format(
**properties))
remote_run.set_recipe_runtime_properties(stream, opts, properties)
LOGGER.info('Using properties: %r', properties)
# Write our annotated_run.py monitoring event.
monitoring_utils.write_build_monitoring_event(build_data_dir, properties)
# Cleanup system and temporary directories.
from slave import cleanup_temp
cleanup_temp.Cleanup(B_DIR)
# Create a cleanup directory so that the recipe engine can assume that it
# exists.
_ensure_directory(_builder_dir(), 'build.dead')
# Execute our recipe.
return _exec_recipe(rt, opts, stream, basedir, tdir, properties)
def shell_main(argv):
if update_scripts.update_scripts():
# Re-execute with the updated annotated_run.py.
rv, _ = _run_command([sys.executable] + argv)
return rv
return main(argv[1:])
if __name__ == '__main__':
logging.basicConfig(level=logging.INFO)
sys.exit(shell_main(sys.argv))