blob: 0dfb991dcfd5a26f4d0b634738464e2645d016b2 [file] [log] [blame]
#!/usr/bin/env python3
# Copyright (c) 2015 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.
"""This script will check out llvm and clang, and then package the results up
to a tgz file."""
from __future__ import print_function
import argparse
import fnmatch
import itertools
import multiprocessing.dummy
import os
import platform
import shutil
import subprocess
import sys
import tarfile
import time
from update import RELEASE_VERSION, STAMP_FILE
# Path constants.
THIS_DIR = os.path.dirname(__file__)
CHROMIUM_DIR = os.path.abspath(os.path.join(THIS_DIR, '..', '..', '..'))
THIRD_PARTY_DIR = os.path.join(THIS_DIR, '..', '..', '..', 'third_party')
BUILDTOOLS_DIR = os.path.join(THIS_DIR, '..', '..', '..', 'buildtools')
LLVM_DIR = os.path.join(THIRD_PARTY_DIR, 'llvm')
LLVM_BOOTSTRAP_DIR = os.path.join(THIRD_PARTY_DIR, 'llvm-bootstrap')
LLVM_BUILD_DIR = os.path.join(THIRD_PARTY_DIR, 'llvm-build')
LLVM_RELEASE_DIR = os.path.join(LLVM_BUILD_DIR, 'Release+Asserts')
EU_STRIP = os.path.join(BUILDTOOLS_DIR, 'third_party', 'eu-strip', 'bin',
def Tee(output, logfile):
print(output, end=' ')
def TeeCmd(cmd, logfile, fail_hard=True):
"""Runs cmd and writes the output to both stdout and logfile."""
# Reading from PIPE can deadlock if one buffer is full but we wait on a
# different one. To work around this, pipe the subprocess's stderr to
# its stdout buffer and don't give it a stdin.
# shell=True is required in cmd.exe since depot_tools has an svn.bat, and
# bat files only work with shell=True set.
proc = subprocess.Popen(cmd, bufsize=1, shell=sys.platform == 'win32',
stdin=open(os.devnull), stdout=subprocess.PIPE,
for line in iter(proc.stdout.readline,''):
Tee(str(line.decode()), logfile)
if proc.poll() is not None:
exit_code = proc.wait()
if exit_code != 0 and fail_hard:
print('Failed:', cmd)
def PrintTarProgress(tarinfo):
return tarinfo
def GetExpectedStamp():
rev_cmd = [sys.executable, os.path.join(THIS_DIR, ''),
return str(subprocess.check_output(rev_cmd).decode()).rstrip()
def GetGsutilPath():
if not 'find_depot_tools' in sys.modules:
sys.path.insert(0, os.path.join(CHROMIUM_DIR, 'build'))
global find_depot_tools
import find_depot_tools
depot_path = find_depot_tools.add_depot_tools_to_path()
if depot_path is None:
print ('depot_tools are not found in PATH. '
'Follow the instructions in this document '
' to install depot_tools and then try again.')
gsutil_path = os.path.join(depot_path, '')
return gsutil_path
def RunGsutil(args):
return[sys.executable, GetGsutilPath()] + args)
def PackageInArchive(directory_path, archive_path):
bin_dir_path = os.path.join(directory_path, 'bin')
if sys.platform != 'win32' and os.path.exists(bin_dir_path):
for f in os.listdir(bin_dir_path):
file_path = os.path.join(bin_dir_path, f)
if not os.path.islink(file_path):['strip', file_path])
with, 'w:gz') as tar:
for f in os.listdir(directory_path):
tar.add(os.path.join(directory_path, f),
arcname=f, filter=PrintTarProgress)
def MaybeUpload(do_upload, filename, gcs_platform, extra_gsutil_args=[]):
gsutil_args = ['cp'] + extra_gsutil_args + [
'-a', 'public-read', filename,
'gs://chromium-browser-clang-staging/%s/%s' % (gcs_platform, filename)
if do_upload:
print('Uploading %s to Google Cloud Storage...' % filename)
exit_code = RunGsutil(gsutil_args)
if exit_code != 0:
print("gsutil failed, exit_code: %s" % exit_code)
print('To upload, run:')
print('gsutil %s' % ' '.join(gsutil_args))
def UploadPDBsToSymbolServer(binaries):
assert sys.platform == 'win32'
# Upload PDB and binary to the symbol server on Windows. Put them into the
# chromium-browser-symsrv bucket, since chrome devs have that in their
# _NT_SYMBOL_PATH already. Executable and PDB must be at paths following a
# certain pattern for the Microsoft debuggers to be able to load them.
# Executable:
# chromium-browser-symsrv/clang-cl.exe/ABCDEFAB01234/clang-cl.ex_
# ABCDEFAB is the executable's timestamp in %08X format, 01234 is the
# executable's image size in %x format. tools/symsrc/
# can compute this ABCDEFAB01234 string for us, so use that.
# The .ex_ instead of .exe at the end means that the file is compressed.
# PDB:
# gs://chromium-browser-symsrv/clang-cl.exe.pdb/AABBCCDD/clang-cl.exe.pd_
# AABBCCDD here is computed from the output of
# dumpbin /all mybinary.exe | find "Format: RSDS"
# but tools/symsrc/ can compute it already, so
# again just use that.
sys.path.insert(0, os.path.join(CHROMIUM_DIR, 'tools', 'symsrc'))
import img_fingerprint, pdb_fingerprint_from_img
files = []
for binary_path in binaries:
binary_path = os.path.join(LLVM_RELEASE_DIR, binary_path)
binary_id = img_fingerprint.GetImgFingerprint(binary_path)
(pdb_id, pdb_path) = pdb_fingerprint_from_img.GetPDBInfoFromImg(binary_path)
files += [(binary_path, binary_id), (pdb_path, pdb_id)]
# The build process builds clang.exe and then copies it to clang-cl.exe
# (both are the same binary and they behave differently on what their
# filename is). Hence, the pdb is at clang.pdb, not at clang-cl.pdb.
# Likewise, lld-link.exe's PDB file is called lld.pdb.
# Compress and upload.
def compress(t):
['makecab', '/D', 'CompressionType=LZX', '/D', 'CompressionMemory=21',
t[0], '/L', os.path.dirname(t[0])], stdout=open(os.devnull, 'w'))
multiprocessing.dummy.Pool().map(compress, files)
for f, f_id in files:
f_cab = f[:-1] + '_'
dest = '%s/%s/%s' % (os.path.basename(f), f_id, os.path.basename(f_cab))
print('Uploading %s to Google Cloud Storage...' % dest)
gsutil_args = ['cp', '-n', '-a', 'public-read', f_cab,
'gs://chromium-browser-symsrv/' + dest]
exit_code = RunGsutil(gsutil_args)
if exit_code != 0:
print("gsutil failed, exit_code: %s" % exit_code)
def main():
parser = argparse.ArgumentParser(description='build and package clang')
parser.add_argument('--upload', action='store_true',
help='Upload the target archive to Google Cloud Storage.')
parser.add_argument('--build-mac-arm', action='store_true',
help='Build arm binaries. Only valid on macOS.')
args = parser.parse_args()
if args.build_mac_arm and sys.platform != 'darwin':
print('--build-mac-arm only valid on macOS')
return 1
if args.build_mac_arm and platform.machine() == 'arm64':
print('--build-mac-arm only valid on intel to cross-build arm')
return 1
expected_stamp = GetExpectedStamp()
pdir = 'clang-' + expected_stamp
if sys.platform == 'darwin':
# When we need to run this script on an arm machine, we need to add a
# --build-mac-intel switch to pick which clang to build, pick the
# 'Mac_arm64' here when there's no flag and 'Mac' when --build-mac-intel is
# passed. Also update the build script to explicitly pass a default triple
# then.
if args.build_mac_arm or platform.machine() == 'arm64':
gcs_platform = 'Mac_arm64'
gcs_platform = 'Mac'
elif sys.platform == 'win32':
gcs_platform = 'Win'
gcs_platform = 'Linux_x64'
with open('buildlog.txt', 'w') as log:
Tee('Starting build\n', log)
# Do a clobber build.
shutil.rmtree(LLVM_BOOTSTRAP_DIR, ignore_errors=True)
shutil.rmtree(LLVM_BOOTSTRAP_INSTALL_DIR, ignore_errors=True)
shutil.rmtree(LLVM_BUILD_DIR, ignore_errors=True)
build_cmd = [
os.path.join(THIS_DIR, ''), '--bootstrap', '--disable-asserts',
'--run-tests', '--pgo'
if args.build_mac_arm:
if sys.platform != 'darwin':
TeeCmd(build_cmd, log)
stamp = open(STAMP_FILE).read().rstrip()
if stamp != expected_stamp:
print('Actual stamp (%s) != expected stamp (%s).' % (stamp, expected_stamp))
return 1
shutil.rmtree(pdir, ignore_errors=True)
# Copy a list of files to the directory we're going to tar up.
# This supports the same patterns that the fnmatch module understands.
# '$V' is replaced by RELEASE_VERSION further down.
exe_ext = '.exe' if sys.platform == 'win32' else ''
want = [
'bin/llvm-pdbutil' + exe_ext,
'bin/llvm-symbolizer' + exe_ext,
'bin/llvm-undname' + exe_ext,
# Copy built-in headers (lib/clang/3.x.y/include).
if sys.platform == 'win32':
# Add LLD.
# Add llvm-ar for LTO.
# Include libclang_rt.builtins.a for Fuchsia targets.
# Add llvm-readobj (symlinked from llvm-readelf) for extracting SONAMEs.
if not args.build_mac_arm:
# TODO(thakis): Figure out why this doesn't build in --build-mac-arm
# builds.
if sys.platform != 'darwin':
# The Fuchsia asan runtime is only built on non-Mac platforms.
if sys.platform == 'darwin':
# AddressSanitizer runtime.
# OS X and iOS builtin libraries for the _IsOSVersionAtLeast runtime
# function.
# Profile runtime (used by profiler and code coverage).
# UndefinedBehaviorSanitizer runtime.
elif sys.platform.startswith('linux'):
# pylint: disable=line-too-long
# Copy the we linked the binaries against.
# Add llvm-objcopy for partition extraction on Android.
# Add llvm-nm.
# AddressSanitizer C runtime (pure C won't link with *_cxx).
# AddressSanitizer C++ runtime.
# AddressSanitizer Android runtime.
# Builtins for Android.
# Builtins for Lacros (and potentially Linux, but not used there atm).
# crtstart/crtend for Linux and Lacros.
# HWASAN Android runtime.
# MemorySanitizer C runtime (pure C won't link with *_cxx).
# MemorySanitizer C++ runtime.
# Profile runtime (used by profiler and code coverage).
# ThreadSanitizer C runtime (pure C won't link with *_cxx).
# ThreadSanitizer C++ runtime.
# UndefinedBehaviorSanitizer C runtime (pure C won't link with *_cxx).
# UndefinedBehaviorSanitizer C++ runtime.
# UndefinedBehaviorSanitizer Android runtime, needed for CFI.
# Ignorelist for MemorySanitizer (used on Linux only).
# pylint: enable=line-too-long
elif sys.platform == 'win32':
# AddressSanitizer C runtime (pure C won't link with *_cxx).
# AddressSanitizer C++ runtime.
# Thunk for AddressSanitizer needed for static build of a shared lib.
# AddressSanitizer runtime for component build.
# Thunk for AddressSanitizer for component build of a shared lib.
# Profile runtime (used by profiler and code coverage).
# UndefinedBehaviorSanitizer C runtime (pure C won't link with *_cxx).
# UndefinedBehaviorSanitizer C++ runtime.
# reclient is a tool for executing programs remotely. When uploading the
# binary to be executed, it needs to know which other files the binary depends
# on. This can include shared libraries, as well as other dependencies not
# explicitly mentioned in the source code (those would be found by reclient's
# include scanner) such as sanitizer ignore lists.
reclient_inputs = {
'clang': [
# Check that all non-glob wanted files exist on disk.
want = [w.replace('$V', RELEASE_VERSION) for w in want]
for w in want:
if '*' in w: continue
if os.path.exists(os.path.join(LLVM_RELEASE_DIR, w)): continue
print('wanted file "%s" but it did not exist' % w, file=sys.stderr)
return 1
# Check that all reclient inputs are in the package.
for tool in reclient_inputs:
reclient_inputs[tool] = [i.replace('$V', RELEASE_VERSION)
for i in reclient_inputs[tool]]
missing = set(reclient_inputs[tool]) - set(want)
if missing:
print('reclient inputs not part of package: ', missing, file=sys.stderr)
return 1
reclient_input_strings = {t: '' for t in reclient_inputs}
# TODO(thakis): Try walking over want and copying the files in there instead
# of walking the directory and doing fnmatch() against want.
for root, dirs, files in os.walk(LLVM_RELEASE_DIR):
dirs.sort() # Walk dirs in sorted order.
# root: third_party/llvm-build/Release+Asserts/lib/..., rel_root: lib/...
rel_root = root[len(LLVM_RELEASE_DIR)+1:]
rel_files = [os.path.join(rel_root, f) for f in files]
wanted_files = list(set(itertools.chain.from_iterable(
fnmatch.filter(rel_files, p) for p in want)))
if wanted_files:
# Guaranteed to not yet exist at this point:
os.makedirs(os.path.join(pdir, rel_root))
for f in sorted(wanted_files):
src = os.path.join(LLVM_RELEASE_DIR, f)
dest = os.path.join(pdir, f)
shutil.copy(src, dest)
# Strip libraries.
if 'libclang_rt.builtins' in f and 'android' in f:
# Keep the builtins' DWARF info for unwinding.
elif sys.platform == 'darwin' and f.endswith('.dylib'):['strip', '-x', dest])
elif (sys.platform.startswith('linux') and
os.path.splitext(f)[1] in ['.so', '.a']):[EU_STRIP, '-g', dest])
# If this is an reclient input, add it to the inputs file(s).
for tool, inputs in reclient_inputs.items():
if any(fnmatch.fnmatch(f, i) for i in inputs):
rel_input = os.path.relpath(dest, os.path.join(pdir, 'bin'))
reclient_input_strings[tool] += ('%s\n' % rel_input)
# Write the reclient inputs files.
if sys.platform != 'win32':
reclient_input_strings['clang++'] = reclient_input_strings['clang']
reclient_input_strings['clang-cl'] = reclient_input_strings['clang']
reclient_input_strings['clang-cl.exe'] = reclient_input_strings.pop('clang')
for tool, string in reclient_input_strings.items():
filename = os.path.join(pdir, 'bin', '%s_remote_toolchain_inputs' % tool)
print('%s:\n%s' % (filename, string))
with open(filename, 'w') as f:
# Set up symlinks.
if sys.platform != 'win32':
os.symlink('clang', os.path.join(pdir, 'bin', 'clang++'))
os.symlink('clang', os.path.join(pdir, 'bin', 'clang-cl'))
os.symlink('lld', os.path.join(pdir, 'bin', 'ld.lld'))
os.symlink('lld', os.path.join(pdir, 'bin', 'ld64.lld'))
os.symlink('lld', os.path.join(pdir, 'bin', 'lld-link'))
os.symlink('lld', os.path.join(pdir, 'bin', 'wasm-ld'))
os.symlink('llvm-readobj', os.path.join(pdir, 'bin', 'llvm-readelf'))
if sys.platform.startswith('linux'):
os.symlink('llvm-objcopy', os.path.join(pdir, 'bin', 'llvm-strip'))
# Make `--target=*-cros-linux-gnu` work with
os.path.join(pdir, 'lib', 'clang', RELEASE_VERSION, 'lib',
# Copy libc++ headers.
if sys.platform == 'darwin':
shutil.copytree(os.path.join(LLVM_BOOTSTRAP_INSTALL_DIR, 'include', 'c++'),
os.path.join(pdir, 'include', 'c++'))
# Create main archive.
PackageInArchive(pdir, pdir + '.tgz')
MaybeUpload(args.upload, pdir + '.tgz', gcs_platform)
# Upload build log next to it.
os.rename('buildlog.txt', pdir + '-buildlog.txt')
pdir + '-buildlog.txt',
extra_gsutil_args=['-z', 'txt'])
os.remove(pdir + '-buildlog.txt')
# Zip up llvm-code-coverage for code coverage.
code_coverage_dir = 'llvm-code-coverage-' + stamp
shutil.rmtree(code_coverage_dir, ignore_errors=True)
os.makedirs(os.path.join(code_coverage_dir, 'bin'))
for filename in ['llvm-cov', 'llvm-profdata']:
shutil.copy(os.path.join(LLVM_RELEASE_DIR, 'bin', filename + exe_ext),
os.path.join(code_coverage_dir, 'bin'))
PackageInArchive(code_coverage_dir, code_coverage_dir + '.tgz')
MaybeUpload(args.upload, code_coverage_dir + '.tgz', gcs_platform)
# Zip up llvm-objdump and related tools for sanitizer coverage and Supersize.
objdumpdir = 'llvmobjdump-' + stamp
shutil.rmtree(objdumpdir, ignore_errors=True)
os.makedirs(os.path.join(objdumpdir, 'bin'))
for filename in [
'llvm-bcanalyzer', 'llvm-cxxfilt', 'llvm-dwarfdump', 'llvm-nm',
shutil.copy(os.path.join(LLVM_RELEASE_DIR, 'bin', filename + exe_ext),
os.path.join(objdumpdir, 'bin'))
llvmobjdump_stamp_file_base = 'llvmobjdump_build_revision'
llvmobjdump_stamp_file = os.path.join(objdumpdir, llvmobjdump_stamp_file_base)
with open(llvmobjdump_stamp_file, 'w') as f:
if sys.platform != 'win32':
os.symlink('llvm-objdump', os.path.join(objdumpdir, 'bin', 'llvm-otool'))
PackageInArchive(objdumpdir, objdumpdir + '.tgz')
MaybeUpload(args.upload, objdumpdir + '.tgz', gcs_platform)
# Zip up clang-tidy for users who opt into it, and Tricium.
clang_tidy_dir = 'clang-tidy-' + stamp
shutil.rmtree(clang_tidy_dir, ignore_errors=True)
os.makedirs(os.path.join(clang_tidy_dir, 'bin'))
shutil.copy(os.path.join(LLVM_RELEASE_DIR, 'bin', 'clang-tidy' + exe_ext),
os.path.join(clang_tidy_dir, 'bin'))
PackageInArchive(clang_tidy_dir, clang_tidy_dir + '.tgz')
MaybeUpload(args.upload, clang_tidy_dir + '.tgz', gcs_platform)
# Zip up clang-format so we can update it (separately from the clang roll).
clang_format_dir = 'clang-format-' + stamp
shutil.rmtree(clang_format_dir, ignore_errors=True)
os.makedirs(os.path.join(clang_format_dir, 'bin'))
shutil.copy(os.path.join(LLVM_RELEASE_DIR, 'bin', 'clang-format' + exe_ext),
os.path.join(clang_format_dir, 'bin'))
PackageInArchive(clang_format_dir, clang_format_dir + '.tgz')
MaybeUpload(args.upload, clang_format_dir + '.tgz', gcs_platform)
# Zip up clang-libs for users who opt into it. We want Clang and LLVM headers
# and libs, as well as a couple binaries. The LLVM parts are needed by the
# Rust build.
clang_libs_dir = 'clang-libs-' + stamp
shutil.rmtree(clang_libs_dir, ignore_errors=True)
os.makedirs(os.path.join(clang_libs_dir, 'include'))
# TODO(danakj): It's possible we need to also include headers from
# LLVM_DIR/clang/lib/AST/ and other subdirs of lib, but we won't include them
# unless we see it's needed, and we can document why.
shutil.copytree(os.path.join(LLVM_DIR, 'clang', 'include', 'clang'),
os.path.join(clang_libs_dir, 'include', 'clang'))
# Copy LLVM includes. The llvm source and build directory includes must be
# merged. llvm-c for C bindings is also included.
# Headers and libs are copied from LLVM_BOOTSTRAP_DIR, not LLVM_RELEASE_DIR,
# because the release libs have LTO so they contain LLVM bitcode while the
# bootstrap libs do not. The Rust build consumes these,the first stage of
# which cannot handle newer LLVM bitcode. The stage 0 rustc is linked against
# an older LLVM.
shutil.copytree(os.path.join(LLVM_DIR, 'llvm', 'include', 'llvm'),
os.path.join(clang_libs_dir, 'include', 'llvm'))
shutil.copytree(os.path.join(LLVM_DIR, 'llvm', 'include', 'llvm-c'),
os.path.join(clang_libs_dir, 'include', 'llvm-c'))
shutil.copytree(os.path.join(LLVM_BOOTSTRAP_DIR, 'include', 'llvm'),
os.path.join(clang_libs_dir, 'include', 'llvm'),
# Copy llvm-config and FileCheck which the Rust build needs
os.makedirs(os.path.join(clang_libs_dir, 'bin'))
shutil.copy(os.path.join(LLVM_BOOTSTRAP_DIR, 'bin', 'llvm-config' + exe_ext),
os.path.join(clang_libs_dir, 'bin'))
shutil.copy(os.path.join(LLVM_BOOTSTRAP_DIR, 'bin', 'FileCheck' + exe_ext),
os.path.join(clang_libs_dir, 'bin'))
os.makedirs(os.path.join(clang_libs_dir, 'lib'))
if sys.platform == 'win32':
clang_libs_want = [
clang_libs_want = [
for lib_path in os.listdir(os.path.join(LLVM_BOOTSTRAP_DIR, 'lib')):
for lib_want in clang_libs_want:
if fnmatch.fnmatch(lib_path, lib_want):
shutil.copy(os.path.join(LLVM_BOOTSTRAP_DIR, 'lib', lib_path),
os.path.join(clang_libs_dir, 'lib'))
PackageInArchive(clang_libs_dir, clang_libs_dir + '.tgz')
MaybeUpload(args.upload, clang_libs_dir + '.tgz', gcs_platform)
if sys.platform == 'darwin':
# dsymutil isn't part of the main zip, and it gets periodically
# deployed to CIPD (manually, not as part of clang rolls) for use in the
# Mac build toolchain.
dsymdir = 'dsymutil-' + stamp
shutil.rmtree(dsymdir, ignore_errors=True)
os.makedirs(os.path.join(dsymdir, 'bin'))
shutil.copy(os.path.join(LLVM_RELEASE_DIR, 'bin', 'dsymutil'),
os.path.join(dsymdir, 'bin'))
PackageInArchive(dsymdir, dsymdir + '.tgz')
MaybeUpload(args.upload, dsymdir + '.tgz', gcs_platform)
# Zip up the translation_unit tool.
translation_unit_dir = 'translation_unit-' + stamp
shutil.rmtree(translation_unit_dir, ignore_errors=True)
os.makedirs(os.path.join(translation_unit_dir, 'bin'))
shutil.copy(os.path.join(LLVM_RELEASE_DIR, 'bin', 'translation_unit' +
os.path.join(translation_unit_dir, 'bin'))
PackageInArchive(translation_unit_dir, translation_unit_dir + '.tgz')
MaybeUpload(args.upload, translation_unit_dir + '.tgz', gcs_platform)
# Zip up the libclang binaries.
libclang_dir = 'libclang-' + stamp
shutil.rmtree(libclang_dir, ignore_errors=True)
os.makedirs(os.path.join(libclang_dir, 'bin'))
os.makedirs(os.path.join(libclang_dir, 'bindings', 'python', 'clang'))
if sys.platform == 'win32':
shutil.copy(os.path.join(LLVM_RELEASE_DIR, 'bin', 'libclang.dll'),
os.path.join(libclang_dir, 'bin'))
for filename in ['', '', '']:
shutil.copy(os.path.join(LLVM_DIR, 'clang', 'bindings', 'python', 'clang',
os.path.join(libclang_dir, 'bindings', 'python', 'clang'))
PackageInArchive(libclang_dir, libclang_dir + '.tgz')
MaybeUpload(args.upload, libclang_dir + '.tgz', gcs_platform)
if sys.platform == 'win32' and args.upload:
binaries = [f for f in want if f.endswith('.exe') or f.endswith('.dll')]
assert 'bin/clang-cl.exe' in binaries
assert 'bin/lld-link.exe' in binaries
start = time.time()
end = time.time()
print('symbol upload took', end - start, 'seconds')
# FIXME: Warn if the file already exists on the server.
if __name__ == '__main__':