Files
carbon-lang/bazel/cc_toolchains/clang_configuration.bzl
T
Chandler Carruth 2787089247 Switch to a Bazel-based runtimes build, and add bootstrapping (#6989)
This also switches to a more Bazel-based install layout, skipping the
FHS-based synthetic layout. The FHS-based layout is still reconstructed
explicitly when building an installable tar-ball.

The biggest change is to configure the just-built install as a Bazel
toolchain, including allowing it to build its own runtime libraries as
native Bazel libraries. This removes the need for a monolithic runtimes
build, all of that code logic is removed.

This should also pave the way to using the just-built toolchain for
doing a full 3-stage bootstrap. Building the 2nd stage is included here
as it was a particularly effective way to test that the Bazel
integration was fully working. Adding a 3rd-stage check for stability is
future work, but should be pretty easy.

There is a down-side: this uses the busybox to do the runtimes
compilation, which means they will be re-built after ~any change to
Carbon. However, the integration with Bazel should largely pay for this,
and we can continue to factor the tests away from depending on built
runtimes in most cases.

Now that we're building and testing the runtimes more directly, this
surfaced a problem with the layout of runtimes on macOS that is fixed
here. All of the Darwin OSes use a custom layout for their resource
directory compared to other targets. We now model this in both the C++
built runtimes and the Bazel built runtimes.

Assisted-by: Gemini via Antigravity
2026-03-31 23:38:03 +00:00

286 lines
11 KiB
Python

# Part of the Carbon Language project, under the Apache License v2.0 with LLVM
# Exceptions. See /LICENSE for license information.
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
"""Starlark repository rules to configure Clang (and LLVM) toolchain for Bazel.
These rules should be run from the `WORKSPACE` file to substitute appropriate
configured values into a `clang_detected_variables.bzl` file that can be used
by the actual toolchain configuration.
"""
def _run(repository_ctx, cmd):
"""Runs the provided `cmd`, checks for failure, and returns the result."""
exec_result = repository_ctx.execute(cmd)
if exec_result.return_code != 0:
fail("Command failed with return code {0}: {1}\n{2}".format(
exec_result.return_code,
str(cmd),
exec_result.stderr,
))
return exec_result
def _clang_version(version_output):
"""Returns version information, or a (None, "unknown") tuple if not found.
Returns both the major version number (16) and the full version number for
caching.
"""
clang_version = None
clang_version_for_cache = "unknown"
version_prefix = "clang version "
version_start = version_output.find(version_prefix)
if version_start == -1:
# No version
return (clang_version, clang_version_for_cache)
version_start += len(version_prefix)
# Find the newline.
version_newline = version_output.find("\n", version_start)
if version_newline == -1:
return (clang_version, clang_version_for_cache)
clang_version_for_cache = version_output[version_start:version_newline]
# Find a dot to indicate something like 'clang version 16.0.1', and grab the
# major version.
version_dot = version_output.find(".", version_start)
if version_dot != -1 and version_dot < version_newline:
clang_version = int(version_output[version_start:version_dot])
return (clang_version, clang_version_for_cache)
def _detect_system_clang(repository_ctx):
"""Detects whether the system-provided clang can be used.
Returns a tuple of (is_clang, environment).
"""
# If the user provides an explicit `CC` environment variable, use that as
# the compiler. This should point at the `clang` executable to use.
cc = repository_ctx.os.environ.get("CC")
cc_path = None
if cc:
cc_path = repository_ctx.path(cc)
if not cc_path.exists:
cc_path = repository_ctx.which(cc)
if not cc_path:
cc_path = repository_ctx.which("clang")
if not cc_path:
fail("Cannot find clang or CC (%s); either correct your path or set the CC environment variable" % cc)
version_output = _run(repository_ctx, [cc_path, "--version"]).stdout
if "clang" not in version_output:
fail("Searching for clang or CC (%s), and found (%s), which is not a Clang compiler" % (cc, cc_path))
clang_version, clang_version_for_cache = _clang_version(version_output)
return (cc_path.realpath, clang_version, clang_version_for_cache)
def _compute_clang_resource_dir(repository_ctx, clang):
"""Runs the `clang` binary to get its resource dir."""
output = _run(
repository_ctx,
[clang, "-no-canonical-prefixes", "--print-resource-dir"],
).stdout
# The only line printed is this path.
return output.splitlines()[0]
def _compute_mac_os_sysroot(repository_ctx):
"""Runs `xcrun` to extract the correct sysroot."""
xcrun = repository_ctx.which("xcrun")
if not xcrun:
fail("`xcrun` not found: is Xcode installed?")
output = _run(repository_ctx, [xcrun, "--show-sdk-path"]).stdout
return output.splitlines()[0]
def _compute_bsd_sysroot(repository_ctx):
"""Look around for sysroot. Return root (/) if nothing found."""
# Try it-just-works for CMake users.
default = "/"
sysroot = repository_ctx.os.environ.get("CMAKE_SYSROOT", default)
sysroot_path = repository_ctx.path(sysroot)
if sysroot_path.exists:
return sysroot_path.realpath
return default
# File content used when computing search paths. This additionally verifies that
# libc++ is installed.
_CLANG_INCLUDE_FILE_CONTENT = """
#if __has_include(<version>)
#include <version>
#endif
#ifndef _LIBCPP_STD_VER
#error "No libc++ install found!"
#endif
"""
def _compute_clang_cpp_include_search_paths(repository_ctx, clang, sysroot):
"""Runs the `clang` binary and extracts the include search paths.
Returns the resulting paths as a list of strings.
"""
# Create a file for Clang to use as input.
repository_ctx.file("_temp", _CLANG_INCLUDE_FILE_CONTENT)
input_file = repository_ctx.path("_temp")
# The only way to get this out of Clang currently is to parse the verbose
# output of the compiler when it is compiling C++ code.
cmd = [
clang,
# Avoid canonicalizing away symlinks.
"-no-canonical-prefixes",
# Extract verbose output.
"-v",
# Just parse the input, don't generate outputs.
"-fsyntax-only",
# Force the language to be C++.
"-x",
"c++",
# Use the input file.
input_file,
# Always use libc++.
"-stdlib=libc++",
]
# We need to use a sysroot to correctly represent a run on macOS.
if repository_ctx.os.name.lower().startswith("mac os"):
if not sysroot:
fail("Must provide a sysroot on macOS!")
cmd.append("--sysroot=" + sysroot)
# Note that verbose output is on stderr, not stdout!
output = _run(repository_ctx, cmd).stderr.splitlines()
# Return the list of directories printed for system headers. These are the
# only ones that Bazel needs us to manually provide. We find these by
# searching for a begin and end marker. We also have to strip off a leading
# space from each path.
include_begin = output.index("#include <...> search starts here:") + 1
include_end = output.index("End of search list.", include_begin)
# Suffix present on framework paths.
framework_suffix = " (framework directory)"
return [
repository_ctx.path(s.lstrip(" ").removesuffix(framework_suffix))
for s in output[include_begin:include_end]
]
def _configure_clang_toolchain_impl(repository_ctx):
# First just symlink in the untemplated parts of the toolchain repo.
repository_ctx.symlink(repository_ctx.attr._clang_toolchain_build, "BUILD")
repository_ctx.symlink(
repository_ctx.attr._clang_cc_toolchain_config,
"cc_toolchain_config.bzl",
)
for file_label in repository_ctx.attr._clang_toolchain_files:
repository_ctx.symlink(file_label, file_label.name)
# Find a Clang C++ compiler, and where it lives. We need to walk symlinks
# here as the other LLVM tools may not be symlinked into the PATH even if
# `clang` is. We also insist on finding the basename of `clang++` as that is
# important for C vs. C++ compiles.
(clang, clang_version, clang_version_for_cache) = _detect_system_clang(
repository_ctx,
)
if clang_version and clang_version < 19:
fail("Found clang {0}. ".format(clang_version) +
"Carbon requires clang >=19. See " +
"https://github.com/carbon-language/carbon-lang/blob/trunk/docs/project/contribution_tools.md#old-llvm-versions")
clang_cpp = clang.dirname.get_child("clang++")
# Compute the various directories used by Clang.
resource_dir = _compute_clang_resource_dir(repository_ctx, clang_cpp)
sysroot_dir = None
if repository_ctx.os.name.lower().startswith("mac os"):
sysroot_dir = _compute_mac_os_sysroot(repository_ctx)
if repository_ctx.os.name == "freebsd":
sysroot_dir = _compute_bsd_sysroot(repository_ctx)
include_dirs = _compute_clang_cpp_include_search_paths(
repository_ctx,
clang_cpp,
sysroot_dir,
)
# We expect that the LLVM binutils live adjacent to llvm-ar.
# First look for llvm-ar adjacent to clang, so that if found,
# it is most likely to match the same version as clang.
# Otherwise, try PATH.
ar_path = clang.dirname.get_child("llvm-ar")
if not ar_path.exists:
ar_path = repository_ctx.which("llvm-ar")
if not ar_path:
fail("`llvm-ar` not found in PATH or adjacent to clang")
# By default Windows uses '\' in its paths. These will be
# interpreted as escape characters and fail the build, thus
# we must manually replace the backslashes with '/'
if repository_ctx.os.name.lower().startswith("windows"):
resource_dir = resource_dir.replace("\\", "/")
include_dirs = [str(s).replace("\\", "/") for s in include_dirs]
repository_ctx.template(
"clang_detected_variables.bzl",
repository_ctx.attr._clang_detected_variables_template,
substitutions = {
"{CLANG_BINDIR}": str(clang.dirname),
"{CLANG_INCLUDE_DIRS}": str(
[str(path) for path in include_dirs],
),
"{CLANG_RESOURCE_DIR}": resource_dir,
"{CLANG_VERSION_FOR_CACHE}": clang_version_for_cache.replace('"', "_").replace("\\", "_"),
"{CLANG_VERSION}": str(clang_version),
"{LLVM_BINDIR}": str(ar_path.dirname),
"{LLVM_SYMBOLIZER}": str(ar_path.dirname.get_child("llvm-symbolizer")),
"{SYSROOT}": str(sysroot_dir),
},
executable = False,
)
configure_clang_toolchain = repository_rule(
implementation = _configure_clang_toolchain_impl,
configure = True,
local = True,
attrs = {
"_clang_cc_toolchain_config": attr.label(
default = Label(
"//bazel/cc_toolchains:clang_cc_toolchain_config.bzl",
),
allow_single_file = True,
),
"_clang_detected_variables_template": attr.label(
default = Label(
"//bazel/cc_toolchains:clang_detected_variables.tpl.bzl",
),
allow_single_file = True,
),
"_clang_toolchain_build": attr.label(
default = Label("//bazel/cc_toolchains:clang_toolchain.BUILD"),
allow_single_file = True,
),
"_clang_toolchain_files": attr.label_list(
default = [
Label("//bazel/cc_toolchains:cc_toolchain_actions.bzl"),
Label("//bazel/cc_toolchains:cc_toolchain_base_features.bzl"),
Label("//bazel/cc_toolchains:cc_toolchain_carbon_project_features.bzl"),
Label("//bazel/cc_toolchains:cc_toolchain_config_features.bzl"),
Label("//bazel/cc_toolchains:cc_toolchain_cpp_features.bzl"),
Label("//bazel/cc_toolchains:cc_toolchain_debugging.bzl"),
Label("//bazel/cc_toolchains:cc_toolchain_features.bzl"),
Label("//bazel/cc_toolchains:cc_toolchain_linking.bzl"),
Label("//bazel/cc_toolchains:cc_toolchain_modules.bzl"),
Label("//bazel/cc_toolchains:cc_toolchain_optimization.bzl"),
Label("//bazel/cc_toolchains:cc_toolchain_sanitizer_features.bzl"),
Label("//bazel/cc_toolchains:cc_toolchain_tools.bzl"),
],
),
},
environ = ["CC"],
)
clang_toolchain_extension = module_extension(
implementation = lambda ctx: configure_clang_toolchain(name = "bazel_cc_toolchain"),
)