Files
datastore/ci/jobs/build_toolchain.py
zeekayandHanzo Dev ee284e8075 finish the rebrand where a person actually reads it
The fork already renamed its internals - the built binary is datastore, the
packages are datastore-server and friends, and the shipped config.xml already
says Datastore - but the surfaces a human reads still said ClickHouse. Issue
templates asked which ClickHouse version was affected, the PR template told
contributors to close upstream's issues, and CI job names, log lines, error
messages and argparse descriptions named a product this repo does not ship.

Only prose moved: display names, log and error messages, comments, docstrings
and form labels. Everything a machine resolves stayed exactly as it was,
because renaming those breaks a build rather than a sentence - binary and path
tokens such as clickhouse-client and /etc/clickhouse-server, python and shell
identifiers, CLICKHOUSE_* environment variables, the /ClickHouse container
mount, docker images in a namespace we do not own, S3 buckets, maven and pypi
coordinates, and every URL pointing at upstream. The attribution in NOTICE and
README is untouched.

Two links were wrong rather than merely stale, so they are corrected here. The
PR template's Closes and Related placeholders sent contributors to upstream's
tracker instead of this one, and an earlier pass had rewritten a source
reference to github.com/Datastore/Datastore, which does not resolve.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-14 14:07:52 -07:00

699 lines
27 KiB
Python

