blob: 5c99c685f0fe56a9358591506ae4523ea0d6dabf [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."""
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:]))