2026-07-23 01:24:42 +02:00
// SPDX-License-Identifier: LGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
2026-07-22 18:25:39 +02:00
2026-04-27 07:04:42 +02:00
module ;
# include <stdio.h>
# include <stdlib.h>
# include <limits.h>
# include <unistd.h>
# include <dlfcn.h>
# include <sys/wait.h>
2026-05-30 19:28:06 +02:00
# include <sys/file.h>
# include <fcntl.h>
# include <errno.h>
2026-04-27 07:04:42 +02:00
module Crafter . Build : Platform_impl ;
2026-04-23 01:57:25 +02:00
import std ;
import : Platform ;
import : Clang ;
feat: project.cpp declares its own dependencies with #pragma crafter dependency
A project.cpp could only import std and Crafter.Build, so anything shared
between build descriptions had to be a header every consumer copies. Now
#pragma crafter dependency "<source>" [branch "<b>" | commit "<sha>"] [project "<file>"]
names a sibling crafter-build project that project.cpp itself imports a
module from. LoadProject scans for the directive, resolves it through
GitProject (a URL) or LocalProject (a path relative to the project file),
forces the result into host-tooling mode (the host triple LoadProject's
own cache is keyed on, native march, no debug, -fPIC so the archive links
into a shared object), builds it, and puts its BMIs and archive on the
project.so command line. The dependency's own project.cpp goes through
the same loader, so its directives resolve recursively; a cycle between
project files is an error instead of unbounded recursion.
The dependency is built on every load, fresh or not: clang crashes rather
than erroring when it imports a BMI whose own imported PCM was rebuilt
underneath it, and the module depfile — which Module::Check already reads —
is what notices the host PCM cache was regenerated. Its artifact mtime is
an input to project.so's staleness check, so editing the dependency
recompiles project.cpp without touching it. The Linux link gains
-fuse-ld=lld because release archives hold ThinLTO bitcode.
ProjectDependency(name) hands the same Configuration* back from inside
CrafterBuildProject, by configuration name or exported module name, so a
test that links the module needn't spell the source twice. It reads a
thread_local frame stack that an RAII guard keeps for the whole of
LoadProject, which also provides the cycle check.
ExpectedOutput() is the link gate's artifact-path lambda hoisted into the
:Clang partition, so the staleness check and the -r runner share the one
rule. Windows branches are wired identically but unverified.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-02 20:03:22 +02:00
import : Interface ;
import : External ;
2026-04-29 03:27:11 +02:00
import : Progress ;
2026-04-23 01:57:25 +02:00
namespace fs = std : : filesystem ;
using namespace Crafter ;
2026-05-30 16:36:45 +00:00
namespace {
2026-05-30 19:28:06 +02:00
// Exclusive advisory lock over one PCM cache dir (`<cache>/<target>-<march>/`),
// held for the duration of a (re)build. Concurrent crafter-build invocations —
// and the worker threads inside a single invocation — that share a cache
// serialize their builds through it: the first builder to acquire the lock
// precompiles std.pcm / the Crafter.Build-*.pcm modules while everyone else
// blocks, then re-checks freshness and reuses the finished file instead of
// re-running clang and racing to overwrite the same path. Each builder must
// pass through the lock before it reaches the compile steps that *read* those
// PCMs, so a reader never observes a half-written file.
//
// The lock lives in the OS, keyed on a `.lock` file in the cache dir, and is
// released automatically when the descriptor/handle closes — including on
// process death — so an interrupted build can never leave a stale lock. The
// descriptor is close-on-exec / non-inheritable so spawned clang processes
// don't keep the lock alive past our own release.
class CacheLock {
2026-07-23 01:24:42 +02:00
std : : int32_t fd_ = - 1 ;
2026-05-30 19:28:06 +02:00
public :
explicit CacheLock ( const fs : : path & cacheDir ) {
fd_ = : : open ( ( cacheDir / " .lock " ) . c_str ( ) , O_RDWR | O_CREAT | O_CLOEXEC , 0644 ) ;
if ( fd_ > = 0 ) {
while ( : : flock ( fd_ , LOCK_EX ) ! = 0 & & errno = = EINTR ) { }
}
}
~ CacheLock ( ) {
if ( fd_ > = 0 ) {
: : close ( fd_ ) ; // closing the descriptor drops the flock
}
}
CacheLock ( const CacheLock & ) = delete ;
CacheLock & operator = ( const CacheLock & ) = delete ;
} ;
fix: key the host PCM cache on source content, add clean, hash project args
Three follow-ons to the stale-build report, all cases of an identity not
capturing something that changes the output.
The host PCM cache under <cache>/crafter.build/<target>-<march>/ is shared by
every crafter-build on the machine, and freshness was a per-file mtime
comparison. That cannot tell "this PCM is newer than my source" from "this PCM
was built from different sources that happen to be newer", so a package install
and a working checkout — or two checkouts of different versions — silently
compiled their project.cpp against each other's declarations. Invalidation now
keys on a stamp over the bytes of every module source, which also covers the
case one file's mtime never could: the cached PCMs import each other, so a
change to :Interface invalidates :Clang's PCM with Crafter.Build-Clang.cppm
untouched.
Project args ApplyStandardArgs does not itself interpret are now folded into
VariantId. Such a flag typically decides what gets compiled or bundled — the
report's example is --no-webgpu dropping entries from cfg.files — and without it
both settings shared one bin dir and interleaved their outputs there, leaving a
bundle matching neither. Sorted and deduplicated so flag order doesn't split the
cache, and inherited by test Configurations.
`crafter-build clean` removes the project's bin/ and build/ trees. It
deliberately does not load project.cpp: cleaning is most often reached when
something is already wrong, and a clean that first needs the project to compile
is useless exactly then.
2026-07-30 17:43:09 +00:00
// The cached Crafter.Build PCMs are keyed by `<target>-<march>` alone, so
// every crafter-build on the machine writes to the same files regardless of
// which share/crafter-build its module sources came from. Freshness used to
// be a per-file mtime comparison, which cannot distinguish "this PCM is
// newer than my source" from "this PCM was built from *different* sources
// that happen to be newer" — so a second install or checkout silently
// compiled its project against the other's declarations. Same failure shape
// as issue #27: no error, a binary built against a layout nobody linked.
//
// A stamp over the bytes of every module source answers what the mtime
// can't. It covers the whole set rather than one file at a time because the
// PCMs import each other: a change to :Interface invalidates :Clang's PCM
// even though Crafter.Build-Clang.cppm is untouched.
std : : string CrafterBuildSourceStamp ( const fs : : path & sourceDir , std : : span < const std : : string_view > moduleNames ) {
std : : string all ;
auto append = [ & all , & sourceDir ] ( const fs : : path & relative ) {
std : : ifstream in ( sourceDir / relative , std : : ios : : binary ) ;
std : : ostringstream buffer ;
buffer < < in . rdbuf ( ) ;
all + = relative . string ( ) ;
all + = ' \0 ' ;
all + = buffer . str ( ) ;
all + = ' \0 ' ;
} ;
for ( std : : string_view name : moduleNames ) {
append ( fs : : path ( std : : format ( " {}.cppm " , name ) ) ) ;
}
return std : : format ( " {:016x} " , std : : hash < std : : string > { } ( all ) ) ;
}
fs : : path CacheStampPath ( const fs : : path & cacheDir ) {
return cacheDir / " crafter-build-sources.stamp " ;
}
std : : string ReadCacheStamp ( const fs : : path & cacheDir ) {
std : : ifstream in ( CacheStampPath ( cacheDir ) , std : : ios : : binary ) ;
std : : string stamp ;
std : : getline ( in , stamp ) ;
return stamp ;
}
void WriteCacheStamp ( const fs : : path & cacheDir , const std : : string & stamp ) {
std : : ofstream ( CacheStampPath ( cacheDir ) , std : : ios : : binary | std : : ios : : trunc ) < < stamp < < ' \n ' ;
}
2026-05-30 16:36:45 +00:00
}
V2: WASI, -r flag, CI pipeline, examples & tests cleanup
WASI / wasm32 target support
- Auto-detect /usr/share/wasi-sysroot on Linux when target starts_with("wasm32")
- Skip -march/-mtune for wasm (clang rejects them)
- Apply -fno-exceptions -fno-c++-static-destructors -mllvm -wasm-enable-sjlj
-D_WASI_EMULATED_SIGNAL to wasm builds (compile + std PCM, kept in sync)
- .wasm output extension in expectedOutputFor and link command
- EnableWasiBrowserRuntime(cfg): opt-in helper that drops index.html +
runtime.js next to the .wasm; runtime.js reads window.CRAFTER_WASM_URL
set in the templated index.html so a single shim handles any output name
-r run flag in the CLI: build then exec the artifact (host targets only;
rejects libraries; auto .exe/.wasm extension handling)
CI pipeline (.forgejo/workflows/ci.yaml)
- Triggers: PR/push to master + manual dispatch
- Single arch-latest container job: install deps, bootstrap, self-rebuild,
run tests, cross-compile mingw, package both archives, upload artifacts
- Rolling 'latest' release published only on push/dispatch to master
mingw cross-compile from Linux now works end-to-end:
- ExternalDependency cache key includes target so per-target glslang builds
don't collide; CMAKE_BUILD_TYPE=Release pinned (otherwise glslang appends
'd' to lib names and breaks linking); cross-compile cmake flags
(CMAKE_SYSTEM_NAME=Windows, CMAKE_*_COMPILER_TARGET=...)
- project.cpp accepts --target=<triple>; Linux-only -Wl,--export-dynamic
and -ldl are gated; mingw glslang skips the standalone exe (its libgcc_eh
link pulls pthread which mingw doesn't link by default)
- mingw compile uses -femulated-tls so std::__once_callable etc reference
the same emutls symbols libstdc++ provides
- mingw link auto-adds -lstdc++exp -lpthread
GetCrafterBuildHome() exposed from the Platform module; LoadProject (Linux
+ Windows) now both use it instead of duplicating the resolution.
Examples reorg: hello-world, library, with-module, wasi, tests — each with
its own README. Tests reorg: per-test directory with inner/ fixture, no
shared tests/fixtures/ tree. New Wasi test verifies .wasm magic bytes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 23:24:46 +02:00
fs : : path Crafter : : GetCrafterBuildHome ( ) {
if ( const char * envHome = std : : getenv ( " CRAFTER_BUILD_HOME " ) ) {
return fs : : path ( envHome ) ;
}
fs : : path hostExe = fs : : read_symlink ( " /proc/self/exe " ) ;
2026-04-29 03:00:53 +02:00
// Walk up from the exe's directory looking for share/crafter-build with
// a known module source present as a sentinel. Handles:
// <prefix>/bin/crafter-build -> <prefix>/share/crafter-build (FHS install)
// <repo>/bin/crafter-build -> <repo>/share/crafter-build (build.sh)
// <repo>/bin/<arch>/crafter-build -> <repo>/share/crafter-build (self-host)
fs : : path dir = hostExe . parent_path ( ) ;
std : : vector < fs : : path > tried ;
for ( ; ; ) {
fs : : path candidate = dir / " share " / " crafter-build " ;
tried . push_back ( candidate ) ;
std : : error_code ec ;
if ( fs : : exists ( candidate / " Crafter.Build.cppm " , ec ) ) {
return candidate ;
}
fs : : path parent = dir . parent_path ( ) ;
if ( parent = = dir ) break ;
dir = parent ;
}
2026-07-23 01:24:42 +02:00
std : : string msg = std : : format ( " could not locate crafter-build runtime assets relative to {} (set CRAFTER_BUILD_HOME). Tried: " , hostExe . string ( ) ) ;
2026-04-29 03:00:53 +02:00
for ( const auto & p : tried ) {
2026-07-23 01:24:42 +02:00
msg + = std : : format ( " \n {} " , p . string ( ) ) ;
2026-04-29 03:00:53 +02:00
}
throw std : : runtime_error ( msg ) ;
V2: WASI, -r flag, CI pipeline, examples & tests cleanup
WASI / wasm32 target support
- Auto-detect /usr/share/wasi-sysroot on Linux when target starts_with("wasm32")
- Skip -march/-mtune for wasm (clang rejects them)
- Apply -fno-exceptions -fno-c++-static-destructors -mllvm -wasm-enable-sjlj
-D_WASI_EMULATED_SIGNAL to wasm builds (compile + std PCM, kept in sync)
- .wasm output extension in expectedOutputFor and link command
- EnableWasiBrowserRuntime(cfg): opt-in helper that drops index.html +
runtime.js next to the .wasm; runtime.js reads window.CRAFTER_WASM_URL
set in the templated index.html so a single shim handles any output name
-r run flag in the CLI: build then exec the artifact (host targets only;
rejects libraries; auto .exe/.wasm extension handling)
CI pipeline (.forgejo/workflows/ci.yaml)
- Triggers: PR/push to master + manual dispatch
- Single arch-latest container job: install deps, bootstrap, self-rebuild,
run tests, cross-compile mingw, package both archives, upload artifacts
- Rolling 'latest' release published only on push/dispatch to master
mingw cross-compile from Linux now works end-to-end:
- ExternalDependency cache key includes target so per-target glslang builds
don't collide; CMAKE_BUILD_TYPE=Release pinned (otherwise glslang appends
'd' to lib names and breaks linking); cross-compile cmake flags
(CMAKE_SYSTEM_NAME=Windows, CMAKE_*_COMPILER_TARGET=...)
- project.cpp accepts --target=<triple>; Linux-only -Wl,--export-dynamic
and -ldl are gated; mingw glslang skips the standalone exe (its libgcc_eh
link pulls pthread which mingw doesn't link by default)
- mingw compile uses -femulated-tls so std::__once_callable etc reference
the same emutls symbols libstdc++ provides
- mingw link auto-adds -lstdc++exp -lpthread
GetCrafterBuildHome() exposed from the Platform module; LoadProject (Linux
+ Windows) now both use it instead of duplicating the resolution.
Examples reorg: hello-world, library, with-module, wasi, tests — each with
its own README. Tests reorg: per-test directory with inner/ fixture, no
shared tests/fixtures/ tree. New Wasi test verifies .wasm magic bytes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 23:24:46 +02:00
}
2026-04-23 01:57:25 +02:00
2026-07-31 11:11:38 +00:00
fs : : file_time_type Crafter : : NewestPrerequisite ( const fs : : path & depFile ) {
std : : ifstream in ( depFile , std : : ios : : binary ) ;
if ( ! in ) return fs : : file_time_type : : max ( ) ;
std : : ostringstream buffer ;
buffer < < in . rdbuf ( ) ;
const std : : string text = std : : move ( buffer ) . str ( ) ;
// Make-rule tokenizing, as clang writes it: `target: prereq prereq`, with a
// trailing '\' continuing the rule onto the next line (clang wraps at ~80
// columns, so any real depfile has several), '\ ' and '\#' escaping a
// character into a filename, and '$$' standing for a literal '$'. A
// backslash that is none of those is a Windows path separator and stays.
std : : vector < std : : string > tokens ;
std : : string token ;
auto flush = [ & tokens , & token ] ( ) {
if ( ! token . empty ( ) ) {
tokens . push_back ( token ) ;
token . clear ( ) ;
}
} ;
for ( std : : size_t i = 0 ; i < text . size ( ) ; + + i ) {
const char c = text [ i ] ;
const char next = i + 1 < text . size ( ) ? text [ i + 1 ] : ' \0 ' ;
if ( c = = ' \\ ' & & ( next = = ' \n ' | | next = = ' \r ' ) ) {
flush ( ) ;
+ + i ;
} else if ( c = = ' \\ ' & & ( next = = ' ' | | next = = ' \t ' | | next = = ' # ' ) ) {
token + = next ;
+ + i ;
} else if ( c = = ' $ ' & & next = = ' $ ' ) {
token + = ' $ ' ;
+ + i ;
} else if ( c = = ' ' | | c = = ' \t ' | | c = = ' \n ' | | c = = ' \r ' ) {
flush ( ) ;
} else {
token + = c ;
}
}
flush ( ) ;
// A token ending in ':' names a rule target, not an input. The first one
// closes the target list (and a Windows `C:\build\x.o:` still ends in ':',
// where splitting on the first colon would not); a later one opens another
// rule, as `-MP` phony targets do, whose name is not a file to stat.
bool sawTarget = false ;
fs : : file_time_type newest = fs : : file_time_type : : min ( ) ;
for ( const std : : string & prerequisite : tokens ) {
if ( prerequisite . ends_with ( ' : ' ) ) {
sawTarget = true ;
continue ;
}
if ( ! sawTarget ) continue ;
std : : error_code ec ;
const fs : : file_time_type stamp = fs : : last_write_time ( prerequisite , ec ) ;
// A prerequisite that has since been deleted makes the recorded set
// unusable as evidence — the source that included it must have changed
// too, and rebuilding is what writes a depfile that no longer names it.
if ( ec ) return fs : : file_time_type : : max ( ) ;
if ( stamp > newest ) newest = stamp ;
}
// No rule at all: a truncated or empty depfile proves nothing.
if ( ! sawTarget ) return fs : : file_time_type : : max ( ) ;
return newest ;
}
2026-07-23 01:24:42 +02:00
bool Crafter : : MatchGlob ( std : : string_view glob , std : : string_view name ) {
std : : size_t gi = 0 ;
std : : size_t ni = 0 ;
std : : size_t star = std : : string_view : : npos ;
std : : size_t mark = 0 ;
while ( ni < name . size ( ) ) {
if ( gi < glob . size ( ) & & ( glob [ gi ] = = ' ? ' | | glob [ gi ] = = name [ ni ] ) ) {
+ + gi ; + + ni ;
} else if ( gi < glob . size ( ) & & glob [ gi ] = = ' * ' ) {
star = gi + + ;
mark = ni ;
} else if ( star ! = std : : string_view : : npos ) {
gi = star + 1 ;
ni = + + mark ;
} else {
return false ;
}
}
while ( gi < glob . size ( ) & & glob [ gi ] = = ' * ' ) + + gi ;
return gi = = glob . size ( ) ;
}
bool Crafter : : MatchAny ( std : : span < const std : : string > globs , std : : string_view name ) {
if ( globs . empty ( ) ) return true ;
for ( const auto & g : globs ) {
if ( MatchGlob ( g , name ) ) return true ;
}
return false ;
}
fix: decide a build step by its exit code, not by whether it printed
RunCommand merged stderr into stdout, dropped pclose's status and returned
the text; every compile, link and archive site then read "printed something"
as "failed". A warning is printing something, so a translation unit that
warned failed the build — but only on the run that actually recompiled it,
since warnings aren't re-emitted for an object that's already up to date.
The same unchanged source therefore passed or failed depending on the state
of the build tree: flaky-looking tests locally, and a cold CI checkout
surfacing every latent warning in a project at once as unrelated failures.
RunCommand is gone, replaced by RunBuildCommand: it goes through
RunCommandChecked, returns "" when the command exited 0 (so every caller's
`if (!result.empty())` error path is unchanged) and hands any warnings to
the new Progress::Diagnostic instead of to the error path. That also closes
the quiet half of the bug — a compiler killed by the OOM killer prints
nothing, so it used to read as success and leave the build carrying on with
a missing object; it now reports the signal that killed it.
Warnings are now shown rather than swallowed, which they weren't in either
direction before: invisible on an incremental build, fatal on a cold one.
Failing on them stays a project's choice, via -Werror in compileFlags.
2026-08-26 00:51:15 +00:00
// See the declaration in Crafter.Build-Platform.cppm for why this branches on
// the exit code rather than on whether the command said anything.
std : : string Crafter : : RunBuildCommand ( std : : string_view cmd ) {
2026-04-29 03:27:11 +02:00
Progress : : EchoCommand ( cmd ) ;
fix: decide a build step by its exit code, not by whether it printed
RunCommand merged stderr into stdout, dropped pclose's status and returned
the text; every compile, link and archive site then read "printed something"
as "failed". A warning is printing something, so a translation unit that
warned failed the build — but only on the run that actually recompiled it,
since warnings aren't re-emitted for an object that's already up to date.
The same unchanged source therefore passed or failed depending on the state
of the build tree: flaky-looking tests locally, and a cold CI checkout
surfacing every latent warning in a project at once as unrelated failures.
RunCommand is gone, replaced by RunBuildCommand: it goes through
RunCommandChecked, returns "" when the command exited 0 (so every caller's
`if (!result.empty())` error path is unchanged) and hands any warnings to
the new Progress::Diagnostic instead of to the error path. That also closes
the quiet half of the bug — a compiler killed by the OOM killer prints
nothing, so it used to read as success and leave the build carrying on with
a missing object; it now reports the signal that killed it.
Warnings are now shown rather than swallowed, which they weren't in either
direction before: invisible on an incremental build, fatal on a cold one.
Failing on them stays a project's choice, via -Werror in compileFlags.
2026-08-26 00:51:15 +00:00
CommandResult r = RunCommandChecked ( cmd ) ;
2026-04-23 01:57:25 +02:00
fix: decide a build step by its exit code, not by whether it printed
RunCommand merged stderr into stdout, dropped pclose's status and returned
the text; every compile, link and archive site then read "printed something"
as "failed". A warning is printing something, so a translation unit that
warned failed the build — but only on the run that actually recompiled it,
since warnings aren't re-emitted for an object that's already up to date.
The same unchanged source therefore passed or failed depending on the state
of the build tree: flaky-looking tests locally, and a cold CI checkout
surfacing every latent warning in a project at once as unrelated failures.
RunCommand is gone, replaced by RunBuildCommand: it goes through
RunCommandChecked, returns "" when the command exited 0 (so every caller's
`if (!result.empty())` error path is unchanged) and hands any warnings to
the new Progress::Diagnostic instead of to the error path. That also closes
the quiet half of the bug — a compiler killed by the OOM killer prints
nothing, so it used to read as success and leave the build carrying on with
a missing object; it now reports the signal that killed it.
Warnings are now shown rather than swallowed, which they weren't in either
direction before: invisible on an incremental build, fatal on a cold one.
Failing on them stays a project's choice, via -Werror in compileFlags.
2026-08-26 00:51:15 +00:00
if ( r . exitCode = = 0 ) {
// Warnings from a step that succeeded. Show them — until now they were
// invisible on an incremental build and fatal on a cold one.
if ( ! r . output . empty ( ) ) Progress : : Diagnostic ( r . output ) ;
return " " ;
2026-04-23 01:57:25 +02:00
}
fix: decide a build step by its exit code, not by whether it printed
RunCommand merged stderr into stdout, dropped pclose's status and returned
the text; every compile, link and archive site then read "printed something"
as "failed". A warning is printing something, so a translation unit that
warned failed the build — but only on the run that actually recompiled it,
since warnings aren't re-emitted for an object that's already up to date.
The same unchanged source therefore passed or failed depending on the state
of the build tree: flaky-looking tests locally, and a cold CI checkout
surfacing every latent warning in a project at once as unrelated failures.
RunCommand is gone, replaced by RunBuildCommand: it goes through
RunCommandChecked, returns "" when the command exited 0 (so every caller's
`if (!result.empty())` error path is unchanged) and hands any warnings to
the new Progress::Diagnostic instead of to the error path. That also closes
the quiet half of the bug — a compiler killed by the OOM killer prints
nothing, so it used to read as success and leave the build carrying on with
a missing object; it now reports the signal that killed it.
Warnings are now shown rather than swallowed, which they weren't in either
direction before: invisible on an incremental build, fatal on a cold one.
Failing on them stays a project's choice, via -Werror in compileFlags.
2026-08-26 00:51:15 +00:00
// A compiler killed outright (the OOM killer is the common one) prints
// nothing, so the old "output means failure" rule read it as success and
// let the build carry on with a missing object. Say what happened instead.
if ( r . output . empty ( ) ) {
return r . crashed
? std : : format ( " terminated by signal {}: {} " , r . signal , cmd )
: std : : format ( " exited with code {}: {} " , r . exitCode , cmd ) ;
2026-04-23 01:57:25 +02:00
}
fix: decide a build step by its exit code, not by whether it printed
RunCommand merged stderr into stdout, dropped pclose's status and returned
the text; every compile, link and archive site then read "printed something"
as "failed". A warning is printing something, so a translation unit that
warned failed the build — but only on the run that actually recompiled it,
since warnings aren't re-emitted for an object that's already up to date.
The same unchanged source therefore passed or failed depending on the state
of the build tree: flaky-looking tests locally, and a cold CI checkout
surfacing every latent warning in a project at once as unrelated failures.
RunCommand is gone, replaced by RunBuildCommand: it goes through
RunCommandChecked, returns "" when the command exited 0 (so every caller's
`if (!result.empty())` error path is unchanged) and hands any warnings to
the new Progress::Diagnostic instead of to the error path. That also closes
the quiet half of the bug — a compiler killed by the OOM killer prints
nothing, so it used to read as success and leave the build carrying on with
a missing object; it now reports the signal that killed it.
Warnings are now shown rather than swallowed, which they weren't in either
direction before: invisible on an incremental build, fatal on a cold one.
Failing on them stays a project's choice, via -Werror in compileFlags.
2026-08-26 00:51:15 +00:00
if ( r . crashed ) {
return std : : format ( " {} \n terminated by signal {} " , r . output , r . signal ) ;
}
return r . output ;
2026-04-23 01:57:25 +02:00
}
feat: project.cpp declares its own dependencies with #pragma crafter dependency
A project.cpp could only import std and Crafter.Build, so anything shared
between build descriptions had to be a header every consumer copies. Now
#pragma crafter dependency "<source>" [branch "<b>" | commit "<sha>"] [project "<file>"]
names a sibling crafter-build project that project.cpp itself imports a
module from. LoadProject scans for the directive, resolves it through
GitProject (a URL) or LocalProject (a path relative to the project file),
forces the result into host-tooling mode (the host triple LoadProject's
own cache is keyed on, native march, no debug, -fPIC so the archive links
into a shared object), builds it, and puts its BMIs and archive on the
project.so command line. The dependency's own project.cpp goes through
the same loader, so its directives resolve recursively; a cycle between
project files is an error instead of unbounded recursion.
The dependency is built on every load, fresh or not: clang crashes rather
than erroring when it imports a BMI whose own imported PCM was rebuilt
underneath it, and the module depfile — which Module::Check already reads —
is what notices the host PCM cache was regenerated. Its artifact mtime is
an input to project.so's staleness check, so editing the dependency
recompiles project.cpp without touching it. The Linux link gains
-fuse-ld=lld because release archives hold ThinLTO bitcode.
ProjectDependency(name) hands the same Configuration* back from inside
CrafterBuildProject, by configuration name or exported module name, so a
test that links the module needn't spell the source twice. It reads a
thread_local frame stack that an RAII guard keeps for the whole of
LoadProject, which also provides the cycle check.
ExpectedOutput() is the link gate's artifact-path lambda hoisted into the
:Clang partition, so the staleness check and the -r runner share the one
rule. Windows branches are wired identically but unverified.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-02 20:03:22 +02:00
// --- Dependencies of project.cpp itself -------------------------------------
//
// #pragma crafter dependency "<source>" [branch "<b>" | commit "<sha>"] [project "<file>"]
//
// names a sibling crafter-build project that project.cpp imports a module
// from. Anything a file needs before it can compile cannot be C++ inside that
// file, so this is a directive next to the `import` it enables — where
// `#include` lives. Each one is fetched, built for the host, and put on
// project.so's module path and link line before project.cpp is compiled. The
// dependency's own project.cpp goes through LoadProject, so its pragmas
// resolve recursively.
namespace {
struct PragmaDependency {
std : : string source ;
std : : string branch ;
std : : string commit ;
std : : string project ;
std : : size_t line = 0 ;
} ;
[[noreturn]] void PragmaError ( const fs : : path & file , std : : size_t line , std : : string_view message ) {
throw std : : runtime_error ( std : : format ( " {}:{}: {} " , file . string ( ) , line , message ) ) ;
}
// The tokens of one directive: bare words and double-quoted strings (no
// escapes — a path or URL never needs one). Reading stops at `//`.
class PragmaCursor {
public :
PragmaCursor ( std : : string_view text , const fs : : path & file , std : : size_t line ) : text_ ( text ) , file_ ( file ) , line_ ( line ) { }
bool AtEnd ( ) {
SkipSpace ( ) ;
return pos_ > = text_ . size ( ) | | text_ . substr ( pos_ ) . starts_with ( " // " ) ;
}
bool Take ( char c ) {
SkipSpace ( ) ;
if ( pos_ > = text_ . size ( ) | | text_ [ pos_ ] ! = c ) return false ;
+ + pos_ ;
return true ;
}
std : : string_view Word ( ) {
SkipSpace ( ) ;
const std : : size_t start = pos_ ;
while ( pos_ < text_ . size ( ) & & IsWordChar ( text_ [ pos_ ] ) ) + + pos_ ;
return text_ . substr ( start , pos_ - start ) ;
}
std : : optional < std : : string_view > Quoted ( ) {
SkipSpace ( ) ;
if ( pos_ > = text_ . size ( ) | | text_ [ pos_ ] ! = ' " ' ) return std : : nullopt ;
const std : : size_t close = text_ . find ( ' " ' , pos_ + 1 ) ;
if ( close = = std : : string_view : : npos ) Fail ( " unterminated string " ) ;
const std : : string_view inner = text_ . substr ( pos_ + 1 , close - pos_ - 1 ) ;
pos_ = close + 1 ;
return inner ;
}
[[noreturn]] void Fail ( std : : string_view message ) const { PragmaError ( file_ , line_ , message ) ; }
private :
static bool IsWordChar ( char c ) { return ( c > = ' a ' & & c < = ' z ' ) | | ( c > = ' A ' & & c < = ' Z ' ) | | ( c > = ' 0 ' & & c < = ' 9 ' ) | | c = = ' _ ' ; }
void SkipSpace ( ) {
while ( pos_ < text_ . size ( ) & & ( text_ [ pos_ ] = = ' ' | | text_ [ pos_ ] = = ' \t ' | | text_ [ pos_ ] = = ' \r ' ) ) + + pos_ ;
}
std : : string_view text_ ;
const fs : : path & file_ ;
std : : size_t line_ ;
std : : size_t pos_ = 0 ;
} ;
// One directive per line, anywhere in the file. Comments aren't parsed:
// a directive inside a block comment is honoured, and `//` lines fall out
// because they don't start with `#`.
std : : vector < PragmaDependency > ScanProjectFilePragmas ( const fs : : path & absProject ) {
std : : vector < PragmaDependency > found ;
std : : ifstream in ( absProject ) ;
std : : string raw ;
for ( std : : size_t lineNo = 1 ; std : : getline ( in , raw ) ; + + lineNo ) {
PragmaCursor cur ( raw , absProject , lineNo ) ;
if ( ! cur . Take ( ' # ' ) | | cur . Word ( ) ! = " pragma " | | cur . Word ( ) ! = " crafter " ) continue ;
const std : : string_view verb = cur . Word ( ) ;
if ( verb ! = " dependency " ) cur . Fail ( std : : format ( " unknown crafter pragma '{}' " , verb ) ) ;
PragmaDependency dep ;
dep . line = lineNo ;
const std : : optional < std : : string_view > source = cur . Quoted ( ) ;
if ( ! source ) cur . Fail ( " expected a quoted source after 'dependency' " ) ;
dep . source = * source ;
while ( ! cur . AtEnd ( ) ) {
const std : : string_view key = cur . Word ( ) ;
std : : string * field = key = = " branch " ? & dep . branch : key = = " commit " ? & dep . commit : key = = " project " ? & dep . project : nullptr ;
if ( ! field ) cur . Fail ( key . empty ( ) ? " unexpected text after the source " : std : : format ( " unknown keyword '{}' (expected branch, commit or project) " , key ) ) ;
if ( ! field - > empty ( ) ) cur . Fail ( std : : format ( " '{}' given twice " , key ) ) ;
const std : : optional < std : : string_view > value = cur . Quoted ( ) ;
if ( ! value | | value - > empty ( ) ) cur . Fail ( std : : format ( " expected a quoted value after '{}' " , key ) ) ;
* field = * value ;
}
if ( dep . source . empty ( ) ) cur . Fail ( " the source is empty " ) ;
if ( ! dep . branch . empty ( ) & & ! dep . commit . empty ( ) ) cur . Fail ( " 'branch' and 'commit' are mutually exclusive " ) ;
found . push_back ( std : : move ( dep ) ) ;
}
return found ;
}
struct ProjectFileFrame {
fs : : path project ;
std : : vector < Configuration * > dependencies ;
} ;
// LoadProject nests: a dependency's project.cpp loads inside its
// consumer's pre-pass, and GitProject inside CrafterBuildProject nests
// again. Several threads may also load at once (ConcurrentCacheRace), so
// the stack is per thread. ProjectDependency() reads the top frame.
thread_local std : : vector < ProjectFileFrame > LoadingFrames ;
// Held for the whole of one LoadProject call. Pushing a file that is
// already on the stack is a cycle, which used to recurse forever.
class ProjectFileGuard {
public :
explicit ProjectFileGuard ( const fs : : path & absProject ) {
for ( const ProjectFileFrame & frame : LoadingFrames ) {
if ( frame . project ! = absProject ) continue ;
std : : string chain ;
for ( const ProjectFileFrame & f : LoadingFrames ) chain + = std : : format ( " {} -> " , f . project . string ( ) ) ;
throw std : : runtime_error ( std : : format ( " circular project dependency: {}{} " , chain , absProject . string ( ) ) ) ;
}
index_ = LoadingFrames . size ( ) ;
LoadingFrames . push_back ( { absProject , { } } ) ;
}
~ ProjectFileGuard ( ) { LoadingFrames . pop_back ( ) ; }
ProjectFileGuard ( const ProjectFileGuard & ) = delete ;
ProjectFileGuard & operator = ( const ProjectFileGuard & ) = delete ;
ProjectFileFrame & Frame ( ) { return LoadingFrames [ index_ ] ; }
private :
std : : size_t index_ = 0 ;
} ;
// A pre-dependency is host tooling: project.so is loaded into the running
// crafter-build, so the dependency is built for the host and against the
Merge branch 'claude/project-dependencies': project.cpp dependencies, AGENTS.md
Brings in `#pragma crafter dependency`, the switch to Crafter.LintRules
for the house style, and the contributor guide in AGENTS.md.
The branch predates the musl host work on master, and both rewrote the
command that compiles project.so, so the merge reconciles them by hand:
- LoadProject compiles project.so for hostConfig.target (HostTarget(),
from master) with the pre-dependency module path and archives from
the branch. -fuse-ld=lld appears once, with both reasons in the
comment.
- ForceHostTooling added -fPIC only for the literal x86_64-pc-linux-gnu
triple. hostConfig now comes from HostTarget(), so on Alpine the host
is x86_64-alpine-linux-musl and the dependency archive would not have
been position-independent. It now matches any *-linux-gnu or
*-linux-musl host, as project.cpp and the cmake flags do since the
musl change, and its comment no longer argues against HostTarget().
- The ProjectDependency test compared the dependency's target against
the glibc triple, which the musl CI job would fail; it compares
against HostTarget().
- README keeps master's paragraph on the bootstrap triple. AGENTS.md
describes the test block as `if (linuxTarget)`, which is what
project.cpp has now.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-02 20:47:23 +02:00
// same PCM cache LoadProject populates — the hostConfig LoadProject itself
// compiles project.so with, so the dependency's std/Crafter.Build PCMs
// land in the cache dir project.so is built against. Recursive, because
// the dependency's project.cpp forwards no arguments but still reads
// CRAFTER_BUILD_MARCH from the environment for its own deps.
feat: project.cpp declares its own dependencies with #pragma crafter dependency
A project.cpp could only import std and Crafter.Build, so anything shared
between build descriptions had to be a header every consumer copies. Now
#pragma crafter dependency "<source>" [branch "<b>" | commit "<sha>"] [project "<file>"]
names a sibling crafter-build project that project.cpp itself imports a
module from. LoadProject scans for the directive, resolves it through
GitProject (a URL) or LocalProject (a path relative to the project file),
forces the result into host-tooling mode (the host triple LoadProject's
own cache is keyed on, native march, no debug, -fPIC so the archive links
into a shared object), builds it, and puts its BMIs and archive on the
project.so command line. The dependency's own project.cpp goes through
the same loader, so its directives resolve recursively; a cycle between
project files is an error instead of unbounded recursion.
The dependency is built on every load, fresh or not: clang crashes rather
than erroring when it imports a BMI whose own imported PCM was rebuilt
underneath it, and the module depfile — which Module::Check already reads —
is what notices the host PCM cache was regenerated. Its artifact mtime is
an input to project.so's staleness check, so editing the dependency
recompiles project.cpp without touching it. The Linux link gains
-fuse-ld=lld because release archives hold ThinLTO bitcode.
ProjectDependency(name) hands the same Configuration* back from inside
CrafterBuildProject, by configuration name or exported module name, so a
test that links the module needn't spell the source twice. It reads a
thread_local frame stack that an RAII guard keeps for the whole of
LoadProject, which also provides the cycle check.
ExpectedOutput() is the link gate's artifact-path lambda hoisted into the
:Clang partition, so the staleness check and the -r runner share the one
rule. Windows branches are wired identically but unverified.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-02 20:03:22 +02:00
void ForceHostTooling ( Configuration & cfg , const Configuration & host , std : : unordered_set < Configuration * > & seen ) {
if ( ! seen . insert ( & cfg ) . second ) return ;
cfg . target = host . target ;
cfg . march = host . march ;
cfg . mtune = host . mtune ;
cfg . debug = false ;
// project.so is a shared object, so the archive it links has to be
// position-independent; static libraries aren't by default. Part of
// VariantId, so this build never shares a bin dir with a normal one.
// Checked before pushing: the same Configuration comes back from the
Merge branch 'claude/project-dependencies': project.cpp dependencies, AGENTS.md
Brings in `#pragma crafter dependency`, the switch to Crafter.LintRules
for the house style, and the contributor guide in AGENTS.md.
The branch predates the musl host work on master, and both rewrote the
command that compiles project.so, so the merge reconciles them by hand:
- LoadProject compiles project.so for hostConfig.target (HostTarget(),
from master) with the pre-dependency module path and archives from
the branch. -fuse-ld=lld appears once, with both reasons in the
comment.
- ForceHostTooling added -fPIC only for the literal x86_64-pc-linux-gnu
triple. hostConfig now comes from HostTarget(), so on Alpine the host
is x86_64-alpine-linux-musl and the dependency archive would not have
been position-independent. It now matches any *-linux-gnu or
*-linux-musl host, as project.cpp and the cmake flags do since the
musl change, and its comment no longer argues against HostTarget().
- The ProjectDependency test compared the dependency's target against
the glibc triple, which the musl CI job would fail; it compares
against HostTarget().
- README keeps master's paragraph on the bootstrap triple. AGENTS.md
describes the test block as `if (linuxTarget)`, which is what
project.cpp has now.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-02 20:47:23 +02:00
// project cache for every consumer in this process. Linux hosts only,
// glibc or musl: Windows targets have no use for the flag.
const bool linuxHost = host . target . ends_with ( " -linux-gnu " ) | | host . target . ends_with ( " -linux-musl " ) ;
if ( linuxHost & & ! std : : ranges : : contains ( cfg . compileFlags , " -fPIC " ) ) {
feat: project.cpp declares its own dependencies with #pragma crafter dependency
A project.cpp could only import std and Crafter.Build, so anything shared
between build descriptions had to be a header every consumer copies. Now
#pragma crafter dependency "<source>" [branch "<b>" | commit "<sha>"] [project "<file>"]
names a sibling crafter-build project that project.cpp itself imports a
module from. LoadProject scans for the directive, resolves it through
GitProject (a URL) or LocalProject (a path relative to the project file),
forces the result into host-tooling mode (the host triple LoadProject's
own cache is keyed on, native march, no debug, -fPIC so the archive links
into a shared object), builds it, and puts its BMIs and archive on the
project.so command line. The dependency's own project.cpp goes through
the same loader, so its directives resolve recursively; a cycle between
project files is an error instead of unbounded recursion.
The dependency is built on every load, fresh or not: clang crashes rather
than erroring when it imports a BMI whose own imported PCM was rebuilt
underneath it, and the module depfile — which Module::Check already reads —
is what notices the host PCM cache was regenerated. Its artifact mtime is
an input to project.so's staleness check, so editing the dependency
recompiles project.cpp without touching it. The Linux link gains
-fuse-ld=lld because release archives hold ThinLTO bitcode.
ProjectDependency(name) hands the same Configuration* back from inside
CrafterBuildProject, by configuration name or exported module name, so a
test that links the module needn't spell the source twice. It reads a
thread_local frame stack that an RAII guard keeps for the whole of
LoadProject, which also provides the cycle check.
ExpectedOutput() is the link gate's artifact-path lambda hoisted into the
:Clang partition, so the staleness check and the -r runner share the one
rule. Windows branches are wired identically but unverified.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-02 20:03:22 +02:00
cfg . compileFlags . emplace_back ( " -fPIC " ) ;
}
for ( Configuration * dep : cfg . dependencies ) ForceHostTooling ( * dep , host , seen ) ;
}
struct ProjectFileDependencies {
std : : vector < Configuration * > configs ;
std : : string compileFlags ; // " -fprebuilt-module-path=…" for each dependency and its transitive deps
std : : string linkFlags ; // every dependency's BuildResult.libs, verbatim
fs : : file_time_type newestArtifact = fs : : file_time_type : : min ( ) ;
} ;
bool IsGitSource ( std : : string_view source ) {
return source . contains ( " :// " ) | | source . starts_with ( " git@ " ) ;
}
ProjectFileDependencies ResolveProjectFileDependencies ( const fs : : path & absProject , const Configuration & host ) {
ProjectFileDependencies out ;
const std : : vector < PragmaDependency > pragmas = ScanProjectFilePragmas ( absProject ) ;
if ( pragmas . empty ( ) ) return out ;
std : : unordered_map < fs : : path , std : : shared_future < BuildResult > > depResults ;
std : : mutex depMutex ;
std : : unordered_set < Configuration * > forced ;
std : : unordered_set < Configuration * > walked ;
std : : unordered_set < std : : string > libs ;
for ( const PragmaDependency & pragma : pragmas ) {
Configuration * cfg = nullptr ;
if ( IsGitSource ( pragma . source ) ) {
GitProjectSpec spec ;
spec . source = { pragma . source , pragma . branch , pragma . commit } ;
if ( ! pragma . project . empty ( ) ) spec . projectFile = pragma . project ;
cfg = GitProject ( spec ) ;
} else {
if ( ! pragma . branch . empty ( ) | | ! pragma . commit . empty ( ) ) PragmaError ( absProject , pragma . line , " 'branch' and 'commit' only apply to a git source " ) ;
// Relative to the project file, not the cwd: LoadProject runs
// from wherever the consumer was invoked.
fs : : path local = ( absProject . parent_path ( ) / pragma . source ) . lexically_normal ( ) ;
if ( fs : : is_directory ( local ) ) {
local / = pragma . project . empty ( ) ? fs : : path ( " project.cpp " ) : fs : : path ( pragma . project ) ;
} else if ( ! pragma . project . empty ( ) ) {
PragmaError ( absProject , pragma . line , " 'project' only applies to a directory source " ) ;
}
if ( ! fs : : exists ( local ) ) PragmaError ( absProject , pragma . line , std : : format ( " no such project file: {} " , local . string ( ) ) ) ;
cfg = LocalProject ( { . projectFile = local } ) ;
}
if ( std : : ranges : : contains ( out . configs , cfg ) ) continue ;
ForceHostTooling ( * cfg , host , forced ) ;
BuildResult built ;
{
Progress : : Task task ( std : : format ( " Building project dependency {} " , cfg - > name ) ) ;
built = Build ( * cfg , depResults , depMutex ) ;
}
if ( ! built . result . empty ( ) ) PragmaError ( absProject , pragma . line , std : : format ( " project dependency '{}' failed to build: {} " , cfg - > name , built . result ) ) ;
out . configs . push_back ( cfg ) ;
libs . insert ( built . libs . begin ( ) , built . libs . end ( ) ) ;
// The module path needs the dependency and everything it depends
// on, same as GetCompileCommand does for a consumer; the newest
// artifact over the same set is what project.so is stale against.
std : : function < void ( Configuration * ) > walk = [ & ] ( Configuration * c ) {
if ( ! walked . insert ( c ) . second ) return ;
out . compileFlags + = std : : format ( " -fprebuilt-module-path={} " , c - > PcmDir ( ) . string ( ) ) ;
std : : error_code ec ;
const fs : : file_time_type t = fs : : last_write_time ( ExpectedOutput ( * c ) , ec ) ;
if ( ! ec & & t > out . newestArtifact ) out . newestArtifact = t ;
for ( Configuration * sub : c - > dependencies ) walk ( sub ) ;
} ;
walk ( cfg ) ;
}
for ( const std : : string & lib : libs ) out . linkFlags + = std : : format ( " {} " , lib ) ;
return out ;
}
// project.so predates its source, the crafter-build that will load it, or
// a pre-dependency artifact — any of them means recompiling.
bool ProjectFileStale ( const fs : : path & artifact , const fs : : path & project , const fs : : path & hostExe , const ProjectFileDependencies & deps ) {
if ( ! fs : : exists ( artifact ) ) return true ;
const fs : : file_time_type built = fs : : last_write_time ( artifact ) ;
return built < fs : : last_write_time ( project ) | | built < fs : : last_write_time ( hostExe ) | | built < deps . newestArtifact ;
}
}
Configuration * Crafter : : ProjectDependency ( std : : string_view name ) {
if ( LoadingFrames . empty ( ) ) throw std : : runtime_error ( std : : format ( " ProjectDependency('{}'): called outside a project file " , name ) ) ;
const ProjectFileFrame & frame = LoadingFrames . back ( ) ;
for ( Configuration * cfg : frame . dependencies ) {
if ( cfg - > name = = name ) return cfg ;
for ( const std : : unique_ptr < Module > & iface : cfg - > interfaces ) {
if ( iface - > name = = name ) return cfg ;
}
}
std : : string declared ;
for ( Configuration * cfg : frame . dependencies ) {
if ( ! declared . empty ( ) ) declared + = " , " ;
declared + = cfg - > name ;
}
throw std : : runtime_error ( std : : format ( " ProjectDependency('{}'): not declared by {} (declared: {}) " , name , frame . project . string ( ) , declared . empty ( ) ? " none " : declared ) ) ;
}
2026-10-02 22:02:40 +02:00
bool Crafter : : IsTopLevelProject ( ) {
// One frame is the project file crafter-build was pointed at; a second
// is only ever pushed for a dependency's project file.
return LoadingFrames . size ( ) < = 1 ;
}
2026-04-27 07:04:42 +02:00
CommandResult Crafter : : RunCommandChecked ( std : : string_view cmd ) {
std : : array < char , 128 > buffer ;
test runner, cross-target runners, lib/exe split
- subprocess-isolated test runner (replaces V1 dlopen-RunTest);
Pass/Fail/Crash/Timeout/Skipped outcomes via :Test partition
- TestRunner abstraction with command templates: Local, Ssh,
SshWin (cmd.exe-shell), QemuUser, FromEnv; probe-based skip
when runner unreachable
- transitive PCM-path propagation in Build(); resolveImport
walks deps recursively; depResults cache keyed by PcmDir()
so per-target builds don't collide
- cfg.sysroot threaded through BuildStdPcm + base compile/link
command (enables aarch64 cross via Arch Linux ARM rootfs)
- lib + exe split: project.cpp defines crafterBuildLib
(LibraryStatic) + crafterBuildExe (Executable depending on
it); build.sh produces lib/libcrafter-build.a alongside
bin/crafter-build for downstream static-link consumers
- Windows DLL+launcher: CRAFTER_API macro, /EXPORT flag for
project.dll's CrafterBuildProject; Crafter::Run as the real
entry point with main.cpp as a thin wrapper
- 18 tests: HelloWorld/WithModule/Defines/CrossProjectModule/
Diamond × (Linux + sshwin:winvm), plus Incremental,
BuildError, Libraries, RunnerClassification, QemuUser,
SshRunner, WindowsViaSsh, CrossArchAarch64
- single ./bin/crafter-build test runs everything; Windows
variants skip gracefully if winvm SSH alias unreachable
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-27 22:32:19 +02:00
CommandResult result { } ;
2026-04-27 07:04:42 +02:00
2026-07-23 01:24:42 +02:00
std : : string with = std : : format ( " {} 2>&1 " , cmd ) ;
2026-04-27 07:04:42 +02:00
FILE * pipe = popen ( with . c_str ( ) , " r " ) ;
if ( ! pipe ) throw std : : runtime_error ( " popen() failed! " ) ;
while ( fgets ( buffer . data ( ) , buffer . size ( ) , pipe ) ! = nullptr ) {
result . output + = buffer . data ( ) ;
}
2026-07-23 01:24:42 +02:00
std : : int32_t status = pclose ( pipe ) ;
2026-04-27 07:04:42 +02:00
if ( WIFEXITED ( status ) ) {
result . exitCode = WEXITSTATUS ( status ) ;
} else if ( WIFSIGNALED ( status ) ) {
test runner, cross-target runners, lib/exe split
- subprocess-isolated test runner (replaces V1 dlopen-RunTest);
Pass/Fail/Crash/Timeout/Skipped outcomes via :Test partition
- TestRunner abstraction with command templates: Local, Ssh,
SshWin (cmd.exe-shell), QemuUser, FromEnv; probe-based skip
when runner unreachable
- transitive PCM-path propagation in Build(); resolveImport
walks deps recursively; depResults cache keyed by PcmDir()
so per-target builds don't collide
- cfg.sysroot threaded through BuildStdPcm + base compile/link
command (enables aarch64 cross via Arch Linux ARM rootfs)
- lib + exe split: project.cpp defines crafterBuildLib
(LibraryStatic) + crafterBuildExe (Executable depending on
it); build.sh produces lib/libcrafter-build.a alongside
bin/crafter-build for downstream static-link consumers
- Windows DLL+launcher: CRAFTER_API macro, /EXPORT flag for
project.dll's CrafterBuildProject; Crafter::Run as the real
entry point with main.cpp as a thin wrapper
- 18 tests: HelloWorld/WithModule/Defines/CrossProjectModule/
Diamond × (Linux + sshwin:winvm), plus Incremental,
BuildError, Libraries, RunnerClassification, QemuUser,
SshRunner, WindowsViaSsh, CrossArchAarch64
- single ./bin/crafter-build test runs everything; Windows
variants skip gracefully if winvm SSH alias unreachable
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-27 22:32:19 +02:00
result . signal = WTERMSIG ( status ) ;
result . exitCode = 128 + result . signal ;
result . crashed = true ;
} else {
result . exitCode = - 1 ;
}
return result ;
}
CommandResult Crafter : : RunCommandWithTimeout ( std : : string_view cmd , std : : chrono : : seconds timeout ) {
std : : array < char , 128 > buffer ;
CommandResult result { } ;
musl host support: bootstrap and run on Alpine (x86_64-alpine-linux-musl)
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>
2026-09-01 23:49:00 +02:00
// -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 ) ;
test runner, cross-target runners, lib/exe split
- subprocess-isolated test runner (replaces V1 dlopen-RunTest);
Pass/Fail/Crash/Timeout/Skipped outcomes via :Test partition
- TestRunner abstraction with command templates: Local, Ssh,
SshWin (cmd.exe-shell), QemuUser, FromEnv; probe-based skip
when runner unreachable
- transitive PCM-path propagation in Build(); resolveImport
walks deps recursively; depResults cache keyed by PcmDir()
so per-target builds don't collide
- cfg.sysroot threaded through BuildStdPcm + base compile/link
command (enables aarch64 cross via Arch Linux ARM rootfs)
- lib + exe split: project.cpp defines crafterBuildLib
(LibraryStatic) + crafterBuildExe (Executable depending on
it); build.sh produces lib/libcrafter-build.a alongside
bin/crafter-build for downstream static-link consumers
- Windows DLL+launcher: CRAFTER_API macro, /EXPORT flag for
project.dll's CrafterBuildProject; Crafter::Run as the real
entry point with main.cpp as a thin wrapper
- 18 tests: HelloWorld/WithModule/Defines/CrossProjectModule/
Diamond × (Linux + sshwin:winvm), plus Incremental,
BuildError, Libraries, RunnerClassification, QemuUser,
SshRunner, WindowsViaSsh, CrossArchAarch64
- single ./bin/crafter-build test runs everything; Windows
variants skip gracefully if winvm SSH alias unreachable
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-27 22:32:19 +02:00
FILE * pipe = popen ( wrapped . c_str ( ) , " r " ) ;
if ( ! pipe ) throw std : : runtime_error ( " popen() failed! " ) ;
while ( fgets ( buffer . data ( ) , buffer . size ( ) , pipe ) ! = nullptr ) {
result . output + = buffer . data ( ) ;
}
2026-07-23 01:24:42 +02:00
std : : int32_t status = pclose ( pipe ) ;
test runner, cross-target runners, lib/exe split
- subprocess-isolated test runner (replaces V1 dlopen-RunTest);
Pass/Fail/Crash/Timeout/Skipped outcomes via :Test partition
- TestRunner abstraction with command templates: Local, Ssh,
SshWin (cmd.exe-shell), QemuUser, FromEnv; probe-based skip
when runner unreachable
- transitive PCM-path propagation in Build(); resolveImport
walks deps recursively; depResults cache keyed by PcmDir()
so per-target builds don't collide
- cfg.sysroot threaded through BuildStdPcm + base compile/link
command (enables aarch64 cross via Arch Linux ARM rootfs)
- lib + exe split: project.cpp defines crafterBuildLib
(LibraryStatic) + crafterBuildExe (Executable depending on
it); build.sh produces lib/libcrafter-build.a alongside
bin/crafter-build for downstream static-link consumers
- Windows DLL+launcher: CRAFTER_API macro, /EXPORT flag for
project.dll's CrafterBuildProject; Crafter::Run as the real
entry point with main.cpp as a thin wrapper
- 18 tests: HelloWorld/WithModule/Defines/CrossProjectModule/
Diamond × (Linux + sshwin:winvm), plus Incremental,
BuildError, Libraries, RunnerClassification, QemuUser,
SshRunner, WindowsViaSsh, CrossArchAarch64
- single ./bin/crafter-build test runs everything; Windows
variants skip gracefully if winvm SSH alias unreachable
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-27 22:32:19 +02:00
if ( WIFEXITED ( status ) ) {
2026-07-23 01:24:42 +02:00
std : : int32_t code = WEXITSTATUS ( status ) ;
test runner, cross-target runners, lib/exe split
- subprocess-isolated test runner (replaces V1 dlopen-RunTest);
Pass/Fail/Crash/Timeout/Skipped outcomes via :Test partition
- TestRunner abstraction with command templates: Local, Ssh,
SshWin (cmd.exe-shell), QemuUser, FromEnv; probe-based skip
when runner unreachable
- transitive PCM-path propagation in Build(); resolveImport
walks deps recursively; depResults cache keyed by PcmDir()
so per-target builds don't collide
- cfg.sysroot threaded through BuildStdPcm + base compile/link
command (enables aarch64 cross via Arch Linux ARM rootfs)
- lib + exe split: project.cpp defines crafterBuildLib
(LibraryStatic) + crafterBuildExe (Executable depending on
it); build.sh produces lib/libcrafter-build.a alongside
bin/crafter-build for downstream static-link consumers
- Windows DLL+launcher: CRAFTER_API macro, /EXPORT flag for
project.dll's CrafterBuildProject; Crafter::Run as the real
entry point with main.cpp as a thin wrapper
- 18 tests: HelloWorld/WithModule/Defines/CrossProjectModule/
Diamond × (Linux + sshwin:winvm), plus Incremental,
BuildError, Libraries, RunnerClassification, QemuUser,
SshRunner, WindowsViaSsh, CrossArchAarch64
- single ./bin/crafter-build test runs everything; Windows
variants skip gracefully if winvm SSH alias unreachable
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-27 22:32:19 +02:00
if ( code = = 124 ) {
result . timedOut = true ;
result . exitCode = 124 ;
} else if ( code > = 128 ) {
result . signal = code - 128 ;
result . exitCode = code ;
result . crashed = true ;
} else {
result . exitCode = code ;
}
} else if ( WIFSIGNALED ( status ) ) {
result . signal = WTERMSIG ( status ) ;
result . exitCode = 128 + result . signal ;
result . crashed = true ;
2026-04-27 07:04:42 +02:00
} else {
result . exitCode = - 1 ;
}
return result ;
}
std : : string Crafter : : BuildStdPcm ( const Configuration & config , fs : : path stdPcm ) {
2026-05-30 19:28:06 +02:00
CacheLock lock ( stdPcm . parent_path ( ) ) ;
2026-04-23 01:57:25 +02:00
if ( config . target = = " x86_64-w64-mingw32 " ) {
std : : vector < std : : string > folders ;
// Iterate through the directory and collect all subdirectories
for ( const auto & entry : fs : : directory_iterator ( " /usr/x86_64-w64-mingw32/include/c++ " ) ) {
if ( entry . is_directory ( ) ) {
folders . push_back ( entry . path ( ) . filename ( ) . string ( ) ) ;
}
}
// Sort the folders by version in descending order
std : : sort ( folders . begin ( ) , folders . end ( ) , [ ] ( const std : : string & a , const std : : string & b ) {
return std : : lexicographical_compare ( b . begin ( ) , b . end ( ) , a . begin ( ) , a . end ( ) ) ;
} ) ;
std : : string mingWversion = folders . front ( ) ;
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 ) ) {
V2: WASI, -r flag, CI pipeline, examples & tests cleanup
WASI / wasm32 target support
- Auto-detect /usr/share/wasi-sysroot on Linux when target starts_with("wasm32")
- Skip -march/-mtune for wasm (clang rejects them)
- Apply -fno-exceptions -fno-c++-static-destructors -mllvm -wasm-enable-sjlj
-D_WASI_EMULATED_SIGNAL to wasm builds (compile + std PCM, kept in sync)
- .wasm output extension in expectedOutputFor and link command
- EnableWasiBrowserRuntime(cfg): opt-in helper that drops index.html +
runtime.js next to the .wasm; runtime.js reads window.CRAFTER_WASM_URL
set in the templated index.html so a single shim handles any output name
-r run flag in the CLI: build then exec the artifact (host targets only;
rejects libraries; auto .exe/.wasm extension handling)
CI pipeline (.forgejo/workflows/ci.yaml)
- Triggers: PR/push to master + manual dispatch
- Single arch-latest container job: install deps, bootstrap, self-rebuild,
run tests, cross-compile mingw, package both archives, upload artifacts
- Rolling 'latest' release published only on push/dispatch to master
mingw cross-compile from Linux now works end-to-end:
- ExternalDependency cache key includes target so per-target glslang builds
don't collide; CMAKE_BUILD_TYPE=Release pinned (otherwise glslang appends
'd' to lib names and breaks linking); cross-compile cmake flags
(CMAKE_SYSTEM_NAME=Windows, CMAKE_*_COMPILER_TARGET=...)
- project.cpp accepts --target=<triple>; Linux-only -Wl,--export-dynamic
and -ldl are gated; mingw glslang skips the standalone exe (its libgcc_eh
link pulls pthread which mingw doesn't link by default)
- mingw compile uses -femulated-tls so std::__once_callable etc reference
the same emutls symbols libstdc++ provides
- mingw link auto-adds -lstdc++exp -lpthread
GetCrafterBuildHome() exposed from the Platform module; LoadProject (Linux
+ Windows) now both use it instead of duplicating the resolution.
Examples reorg: hello-world, library, with-module, wasi, tests — each with
its own README. Tests reorg: per-test directory with inner/ fixture, no
shared tests/fixtures/ tree. New Wasi test verifies .wasm magic bytes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 23:24:46 +02:00
// -femulated-tls keeps PCM TLS access matching libstdc++'s emutls
// definitions; mismatch surfaces as undefined std::__once_callable.
2026-05-30 19:28:06 +02:00
// The std.cc → std.cppm copy uses a plain filename: held under the
// cache lock, no other builder writes this path concurrently.
fs : : path stdCppm = stdPcm . parent_path ( ) / " std.cppm " ;
2026-05-30 16:36:45 +00:00
std : : error_code copyEc ;
fs : : copy_file ( stdCc , stdCppm , fs : : copy_options : : overwrite_existing , copyEc ) ;
if ( copyEc ) {
return std : : format ( " copy {} -> {}: {} " , stdCc . string ( ) , stdCppm . string ( ) , copyEc . message ( ) ) ;
}
fix: decide a build step by its exit code, not by whether it printed
RunCommand merged stderr into stdout, dropped pclose's status and returned
the text; every compile, link and archive site then read "printed something"
as "failed". A warning is printing something, so a translation unit that
warned failed the build — but only on the run that actually recompiled it,
since warnings aren't re-emitted for an object that's already up to date.
The same unchanged source therefore passed or failed depending on the state
of the build tree: flaky-looking tests locally, and a cold CI checkout
surfacing every latent warning in a project at once as unrelated failures.
RunCommand is gone, replaced by RunBuildCommand: it goes through
RunCommandChecked, returns "" when the command exited 0 (so every caller's
`if (!result.empty())` error path is unchanged) and hands any warnings to
the new Progress::Diagnostic instead of to the error path. That also closes
the quiet half of the bug — a compiler killed by the OOM killer prints
nothing, so it used to read as success and leave the build carrying on with
a missing object; it now reports the signal that killed it.
Warnings are now shown rather than swallowed, which they weren't in either
direction before: invisible on an incremental build, fatal on a cold one.
Failing on them stays a project's choice, via -Werror in compileFlags.
2026-08-26 00:51:15 +00:00
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 ( ) ) ) ;
2026-04-27 07:04:42 +02:00
} else {
return " " ;
2026-04-23 01:57:25 +02:00
}
} else {
feat(lint): const-local and constexpr-constant rules
Two rules the AST makes possible, plus the mutation analysis behind them.
const-local reports a local that is never written. It is restricted to SCALARS
— integers, bools, enums, floating types — and that restriction is what makes
the answer exact rather than a guess: a scalar has no member functions, so the
only ways to write one are assignment, ++/--, having its address taken, or
binding to a non-const reference. All four are now tracked in the walk:
- assignment and compound assignment visit their LEFT operand in a write
context, the right one normally;
- ++/-- and & write their operand;
- a call argument is checked against the callee's parameter type, so passing
to `const int&` or by value is a read while `int&` is a write;
- initialising a non-const reference writes what it binds to.
For a class type a non-const method call could mutate it, and deciding that is
the whole-program analysis clang-tidy does, so those are simply out of scope
rather than guessed at.
constexpr-constant promotes a const constant whose initialiser is made only of
literals and operators, so `const int A = 1 << 4;` qualifies and
`const int B = Compute();` does not.
On this repository const-local found 103 candidates, which was too many to be
useful, and the reason was informative: most were range-for bindings and
pointer locals. `for (T* const x : …)` and `T* const p` are not spellings
anybody writes, and the useful constness for a pointer is on the pointee, which
this rule cannot advise on. Excluding both leaves 36, all plain bool or enum
locals worth fixing — isWasm, isPe, exists, writes, isC and so on. Those 36 are
fixed in this commit; the compiler verified every one.
Both rules are report-only. The analysis is exact, but adding const is a
judgement about intent as much as mechanics, and a wrong suggestion should cost
a glance rather than a build. const-local also deliberately does not become a
transform: inserting `const` before a shared type would apply it to every
declarator in a multi-declarator statement, including any that IS written.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 00:50:48 +02:00
const bool isWasm = config . target . starts_with ( " wasm32 " ) ;
V2: WASI, -r flag, CI pipeline, examples & tests cleanup
WASI / wasm32 target support
- Auto-detect /usr/share/wasi-sysroot on Linux when target starts_with("wasm32")
- Skip -march/-mtune for wasm (clang rejects them)
- Apply -fno-exceptions -fno-c++-static-destructors -mllvm -wasm-enable-sjlj
-D_WASI_EMULATED_SIGNAL to wasm builds (compile + std PCM, kept in sync)
- .wasm output extension in expectedOutputFor and link command
- EnableWasiBrowserRuntime(cfg): opt-in helper that drops index.html +
runtime.js next to the .wasm; runtime.js reads window.CRAFTER_WASM_URL
set in the templated index.html so a single shim handles any output name
-r run flag in the CLI: build then exec the artifact (host targets only;
rejects libraries; auto .exe/.wasm extension handling)
CI pipeline (.forgejo/workflows/ci.yaml)
- Triggers: PR/push to master + manual dispatch
- Single arch-latest container job: install deps, bootstrap, self-rebuild,
run tests, cross-compile mingw, package both archives, upload artifacts
- Rolling 'latest' release published only on push/dispatch to master
mingw cross-compile from Linux now works end-to-end:
- ExternalDependency cache key includes target so per-target glslang builds
don't collide; CMAKE_BUILD_TYPE=Release pinned (otherwise glslang appends
'd' to lib names and breaks linking); cross-compile cmake flags
(CMAKE_SYSTEM_NAME=Windows, CMAKE_*_COMPILER_TARGET=...)
- project.cpp accepts --target=<triple>; Linux-only -Wl,--export-dynamic
and -ldl are gated; mingw glslang skips the standalone exe (its libgcc_eh
link pulls pthread which mingw doesn't link by default)
- mingw compile uses -femulated-tls so std::__once_callable etc reference
the same emutls symbols libstdc++ provides
- mingw link auto-adds -lstdc++exp -lpthread
GetCrafterBuildHome() exposed from the Platform module; LoadProject (Linux
+ Windows) now both use it instead of duplicating the resolution.
Examples reorg: hello-world, library, with-module, wasi, tests — each with
its own README. Tests reorg: per-test directory with inner/ fixture, no
shared tests/fixtures/ tree. New Wasi test verifies .wasm magic bytes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 23:24:46 +02:00
// wasi-sdk drops std.cppm at <sysroot>/share/libc++/v1/, the rest of
// the libc++ ecosystem (e.g. /opt/aarch64-rootfs) follows FHS at
// <sysroot>/usr/share/libc++/v1/.
std : : string stdCppm ;
if ( isWasm ) {
stdCppm = std : : format ( " {}/share/libc++/v1/std.cppm " , config . sysroot ) ;
} else if ( config . sysroot . empty ( ) ) {
stdCppm = " /usr/share/libc++/v1/std.cppm " ;
} else {
stdCppm = std : : format ( " {}/usr/share/libc++/v1/std.cppm " , config . sysroot ) ;
2026-07-22 18:25:39 +02:00
// not every distro ships the std module sources (Alpine's
// libc++-dev doesn't); the host copy is interchangeable as long
// as the libc++ versions line up, which the compile of the PCM
// against the sysroot's headers verifies anyway
if ( ! fs : : exists ( stdCppm ) & & fs : : exists ( " /usr/share/libc++/v1/std.cppm " ) ) {
stdCppm = " /usr/share/libc++/v1/std.cppm " ;
}
V2: WASI, -r flag, CI pipeline, examples & tests cleanup
WASI / wasm32 target support
- Auto-detect /usr/share/wasi-sysroot on Linux when target starts_with("wasm32")
- Skip -march/-mtune for wasm (clang rejects them)
- Apply -fno-exceptions -fno-c++-static-destructors -mllvm -wasm-enable-sjlj
-D_WASI_EMULATED_SIGNAL to wasm builds (compile + std PCM, kept in sync)
- .wasm output extension in expectedOutputFor and link command
- EnableWasiBrowserRuntime(cfg): opt-in helper that drops index.html +
runtime.js next to the .wasm; runtime.js reads window.CRAFTER_WASM_URL
set in the templated index.html so a single shim handles any output name
-r run flag in the CLI: build then exec the artifact (host targets only;
rejects libraries; auto .exe/.wasm extension handling)
CI pipeline (.forgejo/workflows/ci.yaml)
- Triggers: PR/push to master + manual dispatch
- Single arch-latest container job: install deps, bootstrap, self-rebuild,
run tests, cross-compile mingw, package both archives, upload artifacts
- Rolling 'latest' release published only on push/dispatch to master
mingw cross-compile from Linux now works end-to-end:
- ExternalDependency cache key includes target so per-target glslang builds
don't collide; CMAKE_BUILD_TYPE=Release pinned (otherwise glslang appends
'd' to lib names and breaks linking); cross-compile cmake flags
(CMAKE_SYSTEM_NAME=Windows, CMAKE_*_COMPILER_TARGET=...)
- project.cpp accepts --target=<triple>; Linux-only -Wl,--export-dynamic
and -ldl are gated; mingw glslang skips the standalone exe (its libgcc_eh
link pulls pthread which mingw doesn't link by default)
- mingw compile uses -femulated-tls so std::__once_callable etc reference
the same emutls symbols libstdc++ provides
- mingw link auto-adds -lstdc++exp -lpthread
GetCrafterBuildHome() exposed from the Platform module; LoadProject (Linux
+ Windows) now both use it instead of duplicating the resolution.
Examples reorg: hello-world, library, with-module, wasi, tests — each with
its own README. Tests reorg: per-test directory with inner/ fixture, no
shared tests/fixtures/ tree. New Wasi test verifies .wasm magic bytes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 23:24:46 +02:00
}
test runner, cross-target runners, lib/exe split
- subprocess-isolated test runner (replaces V1 dlopen-RunTest);
Pass/Fail/Crash/Timeout/Skipped outcomes via :Test partition
- TestRunner abstraction with command templates: Local, Ssh,
SshWin (cmd.exe-shell), QemuUser, FromEnv; probe-based skip
when runner unreachable
- transitive PCM-path propagation in Build(); resolveImport
walks deps recursively; depResults cache keyed by PcmDir()
so per-target builds don't collide
- cfg.sysroot threaded through BuildStdPcm + base compile/link
command (enables aarch64 cross via Arch Linux ARM rootfs)
- lib + exe split: project.cpp defines crafterBuildLib
(LibraryStatic) + crafterBuildExe (Executable depending on
it); build.sh produces lib/libcrafter-build.a alongside
bin/crafter-build for downstream static-link consumers
- Windows DLL+launcher: CRAFTER_API macro, /EXPORT flag for
project.dll's CrafterBuildProject; Crafter::Run as the real
entry point with main.cpp as a thin wrapper
- 18 tests: HelloWorld/WithModule/Defines/CrossProjectModule/
Diamond × (Linux + sshwin:winvm), plus Incremental,
BuildError, Libraries, RunnerClassification, QemuUser,
SshRunner, WindowsViaSsh, CrossArchAarch64
- single ./bin/crafter-build test runs everything; Windows
variants skip gracefully if winvm SSH alias unreachable
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-27 22:32:19 +02:00
std : : string sysrootFlag = config . sysroot . empty ( )
? std : : string ( )
: std : : format ( " --sysroot={} " , config . sysroot ) ;
V2: WASI, -r flag, CI pipeline, examples & tests cleanup
WASI / wasm32 target support
- Auto-detect /usr/share/wasi-sysroot on Linux when target starts_with("wasm32")
- Skip -march/-mtune for wasm (clang rejects them)
- Apply -fno-exceptions -fno-c++-static-destructors -mllvm -wasm-enable-sjlj
-D_WASI_EMULATED_SIGNAL to wasm builds (compile + std PCM, kept in sync)
- .wasm output extension in expectedOutputFor and link command
- EnableWasiBrowserRuntime(cfg): opt-in helper that drops index.html +
runtime.js next to the .wasm; runtime.js reads window.CRAFTER_WASM_URL
set in the templated index.html so a single shim handles any output name
-r run flag in the CLI: build then exec the artifact (host targets only;
rejects libraries; auto .exe/.wasm extension handling)
CI pipeline (.forgejo/workflows/ci.yaml)
- Triggers: PR/push to master + manual dispatch
- Single arch-latest container job: install deps, bootstrap, self-rebuild,
run tests, cross-compile mingw, package both archives, upload artifacts
- Rolling 'latest' release published only on push/dispatch to master
mingw cross-compile from Linux now works end-to-end:
- ExternalDependency cache key includes target so per-target glslang builds
don't collide; CMAKE_BUILD_TYPE=Release pinned (otherwise glslang appends
'd' to lib names and breaks linking); cross-compile cmake flags
(CMAKE_SYSTEM_NAME=Windows, CMAKE_*_COMPILER_TARGET=...)
- project.cpp accepts --target=<triple>; Linux-only -Wl,--export-dynamic
and -ldl are gated; mingw glslang skips the standalone exe (its libgcc_eh
link pulls pthread which mingw doesn't link by default)
- mingw compile uses -femulated-tls so std::__once_callable etc reference
the same emutls symbols libstdc++ provides
- mingw link auto-adds -lstdc++exp -lpthread
GetCrafterBuildHome() exposed from the Platform module; LoadProject (Linux
+ Windows) now both use it instead of duplicating the resolution.
Examples reorg: hello-world, library, with-module, wasi, tests — each with
its own README. Tests reorg: per-test directory with inner/ fixture, no
shared tests/fixtures/ tree. New Wasi test verifies .wasm magic bytes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 23:24:46 +02:00
// wasm32 rejects -march. wasi-libc++ headers require these baselines
// to compile: setjmp.h needs -mllvm -wasm-enable-sjlj (lowered to wasm
// EH); signal.h requires the emulation define; and EH itself isn't
// wired up so -fno-exceptions stays.
std : : string archFlags = isWasm
2026-05-18 05:23:11 +02:00
? std : : string ( " -fno-exceptions -msimd128 -fno-c++-static-destructors -mllvm -wasm-enable-sjlj -D_WASI_EMULATED_SIGNAL " )
V2: WASI, -r flag, CI pipeline, examples & tests cleanup
WASI / wasm32 target support
- Auto-detect /usr/share/wasi-sysroot on Linux when target starts_with("wasm32")
- Skip -march/-mtune for wasm (clang rejects them)
- Apply -fno-exceptions -fno-c++-static-destructors -mllvm -wasm-enable-sjlj
-D_WASI_EMULATED_SIGNAL to wasm builds (compile + std PCM, kept in sync)
- .wasm output extension in expectedOutputFor and link command
- EnableWasiBrowserRuntime(cfg): opt-in helper that drops index.html +
runtime.js next to the .wasm; runtime.js reads window.CRAFTER_WASM_URL
set in the templated index.html so a single shim handles any output name
-r run flag in the CLI: build then exec the artifact (host targets only;
rejects libraries; auto .exe/.wasm extension handling)
CI pipeline (.forgejo/workflows/ci.yaml)
- Triggers: PR/push to master + manual dispatch
- Single arch-latest container job: install deps, bootstrap, self-rebuild,
run tests, cross-compile mingw, package both archives, upload artifacts
- Rolling 'latest' release published only on push/dispatch to master
mingw cross-compile from Linux now works end-to-end:
- ExternalDependency cache key includes target so per-target glslang builds
don't collide; CMAKE_BUILD_TYPE=Release pinned (otherwise glslang appends
'd' to lib names and breaks linking); cross-compile cmake flags
(CMAKE_SYSTEM_NAME=Windows, CMAKE_*_COMPILER_TARGET=...)
- project.cpp accepts --target=<triple>; Linux-only -Wl,--export-dynamic
and -ldl are gated; mingw glslang skips the standalone exe (its libgcc_eh
link pulls pthread which mingw doesn't link by default)
- mingw compile uses -femulated-tls so std::__once_callable etc reference
the same emutls symbols libstdc++ provides
- mingw link auto-adds -lstdc++exp -lpthread
GetCrafterBuildHome() exposed from the Platform module; LoadProject (Linux
+ Windows) now both use it instead of duplicating the resolution.
Examples reorg: hello-world, library, with-module, wasi, tests — each with
its own README. Tests reorg: per-test directory with inner/ fixture, no
shared tests/fixtures/ tree. New Wasi test verifies .wasm magic bytes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 23:24:46 +02:00
: std : : format ( " -march={} -mtune={} " , config . march , config . mtune ) ;
feat: feature-detected browser-wasm variants (relaxed-SIMD) + variant-aware runtime
The browser wasm pipeline hardcoded -msimd128 for every wasm32 target and
baked a single wasm URL into index.html, so newer codegen features that
aren't yet baseline across engines (relaxed SIMD today; threads, future SIMD
revisions later) couldn't be adopted without dropping the browsers that lack
them.
Add a general, feature-parameterized mechanism owned entirely by
Crafter.Build:
- Configuration::wasmVariants declares N codegen variants (label, extra -m
flags, runtime probes). Build() compiles the baseline plus one
outputName.<label>.wasm per variant, recompiling the whole graph (incl.
dep libs + std PCM) with the variant's flags — relaxed-SIMD is per-TU
codegen, not a link switch. wasmVariantFlags folds into VariantId so each
variant's objects/PCMs land in their own build+bin dir.
- EnableWasiBrowserRuntime emits a variants.json manifest (label -> url +
probes), preferred-first with the baseline as the universal fallback.
- The shipped runtime.js runs inlined wasm-feature-detect probes
(relaxed-simd, simd, tail-call, bulk-memory, exception-handling, threads),
picks the first variant whose probes all pass, and falls back to the single
baked CRAFTER_WASM_URL when no manifest is present (backward compatible).
- EnableWasiRelaxedSimdVariant registers the relaxed-SIMD variant — the
motivating case (Chrome 114+/Firefox 120+ enable it by default; Safari
still flag-gates it as of mid-2026).
Verified end to end: a wasm32-wasip1 build emits both wasi-hello.wasm and
wasi-hello.relaxed-simd.wasm + variants.json; Firefox selects the
relaxed-simd variant and runs it.
Resolves #24
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 15:14:18 +00:00
// Match the variant codegen flags the consuming TUs are built with
// (e.g. -mrelaxed-simd) so the std BMI is feature-compatible. wasm only;
// wasmVariantFlags is empty on every other path.
if ( isWasm ) {
2026-07-23 01:24:42 +02:00
for ( const std : : string & f : config . wasmVariantFlags ) archFlags + = std : : format ( " {} " , f ) ;
feat: feature-detected browser-wasm variants (relaxed-SIMD) + variant-aware runtime
The browser wasm pipeline hardcoded -msimd128 for every wasm32 target and
baked a single wasm URL into index.html, so newer codegen features that
aren't yet baseline across engines (relaxed SIMD today; threads, future SIMD
revisions later) couldn't be adopted without dropping the browsers that lack
them.
Add a general, feature-parameterized mechanism owned entirely by
Crafter.Build:
- Configuration::wasmVariants declares N codegen variants (label, extra -m
flags, runtime probes). Build() compiles the baseline plus one
outputName.<label>.wasm per variant, recompiling the whole graph (incl.
dep libs + std PCM) with the variant's flags — relaxed-SIMD is per-TU
codegen, not a link switch. wasmVariantFlags folds into VariantId so each
variant's objects/PCMs land in their own build+bin dir.
- EnableWasiBrowserRuntime emits a variants.json manifest (label -> url +
probes), preferred-first with the baseline as the universal fallback.
- The shipped runtime.js runs inlined wasm-feature-detect probes
(relaxed-simd, simd, tail-call, bulk-memory, exception-handling, threads),
picks the first variant whose probes all pass, and falls back to the single
baked CRAFTER_WASM_URL when no manifest is present (backward compatible).
- EnableWasiRelaxedSimdVariant registers the relaxed-SIMD variant — the
motivating case (Chrome 114+/Firefox 120+ enable it by default; Safari
still flag-gates it as of mid-2026).
Verified end to end: a wasm32-wasip1 build emits both wasi-hello.wasm and
wasi-hello.relaxed-simd.wasm + variants.json; Firefox selects the
relaxed-simd variant and runs it.
Resolves #24
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 15:14:18 +00:00
}
2026-07-22 18:25:39 +02:00
// non-wasm cross builds: the driver ranks the host toolchain's own
// libc++ headers (<install>/../include/c++/v1) above --sysroot, so
// the PCM would mix host libc++ with the foreign libc — force the
refactor: drop Windows host support
crafter-build is moving to a single Flatpak distribution, and there is
no Windows host in that picture: no build.cmd, no MSVC or mingw host
build, no DLL plus launcher. Delete every host-Windows path instead of
leaving it to rot unverified: the cmd.exe command runner and quoting,
the LOCALAPPDATA cache, the MSVC and mingw-host LoadProject variants
with their LIBCXX_DIR and CRAFTER_MINGW_DIR plumbing, the winsock port
probe, the LoadLibrary libclang loader, the console-width and isatty
shims, and the preprocessor guards around the Linux bodies.
The x86_64-w64-mingw32 cross-compile target stays. It is target logic
(output names, -femulated-tls, the libstdc++ link extras, copying a
dependency DLL next to a mingw exe, wine as the runner) and did not
depend on any of the removed code. The msvc target goes with the host
that was the only thing able to use it.
Three target-path warts fixed on the way: ExpectedOutput named a mingw
static library <name>.lib while the archive step wrote lib<name>.a, so
those libraries relinked on every run; the import lib is no longer
copied next to a mingw exe, since the dependency's own bin dir is
already on the link line; and llvm-ar now writes every archive, not
only the LTO ones, so Debug and Release archives are made the same way.
CRAFTER_API and Crafter.Build-Api.h existed only for the DLL boundary.
Nothing downstream used them, so they are removed rather than kept as
an empty macro nobody can verify.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-02 21:56:38 +02:00
// sysroot's copy.
2026-07-22 18:25:39 +02:00
if ( ! isWasm & & ! config . sysroot . empty ( ) ) {
archFlags + = std : : format ( " -nostdinc++ -isystem {}/usr/include/c++/v1 " , config . sysroot ) ;
}
test runner, cross-target runners, lib/exe split
- subprocess-isolated test runner (replaces V1 dlopen-RunTest);
Pass/Fail/Crash/Timeout/Skipped outcomes via :Test partition
- TestRunner abstraction with command templates: Local, Ssh,
SshWin (cmd.exe-shell), QemuUser, FromEnv; probe-based skip
when runner unreachable
- transitive PCM-path propagation in Build(); resolveImport
walks deps recursively; depResults cache keyed by PcmDir()
so per-target builds don't collide
- cfg.sysroot threaded through BuildStdPcm + base compile/link
command (enables aarch64 cross via Arch Linux ARM rootfs)
- lib + exe split: project.cpp defines crafterBuildLib
(LibraryStatic) + crafterBuildExe (Executable depending on
it); build.sh produces lib/libcrafter-build.a alongside
bin/crafter-build for downstream static-link consumers
- Windows DLL+launcher: CRAFTER_API macro, /EXPORT flag for
project.dll's CrafterBuildProject; Crafter::Run as the real
entry point with main.cpp as a thin wrapper
- 18 tests: HelloWorld/WithModule/Defines/CrossProjectModule/
Diamond × (Linux + sshwin:winvm), plus Incremental,
BuildError, Libraries, RunnerClassification, QemuUser,
SshRunner, WindowsViaSsh, CrossArchAarch64
- single ./bin/crafter-build test runs everything; Windows
variants skip gracefully if winvm SSH alias unreachable
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-27 22:32:19 +02:00
if ( ! fs : : exists ( stdPcm ) | | fs : : last_write_time ( stdPcm ) < fs : : last_write_time ( stdCppm ) ) {
fix: decide a build step by its exit code, not by whether it printed
RunCommand merged stderr into stdout, dropped pclose's status and returned
the text; every compile, link and archive site then read "printed something"
as "failed". A warning is printing something, so a translation unit that
warned failed the build — but only on the run that actually recompiled it,
since warnings aren't re-emitted for an object that's already up to date.
The same unchanged source therefore passed or failed depending on the state
of the build tree: flaky-looking tests locally, and a cold CI checkout
surfacing every latent warning in a project at once as unrelated failures.
RunCommand is gone, replaced by RunBuildCommand: it goes through
RunCommandChecked, returns "" when the command exited 0 (so every caller's
`if (!result.empty())` error path is unchanged) and hands any warnings to
the new Progress::Diagnostic instead of to the error path. That also closes
the quiet half of the bug — a compiler killed by the OOM killer prints
nothing, so it used to read as success and leave the build carrying on with
a missing object; it now reports the signal that killed it.
Warnings are now shown rather than swallowed, which they weren't in either
direction before: invisible on an incremental build, fatal on a cold one.
Failing on them stays a project's choice, via -Werror in compileFlags.
2026-08-26 00:51:15 +00:00
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 ( ) ) ) ;
2026-04-27 07:04:42 +02:00
} else {
return " " ;
2026-04-23 01:57:25 +02:00
}
}
}
2026-04-27 07:04:42 +02:00
fs : : path Crafter : : GetCacheDir ( ) {
2026-04-23 01:57:25 +02:00
if ( const char * xdg = std : : getenv ( " XDG_CACHE_HOME " ) ; xdg & & * xdg ) {
return fs : : path ( xdg ) / " crafter.build " ;
}
2026-04-27 07:04:42 +02:00
2026-04-23 01:57:25 +02:00
if ( const char * home = std : : getenv ( " HOME " ) ) {
return fs : : path ( home ) / " .cache " / " crafter.build " ;
}
2026-04-27 07:04:42 +02:00
2026-04-23 01:57:25 +02:00
throw std : : runtime_error ( " Neither XDG_CACHE_HOME nor HOME set " ) ;
}
2026-04-27 07:04:42 +02:00
std : : string Crafter : : GetBaseCommand ( const Configuration & config ) {
2026-04-23 01:57:25 +02:00
std : : string stdlib ;
if ( config . target = = " x86_64-w64-mingw32 " ) {
stdlib = " " ;
} else {
stdlib = " -stdlib=libc++ " ;
}
2026-04-27 07:04:42 +02:00
return std : : format ( " clang++ {} " , stdlib ) ;
}
namespace {
2026-07-23 01:24:42 +02:00
constexpr std : : array < std : : string_view , 11 > CrafterBuildModules = {
2026-04-27 07:04:42 +02:00
" Crafter.Build-Shader " ,
" Crafter.Build-Platform " ,
" Crafter.Build-Interface " ,
" Crafter.Build-Implementation " ,
" Crafter.Build-External " ,
" Crafter.Build-Clang " ,
test runner, cross-target runners, lib/exe split
- subprocess-isolated test runner (replaces V1 dlopen-RunTest);
Pass/Fail/Crash/Timeout/Skipped outcomes via :Test partition
- TestRunner abstraction with command templates: Local, Ssh,
SshWin (cmd.exe-shell), QemuUser, FromEnv; probe-based skip
when runner unreachable
- transitive PCM-path propagation in Build(); resolveImport
walks deps recursively; depResults cache keyed by PcmDir()
so per-target builds don't collide
- cfg.sysroot threaded through BuildStdPcm + base compile/link
command (enables aarch64 cross via Arch Linux ARM rootfs)
- lib + exe split: project.cpp defines crafterBuildLib
(LibraryStatic) + crafterBuildExe (Executable depending on
it); build.sh produces lib/libcrafter-build.a alongside
bin/crafter-build for downstream static-link consumers
- Windows DLL+launcher: CRAFTER_API macro, /EXPORT flag for
project.dll's CrafterBuildProject; Crafter::Run as the real
entry point with main.cpp as a thin wrapper
- 18 tests: HelloWorld/WithModule/Defines/CrossProjectModule/
Diamond × (Linux + sshwin:winvm), plus Incremental,
BuildError, Libraries, RunnerClassification, QemuUser,
SshRunner, WindowsViaSsh, CrossArchAarch64
- single ./bin/crafter-build test runs everything; Windows
variants skip gracefully if winvm SSH alias unreachable
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-27 22:32:19 +02:00
" Crafter.Build-Test " ,
2026-07-23 01:24:42 +02:00
" Crafter.Build-Lint " ,
2026-04-29 03:37:09 +02:00
" Crafter.Build-Progress " ,
2026-05-12 01:16:40 +02:00
" Crafter.Build-Asset " ,
2026-04-27 07:04:42 +02:00
" Crafter.Build " ,
} ;
void EnsureCrafterBuildPcms ( const fs : : path & sourceDir , const fs : : path & cacheDir ) {
2026-05-30 19:28:06 +02:00
CacheLock lock ( cacheDir ) ;
fix: key the host PCM cache on source content, add clean, hash project args
Three follow-ons to the stale-build report, all cases of an identity not
capturing something that changes the output.
The host PCM cache under <cache>/crafter.build/<target>-<march>/ is shared by
every crafter-build on the machine, and freshness was a per-file mtime
comparison. That cannot tell "this PCM is newer than my source" from "this PCM
was built from different sources that happen to be newer", so a package install
and a working checkout — or two checkouts of different versions — silently
compiled their project.cpp against each other's declarations. Invalidation now
keys on a stamp over the bytes of every module source, which also covers the
case one file's mtime never could: the cached PCMs import each other, so a
change to :Interface invalidates :Clang's PCM with Crafter.Build-Clang.cppm
untouched.
Project args ApplyStandardArgs does not itself interpret are now folded into
VariantId. Such a flag typically decides what gets compiled or bundled — the
report's example is --no-webgpu dropping entries from cfg.files — and without it
both settings shared one bin dir and interleaved their outputs there, leaving a
bundle matching neither. Sorted and deduplicated so flag order doesn't split the
cache, and inherited by test Configurations.
`crafter-build clean` removes the project's bin/ and build/ trees. It
deliberately does not load project.cpp: cleaning is most often reached when
something is already wrong, and a clean that first needs the project to compile
is useless exactly then.
2026-07-30 17:43:09 +00:00
// Re-derived under the lock: another builder may have refreshed the
// cache from its own sources while we waited.
std : : string stamp = CrafterBuildSourceStamp ( sourceDir , CrafterBuildModules ) ;
feat(lint): const-local and constexpr-constant rules
Two rules the AST makes possible, plus the mutation analysis behind them.
const-local reports a local that is never written. It is restricted to SCALARS
— integers, bools, enums, floating types — and that restriction is what makes
the answer exact rather than a guess: a scalar has no member functions, so the
only ways to write one are assignment, ++/--, having its address taken, or
binding to a non-const reference. All four are now tracked in the walk:
- assignment and compound assignment visit their LEFT operand in a write
context, the right one normally;
- ++/-- and & write their operand;
- a call argument is checked against the callee's parameter type, so passing
to `const int&` or by value is a read while `int&` is a write;
- initialising a non-const reference writes what it binds to.
For a class type a non-const method call could mutate it, and deciding that is
the whole-program analysis clang-tidy does, so those are simply out of scope
rather than guessed at.
constexpr-constant promotes a const constant whose initialiser is made only of
literals and operators, so `const int A = 1 << 4;` qualifies and
`const int B = Compute();` does not.
On this repository const-local found 103 candidates, which was too many to be
useful, and the reason was informative: most were range-for bindings and
pointer locals. `for (T* const x : …)` and `T* const p` are not spellings
anybody writes, and the useful constness for a pointer is on the pointee, which
this rule cannot advise on. Excluding both leaves 36, all plain bool or enum
locals worth fixing — isWasm, isPe, exists, writes, isC and so on. Those 36 are
fixed in this commit; the compiler verified every one.
Both rules are report-only. The analysis is exact, but adding const is a
judgement about intent as much as mechanics, and a wrong suggestion should cost
a glance rather than a build. const-local also deliberately does not become a
transform: inserting `const` before a shared type would apply it to every
declarator in a multi-declarator statement, including any that IS written.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 00:50:48 +02:00
const bool upToDate = ReadCacheStamp ( cacheDir ) = = stamp ;
2026-07-23 01:24:42 +02:00
for ( std : : string_view name : CrafterBuildModules ) {
fs : : path cppmPath = sourceDir / std : : format ( " {}.cppm " , name ) ;
fs : : path pcmPath = cacheDir / std : : format ( " {}.pcm " , name ) ;
2026-04-27 07:04:42 +02:00
if ( ! fs : : exists ( cppmPath ) ) {
throw std : : runtime_error ( std : : format ( " module source {} not found in {} (set CRAFTER_BUILD_HOME) " , name , sourceDir . string ( ) ) ) ;
}
fix: key the host PCM cache on source content, add clean, hash project args
Three follow-ons to the stale-build report, all cases of an identity not
capturing something that changes the output.
The host PCM cache under <cache>/crafter.build/<target>-<march>/ is shared by
every crafter-build on the machine, and freshness was a per-file mtime
comparison. That cannot tell "this PCM is newer than my source" from "this PCM
was built from different sources that happen to be newer", so a package install
and a working checkout — or two checkouts of different versions — silently
compiled their project.cpp against each other's declarations. Invalidation now
keys on a stamp over the bytes of every module source, which also covers the
case one file's mtime never could: the cached PCMs import each other, so a
change to :Interface invalidates :Clang's PCM with Crafter.Build-Clang.cppm
untouched.
Project args ApplyStandardArgs does not itself interpret are now folded into
VariantId. Such a flag typically decides what gets compiled or bundled — the
report's example is --no-webgpu dropping entries from cfg.files — and without it
both settings shared one bin dir and interleaved their outputs there, leaving a
bundle matching neither. Sorted and deduplicated so flag order doesn't split the
cache, and inherited by test Configurations.
`crafter-build clean` removes the project's bin/ and build/ trees. It
deliberately does not load project.cpp: cleaning is most often reached when
something is already wrong, and a clean that first needs the project to compile
is useless exactly then.
2026-07-30 17:43:09 +00:00
if ( upToDate & & fs : : exists ( pcmPath ) ) {
2026-04-27 07:04:42 +02:00
continue ;
}
std : : string cmd = std : : format (
musl host support: bootstrap and run on Alpine (x86_64-alpine-linux-musl)
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>
2026-09-01 23:49:00 +02:00
" clang++ --target={} -march=native -mtune=native "
2026-04-27 07:04:42 +02:00
" -std=c++26 -stdlib=libc++ -O3 "
" -Wno-reserved-identifier -Wno-reserved-module-identifier "
" -fprebuilt-module-path={} "
" --precompile {} -o {} " ,
musl host support: bootstrap and run on Alpine (x86_64-alpine-linux-musl)
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>
2026-09-01 23:49:00 +02:00
HostTarget ( ) , cacheDir . string ( ) , cppmPath . string ( ) , pcmPath . string ( ) ) ;
2026-04-27 07:04:42 +02:00
CommandResult r = Crafter : : RunCommandChecked ( cmd ) ;
if ( r . exitCode ! = 0 ) {
throw std : : runtime_error ( std : : format ( " Failed to precompile {} (exit {}): {} " , name , r . exitCode , r . output ) ) ;
}
}
fix: key the host PCM cache on source content, add clean, hash project args
Three follow-ons to the stale-build report, all cases of an identity not
capturing something that changes the output.
The host PCM cache under <cache>/crafter.build/<target>-<march>/ is shared by
every crafter-build on the machine, and freshness was a per-file mtime
comparison. That cannot tell "this PCM is newer than my source" from "this PCM
was built from different sources that happen to be newer", so a package install
and a working checkout — or two checkouts of different versions — silently
compiled their project.cpp against each other's declarations. Invalidation now
keys on a stamp over the bytes of every module source, which also covers the
case one file's mtime never could: the cached PCMs import each other, so a
change to :Interface invalidates :Clang's PCM with Crafter.Build-Clang.cppm
untouched.
Project args ApplyStandardArgs does not itself interpret are now folded into
VariantId. Such a flag typically decides what gets compiled or bundled — the
report's example is --no-webgpu dropping entries from cfg.files — and without it
both settings shared one bin dir and interleaved their outputs there, leaving a
bundle matching neither. Sorted and deduplicated so flag order doesn't split the
cache, and inherited by test Configurations.
`crafter-build clean` removes the project's bin/ and build/ trees. It
deliberately does not load project.cpp: cleaning is most often reached when
something is already wrong, and a clean that first needs the project to compile
is useless exactly then.
2026-07-30 17:43:09 +00:00
// Last, so an interrupted rebuild leaves the stamp disagreeing with the
// sources and the next run starts over rather than trusting a half-built
// set of PCMs.
if ( ! upToDate ) WriteCacheStamp ( cacheDir , stamp ) ;
2026-04-27 07:04:42 +02:00
}
}
Configuration Crafter : : LoadProject ( const fs : : path & projectFile , std : : span < const std : : string_view > args ) {
fs : : path absProject = fs : : canonical ( projectFile ) ;
feat: project.cpp declares its own dependencies with #pragma crafter dependency
A project.cpp could only import std and Crafter.Build, so anything shared
between build descriptions had to be a header every consumer copies. Now
#pragma crafter dependency "<source>" [branch "<b>" | commit "<sha>"] [project "<file>"]
names a sibling crafter-build project that project.cpp itself imports a
module from. LoadProject scans for the directive, resolves it through
GitProject (a URL) or LocalProject (a path relative to the project file),
forces the result into host-tooling mode (the host triple LoadProject's
own cache is keyed on, native march, no debug, -fPIC so the archive links
into a shared object), builds it, and puts its BMIs and archive on the
project.so command line. The dependency's own project.cpp goes through
the same loader, so its directives resolve recursively; a cycle between
project files is an error instead of unbounded recursion.
The dependency is built on every load, fresh or not: clang crashes rather
than erroring when it imports a BMI whose own imported PCM was rebuilt
underneath it, and the module depfile — which Module::Check already reads —
is what notices the host PCM cache was regenerated. Its artifact mtime is
an input to project.so's staleness check, so editing the dependency
recompiles project.cpp without touching it. The Linux link gains
-fuse-ld=lld because release archives hold ThinLTO bitcode.
ProjectDependency(name) hands the same Configuration* back from inside
CrafterBuildProject, by configuration name or exported module name, so a
test that links the module needn't spell the source twice. It reads a
thread_local frame stack that an RAII guard keeps for the whole of
LoadProject, which also provides the cycle check.
ExpectedOutput() is the link gate's artifact-path lambda hoisted into the
:Clang partition, so the staleness check and the -r runner share the one
rule. Windows branches are wired identically but unverified.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-02 20:03:22 +02:00
ProjectFileGuard guard ( absProject ) ;
2026-04-27 07:04:42 +02:00
fs : : path buildDir = absProject . parent_path ( ) / " build " ;
if ( ! fs : : exists ( buildDir ) ) {
fs : : create_directories ( buildDir ) ;
}
2026-07-23 01:24:42 +02:00
fs : : path soPath = buildDir / std : : format ( " {}.so " , absProject . stem ( ) . string ( ) ) ;
2026-04-27 07:04:42 +02:00
fs : : path hostExe = fs : : read_symlink ( " /proc/self/exe " ) ;
V2: WASI, -r flag, CI pipeline, examples & tests cleanup
WASI / wasm32 target support
- Auto-detect /usr/share/wasi-sysroot on Linux when target starts_with("wasm32")
- Skip -march/-mtune for wasm (clang rejects them)
- Apply -fno-exceptions -fno-c++-static-destructors -mllvm -wasm-enable-sjlj
-D_WASI_EMULATED_SIGNAL to wasm builds (compile + std PCM, kept in sync)
- .wasm output extension in expectedOutputFor and link command
- EnableWasiBrowserRuntime(cfg): opt-in helper that drops index.html +
runtime.js next to the .wasm; runtime.js reads window.CRAFTER_WASM_URL
set in the templated index.html so a single shim handles any output name
-r run flag in the CLI: build then exec the artifact (host targets only;
rejects libraries; auto .exe/.wasm extension handling)
CI pipeline (.forgejo/workflows/ci.yaml)
- Triggers: PR/push to master + manual dispatch
- Single arch-latest container job: install deps, bootstrap, self-rebuild,
run tests, cross-compile mingw, package both archives, upload artifacts
- Rolling 'latest' release published only on push/dispatch to master
mingw cross-compile from Linux now works end-to-end:
- ExternalDependency cache key includes target so per-target glslang builds
don't collide; CMAKE_BUILD_TYPE=Release pinned (otherwise glslang appends
'd' to lib names and breaks linking); cross-compile cmake flags
(CMAKE_SYSTEM_NAME=Windows, CMAKE_*_COMPILER_TARGET=...)
- project.cpp accepts --target=<triple>; Linux-only -Wl,--export-dynamic
and -ldl are gated; mingw glslang skips the standalone exe (its libgcc_eh
link pulls pthread which mingw doesn't link by default)
- mingw compile uses -femulated-tls so std::__once_callable etc reference
the same emutls symbols libstdc++ provides
- mingw link auto-adds -lstdc++exp -lpthread
GetCrafterBuildHome() exposed from the Platform module; LoadProject (Linux
+ Windows) now both use it instead of duplicating the resolution.
Examples reorg: hello-world, library, with-module, wasi, tests — each with
its own README. Tests reorg: per-test directory with inner/ fixture, no
shared tests/fixtures/ tree. New Wasi test verifies .wasm magic bytes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 23:24:46 +02:00
fs : : path sourceDir = Crafter : : GetCrafterBuildHome ( ) ;
2026-04-27 07:04:42 +02:00
musl host support: bootstrap and run on Alpine (x86_64-alpine-linux-musl)
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>
2026-09-01 23:49:00 +02:00
// 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.
2026-04-27 07:04:42 +02:00
Configuration hostConfig ;
musl host support: bootstrap and run on Alpine (x86_64-alpine-linux-musl)
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>
2026-09-01 23:49:00 +02:00
hostConfig . target = HostTarget ( ) ;
2026-04-27 07:04:42 +02:00
hostConfig . march = " native " ;
hostConfig . mtune = " native " ;
fs : : path cacheDir = GetCacheDir ( ) / std : : format ( " {}-{} " , hostConfig . target , hostConfig . march ) ;
if ( ! fs : : exists ( cacheDir ) ) fs : : create_directories ( cacheDir ) ;
std : : string stdResult = BuildStdPcm ( hostConfig , cacheDir / " std.pcm " ) ;
if ( ! stdResult . empty ( ) ) {
throw std : : runtime_error ( std : : format ( " Failed to build std.pcm: {} " , stdResult ) ) ;
}
EnsureCrafterBuildPcms ( sourceDir , cacheDir ) ;
feat: project.cpp declares its own dependencies with #pragma crafter dependency
A project.cpp could only import std and Crafter.Build, so anything shared
between build descriptions had to be a header every consumer copies. Now
#pragma crafter dependency "<source>" [branch "<b>" | commit "<sha>"] [project "<file>"]
names a sibling crafter-build project that project.cpp itself imports a
module from. LoadProject scans for the directive, resolves it through
GitProject (a URL) or LocalProject (a path relative to the project file),
forces the result into host-tooling mode (the host triple LoadProject's
own cache is keyed on, native march, no debug, -fPIC so the archive links
into a shared object), builds it, and puts its BMIs and archive on the
project.so command line. The dependency's own project.cpp goes through
the same loader, so its directives resolve recursively; a cycle between
project files is an error instead of unbounded recursion.
The dependency is built on every load, fresh or not: clang crashes rather
than erroring when it imports a BMI whose own imported PCM was rebuilt
underneath it, and the module depfile — which Module::Check already reads —
is what notices the host PCM cache was regenerated. Its artifact mtime is
an input to project.so's staleness check, so editing the dependency
recompiles project.cpp without touching it. The Linux link gains
-fuse-ld=lld because release archives hold ThinLTO bitcode.
ProjectDependency(name) hands the same Configuration* back from inside
CrafterBuildProject, by configuration name or exported module name, so a
test that links the module needn't spell the source twice. It reads a
thread_local frame stack that an RAII guard keeps for the whole of
LoadProject, which also provides the cycle check.
ExpectedOutput() is the link gate's artifact-path lambda hoisted into the
:Clang partition, so the staleness check and the -r runner share the one
rule. Windows branches are wired identically but unverified.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-02 20:03:22 +02:00
// After the host PCMs exist (a dependency's build imports Crafter.Build
// from that cache) and before the staleness check (its artifacts are
// inputs to project.so). Built on every load, fresh or not: a dependency
// BMI whose own imports were rebuilt underneath it doesn't fail cleanly,
// clang crashes on it.
ProjectFileDependencies preDeps = ResolveProjectFileDependencies ( absProject , hostConfig ) ;
const bool stale = ProjectFileStale ( soPath , absProject , hostExe , preDeps ) ;
2026-04-27 07:04:42 +02:00
if ( stale ) {
musl host support: bootstrap and run on Alpine (x86_64-alpine-linux-musl)
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>
2026-09-01 23:49:00 +02:00
// -fuse-ld=lld like every other link the tool performs: lld is a hard
Merge branch 'claude/project-dependencies': project.cpp dependencies, AGENTS.md
Brings in `#pragma crafter dependency`, the switch to Crafter.LintRules
for the house style, and the contributor guide in AGENTS.md.
The branch predates the musl host work on master, and both rewrote the
command that compiles project.so, so the merge reconciles them by hand:
- LoadProject compiles project.so for hostConfig.target (HostTarget(),
from master) with the pre-dependency module path and archives from
the branch. -fuse-ld=lld appears once, with both reasons in the
comment.
- ForceHostTooling added -fPIC only for the literal x86_64-pc-linux-gnu
triple. hostConfig now comes from HostTarget(), so on Alpine the host
is x86_64-alpine-linux-musl and the dependency archive would not have
been position-independent. It now matches any *-linux-gnu or
*-linux-musl host, as project.cpp and the cmake flags do since the
musl change, and its comment no longer argues against HostTarget().
- The ProjectDependency test compared the dependency's target against
the glibc triple, which the musl CI job would fail; it compares
against HostTarget().
- README keeps master's paragraph on the bootstrap triple. AGENTS.md
describes the test block as `if (linuxTarget)`, which is what
project.cpp has now.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-02 20:47:23 +02:00
// dependency, binutils' ld is not (Alpine's clang would default to it),
// and a dependency archive built in release holds ThinLTO bitcode,
// which GNU ld can't read.
2026-04-27 07:04:42 +02:00
std : : string compileCmd = std : : format (
musl host support: bootstrap and run on Alpine (x86_64-alpine-linux-musl)
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>
2026-09-01 23:49:00 +02:00
" clang++ --target={} -march=native -mtune=native "
" -std=c++26 -stdlib=libc++ -shared -fPIC -O3 -fuse-ld=lld "
2026-04-27 07:04:42 +02:00
" -Wno-return-type-c-linkage "
feat: project.cpp declares its own dependencies with #pragma crafter dependency
A project.cpp could only import std and Crafter.Build, so anything shared
between build descriptions had to be a header every consumer copies. Now
#pragma crafter dependency "<source>" [branch "<b>" | commit "<sha>"] [project "<file>"]
names a sibling crafter-build project that project.cpp itself imports a
module from. LoadProject scans for the directive, resolves it through
GitProject (a URL) or LocalProject (a path relative to the project file),
forces the result into host-tooling mode (the host triple LoadProject's
own cache is keyed on, native march, no debug, -fPIC so the archive links
into a shared object), builds it, and puts its BMIs and archive on the
project.so command line. The dependency's own project.cpp goes through
the same loader, so its directives resolve recursively; a cycle between
project files is an error instead of unbounded recursion.
The dependency is built on every load, fresh or not: clang crashes rather
than erroring when it imports a BMI whose own imported PCM was rebuilt
underneath it, and the module depfile — which Module::Check already reads —
is what notices the host PCM cache was regenerated. Its artifact mtime is
an input to project.so's staleness check, so editing the dependency
recompiles project.cpp without touching it. The Linux link gains
-fuse-ld=lld because release archives hold ThinLTO bitcode.
ProjectDependency(name) hands the same Configuration* back from inside
CrafterBuildProject, by configuration name or exported module name, so a
test that links the module needn't spell the source twice. It reads a
thread_local frame stack that an RAII guard keeps for the whole of
LoadProject, which also provides the cycle check.
ExpectedOutput() is the link gate's artifact-path lambda hoisted into the
:Clang partition, so the staleness check and the -r runner share the one
rule. Windows branches are wired identically but unverified.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-02 20:03:22 +02:00
" -fprebuilt-module-path={}{} "
Merge branch 'claude/project-dependencies': project.cpp dependencies, AGENTS.md
Brings in `#pragma crafter dependency`, the switch to Crafter.LintRules
for the house style, and the contributor guide in AGENTS.md.
The branch predates the musl host work on master, and both rewrote the
command that compiles project.so, so the merge reconciles them by hand:
- LoadProject compiles project.so for hostConfig.target (HostTarget(),
from master) with the pre-dependency module path and archives from
the branch. -fuse-ld=lld appears once, with both reasons in the
comment.
- ForceHostTooling added -fPIC only for the literal x86_64-pc-linux-gnu
triple. hostConfig now comes from HostTarget(), so on Alpine the host
is x86_64-alpine-linux-musl and the dependency archive would not have
been position-independent. It now matches any *-linux-gnu or
*-linux-musl host, as project.cpp and the cmake flags do since the
musl change, and its comment no longer argues against HostTarget().
- The ProjectDependency test compared the dependency's target against
the glibc triple, which the musl CI job would fail; it compares
against HostTarget().
- README keeps master's paragraph on the bootstrap triple. AGENTS.md
describes the test block as `if (linuxTarget)`, which is what
project.cpp has now.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-02 20:47:23 +02:00
" {} -o {}{} " ,
hostConfig . target , cacheDir . string ( ) , preDeps . compileFlags ,
feat: project.cpp declares its own dependencies with #pragma crafter dependency
A project.cpp could only import std and Crafter.Build, so anything shared
between build descriptions had to be a header every consumer copies. Now
#pragma crafter dependency "<source>" [branch "<b>" | commit "<sha>"] [project "<file>"]
names a sibling crafter-build project that project.cpp itself imports a
module from. LoadProject scans for the directive, resolves it through
GitProject (a URL) or LocalProject (a path relative to the project file),
forces the result into host-tooling mode (the host triple LoadProject's
own cache is keyed on, native march, no debug, -fPIC so the archive links
into a shared object), builds it, and puts its BMIs and archive on the
project.so command line. The dependency's own project.cpp goes through
the same loader, so its directives resolve recursively; a cycle between
project files is an error instead of unbounded recursion.
The dependency is built on every load, fresh or not: clang crashes rather
than erroring when it imports a BMI whose own imported PCM was rebuilt
underneath it, and the module depfile — which Module::Check already reads —
is what notices the host PCM cache was regenerated. Its artifact mtime is
an input to project.so's staleness check, so editing the dependency
recompiles project.cpp without touching it. The Linux link gains
-fuse-ld=lld because release archives hold ThinLTO bitcode.
ProjectDependency(name) hands the same Configuration* back from inside
CrafterBuildProject, by configuration name or exported module name, so a
test that links the module needn't spell the source twice. It reads a
thread_local frame stack that an RAII guard keeps for the whole of
LoadProject, which also provides the cycle check.
ExpectedOutput() is the link gate's artifact-path lambda hoisted into the
:Clang partition, so the staleness check and the -r runner share the one
rule. Windows branches are wired identically but unverified.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-02 20:03:22 +02:00
absProject . string ( ) , soPath . string ( ) , preDeps . linkFlags ) ;
2026-04-27 07:04:42 +02:00
fix: decide a build step by its exit code, not by whether it printed
RunCommand merged stderr into stdout, dropped pclose's status and returned
the text; every compile, link and archive site then read "printed something"
as "failed". A warning is printing something, so a translation unit that
warned failed the build — but only on the run that actually recompiled it,
since warnings aren't re-emitted for an object that's already up to date.
The same unchanged source therefore passed or failed depending on the state
of the build tree: flaky-looking tests locally, and a cold CI checkout
surfacing every latent warning in a project at once as unrelated failures.
RunCommand is gone, replaced by RunBuildCommand: it goes through
RunCommandChecked, returns "" when the command exited 0 (so every caller's
`if (!result.empty())` error path is unchanged) and hands any warnings to
the new Progress::Diagnostic instead of to the error path. That also closes
the quiet half of the bug — a compiler killed by the OOM killer prints
nothing, so it used to read as success and leave the build carrying on with
a missing object; it now reports the signal that killed it.
Warnings are now shown rather than swallowed, which they weren't in either
direction before: invisible on an incremental build, fatal on a cold one.
Failing on them stays a project's choice, via -Werror in compileFlags.
2026-08-26 00:51:15 +00:00
std : : string result = RunBuildCommand ( compileCmd ) ;
2026-04-27 07:04:42 +02:00
if ( ! result . empty ( ) ) {
throw std : : runtime_error ( std : : format ( " Failed to compile project {}: {} " , absProject . string ( ) , result ) ) ;
}
}
void * handle = dlopen ( soPath . c_str ( ) , RTLD_NOW | RTLD_GLOBAL ) ;
if ( ! handle ) {
throw std : : runtime_error ( std : : format ( " Failed to load project {}: {} " , soPath . string ( ) , dlerror ( ) ) ) ;
}
using ProjectFn = Configuration ( * ) ( std : : span < const std : : string_view > ) ;
dlerror ( ) ;
auto fn = reinterpret_cast < ProjectFn > ( dlsym ( handle , " CrafterBuildProject " ) ) ;
if ( const char * err = dlerror ( ) ) {
throw std : : runtime_error ( std : : format ( " CrafterBuildProject not found in {}: {} " , soPath . string ( ) , err ) ) ;
}
feat: project.cpp declares its own dependencies with #pragma crafter dependency
A project.cpp could only import std and Crafter.Build, so anything shared
between build descriptions had to be a header every consumer copies. Now
#pragma crafter dependency "<source>" [branch "<b>" | commit "<sha>"] [project "<file>"]
names a sibling crafter-build project that project.cpp itself imports a
module from. LoadProject scans for the directive, resolves it through
GitProject (a URL) or LocalProject (a path relative to the project file),
forces the result into host-tooling mode (the host triple LoadProject's
own cache is keyed on, native march, no debug, -fPIC so the archive links
into a shared object), builds it, and puts its BMIs and archive on the
project.so command line. The dependency's own project.cpp goes through
the same loader, so its directives resolve recursively; a cycle between
project files is an error instead of unbounded recursion.
The dependency is built on every load, fresh or not: clang crashes rather
than erroring when it imports a BMI whose own imported PCM was rebuilt
underneath it, and the module depfile — which Module::Check already reads —
is what notices the host PCM cache was regenerated. Its artifact mtime is
an input to project.so's staleness check, so editing the dependency
recompiles project.cpp without touching it. The Linux link gains
-fuse-ld=lld because release archives hold ThinLTO bitcode.
ProjectDependency(name) hands the same Configuration* back from inside
CrafterBuildProject, by configuration name or exported module name, so a
test that links the module needn't spell the source twice. It reads a
thread_local frame stack that an RAII guard keeps for the whole of
LoadProject, which also provides the cycle check.
ExpectedOutput() is the link gate's artifact-path lambda hoisted into the
:Clang partition, so the staleness check and the -r runner share the one
rule. Windows branches are wired identically but unverified.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-02 20:03:22 +02:00
guard . Frame ( ) . dependencies = std : : move ( preDeps . configs ) ;
2026-04-27 07:04:42 +02:00
return fn ( args ) ;
2026-04-23 01:57:25 +02:00
}