diff --git a/AGENTS.md b/AGENTS.md index b33dad4..33f5018 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -21,7 +21,6 @@ crafter-build is a C++26 modules build system. A project is described by a `proj Partitions and what they own: -- `:Toolchain` resolves the compiler once per process (`GetToolchain`): clang++, clang, llvm-ar, libclang, libc++'s std module source and the default sysroots, from `CRAFTER_BUILD_LLVM_DIR`, else a toolchain installed beside crafter-build, else PATH. It also holds the version baked in at build time. - `:Clang` holds `Configuration`, `Build()`, `VariantId()`, `ApplyStandardArgs`, `HostTarget`, the CLI entry point `Crafter::Run`, and the lint and test data types. - `:Platform` runs commands, implements `LoadProject` (compile and load `project.cpp`), the host PCM cache, `#pragma crafter dependency`, and all OS-specific code. - `:Interface` and `:Implementation` model module units, their staleness checks and their compiles. @@ -32,7 +31,7 @@ Partitions and what they own: ## Dev environment -Prerequisites on Linux: `clang` (with `libclang`, which the AST lint rules load at runtime), `lld`, `libc++` with its std module source (`share/libc++/v1/std.cppm`), `cmake`, `git`. `crafter-build --version` prints which compiler was resolved; `CRAFTER_BUILD_LLVM_DIR` points it at another LLVM prefix, and `build.sh` honours the same variable. Network access to `forgejo.catcrafts.net` and `github.com` is needed: loading `project.cpp` fetches Crafter.Math, Crafter.Asset and Crafter.LintRules into `~/.cache/crafter.build/external/`, and the bootstrap clones glslang. Those sources are unpinned, so every invocation runs `git pull` on them. +Prerequisites on Linux: `clang` (with `libclang`, which the AST lint rules load at runtime), `lld`, `libc++` with `/usr/share/libc++/v1/std.cppm`, `cmake`, `git`. Network access to `forgejo.catcrafts.net` and `github.com` is needed: loading `project.cpp` fetches Crafter.Math, Crafter.Asset and Crafter.LintRules into `~/.cache/crafter.build/external/`, and the bootstrap clones glslang. Those sources are unpinned, so every invocation runs `git pull` on them. ```bash ./build.sh # bootstrap: bin/crafter-build, lib/libcrafter-build.a, share/crafter-build/ diff --git a/README.md b/README.md index 6af758c..a5ebf0a 100644 --- a/README.md +++ b/README.md @@ -11,8 +11,6 @@ Bootstrap requires `clang`, `cmake`, `git`, `lld`, and `libc++`. ./bin/crafter-build # rebuild via project.cpp ``` -The compiler is resolved once per run: `CRAFTER_BUILD_LLVM_DIR` if set, else a `clang++` installed beside crafter-build (`/bin`), else PATH. `crafter-build --version` prints the version and the compiler in use; `crafter-build --help` lists the other overrides (`CRAFTER_BUILD_LIBCXX_DIR`, `CRAFTER_BUILD_LIBCLANG`, `CRAFTER_BUILD_WASI_SYSROOT`, `CRAFTER_MINGW_DIR`). A `Configuration` targets the host triple unless `cfg.target` says otherwise. - The bootstrap targets the host toolchain's own triple (`clang++ -print-target-triple`): `x86_64-pc-linux-gnu` on glibc distros, `x86_64-alpine-linux-musl` on Alpine/postmarketOS (`apk add clang clang-dev lld libc++-dev llvm-libunwind-dev llvm-runtimes llvm llvm-dev cmake build-base python3 git`). Override with `CRAFTER_BUILD_TARGET=`. CI ships both: `crafter-build-linux-x86_64-v{2,3,4}.tar.gz` (glibc) and `crafter-build-linux-x86_64-musl-v2.tar.gz` (musl, needs `libc++` at runtime — the launcher dlopens `project.so`, so it cannot be static). To build the system as a distro package on Arch: @@ -36,6 +34,7 @@ extern "C" Configuration CrafterBuildProject(std::span a cfg.path = "./"; cfg.name = "myapp"; cfg.outputName = "myapp"; + cfg.target = "x86_64-pc-linux-gnu"; cfg.type = ConfigurationType::Executable; std::array ifaces = { "interfaces/Hello" }; @@ -205,6 +204,7 @@ extern "C" Configuration CrafterBuildProject(std::span) cfg.path = "tests/UnitFoo/"; cfg.name = "UnitFoo"; cfg.outputName = "UnitFoo"; + cfg.target = "x86_64-pc-linux-gnu"; cfg.type = ConfigurationType::Executable; cfg.dependencies = { ParentLib("MyLib") }; // <-- the magic line diff --git a/build.sh b/build.sh index eeff75f..f0c3083 100755 --- a/build.sh +++ b/build.sh @@ -1,126 +1,142 @@ -#!/bin/sh #SPDX-License-Identifier: LGPL-3.0-only #SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® -# 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 +mkdir -p build +mkdir -p bin +mkdir -p lib +mkdir -p share/crafter-build +mkdir -p 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/ -# 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" - cp interfaces/Crafter.Build.cppm share/crafter-build/ -for p in $PARTITIONS; do - cp "interfaces/Crafter.Build-$p.cppm" share/crafter-build/ -done +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/ -if [ ! -d build/glslang/.git ]; then - git clone --depth 1 https://github.com/KhronosGroup/glslang.git build/glslang -fi +git clone https://github.com/KhronosGroup/glslang.git ./build/glslang current_dir=$(pwd) -cd build/glslang +cd ./build/glslang cmake -B build \ - -DCMAKE_C_COMPILER="$CC" \ - -DCMAKE_CXX_COMPILER="$CXX" \ + -DCMAKE_C_COMPILER=clang \ + -DCMAKE_CXX_COMPILER=clang++ \ -DCMAKE_CXX_FLAGS="-stdlib=libc++" \ - -DCMAKE_EXE_LINKER_FLAGS="-stdlib=libc++ $RPATH_FLAG" \ - -DCMAKE_SHARED_LINKER_FLAGS="-stdlib=libc++ $RPATH_FLAG" \ + -DCMAKE_EXE_LINKER_FLAGS="-stdlib=libc++" \ + -DCMAKE_SHARED_LINKER_FLAGS="-stdlib=libc++" \ -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 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)}" +# 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)}" 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; 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 +# 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 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 -D CRAFTER_BUILD_VERSION=\"$VERSION\" -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 -c" -"$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++ --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 +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). -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 -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 -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 +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 -# 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 +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 -"$CXX" --target="$TARGET" -std=c++26 -stdlib=libc++ -O3 -march="$MARCH" -mtune="$MTUNE" -fuse-ld=lld \ - -Wl,--export-dynamic $RPATH_FLAG \ +clang++ --target=$TARGET -std=c++26 -stdlib=libc++ -O3 -march=$MARCH -mtune=$MTUNE -fuse-ld=lld \ + -Wl,--export-dynamic \ -L./build \ - $OBJECTS \ + ./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 \ ./build/main.o \ -lSPIRV -lGenericCodeGen -lglslang -lOSDependent -lMachineIndependent -lglslang-default-resource-limits \ -ldl \ - -o ./bin/crafter-build + -o ./bin/crafter-build \ No newline at end of file diff --git a/examples/hello-world/project.cpp b/examples/hello-world/project.cpp index b2dfa0a..d9dfc51 100644 --- a/examples/hello-world/project.cpp +++ b/examples/hello-world/project.cpp @@ -11,6 +11,7 @@ extern "C" Configuration CrafterBuildProject(std::span) cfg.path = "./"; cfg.name = "hello"; cfg.outputName = "hello"; + cfg.target = "x86_64-pc-linux-gnu"; cfg.type = ConfigurationType::Executable; std::array ifaces = {}; diff --git a/examples/library/project.cpp b/examples/library/project.cpp index f7159e7..24f7a61 100644 --- a/examples/library/project.cpp +++ b/examples/library/project.cpp @@ -15,6 +15,7 @@ extern "C" Configuration CrafterBuildProject(std::span) lib->path = "./mylib/"; lib->name = "MyMath"; lib->outputName = "MyMath"; + lib->target = "x86_64-pc-linux-gnu"; lib->type = ConfigurationType::LibraryStatic; { std::array ifaces = { "MyMath" }; @@ -28,6 +29,7 @@ extern "C" Configuration CrafterBuildProject(std::span) 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() }; diff --git a/examples/tests/project.cpp b/examples/tests/project.cpp index 85f8468..9a026cb 100644 --- a/examples/tests/project.cpp +++ b/examples/tests/project.cpp @@ -14,6 +14,7 @@ extern "C" Configuration CrafterBuildProject(std::span) cfg.path = "./mylib/"; cfg.name = "MyMath"; cfg.outputName = "MyMath"; + cfg.target = "x86_64-pc-linux-gnu"; cfg.type = ConfigurationType::LibraryStatic; std::array ifaces = { "MyMath" }; diff --git a/examples/with-module/project.cpp b/examples/with-module/project.cpp index c7b68cb..f885fba 100644 --- a/examples/with-module/project.cpp +++ b/examples/with-module/project.cpp @@ -11,6 +11,7 @@ extern "C" Configuration CrafterBuildProject(std::span) cfg.path = "./"; cfg.name = "greeter-app"; cfg.outputName = "greeter-app"; + cfg.target = "x86_64-pc-linux-gnu"; cfg.type = ConfigurationType::Executable; std::array ifaces = { "interfaces/Greeter" }; diff --git a/implementations/Crafter.Build-Clang.cpp b/implementations/Crafter.Build-Clang.cpp index 7c7f5c0..6aa9307 100644 --- a/implementations/Crafter.Build-Clang.cpp +++ b/implementations/Crafter.Build-Clang.cpp @@ -9,7 +9,6 @@ export module Crafter.Build:Clang_impl; import std; import :Clang; import :Platform; -import :Toolchain; import :Test; import :Lint; import :Progress; @@ -405,11 +404,6 @@ CompileCommand Crafter::GetCompileCommand(const Configuration& config) { } BuildResult Crafter::Build(Configuration& config, std::unordered_map>& 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). @@ -455,7 +449,7 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_map---: 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 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); + 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; } bool Crafter::ArgQuery::Has(std::string_view flag) const { @@ -1506,7 +1475,7 @@ ArgQuery Crafter::ApplyStandardArgs(Configuration& cfg, std::span 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: @@ -1546,7 +1504,6 @@ 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=), compiles it to a shared object, and invokes its CrafterBuildProject() to obtain a Configuration that @@ -1614,14 +1571,7 @@ Environment: CRAFTER_BUILD_RUNNER_ Default test runner for a target triple. Replace '-' and '.' with '_' in the triple. CLI --runner= overrides this. - 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. + CRAFTER_MINGW_DIR Override mingw-w64 sysroot auto-detect. Exit status: 0 success / all non-skipped tests passed / lint clean / format applied @@ -1649,13 +1599,7 @@ 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 == "--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")) { + if (arg == "-h" || arg == "--help" || (!runTests && !runLint && !runFormat && arg == "help")) { PrintHelp(argv0); return 0; } else if (!runLint && !runFormat && !runClean && arg == "test") { diff --git a/implementations/Crafter.Build-External.cpp b/implementations/Crafter.Build-External.cpp index 9194b1c..c758a67 100644 --- a/implementations/Crafter.Build-External.cpp +++ b/implementations/Crafter.Build-External.cpp @@ -4,7 +4,6 @@ module Crafter.Build:External_impl; import std; import :External; -import :Toolchain; import :Platform; import :Progress; import :Clang; @@ -119,16 +118,15 @@ std::string BuildInjectedCMakeFlags(const fs::path& cmakeBuildDir, std::string_v // suffix to library names assuming a debug build, breaking -l // linking. Pin to Release. out += " -DCMAKE_BUILD_TYPE=Release"; - const Toolchain& tc = GetToolchain(); - out += std::format(" -DCMAKE_C_COMPILER={}", ShellQuote(tc.clang.string())); - out += std::format(" -DCMAKE_CXX_COMPILER={}", ShellQuote(tc.clangxx.string())); + out += " -DCMAKE_C_COMPILER=clang"; + out += " -DCMAKE_CXX_COMPILER=clang++"; // 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 += 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()))); + out += " -DCMAKE_CXX_FLAGS=-stdlib=libc++"; + out += " -DCMAKE_EXE_LINKER_FLAGS=-stdlib=libc++"; + out += " -DCMAKE_SHARED_LINKER_FLAGS=-stdlib=libc++"; } if (target == "x86_64-w64-mingw32") { out += " -DCMAKE_SYSTEM_NAME=Windows"; @@ -144,10 +142,6 @@ std::string BuildInjectedCMakeFlags(const fs::path& cmakeBuildDir, std::string_v std::string ConfigureCMake(const fs::path& cloneDir, const fs::path& cmakeBuildDir, std::span 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) { @@ -157,7 +151,7 @@ std::string ConfigureCMake(const fs::path& cloneDir, const fs::path& cmakeBuildD } else { std::stringstream buf; buf << existing.rdbuf(); - if (buf.str() != configureKey) needsConfigure = true; + if (buf.str() != optionsKey) needsConfigure = true; } } @@ -165,7 +159,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()), injected); + std::string cmd = std::format("cmake -S {} -B {}{}", ShellQuote(fs::absolute(cloneDir).string()), ShellQuote(fs::absolute(cmakeBuildDir).string()), BuildInjectedCMakeFlags(cmakeBuildDir, target)); if (!options.empty()) { cmd += " "; cmd += optionsKey; @@ -176,7 +170,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) << configureKey; + std::ofstream(optionsFile) << optionsKey; return ""; } diff --git a/implementations/Crafter.Build-Lint.cpp b/implementations/Crafter.Build-Lint.cpp index 357fb33..a2c5308 100644 --- a/implementations/Crafter.Build-Lint.cpp +++ b/implementations/Crafter.Build-Lint.cpp @@ -9,7 +9,6 @@ import std; import :Lint; import :Clang; import :Platform; -import :Toolchain; import :Progress; namespace fs = std::filesystem; using namespace Crafter; @@ -93,14 +92,14 @@ namespace { LibClang LoadLibClang() { LibClang lib; std::vector tried; - // 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. + // 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. std::vector candidates; - if (const std::optional& pinned = GetToolchain().libclang) { - candidates.emplace_back(pinned->string()); + if (const char* pinned = std::getenv("CRAFTER_BUILD_LIBCLANG"); pinned && *pinned) { + candidates.emplace_back(pinned); } else { for (std::string_view name : LibClangNames) candidates.emplace_back(name); } @@ -120,7 +119,7 @@ namespace { tried.push_back(name); } if (!lib.handle) { - 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)); + lib.error = std::format("could not load libclang (tried {}); install clang, or point CRAFTER_BUILD_LIBCLANG at it", join(tried)); return lib; } @@ -711,7 +710,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(std::format("{} -print-resource-dir", GetToolchain().clangxx.string())); + CommandResult r = RunCommandChecked("clang++ -print-resource-dir"); if (r.exitCode != 0) return {}; std::string out = std::move(r.output); while (!out.empty() && (out.back() == '\n' || out.back() == '\r')) out.pop_back(); diff --git a/implementations/Crafter.Build-Platform.cpp b/implementations/Crafter.Build-Platform.cpp index 4181bb6..12f98a6 100644 --- a/implementations/Crafter.Build-Platform.cpp +++ b/implementations/Crafter.Build-Platform.cpp @@ -14,7 +14,6 @@ module; module Crafter.Build:Platform_impl; import std; import :Platform; -import :Toolchain; import :Clang; import :Interface; import :External; @@ -596,7 +595,7 @@ std::string Crafter::BuildStdPcm(const Configuration& config, fs::path stdPcm) { std::vector folders; // Iterate through the directory and collect all subdirectories - for (const auto& entry : fs::directory_iterator(GetToolchain().mingwSysroot / "include" / "c++")) { + for (const auto& entry : fs::directory_iterator("/usr/x86_64-w64-mingw32/include/c++")) { if (entry.is_directory()) { folders.push_back(entry.path().filename().string()); } @@ -609,7 +608,7 @@ std::string Crafter::BuildStdPcm(const Configuration& config, fs::path stdPcm) { std::string mingWversion = folders.front(); - fs::path stdCc = GetToolchain().mingwSysroot / "include" / "c++" / mingWversion / "bits" / "std.cc"; + fs::path stdCc = fs::path(std::format("/usr/x86_64-w64-mingw32/include/c++/{}/bits/std.cc", mingWversion)); if(!fs::exists(stdPcm) || fs::last_write_time(stdPcm) < fs::last_write_time(stdCc)) { // -femulated-tls keeps PCM TLS access matching libstdc++'s emutls @@ -622,7 +621,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("{} --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())); + 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())); } else { return ""; } @@ -635,15 +634,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 = GetToolchain().stdCppm.string(); + stdCppm = "/usr/share/libc++/v1/std.cppm"; } 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(GetToolchain().stdCppm)) { - stdCppm = GetToolchain().stdCppm.string(); + if (!fs::exists(stdCppm) && fs::exists("/usr/share/libc++/v1/std.cppm")) { + stdCppm = "/usr/share/libc++/v1/std.cppm"; } } std::string sysrootFlag = config.sysroot.empty() @@ -670,7 +669,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("{} --target={} -std=c++26{}{} -O3 -Wno-reserved-identifier -Wno-reserved-module-identifier --precompile {} -o {}", GetBaseCommand(config), config.target, sysrootFlag, archFlags, stdCppm, stdPcm.string())); + 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())); } else { return ""; } @@ -690,24 +689,17 @@ fs::path Crafter::GetCacheDir() { } std::string Crafter::GetBaseCommand(const Configuration& config) { - 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; + std::string stdlib; + if(config.target == "x86_64-w64-mingw32") { + stdlib = ""; + } else { + stdlib = "-stdlib=libc++"; } - 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; + return std::format("clang++ {}", stdlib); } namespace { - constexpr std::array CrafterBuildModules = { - "Crafter.Build-Toolchain", + constexpr std::array CrafterBuildModules = { "Crafter.Build-Shader", "Crafter.Build-Platform", "Crafter.Build-Interface", @@ -721,7 +713,7 @@ namespace { "Crafter.Build", }; - void EnsureCrafterBuildPcms(const fs::path& sourceDir, const fs::path& cacheDir, const Configuration& host) { + void EnsureCrafterBuildPcms(const fs::path& sourceDir, const fs::path& cacheDir) { CacheLock lock(cacheDir); // Re-derived under the lock: another builder may have refreshed the // cache from its own sources while we waited. @@ -737,12 +729,12 @@ namespace { continue; } std::string cmd = std::format( - "{} --target={} -march=native -mtune=native " - "-std=c++26 -O3 " + "clang++ --target={} -march=native -mtune=native " + "-std=c++26 -stdlib=libc++ -O3 " "-Wno-reserved-identifier -Wno-reserved-module-identifier " "-fprebuilt-module-path={} " "--precompile {} -o {}", - GetBaseCommand(host), host.target, cacheDir.string(), cppmPath.string(), pcmPath.string()); + HostTarget(), 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)); @@ -756,7 +748,6 @@ namespace { } Configuration Crafter::LoadProject(const fs::path& projectFile, std::span args) { - RequireToolchain(); fs::path absProject = fs::canonical(projectFile); ProjectFileGuard guard(absProject); fs::path buildDir = absProject.parent_path() / "build"; @@ -784,7 +775,7 @@ Configuration Crafter::LoadProject(const fs::path& projectFile, std::span sweep; - sweep.emplace(NormalizeTarget(HostTarget()), HostTarget()); + std::set sweep; + sweep.insert(HostTarget()); for (const Test& t : projectCfg.tests) { - if (!t.config.target.empty()) sweep.emplace(NormalizeTarget(t.config.target), t.config.target); + if (!t.config.target.empty()) sweep.insert(t.config.target); } TestSummary aggregate; - for (const auto& [normalized, target] : sweep) { + for (const auto& target : sweep) { RunTestsOptions perTarget = opts; perTarget.targetFilter = target; if (sweep.size() > 1) { @@ -502,7 +499,7 @@ TestSummary Crafter::RunTests(Configuration& projectCfg, const RunTestsOptions& std::vector filtered; filtered.reserve(projectCfg.tests.size()); for (auto& test : projectCfg.tests) { - if (!SameTarget(test.config.target, opts.targetFilter)) continue; + if (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) { diff --git a/implementations/Crafter.Build-Toolchain.cpp b/implementations/Crafter.Build-Toolchain.cpp deleted file mode 100644 index 8a236a9..0000000 --- a/implementations/Crafter.Build-Toolchain.cpp +++ /dev/null @@ -1,182 +0,0 @@ -// 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::hash{}(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 - // /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; -} diff --git a/interfaces/Crafter.Build-Clang.cppm b/interfaces/Crafter.Build-Clang.cppm index 1149661..2bb1b73 100644 --- a/interfaces/Crafter.Build-Clang.cppm +++ b/interfaces/Crafter.Build-Clang.cppm @@ -355,21 +355,12 @@ export namespace Crafter { bool needsAst = false; }; - // The host target triple: what the resolved toolchain's clang++ prints - // for -print-target-triple (see GetToolchain). Used as the default for + // The host target triple, detected once per process by running + // `clang++ -print-target-triple` and cached. Used as the default for // Configuration::target so projects don't have to hardcode it for the - // 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. + // 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. 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; diff --git a/interfaces/Crafter.Build-Toolchain.cppm b/interfaces/Crafter.Build-Toolchain.cppm deleted file mode 100644 index b0fd2dc..0000000 --- a/interfaces/Crafter.Build-Toolchain.cppm +++ /dev/null @@ -1,78 +0,0 @@ -// 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 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++//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> env; - // The of /share/crafter-build; empty when unknown. - fs::path crafterPrefix; - }; - // Pure path resolution: an env override wins, then a toolchain installed - // beside crafter-build (/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(); -} diff --git a/interfaces/Crafter.Build.cppm b/interfaces/Crafter.Build.cppm index de098e0..de659dc 100644 --- a/interfaces/Crafter.Build.cppm +++ b/interfaces/Crafter.Build.cppm @@ -2,7 +2,6 @@ // SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® export module Crafter.Build; -export import :Toolchain; export import :Clang; export import :Platform; export import :Implementation; diff --git a/project.cpp b/project.cpp index c414bce..8ce6800 100644 --- a/project.cpp +++ b/project.cpp @@ -30,30 +30,21 @@ extern "C" Configuration CrafterBuildProject(std::span a CrafterBuildLib->dependencies = { math, asset }; CrafterBuildLib->defines.push_back({"CRAFTER_BUILD_HAS_ASSET", ""}); - // 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())); + // 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)); + } } - // 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 interfaces = { + std::array interfaces = { "interfaces/Crafter.Build", - "interfaces/Crafter.Build-Toolchain", "interfaces/Crafter.Build-Shader", "interfaces/Crafter.Build-Platform", "interfaces/Crafter.Build-Interface", @@ -65,8 +56,7 @@ extern "C" Configuration CrafterBuildProject(std::span a "interfaces/Crafter.Build-Progress", "interfaces/Crafter.Build-Asset", }; - std::array implementations = { - "implementations/Crafter.Build-Toolchain", + std::array implementations = { "implementations/Crafter.Build-Shader", "implementations/Crafter.Build-Platform", "implementations/Crafter.Build-Interface", @@ -128,8 +118,6 @@ extern "C" Configuration CrafterBuildProject(std::span 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() }); diff --git a/tests/TestRunnerSpec/main.cpp b/tests/TestRunnerSpec/main.cpp index 44ae434..12371be 100644 --- a/tests/TestRunnerSpec/main.cpp +++ b/tests/TestRunnerSpec/main.cpp @@ -92,32 +92,6 @@ 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 diff --git a/tests/ToolchainResolve/main.cpp b/tests/ToolchainResolve/main.cpp deleted file mode 100644 index aedb261..0000000 --- a/tests/ToolchainResolve/main.cpp +++ /dev/null @@ -1,169 +0,0 @@ -// 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; -} diff --git a/tests/VersionFlag/main.cpp b/tests/VersionFlag/main.cpp deleted file mode 100644 index 7d59cbb..0000000 --- a/tests/VersionFlag/main.cpp +++ /dev/null @@ -1,46 +0,0 @@ -// 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 args) { - std::vector argv; - for (std::string& arg : args) argv.push_back(arg.data()); - return Run(static_cast(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; -}