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>
2026-10-02 22:26:31 +02:00
// 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 " ;
2026-10-03 16:37:00 +02:00
for ( const char * rel : { " bin/clang++ " , " bin/clang " , " bin/llvm-ar " , " lib/libclang.so " , " include/clang-c/Index.h " , " include/c++/v1/__config " , " share/libc++/v1/std.cppm " , " share/wasi-sysroot/include/.keep " } ) {
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>
2026-10-02 22:26:31 +02:00
Touch ( full / rel ) ;
}
2026-10-03 16:37:00 +02:00
// The prefix's libc++ headers state its release.
std : : ofstream ( full / " include/c++/v1/__config " ) < < " # define _LIBCPP_VERSION 230102 \n " ;
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>
2026-10-02 22:26:31 +02:00
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 " ) ;
2026-10-03 16:37:00 +02:00
Check ( tc . libcxxVersion = = LibcxxVersion ( " /usr/include/c++/v1 " ) , " the system libc++ headers state the release of the system module source " ) ;
Check ( tc . stdModulesDir . empty ( ) , " no crafter-build prefix, no shipped module sources " ) ;
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>
2026-10-02 22:26:31 +02:00
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 " ) ;
2026-10-03 16:37:00 +02:00
Check ( tc . libcxxVersion = = 230102 , " the prefix's libc++ headers state its release " ) ;
Check ( tc . stdModulesDir = = full / " share/crafter-build/libc++-modules " , " module sources for other releases are looked up in crafter-build's own share directory " ) ;
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>
2026-10-02 22:26:31 +02:00
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 " ) ;
2026-10-02 23:18:07 +02:00
Check ( Contains ( tc . LibcxxLinkFlags ( ) , std : : format ( " -L{} " , ( full / " lib " ) . string ( ) ) ) & & Contains ( tc . LibcxxLinkFlags ( ) , std : : format ( " -Wl,-rpath,{} " , ( full / " lib " ) . string ( ) ) ) , " binaries get -L and an rpath for the prefix's lib dir " ) ;
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>
2026-10-02 22:26:31 +02:00
}
{
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 " ) ;
2026-10-02 23:18:07 +02:00
Check ( Contains ( tc . LibcxxLinkFlags ( ) , std : : format ( " -L{} " , ( bare / " lib " ) . string ( ) ) ) & & Contains ( tc . LibcxxLinkFlags ( ) , std : : format ( " -Wl,-rpath,{} " , ( bare / " lib " ) . string ( ) ) ) , " the link flags follow the explicit prefix " ) ;
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>
2026-10-02 22:26:31 +02:00
}
{
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 " ;
2026-10-03 16:37:00 +02:00
fs : : create_directories ( libcxx / " include/c++/v1 " ) ;
std : : ofstream ( libcxx / " include/c++/v1/__config " ) < < " # define _LIBCPP_VERSION 210108 \n " ;
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>
2026-10-02 22:26:31 +02:00
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++ " ) ;
2026-10-03 16:37:00 +02:00
Check ( tc . libcxxVersion = = 210108 , " the release is read from the explicit libc++, not the compiler's " ) ;
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>
2026-10-02 22:26:31 +02:00
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 ;
}