musl host support: bootstrap and run on Alpine (x86_64-alpine-linux-musl)
All checks were successful
CI / build-test-release (push) Successful in 7m43s
CI / release-musl (push) Successful in 8m56s

The Linux host code paths were gated on the x86_64_pc_linux_gnu target
define alone, and LoadProject hardcoded --target=x86_64-pc-linux-gnu when
compiling project.cpp and the module PCMs. Both break on musl hosts: the
glibc release launcher cannot run there at all (no glibc loader; gcompat
lacks libgcc_s and the __isoc23_* symbols), and a musl-built launcher
would still compile project.cpp for the wrong libc.

- The host gates test clang's __linux__ instead of one triple's define:
  they mean "Linux host", whatever the libc (or, later, the CPU).
- LoadProject derives the host triple from HostTarget() (clang's
  -print-target-triple), which is also more correct on glibc distros
  whose triple isn't x86_64-pc-linux-gnu.
- project.cpp / the cmake libc++ flags treat any *-linux-gnu or
  *-linux-musl target as Linux.
- build.sh targets the host toolchain's triple (CRAFTER_BUILD_TARGET
  overrides) and derives the per-target define from it. On Arch this is
  byte-for-byte the previous behaviour.
- CI: a release-musl job bootstraps on alpine:edge, runs the tests, and
  attaches crafter-build-linux-x86_64-musl-v2.tar.gz to the rolling
  latest release (via the API, so the glibc assets survive). Dynamic
  against musl libc++: the launcher dlopens project.so, so it cannot be
  fully static.

First consumer: imsd's package CI, which cross-compiles for aarch64 from
an Alpine container.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Jorijn van der Graaf 2026-09-01 23:49:00 +02:00
commit dc239f4257
7 changed files with 131 additions and 22 deletions

View file

@ -186,3 +186,76 @@ jobs:
override: true override: true
release-dir: dist release-dir: dist
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
# musl build for Alpine / postmarketOS hosts: the glibc launcher above cannot
# run there (musl has no glibc loader and gcompat lacks libgcc_s + the
# __isoc23_* symbols). Bootstrap natively on alpine:edge and ship the
# bootstrap output — the same thing the Arch PKGBUILD installs. Dynamic
# against musl libc++ (crafter-build dlopens project.so, so it cannot be
# fully static); consumers need `apk add clang lld libc++-dev llvm-runtimes`.
release-musl:
needs: build-test-release
if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/master'
runs-on: arch-latest
container:
image: alpine:edge
env:
CRAFTER_BUILD_MARCH: x86-64-v2
CRAFTER_BUILD_MTUNE: generic
steps:
- name: Install build dependencies
# nodejs: actions/checkout is a Node action. build-base: gcc's
# libgcc_s + crt objects, which clang's musl driver links against, plus
# make/binutils for the glslang cmake build (python3: its generators).
# clang-dev + llvm-dev: the libclang header Crafter.Build-Lint includes
# lives under llvm-config --includedir here, not /usr/include. llvm:
# llvm-ar for the LTO static archives the self-host/test builds write.
run: |
apk add -q nodejs git curl jq tar clang clang-dev lld libc++-dev \
llvm-libunwind-dev llvm-runtimes llvm llvm-dev cmake build-base python3
git config --global --add safe.directory '*'
- name: Checkout
uses: actions/checkout@v4
- name: Bootstrap (build.sh)
run: ./build.sh
- name: Run tests
run: CRAFTER_BUILD_HOME=$PWD/share/crafter-build ./bin/crafter-build test
- name: Package artifact
run: |
set -eux
file bin/crafter-build || true
mkdir -p dist
stage=$(mktemp -d)
mkdir -p "$stage/bin" "$stage/lib"
cp bin/crafter-build "$stage/bin/"
cp lib/libcrafter-build.a "$stage/lib/"
cp -r share "$stage/"
tar czf dist/crafter-build-linux-x86_64-musl-v2.tar.gz -C "$stage" .
ls -la dist/
- name: Upload musl artifact
uses: actions/upload-artifact@v3
with: { name: crafter-build-linux-x86_64-musl-v2, path: dist/crafter-build-linux-x86_64-musl-v2.tar.gz, if-no-files-found: error }
# Add the asset to the rolling 'latest' release the first job just
# (re)published, via the API rather than forgejo-release: that action's
# override mode would drop the other job's assets.
- name: Attach to rolling 'latest' release
env:
TOKEN: ${{ secrets.GITHUB_TOKEN }}
API: ${{ github.server_url }}/api/v1/repos/${{ github.repository }}
run: |
set -eu
asset=crafter-build-linux-x86_64-musl-v2.tar.gz
rel=$(curl -fsS -H "Authorization: token $TOKEN" "$API/releases/tags/latest")
id=$(printf '%s' "$rel" | jq -r .id)
for old in $(printf '%s' "$rel" | jq -r --arg n "$asset" '.assets[] | select(.name==$n) | .id'); do
curl -fsS -X DELETE -H "Authorization: token $TOKEN" "$API/releases/$id/assets/$old"
done
curl -fsS -H "Authorization: token $TOKEN" -F "attachment=@dist/$asset" \
"$API/releases/$id/assets?name=$asset" > /dev/null
echo "attached $asset to release $id"

