blob: 8e01d0ced52935ab3e61adb38048f27f5ccfe4a6 [file] [log] [blame]
#!/usr/bin/env python
# Copyright (c) 2012 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.
"""A tool to build chrome, executed by buildbot.
When this is run, the current directory (cwd) should be the outer build
directory (e.g., chrome-release/build/).
For a list of command-line options, call this script with '--help'.
"""
import multiprocessing
import optparse
import os
import re
import signal
import subprocess
import sys
from common import chromium_utils
from slave import build_directory
from slave import goma_utils
# Define a bunch of directory paths (same as bot_update.py)
CURRENT_DIR = os.path.abspath(os.getcwd())
BUILDER_DIR = os.path.dirname(CURRENT_DIR)
SLAVE_DIR = os.path.dirname(BUILDER_DIR)
# GOMA_CACHE_DIR used for caching long-term data.
DEFAULT_GOMA_CACHE_DIR = os.path.join(SLAVE_DIR, 'goma_cache')
# Path of the scripts/slave/ checkout on the slave, found by looking at the
# current compile.py script's path's dirname().
SLAVE_SCRIPTS_DIR = os.path.dirname(os.path.abspath(__file__))
# Path of the build/ checkout on the slave, found relative to the
# scripts/slave/ directory.
BUILD_DIR = os.path.dirname(os.path.dirname(SLAVE_SCRIPTS_DIR))
class EchoDict(dict):
"""Dict that remembers all modified values."""
def __init__(self, *args, **kwargs):
self.overrides = set()
self.adds = set()
super(EchoDict, self).__init__(*args, **kwargs)
def __setitem__(self, key, val):
if not key in self and not key in self.overrides:
self.adds.add(key)
self.overrides.add(key)
super(EchoDict, self).__setitem__(key, val)
def __delitem__(self, key):
self.overrides.add(key)
if key in self.adds:
self.adds.remove(key)
self.overrides.remove(key)
super(EchoDict, self).__delitem__(key)
def print_overrides(self, fh=None):
if not self.overrides:
return
if not fh:
fh = sys.stdout
fh.write('Environment variables modified in compile.py:\n')
for k in sorted(list(self.overrides)):
if k in self:
fh.write(' %s=%s\n' % (k, self[k]))
else:
fh.write(' %s (removed)\n' % k)
fh.write('\n')
def StopGomaClientAndUploadInfo(options, env, exit_status):
"""Stop goma compiler_proxy and upload goma-related information.
Args:
options (Option) : options to specify where to store goma-related info.
env (dict) : used when goma_ctl command executes.
exit_status (int): exit_status sent to monitoring system.
"""
goma_ctl_cmd = [sys.executable,
os.path.join(options.goma_dir, 'goma_ctl.py')]
if options.goma_jsonstatus:
chromium_utils.RunCommand(
goma_ctl_cmd + ['jsonstatus', options.goma_jsonstatus], env=env)
goma_utils.SendGomaTsMon(options.goma_jsonstatus, exit_status,
builder=options.buildbot_buildername,
master=options.buildbot_mastername,
slave=options.buildbot_slavename,
clobber=options.buildbot_clobber)
# If goma compiler_proxy crashes, there could be crash dump.
if options.build_data_dir:
env['GOMACTL_CRASH_REPORT_ID_FILE'] = os.path.join(options.build_data_dir,
'crash_report_id_file')
# We must stop the proxy to dump GomaStats.
chromium_utils.RunCommand(goma_ctl_cmd + ['stop'], env=env)
override_gsutil = None
if options.gsutil_py_path:
# Needs to add '--', otherwise gsutil options will be passed to gsutil.py.
override_gsutil = [sys.executable, options.gsutil_py_path, '--']
goma_utils.UploadGomaCompilerProxyInfo(override_gsutil=override_gsutil,
builder=options.buildbot_buildername,
master=options.buildbot_mastername,
slave=options.buildbot_slavename,
clobber=options.buildbot_clobber)
# Upload GomaStats to make it monitored.
if env.get('GOMA_DUMP_STATS_FILE'):
goma_utils.SendGomaStats(env['GOMA_DUMP_STATS_FILE'],
env.get('GOMACTL_CRASH_REPORT_ID_FILE'),
options.build_data_dir)
# TODO(tikuta): move to goma_utils.py
def goma_setup(options, env):
"""Sets up goma if necessary.
If using the Goma compiler, first call goma_ctl to ensure the proxy is
available, and returns (True, instance of cloudtail subprocess).
If it failed to start up compiler_proxy, modify options.compiler
and options.goma_dir, modify env to GOMA_DISABLED=true,
and returns (False, None).
"""
cloudtail_pid_file = options.cloudtail_pid_file
if cloudtail_pid_file and os.path.exists(cloudtail_pid_file):
os.remove(cloudtail_pid_file)
if options.compiler not in ('goma', 'goma-clang'):
# Unset goma_dir to make sure we'll not use goma.
options.goma_dir = None
return False, None
if options.goma_fail_fast:
# startup fails when initial ping failed.
env['GOMA_FAIL_FAST'] = 'true'
else:
# If a network error continues 30 minutes, compiler_proxy make the compile
# failed. When people use goma, they expect using goma is faster than
# compile locally. If goma cannot guarantee that, let it make compile
# as error.
env['GOMA_ALLOWED_NETWORK_ERROR_DURATION'] = '1800'
# Caches CRLs in GOMA_CACHE_DIR.
# Since downloading CRLs is usually slow, caching them may improves
# compiler_proxy start time.
if not os.path.exists(options.goma_cache_dir):
os.mkdir(options.goma_cache_dir, 0700)
env['GOMA_CACHE_DIR'] = options.goma_cache_dir
# Enable DepsCache. DepsCache caches the list of files to send goma server.
# This will greatly improve build speed when cache is warmed.
# The cache file is stored in the target output directory.
env['GOMA_DEPS_CACHE_DIR'] = (
options.goma_deps_cache_dir or options.target_output_dir)
if options.goma_hermetic:
env['GOMA_HERMETIC'] = options.goma_hermetic
if options.goma_enable_remote_link:
env['GOMA_ENABLE_REMOTE_LINK'] = options.goma_enable_remote_link
if options.goma_store_local_run_output:
env['GOMA_STORE_LOCAL_RUN_OUTPUT'] = options.goma_store_local_run_output
if options.build_data_dir:
env['GOMA_DUMP_STATS_FILE'] = os.path.join(options.build_data_dir,
'goma_stats_proto')
if options.goma_service_account_json_file:
env['GOMA_SERVICE_ACCOUNT_JSON_FILE'] = \
options.goma_service_account_json_file
goma_start_command = ['restart']
goma_ctl_cmd = [sys.executable,
os.path.join(options.goma_dir, 'goma_ctl.py')]
result = chromium_utils.RunCommand(goma_ctl_cmd + goma_start_command, env=env)
if not result:
# goma started sucessfully.
# Making cloudtail to upload the latest log.
# TODO(yyanagisawa): install cloudtail from CIPD.
cloudtail_path = '/opt/infra-tools/cloudtail'
if chromium_utils.IsWindows():
cloudtail_path = 'C:\\infra-tools\\cloudtail'
try:
cloudtail_proc = subprocess.Popen(
[cloudtail_path, 'tail', '--log-id', 'goma_compiler_proxy', '--path',
goma_utils.GetLatestGomaCompilerProxyInfo()])
if cloudtail_pid_file:
with open(cloudtail_pid_file,'w') as f:
f.write('%d' % cloudtail_proc.pid)
except Exception as e:
print 'failed to invoke cloudtail: %s' % e
return True, None
return True, cloudtail_proc
StopGomaClientAndUploadInfo(options, env, -1)
if options.goma_disable_local_fallback:
print 'error: failed to start goma; fallback has been disabled'
raise Exception('failed to start goma')
print 'warning: failed to start goma. falling back to non-goma'
# Drop goma from options.compiler
options.compiler = options.compiler.replace('goma-', '')
if options.compiler == 'goma':
options.compiler = None
# Reset options.goma_dir.
options.goma_dir = None
env['GOMA_DISABLED'] = '1'
return False, None
# TODO(tikuta): move to goma_utils.py
def goma_teardown(options, env, exit_status, cloudtail_proc):
"""Tears down goma if necessary. """
cloudtail_pid_file = options.cloudtail_pid_file
if options.goma_dir:
StopGomaClientAndUploadInfo(options, env, exit_status)
if cloudtail_proc:
cloudtail_proc.terminate()
cloudtail_proc.wait()
elif cloudtail_pid_file:
with open(cloudtail_pid_file) as f:
pid = int(f.read())
os.kill(pid, signal.SIGTERM)
os.remove(cloudtail_pid_file)
def maybe_set_official_build_envvars(options, env):
if options.mode == 'google_chrome' or options.mode == 'official':
env['CHROMIUM_BUILD'] = '_google_chrome'
if options.mode == 'official':
# Official builds are always Google Chrome.
env['CHROME_BUILD_TYPE'] = '_official'
class EnsureUpToDateFilter(chromium_utils.RunCommandFilter):
"""Filter for RunCommand that checks whether the output contains ninja's
message for a no-op build."""
def __init__(self):
self.was_up_to_date = False
def FilterLine(self, a_line):
if 'ninja: no work to do.' in a_line:
self.was_up_to_date = True
return a_line
def NeedEnvFileUpdateOnWin(env):
"""Returns true if environment file need to be updated."""
# Following GOMA_* are applied to compiler_proxy not gomacc,
# you do not need to update environment files.
ignore_envs = (
'GOMA_API_KEY_FILE',
'GOMA_DEPS_CACHE_DIR',
'GOMA_HERMETIC',
'GOMA_RPC_EXTRA_PARAMS',
'GOMA_ALLOWED_NETWORK_ERROR_DURATION'
)
for key in env.overrides:
if key not in ignore_envs:
return True
return False
def UpdateWindowsEnvironment(envfile_dir, env):
"""Update windows environment in environment.{x86,x64}.
Args:
envfile_dir: a directory name environment.{x86,x64} are stored.
env: an instance of EchoDict that represents environment.
"""
# envvars_to_save come from _ExtractImportantEnvironment in
# https://chromium.googlesource.com/external/gyp/+/\
# master/pylib/gyp/msvs_emuation.py
# You must update this when the original code is updated.
envvars_to_save = (
'goma_.*', # TODO(scottmg): This is ugly, but needed for goma.
'include',
'lib',
'libpath',
'path',
'pathext',
'systemroot',
'temp',
'tmp',
)
env_to_store = {}
for envvar in envvars_to_save:
compiled = re.compile(envvar, re.IGNORECASE)
for key in env.overrides:
if compiled.match(key):
if envvar == 'path':
env_to_store[key] = (os.path.dirname(sys.executable) +
os.pathsep + env[key])
else:
env_to_store[key] = env[key]
if not env_to_store:
return
nul = '\0'
for arch in ['x86', 'x64']:
path = os.path.join(envfile_dir, 'environment.%s' % arch)
print '%s will be updated with %s.' % (path, env_to_store)
env_in_file = {}
with open(path) as f:
for entry in f.read().split(nul):
if not entry:
continue
key, value = entry.split('=', 1)
env_in_file[key] = value
env_in_file.update(env_to_store)
with open(path, 'wb') as f:
f.write(nul.join(['%s=%s' % (k, v) for k, v in env_in_file.iteritems()]))
f.write(nul * 2)
# TODO(tikuta): move to goma_utils
def determine_goma_jobs():
# We would like to speed up build on Windows a bit, since it is slowest.
number_of_processors = 0
try:
number_of_processors = multiprocessing.cpu_count()
except NotImplementedError:
print 'cpu_count() is not implemented, using default value 50.'
return 50
assert number_of_processors > 0
# When goma is used, 10 * number_of_processors is basically good in
# various situations according to our measurement. Build speed won't
# be improved if -j is larger than that.
#
# Since Mac had process number limitation before, we had to set
# the upper limit to 50. Now that the process number limitation is 2000,
# so we would be able to use 10 * number_of_processors.
# For the safety, we'd like to set the upper limit to 200.
#
# Note that currently most try-bot build slaves have 8 processors.
if chromium_utils.IsMac() or chromium_utils.IsWindows():
return min(10 * number_of_processors, 200)
# For Linux, we also would like to use 10 * cpu. However, not sure
# backend resource is enough, so let me set Linux and Linux x64 builder
# only for now.
hostname = goma_utils.GetShortHostname()
if hostname in (
['build14-m1', 'build48-m1'] +
# Also increasing cpus for v8/blink trybots.
['build%d-m4' % x for x in xrange(45, 48)] +
# Also increasing cpus for LTO buildbots.
['slave%d-c1' % x for x in [20, 33] + range(78, 108)]):
return min(10 * number_of_processors, 200)
return 50
def main_ninja(options, args, env):
"""This function calls ninja.
Args:
options (Option): options for ninja command.
args (str): extra args for ninja command.
env (dict): Used when ninja command executes.
Returns:
int: ninja command exit status.
"""
exit_status = -1
try:
print 'chdir to %s' % options.src_dir
os.chdir(options.src_dir)
command = [options.ninja_path, '-w', 'dupbuild=err',
'-C', options.target_output_dir]
# HACK(yyanagisawa): update environment files on |env| update.
# For compiling on Windows, environment in environment files are used.
# It means even if enviroment such as GOMA_DISABLED is updated in
# compile.py, the update will be ignored.
# We need to update environment files to reflect the update.
if chromium_utils.IsWindows() and NeedEnvFileUpdateOnWin(env):
print 'Updating environment.{x86,x64} files.'
UpdateWindowsEnvironment(options.target_output_dir, env)
if options.clobber:
print 'Removing %s' % options.target_output_dir
# Deleting output_dir would also delete all the .ninja files necessary to
# build. Clobbering should run before runhooks (which creates .ninja
# files). For now, only delete all non-.ninja files.
# TODO(thakis): Make "clobber" a step that runs before "runhooks".
# Once the master has been restarted, remove all clobber handling
# from compile.py, https://crbug.com/574557
build_directory.RmtreeExceptNinjaOrGomaFiles(options.target_output_dir)
command.extend(options.build_args)
command.extend(args)
maybe_set_official_build_envvars(options, env)
if options.compiler:
print 'using', options.compiler
if options.compiler in ('goma', 'goma-clang'):
assert options.goma_dir
assert options.goma_jobs
command.append('-j%d' % options.goma_jobs)
# Run the build.
env.print_overrides()
exit_status = chromium_utils.RunCommand(command, env=env)
if exit_status == 0 and options.ninja_ensure_up_to_date:
# Run the build again if we want to check that the no-op build is clean.
filter_obj = EnsureUpToDateFilter()
# Append `-d explain` to help diagnose in the failure case.
command += ['-d', 'explain']
chromium_utils.RunCommand(command, env=env, filter_obj=filter_obj)
if not filter_obj.was_up_to_date:
print 'Failing build because ninja reported work to do.'
print 'This means that after completing a compile, another was run and'
print 'it resulted in still having work to do (that is, a no-op build'
print 'wasn\'t a no-op). Consult the first "ninja explain:" line for a'
print 'likely culprit.'
return 1
return exit_status
finally:
override_gsutil = None
if options.gsutil_py_path:
# Needs to add '--', otherwise gsutil options will be passed to gsutil.py.
override_gsutil = [sys.executable, options.gsutil_py_path, '--']
goma_utils.UploadNinjaLog(
options.target_output_dir, options.compiler, command, exit_status,
override_gsutil=override_gsutil)
def get_target_build_dir(options):
"""Keep this function in sync with src/build/landmines.py"""
if chromium_utils.IsLinux() and options.cros_board:
# When building ChromeOS's Simple Chrome workflow, the output directory
# has a CROS board name suffix.
outdir = 'out_%s' % (options.cros_board,)
elif options.out_dir:
outdir = options.out_dir
else:
outdir = 'out'
return os.path.abspath(os.path.join(options.src_dir, outdir, options.target))
def get_parsed_options():
option_parser = optparse.OptionParser()
option_parser.add_option('--clobber', action='store_true', default=False,
help='delete the output directory before compiling')
option_parser.add_option('--target', default='Release',
help='build target (Debug or Release)')
option_parser.add_option('--src-dir', default=None,
help='path to the root of the source tree')
option_parser.add_option('--mode', default='dev',
help='build mode (dev or official) controlling '
'environment variables set during build')
# TODO(thakis): Remove this, https://crbug.com/622768
option_parser.add_option('--build-tool', default=None, help='ignored')
option_parser.add_option('--build-args', action='append', default=[],
help='arguments to pass to the build tool')
option_parser.add_option('--build-data-dir', action='store',
help='specify a build data directory.')
option_parser.add_option('--compiler', default=None,
help='specify alternative compiler (e.g. clang)')
if chromium_utils.IsLinux():
option_parser.add_option('--cros-board', action='store',
help='If building for the ChromeOS Simple Chrome '
'workflow, the name of the ChromeOS board.')
option_parser.add_option('--out-dir', action='store',
help='Specify a custom output directory.')
option_parser.add_option('--goma-dir',
default=os.path.join(BUILD_DIR, 'goma'),
help='specify goma directory')
option_parser.add_option('--goma-cache-dir',
default=DEFAULT_GOMA_CACHE_DIR,
help='specify goma cache directory')
option_parser.add_option('--goma-deps-cache-dir',
help='specify goma deps cache directory')
option_parser.add_option('--goma-hermetic', default='error',
help='Set goma hermetic mode')
option_parser.add_option('--goma-enable-remote-link', default=None,
help='Enable goma remote link.')
option_parser.add_option('--goma-store-local-run-output', default=None,
help='Store local run output to goma servers.')
option_parser.add_option('--goma-fail-fast', action='store_true')
option_parser.add_option('--goma-disable-local-fallback', action='store_true')
option_parser.add_option('--goma-jsonstatus',
help='Specify a file to dump goma_ctl jsonstatus.')
option_parser.add_option('--goma-service-account-json-file',
help='Specify a file containing goma service account'
' credentials')
option_parser.add_option('--goma-jobs', default=None,
help='The number of jobs for ninja -j.')
option_parser.add_option('--gsutil-py-path',
help='Specify path to gsutil.py script '
'in depot_tools.')
option_parser.add_option('--ninja-path', default='ninja',
help='Specify path to the ninja tool.')
option_parser.add_option('--ninja-ensure-up-to-date', action='store_true',
help='Checks the output of the ninja builder to '
'confirm that a second compile immediately '
'the first is a no-op.')
option_parser.add_option('--cloudtail-pid-file', default=None,
help='Specify a file to store pid of cloudtail')
# Arguments to pass buildbot properties.
option_parser.add_option('--buildbot-buildername', default='unknown',
help='buildbot buildername')
option_parser.add_option('--buildbot-mastername', default='unknown',
help='buildbot mastername')
option_parser.add_option('--buildbot-slavename', default='unknown',
help='buildbot slavename')
option_parser.add_option('--buildbot-clobber', help='buildbot clobber')
options, args = option_parser.parse_args()
if not options.src_dir:
options.src_dir = 'src'
options.src_dir = os.path.abspath(options.src_dir)
options.target_output_dir = get_target_build_dir(options)
assert options.build_tool in (None, 'ninja')
return options, args
def real_main():
options, args = get_parsed_options()
# Prepare environment.
env = EchoDict(os.environ)
# start goma
goma_ready, goma_cloudtail = goma_setup(options, env)
if not goma_ready:
assert options.compiler not in ('goma', 'goma-clang')
assert options.goma_dir is None
elif options.goma_jobs is None:
options.goma_jobs = determine_goma_jobs()
# build
exit_status = main_ninja(options, args, env)
# stop goma
goma_teardown(options, env, exit_status, goma_cloudtail)
return exit_status
if '__main__' == __name__:
sys.exit(real_main())