blob: 02488a6c3306a17303831b7a96569dfd26ed7cb2 [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.
"""Windows can't run .sh files, so this is a Python implementation of
update.sh. This script should replace update.sh on all platforms eventually."""
import argparse
import os
import re
import shutil
import subprocess
import stat
import sys
import time
# Do NOT CHANGE this if you don't know what you're doing -- see
# https://code.google.com/p/chromium/wiki/UpdatingClang
# Reverting problematic clang rolls is safe, though.
# Note: this revision is only used for Windows. Other platforms use update.sh.
LLVM_WIN_REVISION = 'HEAD'
# ASan on Windows is useful enough to use it even while the clang/win is still
# in bringup. Use a pinned revision to make it slightly more stable.
use_head_revision = ('LLVM_FORCE_HEAD_REVISION' in os.environ or
not re.search(r'\b(asan)=1', os.environ.get('GYP_DEFINES', '')))
if not use_head_revision:
LLVM_WIN_REVISION = '235968'
# Path constants. (All of these should be absolute paths.)
THIS_DIR = os.path.abspath(os.path.dirname(__file__))
CHROMIUM_DIR = os.path.abspath(os.path.join(THIS_DIR, '..', '..', '..'))
LLVM_DIR = os.path.join(CHROMIUM_DIR, 'third_party', 'llvm')
CHROME_TOOLS_SHIM_DIR = os.path.join(LLVM_DIR, 'tools', 'chrometools')
LLVM_BUILD_DIR = os.path.join(CHROMIUM_DIR, 'third_party', 'llvm-build',
'Release+Asserts')
COMPILER_RT_BUILD_DIR = os.path.join(LLVM_BUILD_DIR, '32bit-compiler-rt')
CLANG_DIR = os.path.join(LLVM_DIR, 'tools', 'clang')
LLD_DIR = os.path.join(LLVM_DIR, 'tools', 'lld')
COMPILER_RT_DIR = os.path.join(LLVM_DIR, 'projects', 'compiler-rt')
STAMP_FILE = os.path.join(LLVM_BUILD_DIR, 'cr_build_revision')
VERSION = '3.7.0'
LLVM_REPO_URL='https://llvm.org/svn/llvm-project'
if 'LLVM_REPO_URL' in os.environ:
LLVM_REPO_URL = os.environ['LLVM_REPO_URL']
def ReadStampFile():
"""Return the contents of the stamp file, or '' if it doesn't exist."""
try:
with open(STAMP_FILE, 'r') as f:
return f.read();
except IOError:
return ''
def WriteStampFile(s):
"""Write s to the stamp file."""
if not os.path.exists(LLVM_BUILD_DIR):
os.makedirs(LLVM_BUILD_DIR)
with open(STAMP_FILE, 'w') as f:
f.write(s)
def PrintRevision():
"""Print the current Clang revision."""
# gyp runs update.py --print-revision even when clang isn't used.
# It won't use the value, but we must not error.
if not os.path.exists(LLVM_DIR):
print "0"
return
# TODO(hans): This needs an update when we move to prebuilt Clang binaries.
svn_info = subprocess.check_output(['svn', 'info', LLVM_DIR], shell=True)
m = re.search(r'Revision: (\d+)', svn_info)
assert m
print m.group(1)
def RmTree(dir):
"""Delete dir."""
def ChmodAndRetry(func, path, _):
# Subversion can leave read-only files around.
if not os.access(path, os.W_OK):
os.chmod(path, stat.S_IWUSR)
return func(path)
raise
shutil.rmtree(dir, onerror=ChmodAndRetry)
def RunCommand(command, fail_hard=True):
"""Run command and return success (True) or failure; or if fail_hard is
True, exit on failure."""
print 'Running %s' % (str(command))
if subprocess.call(command, shell=True) == 0:
return True
print 'Failed.'
if fail_hard:
sys.exit(1)
return False
def CopyFile(src, dst):
"""Copy a file from src to dst."""
shutil.copy(src, dst)
print "Copying %s to %s" % (src, dst)
def CopyDirectoryContents(src, dst, filename_filter=None):
"""Copy the files from directory src to dst
with an optional filename filter."""
if not os.path.exists(dst):
os.makedirs(dst)
for root, _, files in os.walk(src):
for f in files:
if filename_filter and not re.match(filename_filter, f):
continue
CopyFile(os.path.join(root, f), dst)
def Checkout(name, url, dir):
"""Checkout the SVN module at url into dir. Use name for the log message."""
print "Checking out %s r%s into '%s'" % (name, LLVM_WIN_REVISION, dir)
command = ['svn', 'checkout', '--force', url + '@' + LLVM_WIN_REVISION, dir]
if RunCommand(command, fail_hard=False):
return
if os.path.isdir(dir):
print "Removing %s." % (dir)
RmTree(dir)
print "Retrying."
RunCommand(command)
def DeleteChromeToolsShim():
shutil.rmtree(CHROME_TOOLS_SHIM_DIR, ignore_errors=True)
def CreateChromeToolsShim():
"""Hooks the Chrome tools into the LLVM build.
Several Chrome tools have dependencies on LLVM/Clang libraries. The LLVM build
detects implicit tools in the tools subdirectory, so this helper install a
shim CMakeLists.txt that forwards to the real directory for the Chrome tools.
Note that the shim directory name intentionally has no - or _. The implicit
tool detection logic munges them in a weird way."""
assert not any(i in os.path.basename(CHROME_TOOLS_SHIM_DIR) for i in '-_')
os.mkdir(CHROME_TOOLS_SHIM_DIR)
with file(os.path.join(CHROME_TOOLS_SHIM_DIR, 'CMakeLists.txt'), 'w') as f:
f.write('# Automatically generated by tools/clang/scripts/update.py. ' +
'Do not edit.\n')
f.write('# Since tools/clang is located in another directory, use the \n')
f.write('# two arg version to specify where build artifacts go. CMake\n')
f.write('# disallows reuse of the same binary dir for multiple source\n')
f.write('# dirs, so the build artifacts need to go into a subdirectory.\n')
f.write('add_subdirectory(${CHROMIUM_TOOLS_SRC} ' +
'${CMAKE_CURRENT_BINARY_DIR}/a)\n')
def AddCMakeToPath():
"""Look for CMake and add it to PATH if it's not there already."""
try:
# First check if cmake is already on PATH.
subprocess.call(['cmake', '--version'])
return
except OSError as e:
if e.errno != os.errno.ENOENT:
raise
cmake_dir = 'C:\\Program Files (x86)\\CMake\\bin'
if os.path.isdir(cmake_dir):
os.environ['PATH'] = os.environ.get('PATH', '') + os.pathsep + cmake_dir
return
print 'Failed to find CMake!'
sys.exit(1)
vs_version = None
def GetVSVersion():
global vs_version
if vs_version:
return vs_version
# Try using the toolchain in depot_tools.
# This sets environment variables used by SelectVisualStudioVersion below.
sys.path.append(os.path.join(CHROMIUM_DIR, 'build'))
import vs_toolchain
vs_toolchain.SetEnvironmentAndGetRuntimeDllDirs()
# Use gyp to find the MSVS installation, either in depot_tools as per above,
# or a system-wide installation otherwise.
sys.path.append(os.path.join(CHROMIUM_DIR, 'tools', 'gyp', 'pylib'))
import gyp.MSVSVersion
vs_version = gyp.MSVSVersion.SelectVisualStudioVersion('2013')
return vs_version
def SubversionCmakeArg():
# Since cmake's find_program can only find .exe and .com,
# svn.bat in depot_tools will be ignored.
default_pathext = ('.com', '.exe', '.bat', '.cmd')
for path in os.environ.get('PATH', '').split(os.pathsep):
for ext in default_pathext:
candidate = os.path.join(path, 'svn' + ext)
if os.path.isfile(candidate):
return '-DSubversion_SVN_EXECUTABLE=%s' % candidate
return ''
def UpdateClang(args):
print 'Updating Clang to %s...' % (LLVM_WIN_REVISION)
if LLVM_WIN_REVISION != 'HEAD' and ReadStampFile() == LLVM_WIN_REVISION:
print 'Already up to date.'
return 0
AddCMakeToPath()
# Reset the stamp file in case the build is unsuccessful.
WriteStampFile('')
DeleteChromeToolsShim();
Checkout('LLVM', LLVM_REPO_URL + '/llvm/trunk', LLVM_DIR)
Checkout('Clang', LLVM_REPO_URL + '/cfe/trunk', CLANG_DIR)
Checkout('LLD', LLVM_REPO_URL + '/lld/trunk', LLD_DIR)
Checkout('compiler-rt', LLVM_REPO_URL + '/compiler-rt/trunk', COMPILER_RT_DIR)
CreateChromeToolsShim();
if not os.path.exists(LLVM_BUILD_DIR):
os.makedirs(LLVM_BUILD_DIR)
os.chdir(LLVM_BUILD_DIR)
# If building at head, define a macro that plugins can use for #ifdefing
# out code that builds at head, but not at CLANG_REVISION or vice versa.
cflags = cxxflags = ''
# TODO(thakis): Set this only conditionally if use_head_revision once posix
# and win clang are in sync. At the moment, the plugins only build at clang
# head on posix, but they build at both head and the pinned win version :-/
cflags += ' -DLLVM_FORCE_HEAD_REVISION'
cxxflags += ' -DLLVM_FORCE_HEAD_REVISION'
cmake_args = ['-GNinja', '-DCMAKE_BUILD_TYPE=Release',
'-DLLVM_ENABLE_ASSERTIONS=ON', SubversionCmakeArg(),
'-DCMAKE_C_FLAGS=' + cflags,
'-DCMAKE_CXX_FLAGS=' + cxxflags,
'-DCHROMIUM_TOOLS_SRC=%s' % os.path.join(
CHROMIUM_DIR, 'tools', 'clang'),
'-DCHROMIUM_TOOLS=%s' % ';'.join(args.tools)]
RunCommand(GetVSVersion().SetupScript('x64') +
['&&', 'cmake'] + cmake_args + [LLVM_DIR])
RunCommand(GetVSVersion().SetupScript('x64') + ['&&', 'ninja', 'all'])
# Do an x86 build of compiler-rt to get the 32-bit ASan run-time.
# TODO(hans): Remove once the regular build above produces this.
if not os.path.exists(COMPILER_RT_BUILD_DIR):
os.makedirs(COMPILER_RT_BUILD_DIR)
os.chdir(COMPILER_RT_BUILD_DIR)
RunCommand(GetVSVersion().SetupScript('x86') +
['&&', 'cmake'] + cmake_args + [LLVM_DIR])
RunCommand(GetVSVersion().SetupScript('x86') + ['&&', 'ninja', 'compiler-rt'])
# TODO(hans): Make this (and the .gypi and .isolate files) version number
# independent.
asan_rt_lib_src_dir = os.path.join(COMPILER_RT_BUILD_DIR, 'lib', 'clang',
VERSION, 'lib', 'windows')
asan_rt_lib_dst_dir = os.path.join(LLVM_BUILD_DIR, 'lib', 'clang',
VERSION, 'lib', 'windows')
CopyDirectoryContents(asan_rt_lib_src_dir, asan_rt_lib_dst_dir,
r'^.*-i386\.lib$')
CopyDirectoryContents(asan_rt_lib_src_dir, asan_rt_lib_dst_dir,
r'^.*-i386\.dll$')
CopyFile(os.path.join(asan_rt_lib_src_dir, '..', '..', 'asan_blacklist.txt'),
os.path.join(asan_rt_lib_dst_dir, '..', '..'))
# Make an extra copy of the sanitizer headers, to be put on the include path
# of the fallback compiler.
sanitizer_include_dir = os.path.join(LLVM_BUILD_DIR, 'lib', 'clang', VERSION,
'include', 'sanitizer')
aux_sanitizer_include_dir = os.path.join(LLVM_BUILD_DIR, 'lib', 'clang',
VERSION, 'include_sanitizer',
'sanitizer')
if not os.path.exists(aux_sanitizer_include_dir):
os.makedirs(aux_sanitizer_include_dir)
for _, _, files in os.walk(sanitizer_include_dir):
for f in files:
CopyFile(os.path.join(sanitizer_include_dir, f),
aux_sanitizer_include_dir)
if args.run_tests:
os.chdir(LLVM_BUILD_DIR)
RunCommand(GetVSVersion().SetupScript('x64') +
['&&', 'ninja', 'cr-check-all'])
WriteStampFile(LLVM_WIN_REVISION)
print 'Clang update was successful.'
return 0
def main():
if not sys.platform in ['win32', 'cygwin']:
# For non-Windows, fall back to update.sh.
# TODO(hans): Make update.py replace update.sh completely.
# This script is called by gclient. gclient opens its hooks subprocesses
# with (stdout=subprocess.PIPE, stderr=subprocess.STDOUT) and then does
# custom output processing that breaks printing '\r' characters for
# single-line updating status messages as printed by curl and wget.
# Work around this by setting stderr of the update.sh process to stdin (!):
# gclient doesn't redirect stdin, and while stdin itself is read-only, a
# dup()ed sys.stdin is writable, try
# fd2 = os.dup(sys.stdin.fileno()); os.write(fd2, 'hi')
# TODO: Fix gclient instead, http://crbug.com/95350
try:
stderr = os.fdopen(os.dup(sys.stdin.fileno()))
except:
stderr = sys.stderr
return subprocess.call(
[os.path.join(os.path.dirname(__file__), 'update.sh')] + sys.argv[1:],
stderr=stderr)
parser = argparse.ArgumentParser(description='Build Clang.')
parser.add_argument('--tools', nargs='*',
default=['plugins', 'blink_gc_plugin'])
# For now, this flag is only used for the non-Windows flow, but argparser gets
# mad if it sees a flag it doesn't recognize.
parser.add_argument('--if-needed', action='store_true')
parser.add_argument('--print-revision', action='store_true')
parser.add_argument('--run-tests', action='store_true')
args = parser.parse_args()
if args.print_revision:
PrintRevision()
return 0
if not re.search(r'\b(clang|asan)=1', os.environ.get('GYP_DEFINES', '')):
print 'Skipping Clang update (clang=1 was not set in GYP_DEFINES).'
return 0
if re.search(r'\b(make_clang_dir)=', os.environ.get('GYP_DEFINES', '')):
print 'Skipping Clang update (make_clang_dir= was set in GYP_DEFINES).'
return 0
return UpdateClang(args)
if __name__ == '__main__':
sys.exit(main())