blob: d906adb499f0890063def61efb5ada8131ac509c [file] [log] [blame]
# Copyright 2018 The Fuchsia Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Recipe for building GCC toolchain."""
from contextlib import contextmanager
from recipe_engine.recipe_api import StepFailure
import pipes
import re
DEPS = [
'fuchsia/git',
'fuchsia/gitiles',
'fuchsia/goma',
'fuchsia/hash',
'fuchsia/jiri',
'fuchsia/macos_sdk',
'fuchsia/upload',
'recipe_engine/buildbucket',
'recipe_engine/cipd',
'recipe_engine/context',
'recipe_engine/file',
'recipe_engine/json',
'recipe_engine/path',
'recipe_engine/platform',
'recipe_engine/properties',
'recipe_engine/raw_io',
'recipe_engine/runtime',
'recipe_engine/scheduler',
'recipe_engine/step',
'recipe_engine/url',
]
BINUTILS_GIT = 'https://gnu.googlesource.com/binutils-gdb'
BINUTILS_REF = 'refs/heads/binutils-2_34-branch'
GCC_GIT = 'https://gnu.googlesource.com/gcc'
GCC_REF = 'refs/heads/releases/gcc-9'
# Bump this number whenever changing this recipe in ways that affect the
# package built without also changing any upstream revision pin.
# TODO(crbug.com/947158): Remove this when the recipes repo/rev are available.
RECIPE_SALT = ''
def RunSteps(api):
use_goma = api.platform.arch != 'arm'
if use_goma:
api.goma.ensure()
compile_jobs = api.goma.jobs
goma_context = lambda: api.goma.build_with_goma()
else:
compile_jobs = api.platform.cpu_count
@contextmanager
def null_context():
yield
goma_context = null_context
prod = api.buildbucket.builder_id.bucket == 'prod'
binutils_dir = api.path['start_dir'].join('binutils-gdb')
gcc_dir = api.path['start_dir'].join('gcc')
projects = {
'binutils-gdb': (BINUTILS_GIT, binutils_dir, BINUTILS_REF),
'gcc': (GCC_GIT, gcc_dir, GCC_REF),
}
gitiles_commit = api.buildbucket.build_input.gitiles_commit
if gitiles_commit.host and gitiles_commit.project and gitiles_commit.id:
projects[gitiles_commit.project] = (
'https://%s/%s' % (gitiles_commit.host, gitiles_commit.project),
api.path['start_dir'].join(gitiles_commit.project), gitiles_commit.id)
with api.context(infra_steps=True):
with api.step.nest('binutils-gdb'):
binutils_revision = api.git.checkout(*projects['binutils-gdb'])
binutils_pkgversion = '%s %s' % (projects['binutils-gdb'][0],
binutils_revision)
with api.step.nest('gcc'):
gcc_revision = api.git.checkout(*projects['gcc'])
gcc_pkgversion = '%s %s' % (projects['gcc'][0], gcc_revision)
with api.step.nest('ensure_packages'):
with api.context(infra_steps=True):
pkgs = api.cipd.EnsureFile()
pkgs.add_package(
'fuchsia/third_party/clang/${platform}', 'goma' if use_goma else
'git_revision:ea93d7d6421612e9ea51b321eaf97fbdd64fe39b')
if api.platform.name == 'linux':
pkgs.add_package('fuchsia/sysroot/${platform}', 'latest')
cipd_dir = api.path['start_dir'].join('cipd')
api.cipd.ensure(cipd_dir, pkgs)
if api.platform.name == 'linux':
host_sysroot = cipd_dir
elif api.platform.name == 'mac':
# TODO(IN-148): Eventually use our own hermetic sysroot as for Linux.
with api.macos_sdk():
step_result = api.step(
'xcrun', ['xcrun', '--show-sdk-path'],
stdout=api.raw_io.output(name='sdk-path', add_output_log=True),
step_test_data=lambda: api.raw_io.test_api.stream_output(
'/some/xcode/path'))
host_sysroot = step_result.stdout.strip()
else: # pragma: no cover
assert False, 'what platform?'
with api.context(cwd=gcc_dir):
# download GCC dependencies: GMP, ISL, MPC and MPFR libraries
api.step('download prerequisites',
[gcc_dir.join('contrib', 'download_prerequisites')])
staging_dir = api.path.mkdtemp('gcc')
pkg_name = 'gcc-%s' % api.platform.name.replace('mac', 'darwin')
pkg_dir = staging_dir.join(pkg_name)
api.file.ensure_directory('create pkg dir', pkg_dir)
# Some of the makefile logic splits $CC at its first word and injects
# a switch there. So make $CC and $CXX be single words by writing
# little scripts. Autoconf does some checks with CPPFLAGS but not
# CFLAGS and other checks with CFLAGS but not CPPFLAGS. The sysroot
# is necessary for all cases, so fold that into the script too so it's
# impossible to omit it in any $CC or $CXX invocation.
cc_path = staging_dir.join('host-cc')
cxx_path = staging_dir.join('host-cxx')
gomacc_path = api.goma.goma_dir.join('gomacc') if use_goma else ''
for script, compiler in [(cc_path, 'clang'), (cxx_path, 'clang++')]:
compiler_path = cipd_dir.join('bin', compiler)
if api.platform.name == 'mac' and compiler == 'clang++':
# Our host toolchain for Mac provides static libc++ but doesn't know how
# to link it in by itself. Things in LIBS or LDFLAGS get put onto `ar`
# command lines for static libraries, which doesn't go well. A link
# input on non-linking clang++ command lines is useless though harmless,
# but it generates a lot of warning noise that makes the build logs hard
# to read and slow to collect. So the wrapper script tries to add it
# only to linking command lines.
script_text = """#!/bin/sh
extra=(-nostdlib++ %s)
for arg; do
case "$arg" in
-[cE])
extra=()
break
;;
esac
done
exec %s %s --sysroot=%s "$@" "${extra[@]}"
""" % (cipd_dir.join('lib',
'libc++.a'), gomacc_path, compiler_path, host_sysroot)
else:
script_text = """#!/bin/sh
exec %s %s --sysroot=%s "$@"
""" % (gomacc_path, compiler_path, host_sysroot)
api.file.write_text('write %s script' % api.path.basename(script), script,
script_text)
api.step('make %s executable' % api.path.basename(script),
['chmod', '+x', script])
host_cflags = '-O3'
if api.platform.name != 'mac':
# LTO works for binutils on Linux but fails on macOS.
host_cflags += ' -flto'
host_compiler_args = {
'CC': '%s' % cc_path,
'CXX': '%s' % cxx_path,
'CFLAGS': host_cflags,
'CXXFLAGS': host_cflags,
}
if api.platform.name != 'mac':
# Always link libc++ statically in case a shared library is available.
host_compiler_args['CXXFLAGS'] += ' -static-libstdc++'
host_compiler_args = sorted(
'%s=%s' % item for item in host_compiler_args.iteritems())
# We force LIMITS_H_TEST=true to tell the compiler to install a <limits.h>
# that requires a system <limits.h> it can find via #include_next. But we
# don't have any system headers for the just-built compiler building target
# code (i.e. libgcc). So mock up a little include directory that contains
# just an empty <limits.h> for it to find.
mock_include_dir = staging_dir.join('mock-include')
api.file.ensure_directory('create mock include dir', mock_include_dir)
api.file.write_text('write dummy <limits.h>',
mock_include_dir.join('limits.h'), '/* dummy */')
with api.macos_sdk(), goma_context():
for target, enable_targets in [('aarch64', 'arm-eabi'),
('x86_64', 'x86_64-pep')]:
# configure arguments that are the same for binutils and gcc.
common_args = host_compiler_args + [
'--prefix=', # we're building a relocatable package
'--target=%s-elf' % target,
'--enable-initfini-array', # Fuchsia uses .init/.fini arrays
'--enable-gold', # Zircon uses gold for userspace build
# Enable plugins and threading for Gold.
# This also happens to make it explicitly link in -lpthread and -dl,
# which are required by host_clang's static libc++.
'--enable-plugins',
'--enable-threads',
'--disable-werror', # ignore warnings reported by Clang
'--disable-nls', # no need for localization
'--with-included-gettext', # use include gettext library
]
# build binutils
with api.step.nest('%s binutils' % target):
binutils_build_dir = staging_dir.join('binutils_%s_build_dir' % target)
api.file.ensure_directory('create build dir', binutils_build_dir)
with api.context(cwd=binutils_build_dir):
def binutils_make_step(name, prefix, jobs, make_args=None):
# As of 2.32, gold/testsuite/Makefile.am unconditionally edits in
# a -B.../ switch to make the compiler use the just-built gold as
# the linker for test suite binaries. This is wrong when
# building a cross-linker. Force it to a no-op on the make
# command line to work around the bug.
# TODO(mcgrathr): Drop this when we roll to a binutils that has
# this fixed upstream.
make_args = make_args or []
make_args.append('MAKEOVERRIDES=editcc=-eb')
return api.step(name, ['make', '-j%s' % jobs] + make_args + [
'%s-%s' % (prefix, component)
for component in ['binutils', 'gas', 'ld', 'gold']
])
api.step(
'configure',
[
binutils_dir.join('configure'),
'--with-pkgversion=%s' % binutils_pkgversion,
'--enable-deterministic-archives', # more deterministic builds
'--enable-targets=%s' % enable_targets,
] + common_args)
binutils_make_step('build', 'all', compile_jobs)
try:
binutils_make_step('test', 'check', api.platform.cpu_count, ['-k'])
except StepFailure as error:
logs = {
l[0]:
api.file.read_text('/'.join(l),
binutils_build_dir.join(*l)).splitlines()
for l in [
('gas', 'testsuite', 'gas.log'),
('binutils', 'binutils.log'),
('ld', 'ld.log'),
('gold', 'testsuite', 'test-suite.log'),
]
}
step_result = api.step('binutils test failure', cmd=None)
for name, text in logs.iteritems():
step_result.presentation.logs[name] = text
raise error
binutils_make_step('install', 'install-strip', 1,
['DESTDIR=%s' % pkg_dir])
# build gcc
with api.step.nest('%s gcc' % target):
gcc_build_dir = staging_dir.join('gcc_%s_build_dir' % target)
api.file.ensure_directory('create build dir', gcc_build_dir)
with api.context(
cwd=gcc_build_dir, env_prefixes={'PATH': [pkg_dir.join('bin')]}):
gcc_goals = ['gcc', 'target-libgcc']
def gcc_make_step(name, jobs, args, **kwargs):
cmd = [
'make',
'-j%s' % jobs,
'MAKEOVERRIDES=USE_GCC_STDINT=provide LIMITS_H_TEST=true',
# Force flags for libgcc to get <limits.h> kludge.
'CPPFLAGS_FOR_TARGET=-idirafter %s' % mock_include_dir,
]
# Recipes won't let us capture stdout and stderr as a single stream,
# so use shell redirection to put stderr onto stdout.
cmd = [
'/bin/sh', '-c',
' '.join([pipes.quote(s) for s in cmd + args] + ['2>&1'])
]
return api.step(name, cmd, **kwargs)
api.step(
'configure',
[
gcc_dir.join('configure'),
'--with-pkgversion=%s' % gcc_pkgversion,
'--enable-languages=c,c++',
'--disable-libstdcxx', # we don't need libstdc++
'--disable-libssp', # we don't need libssp either
'--disable-libquadmath', # and nor do we need libquadmath
] + common_args + (
# The runtimes must observe the Fuchsia ABI, which reserves x18.
['CFLAGS_FOR_TARGET=-g -O2 -ffixed-x18']
if target == 'aarch64' else []))
gcc_make_step(
'build',
compile_jobs, ['all-%s' % goal for goal in gcc_goals],
stdout=api.raw_io.output(
name='%s gcc build log' % target, add_output_log=True))
# Skip tests on Mac because they are unreasonably slow.
if api.platform.name != 'mac':
try:
gcc_make_step('test', api.platform.cpu_count, ['check-gcc'])
finally:
logs = {
l[-1]:
api.file.read_text('gcc %s %s' % (target, '/'.join(l)),
gcc_build_dir.join(*l)).splitlines()
for l in [
('gcc', 'testsuite', 'gcc', 'gcc.log'),
('gcc', 'testsuite', 'g++', 'g++.log'),
]
}
step_result = api.step('test logs', cmd=None)
for name, text in logs.iteritems():
step_result.presentation.logs[name] = text
gcc_make_step('install', 1, ['DESTDIR=%s' % pkg_dir] +
['install-strip-%s' % goal for goal in gcc_goals])
binutils_version = api.file.read_text(
'binutils version',
binutils_dir.join('bfd', 'version.m4'),
test_data='m4_define([BFD_VERSION], [2.27.0])')
m = re.match(r'm4_define\(\[BFD_VERSION\], \[([^]]+)\]\)', binutils_version)
assert m and m.group(1), ('bfd/version.m4 has unexpected format: %r' %
binutils_version)
binutils_version = m.group(1)
gcc_version = api.file.read_text('gcc version',
gcc_dir.join('gcc', 'BASE-VER')).rstrip()
version = ','.join([gcc_version, binutils_version])
cipd_git_repository = ','.join([GCC_GIT, BINUTILS_GIT])
cipd_git_revision = ','.join([gcc_revision, binutils_revision])
# Add a bogus "salt" repository/revision to represent recipe changes when
# the upstream revisions haven't changed. Bump this number whenever
# changing this recipe in ways that affect the package built without also
# changing any upstream revision pin.
# TODO(crbug.com/947158): Replace this with the recipes repo/rev when
# infra makes that available here.
if RECIPE_SALT: # pragma: no cover
cipd_git_repository += ',<salt>'
cipd_git_revision += ',%s' % RECIPE_SALT
isolated = api.upload.upload_isolated(pkg_dir)
# The prod builders actually publish to CIPD. The CI builders may run
# quicker and they are the ones that trigger the test builds using the
# toolchain just built.
if prod:
api.upload.cipd_package(
'fuchsia/third_party/gcc/${platform}',
pkg_dir, [api.upload.DirectoryPath(pkg_dir)],
{'git_revision': cipd_git_revision},
repository=cipd_git_repository,
extra_tags={'version': version})
elif api.platform.name == 'linux' and api.platform.arch == 'intel':
host = 'fuchsia.googlesource.com'
project = 'fuchsia'
refs = api.gitiles.refs('https://%s/%s' % (host, project))
ref = 'refs/heads/master'
revision = refs.get(ref, 'HEAD')
# Do a full integration build. This will use the just-built toolchain
# to build all of Fuchsia to check whether there are any regressions.
#
# TODO(43620): This is modelled on the clang_toolchain.py code.
# Ideally this logic would be shared somewhere in a recipe module,
# perhaps just api.build. Ideally the list of bots to test for each
# toolchain would be shared with/generated from fuchsia.star directly.
api.scheduler.emit_trigger(
api.scheduler.BuildbucketTrigger(
properties={
'gcc_toolchain': {
'git_repository': cipd_git_repository,
'git_revision': cipd_git_revision,
},
'build.gcc_toolchain': {
'type': 'isolated',
'instance': isolated,
},
'checkout.buildset':
'commit/gitiles/%s/%s/+/%s' % (host, project, revision),
},
tags={
'buildset':
'commit/gitiles/%s/+/%s' %
(GCC_GIT.split('://')[1], gcc_revision),
'gitiles_ref':
ref,
}),
project=project,
jobs=[
'gcc_toolchain.%s' % bot for bot in [
'bringup.arm64-gcc-qemu_kvm',
'bringup.x64-gcc-qemu_kvm',
'zbi_tests-arm64-gcc',
'zbi_tests-x64-gcc',
]
])
def GenTests(api):
binutils_revision = '3d861fdb826c2f5cf270dd5f585d0e6057e1bf4f'
gcc_revision = '4b5e15daff8b54440e3fda451c318ad31e532fab'
cipd_revision = ','.join([gcc_revision, binutils_revision])
if RECIPE_SALT: # pragma: no cover
cipd_revision += ',%s' % RECIPE_SALT
for bucket in ('ci', 'prod'):
for platform, arch in (('linux', 'intel'), ('linux', 'arm'), ('mac',
'intel')):
def Test(name, git_repo=GCC_GIT, revision=gcc_revision, fail=False):
# The point of this helper is to capture the loop variables.
# pylint: disable=cell-var-from-loop
result = (
api.test('%s_%s' % (bucket, name)) + api.buildbucket.ci_build(
project='fuchsia',
bucket=bucket,
git_repo=git_repo,
revision=revision,
) + api.runtime(is_luci=True, is_experimental=False) +
api.platform.name(platform) + api.platform.arch(arch) +
api.platform.bits(64))
# pylint: disable=cell-var-from-loop
if bucket == 'ci' and not fail and platform == 'linux' and arch == 'intel':
result += api.gitiles.refs('refs', ('refs/heads/master', 'b' * 40))
return result
yield (Test('%s_%s' % (platform, arch)) +
api.step_data('binutils-gdb.git rev-parse',
api.raw_io.stream_output(binutils_revision)) +
api.step_data('gcc.git rev-parse',
api.raw_io.stream_output(gcc_revision)))
for salt in ['', 999] + ([RECIPE_SALT] if RECIPE_SALT else []):
salt = '%s' % salt
if bucket == 'prod':
yield (
Test('%s_%s_new%s' % (platform, arch, salt)) +
api.step_data('binutils-gdb.git rev-parse',
api.raw_io.stream_output(binutils_revision)) +
api.step_data('gcc.git rev-parse',
api.raw_io.stream_output(gcc_revision)) +
api.step_data(
'cipd.cipd search fuchsia/third_party/gcc/${platform} git_revision:'
+ cipd_revision, api.json.output({'result': []})) +
api.step_data('gcc version', api.file.read_text('7.1.2\n')))
yield (Test(
'%s_%s_binutils_test_fail%s' % (platform, arch, salt),
git_repo=BINUTILS_GIT,
revision=binutils_revision,
fail=True) + api.step_data('gcc.git rev-parse',
api.raw_io.stream_output(gcc_revision)) +
api.step_data('x86_64 binutils.test', retcode=1))
if platform != 'mac':
yield (
Test('%s_%s_gcc_test_fail%s' %
(platform, arch, salt), fail=True) +
api.step_data('binutils-gdb.git rev-parse',
api.raw_io.stream_output(binutils_revision)) +
api.step_data('aarch64 gcc.test', retcode=1))