blob: 41e06aa70b1f25178b424737046ebff1400acf4e [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 datetime
import errno
import getpass
import glob
import gzip
import json
import multiprocessing
import optparse
import os
import re
import shlex
import shutil
import socket
import sys
import tempfile
import time
from common import chromium_utils
from slave import build_directory
from slave import slave_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.
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))
# The Google Cloud Storage bucket to store logs related to goma.
GOMA_LOG_GS_BUCKET = 'chrome-goma-log'
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 ReadHKLMValue(path, value):
"""Retrieve the install path from the registry for Visual Studio 8.0 and
Incredibuild."""
# Only available on Windows.
# pylint: disable=F0401
import win32api, win32con
try:
regkey = win32api.RegOpenKeyEx(win32con.HKEY_LOCAL_MACHINE, path, 0,
win32con.KEY_READ)
value = win32api.RegQueryValueEx(regkey, value)[0]
win32api.RegCloseKey(regkey)
return value
except win32api.error:
return None
def GetShortHostname():
"""Get this machine's short hostname in lower case."""
return socket.gethostname().split('.')[0].lower()
def goma_setup(options, env):
"""Sets up goma if necessary.
If using the Goma compiler, first call goma_ctl with ensure_start
(or restart in clobber mode) to ensure the proxy is available, and returns
True.
If it failed to start up compiler_proxy, modify options.compiler and
options.goma_dir and returns False
"""
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
hostname = GetShortHostname()
# HACK(shinyak, yyanagisawa, goma): Windows NO_NACL_GOMA (crbug.com/390764)
# Building NaCl untrusted code using goma brings large performance
# improvement but it sometimes cause build failure by race condition.
# Let me enable goma build on goma canary buildslaves to confirm the issue
# has been fixed by a workaround.
# vm*-m4 are trybots. build*-m1 and vm*-m1 are all goma canary bots.
if hostname in ['build28-m1', 'build58-m1', 'vm191-m1', 'vm480-m1',
'vm820-m1', 'vm821-m1', 'vm848-m1']:
env['NO_NACL_GOMA'] = 'false'
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(GOMA_CACHE_DIR):
os.mkdir(GOMA_CACHE_DIR, 0700)
env['GOMA_CACHE_DIR'] = 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.target_output_dir
if not env.get('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.goma_dump_stats_file:
env['GOMA_DUMP_STATS_FILE'] = options.goma_dump_stats_file
# goma is requested.
goma_key = os.path.join(options.goma_dir, 'goma.key')
if os.path.exists(goma_key):
env['GOMA_API_KEY_FILE'] = goma_key
if chromium_utils.IsWindows():
env['GOMA_RPC_EXTRA_PARAMS'] = '?win'
goma_start_command = ['restart'] if options.clobber else ['ensure_start']
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.
return True
if options.goma_jsonstatus:
chromium_utils.RunCommand(
goma_ctl_cmd + ['jsonstatus', options.goma_jsonstatus], env=env)
# Upload compiler_proxy.INFO to investigate the reason of compiler_proxy
# start-up failure.
UploadGomaCompilerProxyInfo()
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
def GetGomaTmpDirectory():
"""Get goma's temp directory."""
candidates = ['GOMA_TMP_DIR', 'TEST_TMPDIR', 'TMPDIR', 'TMP']
for candidate in candidates:
value = os.environ.get(candidate)
if value and os.path.isdir(value):
return value
return '/tmp'
def GetLatestGomaCompilerProxyInfo():
"""Get a filename of the latest goma comiler_proxy.INFO."""
dirname = GetGomaTmpDirectory()
info_pattern = os.path.join(dirname, 'compiler_proxy.*.INFO.*')
candidates = glob.glob(info_pattern)
if not candidates:
return
return sorted(candidates, reverse=True)[0]
def UploadToGomaLogGS(file_path, gs_filename, text_to_append=None):
"""Upload a file to Google Cloud Storage (gs://chrome-goma-log).
Note that the uploaded file would automatically be gzip compressed.
Args:
file_path: a path of a file to be uploaded.
gs_filename: a name of a file in Google Storage.
text_to_append: an addtional text to be added to a file in GS.
Returns:
a stored path name without the bucket name in GS.
"""
hostname = GetShortHostname()
today = datetime.datetime.utcnow().date()
log_path = '%s/%s/%s.gz' % (
today.strftime('%Y/%m/%d'), hostname, gs_filename)
gs_path = 'gs://%s/%s' % (GOMA_LOG_GS_BUCKET, log_path)
temp = tempfile.NamedTemporaryFile(delete=False)
try:
with temp as f_out:
with gzip.GzipFile(fileobj=f_out) as gzipf_out:
with open(file_path) as f_in:
shutil.copyfileobj(f_in, gzipf_out)
if text_to_append:
gzipf_out.write(text_to_append)
slave_utils.GSUtilCopy(temp.name, gs_path)
print "Copied log file to %s" % gs_path
finally:
os.remove(temp.name)
return log_path
def UploadGomaCompilerProxyInfo():
"""Upload goma compiler_proxy.INFO to Google Storage."""
latest_info = GetLatestGomaCompilerProxyInfo()
# Since a filename of compiler_proxy.INFO is fairly unique,
# we might be able to upload it as-is.
log_path = UploadToGomaLogGS(latest_info, os.path.basename(latest_info))
viewer_url = ('http://chromium-build-stats.appspot.com/compiler_proxy_log/'
+ log_path)
print 'Visualization at %s' % viewer_url
def goma_teardown(options, env):
"""Tears down goma if necessary. """
if (options.compiler in ('goma', 'goma-clang') and
options.goma_dir):
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)
# Always stop the proxy for now to allow in-place update.
chromium_utils.RunCommand(goma_ctl_cmd + ['stop'], env=env)
UploadGomaCompilerProxyInfo()
def UploadNinjaLog(options, command, exit_status):
"""Upload .ninja_log to Google Cloud Storage (gs://chrome-goma-log),
in the same folder with goma's compiler_proxy.INFO.
Args:
options: compile.py's options.
command: command line.
exit_status: ninja's exit status.
"""
ninja_log_path = os.path.join(options.target_output_dir, '.ninja_log')
try:
st = os.stat(ninja_log_path)
mtime = datetime.datetime.fromtimestamp(st.st_mtime)
except OSError, e:
print e
return
cwd = os.getcwd()
platform = chromium_utils.PlatformName()
info = {'cmdline': command,
'cwd': cwd,
'platform': platform,
'exit': exit_status,
'argv': sys.argv,
'env': {}}
for k, v in os.environ.iteritems():
info['env'][k] = v
if options.compiler:
info['compiler'] = options.compiler
compiler_proxy_info = GetLatestGomaCompilerProxyInfo()
if compiler_proxy_info:
info['compiler_proxy_info'] = compiler_proxy_info
username = getpass.getuser()
hostname = GetShortHostname()
pid = os.getpid()
ninja_log_filename = 'ninja_log.%s.%s.%s.%d' % (
hostname, username, mtime.strftime('%Y%m%d-%H%M%S'), pid)
additional_text = '# end of ninja log\n' + json.dumps(info)
log_path = UploadToGomaLogGS(
ninja_log_path, ninja_log_filename, additional_text)
viewer_url = 'http://chromium-build-stats.appspot.com/ninja_log/' + log_path
print 'Visualization at %s' % viewer_url
def common_xcode_settings(command, options, env, compiler=None):
"""
Sets desirable Mac environment variables and command-line options
that are common to the Xcode builds.
"""
compiler = options.compiler
assert compiler in (None, 'clang', 'goma', 'goma-clang')
if compiler == 'goma':
print 'using goma'
assert options.goma_dir
command.insert(0, '%s/goma-xcodebuild' % options.goma_dir)
return
cc = None
ldplusplus = None
src_path = os.path.dirname(options.build_dir)
if compiler in ('clang', 'goma-clang'):
clang_bin_dir = os.path.abspath(os.path.join(
src_path, 'third_party', 'llvm-build', 'Release+Asserts', 'bin'))
cc = os.path.join(clang_bin_dir, 'clang')
ldplusplus = os.path.join(clang_bin_dir, 'clang++')
if compiler == 'goma-clang':
print 'using goma-clang'
if options.clobber:
# Disable compiles on local machine. When the goma server-side object
# file cache is warm, this can speed up clobber builds by up to 30%.
env['GOMA_USE_LOCAL'] = '0'
assert options.goma_dir
command.insert(0, '%s/goma-xcodebuild' % options.goma_dir)
if cc:
print 'Forcing CC = %s' % cc
env['CC'] = cc
if ldplusplus:
print 'Forcing LDPLUSPLUS = %s' % ldplusplus
env['LDPLUSPLUS'] = ldplusplus
# RunCommandFilter for xcodebuild
class XcodebuildFilter(chromium_utils.RunCommandFilter):
"""xcodebuild filter"""
# This isn't a full on state machine because there are some builds that
# invoke xcodebuild as part of a target action. Instead it relies on
# the consistent format that Xcode uses for its steps. The output follows
# the pattern of:
# 1. a section line for the target
# 2. a "header" for each thing done (Compile, PhaseScriptExecution, etc.)
# 3. all the commands under that step (cd, setenv, /Developer/usr/bin/gcc,
# etc.)
# 4. a blank line
# 5. any raw output from the command for the step
# [loop to 2 for each thing on this target]
# [loop to 1 for each target]
# 6. "** BUILD SUCCEEDED **" or "** BUILD FAILED **". If the build failed,
# an epilog of:
# "The following build commands failed:"
# [target_name]:
# [header(s) from #3, but with a full path in some cases]
# "(## failure[s])"
# So this filter works by watching for some common strings that mark the
# start of a "section" and buffers or sending on as needed.
# Enum for the current mode.
class LineMode(object):
# Class has no __init__ method
# pylint: disable=W0232
BufferAsCommand, Unbuffered, DroppingFailures = range(3)
# Enum for output types.
class LineType(object):
# Class has no __init__ method
# pylint: disable=W0232
Header, Command, Info, Raw = range(4)
section_regex = re.compile('^=== BUILD (NATIVE|AGGREGATE) TARGET (.+) OF '
'PROJECT (.+) WITH CONFIGURATION (.+) ===\n$')
section_replacement = r'====Building \3:\2 (\4)\n'
step_headers = (
'CompileC',
'CompileXIB',
'CopyPlistFile',
'CopyPNGFile',
'CopyStringsFile',
'CpResource',
'CreateUniversalBinary',
'Distributed-CompileC',
'GenerateDSYMFile',
'Ld',
'Libtool',
'PBXCp',
'PhaseScriptExecution',
'ProcessInfoPlistFile',
'Preprocess',
'ProcessPCH',
'ProcessPCH++',
'Strip',
'Stripping',
'Touch',
)
# Put an space on the end of the headers since that is how they should
# actually appear in the output line.
step_headers = tuple([x + ' ' for x in step_headers])
lines_to_drop = (
'Check dependencies\n',
)
gyp_info_lines = (
# GYP rules use make for inputs/outputs, so if no work is done, this is
# output. If this all that shows up, not much point in showing the command
# in the log, just show this to show the rules did nothing.
'make: Nothing to be done for `all\'.\n',
)
gyp_info_prefixes = (
# These are for Xcode's ui to show while work is being done, if this is
# the only output, don't bother showing the command.
'note: ',
)
failures_start = 'The following build commands failed:\n'
failures_end_regex = re.compile('^\\([0-9]+ failures?\\)\n$')
def __init__(self, full_log_file=None):
# super
chromium_utils.RunCommandFilter.__init__(self)
self.line_mode = XcodebuildFilter.LineMode.Unbuffered
self.full_log_file = full_log_file
# self.ResetPushed() does the real rest, by pylint doesn't like them being
# 'defined' outside of __init__.
self.pushed_commands = None
self.pushed_infos = None
self.to_go = None
self.ResetPushed()
def ResetPushed(self):
"""Clear out all pushed output"""
self.pushed_commands = ''
self.pushed_infos = ''
self.to_go = None
def PushLine(self, line_type, a_line):
"""Queues up a line for output into the right buffer."""
# Only expect one push per line filtered/processed, so to_go should always
# be empty anytime this is called.
assert self.to_go is None
if line_type == XcodebuildFilter.LineType.Header:
self.to_go = a_line
# Anything in commands or infos was from previous block, so clear the
# commands but leave the infos, that way they the shortened output will
# be returned for this step.
self.pushed_commands = ''
elif line_type == XcodebuildFilter.LineType.Command:
# Infos should never come before commands.
assert self.pushed_infos == ''
self.pushed_commands += a_line
elif line_type == XcodebuildFilter.LineType.Info:
self.pushed_infos += a_line
elif line_type == XcodebuildFilter.LineType.Raw:
self.to_go = a_line
def AssembleOutput(self):
"""If there is any output ready to go, all the buffered bits are glued
together and returned."""
if self.to_go is None:
return None
result = self.pushed_commands + self.pushed_infos + self.to_go
self.ResetPushed()
return result
def ProcessLine(self, a_line):
"""Looks at the line and current mode, pushing anything needed into the
pipeline for output."""
# Look for section or step headers.
section_match = self.section_regex.match(a_line)
if section_match:
self.line_mode = XcodebuildFilter.LineMode.Unbuffered
self.PushLine(XcodebuildFilter.LineType.Header,
section_match.expand(self.section_replacement))
return
if a_line.startswith(self.step_headers):
self.line_mode = XcodebuildFilter.LineMode.BufferAsCommand
# Just report the step and the output file (first two things), helps
# makes the warnings/errors stick out more.
parsed = shlex.split(a_line)
if len(parsed) >= 2:
a_line = '%s %s\n' % (parsed[0], parsed[1])
self.PushLine(XcodebuildFilter.LineType.Header, '____' + a_line)
return
# Remove the ending summary about failures since that seems to confuse some
# folks looking at logs (the data is all inline when it happened).
if self.line_mode == XcodebuildFilter.LineMode.Unbuffered and \
a_line == self.failures_start:
self.line_mode = XcodebuildFilter.LineMode.DroppingFailures
# Push an empty string for output to flush any info lines.
self.PushLine(XcodebuildFilter.LineType.Raw, '')
return
if self.line_mode == XcodebuildFilter.LineMode.DroppingFailures:
if self.failures_end_regex.match(a_line):
self.line_mode = XcodebuildFilter.LineMode.Unbuffered
return
# Wasn't a header, direct the line based on the mode the filter is in.
if self.line_mode == XcodebuildFilter.LineMode.BufferAsCommand:
# Blank line moves to unbuffered.
if a_line == '\n':
self.line_mode = XcodebuildFilter.LineMode.Unbuffered
else:
self.PushLine(XcodebuildFilter.LineType.Command, a_line)
return
# By design, GYP generates some lines of output all the time. Save them
# off as info lines so if they are the only output the command lines can
# be skipped.
if (a_line in self.gyp_info_lines) or \
a_line.startswith(self.gyp_info_prefixes):
self.PushLine(XcodebuildFilter.LineType.Info, a_line)
return
# Drop lines that are pure noise in the logs and never wanted.
if (a_line == '\n') or (a_line in self.lines_to_drop):
return
# It's a keeper!
self.PushLine(XcodebuildFilter.LineType.Raw, a_line)
def FilterLine(self, a_line):
"""Called by RunCommand for each line of output."""
# Log it
if self.full_log_file:
self.full_log_file.write(a_line)
# Process it
self.ProcessLine(a_line)
# Return what ever we've got
return self.AssembleOutput()
def FilterDone(self, last_bits):
"""Called by RunCommand when the command is done."""
# last_bits will be anything after the last newline, send it on raw to
# flush out anything.
self.PushLine(XcodebuildFilter.LineType.Raw, last_bits)
return self.AssembleOutput()
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'
def main_xcode(options, args):
"""Interprets options, clobbers object files, and calls xcodebuild.
"""
env = EchoDict(os.environ)
goma_ready = goma_setup(options, env)
if not goma_ready:
assert options.compiler not in ('goma', 'goma-clang')
assert options.goma_dir is None
# Print some basic information about xcodebuild to ease debugging.
chromium_utils.RunCommand(['xcodebuild', '-sdk', '-version'], env=env)
# If the project isn't in args, add all.xcodeproj to simplify configuration.
command = ['xcodebuild', '-configuration', options.target]
# TODO(mmoss) Support the old 'args' usage until we're confident the master is
# switched to passing '--solution' everywhere.
if not '-project' in args:
# TODO(mmoss) Temporary hack to ignore the Windows --solution flag that is
# passed to all builders. This can be taken out once the master scripts are
# updated to only pass platform-appropriate --solution values.
if (not options.solution or
os.path.splitext(options.solution)[1] != '.xcodeproj'):
options.solution = '../build/all.xcodeproj'
command.extend(['-project', options.solution])
if options.xcode_target:
command.extend(['-target', options.xcode_target])
# Note: this clobbers all targets, not just Debug or Release.
if options.clobber:
clobber_dir = os.path.dirname(options.target_output_dir)
print 'Removing %s' % clobber_dir
# Deleting output_dir would also delete all the .ninja files. iOS builds
# generates ninja configuration inside the xcodebuild directory to be able
# to run sub builds. crbug.com/138950 is tracking this issue.
# Moreover 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.
build_directory.RmtreeExceptNinjaOrGomaFiles(clobber_dir)
common_xcode_settings(command, options, env, options.compiler)
maybe_set_official_build_envvars(options, env)
# Add on any remaining args
command.extend(args)
# Set up the filter before changing directories so the raw build log can
# be recorded.
# Support a local file blocking filters (for debugging). Also check the
# Xcode version to make sure it is 3.2, as that is what the filter is coded
# to.
xcodebuild_filter = None
no_filter_path = os.path.join(os.getcwd(), 'no_xcodebuild_filter')
xcode_info = chromium_utils.GetCommandOutput(['xcodebuild', '-version'])
if os.path.exists(no_filter_path):
print 'NOTE: "%s" exists, output is unfiltered' % no_filter_path
elif not xcode_info.startswith('Xcode 3.2.'):
# Note: The filter sometimes hides real errors on 4.x+, see crbug.com/260989
print 'NOTE: Not using Xcode 3.2, output is unfiltered'
else:
full_log_path = os.path.join(os.getcwd(), 'full_xcodebuild_log.txt')
full_log = open(full_log_path, 'w')
now = datetime.datetime.now()
full_log.write('Build started ' + now.isoformat() + '\n\n\n')
print 'NOTE: xcodebuild output filtered, full log at: "%s"' % full_log_path
xcodebuild_filter = XcodebuildFilter(full_log)
try:
os.makedirs(options.build_dir)
except OSError, e:
if e.errno != errno.EEXIST:
raise
os.chdir(options.build_dir)
# Run the build.
env.print_overrides()
result = chromium_utils.RunCommand(command, env=env,
filter_obj=xcodebuild_filter)
goma_teardown(options, env)
return result
def common_make_settings(
command, options, env, crosstool=None, compiler=None):
"""
Sets desirable environment variables and command-line options that are used
in the Make build.
"""
assert compiler in (None, 'clang', 'goma', 'goma-clang')
maybe_set_official_build_envvars(options, env)
# Don't stop at the first error.
command.append('-k')
# Set jobs parallelization based on number of cores.
jobs = os.sysconf('SC_NPROCESSORS_ONLN')
# Test if we can use ccache.
ccache = ''
if chromium_utils.IsLinux():
if os.path.exists('/usr/bin/ccache'):
# The default CCACHE_DIR is $HOME/.ccache which, on some of our
# bots, is over NFS. This is intentional. Talk to thestig or
# mmoss if you have questions.
ccache = 'ccache '
# Setup crosstool environment variables.
if crosstool:
env['AR'] = crosstool + '-ar'
env['AS'] = crosstool + '-as'
env['CC'] = ccache + crosstool + '-gcc'
env['CXX'] = ccache + crosstool + '-g++'
env['LD'] = crosstool + '-ld'
env['RANLIB'] = crosstool + '-ranlib'
command.append('-j%d' % jobs)
# Don't use build-in rules.
command.append('-r')
return
if compiler in ('goma', 'goma-clang'):
print 'using', compiler
goma_jobs = 50
if jobs < goma_jobs:
jobs = goma_jobs
command.append('-j%d' % jobs)
return
if compiler == 'clang':
command.append('-r')
command.append('-j%d' % jobs)
def main_make(options, args):
"""Interprets options, clobbers object files, and calls make.
"""
env = EchoDict(os.environ)
goma_ready = goma_setup(options, env)
if not goma_ready:
assert options.compiler not in ('goma', 'goma-clang')
assert options.goma_dir is None
command = ['make']
# Try to build from <build_dir>/Makefile, or if that doesn't exist,
# from the top-level Makefile.
if os.path.isfile(os.path.join(options.build_dir, 'Makefile')):
working_dir = options.build_dir
else:
working_dir = options.src_dir
os.chdir(working_dir)
common_make_settings(command, options, env, options.crosstool,
options.compiler)
# V=1 prints the actual executed command
if options.verbose:
command.extend(['V=1'])
command.extend(options.build_args + args)
# Run the build.
env.print_overrides()
result = 0
def clobber():
print 'Removing %s' % options.target_output_dir
chromium_utils.RemoveDirectory(options.target_output_dir)
assert ',' not in options.target, (
'Used to allow multiple comma-separated targets for make. This should not be'
' in use any more. Asserting from orbit. It\'s the only way to be sure')
if options.clobber:
clobber()
target_command = command + ['BUILDTYPE=' + options.target]
result = chromium_utils.RunCommand(target_command, env=env)
if result and not options.clobber:
clobber()
goma_teardown(options, env)
return result
def main_make_android(options, args):
"""Interprets options, clobbers object files, and calls make.
"""
env = EchoDict(os.environ)
goma_ready = goma_setup(options, env)
if not goma_ready:
assert options.compiler not in ('goma', 'goma-clang')
assert options.goma_dir is None
command = ['make']
if goma_ready:
gomacc = os.path.join(options.goma_dir, 'gomacc')
command.extend(['CC_WRAPPER=' + gomacc,
'CXX_WRAPPER=' + gomacc,
'JAVAC_WRAPPER=' + gomacc,
'-j150',
'-l%d' % os.sysconf('SC_NPROCESSORS_ONLN'),
])
working_dir = options.src_dir
os.chdir(working_dir)
# V=1 prints the actual executed command
if options.verbose:
command.extend(['V=1'])
command.extend(options.build_args + args)
# Run the build.
env.print_overrides()
result = 0
bad_path_patterns = [
'out/target/common/obj/JAVA_LIBRARIES/*webview*',
'out/target/common/R/com/android/*webview*',
'out/target/product/*/obj/SHARED_LIBRARIES/*webview*',
'out/target/product/*/system/lib/*webview*',
'out/target/product/*/system/app/*webview*',
'out/host/*/obj/EXECUTABLES/*gyp*',
'out/host/*/obj/STATIC_LIBRARIES/*gyp*',
'out/host/*/obj/NOTICE_FILES/*gyp*',
'out/host/*/obj/GYP',
'out/target/product/*/obj/EXECUTABLES/*gyp*',
'out/target/product/*/obj/STATIC_LIBRARIES/*gyp*',
'out/target/product/*/obj/NOTICE_FILES/*gyp*',
'out/target/product/*/obj/GYP',
]
def clobber():
print 'Removing %s' % options.target_output_dir
chromium_utils.RemoveDirectory(options.target_output_dir)
# The Android.mk build system handles deps differently than the 'regular'
# Chromium makefiles which can lead to targets not being rebuilt properly.
# Fixing this is actually quite hard so we always delete at least
# everything Chrome related from out.
if options.clobber:
clobber()
else:
for path in bad_path_patterns:
paths = chromium_utils.RemoveGlobbedPaths(path)
print '\n'.join(['Removed {}'.format(removed) for removed in paths])
result = chromium_utils.RunCommand(command, env=env)
goma_teardown(options, env)
return result
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)
def main_ninja(options, args):
"""Interprets options, clobbers object files, and calls ninja."""
# Prepare environment.
env = EchoDict(os.environ)
goma_ready = goma_setup(options, env)
exit_status = -1
try:
if not goma_ready:
assert options.compiler not in ('goma', 'goma-clang')
assert options.goma_dir is None
# ninja is different from all the other build systems in that it requires
# most configuration to be done at gyp time. This is why this function does
# less than the other comparable functions in this file.
print 'chdir to %s' % options.src_dir
os.chdir(options.src_dir)
command = ['ninja', '-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.
build_directory.RmtreeExceptNinjaOrGomaFiles(options.target_output_dir)
if options.verbose:
command.append('-v')
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
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.
#
# For linux, let me keep the current value 50. It's fast enough
# compared to the other platforms.
#
# 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)
return 50
goma_jobs = determine_goma_jobs()
command.append('-j%d' % 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:
goma_teardown(options, env)
UploadNinjaLog(options, command, exit_status)
def main_win(options, args):
"""Interprets options, clobbers object files, and calls the build tool.
"""
if not options.solution:
options.solution = 'all.sln'
solution = os.path.join(options.build_dir, options.solution)
# Prefer the version specified in the .sln. When devenv.com is used at the
# command line to start a build, it doesn't accept sln file from a different
# version.
if not options.msvs_version:
sln = open(os.path.join(solution), 'rU')
header = sln.readline().strip()
sln.close()
if header.endswith('13.00'):
options.msvs_version = '12'
elif header.endswith('12.00'):
options.msvs_version = '11'
elif header.endswith('11.00'):
options.msvs_version = '10'
elif header.endswith('10.00'):
options.msvs_version = '9'
elif header.endswith('9.00'):
options.msvs_version = '8'
else:
print >> sys.stderr, "Unknown sln header:\n" + header
return 1
REG_ROOT = 'SOFTWARE\\Microsoft\\VisualStudio\\'
devenv = ReadHKLMValue(REG_ROOT + options.msvs_version + '.0', 'InstallDir')
if devenv:
devenv = os.path.join(devenv, 'devenv.com')
else:
print >> sys.stderr, ("MSVS %s was requested but is not installed." %
options.msvs_version)
return 1
ib = ReadHKLMValue('SOFTWARE\\Xoreax\\IncrediBuild\\Builder', 'Folder')
if ib:
ib = os.path.join(ib, 'BuildConsole.exe')
if ib and os.path.exists(ib) and not options.no_ib:
tool = ib
if options.arch == 'x64':
tool_options = ['/Cfg=%s|x64' % options.target]
else:
tool_options = ['/Cfg=%s|Win32' % options.target]
if options.project:
tool_options.extend(['/Prj=%s' % options.project])
else:
tool = devenv
if options.arch == 'x64':
tool_options = ['/Build', '%s|x64' % options.target]
else:
tool_options = ['/Build', options.target]
if options.project:
tool_options.extend(['/Project', options.project])
def clobber():
print 'Removing %s' % options.target_output_dir
chromium_utils.RemoveDirectory(options.target_output_dir)
if options.clobber:
clobber()
else:
# Remove the log file so it doesn't grow without limit,
chromium_utils.RemoveFile(options.target_output_dir, 'debug.log')
# Remove the chrome.dll version resource so it picks up the new svn
# revision, unless user explicitly asked not to remove it. See
# Bug 1064677 for more details.
if not options.keep_version_file:
chromium_utils.RemoveFile(options.target_output_dir, 'obj', 'chrome_dll',
'chrome_dll_version.rc')
env = EchoDict(os.environ)
# no goma support yet for this build tool.
assert options.compiler != 'goma'
maybe_set_official_build_envvars(options, env)
result = -1
command = [tool, solution] + tool_options + args
errors = []
# Examples:
# midl : command line error MIDL1003 : error returned by the C
# preprocessor (-1073741431)
#
# Error executing C:\PROGRA~2\MICROS~1\Common7\Tools\Bin\Midl.Exe (tool
# returned code: 1282)
#
# ---
#
# cl : Command line error D8027 : cannot execute 'C:\Program Files
# (x86)\Microsoft Visual Studio 8\VC\bin\c2.dll'
#
# ---
#
# Warning: Could not delete file "c:\b\slave\win\build\src\build\Debug\
# chrome.dll" : Access is denied
# --------------------Build System Warning--------------------------------
# -------
# Could not delete file:
# Could not delete file "c:\b\slave\win\build\src\build\Debug\
# chrome.dll" : Access is denied
# (Automatically running xgHandle on first 10 files that could not be
# deleted)
# Searching for '\Device\HarddiskVolume1\b\slave\win\build\src\build\
# Debug\chrome.dll':
# No handles found.
# (xgHandle utility returned code: 0x00000000)
#
# ---
#
# webkit.lib(WebGeolocationError.obj) : fatal error LNK1318: Unexpected PDB
# error; OK (0) ''
#
# Error executing link.exe (tool returned code: 1318)
#
# ---
#
# browser.lib(background_application_list_model.obj) : fatal error LNK1000:
# Internal error during IMAGE::Pass2
# (along with a register dump)
#
# ---
#
# ...\browser\history\download_create_info.cc : fatal error C1033: cannot open
# program database '...\src\build\debug\obj\browser\browser\vc80_ib_2.idb'
#
# ---
#
# --------------------Build System Error (Agent 'Ib1 (CPU 1)')----------------
# Fatalerror:
# Failed to execute command: extension_function_registry (ID 1591)
# Failed to update directory: E:\b\build\slave\win\build\src\build\Release
# File table management has failed.
# Shared stream group lock abandoned, marking as corrupt
# --------
# Unable to complete operation (retried 10 times): cl: foo.cc -> foo.obj
known_toolset_bugs = [
'\\c2.dll',
'Midl.Exe (tool returned code: 1282)',
'LINK : fatal error LNK1102: out of memory',
'fatal error LNK1318: Unexpected PDB error',
'fatal error LNK1000: Internal error during IMAGE::Pass2',
'fatal error C1033',
'Build System Error',
]
def scan(line):
for known_line in known_toolset_bugs:
if known_line in line:
errors.append(line)
break
env.print_overrides()
result = chromium_utils.RunCommand(
command, parser_func=scan, env=env, universal_newlines=True)
if errors:
print '\n\nRetrying a clobber build because of:'
print '\n'.join((' ' + l for l in errors))
print 'Removing %s' % options.target_output_dir
for _ in range(3):
try:
chromium_utils.RemoveDirectory(options.target_output_dir)
break
except OSError, e:
print e
print '\nSleeping 15 seconds. Lovely windows file locks.'
time.sleep(15)
else:
print 'Failed to delete a file 3 times in a row, aborting.'
return 1
result = chromium_utils.RunCommand(command, env=env)
# TODO(maruel): As soon as the try server is restarted, replace with:
# if result and not options.clobber and options.clobber_post_fail:
if result and not options.clobber:
clobber()
return result
def get_target_build_dir(args, options):
"""Keep this function in sync with src/build/landmines.py"""
build_tool = options.build_tool
ret = None
if build_tool == 'xcode':
relpath = os.path.join('xcodebuild',
options.target + ('-iphoneos' if 'iphoneos' in args else ''))
elif build_tool in ['make', 'ninja']:
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'
relpath = os.path.join(outdir, options.target)
elif build_tool == 'make-android':
relpath = os.path.join('out')
elif build_tool in ['vs', 'ib']:
relpath = os.path.join('build', options.target)
else:
raise NotImplementedError()
ret = os.path.join(options.src_dir, relpath)
return os.path.abspath(ret)
def real_main():
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('--clobber-post-fail', action='store_true',
default=False,
help='delete the output directory after compiling '
'only if it failed. Do not affect ninja.')
option_parser.add_option('--keep-version-file', action='store_true',
default=False,
help='do not delete the chrome_dll_version.rc file '
'before compiling (ignored if --clobber is '
'used')
option_parser.add_option('--target', default='Release',
help='build target (Debug or Release)')
option_parser.add_option('--arch', default=None,
help='target architecture (ia32, x64, ...')
option_parser.add_option('--solution', default=None,
help='name of solution/sub-project to build')
option_parser.add_option('--project', default=None,
help='name of project to build')
option_parser.add_option('--build-dir', help='ignored')
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')
option_parser.add_option('--build-tool', default=None,
help='specify build tool (ib, vs, xcode)')
option_parser.add_option('--build-args', action='append', default=[],
help='arguments to pass to the build tool')
option_parser.add_option('--compiler', default=None,
help='specify alternative compiler (e.g. clang)')
if chromium_utils.IsWindows():
# Windows only.
option_parser.add_option('--no-ib', action='store_true', default=False,
help='use Visual Studio instead of IncrediBuild')
option_parser.add_option('--msvs_version',
help='VisualStudio version to use')
# For linux to arm cross compile.
option_parser.add_option('--crosstool', default=None,
help='optional path to crosstool toolset')
option_parser.add_option('--llvm-tsan', action='store_true',
default=False,
help='build with LLVM\'s ThreadSanitizer')
if chromium_utils.IsMac():
# Mac only.
option_parser.add_option('--xcode-target', default=None,
help='Target from the xcodeproj file')
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-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-dump-stats-file', action='store',
help='Specify a file to dump serialized GomaStats.')
option_parser.add_option('--goma-jsonstatus',
help='Specify a file to dump goma_ctl jsonstatus.')
option_parser.add_option('--verbose', action='store_true')
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.')
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.build_dir = os.path.abspath(build_directory.GetBuildOutputDirectory(
os.path.basename(options.src_dir)))
if options.build_tool is None:
if chromium_utils.IsWindows():
# We're in the process of moving to ninja by default on Windows, see
# http://crbug.com/303291.
if build_directory.AreNinjaFilesNewerThanMSVSFiles(
src_dir=options.src_dir):
main = main_ninja
options.build_tool = 'ninja'
if options.project:
args += [options.project]
else:
main = main_win
options.build_tool = 'vs'
elif chromium_utils.IsMac():
# We're in the process of moving to ninja by default on Mac, see
# http://crbug.com/294387
# Builders for different branches will use either xcode or ninja depending
# on the release channel for a while. Until all release channels are on
# ninja, use build file mtime to figure out which build system to use.
# TODO(thakis): Just use main_ninja once the transition is complete.
if build_directory.AreNinjaFilesNewerThanXcodeFiles(
src_dir=options.src_dir):
main = main_ninja
options.build_tool = 'ninja'
# There is no standard way to pass a build target (such as 'base') to
# compile.py. --target specifies Debug or Release. --project could do
# that, but it's only supported by the msvs build tool at the moment.
# Because of that, most build masters pass additional options to the
# build tool to specify the build target. For xcode, these are in the
# form of '-project blah.xcodeproj -target buildtarget'. Translate these
# into ninja options, if needed.
xcode_option_parse = optparse.OptionParser()
xcode_option_parse.add_option('--project')
xcode_option_parse.add_option('--target', action='append', default=[])
xcode_options, xcode_args = xcode_option_parse.parse_args(
[re.sub('^-', '--', a) for a in args]) # optparse wants --options.
args = xcode_options.target + xcode_args
else:
main = main_xcode
options.build_tool = 'xcode'
elif chromium_utils.IsLinux():
main = main_ninja
options.build_tool = 'ninja'
else:
print 'Please specify --build-tool.'
return 1
else:
build_tool_map = {
'ib' : main_win,
'vs' : main_win,
'make' : main_make,
'make-android' : main_make_android,
'ninja' : main_ninja,
'xcode' : main_xcode,
}
main = build_tool_map.get(options.build_tool)
if not main:
sys.stderr.write('Unknown build tool %s.\n' % repr(options.build_tool))
return 2
options.target_output_dir = get_target_build_dir(args, options)
return main(options, args)
if '__main__' == __name__:
sys.exit(real_main())