View file

@ -18,6 +18,8 @@ Bootstrap requires `clang`, `cmake`, `git`, `lld`, and `libc++`.
For distro-packaged installs, `crafter-build` finds its modules at `<prefix>/share/crafter-build/` automatically — no env var required. For distro-packaged installs, `crafter-build` finds its modules at `<prefix>/share/crafter-build/` automatically — no env var required.
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: To build the system as a distro package on Arch:
```bash ```bash

View file

@ -44,10 +44,23 @@ cd ../../
MARCH="${CRAFTER_BUILD_MARCH:-native}" MARCH="${CRAFTER_BUILD_MARCH:-native}"
MTUNE="${CRAFTER_BUILD_MTUNE:-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)}"
TARGET_DEFINE="CRAFTER_BUILD_CONFIGURATION_TARGET_$(printf '%s' "$TARGET" | tr - _)"
common_options="--target=x86_64-pc-linux-gnu -stdlib=libc++ -I./build/glslang -std=c++26 -O3 -march=$MARCH -mtune=$MTUNE -fprebuilt-module-path=./build -D CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_pc_linux_gnu -D CRAFTER_BUILD_CONFIGURATION_TYPE_EXECUTABLE -c" # 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 add /usr/include itself: that breaks libc++'s #include_next).
LLVM_INCDIR="$(llvm-config --includedir 2>/dev/null || true)"
case "$LLVM_INCDIR" in ""|/usr/include) LLVM_INCFLAG="" ;; *) LLVM_INCFLAG="-I$LLVM_INCDIR" ;; esac
clang++ --target=x86_64-pc-linux-gnu -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 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"
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-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-Platform.cppm -o ./build/Crafter.Build-Platform.o
@ -100,7 +113,7 @@ ar rcs ./lib/libcrafter-build.a \
./build/Crafter.Build-Progress_impl.o \ ./build/Crafter.Build-Progress_impl.o \
./build/Crafter.Build-Asset_impl.o ./build/Crafter.Build-Asset_impl.o
clang++ -std=c++26 -stdlib=libc++ -O3 -march=$MARCH -mtune=$MTUNE -fuse-ld=lld \ clang++ --target=$TARGET -std=c++26 -stdlib=libc++ -O3 -march=$MARCH -mtune=$MTUNE -fuse-ld=lld \
-Wl,--export-dynamic \ -Wl,--export-dynamic \
-L./build \ -L./build \
./build/Crafter.Build-Shader.o \ ./build/Crafter.Build-Shader.o \

View file

