## Summary
This PR adds cross-compilation support for three new target platforms,
upgrades LLVM to 21.1.8, and overhauls the CI pipelines around
cross-builds and testing.
## Cross-compilation
New target triples accepted via `-DCLICE_TARGET_TRIPLE=...`:
| Target triple | Host | Output |
|---|---|---|
| `x86_64-apple-darwin` | macos-15 (arm64) | macOS x64 |
| `aarch64-linux-gnu` | ubuntu-24.04 (x64) | Linux arm64 |
| `aarch64-pc-windows-msvc` | windows-2025 (x64) | Windows arm64 |
- `cmake/toolchain.cmake` — maps `CLICE_TARGET_TRIPLE` to
`CMAKE_SYSTEM_NAME`/`CMAKE_SYSTEM_PROCESSOR`/compiler `--target`; picks
up conda aarch64 sysroot when cross-compiling Linux.
- `cmake/llvm.cmake` — forwards target platform/arch to `setup-llvm.py`
so the right prebuilt LLVM is downloaded for the target.
- `CMakeLists.txt` — uses a host-side `flatc` from `PATH` under
`CMAKE_CROSSCOMPILING` instead of the in-tree target build.
- `pixi.toml`:
- Adds `osx-64`, `linux-aarch64`, `win-arm64` platforms.
- New environments: `cross-macos-x64`, `cross-linux-aarch64` (adds
`gcc_linux-aarch64` + `sysroot_linux-aarch64`), `cross-windows-arm64`.
- New lightweight `test-run` env used on native ARM/x64 runners to
execute cross-built artifacts (pulls in upstream clang+lld on macOS so
tests don't fall back to Apple clang).
- `scripts/activate_cross_linux.sh` — exports `CONDA_PREFIX`-relative
paths for the aarch64 toolchain.
- `scripts/build-llvm.py` — `--target-triple` support and a
`build_native_tools()` helper that produces host `llvm-tblgen` /
`clang-tblgen` needed when cross-compiling LLVM itself.
## LLVM upgrade 21.1.4 → 21.1.8
- `cmake/package.cmake` bumps `setup_llvm("21.1.8")`.
- `config/llvm-manifest.json` regenerated with 6 new cross-compiled
entries and a new `arch` field on every entry so lookup is `(version,
platform, arch, lto, build_type)`.
- `scripts/setup-llvm.py` — honours the new `arch` field when resolving
artifacts.
- `scripts/update-llvm-version.py` (new) — single-call version bump
across `package.cmake` + manifest.
- `scripts/validate-llvm-components.py` (new) — scans the LLVM source
tree for library targets and diffs them against
`scripts/llvm-components.json` to catch stale/misspelled component names
before a build.
- `scripts/llvm-components.json` (new) — explicit allow-list of required
LLVM/Clang library targets used by `build-llvm.py`.
## CI changes
- `.github/workflows/build-llvm.yml`:
- Adds `workflow_dispatch` with `llvm_version`, `skip_upload`, `skip_pr`
inputs.
- Matrix extended with the 6 cross-compile entries (2 per new platform:
RelWithDebInfo ± LTO).
- `build clice` / test / prune steps gated on `!matrix.target_triple`
for cross-builds; cross-built LTO entries apply the native prune
manifest (arch-independent).
- Cross-compiled binary architecture is verified with `file(1)`.
- New `upload` job triggered by `workflow_dispatch` pushes artifacts to
`clice-io/clice-llvm` and hands the manifest off to the next job.
- `.github/workflows/test-cmake.yml`:
- Build matrix gains three `build_only: true` cross entries that upload
`bin/` + `lib/` artifacts.
- New `test-cross` job runs on native `macos-15-intel`,
`ubuntu-24.04-arm`, `windows-11-arm` runners, downloads the cross-built
artifacts, and runs unit / integration / smoke tests under the
`test-run` pixi env.
- Cache keys now include `target_triple` so native and cross builds
don't collide.
- `.github/workflows/publish-clice.yml`:
- Three additional release artifacts for the new targets
(`clice-x86_64-macos-darwin`, `clice-aarch64-linux-gnu`,
`clice-aarch64-windows-msvc`), each with a matching `-symbol` archive.
## Compatibility
- All existing native builds and tests are preserved; cross entries are
additive.
- `Debug` + ASAN remains disabled on Windows (`llvm_mode == Debug && os
== windows-*` no longer appends `-asan`).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
353 lines
12 KiB
Python
353 lines
12 KiB
Python
#!/usr/bin/env python3
|
|
import sys
|
|
import subprocess
|
|
import shutil
|
|
import argparse
|
|
import os
|
|
import json
|
|
from pathlib import Path
|
|
|
|
|
|
def normalize_mode(value: str) -> str:
|
|
mapping = {
|
|
"debug": "Debug",
|
|
"release": "Release",
|
|
"relwithdebinfo": "RelWithDebInfo",
|
|
"releasedbg": "RelWithDebInfo",
|
|
}
|
|
key = value.strip().lower()
|
|
if key in mapping:
|
|
return mapping[key]
|
|
raise argparse.ArgumentTypeError(
|
|
f"Invalid mode '{value}'. Choose from Debug, Release, RelWithDebInfo."
|
|
)
|
|
|
|
|
|
def build_native_tools(project_root: Path, build_dir: Path) -> Path:
|
|
"""Build native host tablegen tools for cross-compilation.
|
|
|
|
When cross-compiling LLVM, build tools like llvm-tblgen must run on the
|
|
host but would otherwise be compiled for the target architecture. This
|
|
function performs a minimal native build and returns the bin directory
|
|
containing host-runnable executables.
|
|
"""
|
|
native_dir = build_dir.parent / f"{build_dir.name}-native-tools"
|
|
native_dir.mkdir(exist_ok=True)
|
|
source_dir = project_root / "llvm"
|
|
|
|
cmake_args = [
|
|
"-G",
|
|
"Ninja",
|
|
"-DCMAKE_BUILD_TYPE=Release",
|
|
"-DLLVM_ENABLE_PROJECTS=clang;clang-tools-extra",
|
|
"-DLLVM_TARGETS_TO_BUILD=Native",
|
|
"-DLLVM_DISABLE_ASSEMBLY_FILES=ON",
|
|
"-DCMAKE_C_FLAGS=-w",
|
|
"-DCMAKE_CXX_FLAGS=-w",
|
|
]
|
|
|
|
if sys.platform == "win32":
|
|
cmake_args += [
|
|
"-DCMAKE_C_COMPILER=clang-cl",
|
|
"-DCMAKE_CXX_COMPILER=clang-cl",
|
|
]
|
|
else:
|
|
cmake_args += [
|
|
"-DCMAKE_C_COMPILER=clang",
|
|
"-DCMAKE_CXX_COMPILER=clang++",
|
|
]
|
|
|
|
print(f"\nConfiguring native host tools in {native_dir}...")
|
|
subprocess.check_call(
|
|
["cmake", "-S", str(source_dir), "-B", str(native_dir)] + cmake_args
|
|
)
|
|
|
|
required_tools = ["llvm-tblgen", "llvm-min-tblgen", "clang-tblgen"]
|
|
optional_tools = ["clang-tidy-confusable-chars-gen"]
|
|
|
|
for tool in required_tools:
|
|
print(f"Building native {tool}...")
|
|
subprocess.check_call(["cmake", "--build", str(native_dir), "--target", tool])
|
|
|
|
for tool in optional_tools:
|
|
try:
|
|
print(f"Building native {tool} (optional)...")
|
|
subprocess.check_call(
|
|
["cmake", "--build", str(native_dir), "--target", tool]
|
|
)
|
|
except subprocess.CalledProcessError:
|
|
print(f" {tool} not available, skipping.")
|
|
|
|
bin_dir = native_dir / "bin"
|
|
print(f"Native host tools ready in {bin_dir}")
|
|
return bin_dir
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(
|
|
description="Build LLVM with specific configurations."
|
|
)
|
|
parser.add_argument(
|
|
"--llvm-src",
|
|
help="Path to llvm-project source root (defaults to current working directory)",
|
|
)
|
|
parser.add_argument(
|
|
"--mode",
|
|
default="Release",
|
|
type=normalize_mode,
|
|
choices=["Debug", "Release", "RelWithDebInfo"],
|
|
help="Build mode (default: Release)",
|
|
)
|
|
parser.add_argument(
|
|
"--lto",
|
|
default="OFF",
|
|
type=lambda s: s.upper(),
|
|
choices=["ON", "OFF"],
|
|
help="Enable LTO (default: OFF)",
|
|
)
|
|
parser.add_argument(
|
|
"--build-dir",
|
|
help="Custom build directory (relative to project root or absolute)",
|
|
)
|
|
parser.add_argument(
|
|
"--target-triple",
|
|
help="Cross-compilation target triple (e.g. x86_64-apple-darwin, aarch64-linux-gnu, aarch64-pc-windows-msvc)",
|
|
)
|
|
|
|
args = parser.parse_args()
|
|
|
|
repo_root = Path(__file__).resolve().parent.parent
|
|
toolchain_file = repo_root / "cmake" / "toolchain.cmake"
|
|
if not toolchain_file.exists():
|
|
print(f"Error: toolchain file not found at {toolchain_file}")
|
|
sys.exit(1)
|
|
|
|
if args.llvm_src:
|
|
project_root = Path(args.llvm_src).expanduser().resolve()
|
|
else:
|
|
project_root = Path.cwd()
|
|
os.chdir(project_root)
|
|
|
|
if not (project_root / "llvm" / "CMakeLists.txt").exists():
|
|
print(f"Error: Could not find 'llvm/CMakeLists.txt' in {project_root}")
|
|
print("Please run this script from the root of the llvm-project repository.")
|
|
sys.exit(1)
|
|
|
|
lto_enabled = args.lto == "ON"
|
|
mode_for_dir = args.mode.lower()
|
|
|
|
if args.build_dir:
|
|
build_dir = Path(args.build_dir)
|
|
if not build_dir.is_absolute():
|
|
build_dir = project_root / build_dir
|
|
else:
|
|
build_dir = f"build-{mode_for_dir}"
|
|
if lto_enabled:
|
|
build_dir += "-lto"
|
|
build_dir = project_root / build_dir
|
|
install_prefix = build_dir.parent / f"{build_dir.name}-install"
|
|
|
|
print("--- Configuration ---")
|
|
print(f"Mode: {args.mode}")
|
|
print(f"LTO: {args.lto}")
|
|
print(f"Target Triple: {args.target_triple or '(native)'}")
|
|
print(f"Root: {project_root}")
|
|
print(f"Build Dir: {build_dir}")
|
|
print(f"Install Prefix: {install_prefix}")
|
|
print(f"Toolchain: {toolchain_file}")
|
|
print("---------------------")
|
|
|
|
components_path = Path(__file__).resolve().parent / "llvm-components.json"
|
|
with components_path.open() as f:
|
|
llvm_distribution_components = json.load(f)["components"]
|
|
|
|
components_joined = ";".join(llvm_distribution_components)
|
|
cmake_args = [
|
|
"-G",
|
|
"Ninja",
|
|
f"-DCMAKE_INSTALL_PREFIX={install_prefix}",
|
|
]
|
|
|
|
if sys.platform == "win32":
|
|
# Use clang-cl (MSVC driver) on Windows so that LLVM's CMake
|
|
# generates correct MSVC-style linker flags for LTO, etc.
|
|
c_flags = "-w"
|
|
if args.target_triple:
|
|
c_flags += f" --target={args.target_triple}"
|
|
cmake_args += [
|
|
"-DCMAKE_C_COMPILER=clang-cl",
|
|
"-DCMAKE_CXX_COMPILER=clang-cl",
|
|
f"-DCMAKE_C_FLAGS={c_flags}",
|
|
f"-DCMAKE_CXX_FLAGS={c_flags}",
|
|
"-DLLVM_USE_LINKER=lld-link",
|
|
]
|
|
else:
|
|
cmake_args += [
|
|
f"-DCMAKE_TOOLCHAIN_FILE={toolchain_file.as_posix()}",
|
|
"-DCMAKE_C_FLAGS=-w",
|
|
"-DCMAKE_CXX_FLAGS=-w",
|
|
"-DLLVM_USE_LINKER=lld",
|
|
]
|
|
|
|
cmake_args += [
|
|
"-DLLVM_ENABLE_ZLIB=OFF",
|
|
"-DLLVM_ENABLE_ZSTD=OFF",
|
|
"-DLLVM_ENABLE_LIBXML2=OFF",
|
|
"-DLLVM_ENABLE_BINDINGS=OFF",
|
|
"-DLLVM_ENABLE_IDE=OFF",
|
|
"-DLLVM_ENABLE_Z3_SOLVER=OFF",
|
|
"-DLLVM_ENABLE_LIBEDIT=OFF",
|
|
"-DLLVM_ENABLE_LIBPFM=OFF",
|
|
"-DLLVM_ENABLE_OCAMLDOC=OFF",
|
|
"-DLLVM_ENABLE_PLUGINS=OFF",
|
|
"-DLLVM_INCLUDE_UTILS=OFF",
|
|
"-DLLVM_INCLUDE_TESTS=OFF",
|
|
"-DLLVM_INCLUDE_EXAMPLES=OFF",
|
|
"-DLLVM_INCLUDE_BENCHMARKS=OFF",
|
|
"-DLLVM_INCLUDE_DOCS=OFF",
|
|
"-DLLVM_BUILD_UTILS=OFF",
|
|
"-DLLVM_BUILD_TOOLS=OFF",
|
|
"-DCLANG_BUILD_TOOLS=OFF",
|
|
"-DCLANG_INCLUDE_DOCS=OFF",
|
|
"-DCLANG_INCLUDE_TESTS=OFF",
|
|
"-DCLANG_TOOL_CLANG_IMPORT_TEST_BUILD=OFF",
|
|
"-DCLANG_TOOL_CLANG_LINKER_WRAPPER_BUILD=OFF",
|
|
"-DCLANG_TOOL_C_INDEX_TEST_BUILD=OFF",
|
|
"-DCLANG_TOOL_LIBCLANG_BUILD=OFF",
|
|
"-DCLANG_ENABLE_CLANGD=OFF",
|
|
"-DLLVM_BUILD_LLVM_C_DYLIB=OFF",
|
|
"-DLLVM_LINK_LLVM_DYLIB=OFF",
|
|
"-DLLVM_ENABLE_RTTI=OFF",
|
|
# Enable features
|
|
"-DLLVM_INCLUDE_TOOLS=ON",
|
|
"-DLLVM_PARALLEL_LINK_JOBS=1",
|
|
"-DCMAKE_JOB_POOL_LINK=console",
|
|
"-DLLVM_ENABLE_PROJECTS=clang;clang-tools-extra",
|
|
"-DLLVM_TARGETS_TO_BUILD=all",
|
|
"-DLLVM_DISABLE_ASSEMBLY_FILES=ON",
|
|
# Distribution
|
|
f"-DLLVM_DISTRIBUTION_COMPONENTS={components_joined}",
|
|
]
|
|
|
|
ccache_env = os.environ.get("CCACHE_PROGRAM") or os.environ.get("CCACHE")
|
|
ccache_program = shutil.which(ccache_env) if ccache_env else shutil.which("ccache")
|
|
if not ccache_program and ccache_env:
|
|
# Fall back to the env value as-is if it points to a real path.
|
|
candidate = Path(ccache_env)
|
|
if candidate.exists():
|
|
ccache_program = candidate.as_posix()
|
|
|
|
if ccache_program:
|
|
ccache_path = Path(ccache_program).as_posix()
|
|
print(f"Using ccache: {ccache_path}")
|
|
cmake_args.append("-DLLVM_CCACHE_BUILD=ON")
|
|
cmake_args.append(f"-DCCACHE_PROGRAM={ccache_path}")
|
|
else:
|
|
print("ccache not found; proceeding without it.")
|
|
|
|
is_shared = "OFF"
|
|
if args.mode == "Debug":
|
|
cmake_args.append("-DCMAKE_BUILD_TYPE=Debug")
|
|
# ASAN is incompatible with -MDd on Windows (clang-cl), skip it there.
|
|
if sys.platform != "win32":
|
|
cmake_args.append("-DLLVM_USE_SANITIZER=Address")
|
|
is_shared = "ON"
|
|
elif args.mode == "Release":
|
|
cmake_args.append("-DCMAKE_BUILD_TYPE=Release")
|
|
elif args.mode == "RelWithDebInfo":
|
|
cmake_args.append("-DCMAKE_BUILD_TYPE=RelWithDebInfo")
|
|
|
|
if sys.platform == "win32":
|
|
is_shared = "OFF"
|
|
cmake_args.append(f"-DBUILD_SHARED_LIBS={is_shared}")
|
|
|
|
if lto_enabled:
|
|
cmake_args.append("-DLLVM_ENABLE_LTO=Thin")
|
|
else:
|
|
cmake_args.append("-DLLVM_ENABLE_LTO=OFF")
|
|
|
|
if args.target_triple:
|
|
cmake_args.append(f"-DCLICE_TARGET_TRIPLE={args.target_triple}")
|
|
cmake_args.append(f"-DLLVM_HOST_TRIPLE={args.target_triple}")
|
|
|
|
# When cross-compiling, clear conda's host-platform flags so they
|
|
# don't leak into the target build (e.g. -L pointing to x86_64 libs).
|
|
# This must happen before the native-tools build too so we don't
|
|
# contaminate the native configure with target-arch link flags.
|
|
for var in ["LIBRARY_PATH", "LDFLAGS", "CFLAGS", "CXXFLAGS", "CPPFLAGS"]:
|
|
os.environ.pop(var, None)
|
|
|
|
# Cross-compilation needs native host tools (tablegen, etc.) that can
|
|
# run on the build machine. macOS handles this transparently via
|
|
# Rosetta 2, but Linux and Windows require a separate native build.
|
|
if sys.platform != "darwin":
|
|
native_bin_dir = build_native_tools(project_root, build_dir)
|
|
cmake_args.append(f"-DLLVM_NATIVE_TOOL_DIR={native_bin_dir}")
|
|
|
|
build_dir.mkdir(exist_ok=True)
|
|
|
|
print(f"\nConfiguring in {build_dir}...")
|
|
try:
|
|
source_dir = project_root / "llvm"
|
|
subprocess.check_call(
|
|
["cmake", "-S", str(source_dir), "-B", str(build_dir)] + cmake_args
|
|
)
|
|
except subprocess.CalledProcessError:
|
|
print("CMake configuration failed!")
|
|
sys.exit(1)
|
|
|
|
print("\nBuilding 'install-distribution' target...")
|
|
try:
|
|
subprocess.check_call(
|
|
["cmake", "--build", str(build_dir), "--target", "install-distribution"]
|
|
)
|
|
except subprocess.CalledProcessError:
|
|
print("Build failed!")
|
|
sys.exit(1)
|
|
|
|
print("\nCopying internal Sema headers...")
|
|
clang_sema_dir = project_root / "clang/lib/Sema"
|
|
install_sema_dir = install_prefix / "include/clang/Sema"
|
|
install_sema_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
headers_to_copy = ["CoroutineStmtBuilder.h", "TypeLocBuilder.h", "TreeTransform.h"]
|
|
|
|
for header in headers_to_copy:
|
|
src = clang_sema_dir / header
|
|
dst = install_sema_dir / header
|
|
if src.exists():
|
|
shutil.copy(src, dst)
|
|
print(f" Copied {header}")
|
|
else:
|
|
print(f" Warning: {header} not found in source.")
|
|
|
|
def human_readable(num: int) -> str:
|
|
for unit in ["B", "KB", "MB", "GB"]:
|
|
if num < 1024.0:
|
|
return f"{num:,.1f}{unit}"
|
|
num /= 1024.0
|
|
return f"{num:.1f}TB"
|
|
|
|
lib_dir = install_prefix / "lib"
|
|
sizes = []
|
|
if lib_dir.exists():
|
|
for p in lib_dir.rglob("*"):
|
|
if p.is_file():
|
|
sizes.append((p, p.stat().st_size))
|
|
sizes.sort(key=lambda x: x[1], reverse=True)
|
|
|
|
total_size = sum(sz for _, sz in sizes)
|
|
print(f"\nLibrary size summary under {lib_dir}:")
|
|
print(f" Total: {human_readable(total_size)} across {len(sizes)} files")
|
|
for path, sz in sizes:
|
|
rel = path.relative_to(install_prefix)
|
|
print(f" {human_readable(sz):>8} {rel}")
|
|
if not sizes:
|
|
print(" (no files found)")
|
|
|
|
print(f"\nSuccess! Artifacts installed to: {install_prefix}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|