feat(toolchain): resolve the compiler in one place
Every clang++, clang and llvm-ar the tool spawned was a bare name on PATH, the std module source was /usr/share/libc++/v1/std.cppm, the WASI sysroot /usr/share/wasi-sysroot and the mingw headers /usr/x86_64-w64-mingw32, spelled out at eighteen call sites. A toolchain installed anywhere else, which is what the Flatpak ships under /app, had no way in. Crafter.Build:Toolchain resolves all of it once per process: CRAFTER_BUILD_LLVM_DIR, else a clang++ installed beside crafter-build (<prefix>/bin, found through the same walk GetCrafterBuildHome does), else PATH. What sits beside the compiler is used when present: its libclang.so, its clang-c headers, its std module source, its WASI sysroot. An explicit prefix without a clang++ is an error rather than a fallback, for the same reason CRAFTER_BUILD_LIBCLANG was already exclusive. CRAFTER_BUILD_LIBCXX_DIR points at a libc++ install other than the compiler's own, and CRAFTER_MINGW_DIR now does what its help line always claimed: it is passed as --sysroot for the mingw target. Binaries linked by a toolchain outside /usr get an rpath to its lib dir, since its libc++.so.1 is not on the loader's default path. HostTarget() is the resolved clang's -print-target-triple. A clang built without a vendor reports x86_64-unknown-linux-gnu where Arch's reports x86_64-pc-linux-gnu, so target equality that decides whether a test runs locally and which targets a bare `test` sweeps goes through SameTarget, which ignores the vendor field. The strings themselves are never rewritten: clang rejects a PCM whose triple differs from the importing unit's. The examples and README stop hardcoding the host triple; the default is already the host. `crafter-build --version` prints the version baked in at build time (CRAFTER_BUILD_VERSION, else `git describe`, else 0.0.0-dev), the compiler and the host triple, and is answered before any argument is forwarded to project.cpp. The version is a define on the library, so each version is its own library variant and relinks the executable and tests once per commit. build.sh gains a shebang and `set -eu`, honours CRAFTER_BUILD_LLVM_DIR and CRAFTER_BUILD_VERSION, skips the glslang clone when it is already there, and writes a fresh archive instead of appending to one that may hold members from an older layout. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
68e9b6bfa5
commit
046609ff6f
18 changed files with 767 additions and 190 deletions
188
build.sh
188
build.sh
|
|
@ -1,141 +1,125 @@
|
|||
#!/bin/sh
|
||||
#SPDX-License-Identifier: LGPL-3.0-only
|
||||
#SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||
|
||||
mkdir -p build
|
||||
mkdir -p bin
|
||||
mkdir -p lib
|
||||
mkdir -p share/crafter-build
|
||||
mkdir -p share/crafter-build/wasi-runtime
|
||||
# Bootstrap: compile crafter-build by hand, without crafter-build. The
|
||||
# self-host build through project.cpp follows (`./bin/crafter-build`).
|
||||
set -eu
|
||||
|
||||
# The toolchain. CRAFTER_BUILD_LLVM_DIR names an LLVM prefix (the Flatpak
|
||||
# sets /app); otherwise the tools come from PATH. The same rule the built
|
||||
# binary applies at run time (Crafter.Build:Toolchain).
|
||||
if [ -n "${CRAFTER_BUILD_LLVM_DIR:-}" ]; then
|
||||
CXX="$CRAFTER_BUILD_LLVM_DIR/bin/clang++"
|
||||
CC="$CRAFTER_BUILD_LLVM_DIR/bin/clang"
|
||||
AR="$CRAFTER_BUILD_LLVM_DIR/bin/llvm-ar"
|
||||
LLVM_CONFIG="$CRAFTER_BUILD_LLVM_DIR/bin/llvm-config"
|
||||
# A prefix other than the system one keeps its libc++ off the loader's
|
||||
# default path; the rpath is what lets the binary start.
|
||||
if [ "$CRAFTER_BUILD_LLVM_DIR" != /usr ]; then
|
||||
RPATH_FLAG="-Wl,-rpath,$CRAFTER_BUILD_LLVM_DIR/lib"
|
||||
else
|
||||
RPATH_FLAG=""
|
||||
fi
|
||||
if [ -f "$CRAFTER_BUILD_LLVM_DIR/share/libc++/v1/std.cppm" ]; then
|
||||
STD_CPPM="$CRAFTER_BUILD_LLVM_DIR/share/libc++/v1/std.cppm"
|
||||
else
|
||||
STD_CPPM=/usr/share/libc++/v1/std.cppm
|
||||
fi
|
||||
else
|
||||
CXX=clang++
|
||||
CC=clang
|
||||
AR=llvm-ar
|
||||
LLVM_CONFIG=llvm-config
|
||||
RPATH_FLAG=""
|
||||
STD_CPPM=/usr/share/libc++/v1/std.cppm
|
||||
fi
|
||||
|
||||
# The version baked into the binary: the environment (CI sets it from the
|
||||
# tag), else the nearest tag, else a development marker. `latest` is the
|
||||
# rolling CI tag and never a version.
|
||||
VERSION="${CRAFTER_BUILD_VERSION:-$(git describe --tags --always --dirty --exclude=latest 2>/dev/null || echo 0.0.0-dev)}"
|
||||
|
||||
mkdir -p build bin lib share/crafter-build share/crafter-build/wasi-runtime
|
||||
|
||||
cp wasi-runtime/runtime.js share/crafter-build/wasi-runtime/
|
||||
cp wasi-runtime/index.html.in share/crafter-build/wasi-runtime/
|
||||
|
||||
cp interfaces/Crafter.Build.cppm share/crafter-build/
|
||||
cp interfaces/Crafter.Build-Shader.cppm share/crafter-build/
|
||||
cp interfaces/Crafter.Build-Platform.cppm share/crafter-build/
|
||||
cp interfaces/Crafter.Build-Interface.cppm share/crafter-build/
|
||||
cp interfaces/Crafter.Build-Implementation.cppm share/crafter-build/
|
||||
cp interfaces/Crafter.Build-External.cppm share/crafter-build/
|
||||
cp interfaces/Crafter.Build-Clang.cppm share/crafter-build/
|
||||
cp interfaces/Crafter.Build-Test.cppm share/crafter-build/
|
||||
cp interfaces/Crafter.Build-Lint.cppm share/crafter-build/
|
||||
cp interfaces/Crafter.Build-Progress.cppm share/crafter-build/
|
||||
cp interfaces/Crafter.Build-Asset.cppm share/crafter-build/
|
||||
# Partition order is the import order: every unit is listed before the
|
||||
# units that import it, and the umbrella last.
|
||||
PARTITIONS="Toolchain Shader Platform Interface Implementation External Clang Test Lint Progress Asset"
|
||||
|
||||
git clone https://github.com/KhronosGroup/glslang.git ./build/glslang
|
||||
cp interfaces/Crafter.Build.cppm share/crafter-build/
|
||||
for p in $PARTITIONS; do
|
||||
cp "interfaces/Crafter.Build-$p.cppm" share/crafter-build/
|
||||
done
|
||||
|
||||
if [ ! -d build/glslang/.git ]; then
|
||||
git clone --depth 1 https://github.com/KhronosGroup/glslang.git build/glslang
|
||||
fi
|
||||
|
||||
current_dir=$(pwd)
|
||||
|
||||
cd ./build/glslang
|
||||
cd build/glslang
|
||||
cmake -B build \
|
||||
-DCMAKE_C_COMPILER=clang \
|
||||
-DCMAKE_CXX_COMPILER=clang++ \
|
||||
-DCMAKE_C_COMPILER="$CC" \
|
||||
-DCMAKE_CXX_COMPILER="$CXX" \
|
||||
-DCMAKE_CXX_FLAGS="-stdlib=libc++" \
|
||||
-DCMAKE_EXE_LINKER_FLAGS="-stdlib=libc++" \
|
||||
-DCMAKE_SHARED_LINKER_FLAGS="-stdlib=libc++" \
|
||||
-DCMAKE_EXE_LINKER_FLAGS="-stdlib=libc++ $RPATH_FLAG" \
|
||||
-DCMAKE_SHARED_LINKER_FLAGS="-stdlib=libc++ $RPATH_FLAG" \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DCMAKE_ARCHIVE_OUTPUT_DIRECTORY=$current_dir/build \
|
||||
-DCMAKE_LIBRARY_OUTPUT_DIRECTORY=$current_dir/build \
|
||||
-DCMAKE_ARCHIVE_OUTPUT_DIRECTORY="$current_dir/build" \
|
||||
-DCMAKE_LIBRARY_OUTPUT_DIRECTORY="$current_dir/build" \
|
||||
-DENABLE_OPT=OFF
|
||||
|
||||
cmake --build build --config Release
|
||||
cd ../../
|
||||
|
||||
MARCH="${CRAFTER_BUILD_MARCH:-native}"
|
||||
MTUNE="${CRAFTER_BUILD_MTUNE:-native}"
|
||||
# The bootstrap targets the host toolchain's own triple: x86_64-pc-linux-gnu
|
||||
# on glibc distros, x86_64-alpine-linux-musl on Alpine. Override with
|
||||
# CRAFTER_BUILD_TARGET. The per-target define (triple with '-' -> '_') is the
|
||||
# same one the self-hosted build emits, so the host-gated code paths match.
|
||||
TARGET="${CRAFTER_BUILD_TARGET:-$(clang++ -print-target-triple)}"
|
||||
# The bootstrap targets the toolchain's own triple: x86_64-pc-linux-gnu on
|
||||
# glibc distros, x86_64-unknown-linux-gnu for the bundled LLVM,
|
||||
# x86_64-alpine-linux-musl on Alpine. Override with CRAFTER_BUILD_TARGET.
|
||||
TARGET="${CRAFTER_BUILD_TARGET:-$("$CXX" -print-target-triple)}"
|
||||
TARGET_DEFINE="CRAFTER_BUILD_CONFIGURATION_TARGET_$(printf '%s' "$TARGET" | tr - _)"
|
||||
|
||||
# Crafter.Build-Lint includes libclang's clang-c/Index.h. glibc distros keep
|
||||
# it in /usr/include; Alpine keeps LLVM under /usr/lib/llvmNN, so ask
|
||||
# llvm-config and add the dir when it is not already on the default path
|
||||
# it in /usr/include; other prefixes keep it under their own include dir, so
|
||||
# ask llvm-config and add the dir when it is not already on the default path
|
||||
# (never add /usr/include itself: that breaks libc++'s #include_next).
|
||||
LLVM_INCDIR="$(llvm-config --includedir 2>/dev/null || true)"
|
||||
LLVM_INCDIR="$("$LLVM_CONFIG" --includedir 2>/dev/null || true)"
|
||||
case "$LLVM_INCDIR" in ""|/usr/include) LLVM_INCFLAG="" ;; *) LLVM_INCFLAG="-I$LLVM_INCDIR" ;; esac
|
||||
|
||||
common_options="--target=$TARGET -stdlib=libc++ -I./build/glslang $LLVM_INCFLAG -std=c++26 -O3 -march=$MARCH -mtune=$MTUNE -fprebuilt-module-path=./build -D $TARGET_DEFINE -D CRAFTER_BUILD_CONFIGURATION_TYPE_EXECUTABLE -c"
|
||||
common_options="--target=$TARGET -stdlib=libc++ -I./build/glslang $LLVM_INCFLAG -std=c++26 -O3 -march=$MARCH -mtune=$MTUNE -fprebuilt-module-path=./build -D $TARGET_DEFINE -D CRAFTER_BUILD_CONFIGURATION_TYPE_EXECUTABLE -D CRAFTER_BUILD_VERSION=\"$VERSION\" -c"
|
||||
|
||||
clang++ --target=$TARGET -std=c++26 -stdlib=libc++ -O3 -march=$MARCH -mtune=$MTUNE -Wno-reserved-identifier -Wno-reserved-module-identifier --precompile /usr/share/libc++/v1/std.cppm -o ./build/std.pcm
|
||||
"$CXX" --target="$TARGET" -std=c++26 -stdlib=libc++ -O3 -march="$MARCH" -mtune="$MTUNE" -Wno-reserved-identifier -Wno-reserved-module-identifier --precompile "$STD_CPPM" -o ./build/std.pcm
|
||||
|
||||
clang++ $common_options -fmodule-output interfaces/Crafter.Build-Shader.cppm -o ./build/Crafter.Build-Shader.o
|
||||
clang++ $common_options -fmodule-output interfaces/Crafter.Build-Platform.cppm -o ./build/Crafter.Build-Platform.o
|
||||
clang++ $common_options -fmodule-output interfaces/Crafter.Build-Interface.cppm -o ./build/Crafter.Build-Interface.o
|
||||
clang++ $common_options -fmodule-output interfaces/Crafter.Build-Implementation.cppm -o ./build/Crafter.Build-Implementation.o
|
||||
clang++ $common_options -fmodule-output interfaces/Crafter.Build-External.cppm -o ./build/Crafter.Build-External.o
|
||||
clang++ $common_options -fmodule-output interfaces/Crafter.Build-Clang.cppm -o ./build/Crafter.Build-Clang.o
|
||||
clang++ $common_options -fmodule-output interfaces/Crafter.Build-Test.cppm -o ./build/Crafter.Build-Test.o
|
||||
clang++ $common_options -fmodule-output interfaces/Crafter.Build-Lint.cppm -o ./build/Crafter.Build-Lint.o
|
||||
clang++ $common_options -fmodule-output interfaces/Crafter.Build-Progress.cppm -o ./build/Crafter.Build-Progress.o
|
||||
# Asset partition: bootstrap compiles it WITHOUT CRAFTER_BUILD_HAS_ASSET so
|
||||
# CompressAsset takes the stub branch and the impl unit doesn't try to
|
||||
# `import Crafter.Asset` (no Crafter.Asset PCM exists at this stage — the
|
||||
# self-host pass that follows wires in the real dep).
|
||||
clang++ $common_options -fmodule-output interfaces/Crafter.Build-Asset.cppm -o ./build/Crafter.Build-Asset.o
|
||||
clang++ $common_options -fmodule-output interfaces/Crafter.Build.cppm -o ./build/Crafter.Build.o
|
||||
OBJECTS=""
|
||||
for p in $PARTITIONS; do
|
||||
"$CXX" $common_options -fmodule-output "interfaces/Crafter.Build-$p.cppm" -o "./build/Crafter.Build-$p.o"
|
||||
OBJECTS="$OBJECTS ./build/Crafter.Build-$p.o"
|
||||
done
|
||||
"$CXX" $common_options -fmodule-output interfaces/Crafter.Build.cppm -o ./build/Crafter.Build.o
|
||||
OBJECTS="$OBJECTS ./build/Crafter.Build.o"
|
||||
|
||||
clang++ $common_options ./implementations/Crafter.Build-Shader.cpp -o ./build/Crafter.Build-Shader_impl.o
|
||||
clang++ $common_options ./implementations/Crafter.Build-Platform.cpp -o ./build/Crafter.Build-Platform_impl.o
|
||||
clang++ $common_options ./implementations/Crafter.Build-Interface.cpp -o ./build/Crafter.Build-Interface_impl.o
|
||||
clang++ $common_options ./implementations/Crafter.Build-Implementation.cpp -o ./build/Crafter.Build-Implementation_impl.o
|
||||
clang++ $common_options ./implementations/Crafter.Build-External.cpp -o ./build/Crafter.Build-External_impl.o
|
||||
clang++ $common_options ./implementations/Crafter.Build-Clang.cpp -o ./build/Crafter.Build-Clang_impl.o
|
||||
clang++ $common_options ./implementations/Crafter.Build-Test.cpp -o ./build/Crafter.Build-Test_impl.o
|
||||
clang++ $common_options ./implementations/Crafter.Build-Lint.cpp -o ./build/Crafter.Build-Lint_impl.o
|
||||
clang++ $common_options ./implementations/Crafter.Build-Progress.cpp -o ./build/Crafter.Build-Progress_impl.o
|
||||
clang++ $common_options ./implementations/Crafter.Build-Asset.cpp -o ./build/Crafter.Build-Asset_impl.o
|
||||
clang++ $common_options ./implementations/main.cpp -o ./build/main.o
|
||||
for p in $PARTITIONS; do
|
||||
"$CXX" $common_options "./implementations/Crafter.Build-$p.cpp" -o "./build/Crafter.Build-${p}_impl.o"
|
||||
OBJECTS="$OBJECTS ./build/Crafter.Build-${p}_impl.o"
|
||||
done
|
||||
"$CXX" $common_options ./implementations/main.cpp -o ./build/main.o
|
||||
|
||||
ar rcs ./lib/libcrafter-build.a \
|
||||
./build/Crafter.Build-Shader.o \
|
||||
./build/Crafter.Build-Platform.o \
|
||||
./build/Crafter.Build-Interface.o \
|
||||
./build/Crafter.Build-Implementation.o \
|
||||
./build/Crafter.Build-External.o \
|
||||
./build/Crafter.Build-Clang.o \
|
||||
./build/Crafter.Build-Test.o \
|
||||
./build/Crafter.Build-Lint.o \
|
||||
./build/Crafter.Build-Progress.o \
|
||||
./build/Crafter.Build-Asset.o \
|
||||
./build/Crafter.Build.o \
|
||||
./build/Crafter.Build-Shader_impl.o \
|
||||
./build/Crafter.Build-Platform_impl.o \
|
||||
./build/Crafter.Build-Interface_impl.o \
|
||||
./build/Crafter.Build-Implementation_impl.o \
|
||||
./build/Crafter.Build-External_impl.o \
|
||||
./build/Crafter.Build-Clang_impl.o \
|
||||
./build/Crafter.Build-Test_impl.o \
|
||||
./build/Crafter.Build-Lint_impl.o \
|
||||
./build/Crafter.Build-Progress_impl.o \
|
||||
./build/Crafter.Build-Asset_impl.o
|
||||
# A fresh archive each time: `ar rcs` into an existing one keeps members a
|
||||
# previous layout left behind.
|
||||
rm -f ./lib/libcrafter-build.a
|
||||
"$AR" rcs ./lib/libcrafter-build.a $OBJECTS
|
||||
|
||||
clang++ --target=$TARGET -std=c++26 -stdlib=libc++ -O3 -march=$MARCH -mtune=$MTUNE -fuse-ld=lld \
|
||||
-Wl,--export-dynamic \
|
||||
"$CXX" --target="$TARGET" -std=c++26 -stdlib=libc++ -O3 -march="$MARCH" -mtune="$MTUNE" -fuse-ld=lld \
|
||||
-Wl,--export-dynamic $RPATH_FLAG \
|
||||
-L./build \
|
||||
./build/Crafter.Build-Shader.o \
|
||||
./build/Crafter.Build-Platform.o \
|
||||
./build/Crafter.Build-Interface.o \
|
||||
./build/Crafter.Build-Implementation.o \
|
||||
./build/Crafter.Build-External.o \
|
||||
./build/Crafter.Build-Clang.o \
|
||||
./build/Crafter.Build-Test.o \
|
||||
./build/Crafter.Build-Lint.o \
|
||||
./build/Crafter.Build-Progress.o \
|
||||
./build/Crafter.Build-Asset.o \
|
||||
./build/Crafter.Build.o \
|
||||
./build/Crafter.Build-Shader_impl.o \
|
||||
./build/Crafter.Build-Platform_impl.o \
|
||||
./build/Crafter.Build-Interface_impl.o \
|
||||
./build/Crafter.Build-Implementation_impl.o \
|
||||
./build/Crafter.Build-External_impl.o \
|
||||
./build/Crafter.Build-Clang_impl.o \
|
||||
./build/Crafter.Build-Test_impl.o \
|
||||
./build/Crafter.Build-Lint_impl.o \
|
||||
./build/Crafter.Build-Progress_impl.o \
|
||||
./build/Crafter.Build-Asset_impl.o \
|
||||
$OBJECTS \
|
||||
./build/main.o \
|
||||
-lSPIRV -lGenericCodeGen -lglslang -lOSDependent -lMachineIndependent -lglslang-default-resource-limits \
|
||||
-ldl \
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view>)
|
|||
cfg.path = "./";
|
||||
cfg.name = "hello";
|
||||
cfg.outputName = "hello";
|
||||
cfg.target = "x86_64-pc-linux-gnu";
|
||||
cfg.type = ConfigurationType::Executable;
|
||||
|
||||
std::array<fs::path, 0> ifaces = {};
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view>)
|
|||
lib->path = "./mylib/";
|
||||
lib->name = "MyMath";
|
||||
lib->outputName = "MyMath";
|
||||
lib->target = "x86_64-pc-linux-gnu";
|
||||
lib->type = ConfigurationType::LibraryStatic;
|
||||
{
|
||||
std::array<fs::path, 1> ifaces = { "MyMath" };
|
||||
|
|
@ -29,7 +28,6 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view>)
|
|||
cfg.path = "./";
|
||||
cfg.name = "math-app";
|
||||
cfg.outputName = "math-app";
|
||||
cfg.target = "x86_64-pc-linux-gnu";
|
||||
cfg.type = ConfigurationType::Executable;
|
||||
cfg.dependencies = { lib.get() };
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view>)
|
|||
cfg.path = "./mylib/";
|
||||
cfg.name = "MyMath";
|
||||
cfg.outputName = "MyMath";
|
||||
cfg.target = "x86_64-pc-linux-gnu";
|
||||
cfg.type = ConfigurationType::LibraryStatic;
|
||||
|
||||
std::array<fs::path, 1> ifaces = { "MyMath" };
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view>)
|
|||
cfg.path = "./";
|
||||
cfg.name = "greeter-app";
|
||||
cfg.outputName = "greeter-app";
|
||||
cfg.target = "x86_64-pc-linux-gnu";
|
||||
cfg.type = ConfigurationType::Executable;
|
||||
|
||||
std::array<fs::path, 1> ifaces = { "interfaces/Greeter" };
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ export module Crafter.Build:Clang_impl;
|
|||
import std;
|
||||
import :Clang;
|
||||
import :Platform;
|
||||
import :Toolchain;
|
||||
import :Test;
|
||||
import :Lint;
|
||||
import :Progress;
|
||||
|
|
@ -404,6 +405,11 @@ CompileCommand Crafter::GetCompileCommand(const Configuration& config) {
|
|||
}
|
||||
|
||||
BuildResult Crafter::Build(Configuration& config, std::unordered_map<fs::path, std::shared_future<BuildResult>>& depResults, std::mutex& depMutex) {
|
||||
// Before anything is spawned: one message naming the missing compiler
|
||||
// beats a clang++-not-found per translation unit.
|
||||
if (const Toolchain& tc = GetToolchain(); !tc.error.empty()) {
|
||||
return {tc.error, false, {}};
|
||||
}
|
||||
// Reset per-build cached state on every Module/ModulePartition so that
|
||||
// successive Build() calls on the same Configuration re-evaluate mtimes
|
||||
// (incremental-rebuild test scenarios).
|
||||
|
|
@ -449,7 +455,7 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_map<fs::path, s
|
|||
// triples (wasi, wasip1, wasip2, ...); the sysroot's per-triple subdirs
|
||||
// handle the differences.
|
||||
if (config.sysroot.empty() && config.target.starts_with("wasm32")) {
|
||||
config.sysroot = "/usr/share/wasi-sysroot";
|
||||
config.sysroot = GetToolchain().wasiSysroot.string();
|
||||
}
|
||||
|
||||
fs::path buildDir = config.BuildDir();
|
||||
|
|
@ -780,7 +786,7 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_map<fs::path, s
|
|||
Progress::Task task(std::format("Compiling {}.c", cFile.filename().string()));
|
||||
if (buildCancelled.load(std::memory_order_relaxed)) return;
|
||||
|
||||
std::string result = RunBuildCommand(std::format("clang {0}.c --target={1}{2} -O3{3} -c{4}{5}{6} -MD -MF {7}_source.o.d -o {7}_source.o", cFile.string(), config.target, cArchFlags, ltoCompileFlags, includeFlags, defineFlags, userFlags, (buildDir / cFile.filename()).string()));
|
||||
std::string result = RunBuildCommand(std::format("{8} {0}.c --target={1}{2} -O3{3} -c{4}{5}{6} -MD -MF {7}_source.o.d -o {7}_source.o", cFile.string(), config.target, cArchFlags, ltoCompileFlags, includeFlags, defineFlags, userFlags, (buildDir / cFile.filename()).string(), GetToolchain().clang.string()));
|
||||
if (result.empty()) return;
|
||||
|
||||
bool expected = false;
|
||||
|
|
@ -1005,6 +1011,13 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_map<fs::path, s
|
|||
// link below ends with linkExtras, so this reaches them all; the static-lib
|
||||
// archive path uses ar instead and is handled separately.
|
||||
linkExtras += ltoLinkFlags;
|
||||
// Host links use the toolchain's own libc++. When the toolchain lives
|
||||
// outside /usr (a bundled one) the binary needs an rpath to find
|
||||
// libc++.so.1 at run time. mingw links libstdc++ and every sysroot build
|
||||
// (wasm included) brings its own libc++.
|
||||
if (config.target != "x86_64-w64-mingw32" && config.sysroot.empty()) {
|
||||
linkExtras += GetToolchain().LibcxxLinkFlags();
|
||||
}
|
||||
// mingw uses libstdc++; C++26 std::print/format extras live in libstdc++exp.
|
||||
// libstdc++ on mingw uses winpthreads for std::atomic_wait /
|
||||
// counting_semaphore / stop_token, so -lpthread is required as soon as
|
||||
|
|
@ -1142,7 +1155,7 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_map<fs::path, s
|
|||
// pull the needed members. llvm-ar writes a bitcode-aware index
|
||||
// and handles plain ELF and COFF objects just as well, so every
|
||||
// build uses it.
|
||||
buildResult.result = RunBuildCommand(std::format("llvm-ar rcs {}.a {}", (outputDir/fs::path(std::string("lib")+config.outputName)).string(), files));
|
||||
buildResult.result = RunBuildCommand(std::format("{} rcs {}.a {}", GetToolchain().llvmAr.string(), (outputDir/fs::path(std::string("lib")+config.outputName)).string(), files));
|
||||
} else {
|
||||
// LibraryDynamic. Output names follow each target's convention so
|
||||
// consumers can link via the standard linker search:
|
||||
|
|
@ -1420,14 +1433,32 @@ void Crafter::EnableWasiRelaxedSimdVariant(Configuration& cfg) {
|
|||
}
|
||||
|
||||
std::string Crafter::HostTarget() {
|
||||
static const std::string Cached = []() -> std::string {
|
||||
CommandResult r = RunCommandChecked("clang++ -print-target-triple");
|
||||
if (r.exitCode != 0) return {};
|
||||
std::string out = std::move(r.output);
|
||||
while (!out.empty() && (out.back() == '\n' || out.back() == '\r')) out.pop_back();
|
||||
return out;
|
||||
}();
|
||||
return Cached;
|
||||
return GetToolchain().hostTriple;
|
||||
}
|
||||
|
||||
std::string Crafter::NormalizeTarget(std::string_view triple) {
|
||||
// <arch>-<vendor>-<os>-<env>: drop the vendor. A three-part triple only
|
||||
// carries one when its middle part is a vendor name rather than an OS
|
||||
// (x86_64-unknown-wasip1 yes, x86_64-linux-gnu no).
|
||||
std::vector<std::string_view> parts;
|
||||
for (std::size_t begin = 0;;) {
|
||||
std::size_t dash = triple.find('-', begin);
|
||||
parts.push_back(dash == std::string_view::npos ? triple.substr(begin) : triple.substr(begin, dash - begin));
|
||||
if (dash == std::string_view::npos) break;
|
||||
begin = dash + 1;
|
||||
}
|
||||
const bool hasVendor = parts.size() == 4 || (parts.size() == 3 && (parts[1] == "unknown" || parts[1] == "pc"));
|
||||
if (hasVendor) parts.erase(parts.begin() + 1);
|
||||
std::string out;
|
||||
for (std::string_view part : parts) {
|
||||
if (!out.empty()) out += '-';
|
||||
out += part;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
bool Crafter::SameTarget(std::string_view a, std::string_view b) {
|
||||
return a == b || NormalizeTarget(a) == NormalizeTarget(b);
|
||||
}
|
||||
|
||||
bool Crafter::ArgQuery::Has(std::string_view flag) const {
|
||||
|
|
@ -1475,7 +1506,7 @@ ArgQuery Crafter::ApplyStandardArgs(Configuration& cfg, std::span<const std::str
|
|||
// ApplyStandardArgs, but doing it here makes dep PcmDirs consistent
|
||||
// between the consumer's command-construction and the dep's own build.)
|
||||
if (cfg.sysroot.empty() && cfg.target.starts_with("wasm32")) {
|
||||
cfg.sysroot = "/usr/share/wasi-sysroot";
|
||||
cfg.sysroot = GetToolchain().wasiSysroot.string();
|
||||
}
|
||||
return ArgQuery{args};
|
||||
}
|
||||
|
|
@ -1495,6 +1526,17 @@ std::vector<fs::path> Crafter::CleanProject(const fs::path& projectFile) {
|
|||
return removed;
|
||||
}
|
||||
|
||||
static void PrintVersion() {
|
||||
std::println("crafter-build {}", CrafterBuildVersion());
|
||||
const Toolchain& tc = GetToolchain();
|
||||
if (!tc.error.empty()) {
|
||||
std::println("compiler: {}", tc.error);
|
||||
return;
|
||||
}
|
||||
std::println("compiler {} ({})", tc.clangVersion, tc.clangxx.string());
|
||||
std::println("host {}", tc.hostTriple);
|
||||
}
|
||||
|
||||
static void PrintHelp(std::string_view argv0) {
|
||||
std::println(
|
||||
R"(Usage:
|
||||
|
|
@ -1504,6 +1546,7 @@ R"(Usage:
|
|||
{0} format [format-options] [globs...] Apply the project's transform rules (rewrites files)
|
||||
{0} clean Delete the project's bin/ and build/ trees
|
||||
{0} help | -h | --help Show this help
|
||||
{0} --version Print the version and the compiler in use
|
||||
|
||||
Loads ./project.cpp (override with --project=<path>), compiles it to a shared
|
||||
object, and invokes its CrafterBuildProject() to obtain a Configuration that
|
||||
|
|
@ -1571,7 +1614,14 @@ Environment:
|
|||
CRAFTER_BUILD_RUNNER_<TARGET> Default test runner for a target triple.
|
||||
Replace '-' and '.' with '_' in the
|
||||
triple. CLI --runner= overrides this.
|
||||
CRAFTER_MINGW_DIR Override mingw-w64 sysroot auto-detect.
|
||||
CRAFTER_BUILD_LLVM_DIR LLVM prefix to use instead of a toolchain
|
||||
installed beside crafter-build or PATH.
|
||||
CRAFTER_BUILD_LIBCXX_DIR libc++ install to compile and link against
|
||||
instead of the compiler's own.
|
||||
CRAFTER_BUILD_LIBCLANG Exact libclang to load for lint.
|
||||
CRAFTER_BUILD_WASI_SYSROOT WASI sysroot for wasm32 targets.
|
||||
CRAFTER_MINGW_DIR mingw-w64 sysroot, passed as --sysroot.
|
||||
CRAFTER_BUILD_HOME Directory holding the module sources.
|
||||
|
||||
Exit status:
|
||||
0 success / all non-skipped tests passed / lint clean / format applied
|
||||
|
|
@ -1599,7 +1649,13 @@ std::int32_t Crafter::Run(std::int32_t argc, char** argv) {
|
|||
|
||||
for (std::int32_t i = 1; i < argc; ++i) {
|
||||
std::string_view arg = argv[i];
|
||||
if (arg == "-h" || arg == "--help" || (!runTests && !runLint && !runFormat && arg == "help")) {
|
||||
if (arg == "--version") {
|
||||
// Answered here, before the project file is looked for, so
|
||||
// it works in any directory and is never forwarded to
|
||||
// project.cpp as an unknown argument.
|
||||
PrintVersion();
|
||||
return 0;
|
||||
} else if (arg == "-h" || arg == "--help" || (!runTests && !runLint && !runFormat && arg == "help")) {
|
||||
PrintHelp(argv0);
|
||||
return 0;
|
||||
} else if (!runLint && !runFormat && !runClean && arg == "test") {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
module Crafter.Build:External_impl;
|
||||
import std;
|
||||
import :External;
|
||||
import :Toolchain;
|
||||
import :Platform;
|
||||
import :Progress;
|
||||
import :Clang;
|
||||
|
|
@ -118,15 +119,16 @@ std::string BuildInjectedCMakeFlags(const fs::path& cmakeBuildDir, std::string_v
|
|||
// suffix to library names assuming a debug build, breaking -l<name>
|
||||
// linking. Pin to Release.
|
||||
out += " -DCMAKE_BUILD_TYPE=Release";
|
||||
out += " -DCMAKE_C_COMPILER=clang";
|
||||
out += " -DCMAKE_CXX_COMPILER=clang++";
|
||||
const Toolchain& tc = GetToolchain();
|
||||
out += std::format(" -DCMAKE_C_COMPILER={}", ShellQuote(tc.clang.string()));
|
||||
out += std::format(" -DCMAKE_CXX_COMPILER={}", ShellQuote(tc.clangxx.string()));
|
||||
// Stdlib choice: libc++ for Linux targets (matches how crafter-build
|
||||
// itself is built with -stdlib=libc++); the mingw cross-compile uses the
|
||||
// libstdc++ shipped by the mingw-w64 toolchain, so it keeps the default.
|
||||
if (target.ends_with("-linux-gnu") || target.ends_with("-linux-musl")) {
|
||||
out += " -DCMAKE_CXX_FLAGS=-stdlib=libc++";
|
||||
out += " -DCMAKE_EXE_LINKER_FLAGS=-stdlib=libc++";
|
||||
out += " -DCMAKE_SHARED_LINKER_FLAGS=-stdlib=libc++";
|
||||
out += std::format(" -DCMAKE_CXX_FLAGS={}", ShellQuote(std::format("-stdlib=libc++{}", tc.LibcxxCompileFlags())));
|
||||
out += std::format(" -DCMAKE_EXE_LINKER_FLAGS={}", ShellQuote(std::format("-stdlib=libc++{}", tc.LibcxxLinkFlags())));
|
||||
out += std::format(" -DCMAKE_SHARED_LINKER_FLAGS={}", ShellQuote(std::format("-stdlib=libc++{}", tc.LibcxxLinkFlags())));
|
||||
}
|
||||
if (target == "x86_64-w64-mingw32") {
|
||||
out += " -DCMAKE_SYSTEM_NAME=Windows";
|
||||
|
|
@ -142,6 +144,10 @@ std::string BuildInjectedCMakeFlags(const fs::path& cmakeBuildDir, std::string_v
|
|||
std::string ConfigureCMake(const fs::path& cloneDir, const fs::path& cmakeBuildDir, std::span<const std::string> options, std::string_view target) {
|
||||
fs::path optionsFile = cmakeBuildDir / ".crafter-options";
|
||||
std::string optionsKey = JoinOptions(options);
|
||||
// The injected flags are inputs too: a different compiler or libc++
|
||||
// must reconfigure rather than reuse a CMakeCache made with the old one.
|
||||
const std::string injected = BuildInjectedCMakeFlags(cmakeBuildDir, target);
|
||||
const std::string configureKey = injected + '\n' + optionsKey;
|
||||
|
||||
bool needsConfigure = !fs::exists(cmakeBuildDir / "CMakeCache.txt");
|
||||
if (!needsConfigure) {
|
||||
|
|
@ -151,7 +157,7 @@ std::string ConfigureCMake(const fs::path& cloneDir, const fs::path& cmakeBuildD
|
|||
} else {
|
||||
std::stringstream buf;
|
||||
buf << existing.rdbuf();
|
||||
if (buf.str() != optionsKey) needsConfigure = true;
|
||||
if (buf.str() != configureKey) needsConfigure = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -159,7 +165,7 @@ std::string ConfigureCMake(const fs::path& cloneDir, const fs::path& cmakeBuildD
|
|||
|
||||
if (!fs::exists(cmakeBuildDir)) fs::create_directories(cmakeBuildDir);
|
||||
|
||||
std::string cmd = std::format("cmake -S {} -B {}{}", ShellQuote(fs::absolute(cloneDir).string()), ShellQuote(fs::absolute(cmakeBuildDir).string()), BuildInjectedCMakeFlags(cmakeBuildDir, target));
|
||||
std::string cmd = std::format("cmake -S {} -B {}{}", ShellQuote(fs::absolute(cloneDir).string()), ShellQuote(fs::absolute(cmakeBuildDir).string()), injected);
|
||||
if (!options.empty()) {
|
||||
cmd += " ";
|
||||
cmd += optionsKey;
|
||||
|
|
@ -170,7 +176,7 @@ std::string ConfigureCMake(const fs::path& cloneDir, const fs::path& cmakeBuildD
|
|||
return std::format("cmake configure failed (exit {}): {}", r.exitCode, r.output);
|
||||
}
|
||||
|
||||
std::ofstream(optionsFile) << optionsKey;
|
||||
std::ofstream(optionsFile) << configureKey;
|
||||
return "";
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import std;
|
|||
import :Lint;
|
||||
import :Clang;
|
||||
import :Platform;
|
||||
import :Toolchain;
|
||||
import :Progress;
|
||||
namespace fs = std::filesystem;
|
||||
using namespace Crafter;
|
||||
|
|
@ -92,14 +93,14 @@ namespace {
|
|||
LibClang LoadLibClang() {
|
||||
LibClang lib;
|
||||
std::vector<std::string> tried;
|
||||
// CRAFTER_BUILD_LIBCLANG pins an exact path, mirroring the
|
||||
// CRAFTER_MINGW_DIR override used elsewhere. It is exclusive: pointing
|
||||
// it at a broken path must fail loudly rather than quietly succeed with
|
||||
// some other libclang, or the override is useless for diagnosing which
|
||||
// library is actually in play.
|
||||
// The toolchain pins libclang when it knows where it is: the
|
||||
// CRAFTER_BUILD_LIBCLANG override, or the libclang.so beside the
|
||||
// compiler. A pin is exclusive: pointing the override at a broken
|
||||
// path must fail loudly rather than quietly succeed with some other
|
||||
// libclang, or it is useless for diagnosing which library is in play.
|
||||
std::vector<std::string> candidates;
|
||||
if (const char* pinned = std::getenv("CRAFTER_BUILD_LIBCLANG"); pinned && *pinned) {
|
||||
candidates.emplace_back(pinned);
|
||||
if (const std::optional<fs::path>& pinned = GetToolchain().libclang) {
|
||||
candidates.emplace_back(pinned->string());
|
||||
} else {
|
||||
for (std::string_view name : LibClangNames) candidates.emplace_back(name);
|
||||
}
|
||||
|
|
@ -119,7 +120,7 @@ namespace {
|
|||
tried.push_back(name);
|
||||
}
|
||||
if (!lib.handle) {
|
||||
lib.error = std::format("could not load libclang (tried {}); install clang, or point CRAFTER_BUILD_LIBCLANG at it", join(tried));
|
||||
lib.error = std::format("could not load libclang (tried {}); install clang, set CRAFTER_BUILD_LLVM_DIR, or point CRAFTER_BUILD_LIBCLANG at it", join(tried));
|
||||
return lib;
|
||||
}
|
||||
|
||||
|
|
@ -710,7 +711,7 @@ namespace {
|
|||
// explicitly. Cached like HostTarget, and for the same reason.
|
||||
const std::string& ClangResourceDir() {
|
||||
static const std::string Cached = []() -> std::string {
|
||||
CommandResult r = RunCommandChecked("clang++ -print-resource-dir");
|
||||
CommandResult r = RunCommandChecked(std::format("{} -print-resource-dir", GetToolchain().clangxx.string()));
|
||||
if (r.exitCode != 0) return {};
|
||||
std::string out = std::move(r.output);
|
||||
while (!out.empty() && (out.back() == '\n' || out.back() == '\r')) out.pop_back();
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ module;
|
|||
module Crafter.Build:Platform_impl;
|
||||
import std;
|
||||
import :Platform;
|
||||
import :Toolchain;
|
||||
import :Clang;
|
||||
import :Interface;
|
||||
import :External;
|
||||
|
|
@ -595,7 +596,7 @@ std::string Crafter::BuildStdPcm(const Configuration& config, fs::path stdPcm) {
|
|||
std::vector<std::string> folders;
|
||||
|
||||
// Iterate through the directory and collect all subdirectories
|
||||
for (const auto& entry : fs::directory_iterator("/usr/x86_64-w64-mingw32/include/c++")) {
|
||||
for (const auto& entry : fs::directory_iterator(GetToolchain().mingwSysroot / "include" / "c++")) {
|
||||
if (entry.is_directory()) {
|
||||
folders.push_back(entry.path().filename().string());
|
||||
}
|
||||
|
|
@ -608,7 +609,7 @@ std::string Crafter::BuildStdPcm(const Configuration& config, fs::path stdPcm) {
|
|||
|
||||
std::string mingWversion = folders.front();
|
||||
|
||||
fs::path stdCc = fs::path(std::format("/usr/x86_64-w64-mingw32/include/c++/{}/bits/std.cc", mingWversion));
|
||||
fs::path stdCc = GetToolchain().mingwSysroot / "include" / "c++" / mingWversion / "bits" / "std.cc";
|
||||
|
||||
if(!fs::exists(stdPcm) || fs::last_write_time(stdPcm) < fs::last_write_time(stdCc)) {
|
||||
// -femulated-tls keeps PCM TLS access matching libstdc++'s emutls
|
||||
|
|
@ -621,7 +622,7 @@ std::string Crafter::BuildStdPcm(const Configuration& config, fs::path stdPcm) {
|
|||
if (copyEc) {
|
||||
return std::format("copy {} -> {}: {}", stdCc.string(), stdCppm.string(), copyEc.message());
|
||||
}
|
||||
return RunBuildCommand(std::format("clang++ --target={} -march={} -mtune={} -femulated-tls -O3 -std=c++26 -Wno-reserved-identifier -Wno-reserved-module-identifier --precompile {} -o {}", config.target, config.march, config.mtune, stdCppm.string(), stdPcm.string()));
|
||||
return RunBuildCommand(std::format("{} --target={} -march={} -mtune={} -femulated-tls -O3 -std=c++26 -Wno-reserved-identifier -Wno-reserved-module-identifier --precompile {} -o {}", GetBaseCommand(config), config.target, config.march, config.mtune, stdCppm.string(), stdPcm.string()));
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
|
|
@ -634,15 +635,15 @@ std::string Crafter::BuildStdPcm(const Configuration& config, fs::path stdPcm) {
|
|||
if (isWasm) {
|
||||
stdCppm = std::format("{}/share/libc++/v1/std.cppm", config.sysroot);
|
||||
} else if (config.sysroot.empty()) {
|
||||
stdCppm = "/usr/share/libc++/v1/std.cppm";
|
||||
stdCppm = GetToolchain().stdCppm.string();
|
||||
} else {
|
||||
stdCppm = std::format("{}/usr/share/libc++/v1/std.cppm", config.sysroot);
|
||||
// not every distro ships the std module sources (Alpine's
|
||||
// libc++-dev doesn't); the host copy is interchangeable as long
|
||||
// as the libc++ versions line up, which the compile of the PCM
|
||||
// against the sysroot's headers verifies anyway
|
||||
if (!fs::exists(stdCppm) && fs::exists("/usr/share/libc++/v1/std.cppm")) {
|
||||
stdCppm = "/usr/share/libc++/v1/std.cppm";
|
||||
if (!fs::exists(stdCppm) && fs::exists(GetToolchain().stdCppm)) {
|
||||
stdCppm = GetToolchain().stdCppm.string();
|
||||
}
|
||||
}
|
||||
std::string sysrootFlag = config.sysroot.empty()
|
||||
|
|
@ -669,7 +670,7 @@ std::string Crafter::BuildStdPcm(const Configuration& config, fs::path stdPcm) {
|
|||
archFlags += std::format(" -nostdinc++ -isystem {}/usr/include/c++/v1", config.sysroot);
|
||||
}
|
||||
if(!fs::exists(stdPcm) || fs::last_write_time(stdPcm) < fs::last_write_time(stdCppm)) {
|
||||
return RunBuildCommand(std::format("clang++ --target={} -std=c++26 -stdlib=libc++{}{} -O3 -Wno-reserved-identifier -Wno-reserved-module-identifier --precompile {} -o {}", config.target, sysrootFlag, archFlags, stdCppm, stdPcm.string()));
|
||||
return RunBuildCommand(std::format("{} --target={} -std=c++26{}{} -O3 -Wno-reserved-identifier -Wno-reserved-module-identifier --precompile {} -o {}", GetBaseCommand(config), config.target, sysrootFlag, archFlags, stdCppm, stdPcm.string()));
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
|
|
@ -689,17 +690,24 @@ fs::path Crafter::GetCacheDir() {
|
|||
}
|
||||
|
||||
std::string Crafter::GetBaseCommand(const Configuration& config) {
|
||||
std::string stdlib;
|
||||
if(config.target == "x86_64-w64-mingw32") {
|
||||
stdlib = "";
|
||||
} else {
|
||||
stdlib = "-stdlib=libc++";
|
||||
const Toolchain& tc = GetToolchain();
|
||||
std::string command = tc.clangxx.string();
|
||||
if (config.target == "x86_64-w64-mingw32") {
|
||||
// libstdc++ from the mingw toolchain, found by clang's own mingw
|
||||
// discovery unless the sysroot was given explicitly.
|
||||
if (tc.mingwSysrootExplicit) command += std::format(" --sysroot={}", tc.mingwSysroot.string());
|
||||
return command;
|
||||
}
|
||||
return std::format("clang++ {}", stdlib);
|
||||
command += " -stdlib=libc++";
|
||||
// A sysroot build (wasm, a cross rootfs) takes libc++ from the sysroot;
|
||||
// only host builds can be pointed at an explicit libc++ install.
|
||||
if (config.sysroot.empty()) command += tc.LibcxxCompileFlags();
|
||||
return command;
|
||||
}
|
||||
|
||||
namespace {
|
||||
constexpr std::array<std::string_view, 11> CrafterBuildModules = {
|
||||
constexpr std::array<std::string_view, 12> CrafterBuildModules = {
|
||||
"Crafter.Build-Toolchain",
|
||||
"Crafter.Build-Shader",
|
||||
"Crafter.Build-Platform",
|
||||
"Crafter.Build-Interface",
|
||||
|
|
@ -713,7 +721,7 @@ namespace {
|
|||
"Crafter.Build",
|
||||
};
|
||||
|
||||
void EnsureCrafterBuildPcms(const fs::path& sourceDir, const fs::path& cacheDir) {
|
||||
void EnsureCrafterBuildPcms(const fs::path& sourceDir, const fs::path& cacheDir, const Configuration& host) {
|
||||
CacheLock lock(cacheDir);
|
||||
// Re-derived under the lock: another builder may have refreshed the
|
||||
// cache from its own sources while we waited.
|
||||
|
|
@ -729,12 +737,12 @@ namespace {
|
|||
continue;
|
||||
}
|
||||
std::string cmd = std::format(
|
||||
"clang++ --target={} -march=native -mtune=native "
|
||||
"-std=c++26 -stdlib=libc++ -O3 "
|
||||
"{} --target={} -march=native -mtune=native "
|
||||
"-std=c++26 -O3 "
|
||||
"-Wno-reserved-identifier -Wno-reserved-module-identifier "
|
||||
"-fprebuilt-module-path={} "
|
||||
"--precompile {} -o {}",
|
||||
HostTarget(), cacheDir.string(), cppmPath.string(), pcmPath.string());
|
||||
GetBaseCommand(host), host.target, cacheDir.string(), cppmPath.string(), pcmPath.string());
|
||||
CommandResult r = Crafter::RunCommandChecked(cmd);
|
||||
if (r.exitCode != 0) {
|
||||
throw std::runtime_error(std::format("Failed to precompile {} (exit {}): {}", name, r.exitCode, r.output));
|
||||
|
|
@ -748,6 +756,7 @@ namespace {
|
|||
}
|
||||
|
||||
Configuration Crafter::LoadProject(const fs::path& projectFile, std::span<const std::string_view> args) {
|
||||
RequireToolchain();
|
||||
fs::path absProject = fs::canonical(projectFile);
|
||||
ProjectFileGuard guard(absProject);
|
||||
fs::path buildDir = absProject.parent_path() / "build";
|
||||
|
|
@ -775,7 +784,7 @@ Configuration Crafter::LoadProject(const fs::path& projectFile, std::span<const
|
|||
throw std::runtime_error(std::format("Failed to build std.pcm: {}", stdResult));
|
||||
}
|
||||
|
||||
EnsureCrafterBuildPcms(sourceDir, cacheDir);
|
||||
EnsureCrafterBuildPcms(sourceDir, cacheDir, hostConfig);
|
||||
|
||||
// After the host PCMs exist (a dependency's build imports Crafter.Build
|
||||
// from that cache) and before the staleness check (its artifacts are
|
||||
|
|
@ -792,13 +801,13 @@ Configuration Crafter::LoadProject(const fs::path& projectFile, std::span<const
|
|||
// and a dependency archive built in release holds ThinLTO bitcode,
|
||||
// which GNU ld can't read.
|
||||
std::string compileCmd = std::format(
|
||||
"clang++ --target={} -march=native -mtune=native "
|
||||
"-std=c++26 -stdlib=libc++ -shared -fPIC -O3 -fuse-ld=lld "
|
||||
"{} --target={} -march=native -mtune=native "
|
||||
"-std=c++26 -shared -fPIC -O3 -fuse-ld=lld "
|
||||
"-Wno-return-type-c-linkage "
|
||||
"-fprebuilt-module-path={}{} "
|
||||
"{} -o {}{}",
|
||||
hostConfig.target, cacheDir.string(), preDeps.compileFlags,
|
||||
absProject.string(), soPath.string(), preDeps.linkFlags);
|
||||
"{} -o {}{}{}",
|
||||
GetBaseCommand(hostConfig), hostConfig.target, cacheDir.string(), preDeps.compileFlags,
|
||||
absProject.string(), soPath.string(), preDeps.linkFlags, GetToolchain().LibcxxLinkFlags());
|
||||
|
||||
std::string result = RunBuildCommand(compileCmd);
|
||||
if (!result.empty()) {
|
||||
|
|
|
|||
|
|
@ -181,7 +181,7 @@ TestRunner TestRunner::ForTarget(const Configuration& cfg) {
|
|||
|
||||
// Same triple as the host → run the binary directly. Covers the common
|
||||
// case (cfg.target defaulted to HostTarget()) without any wrapper.
|
||||
if (target == HostTarget()) return Local();
|
||||
if (SameTarget(target, HostTarget())) return Local();
|
||||
|
||||
// Windows targets run under Wine; there is no Windows host.
|
||||
if (TargetIsWindows(target)) return Wine();
|
||||
|
|
@ -466,13 +466,16 @@ TestSummary Crafter::RunTests(Configuration& projectCfg, const RunTestsOptions&
|
|||
// user having to know which targets exist. An explicit --target=X
|
||||
// bypasses the sweep and runs that target only.
|
||||
if (opts.targetFilter.empty()) {
|
||||
std::set<std::string> sweep;
|
||||
sweep.insert(HostTarget());
|
||||
// Keyed by the normalized triple so x86_64-pc-linux-gnu and
|
||||
// x86_64-unknown-linux-gnu make one pass; the first spelling seen is
|
||||
// the one shown and filtered on.
|
||||
std::map<std::string, std::string> sweep;
|
||||
sweep.emplace(NormalizeTarget(HostTarget()), HostTarget());
|
||||
for (const Test& t : projectCfg.tests) {
|
||||
if (!t.config.target.empty()) sweep.insert(t.config.target);
|
||||
if (!t.config.target.empty()) sweep.emplace(NormalizeTarget(t.config.target), t.config.target);
|
||||
}
|
||||
TestSummary aggregate;
|
||||
for (const auto& target : sweep) {
|
||||
for (const auto& [normalized, target] : sweep) {
|
||||
RunTestsOptions perTarget = opts;
|
||||
perTarget.targetFilter = target;
|
||||
if (sweep.size() > 1) {
|
||||
|
|
@ -499,7 +502,7 @@ TestSummary Crafter::RunTests(Configuration& projectCfg, const RunTestsOptions&
|
|||
std::vector<Test*> filtered;
|
||||
filtered.reserve(projectCfg.tests.size());
|
||||
for (auto& test : projectCfg.tests) {
|
||||
if (test.config.target != opts.targetFilter) continue;
|
||||
if (!SameTarget(test.config.target, opts.targetFilter)) continue;
|
||||
if (!MatchAny(opts.globs, test.config.name)) continue;
|
||||
test.runner = TestRunner::FromEnv(test.config.target, TestRunner::ForTarget(test.config));
|
||||
if (opts.runnerOverride) {
|
||||
|
|
|
|||
182
implementations/Crafter.Build-Toolchain.cpp
Normal file
182
implementations/Crafter.Build-Toolchain.cpp
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||
|
||||
module;
|
||||
// Supplied on the command line by build.sh and project.cpp (from the
|
||||
// CRAFTER_BUILD_VERSION environment variable or `git describe`); a build
|
||||
// that passes nothing is a development build.
|
||||
#ifndef CRAFTER_BUILD_VERSION
|
||||
#define CRAFTER_BUILD_VERSION "0.0.0-dev"
|
||||
#endif
|
||||
module Crafter.Build:Toolchain_impl;
|
||||
import std;
|
||||
import :Toolchain;
|
||||
import :Platform;
|
||||
namespace fs = std::filesystem;
|
||||
using namespace Crafter;
|
||||
|
||||
namespace {
|
||||
constexpr std::string_view EnvVars[] = {
|
||||
"CRAFTER_BUILD_LLVM_DIR",
|
||||
"CRAFTER_BUILD_LIBCXX_DIR",
|
||||
"CRAFTER_BUILD_LIBCLANG",
|
||||
"CRAFTER_BUILD_WASI_SYSROOT",
|
||||
"CRAFTER_MINGW_DIR",
|
||||
};
|
||||
|
||||
std::string_view Env(const ToolchainInputs& in, std::string_view name) {
|
||||
auto it = in.env.find(name);
|
||||
return it == in.env.end() ? std::string_view{} : std::string_view(it->second);
|
||||
}
|
||||
|
||||
// "Arch Linux clang version 22.1.8" / "clang version 22.1.8" -> "22.1.8".
|
||||
std::string ShortVersion(std::string_view versionLine) {
|
||||
constexpr std::string_view Marker = "version ";
|
||||
std::size_t at = versionLine.find(Marker);
|
||||
if (at == std::string_view::npos) return "unknown";
|
||||
std::string_view rest = versionLine.substr(at + Marker.size());
|
||||
std::size_t end = rest.find_first_of(" \t(");
|
||||
return std::string(rest.substr(0, end));
|
||||
}
|
||||
|
||||
std::string Trimmed(std::string s) {
|
||||
while (!s.empty() && (s.back() == '\n' || s.back() == '\r' || s.back() == ' ')) s.pop_back();
|
||||
return s;
|
||||
}
|
||||
}
|
||||
|
||||
std::string Toolchain::LibcxxCompileFlags() const {
|
||||
if (libcxxDir.empty()) return {};
|
||||
return std::format(" -nostdinc++ -isystem {}", (libcxxDir / "include" / "c++" / "v1").string());
|
||||
}
|
||||
|
||||
std::string Toolchain::LibcxxLinkFlags() const {
|
||||
std::string flags;
|
||||
if (!libcxxDir.empty()) {
|
||||
fs::path lib = libcxxDir / "lib";
|
||||
flags += std::format(" -L{} -Wl,-rpath,{}", lib.string(), lib.string());
|
||||
} else if (!prefix.empty() && prefix != "/usr") {
|
||||
flags += std::format(" -Wl,-rpath,{}", (prefix / "lib").string());
|
||||
}
|
||||
return flags;
|
||||
}
|
||||
|
||||
std::string Toolchain::Key() const {
|
||||
std::string identity;
|
||||
for (const std::string& part : {clangxx.string(), clangVersion, libcxxDir.string(), stdCppm.string()}) {
|
||||
identity += part;
|
||||
identity += '\0';
|
||||
}
|
||||
return std::format("{}-{:08x}", ShortVersion(clangVersion), static_cast<std::uint32_t>(std::hash<std::string>{}(identity)));
|
||||
}
|
||||
|
||||
Toolchain Crafter::ResolveToolchain(const ToolchainInputs& in) {
|
||||
Toolchain tc;
|
||||
|
||||
// 1. The compiler: an explicit prefix, else one installed beside
|
||||
// crafter-build, else PATH. An explicit prefix without a clang++ is an
|
||||
// error rather than a fallback, for the same reason CRAFTER_BUILD_LIBCLANG
|
||||
// is exclusive: an override that silently yields to something else
|
||||
// cannot be used to diagnose which compiler is in play.
|
||||
if (std::string_view dir = Env(in, "CRAFTER_BUILD_LLVM_DIR"); !dir.empty()) {
|
||||
tc.prefix = fs::path(dir);
|
||||
if (!fs::exists(tc.prefix / "bin" / "clang++")) {
|
||||
tc.error = std::format("clang++ not found at {} (CRAFTER_BUILD_LLVM_DIR)", (tc.prefix / "bin" / "clang++").string());
|
||||
}
|
||||
} else if (!in.crafterPrefix.empty() && fs::exists(in.crafterPrefix / "bin" / "clang++")) {
|
||||
tc.prefix = in.crafterPrefix;
|
||||
}
|
||||
if (tc.prefix.empty()) {
|
||||
tc.clangxx = "clang++";
|
||||
tc.clang = "clang";
|
||||
tc.llvmAr = "llvm-ar";
|
||||
} else {
|
||||
tc.clangxx = tc.prefix / "bin" / "clang++";
|
||||
tc.clang = tc.prefix / "bin" / "clang";
|
||||
tc.llvmAr = tc.prefix / "bin" / "llvm-ar";
|
||||
if (tc.prefix != "/usr" && fs::exists(tc.prefix / "include" / "clang-c" / "Index.h")) {
|
||||
tc.llvmIncludeDir = tc.prefix / "include";
|
||||
}
|
||||
}
|
||||
|
||||
// 2. libclang. The override is exclusive and unchecked on purpose: a
|
||||
// broken path must fail loudly when dlopened, not be papered over.
|
||||
if (std::string_view pinned = Env(in, "CRAFTER_BUILD_LIBCLANG"); !pinned.empty()) {
|
||||
tc.libclang = fs::path(pinned);
|
||||
} else if (!tc.prefix.empty() && fs::exists(tc.prefix / "lib" / "libclang.so")) {
|
||||
tc.libclang = tc.prefix / "lib" / "libclang.so";
|
||||
}
|
||||
|
||||
// 3. libc++ and its std module source. The compiler's own copy lives at
|
||||
// <prefix>/share/libc++/v1 when the prefix carries libc++; the system
|
||||
// copy is the last resort either way.
|
||||
const fs::path systemStdCppm = "/usr/share/libc++/v1/std.cppm";
|
||||
if (std::string_view dir = Env(in, "CRAFTER_BUILD_LIBCXX_DIR"); !dir.empty()) {
|
||||
tc.libcxxDir = fs::path(dir);
|
||||
tc.stdCppm = tc.libcxxDir / "share" / "libc++" / "v1" / "std.cppm";
|
||||
} else if (!tc.prefix.empty() && fs::exists(tc.prefix / "share" / "libc++" / "v1" / "std.cppm")) {
|
||||
tc.stdCppm = tc.prefix / "share" / "libc++" / "v1" / "std.cppm";
|
||||
} else {
|
||||
tc.stdCppm = systemStdCppm;
|
||||
}
|
||||
|
||||
// 4. Sysroots for the cross targets that have a conventional location.
|
||||
if (std::string_view dir = Env(in, "CRAFTER_BUILD_WASI_SYSROOT"); !dir.empty()) {
|
||||
tc.wasiSysroot = fs::path(dir);
|
||||
} else if (!tc.prefix.empty() && fs::exists(tc.prefix / "share" / "wasi-sysroot")) {
|
||||
tc.wasiSysroot = tc.prefix / "share" / "wasi-sysroot";
|
||||
} else {
|
||||
tc.wasiSysroot = "/usr/share/wasi-sysroot";
|
||||
}
|
||||
if (std::string_view dir = Env(in, "CRAFTER_MINGW_DIR"); !dir.empty()) {
|
||||
tc.mingwSysroot = fs::path(dir);
|
||||
tc.mingwSysrootExplicit = true;
|
||||
} else {
|
||||
tc.mingwSysroot = "/usr/x86_64-w64-mingw32";
|
||||
}
|
||||
return tc;
|
||||
}
|
||||
|
||||
const Toolchain& Crafter::GetToolchain() {
|
||||
static const Toolchain Resolved = []() {
|
||||
ToolchainInputs in;
|
||||
for (std::string_view name : EnvVars) {
|
||||
if (const char* value = std::getenv(std::string(name).c_str()); value && *value) {
|
||||
in.env.emplace(std::string(name), value);
|
||||
}
|
||||
}
|
||||
// GetCrafterBuildHome throws when no share/crafter-build is found
|
||||
// near the executable; a toolchain can still come from PATH then.
|
||||
try {
|
||||
in.crafterPrefix = GetCrafterBuildHome().parent_path().parent_path();
|
||||
} catch (const std::exception&) {}
|
||||
|
||||
Toolchain tc = ResolveToolchain(in);
|
||||
if (!tc.error.empty()) return tc;
|
||||
|
||||
CommandResult triple = RunCommandChecked(std::format("{} -print-target-triple", tc.clangxx.string()));
|
||||
if (triple.exitCode != 0) {
|
||||
tc.error = std::format("could not run {} (exit {}): {}", tc.clangxx.string(), triple.exitCode, Trimmed(triple.output));
|
||||
if (tc.prefix.empty()) tc.error += "; install clang or set CRAFTER_BUILD_LLVM_DIR";
|
||||
return tc;
|
||||
}
|
||||
tc.hostTriple = Trimmed(std::move(triple.output));
|
||||
CommandResult version = RunCommandChecked(std::format("{} --version", tc.clangxx.string()));
|
||||
if (version.exitCode == 0) {
|
||||
std::string first = version.output.substr(0, version.output.find('\n'));
|
||||
tc.clangVersion = Trimmed(std::move(first));
|
||||
}
|
||||
return tc;
|
||||
}();
|
||||
return Resolved;
|
||||
}
|
||||
|
||||
const Toolchain& Crafter::RequireToolchain() {
|
||||
const Toolchain& tc = GetToolchain();
|
||||
if (!tc.error.empty()) throw std::runtime_error(tc.error);
|
||||
return tc;
|
||||
}
|
||||
|
||||
std::string_view Crafter::CrafterBuildVersion() {
|
||||
return CRAFTER_BUILD_VERSION;
|
||||
}
|
||||
|
|
@ -355,12 +355,21 @@ export namespace Crafter {
|
|||
bool needsAst = false;
|
||||
};
|
||||
|
||||
// The host target triple, detected once per process by running
|
||||
// `clang++ -print-target-triple` and cached. Used as the default for
|
||||
// The host target triple: what the resolved toolchain's clang++ prints
|
||||
// for -print-target-triple (see GetToolchain). Used as the default for
|
||||
// Configuration::target so projects don't have to hardcode it for the
|
||||
// no-cross-compile case. Returns "" if clang isn't on PATH or doesn't
|
||||
// print a triple — those projects still need an explicit cfg.target.
|
||||
// no-cross-compile case. Returns "" when no compiler could be resolved;
|
||||
// those projects still need an explicit cfg.target, and the first build
|
||||
// step reports the missing compiler.
|
||||
std::string HostTarget();
|
||||
// Target equality that ignores the vendor field: x86_64-pc-linux-gnu,
|
||||
// x86_64-unknown-linux-gnu and x86_64-linux-gnu are one target as far as
|
||||
// "does this test run on this host" goes, and which spelling a clang
|
||||
// reports depends on how it was built. The strings themselves are never
|
||||
// rewritten: clang rejects a PCM whose triple string differs from the
|
||||
// importing unit's, so what the user wrote is what the compiler sees.
|
||||
std::string NormalizeTarget(std::string_view triple);
|
||||
bool SameTarget(std::string_view a, std::string_view b);
|
||||
|
||||
struct Configuration {
|
||||
fs::path path;
|
||||
|
|
|
|||
78
interfaces/Crafter.Build-Toolchain.cppm
Normal file
78
interfaces/Crafter.Build-Toolchain.cppm
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||
|
||||
export module Crafter.Build:Toolchain;
|
||||
import std;
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
export namespace Crafter {
|
||||
// The compiler crafter-build drives and what sits next to it, resolved
|
||||
// once per process by GetToolchain(). Every clang++, clang and llvm-ar
|
||||
// the tool spawns, the libclang the linter dlopens, the std module
|
||||
// source and the default sysroots come from here, so a toolchain
|
||||
// installed anywhere other than /usr (the Flatpak carries its own under
|
||||
// /app) needs no per-call-site knowledge.
|
||||
struct Toolchain {
|
||||
// LLVM install prefix. Empty when the tools are bare names on PATH.
|
||||
fs::path prefix;
|
||||
fs::path clangxx;
|
||||
fs::path clang;
|
||||
fs::path llvmAr;
|
||||
// Where clang-c/Index.h lives when it is not on the default include
|
||||
// path; empty otherwise.
|
||||
fs::path llvmIncludeDir;
|
||||
// The exact libclang to dlopen; nullopt means search the usual names.
|
||||
std::optional<fs::path> libclang;
|
||||
// A libc++ install other than the compiler's own
|
||||
// (CRAFTER_BUILD_LIBCXX_DIR); empty means clang finds its own.
|
||||
fs::path libcxxDir;
|
||||
// libc++'s std module source for host builds.
|
||||
fs::path stdCppm;
|
||||
fs::path wasiSysroot;
|
||||
// Holds include/c++/<ver>/bits/std.cc for the mingw target. Passed to
|
||||
// clang as --sysroot only when it was set explicitly: clang's own
|
||||
// mingw discovery handles the distro layout, and a --sysroot pointing
|
||||
// at the triple directory would lose crtbegin.o and libgcc.
|
||||
fs::path mingwSysroot;
|
||||
bool mingwSysrootExplicit = false;
|
||||
// `clang++ -print-target-triple` and the first line of
|
||||
// `clang++ --version`; both empty when clang++ could not be run.
|
||||
std::string hostTriple;
|
||||
std::string clangVersion;
|
||||
// Why clang++ could not be resolved or run; empty when it could.
|
||||
std::string error;
|
||||
|
||||
// Flags for compiling against libcxxDir; empty without one.
|
||||
std::string LibcxxCompileFlags() const;
|
||||
// Flags for linking against the libc++ the compiler will use: an
|
||||
// rpath into the prefix when that is not the system one (a bundled
|
||||
// toolchain's libc++.so.1 is not on the loader's default path), plus
|
||||
// -L and an rpath for an explicit libcxxDir.
|
||||
std::string LibcxxLinkFlags() const;
|
||||
// Names this toolchain for cache directory names: the clang version
|
||||
// followed by a hash of what decides which headers and libraries a
|
||||
// PCM is built against. Two toolchains with different keys never
|
||||
// share a PCM.
|
||||
std::string Key() const;
|
||||
};
|
||||
|
||||
// What ResolveToolchain reads, so a test can feed it a staged layout.
|
||||
struct ToolchainInputs {
|
||||
// The CRAFTER_* variables, present only when set and non-empty.
|
||||
std::map<std::string, std::string, std::less<>> env;
|
||||
// The <prefix> of <prefix>/share/crafter-build; empty when unknown.
|
||||
fs::path crafterPrefix;
|
||||
};
|
||||
// Pure path resolution: an env override wins, then a toolchain installed
|
||||
// beside crafter-build (<crafterPrefix>/bin/clang++), then bare names on
|
||||
// PATH. Never runs a process, so hostTriple and clangVersion stay empty.
|
||||
Toolchain ResolveToolchain(const ToolchainInputs& inputs);
|
||||
// ResolveToolchain over the real environment plus the two clang probes,
|
||||
// computed once and cached. Never throws.
|
||||
const Toolchain& GetToolchain();
|
||||
// GetToolchain(), or a std::runtime_error carrying its error.
|
||||
const Toolchain& RequireToolchain();
|
||||
// The version baked in when crafter-build was built; "0.0.0-dev" for a
|
||||
// build that was given none.
|
||||
std::string_view CrafterBuildVersion();
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@
|
|||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||
|
||||
export module Crafter.Build;
|
||||
export import :Toolchain;
|
||||
export import :Clang;
|
||||
export import :Platform;
|
||||
export import :Implementation;
|
||||
|
|
|
|||
38
project.cpp
38
project.cpp
|
|
@ -30,21 +30,30 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
|
|||
|
||||
CrafterBuildLib->dependencies = { math, asset };
|
||||
CrafterBuildLib->defines.push_back({"CRAFTER_BUILD_HAS_ASSET", ""});
|
||||
// Crafter.Build-Lint includes libclang's clang-c/Index.h. glibc distros
|
||||
// keep it in /usr/include; Alpine keeps LLVM under /usr/lib/llvmNN, so ask
|
||||
// llvm-config and add the dir when it is not already on the default path
|
||||
// (never /usr/include itself: that breaks libc++'s #include_next).
|
||||
if (CrafterBuildLib->target.ends_with("-linux-gnu") || CrafterBuildLib->target.ends_with("-linux-musl")) {
|
||||
auto llvmInc = RunCommandWithTimeout("llvm-config --includedir 2>/dev/null", std::chrono::seconds(10));
|
||||
std::string incDir = llvmInc.output;
|
||||
while (!incDir.empty() && (incDir.back() == '\n' || incDir.back() == '\r')) incDir.pop_back();
|
||||
if (llvmInc.exitCode == 0 && !incDir.empty() && incDir != "/usr/include") {
|
||||
CrafterBuildLib->compileFlags.push_back(std::format("-I{}", incDir));
|
||||
}
|
||||
// Crafter.Build-Lint includes libclang's clang-c/Index.h, which is only
|
||||
// on the default include path for a toolchain installed in /usr.
|
||||
if (const fs::path& llvmInclude = GetToolchain().llvmIncludeDir; !llvmInclude.empty()) {
|
||||
CrafterBuildLib->compileFlags.push_back(std::format("-I{}", llvmInclude.string()));
|
||||
}
|
||||
// The version the binary reports: CI sets it from the tag, a checkout
|
||||
// describes itself, anything else is a development build. A define on
|
||||
// the library, so each version is its own library variant.
|
||||
std::string version;
|
||||
if (const char* env = std::getenv("CRAFTER_BUILD_VERSION"); env && *env) {
|
||||
version = env;
|
||||
} else {
|
||||
auto described = RunCommandWithTimeout("git describe --tags --always --dirty --exclude=latest 2>/dev/null", std::chrono::seconds(10));
|
||||
version = described.output;
|
||||
while (!version.empty() && (version.back() == '\n' || version.back() == '\r')) version.pop_back();
|
||||
if (described.exitCode != 0 || version.empty()) version = "0.0.0-dev";
|
||||
}
|
||||
// Backslash-escaped quotes: the define travels through the shell before
|
||||
// it reaches clang, which must still see a string literal.
|
||||
CrafterBuildLib->defines.push_back({"CRAFTER_BUILD_VERSION", std::format("\\\"{}\\\"", version)});
|
||||
{
|
||||
std::array<fs::path, 11> interfaces = {
|
||||
std::array<fs::path, 12> interfaces = {
|
||||
"interfaces/Crafter.Build",
|
||||
"interfaces/Crafter.Build-Toolchain",
|
||||
"interfaces/Crafter.Build-Shader",
|
||||
"interfaces/Crafter.Build-Platform",
|
||||
"interfaces/Crafter.Build-Interface",
|
||||
|
|
@ -56,7 +65,8 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
|
|||
"interfaces/Crafter.Build-Progress",
|
||||
"interfaces/Crafter.Build-Asset",
|
||||
};
|
||||
std::array<fs::path, 10> implementations = {
|
||||
std::array<fs::path, 11> implementations = {
|
||||
"implementations/Crafter.Build-Toolchain",
|
||||
"implementations/Crafter.Build-Shader",
|
||||
"implementations/Crafter.Build-Platform",
|
||||
"implementations/Crafter.Build-Interface",
|
||||
|
|
@ -118,6 +128,8 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
|
|||
cfg.AddTest("StandardArgs").Dependencies({ CrafterBuildLib.get() });
|
||||
cfg.AddTest("TestRunnerSpec").Dependencies({ CrafterBuildLib.get() });
|
||||
cfg.AddTest("VariantId").Dependencies({ CrafterBuildLib.get() });
|
||||
cfg.AddTest("ToolchainResolve").Dependencies({ CrafterBuildLib.get() });
|
||||
cfg.AddTest("VersionFlag").Dependencies({ CrafterBuildLib.get() });
|
||||
cfg.AddTest("WasiBrowserRuntime").Dependencies({ CrafterBuildLib.get() });
|
||||
cfg.AddTest("WasmVariants").Dependencies({ CrafterBuildLib.get() });
|
||||
cfg.AddTest("RunSingleTestExit").Dependencies({ CrafterBuildLib.get() });
|
||||
|
|
|
|||
|
|
@ -92,6 +92,32 @@ int main() {
|
|||
Check(r.name == "wine", "ForTarget(mingw) uses wine");
|
||||
}
|
||||
|
||||
// SameTarget: the vendor field is spelling, not identity. A clang built
|
||||
// without a vendor reports x86_64-unknown-linux-gnu where Arch's reports
|
||||
// x86_64-pc-linux-gnu; both are the host, and a project that wrote the
|
||||
// other spelling must still run its tests locally.
|
||||
{
|
||||
Check(SameTarget("x86_64-pc-linux-gnu", "x86_64-unknown-linux-gnu"), "pc and unknown vendors compare equal");
|
||||
Check(SameTarget("x86_64-pc-linux-gnu", "x86_64-linux-gnu"), "a vendorless triple compares equal");
|
||||
Check(SameTarget("x86_64-alpine-linux-musl", "x86_64-linux-musl"), "a distro vendor compares equal");
|
||||
Check(SameTarget("aarch64-unknown-linux-gnu", "aarch64-linux-gnu"), "the vendor is dropped on other arches too");
|
||||
Check(SameTarget("wasm32-unknown-wasip1", "wasm32-wasip1"), "a three-part triple with a vendor compares equal");
|
||||
Check(!SameTarget("x86_64-pc-linux-gnu", "x86_64-linux-musl"), "a different environment stays different");
|
||||
Check(!SameTarget("x86_64-pc-linux-gnu", "aarch64-unknown-linux-gnu"), "a different arch stays different");
|
||||
Check(!SameTarget("x86_64-w64-mingw32", "x86_64-linux-gnu"), "mingw's w64 is not a vendor to drop");
|
||||
Check(NormalizeTarget("x86_64-w64-mingw32") == "x86_64-w64-mingw32", "a mingw triple is left alone");
|
||||
Check(NormalizeTarget("wasm32-wasip1") == "wasm32-wasip1", "a two-part triple is left alone");
|
||||
|
||||
Configuration pc;
|
||||
pc.target = "x86_64-pc-linux-gnu";
|
||||
Configuration unknown;
|
||||
unknown.target = "x86_64-unknown-linux-gnu";
|
||||
if (SameTarget(HostTarget(), "x86_64-linux-gnu")) {
|
||||
Check(TestRunner::ForTarget(pc).IsLocal(), "ForTarget runs the pc spelling locally on an x86_64 glibc host");
|
||||
Check(TestRunner::ForTarget(unknown).IsLocal(), "ForTarget runs the unknown spelling locally on an x86_64 glibc host");
|
||||
}
|
||||
}
|
||||
|
||||
// FromEnv: when the env var is set, it wins over the fallback.
|
||||
{
|
||||
// Use a unique fake triple so we don't stomp on a real env var the
|
||||
|
|
|
|||
169
tests/ToolchainResolve/main.cpp
Normal file
169
tests/ToolchainResolve/main.cpp
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||
|
||||
import std;
|
||||
import Crafter.Build;
|
||||
namespace fs = std::filesystem;
|
||||
using namespace Crafter;
|
||||
|
||||
// ResolveToolchain is pure path resolution, so this test needs no compiler:
|
||||
// it stages fake prefixes under the temp dir and checks which one wins,
|
||||
// what is picked up beside it, and what the defaults are when nothing is.
|
||||
// The last block runs the real GetToolchain() once, which is the only part
|
||||
// that spawns clang.
|
||||
namespace {
|
||||
std::int32_t Failures = 0;
|
||||
|
||||
void Check(bool cond, std::string_view msg) {
|
||||
if (!cond) {
|
||||
std::println(std::cerr, "FAIL: {}", msg);
|
||||
++Failures;
|
||||
}
|
||||
}
|
||||
|
||||
void Touch(const fs::path& file) {
|
||||
fs::create_directories(file.parent_path());
|
||||
std::ofstream(file) << "";
|
||||
}
|
||||
|
||||
bool Contains(std::string_view haystack, std::string_view needle) {
|
||||
return haystack.find(needle) != std::string_view::npos;
|
||||
}
|
||||
}
|
||||
|
||||
int main() {
|
||||
const fs::path scratch = fs::temp_directory_path() / "crafter-build-toolchain-resolve";
|
||||
std::error_code ec;
|
||||
fs::remove_all(scratch, ec);
|
||||
fs::create_directories(scratch);
|
||||
|
||||
// A prefix with everything crafter-build looks for beside the compiler,
|
||||
// and one with the compiler alone.
|
||||
const fs::path full = scratch / "full";
|
||||
for (const char* rel : {"bin/clang++", "bin/clang", "bin/llvm-ar", "lib/libclang.so", "include/clang-c/Index.h", "share/libc++/v1/std.cppm", "share/wasi-sysroot/include/.keep"}) {
|
||||
Touch(full / rel);
|
||||
}
|
||||
const fs::path bare = scratch / "bare";
|
||||
Touch(bare / "bin/clang++");
|
||||
|
||||
{
|
||||
// Nothing set and nothing beside crafter-build: bare names on PATH
|
||||
// and the system locations.
|
||||
Toolchain tc = ResolveToolchain({});
|
||||
Check(tc.error.empty(), "the PATH fallback is not an error");
|
||||
Check(tc.prefix.empty(), "the PATH fallback has no prefix");
|
||||
Check(tc.clangxx == "clang++" && tc.clang == "clang" && tc.llvmAr == "llvm-ar", "the PATH fallback uses bare tool names");
|
||||
Check(!tc.libclang.has_value(), "the PATH fallback searches libclang by name");
|
||||
Check(tc.llvmIncludeDir.empty(), "the PATH fallback adds no include dir");
|
||||
Check(tc.stdCppm == "/usr/share/libc++/v1/std.cppm", "the system std module source is the default");
|
||||
Check(tc.wasiSysroot == "/usr/share/wasi-sysroot", "the system wasi sysroot is the default");
|
||||
Check(tc.mingwSysroot == "/usr/x86_64-w64-mingw32" && !tc.mingwSysrootExplicit, "the default mingw dir is not passed as --sysroot");
|
||||
Check(tc.LibcxxCompileFlags().empty() && tc.LibcxxLinkFlags().empty(), "the PATH fallback adds no libc++ flags");
|
||||
}
|
||||
{
|
||||
// A toolchain installed beside crafter-build wins over PATH, and
|
||||
// everything found beside it is used.
|
||||
ToolchainInputs in;
|
||||
in.crafterPrefix = full;
|
||||
Toolchain tc = ResolveToolchain(in);
|
||||
Check(tc.prefix == full, "the prefix beside crafter-build is picked");
|
||||
Check(tc.clangxx == full / "bin/clang++" && tc.clang == full / "bin/clang" && tc.llvmAr == full / "bin/llvm-ar", "the tools come from that prefix");
|
||||
Check(tc.libclang == full / "lib/libclang.so", "libclang beside the compiler is pinned");
|
||||
Check(tc.llvmIncludeDir == full / "include", "clang-c headers beside the compiler are added to the include path");
|
||||
Check(tc.stdCppm == full / "share/libc++/v1/std.cppm", "the prefix's std module source is used");
|
||||
Check(tc.wasiSysroot == full / "share/wasi-sysroot", "the prefix's wasi sysroot is used");
|
||||
Check(tc.LibcxxCompileFlags().empty(), "the compiler finds its own libc++ headers without flags");
|
||||
Check(Contains(tc.LibcxxLinkFlags(), std::format("-Wl,-rpath,{}", (full / "lib").string())), "binaries get an rpath into the prefix's lib dir");
|
||||
}
|
||||
{
|
||||
ToolchainInputs in;
|
||||
in.crafterPrefix = scratch / "nothing-here";
|
||||
Toolchain tc = ResolveToolchain(in);
|
||||
Check(tc.prefix.empty() && tc.clangxx == "clang++", "a crafter-build prefix without clang++ falls back to PATH");
|
||||
}
|
||||
{
|
||||
// The environment wins over the prefix beside crafter-build. A
|
||||
// prefix with nothing but the compiler keeps the system defaults for
|
||||
// everything else.
|
||||
ToolchainInputs in;
|
||||
in.crafterPrefix = full;
|
||||
in.env["CRAFTER_BUILD_LLVM_DIR"] = bare.string();
|
||||
Toolchain tc = ResolveToolchain(in);
|
||||
Check(tc.error.empty(), "an explicit prefix with a clang++ is accepted");
|
||||
Check(tc.prefix == bare, "CRAFTER_BUILD_LLVM_DIR wins over the prefix beside crafter-build");
|
||||
Check(!tc.libclang.has_value(), "no libclang.so in the prefix: search by name");
|
||||
Check(tc.llvmIncludeDir.empty(), "no clang-c headers in the prefix: nothing added");
|
||||
Check(tc.stdCppm == "/usr/share/libc++/v1/std.cppm", "no std module in the prefix: the system copy");
|
||||
Check(tc.wasiSysroot == "/usr/share/wasi-sysroot", "no wasi sysroot in the prefix: the system one");
|
||||
Check(Contains(tc.LibcxxLinkFlags(), std::format("-Wl,-rpath,{}", (bare / "lib").string())), "the rpath follows the explicit prefix");
|
||||
}
|
||||
{
|
||||
ToolchainInputs in;
|
||||
in.env["CRAFTER_BUILD_LLVM_DIR"] = (scratch / "missing").string();
|
||||
Toolchain tc = ResolveToolchain(in);
|
||||
Check(!tc.error.empty() && Contains(tc.error, "CRAFTER_BUILD_LLVM_DIR"), "an explicit prefix without clang++ is an error, not a fallback");
|
||||
}
|
||||
{
|
||||
ToolchainInputs in;
|
||||
in.env["CRAFTER_BUILD_LLVM_DIR"] = "/usr";
|
||||
Toolchain tc = ResolveToolchain(in);
|
||||
Check(tc.LibcxxLinkFlags().empty(), "the system prefix needs no rpath");
|
||||
Check(tc.llvmIncludeDir.empty(), "the system prefix needs no include dir");
|
||||
}
|
||||
{
|
||||
ToolchainInputs in;
|
||||
in.crafterPrefix = full;
|
||||
in.env["CRAFTER_BUILD_LIBCLANG"] = "/nonexistent/libclang.so";
|
||||
Check(ResolveToolchain(in).libclang == fs::path("/nonexistent/libclang.so"), "CRAFTER_BUILD_LIBCLANG is taken verbatim, even when it does not exist");
|
||||
}
|
||||
{
|
||||
// An explicit libc++ install: headers, library and std module all
|
||||
// come from it, whatever the compiler would have used.
|
||||
const fs::path libcxx = scratch / "libcxx";
|
||||
ToolchainInputs in;
|
||||
in.crafterPrefix = full;
|
||||
in.env["CRAFTER_BUILD_LIBCXX_DIR"] = libcxx.string();
|
||||
Toolchain tc = ResolveToolchain(in);
|
||||
Check(tc.libcxxDir == libcxx, "CRAFTER_BUILD_LIBCXX_DIR is recorded");
|
||||
Check(tc.stdCppm == libcxx / "share/libc++/v1/std.cppm", "the std module source follows the explicit libc++");
|
||||
Check(Contains(tc.LibcxxCompileFlags(), std::format("-nostdinc++ -isystem {}", (libcxx / "include/c++/v1").string())), "compiles use the explicit libc++ headers");
|
||||
Check(Contains(tc.LibcxxLinkFlags(), std::format("-L{}", (libcxx / "lib").string())) && Contains(tc.LibcxxLinkFlags(), std::format("-Wl,-rpath,{}", (libcxx / "lib").string())), "links use the explicit libc++ library dir, with an rpath");
|
||||
}
|
||||
{
|
||||
ToolchainInputs in;
|
||||
in.env["CRAFTER_BUILD_WASI_SYSROOT"] = "/opt/wasi";
|
||||
in.env["CRAFTER_MINGW_DIR"] = "/opt/mingw";
|
||||
Toolchain tc = ResolveToolchain(in);
|
||||
Check(tc.wasiSysroot == "/opt/wasi", "CRAFTER_BUILD_WASI_SYSROOT overrides the wasi sysroot");
|
||||
Check(tc.mingwSysroot == "/opt/mingw" && tc.mingwSysrootExplicit, "CRAFTER_MINGW_DIR overrides the mingw dir and is passed as --sysroot");
|
||||
}
|
||||
{
|
||||
// The cache key separates toolchains that would build different
|
||||
// PCMs and nothing else.
|
||||
Toolchain a = ResolveToolchain({});
|
||||
a.clangVersion = "clang version 22.1.8";
|
||||
Toolchain b = a;
|
||||
b.clangVersion = "Arch Linux clang version 22.1.9";
|
||||
Toolchain c = a;
|
||||
c.libcxxDir = "/opt/libcxx";
|
||||
Toolchain d = ResolveToolchain({});
|
||||
d.clangVersion = a.clangVersion;
|
||||
Check(a.Key().starts_with("22.1.8-"), std::format("the key starts with the short clang version (got '{}')", a.Key()));
|
||||
Check(b.Key().starts_with("22.1.9-"), "the short version is parsed past a distro prefix");
|
||||
Check(a.Key() != b.Key(), "a different clang version changes the key");
|
||||
Check(a.Key() != c.Key(), "a different libc++ location changes the key");
|
||||
Check(a.Key() == d.Key(), "the key is deterministic");
|
||||
}
|
||||
{
|
||||
// The real toolchain on this machine resolves and the probes work;
|
||||
// HostTarget is the probe's answer.
|
||||
const Toolchain& real = GetToolchain();
|
||||
Check(real.error.empty(), std::format("the real toolchain resolves (error: '{}')", real.error));
|
||||
Check(!real.hostTriple.empty() && real.hostTriple == HostTarget(), "HostTarget comes from the toolchain probe");
|
||||
Check(!real.clangVersion.empty(), "the clang version was probed");
|
||||
Check(!real.Key().starts_with("unknown-"), std::format("the real clang version parses (got '{}')", real.Key()));
|
||||
}
|
||||
|
||||
fs::remove_all(scratch, ec);
|
||||
return Failures == 0 ? 0 : 1;
|
||||
}
|
||||
46
tests/VersionFlag/main.cpp
Normal file
46
tests/VersionFlag/main.cpp
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||
|
||||
import std;
|
||||
import Crafter.Build;
|
||||
namespace fs = std::filesystem;
|
||||
using namespace Crafter;
|
||||
|
||||
// `--version` has to work from any directory, before a project file is
|
||||
// looked for, and must be answered rather than forwarded to project.cpp as
|
||||
// one more unknown argument.
|
||||
namespace {
|
||||
std::int32_t Failures = 0;
|
||||
|
||||
void Check(bool cond, std::string_view msg) {
|
||||
if (!cond) {
|
||||
std::println(std::cerr, "FAIL: {}", msg);
|
||||
++Failures;
|
||||
}
|
||||
}
|
||||
|
||||
std::int32_t RunWith(std::vector<std::string> args) {
|
||||
std::vector<char*> argv;
|
||||
for (std::string& arg : args) argv.push_back(arg.data());
|
||||
return Run(static_cast<std::int32_t>(argv.size()), argv.data());
|
||||
}
|
||||
}
|
||||
|
||||
int main() {
|
||||
Check(!CrafterBuildVersion().empty(), "a version string is always baked in");
|
||||
|
||||
// An empty directory: no project.cpp to fall back to.
|
||||
const fs::path scratch = fs::temp_directory_path() / "crafter-build-version-flag";
|
||||
std::error_code ec;
|
||||
fs::remove_all(scratch, ec);
|
||||
fs::create_directories(scratch);
|
||||
fs::current_path(scratch);
|
||||
|
||||
Check(RunWith({"crafter-build", "--version"}) == 0, "--version exits 0 without a project file");
|
||||
Check(RunWith({"crafter-build", "--version", "--some-project-flag"}) == 0, "--version is answered before unknown arguments are forwarded");
|
||||
Check(RunWith({"crafter-build", "--some-project-flag", "--version"}) == 0, "--version is answered wherever it appears");
|
||||
|
||||
fs::current_path(fs::temp_directory_path());
|
||||
fs::remove_all(scratch, ec);
|
||||
return Failures == 0 ? 0 : 1;
|
||||
}
|
||||
Loading…
Reference in a new issue