@ -307,7 +307,7 @@ CompileCommand Crafter::GetCompileCommand(const Configuration& config) {
} }
if(config.type == ConfigurationType::LibraryDynamic) { if(config.type == ConfigurationType::LibraryDynamic) {
#ifdef CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_pc_linux_gnu #ifdef __linux__
out.command += " -fPIC -D CRAFTER_BUILD_CONFIGURATION_TYPE_SHARED_LIBRARY"; out.command += " -fPIC -D CRAFTER_BUILD_CONFIGURATION_TYPE_SHARED_LIBRARY";
#endif #endif
#if defined(CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_pc_windows_msvc) || defined(CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_w64_mingw32) #if defined(CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_pc_windows_msvc) || defined(CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_w64_mingw32)
@ -453,7 +453,7 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_map<fs::path, s
// users supply cfg.sysroot pointing at their wasi-sdk install. Covers all // users supply cfg.sysroot pointing at their wasi-sdk install. Covers all
// wasm32-* triples (wasi, wasip1, wasip2, ...); the sysroot's per-triple // wasm32-* triples (wasi, wasip1, wasip2, ...); the sysroot's per-triple
// subdirs handle the differences. // subdirs handle the differences.
#ifdef CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_pc_linux_gnu #ifdef __linux__
if (config.sysroot.empty() && config.target.starts_with("wasm32")) { if (config.sysroot.empty() && config.target.starts_with("wasm32")) {
config.sysroot = "/usr/share/wasi-sysroot"; config.sysroot = "/usr/share/wasi-sysroot";
} }
@ -1131,7 +1131,7 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_map<fs::path, s
if(buildResult.repack) { if(buildResult.repack) {
Progress::Task task(std::format("Linking {}", config.outputName)); Progress::Task task(std::format("Linking {}", config.outputName));
if(config.type == ConfigurationType::Executable) { if(config.type == ConfigurationType::Executable) {
#ifdef CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_pc_linux_gnu #ifdef __linux__
if(config.target == "x86_64-w64-mingw32") { if(config.target == "x86_64-w64-mingw32") {
try { try {
// Copy any LibraryDynamic dependency DLLs alongside // Copy any LibraryDynamic dependency DLLs alongside
@ -1204,7 +1204,7 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_map<fs::path, s
} }
#endif #endif
} else if(config.type == ConfigurationType::LibraryStatic) { } else if(config.type == ConfigurationType::LibraryStatic) {
#ifdef CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_pc_linux_gnu #ifdef __linux__
// ThinLTO emits LLVM bitcode objects; plain `ar` writes an archive // ThinLTO emits LLVM bitcode objects; plain `ar` writes an archive
// index that omits their symbols, so a consumer's lld link can't // index that omits their symbols, so a consumer's lld link can't
// pull the needed members. llvm-ar writes a bitcode-aware index. // pull the needed members. llvm-ar writes a bitcode-aware index.
@ -1550,7 +1550,7 @@ ArgQuery Crafter::ApplyStandardArgs(Configuration& cfg, std::span<const std::str
// includes it. (Build() also runs this once more for callers that bypassed // includes it. (Build() also runs this once more for callers that bypassed
// ApplyStandardArgs, but doing it here makes dep PcmDirs consistent // ApplyStandardArgs, but doing it here makes dep PcmDirs consistent
// between the consumer's command-construction and the dep's own build.) // between the consumer's command-construction and the dep's own build.)
#ifdef CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_pc_linux_gnu #ifdef __linux__
if (cfg.sysroot.empty() && cfg.target.starts_with("wasm32")) { if (cfg.sysroot.empty() && cfg.target.starts_with("wasm32")) {
cfg.sysroot = "/usr/share/wasi-sysroot"; cfg.sysroot = "/usr/share/wasi-sysroot";
} }

View file

@ -137,8 +137,8 @@ std::string BuildInjectedCMakeFlags(const fs::path& cmakeBuildDir, std::string_v
// itself is built with -stdlib=libc++); mingw cross-compile uses libstdc++ // itself is built with -stdlib=libc++); mingw cross-compile uses libstdc++
// shipped by the mingw-w64 toolchain so we leave the default; Windows // shipped by the mingw-w64 toolchain so we leave the default; Windows
// native (msvc target) similarly uses libc++ via LIBCXX_DIR. // native (msvc target) similarly uses libc++ via LIBCXX_DIR.
#ifdef CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_pc_linux_gnu #ifdef __linux__
if (target == "x86_64-pc-linux-gnu") { if (target.ends_with("-linux-gnu") || target.ends_with("-linux-musl")) {
out += " -DCMAKE_CXX_FLAGS=-stdlib=libc++"; out += " -DCMAKE_CXX_FLAGS=-stdlib=libc++";
out += " -DCMAKE_EXE_LINKER_FLAGS=-stdlib=libc++"; out += " -DCMAKE_EXE_LINKER_FLAGS=-stdlib=libc++";
out += " -DCMAKE_SHARED_LINKER_FLAGS=-stdlib=libc++"; out += " -DCMAKE_SHARED_LINKER_FLAGS=-stdlib=libc++";

View file

@ -8,7 +8,7 @@ module;
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
#include <limits.h> #include <limits.h>
#ifdef CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_pc_linux_gnu #ifdef __linux__
#include <unistd.h> #include <unistd.h>
#include <dlfcn.h> #include <dlfcn.h>
#include <sys/wait.h> #include <sys/wait.h>
@ -41,7 +41,7 @@ namespace {
// descriptor is close-on-exec / non-inheritable so spawned clang processes // descriptor is close-on-exec / non-inheritable so spawned clang processes
// don't keep the lock alive past our own release. // don't keep the lock alive past our own release.
class CacheLock { class CacheLock {
#ifdef CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_pc_linux_gnu #ifdef __linux__
std::int32_t fd_ = -1; std::int32_t fd_ = -1;
public: public:
explicit CacheLock(const fs::path& cacheDir) { explicit CacheLock(const fs::path& cacheDir) {
@ -765,7 +765,7 @@ Configuration Crafter::LoadProject(const fs::path& projectFile, std::span<const
} }
#endif #endif
#ifdef CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_pc_linux_gnu #ifdef __linux__
CommandResult Crafter::RunCommandChecked(std::string_view cmd) { CommandResult Crafter::RunCommandChecked(std::string_view cmd) {
std::array<char, 128> buffer; std::array<char, 128> buffer;
@ -796,7 +796,9 @@ CommandResult Crafter::RunCommandWithTimeout(std::string_view cmd, std::chrono::
std::array<char, 128> buffer; std::array<char, 128> buffer;
CommandResult result{}; CommandResult result{};
std::string wrapped = std::format("timeout --kill-after=2 {} {} 2>&1", timeout.count(), cmd); // -k (not --kill-after=): the short form is what GNU coreutils and busybox
// (Alpine) have in common.
std::string wrapped = std::format("timeout -k 2 {} {} 2>&1", timeout.count(), cmd);
FILE* pipe = popen(wrapped.c_str(), "r"); FILE* pipe = popen(wrapped.c_str(), "r");
if (!pipe) throw std::runtime_error("popen() failed!"); if (!pipe) throw std::runtime_error("popen() failed!");
@ -968,12 +970,12 @@ namespace {
continue; continue;
} }
std::string cmd = std::format( std::string cmd = std::format(
"clang++ --target=x86_64-pc-linux-gnu -march=native -mtune=native " "clang++ --target={} -march=native -mtune=native "
"-std=c++26 -stdlib=libc++ -O3 " "-std=c++26 -stdlib=libc++ -O3 "
"-Wno-reserved-identifier -Wno-reserved-module-identifier " "-Wno-reserved-identifier -Wno-reserved-module-identifier "
"-fprebuilt-module-path={} " "-fprebuilt-module-path={} "
"--precompile {} -o {}", "--precompile {} -o {}",
cacheDir.string(), cppmPath.string(), pcmPath.string()); HostTarget(), cacheDir.string(), cppmPath.string(), pcmPath.string());
CommandResult r = Crafter::RunCommandChecked(cmd); CommandResult r = Crafter::RunCommandChecked(cmd);
if (r.exitCode != 0) { if (r.exitCode != 0) {
throw std::runtime_error(std::format("Failed to precompile {} (exit {}): {}", name, r.exitCode, r.output)); throw std::runtime_error(std::format("Failed to precompile {} (exit {}): {}", name, r.exitCode, r.output));
@ -998,8 +1000,11 @@ Configuration Crafter::LoadProject(const fs::path& projectFile, std::span<const
fs::path sourceDir = Crafter::GetCrafterBuildHome(); fs::path sourceDir = Crafter::GetCrafterBuildHome();
// The host toolchain's own triple (x86_64-pc-linux-gnu on glibc distros,
// x86_64-alpine-linux-musl on Alpine): project.cpp and the module PCMs
// it imports must be built for the libc crafter-build itself runs on.
Configuration hostConfig; Configuration hostConfig;
hostConfig.target = "x86_64-pc-linux-gnu"; hostConfig.target = HostTarget();
hostConfig.march = "native"; hostConfig.march = "native";
hostConfig.mtune = "native"; hostConfig.mtune = "native";
fs::path cacheDir = GetCacheDir() / std::format("{}-{}", hostConfig.target, hostConfig.march); fs::path cacheDir = GetCacheDir() / std::format("{}-{}", hostConfig.target, hostConfig.march);
@ -1015,13 +1020,15 @@ Configuration Crafter::LoadProject(const fs::path& projectFile, std::span<const
const bool stale = !fs::exists(soPath) || fs::last_write_time(soPath) < fs::last_write_time(absProject) || fs::last_write_time(soPath) < fs::last_write_time(hostExe); const bool stale = !fs::exists(soPath) || fs::last_write_time(soPath) < fs::last_write_time(absProject) || fs::last_write_time(soPath) < fs::last_write_time(hostExe);
if (stale) { if (stale) {
// -fuse-ld=lld like every other link the tool performs: lld is a hard
// dependency, binutils' ld is not (Alpine's clang would default to it).
std::string compileCmd = std::format( std::string compileCmd = std::format(
"clang++ --target=x86_64-pc-linux-gnu -march=native -mtune=native " "clang++ --target={} -march=native -mtune=native "
"-std=c++26 -stdlib=libc++ -shared -fPIC -O3 " "-std=c++26 -stdlib=libc++ -shared -fPIC -O3 -fuse-ld=lld "
"-Wno-return-type-c-linkage " "-Wno-return-type-c-linkage "
"-fprebuilt-module-path={} " "-fprebuilt-module-path={} "
"{} -o {}", "{} -o {}",
cacheDir.string(), hostConfig.target, cacheDir.string(),
absProject.string(), soPath.string()); absProject.string(), soPath.string());
std::string result = RunBuildCommand(compileCmd); std::string result = RunBuildCommand(compileCmd);

View file

@ -32,6 +32,18 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
CrafterBuildLib->dependencies = { math, asset }; CrafterBuildLib->dependencies = { math, asset };
CrafterBuildLib->defines.push_back({"CRAFTER_BUILD_HAS_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));
}
}
{ {
std::array<fs::path, 11> interfaces = { std::array<fs::path, 11> interfaces = {
"interfaces/Crafter.Build", "interfaces/Crafter.Build",
@ -87,7 +99,9 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
std::array<fs::path, 1> implementations = { "implementations/main" }; std::array<fs::path, 1> implementations = { "implementations/main" };
cfg.GetInterfacesAndImplementations(interfaces, implementations); cfg.GetInterfacesAndImplementations(interfaces, implementations);
} }
if (cfg.target == "x86_64-pc-linux-gnu") { // Linux hosts, glibc or musl (Alpine): -ldl is a libc stub on musl, harmless.
const bool linuxTarget = cfg.target.ends_with("-linux-gnu") || cfg.target.ends_with("-linux-musl");
if (linuxTarget) {
cfg.linkFlags.push_back("-Wl,--export-dynamic"); cfg.linkFlags.push_back("-Wl,--export-dynamic");
cfg.linkFlags.push_back("-ldl"); cfg.linkFlags.push_back("-ldl");
} }
@ -102,7 +116,7 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
// links each test exe against CrafterBuildLib built from the local // links each test exe against CrafterBuildLib built from the local
// sources — so the code under test is whatever's in this checkout. // sources — so the code under test is whatever's in this checkout.
// Mirrors how downstream consumers link their own libraries into tests. // Mirrors how downstream consumers link their own libraries into tests.
if (cfg.target == "x86_64-pc-linux-gnu") { if (linuxTarget) {
cfg.AddTest("HelloWorld").Dependencies({ CrafterBuildLib.get() }); cfg.AddTest("HelloWorld").Dependencies({ CrafterBuildLib.get() });
cfg.AddTest("StaticLib").Dependencies({ CrafterBuildLib.get() }); cfg.AddTest("StaticLib").Dependencies({ CrafterBuildLib.get() });
cfg.AddTest("ModuleInterface").Dependencies({ CrafterBuildLib.get() }); cfg.AddTest("ModuleInterface").Dependencies({ CrafterBuildLib.get() });