blob: 84559c402891f9eedb779c3bb35e114669788e8c [file] [log] [blame]
#!/usr/bin/env python3
# Copyright 2021 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Builds the clang toolchain for Ti50."""
# pylint: disable=cros-logging-import
import argparse
import logging
import os
from pathlib import Path
import shlex
import shutil
import subprocess
import sys
from typing import List
# CFlags used when building RISCV runtime libraries.
RISCV_RUNTIME_CFLAGS = (
'-O2',
'--target=riscv32-unknown-elf',
'-DVISIBILITY_HIDDEN',
'-DNDEBUG',
'-fno-builtin',
'-fvisibility=hidden',
'-fomit-frame-pointer',
'-mno-relax',
'-fforce-enable-int128',
'-DCRT_HAS_INITFINI_ARRAY',
'-march=rv32imcxsoteria',
'-ffunction-sections',
'-fdata-sections',
'-fstack-size-section',
'-mcmodel=medlow',
'-Wno-unused-command-line-argument',
)
# Flags passed to cmake command for building LLVM
# TODO(sukhomlinov): These args are copy-pasted; they might not all be
# necessary.
CMAKE_FLAGS = (
'-G',
'Ninja',
'-DCMAKE_BUILD_TYPE=Release',
'-DLLVM_ENABLE_LIBCXX=ON',
'-DLLVM_OPTIMIZED_TABLEGEN=ON',
'-DLLVM_BUILD_TESTS=OFF',
'-DCLANG_ENABLE_STATIC_ANALYZER=ON',
'-DCLANG_DEFAULT_RTLIB="compiler-rt"',
'-DLLVM_DEFAULT_TARGET_TRIPLE=riscv32-unknown-elf',
'-DLLVM_INSTALL_BINUTILS_SYMLINKS=ON',
'-DLLVM_INSTALL_CCTOOLS_SYMLINKS=ON',
'-DCLANG_DEFAULT_LINKER=ld.lld',
'-DLLVM_ENABLE_BACKTRACES=OFF',
'-DLLVM_INCLUDE_EXAMPLES=OFF',
'-DLLVM_DYLIB_COMPONENTS=""',
'-DLLVM_LINK_LLVM_DYLIB=OFF',
'-DLLVM_BUILD_STATIC=OFF',
'-DLLVM_INSTALL_UTILS=ON',
'-DLLVM_ENABLE_Z3_SOLVER=OFF',
'-DLLVM_USE_RELATIVE_PATHS_IN_FILES=ON',
'-DLLVM_TARGETS_TO_BUILD=RISCV;X86',
'-DLLVM_ENABLE_PROJECTS=clang;clang-tools-extra;lldb;lld;polly',
)
# Flags for configuring newlib.
NEWLIB_CONFIG_FLAGS = (
'--target=riscv32-unknown-elf',
'--enable-newlib-reent-small',
'--disable-newlib-fvwrite-in-streamio',
'--disable-newlib-fseek-optimization',
'--disable-newlib-wide-orient',
'--enable-newlib-nano-malloc',
'--disable-newlib-unbuf-stream-opt',
'--enable-lite-exit',
'--enable-newlib-global-atexit',
'--enable-newlib-nano-formatted-io',
'--disable-newlib-supplied-syscalls',
'--disable-nls',
'--enable-newlib-io-c99-formats',
'--enable-newlib-io-long-long',
)
def remove_path_if_exists(file_or_dir: Path):
"""Removes the given file/directory/symlink if it exists."""
if file_or_dir.is_file() or file_or_dir.is_symlink():
file_or_dir.unlink()
elif file_or_dir.is_dir():
shutil.rmtree(file_or_dir)
else:
assert not file_or_dir.exists(), file_or_dir
def find_clang_resource_dir(llvm_install_dir: Path) -> Path:
"""Locates the resource directory of the clang in |llvm_install_dir|."""
return Path(
subprocess.check_output(
[str(llvm_install_dir / 'bin' / 'clang'), '-print-resource-dir'],
encoding='utf-8',
).strip())
def build_llvm(llvm_dir: Path, build_dir: Path, install_dir: Path):
"""Builds and installs LLVM to install_dir, plus crt{begin,end} for RISCV."""
build_dir.mkdir(parents=True, exist_ok=True)
if not (build_dir / 'build.ninja').exists():
logging.info('Configuring LLVM')
cmd = [
'cmake',
f'-DCMAKE_INSTALL_PREFIX={install_dir}',
f'-DDEFAULT_SYSROOT={install_dir}',
f'-DGCC_INSTALL_PREFIX={install_dir}',
str(llvm_dir / 'llvm'),
]
cmd += CMAKE_FLAGS
subprocess.check_call(cmd, cwd=build_dir)
logging.info('Building + installing LLVM')
subprocess.check_call(['ninja', 'install'], cwd=build_dir)
# And now we build the runtime library. Whee.
clang_rtlib_prefix = [
str(install_dir / 'bin' / 'clang'),
'-c',
'-v',
]
clang_rtlib_prefix += RISCV_RUNTIME_CFLAGS
libdir = find_clang_resource_dir(install_dir)
(libdir / 'lib').mkdir(parents=True, exist_ok=True)
subprocess.check_call(clang_rtlib_prefix + [
str(llvm_dir / 'compiler-rt' / 'lib' / 'crt' / 'crtbegin.c'),
'-o',
str(libdir / 'lib' / 'clang_rt.crtbegin-riscv32.o'),
])
subprocess.check_call(clang_rtlib_prefix + [
str(llvm_dir / 'compiler-rt' / 'lib' / 'crt' / 'crtend.c'),
'-o',
str(libdir / 'lib' / 'clang_rt.crtend-riscv32.o'),
])
def build_newlib(rv_clang_bin: Path, newlib_dir: Path, newlib_build_dir: Path,
install_dir: Path):
"""Builds and installs newlib into |install_dir|."""
newlib_build_dir.mkdir(parents=True)
logging.info("Setting up newlib's build")
env_with_rv_clang = os.environ.copy()
# ...I don't *want* to have all of this in the environment, but if all of
# this (ignoring PATH) is provided to |./configure| as args, |make| gets
# angry that it doesn't see the same values in its environment when invoked.
#
# TODO(sukhomlinov): This seems like brokenness in newlib, maybe?
env_with_rv_clang.update({
'LDFLAGS': f'{env_with_rv_clang.get("LDFLAGS", "")} -fuse-ld=lld',
'PATH': f'{rv_clang_bin}:{env_with_rv_clang["PATH"]}',
})
tool_substitutions = (
('AR', 'llvm-ar'),
('AS', 'llvm-as'),
('NM', 'llvm-nm'),
('OBJDUMP', 'llvm-objdump'),
('RANLIB', 'llvm-ranlib'),
)
for var_name, executable in tool_substitutions:
executable_path = install_dir / 'bin' / executable
env_with_rv_clang.update({
var_name: executable_path,
f'{var_name}_FOR_TARGET': executable_path,
})
env_with_rv_clang.update({
f'{var_name}_FOR_TARGET': install_dir / 'bin' / exe
for var_name, exe in (('CC', 'clang'), ('CXX', 'clang++'))
})
cmd = [
'./configure',
f'--prefix={newlib_build_dir}',
f'CFLAGS_FOR_TARGET={" ".join(RISCV_RUNTIME_CFLAGS)}',
f'CXXFLAGS_FOR_TARGET={" ".join(RISCV_RUNTIME_CFLAGS)}',
'LDFLAGS=-fuse-ld=lld',
'LDFLAGS_FOR_TARGET=-fuse-ld=lld',
]
cmd += NEWLIB_CONFIG_FLAGS
subprocess.check_call(cmd, cwd=newlib_dir, env=env_with_rv_clang)
logging.info('Building newlib')
jobs = f'-j{os.cpu_count()}'
subprocess.check_call(['make', jobs], cwd=newlib_dir, env=env_with_rv_clang)
subprocess.check_call(['make', jobs, 'install'],
cwd=newlib_dir,
env=env_with_rv_clang)
logging.info("Fixing newlib's installation")
# Quoting the script that I'm basing this off of:
# """
# move includes/libs to where they can be found by default
# this is also a hack, but avoid adding paths explicitly
# This is because riscv32-unknown-elf target only use
# $INSTALL_DIR/lib and $INSTALL_DIR/lib/clang/11.0.0/lib paths,
# ignoring --libdir and --sysroot for cmake configure
# $INSTALL_DIR/include contains llvm headers not used for C compilation
# normally, unless you build llvm tools, but directory is in default
# search path, so use it for libc headers.
# """
installed_include_dir = install_dir / 'include'
old_include_dir = install_dir / 'include.old'
if installed_include_dir.exists():
shutil.move(installed_include_dir, old_include_dir)
shutil.copytree(newlib_build_dir / 'riscv32-unknown-elf' / 'include',
installed_include_dir)
if old_include_dir.exists():
shutil.rmtree(old_include_dir)
for f in (install_dir / 'lib').glob('*.a'):
f.unlink()
lib_base = install_dir / 'lib'
for f in (newlib_build_dir / 'riscv32-unknown-elf' / 'lib').iterdir():
shutil.copy2(f, lib_base / f.name)
def build_compiler_rt(llvm_path: Path, compiler_rt_build_dir: Path,
install_dir: Path):
"""Builds compiler_rt and installs artifacts in |install_dir|."""
compiler_rt_build_dir.mkdir(parents=True)
run_clang = [str(install_dir / 'bin' / 'clang'), '-c']
run_clang += RISCV_RUNTIME_CFLAGS
compiler_rt_builtins = llvm_path / 'compiler-rt' / 'lib' / 'builtins'
subprocess.check_call(
run_clang + [
str(compiler_rt_builtins / 'riscv' / 'mulsi3.S'),
],
cwd=compiler_rt_build_dir,
)
# It's expected that some of these builds fail for various reasons (no atomic
# uint64_t on riscv, etc.)
expected_failures = {
'atomic_flag_clear.c',
'atomic_flag_clear_explicit.c',
'atomic_flag_test_and_set.c',
'atomic_flag_test_and_set_explicit.c',
'atomic_signal_fence.c',
'atomic_thread_fence.c',
'emutls.c',
'enable_execute_stack.c',
}
had_unexpected_results = False
for f in compiler_rt_builtins.glob('*.c'):
# Build the expected failures, since doing so is quick, and if they _do_
# successfully build, that sounds like smoke to me.
command = run_clang + [str(f)]
should_fail = f.name in expected_failures
command_string = ' '.join(shlex.quote(x) for x in command)
try:
stdout = subprocess.check_output(
command,
cwd=compiler_rt_build_dir,
stderr=subprocess.STDOUT,
encoding='utf-8',
)
except subprocess.CalledProcessError as e:
if should_fail:
logging.info('Running %s failed as expected', command_string)
continue
logging.error('Running %s failed unexpectedly; output:\n%s',
command_string, e.stdout)
had_unexpected_results = True
else:
if not should_fail:
logging.info('Running %s succeeded as expected', command_string)
continue
logging.error('Running %s succeeded unexpectedly; output:\n%s',
command_string, stdout)
had_unexpected_results = True
if had_unexpected_results:
raise ValueError(
"Manual builds of compiler-rt bits didn't go as planned; please see "
'logging output')
libdir = find_clang_resource_dir(install_dir)
link_command = [
str(install_dir / 'bin' / 'llvm-ar'),
'rc',
str(libdir / 'libclang_rt.builtins-riscv32.a'),
]
link_command += (str(x) for x in compiler_rt_build_dir.glob('*.o'))
subprocess.check_call(link_command)
def log_install_paths(install_dir: Path):
"""Prints install paths to stdout."""
clang = str(install_dir / 'bin' / 'clang')
subprocess.check_call([clang, '-E', '-x', 'c++', '/dev/null', '-v'])
subprocess.check_call([clang, '-print-search-dirs'])
def get_parser():
"""Creates a parser for commandline args."""
parser = argparse.ArgumentParser(
description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument(
'--work-dir',
required=True,
type=Path,
help='Path to put build artifacts in.',
)
parser.add_argument(
'--llvm-dir',
required=True,
type=Path,
help='Path to the LLVM checkout.',
)
parser.add_argument(
'--newlib-dir',
required=True,
type=Path,
help='Path to the newlib checkout.',
)
parser.add_argument(
'--no-clean-llvm',
action='store_false',
dest='clean_llvm',
help="Don't wipe out LLVM's build directory if it exists.",
)
parser.add_argument(
'--no-clean-newlib',
action='store_false',
dest='clean_newlib',
help="Don't wipe out newlib's build directory if it exists.",
)
parser.add_argument(
'--install-dir',
required=True,
type=Path,
help='Path to the place to install artifacts.',
)
return parser
def main(argv: List[str]):
logging.basicConfig(
format='%(asctime)s: %(levelname)s: %(filename)s:%(lineno)d: %(message)s',
level=logging.INFO,
)
opts = get_parser().parse_args(argv)
work_dir = opts.work_dir
install_dir = opts.install_dir.resolve()
llvm_path = opts.llvm_dir.resolve()
newlib_path = opts.newlib_dir.resolve()
work_dir.mkdir(parents=True, exist_ok=True)
remove_path_if_exists(install_dir)
install_dir.mkdir(parents=True)
llvm_build_dir = work_dir / 'llvm-build' / 'Release'
if opts.clean_llvm:
remove_path_if_exists(llvm_build_dir)
build_llvm(llvm_path, llvm_build_dir, install_dir)
newlib_build_dir = work_dir / 'newlib' / 'build'
if opts.clean_newlib:
# Ugly: newlib builds to _both_ the build dir and an in-git-directory dir;
# would be nice if it only used one.
remove_path_if_exists(newlib_build_dir)
# In an |emerge| flow, we don't need this, but repeated |ebuild|s could
# leave us with a dirty source directory.
if (newlib_path / 'Makefile').exists():
remove_path_if_exists(newlib_path / 'riscv32-unknown-elf')
subprocess.check_call(['make', 'distclean'], cwd=newlib_path)
build_newlib(
rv_clang_bin=install_dir / 'bin',
newlib_dir=newlib_path,
newlib_build_dir=newlib_build_dir,
install_dir=install_dir,
)
compiler_rt_build_dir = work_dir / 'compiler-rt-build'
remove_path_if_exists(compiler_rt_build_dir)
build_compiler_rt(llvm_path, compiler_rt_build_dir, install_dir)
log_install_paths(install_dir)
if __name__ == '__main__':
sys.exit(main(sys.argv[1:]))