import argparse
import glob
import os
import platform
import shutil
from ci.praktika.result import Result
from ci.praktika.utils import MetaClasses, Shell, Utils
TEMP = "/tmp"
LLVM_SOURCE_DIR = f"{TEMP}/llvm-project"
NINJA_SOURCE_DIR = f"{TEMP}/ninja-src"
NINJA_BUILD_DIR = f"{TEMP}/ninja-build"
NINJA_LOG_SHARE_DIR = "share/clickhouse-build"
STAGE1_BUILD_DIR = f"{TEMP}/toolchain-stage1"
STAGE1_INSTALL_DIR = f"{TEMP}/toolchain-stage1-install"
STAGE2_BUILD_DIR = f"{TEMP}/toolchain-stage2"
STAGE2_INSTALL_DIR = f"{TEMP}/toolchain-stage2-install"
CH_PROFILE_BUILD_DIR = f"{TEMP}/toolchain-ch-profile"
CH_BOLT_BUILD_DIR = f"{TEMP}/toolchain-ch-bolt"
PROFDATA_PATH = f"{TEMP}/clang.profdata"
BOLT_PROFILES_DIR = f"{TEMP}/bolt-profiles"
BOLT_FDATA_PATH = f"{TEMP}/bolt.fdata"
REPO_PATH = "/ClickHouse"
OUTPUT_DIR = f"{Utils.cwd()}/ci/tmp"
# BOLT profile collection settings: time-limited to avoid filling disk
# with fdata files (~15 MB per compilation unit)
BOLT_PROFILE_TIMEOUT = 1200 # 20 minutes
BOLT_PROFILE_PARALLELISM = 4
# All LLVM projects for the final toolchain (note: "all" doesn't work with cmake)
STAGE2_LLVM_PROJECTS = "clang;clang-tools-extra;lld;bolt;polly"
# Cross-target triples for compiler-rt builtins. Builtins are freestanding C code
# (no sysroot needed), built via LLVM_BUILTIN_TARGETS so the toolchain is
# self-contained for all Datastore cross-compilation targets.
# Must match the architectures supported in contrib/compiler-rt-cmake/CMakeLists.txt.
CROSS_BUILTIN_TARGETS = [
("x86_64-unknown-linux-gnu", "Linux"),
("aarch64-unknown-linux-gnu", "Linux"),
("s390x-unknown-linux-gnu", "Linux"),
("powerpc64le-unknown-linux-gnu", "Linux"),
("riscv64-unknown-linux-gnu", "Linux"),
("loongarch64-unknown-linux-gnu", "Linux"),
("x86_64-pc-freebsd13", "FreeBSD"),
("aarch64-unknown-freebsd13", "FreeBSD"),
("powerpc64le-unknown-freebsd13", "FreeBSD"),
]
class JobStages(metaclass=MetaClasses.WithIter):
CLONE_LLVM = "clone_llvm"
BUILD_NINJA = "build_ninja"
STAGE1_BUILD = "stage1_build"
PROFILE_COLLECTION = "profile_collection"
STAGE2_BUILD = "stage2_build"
BOLT_OPTIMIZATION = "bolt_optimization"
PACKAGE = "package"
def get_arch():
machine = platform.machine()
if machine == "x86_64":
return "x86_64"
elif machine == "aarch64":
return "aarch64"
else:
raise RuntimeError(f"Unsupported architecture: {machine}")
def get_toolchain_file():
arch = get_arch()
return f"{REPO_PATH}/cmake/linux/toolchain-{arch}.cmake"
def clean_dirs(*dirs):
for d in dirs:
if os.path.exists(d):
print(f"Cleaning {d}")
shutil.rmtree(d, ignore_errors=True)
def parse_args():
parser = argparse.ArgumentParser(description="Build PGO+BOLT optimized clang")
parser.add_argument(
"--param",
help="Optional user-defined job start stage (for local run)",
default=None,
)
return parser.parse_args()
def main():
args = parse_args()
stages = list(JobStages)
stage = args.param or JobStages.CLONE_LLVM
if stage:
assert stage in JobStages, f"--param must be one of [{list(JobStages)}]"
print(f"Job will start from stage [{stage}]")
while stage in stages:
stages.pop(0)
stages.insert(0, stage)
arch = get_arch()
toolchain_file = get_toolchain_file()
print(f"Building toolchain for {arch}")
print(f"Using Datastore toolchain file: {toolchain_file}")
os.makedirs(OUTPUT_DIR, exist_ok=True)
res = True
results = []
if os.getuid() == 0:
Shell.check(f"git config --global --add safe.directory {Utils.cwd()}")
# Stage 0: Clone LLVM
if res and JobStages.CLONE_LLVM in stages:
clean_dirs(LLVM_SOURCE_DIR)
results.append(
Result.from_commands_run(
name="Clone LLVM",
command=(
f"git clone --depth 1 --branch release/21.x"
f" https://github.com/llvm/llvm-project.git {LLVM_SOURCE_DIR}"
),
retries=3,
)
)
res = results[-1].is_ok()
# Stage 0.5: Build custom Ninja with timing-based scheduling
# Ninja 1.12.1 has prev_elapsed_time_millis on Edge (populated from .ninja_log).
# We patch EdgeWeightHeuristic to use historical build times as edge weights,
# giving 11-21% build time reduction via critical path scheduling.
CUSTOM_NINJA = f"{NINJA_BUILD_DIR}/ninja"
if res and JobStages.BUILD_NINJA in stages:
clean_dirs(NINJA_SOURCE_DIR, NINJA_BUILD_DIR)
results.append(
Result.from_commands_run(
name="Clone Ninja v1.12.1",
command=(
f"git clone --depth 1 --branch v1.12.1"
f" https://github.com/ninja-build/ninja.git {NINJA_SOURCE_DIR}"
),
retries=3,
)
)
res = results[-1].is_ok()
if res:
# Patch EdgeWeightHeuristic to use .ninja_log timing data
results.append(
Result.from_commands_run(
name="Patch Ninja EdgeWeightHeuristic",
command=(
f"sed -i 's/return edge->is_phony() ? 0 : 1;/"
f"int64_t w = edge->prev_elapsed_time_millis < 0 ? 1 : edge->prev_elapsed_time_millis;\\n"
f" return edge->is_phony() ? 0 : w;/' {NINJA_SOURCE_DIR}/src/build.cc"
),
)
)
res = results[-1].is_ok()
if res:
results.append(
Result.from_commands_run(
name="Build Ninja",
command=[
f"cmake -B {NINJA_BUILD_DIR} -S {NINJA_SOURCE_DIR} -DCMAKE_BUILD_TYPE=Release -DBUILD_TESTING=OFF",
f"cmake --build {NINJA_BUILD_DIR}",
],
)
)
res = results[-1].is_ok()
if res:
print(f"Custom Ninja built at {CUSTOM_NINJA}")
# Clean up source dir to save space
clean_dirs(NINJA_SOURCE_DIR)
# Stage 1: Build instrumented clang for PGO profile collection
# Only needs clang + lld (for compilation/linking) and compiler-rt (profiling runtime)
if res and JobStages.STAGE1_BUILD in stages:
clean_dirs(STAGE1_BUILD_DIR, STAGE1_INSTALL_DIR)
os.makedirs(STAGE1_BUILD_DIR, exist_ok=True)
cmake_cmd = (
f"cmake -G Ninja"
f' -DLLVM_ENABLE_PROJECTS="clang;lld"'
f' -DLLVM_ENABLE_RUNTIMES="compiler-rt"'
f" -DLLVM_TARGETS_TO_BUILD=Native"
f" -DCMAKE_BUILD_TYPE=Release"
f" -DLLVM_BUILD_INSTRUMENTED=IR"
f" -DCMAKE_C_COMPILER=clang-21"
f" -DCMAKE_CXX_COMPILER=clang++-21"
f" -DLLVM_ENABLE_LLD=ON"
f" -DLLVM_ENABLE_TERMINFO=OFF"
f" -DLLVM_ENABLE_ZLIB=FORCE_ON"
f" -DLLVM_ENABLE_ZSTD=OFF"
f" -DCMAKE_INSTALL_PREFIX={STAGE1_INSTALL_DIR}"
f" -S {LLVM_SOURCE_DIR}/llvm"
f" -B {STAGE1_BUILD_DIR}"
)
results.append(
Result.from_commands_run(
name="Stage 1 CMake (instrumented clang)",
command=cmake_cmd,
)
)
res = results[-1].is_ok()
if res:
results.append(
Result.from_commands_run(
name="Stage 1 Build (instrumented clang)",
command=f"{CUSTOM_NINJA} -C {STAGE1_BUILD_DIR} clang lld",
)
)
res = results[-1].is_ok()
if res:
results.append(
Result.from_commands_run(
name="Stage 1 Install",
command=(
f"{CUSTOM_NINJA} -C {STAGE1_BUILD_DIR}"
f" install-clang install-clang-resource-headers install-lld"
),
)
)
res = results[-1].is_ok()
# Install compiler-rt headers (xray, sanitizer, etc.) into the clang resource
# directory so that Datastore can find <xray/xray_interface.h> when compiled
# with this toolchain.
if res:
resource_dirs = glob.glob(
f"{STAGE1_INSTALL_DIR}/lib/clang/*/include"
)
if resource_dirs:
resource_include = resource_dirs[0]
results.append(
Result.from_commands_run(
name="Install compiler-rt headers",
command=(
f"cp -r {LLVM_SOURCE_DIR}/compiler-rt/include/xray"
f" {resource_include}/xray"
),
)
)
res = results[-1].is_ok()
else:
print(
f"WARNING: No clang resource directory found in"
f" {STAGE1_INSTALL_DIR}/lib/clang/*/include"
)
# Stage 2: Profile collection - build Datastore with instrumented clang
if res and JobStages.PROFILE_COLLECTION in stages:
clean_dirs(CH_PROFILE_BUILD_DIR)
# Checkout submodules first
results.append(
Result.from_commands_run(
name="Checkout submodules for profile collection",
command=[
f"git -C {REPO_PATH} submodule sync",
f"git -C {REPO_PATH} submodule init",
f"{REPO_PATH}/contrib/update-submodules.sh --max-procs 10",
],
retries=3,
)
)
res = results[-1].is_ok()
if res:
cmake_cmd = (
f"cmake"
f" -DCMAKE_BUILD_TYPE=None"
f" -DENABLE_THINLTO=0"
f" -DCMAKE_C_COMPILER={STAGE1_INSTALL_DIR}/bin/clang"
f" -DCMAKE_CXX_COMPILER={STAGE1_INSTALL_DIR}/bin/clang++"
f" -DCOMPILER_CACHE=disabled"
f" -DENABLE_TESTS=0"
f" -DENABLE_UTILS=0"
f" -DCMAKE_TOOLCHAIN_FILE={toolchain_file}"
f" {REPO_PATH}"
f" -B {CH_PROFILE_BUILD_DIR}"
)
results.append(
Result.from_commands_run(
name="Profile collection CMake",
command=cmake_cmd,
)
)
res = results[-1].is_ok()
if res:
# Build may fail at link step but profraw files from compilation
# steps are still useful for PGO
build_result = Result.from_commands_run(
name="Profile collection build (Datastore)",
command=f"{CUSTOM_NINJA} -C {CH_PROFILE_BUILD_DIR} clickhouse",
)
if not build_result.is_ok():
print(
"Datastore build finished with errors"
" (link failures with instrumented compiler are expected)."
" Profraw files from compilation steps should still be available."
)
build_result.status = Result.Status.OK
build_result.info = "Build failed at link step (expected); profraw files collected"
results.append(build_result)
# Merge profraw files with llvm-profdata-21 (it supports the zlib-compressed
# profile format the instrumented clang can emit)
profraw_dir = f"{STAGE1_BUILD_DIR}/profiles/"
if os.path.isdir(profraw_dir) and os.listdir(profraw_dir):
results.append(
Result.from_commands_run(
name="Merge PGO profiles",
command=(
f"llvm-profdata-21 merge"
f" -output={PROFDATA_PATH}"
f" {profraw_dir}"
),
)
)
res = results[-1].is_ok()
else:
print(f"ERROR: No profraw files found in {profraw_dir}")
res = False
# Save .ninja_log for packaging (contains build timing data for scheduling)
ninja_log_src = f"{CH_PROFILE_BUILD_DIR}/.ninja_log"
ninja_log_saved = f"{TEMP}/clickhouse-ninja-log"
if os.path.exists(ninja_log_src):
shutil.copy2(ninja_log_src, ninja_log_saved)
print(f"Saved .ninja_log ({os.path.getsize(ninja_log_saved)} bytes)")
else:
print("WARNING: .ninja_log not found after profile collection build")
# Clean up to free disk space (~80 GB)
print("Cleaning Stage 1 build and CH profile build to free disk space")
clean_dirs(STAGE1_BUILD_DIR, CH_PROFILE_BUILD_DIR, STAGE1_INSTALL_DIR)
# Stage 3: Build PGO-optimized clang with all projects/targets and BOLT-ready flags
if res and JobStages.STAGE2_BUILD in stages:
clean_dirs(STAGE2_BUILD_DIR, STAGE2_INSTALL_DIR)
os.makedirs(STAGE2_BUILD_DIR, exist_ok=True)
builtin_targets = ";".join(t for t, _ in CROSS_BUILTIN_TARGETS)
# Create a minimal stub sysroot for cross-target builtins compilation.
# Uses --sysroot to isolate from host headers, -ffreestanding for
# compiler-provided headers (stdint.h, stddef.h, etc.), and
# COMPILER_RT_BAREMETAL_BUILD=ON to exclude files needing full libc.
# Remaining files need only a few OS headers which we stub out here.
cross_sysroot = f"{TEMP}/cross-builtins-sysroot"
cross_inc = f"{cross_sysroot}/include"
for subdir in ["", "sys", "linux", "asm"]:
os.makedirs(f"{cross_inc}/{subdir}", exist_ok=True)
with open(f"{cross_inc}/assert.h", "w") as f:
f.write(
"#define assert(x) ((void)0)\n"
"#define static_assert _Static_assert\n"
)
with open(f"{cross_inc}/sys/auxv.h", "w") as f:
# Stubs for cpu_model/aarch64.c (FreeBSD: elf_aux_info, Linux: getauxval)
f.write(
"#pragma once\n"
"#define AT_HWCAP 16\n"
"#define AT_HWCAP2 26\n"
"int elf_aux_info(int, void *, int);\n"
"unsigned long getauxval(unsigned long);\n"
)
with open(f"{cross_inc}/linux/unistd.h", "w") as f:
f.write("#include <asm/unistd.h>\n")
with open(f"{cross_inc}/asm/unistd.h", "w") as f:
# Stub for clear_cache.c on riscv64
f.write("#define __NR_riscv_flush_icache 259\n")
cross_flags = (
f"-ffreestanding --sysroot={cross_sysroot} -isystem {cross_inc}"
)
builtin_cmake_args = " ".join(
f"-DBUILTINS_{triple}_CMAKE_SYSTEM_NAME={system}"
f" -DBUILTINS_{triple}_COMPILER_RT_BAREMETAL_BUILD=ON"
f' -DBUILTINS_{triple}_CMAKE_C_FLAGS="{cross_flags}"'
f' -DBUILTINS_{triple}_CMAKE_CXX_FLAGS="{cross_flags}"'
for triple, system in CROSS_BUILTIN_TARGETS
)
cmake_cmd = (
f"cmake -G Ninja"
f' -DLLVM_ENABLE_PROJECTS="{STAGE2_LLVM_PROJECTS}"'
f' -DLLVM_ENABLE_RUNTIMES="compiler-rt"'
f" -DLLVM_TARGETS_TO_BUILD=all"
f" -DCMAKE_BUILD_TYPE=Release"
f" -DLLVM_PROFDATA_FILE={PROFDATA_PATH}"
f" -DCMAKE_C_COMPILER=clang-21"
f" -DCMAKE_CXX_COMPILER=clang++-21"
f" -DLLVM_ENABLE_LLD=ON"
f" -DLLVM_ENABLE_LTO=Thin"
f' -DCMAKE_EXE_LINKER_FLAGS="-Wl,--emit-relocs,-znow"'
f' -DCMAKE_SHARED_LINKER_FLAGS="-Wl,--emit-relocs,-znow"'
f" -DLLVM_ENABLE_TERMINFO=OFF"
f" -DLLVM_ENABLE_ZLIB=FORCE_ON"
f" -DLLVM_ENABLE_ZSTD=OFF"
f" -DLLVM_BINUTILS_INCDIR=/usr/include"
f' -DLLVM_BUILTIN_TARGETS="{builtin_targets}"'
f" {builtin_cmake_args}"
f" -DCMAKE_INSTALL_PREFIX={STAGE2_INSTALL_DIR}"
f" -S {LLVM_SOURCE_DIR}/llvm"
f" -B {STAGE2_BUILD_DIR}"
)
results.append(
Result.from_commands_run(
name="Stage 2 CMake (PGO-optimized clang)",
command=cmake_cmd,
)
)
res = results[-1].is_ok()
if res:
results.append(
Result.from_commands_run(
name="Stage 2 Build",
command=f"{CUSTOM_NINJA} -C {STAGE2_BUILD_DIR}",
)
)
res = results[-1].is_ok()
if res:
results.append(
Result.from_commands_run(
name="Stage 2 Install",
command=f"{CUSTOM_NINJA} -C {STAGE2_BUILD_DIR} install",
)
)
res = results[-1].is_ok()
# Clean stage2 build dir, keep install
print("Cleaning Stage 2 build directory to free disk space")
clean_dirs(STAGE2_BUILD_DIR)
# Stage 4: BOLT optimization (best-effort)
# BOLT can fail due to architecture-specific issues (e.g., ADR relaxation
# on aarch64 with AMDGPU code) or disk space constraints (fdata files are
# ~15 MB per compilation unit). If BOLT fails, we still have a PGO-optimized
# toolchain which provides ~23% compilation speedup.
if res and JobStages.BOLT_OPTIMIZATION in stages:
bolt_ok = True
bolt_results = []
clang_binary = f"{STAGE2_INSTALL_DIR}/bin/clang-21"
# Find the actual clang binary (it may be clang-21, clang-20, etc.)
if not os.path.exists(clang_binary):
candidates = sorted(
glob.glob(f"{STAGE2_INSTALL_DIR}/bin/clang-[0-9]*"),
reverse=True,
)
if candidates:
clang_binary = candidates[0]
else:
clang_binary = f"{STAGE2_INSTALL_DIR}/bin/clang"
clang_basename = os.path.basename(clang_binary)
print(f"BOLT target binary: {clang_binary}")
llvm_bolt = f"{STAGE2_INSTALL_DIR}/bin/llvm-bolt"
merge_fdata = f"{STAGE2_INSTALL_DIR}/bin/merge-fdata"
clang_instrumented = f"{clang_binary}.inst"
clang_bolted = f"{clang_binary}.bolt"
clangpp_inst = None # set in step 2 if BOLT instrumentation succeeds
clean_dirs(BOLT_PROFILES_DIR, CH_BOLT_BUILD_DIR)
os.makedirs(BOLT_PROFILES_DIR, exist_ok=True)
# Step 1: Instrument clang with BOLT
result = Result.from_commands_run(
name="BOLT instrument clang",
command=(
f"{llvm_bolt} {clang_binary}"
f" -o {clang_instrumented}"
f" -instrument"
f" --instrumentation-file-append-pid"
f" --instrumentation-file={BOLT_PROFILES_DIR}/prof"
),
)
bolt_results.append(result)
if not result.is_ok():
bolt_ok = False
print(
"BOLT instrumentation failed"
" (known issue on aarch64 with AMDGPU backend code)."
" Continuing with PGO-only toolchain."
)
# Step 2: Create symlinks for the instrumented binary so cmake can
# use it as both C and C++ compiler (clang dispatches based on argv[0])
if bolt_ok:
inst_dir = os.path.dirname(clang_instrumented)
clangpp_inst = os.path.join(inst_dir, "clang++.inst")
try:
if os.path.exists(clangpp_inst):
os.remove(clangpp_inst)
os.symlink(os.path.basename(clang_instrumented), clangpp_inst)
print(f"Created symlink: {clangpp_inst} -> {os.path.basename(clang_instrumented)}")
except OSError as e:
print(f"Failed to create clang++ symlink: {e}")
bolt_ok = False
# Step 3: Configure Datastore build with BOLT-instrumented clang
if bolt_ok:
cmake_cmd = (
f"cmake"
f" -DCMAKE_BUILD_TYPE=None"
f" -DENABLE_THINLTO=0"
f" -DCMAKE_C_COMPILER={clang_instrumented}"
f" -DCMAKE_CXX_COMPILER={clangpp_inst}"
f" -DCOMPILER_CACHE=disabled"
f" -DENABLE_TESTS=0"
f" -DENABLE_UTILS=0"
f" -DCMAKE_TOOLCHAIN_FILE={toolchain_file}"
f" {REPO_PATH}"
f" -B {CH_BOLT_BUILD_DIR}"
)
result = Result.from_commands_run(
name="BOLT profile collection CMake",
command=cmake_cmd,
)
bolt_results.append(result)
if not result.is_ok():
bolt_ok = False
print("BOLT profile collection cmake failed. Continuing with PGO-only.")
# Step 4: Time-limited build to collect BOLT profiles
# Each compilation unit writes ~15 MB fdata file. With -j4 and a 20 minute
# timeout, we expect ~300 compilations producing ~4.5 GB of profiles,
# which gives BOLT enough data to optimize hot code paths.
if bolt_ok:
result = Result.from_commands_run(
name="BOLT profile collection build (time-limited)",
command=(
f"bash -c 'timeout --signal=INT --kill-after=120"
f" {BOLT_PROFILE_TIMEOUT}"
f" {CUSTOM_NINJA} -j{BOLT_PROFILE_PARALLELISM} -k0"
f" -C {CH_BOLT_BUILD_DIR} clickhouse"
f" ; exit 0'"
),
)
bolt_results.append(result)
# Check if we collected any profiles
fdata_files = glob.glob(f"{BOLT_PROFILES_DIR}/prof.*")
if not fdata_files:
bolt_ok = False
print("No BOLT fdata profiles collected. Continuing with PGO-only.")
else:
print(f"Collected {len(fdata_files)} BOLT profile files")
# Step 5: Merge BOLT profiles
if bolt_ok:
result = Result.from_commands_run(
name="Merge BOLT profiles",
command=(
f"{merge_fdata} -o {BOLT_FDATA_PATH}"
f" {BOLT_PROFILES_DIR}/prof.*"
),
)
bolt_results.append(result)
if not result.is_ok():
bolt_ok = False
print("BOLT profile merge failed. Continuing with PGO-only.")
# Step 6: Apply BOLT optimization
# Flags match upstream perf-helper.py bolt_optimize
if bolt_ok:
result = Result.from_commands_run(
name="BOLT optimize clang",
command=(
f"{llvm_bolt} {clang_binary}"
f" -o {clang_bolted}"
f" -data={BOLT_FDATA_PATH}"
f" -reorder-blocks=ext-tsp"
f" -reorder-functions=cdsort"
f" -split-functions"
f" -split-all-cold"
f" -split-eh"
f" -dyno-stats"
f" -use-gnu-stack"
),
)
bolt_results.append(result)
if not result.is_ok():
bolt_ok = False
print("BOLT optimization failed. Continuing with PGO-only.")
# Step 7: Replace original binary with BOLTed version
if bolt_ok:
result = Result.from_commands_run(
name="Install BOLTed clang",
command=f"mv {clang_bolted} {clang_binary}",
)
bolt_results.append(result)
if not result.is_ok():
bolt_ok = False
if bolt_ok:
print("BOLT optimization applied successfully")
results.extend(bolt_results)
else:
print("Packaging PGO-only toolchain (BOLT was skipped or failed)")
# Mark all BOLT results as skipped so they don't fail the overall job
for r in bolt_results:
r.status = Result.Status.SKIPPED
results.extend(bolt_results)
# Clean up BOLT intermediates
print("Cleaning BOLT intermediate files")
clean_dirs(CH_BOLT_BUILD_DIR, BOLT_PROFILES_DIR)
for f in [clang_instrumented, clangpp_inst, BOLT_FDATA_PATH, clang_bolted]:
try:
if f and os.path.exists(f):
os.remove(f)
except OSError:
pass
# Stage 5: Package
if res and JobStages.PACKAGE in stages:
# Copy custom Ninja binary into the toolchain
if os.path.exists(CUSTOM_NINJA):
ninja_dest = f"{STAGE2_INSTALL_DIR}/bin/ninja"
shutil.copy2(CUSTOM_NINJA, ninja_dest)
os.chmod(ninja_dest, 0o755)
print(f"Installed custom Ninja to {ninja_dest}")
# Copy .ninja_log for pre-seeding CI builds
ninja_log_saved = f"{TEMP}/clickhouse-ninja-log"
if os.path.exists(ninja_log_saved):
ninja_log_dir = f"{STAGE2_INSTALL_DIR}/{NINJA_LOG_SHARE_DIR}"
os.makedirs(ninja_log_dir, exist_ok=True)
shutil.copy2(ninja_log_saved, f"{ninja_log_dir}/ninja_log")
print(f"Installed .ninja_log to {ninja_log_dir}/ninja_log")
# LLVM installs the versioned `clang-21` plus unversioned `clang++`->`clang`->`clang-21`,
# but not a versioned `clang++-21`. Datastore selects compilers by versioned name, so
# without this the C++ compiler resolves to whatever `clang++-21` is elsewhere on PATH (e.g.
# a distro one) while C/ASM use this toolchain - a silent mismatch. Add the missing symlink.
clangpp = f"{STAGE2_INSTALL_DIR}/bin/clang++-21"
if not os.path.lexists(clangpp):
os.symlink("clang", clangpp)
print(f"Created symlink {clangpp} -> clang")
# Strip ELF executables and shared libraries to reduce archive size
# (relocations from --emit-relocs and LTO symbols are no longer needed).
# Use "file" to skip scripts (Python, Perl, shell) that strip cannot handle.
results.append(
Result.from_commands_run(
name="Strip binaries",
command=(
f"find {STAGE2_INSTALL_DIR}/bin -type f -executable"
f" -exec sh -c 'file \"$1\" | grep -q ELF && strip --strip-unneeded \"$1\"' _ {{}} \\;"
f" && find {STAGE2_INSTALL_DIR}/lib -name '*.so*' -type f"
f" -exec strip --strip-unneeded {{}} +"
),
)
)
res = results[-1].is_ok()
if res:
output_path = f"{OUTPUT_DIR}/clang-pgo-bolt.tar.zst"
results.append(
Result.from_commands_run(
name="Package toolchain",
command=(
f"tar -C {STAGE2_INSTALL_DIR} -cf - ."
f" | zstd -T0 -19 -o {output_path}"
),
)
)
res = results[-1].is_ok()
if res:
file_size_mb = os.path.getsize(output_path) / (1024 * 1024)
print(f"Toolchain archive size: {file_size_mb:.1f} MB")
Result.create_from(results=results).complete_job()
if __name__ == "__main__":
main()