2026-07-23 01:24:42 +02:00
// SPDX-License-Identifier: LGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
2026-04-23 01:57:25 +02:00
2026-05-19 00:50:06 +02:00
module ;
# if defined(CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_pc_windows_msvc) || defined(CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_w64_mingw32)
# include <winsock2.h>
# include <ws2tcpip.h>
2026-05-27 03:15:19 +00:00
// <rpc.h> (pulled in transitively) defines `interface` as a macro for `struct`,
// which collides with local variables named `interface` in this TU.
# undef interface
2026-05-19 00:50:06 +02:00
# else
# include <sys/socket.h>
# include <netinet/in.h>
# include <unistd.h>
# endif
2026-04-23 01:57:25 +02:00
export module Crafter . Build : Clang_impl ;
import std ;
import : Clang ;
2026-04-27 07:04:42 +02:00
import : Platform ;
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
import : Test ;
2026-07-23 01:24:42 +02:00
import : Lint ;
2026-04-29 03:27:11 +02:00
import : Progress ;
2026-05-12 01:16:40 +02:00
import : Asset ;
2026-04-23 01:57:25 +02:00
namespace fs = std : : filesystem ;
2026-04-27 07:04:42 +02:00
using namespace Crafter ;
2026-04-23 01:57:25 +02:00
2026-04-27 07:04:42 +02:00
2026-07-30 17:12:24 +00:00
namespace {
// Map one `import X;` name onto either a module this Configuration owns or
// one exported by a Configuration reachable through its dependency DAG,
// appending the matching staleness edge. False means nothing in reach
// provides X — either it's supplied from outside the graph (`std`) or the
// dependency isn't wired up *yet*, which is why callers remember the name.
bool ResolveImportName ( Configuration & cfg , const std : : string & importName , std : : vector < Module * > & localDeps , std : : vector < std : : pair < Module * , fs : : path > > & externalDeps ) {
for ( const std : : unique_ptr < Module > & interface : cfg . interfaces ) {
2026-04-27 07:04:42 +02:00
if ( interface - > name = = importName ) {
localDeps . push_back ( interface . get ( ) ) ;
return true ;
}
}
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 : : unordered_set < Configuration * > seen ;
std : : function < bool ( Configuration * ) > walk = [ & ] ( Configuration * depCfg ) - > bool {
if ( ! seen . insert ( depCfg ) . second ) return false ;
2026-04-27 07:04:42 +02:00
for ( const std : : unique_ptr < Module > & depInterface : depCfg - > interfaces ) {
if ( depInterface - > name = = importName ) {
2026-07-23 01:24:42 +02:00
fs : : path depPcmPath = std : : format ( " {}.pcm " , ( depCfg - > PcmDir ( ) / depInterface - > path . filename ( ) ) . string ( ) ) ;
2026-04-27 07:04:42 +02:00
externalDeps . emplace_back ( depInterface . get ( ) , std : : move ( depPcmPath ) ) ;
return true ;
}
}
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
for ( Configuration * sub : depCfg - > dependencies ) {
if ( walk ( sub ) ) return true ;
}
return false ;
} ;
2026-07-30 17:12:24 +00:00
for ( Configuration * depCfg : cfg . dependencies ) {
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 ( walk ( depCfg ) ) return true ;
2026-04-27 07:04:42 +02:00
}
return false ;
2026-07-30 17:12:24 +00:00
}
}
void Configuration : : ResolvePendingImports ( ) {
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
// Same resolution the scan used, and against both dependency kinds — a
// second GetInterfacesAndImplementations call can add interfaces that an
// earlier batch's import was looking for, so a pending name may land on a
// local module and not just an external one.
auto sweep = [ this ] ( std : : vector < std : : string > & pending , std : : vector < Module * > & localDeps , std : : vector < std : : pair < Module * , fs : : path > > & externalDeps ) {
2026-07-30 17:12:24 +00:00
std : : erase_if ( pending , [ & ] ( const std : : string & name ) {
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
return ResolveImportName ( * this , name , localDeps , externalDeps ) ;
2026-07-30 17:12:24 +00:00
} ) ;
} ;
for ( const std : : unique_ptr < Module > & interface : interfaces ) {
fix: track what a primary module interface imports
A primary module interface unit — `export module Widget;`, no partitions —
recorded nothing about what it imported. GetInterfacesAndImplementations
registered the Module and then erased the file from the scan list, so the
import pass only ever saw partitions, and Module had no vectors to hold an
edge anyway.
Two consequences, both reported as issue #26:
Module::Check consulted only its own .cppm and its partitions. A data
member added to an imported module left Widget.pcm, Widget.o and every
consumer object untouched while the imported library rebuilt and both
binaries relinked — one executable holding two class layouts, no
diagnostic, and a crash somewhere unrelated. Wiping build/ was the only
cure, so `crafter-build test` could not be trusted straight after an
interface edit.
Module::Compile waited on nothing. Two modules in one Configuration
compile on concurrent threads, so a primary interface importing a
sibling was a coin flip between working and "module 'Base' not found".
Partitions never had either problem — they carry the same three vectors and
Check/Compile honour them — which is why the gap only surfaced on a module
whose interface is one flat unit.
Module now carries moduleDependencies, externalModuleDependencies and
pendingImports with the same meanings as on ModulePartition; primary units
stay in the scan list so their imports land there; Check sees through them;
Compile orders itself behind a local sibling; and ResolvePendingImports
sweeps them so an edge survives dependencies being wired up afterwards.
Build() now Checks every interface before spawning any compile thread — the
`compiled` flag a waiter blocks on is raised either by a Compile that runs
or by the Check that decides none is needed, so a Check still pending while
another module's thread waits would have hung the build.
Resolves #26
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-30 18:19:04 +00:00
sweep ( interface - > pendingImports , interface - > moduleDependencies , interface - > externalModuleDependencies ) ;
2026-07-30 17:12:24 +00:00
for ( const std : : unique_ptr < ModulePartition > & partition : interface - > partitions ) {
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
sweep ( partition - > pendingImports , partition - > moduleDependencies , partition - > externalModuleDependencies ) ;
2026-07-30 17:12:24 +00:00
}
}
for ( Implementation & implementation : implementations ) {
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
sweep ( implementation . pendingImports , implementation . moduleDependencies , implementation . externalModuleDependencies ) ;
2026-07-30 17:12:24 +00:00
}
}
void Configuration : : GetInterfacesAndImplementations ( std : : span < fs : : path > interfaces , std : : span < fs : : path > implementations ) {
auto resolveImport = [ this ] ( const std : : string & importName , std : : vector < Module * > & localDeps , std : : vector < std : : pair < Module * , fs : : path > > & externalDeps , std : : vector < std : : string > & pending ) {
if ( ! ResolveImportName ( * this , importName , localDeps , externalDeps ) ) {
pending . push_back ( importName ) ;
}
2026-04-27 07:04:42 +02:00
} ;
std : : vector < std : : tuple < fs : : path , std : : string , ModulePartition * , Module * > > tempModulePaths = std : : vector < std : : tuple < fs : : path , std : : string , ModulePartition * , Module * > > ( interfaces . size ( ) ) ;
for ( std : : uint16_t i = 0 ; i < interfaces . size ( ) ; i + + ) {
2026-04-30 02:20:19 +02:00
// Resolve to absolute now so the stored path survives cwd changes
// (matters for GitProject deps loaded from a different working dir).
fs : : path file = fs : : absolute ( path / interfaces [ i ] ) . lexically_normal ( ) ;
2026-04-27 07:04:42 +02:00
file + = " .cppm " ;
std : : ifstream t ( file ) ;
std : : stringstream buffer ;
buffer < < t . rdbuf ( ) ;
std : : string fileContent = buffer . str ( ) ;
fileContent = std : : regex_replace ( fileContent , std : : regex ( R " (//[^ \n ]*) " ) , " " ) ;
fileContent = std : : regex_replace ( fileContent , std : : regex ( R " (/ \ *.*? \ */) " ) , " " ) ;
tempModulePaths [ i ] = { file , fileContent , nullptr , nullptr } ;
}
fix: track what a primary module interface imports
A primary module interface unit — `export module Widget;`, no partitions —
recorded nothing about what it imported. GetInterfacesAndImplementations
registered the Module and then erased the file from the scan list, so the
import pass only ever saw partitions, and Module had no vectors to hold an
edge anyway.
Two consequences, both reported as issue #26:
Module::Check consulted only its own .cppm and its partitions. A data
member added to an imported module left Widget.pcm, Widget.o and every
consumer object untouched while the imported library rebuilt and both
binaries relinked — one executable holding two class layouts, no
diagnostic, and a crash somewhere unrelated. Wiping build/ was the only
cure, so `crafter-build test` could not be trusted straight after an
interface edit.
Module::Compile waited on nothing. Two modules in one Configuration
compile on concurrent threads, so a primary interface importing a
sibling was a coin flip between working and "module 'Base' not found".
Partitions never had either problem — they carry the same three vectors and
Check/Compile honour them — which is why the gap only surfaced on a module
whose interface is one flat unit.
Module now carries moduleDependencies, externalModuleDependencies and
pendingImports with the same meanings as on ModulePartition; primary units
stay in the scan list so their imports land there; Check sees through them;
Compile orders itself behind a local sibling; and ResolvePendingImports
sweeps them so an edge survives dependencies being wired up afterwards.
Build() now Checks every interface before spawning any compile thread — the
`compiled` flag a waiter blocks on is raised either by a Compile that runs
or by the Check that decides none is needed, so a Check still pending while
another module's thread waits would have hung the build.
Resolves #26
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-30 18:19:04 +00:00
// Primary interface units first, so the partition pass below can find their
// parent Module. They stay in tempModulePaths — a primary unit's own
// `import X;` lines are layout dependencies exactly like a partition's, and
// dropping the entry here is what left them unrecorded (issue #26). Marked
// by a null partition slot with the Module slot filled in.
for ( std : : tuple < fs : : path , std : : string , ModulePartition * , Module * > & file : tempModulePaths ) {
2026-04-27 07:04:42 +02:00
std : : smatch match ;
if ( std : : regex_search ( std : : get < 1 > ( file ) , match , std : : regex ( R " (export module ([a-zA-Z0-9_ \ . \ -]+);) " ) ) ) {
fix: track what a primary module interface imports
A primary module interface unit — `export module Widget;`, no partitions —
recorded nothing about what it imported. GetInterfacesAndImplementations
registered the Module and then erased the file from the scan list, so the
import pass only ever saw partitions, and Module had no vectors to hold an
edge anyway.
Two consequences, both reported as issue #26:
Module::Check consulted only its own .cppm and its partitions. A data
member added to an imported module left Widget.pcm, Widget.o and every
consumer object untouched while the imported library rebuilt and both
binaries relinked — one executable holding two class layouts, no
diagnostic, and a crash somewhere unrelated. Wiping build/ was the only
cure, so `crafter-build test` could not be trusted straight after an
interface edit.
Module::Compile waited on nothing. Two modules in one Configuration
compile on concurrent threads, so a primary interface importing a
sibling was a coin flip between working and "module 'Base' not found".
Partitions never had either problem — they carry the same three vectors and
Check/Compile honour them — which is why the gap only surfaced on a module
whose interface is one flat unit.
Module now carries moduleDependencies, externalModuleDependencies and
pendingImports with the same meanings as on ModulePartition; primary units
stay in the scan list so their imports land there; Check sees through them;
Compile orders itself behind a local sibling; and ResolvePendingImports
sweeps them so an edge survives dependencies being wired up afterwards.
Build() now Checks every interface before spawning any compile thread — the
`compiled` flag a waiter blocks on is raised either by a Compile that runs
or by the Check that decides none is needed, so a Check still pending while
another module's thread waits would have hung the build.
Resolves #26
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-30 18:19:04 +00:00
fs : : path pthCpy = std : : get < 0 > ( file ) ;
pthCpy . replace_extension ( " " ) ;
this - > interfaces . push_back ( std : : make_unique < Module > ( std : : move ( match [ 1 ] . str ( ) ) , std : : move ( pthCpy ) ) ) ;
std : : get < 3 > ( file ) = this - > interfaces . back ( ) . get ( ) ;
2026-04-27 07:04:42 +02:00
}
fix: track what a primary module interface imports
A primary module interface unit — `export module Widget;`, no partitions —
recorded nothing about what it imported. GetInterfacesAndImplementations
registered the Module and then erased the file from the scan list, so the
import pass only ever saw partitions, and Module had no vectors to hold an
edge anyway.
Two consequences, both reported as issue #26:
Module::Check consulted only its own .cppm and its partitions. A data
member added to an imported module left Widget.pcm, Widget.o and every
consumer object untouched while the imported library rebuilt and both
binaries relinked — one executable holding two class layouts, no
diagnostic, and a crash somewhere unrelated. Wiping build/ was the only
cure, so `crafter-build test` could not be trusted straight after an
interface edit.
Module::Compile waited on nothing. Two modules in one Configuration
compile on concurrent threads, so a primary interface importing a
sibling was a coin flip between working and "module 'Base' not found".
Partitions never had either problem — they carry the same three vectors and
Check/Compile honour them — which is why the gap only surfaced on a module
whose interface is one flat unit.
Module now carries moduleDependencies, externalModuleDependencies and
pendingImports with the same meanings as on ModulePartition; primary units
stay in the scan list so their imports land there; Check sees through them;
Compile orders itself behind a local sibling; and ResolvePendingImports
sweeps them so an edge survives dependencies being wired up afterwards.
Build() now Checks every interface before spawning any compile thread — the
`compiled` flag a waiter blocks on is raised either by a Compile that runs
or by the Check that decides none is needed, so a Check still pending while
another module's thread waits would have hung the build.
Resolves #26
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-30 18:19:04 +00:00
}
2026-04-27 07:04:42 +02:00
for ( std : : uint16_t i = 0 ; i < tempModulePaths . size ( ) ; i + + ) {
std : : smatch match ;
fix: track what a primary module interface imports
A primary module interface unit — `export module Widget;`, no partitions —
recorded nothing about what it imported. GetInterfacesAndImplementations
registered the Module and then erased the file from the scan list, so the
import pass only ever saw partitions, and Module had no vectors to hold an
edge anyway.
Two consequences, both reported as issue #26:
Module::Check consulted only its own .cppm and its partitions. A data
member added to an imported module left Widget.pcm, Widget.o and every
consumer object untouched while the imported library rebuilt and both
binaries relinked — one executable holding two class layouts, no
diagnostic, and a crash somewhere unrelated. Wiping build/ was the only
cure, so `crafter-build test` could not be trusted straight after an
interface edit.
Module::Compile waited on nothing. Two modules in one Configuration
compile on concurrent threads, so a primary interface importing a
sibling was a coin flip between working and "module 'Base' not found".
Partitions never had either problem — they carry the same three vectors and
Check/Compile honour them — which is why the gap only surfaced on a module
whose interface is one flat unit.
Module now carries moduleDependencies, externalModuleDependencies and
pendingImports with the same meanings as on ModulePartition; primary units
stay in the scan list so their imports land there; Check sees through them;
Compile orders itself behind a local sibling; and ResolvePendingImports
sweeps them so an edge survives dependencies being wired up afterwards.
Build() now Checks every interface before spawning any compile thread — the
`compiled` flag a waiter blocks on is raised either by a Compile that runs
or by the Check that decides none is needed, so a Check still pending while
another module's thread waits would have hung the build.
Resolves #26
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-30 18:19:04 +00:00
if ( std : : get < 3 > ( tempModulePaths [ i ] ) ! = nullptr ) {
continue ;
}
2026-04-27 07:04:42 +02:00
if ( std : : regex_search ( std : : get < 1 > ( tempModulePaths [ i ] ) , match , std : : regex ( R " (export module ([a-zA-Z_0-9 \ . \ -]+):([a-zA-Z_0-9 \ . \ -]+);) " ) ) ) {
for ( const std : : unique_ptr < Module > & modulee : this - > interfaces ) {
if ( modulee - > name = = match [ 1 ] ) {
std : : string name = match [ 2 ] . str ( ) ;
fs : : path pthCpy = std : : get < 0 > ( tempModulePaths [ i ] ) ;
pthCpy . replace_extension ( " " ) ;
std : : unique_ptr < ModulePartition > partition = std : : make_unique < ModulePartition > ( std : : move ( name ) , std : : move ( pthCpy ) ) ;
std : : get < 2 > ( tempModulePaths [ i ] ) = partition . get ( ) ;
modulee - > partitions . push_back ( std : : move ( partition ) ) ;
std : : get < 3 > ( tempModulePaths [ i ] ) = modulee . get ( ) ;
goto next ;
}
}
2026-07-23 01:24:42 +02:00
throw std : : runtime_error ( std : : format ( " Module {} not found, referenced in {} " , match [ 1 ] . str ( ) , std : : get < 0 > ( tempModulePaths [ i ] ) . string ( ) ) ) ;
2026-04-27 07:04:42 +02:00
} else {
2026-07-23 01:24:42 +02:00
throw std : : runtime_error ( std : : format ( " No module declaration found in {} " , std : : get < 0 > ( tempModulePaths [ i ] ) . string ( ) ) ) ;
2026-04-27 07:04:42 +02:00
}
next : ;
}
for ( std : : tuple < fs : : path , std : : string , ModulePartition * , Module * > & file : tempModulePaths ) {
ModulePartition * partition = std : : get < 2 > ( file ) ;
Module * parentModule = std : : get < 3 > ( file ) ;
const std : : string & fileContent = std : : get < 1 > ( file ) ;
fix: track what a primary module interface imports
A primary module interface unit — `export module Widget;`, no partitions —
recorded nothing about what it imported. GetInterfacesAndImplementations
registered the Module and then erased the file from the scan list, so the
import pass only ever saw partitions, and Module had no vectors to hold an
edge anyway.
Two consequences, both reported as issue #26:
Module::Check consulted only its own .cppm and its partitions. A data
member added to an imported module left Widget.pcm, Widget.o and every
consumer object untouched while the imported library rebuilt and both
binaries relinked — one executable holding two class layouts, no
diagnostic, and a crash somewhere unrelated. Wiping build/ was the only
cure, so `crafter-build test` could not be trusted straight after an
interface edit.
Module::Compile waited on nothing. Two modules in one Configuration
compile on concurrent threads, so a primary interface importing a
sibling was a coin flip between working and "module 'Base' not found".
Partitions never had either problem — they carry the same three vectors and
Check/Compile honour them — which is why the gap only surfaced on a module
whose interface is one flat unit.
Module now carries moduleDependencies, externalModuleDependencies and
pendingImports with the same meanings as on ModulePartition; primary units
stay in the scan list so their imports land there; Check sees through them;
Compile orders itself behind a local sibling; and ResolvePendingImports
sweeps them so an edge survives dependencies being wired up afterwards.
Build() now Checks every interface before spawning any compile thread — the
`compiled` flag a waiter blocks on is raised either by a Compile that runs
or by the Check that decides none is needed, so a Check still pending while
another module's thread waits would have hung the build.
Resolves #26
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-30 18:19:04 +00:00
// Primary interface unit: record only its module imports. Its
// `[export] import :Part;` lines need no edge — Module::Check walks
// every partition it owns and Module::Compile builds them all before
// precompiling itself, so partitions are covered wholesale.
if ( partition = = nullptr ) {
std : : regex primaryPattern ( R " (import ([a-zA-Z_0-9 \ . \ -]+);) " ) ;
std : : sregex_iterator primaryCurrent ( fileContent . begin ( ) , fileContent . end ( ) , primaryPattern ) ;
std : : sregex_iterator primaryLast ;
while ( primaryCurrent ! = primaryLast ) {
std : : smatch match = * primaryCurrent ;
resolveImport ( match [ 1 ] . str ( ) , parentModule - > moduleDependencies , parentModule - > externalModuleDependencies , parentModule - > pendingImports ) ;
+ + primaryCurrent ;
}
continue ;
}
2026-04-27 07:04:42 +02:00
std : : regex partitionPattern ( R " (import :([a-zA-Z_ \ -0-9 \ .]+);) " ) ;
std : : sregex_iterator currentMatch ( fileContent . begin ( ) , fileContent . end ( ) , partitionPattern ) ;
std : : sregex_iterator lastMatch ;
while ( currentMatch ! = lastMatch ) {
std : : smatch match = * currentMatch ;
for ( std : : unique_ptr < ModulePartition > & sibling : parentModule - > partitions ) {
if ( sibling - > name = = match [ 1 ] ) {
partition - > partitionDependencies . push_back ( sibling . get ( ) ) ;
goto next2 ;
}
}
throw std : : runtime_error ( std : : format ( " imported partition {}:{} not found, referenced in {} " , parentModule - > name , match [ 1 ] . str ( ) , std : : get < 0 > ( file ) . string ( ) ) ) ;
next2 : + + currentMatch ;
}
std : : regex modulePattern ( R " (import ([a-zA-Z_0-9 \ . \ -]+);) " ) ;
std : : sregex_iterator modCurrent ( fileContent . begin ( ) , fileContent . end ( ) , modulePattern ) ;
while ( modCurrent ! = lastMatch ) {
std : : smatch match = * modCurrent ;
2026-07-30 17:12:24 +00:00
resolveImport ( match [ 1 ] . str ( ) , partition - > moduleDependencies , partition - > externalModuleDependencies , partition - > pendingImports ) ;
2026-04-27 07:04:42 +02:00
+ + modCurrent ;
}
}
for ( const fs : : path & tempFile : implementations ) {
2026-04-30 02:20:19 +02:00
fs : : path file = fs : : absolute ( path / tempFile ) . lexically_normal ( ) ;
2026-04-27 07:04:42 +02:00
file + = " .cpp " ;
std : : ifstream t ( file ) ;
std : : stringstream buffer ;
buffer < < t . rdbuf ( ) ;
std : : string fileContent = buffer . str ( ) ;
fileContent = std : : regex_replace ( fileContent , std : : regex ( R " (//[^ \n ]*) " ) , " " ) ;
fileContent = std : : regex_replace ( fileContent , std : : regex ( R " (/ \ *.*? \ */) " ) , " " ) ;
std : : smatch match ;
fs : : path fileCopy = file ;
fileCopy . replace_extension ( " " ) ;
Implementation & implementation = this - > implementations . emplace_back ( std : : move ( fileCopy ) ) ;
if ( std : : regex_search ( fileContent , match , std : : regex ( R " (module ([a-zA-Z0-9_ \ . \ -]+)(:[a-zA-Z0-9_ \ . \ -]+)? \ s*;) " ) ) ) {
bool isPartitionImpl = match [ 2 ] . length ( ) > 0 ;
for ( const std : : unique_ptr < Module > & interface : this - > interfaces ) {
if ( interface - > name = = match [ 1 ] ) {
if ( ! isPartitionImpl ) {
implementation . moduleDependencies . push_back ( interface . get ( ) ) ;
}
std : : regex partitionPattern ( R " (import :([a-zA-Z_ \ -0-9 \ .]+);) " ) ;
std : : sregex_iterator currentMatch ( fileContent . begin ( ) , fileContent . end ( ) , partitionPattern ) ;
std : : sregex_iterator lastMatch ;
while ( currentMatch ! = lastMatch ) {
std : : smatch match2 = * currentMatch ;
for ( const std : : unique_ptr < ModulePartition > & partition : interface - > partitions ) {
if ( partition - > name = = match2 [ 1 ] ) {
implementation . partitionDependencies . push_back ( partition . get ( ) ) ;
goto next3 ;
}
}
throw std : : runtime_error ( std : : format ( " imported partition {}:{} not found, referenced in {} " , match [ 1 ] . str ( ) , match2 [ 1 ] . str ( ) , file . string ( ) ) ) ;
next3 : + + currentMatch ;
}
std : : regex modulePattern ( R " (import ([a-zA-Z_0-9 \ . \ -]+);) " ) ;
std : : sregex_iterator modCurrent ( fileContent . begin ( ) , fileContent . end ( ) , modulePattern ) ;
while ( modCurrent ! = lastMatch ) {
std : : smatch match2 = * modCurrent ;
if ( match2 [ 1 ] ! = match [ 1 ] ) {
2026-07-30 17:12:24 +00:00
resolveImport ( match2 [ 1 ] . str ( ) , implementation . moduleDependencies , implementation . externalModuleDependencies , implementation . pendingImports ) ;
2026-04-27 07:04:42 +02:00
}
+ + modCurrent ;
}
goto next4 ;
}
}
throw std : : runtime_error ( std : : format ( " Module {} not found not found, referenced in {} " , match [ 1 ] . str ( ) , file . string ( ) ) ) ;
next4 : ;
} else {
std : : regex pattern ( R " (import ([a-zA-Z_ \ -0-9 \ .]+);) " ) ;
std : : sregex_iterator currentMatch ( fileContent . begin ( ) , fileContent . end ( ) , pattern ) ;
std : : sregex_iterator lastMatch ;
while ( currentMatch ! = lastMatch ) {
std : : smatch match2 = * currentMatch ;
2026-07-30 17:12:24 +00:00
resolveImport ( match2 [ 1 ] . str ( ) , implementation . moduleDependencies , implementation . externalModuleDependencies , implementation . pendingImports ) ;
2026-04-27 07:04:42 +02:00
+ + currentMatch ;
}
}
}
}
refactor: extract GetCompileCommand and StdPcmDir out of Build
The clang invocation was assembled inline across four regions of Build,
interleaved with the dependency-graph walk, so nothing else could ask "what
flags does this Configuration compile with". The linter's AST layer needs
exactly that, and it cannot approximate it: a precompiled module is rejected
outright by a translation unit whose target features differ from the one that
wrote it. Dropping just -march=native produces hundreds of "compiled with the
target feature '+avx512bw' but the current translation unit is not" errors and
no usable parse, so reconstructed flags fail hard rather than degrade.
GetCompileCommand is the config-pure part: target, arch, standard,
configuration defines, module search paths, includes, user compileFlags,
optimisation and LTO. Build appends only what depends on work having happened
— dependency public flags and external dependency flags. The sub-strings it
also needs on their own (includes, defines, user flags, LTO) come back as
struct members, so the .c compile path is unchanged.
Verified by probing `command` at the equivalent point before and after and
diffing: byte-identical across all 23 configurations exercised by a full build
plus the test suite.
Two incidental simplifications fell out. pcmDir was recomputing what
Configuration::PcmDir() already returns, and cmakeBuildType is now a
one-liner. GetCompileCommand is also most of what a compile_commands.json
would need, which this repo lacks.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 23:15:20 +02:00
fs : : path Crafter : : StdPcmDir ( const Configuration & config ) {
// The std PCM is cached per target+march, but a wasm variant pass compiles
// it with extra codegen flags (e.g. -mrelaxed-simd) — its BMI must not be
// shared with the baseline's, or the consuming TUs see a target-feature
// mismatch. Suffix the cache dir with the variant flags when present.
std : : string stdPcmKey = std : : format ( " {}-{} " , config . target , config . march ) ;
for ( const std : : string & f : config . wasmVariantFlags ) {
stdPcmKey + = " + " ;
for ( char c : f ) stdPcmKey + = ( c = = ' / ' | | c = = ' \\ ' ) ? ' _ ' : c ;
}
return GetCacheDir ( ) / stdPcmKey ;
}
CompileCommand Crafter : : GetCompileCommand ( const Configuration & config ) {
CompileCommand out ;
out . stdPcmDir = StdPcmDir ( config ) ;
out . pcmDir = config . PcmDir ( ) ;
std : : string editedTarget = config . target ;
std : : replace ( editedTarget . begin ( ) , editedTarget . end ( ) , ' - ' , ' _ ' ) ;
// wasm32 targets reject -march and silently ignore -mtune (clang errors on
// the former). Skip both for any wasm32-* triple.
bool isWasm = config . target . starts_with ( " wasm32 " ) ;
std : : string archFlags = isWasm
? std : : string ( )
: std : : format ( " -march={} -mtune={} " , config . march , config . mtune ) ;
out . command = std : : format ( " {} --target={}{} -std=c++26 -D CRAFTER_BUILD_CONFIGURATION_TARGET= \\ \" {} \\ \" -D CRAFTER_BUILD_CONFIGURATION_TARGET_{} -fprebuilt-module-path={} -fprebuilt-module-path={} " , GetBaseCommand ( config ) , config . target , archFlags , editedTarget , editedTarget , out . stdPcmDir . string ( ) , out . pcmDir . string ( ) ) ;
if ( ! config . sysroot . empty ( ) ) {
out . command + = std : : format ( " --sysroot={} " , config . sysroot ) ;
}
if ( isWasm ) {
// -mllvm is consumed by codegen but not the link driver, which is the
// same command line; quiet the unused-flag warning rather than split
// compile and link commands.
out . command + = " -fno-exceptions -msimd128 -fno-c++-static-destructors -mllvm -wasm-enable-sjlj -D_WASI_EMULATED_SIGNAL -Wno-unused-command-line-argument " ;
// Active variant pass (see the variant driver at the end of Build):
// extra codegen flags (e.g. -mrelaxed-simd) applied to every TU. Empty
// for the baseline build. Part of VariantId, so these objects/PCMs
// land in their own dir.
for ( const std : : string & f : config . wasmVariantFlags ) {
out . command + = std : : format ( " {} " , f ) ;
}
}
if ( config . target = = " x86_64-w64-mingw32 " ) {
// mingw libstdc++ defines TLS via __emutls_v.* (emulated TLS); without
// -femulated-tls clang generates native-TLS references that don't
// match. Symptom: undefined std::__once_callable / __once_call at
// link time. Also -Wno-unused… because -femulated-tls is a codegen
// flag the link driver doesn't consume.
out . command + = " -femulated-tls -Wno-unused-command-line-argument " ;
}
if ( config . type = = ConfigurationType : : LibraryDynamic ) {
# ifdef CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_pc_linux_gnu
out . command + = " -fPIC -D CRAFTER_BUILD_CONFIGURATION_TYPE_SHARED_LIBRARY " ;
# endif
# if defined(CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_pc_windows_msvc) || defined(CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_w64_mingw32)
out . command + = " -D CRAFTER_BUILD_CONFIGURATION_TYPE_SHARED_LIBRARY " ;
# endif
} else if ( config . type = = ConfigurationType : : Executable ) {
out . command + = " -D CRAFTER_BUILD_CONFIGURATION_TYPE_EXECUTABLE " ;
// On Windows targets the API uses __declspec(dllimport) when consuming
// a DLL. Set the macro for executables so CRAFTER_API resolves to
// dllimport in their PCM cache (separate from the lib's PCM cache,
// which gets dllexport). Harmless if the exe doesn't actually link a
// crafter DLL — CRAFTER_API only matters at API call sites.
if ( config . target = = " x86_64-w64-mingw32 " | | config . target = = " x86_64-pc-windows-msvc " ) {
out . command + = " -D CRAFTER_BUILD_DLL_IMPORT " ;
}
} else {
out . command + = " -D CRAFTER_BUILD_CONFIGURATION_TYPE_LIBRARY " ;
}
// -I propagation that's valid for both C and C++ compiles. Module-only
// bits (-fprebuilt-module-path) stay on `command` only.
{
std : : unordered_set < Configuration * > seen ;
std : : function < void ( Configuration * ) > addFlags = [ & ] ( Configuration * dep ) {
if ( ! seen . insert ( dep ) . second ) return ;
for ( const auto & entry : fs : : recursive_directory_iterator ( dep - > path ) ) {
if ( entry . is_directory ( ) & & entry . path ( ) . filename ( ) = = " include " ) {
out . includeFlags + = std : : format ( " -I{} " , entry . path ( ) . string ( ) ) ;
}
}
out . includeFlags + = std : : format ( " -I{} " , dep - > path . string ( ) ) ;
out . command + = std : : format ( " -fprebuilt-module-path={} " , dep - > PcmDir ( ) . string ( ) ) ;
for ( Configuration * sub : dep - > dependencies ) {
addFlags ( sub ) ;
}
} ;
for ( Configuration * dep : config . dependencies ) {
addFlags ( dep ) ;
}
}
out . command + = out . includeFlags ;
// Defines belong on both C and C++ compiles so vendored C dependencies
// can see configuration-level macros consistently with module sources.
for ( const Define & define : config . defines ) {
if ( define . value . empty ( ) ) {
out . defineFlags + = std : : format ( " -D {} " , define . name ) ;
} else {
out . defineFlags + = std : : format ( " -D {}={} " , define . name , define . value ) ;
}
}
out . command + = out . defineFlags ;
// Track caller-provided compileFlags separately so the .c compile can
// pick them up too (vendored C deps usually need -I from this set).
for ( const std : : string & flag : config . compileFlags ) {
out . userFlags + = std : : format ( " {} " , flag ) ;
}
out . command + = out . userFlags ;
// Behaviour-neutral release performance for the binaries we emit: ThinLTO
// (cross-TU inlining) plus dead-section GC and safe identical-code folding.
// Default-on in Release, never Debug (keeps builds fast and debuggable).
// Excluded for wasm32 — its -mllvm/sjlj codegen flags don't compose with
// LTO here — and for nvcc .cu objects below (they can't emit LLVM bitcode,
// so they link in as ordinary objects alongside the bitcode ones). ThinLTO
// rather than monolithic -flto so link time and memory scale with arbitrary
// user project sizes. Compile-side flags (-flto, -ffunction/-fdata-sections)
// ride on `command`, which is reused as the link base; the link-only -Wl
// flags go on linkExtras so they don't warn on each -c compile.
out . useLto = ! config . debug & & ! isWasm ;
out . ltoCompileFlags = out . useLto ? " -flto=thin -ffunction-sections -fdata-sections " : " " ;
out . ltoLinkFlags = out . useLto ? " -flto=thin -Wl,--gc-sections -Wl,--icf=safe " : " " ;
if ( config . debug ) {
out . command + = " -g -D CRAFTER_BUILD_CONFIGURATION_DEBUG " ;
} else {
out . command + = " -O3 " ;
}
out . command + = out . ltoCompileFlags ;
return out ;
}
2026-04-27 07:04:42 +02:00
BuildResult Crafter : : Build ( Configuration & config , std : : unordered_map < fs : : path , std : : shared_future < BuildResult > > & depResults , std : : mutex & depMutex ) {
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
// Reset per-build cached state on every Module/ModulePartition so that
// successive Build() calls on the same Configuration re-evaluate mtimes
fix: scope per-build module-state reset to the config being built
Build() resets each Module/ModulePartition's per-build `compiled`/`checked`
flags so a reused Configuration re-evaluates mtimes. That reset recursed into
cfg.dependencies — but dependency Configurations are shared across the build
DAG and each is compiled concurrently by its own Build() call.
A parent/sibling's recursive reset could therefore clear a shared dependency's
module `compiled` atomic *after* that dependency's module-compile thread had
set it true and exited, but before an intra-config waiter (its impl, or a
dependent partition) ran compiled.wait(false). The waiter then blocked forever
on a flag nothing would re-signal: the build froze mid-compile, idle, with no
compiler process alive — exactly the hang in issue #16.
Reset only the current configuration's own modules. Every config in the tree
already gets its own Build() call (the per-PcmDir builder registered in
depResults), which resets its own state at the top of that call, sequenced
before its compile threads spawn. Cross-config module state is consulted only
via PCM file mtimes and the depResults futures, never via these flags, so the
narrower reset is correct and removes the data race entirely.
Adds ConcurrentDependencyReset: builds a static-lib dependency fully, then
builds a consumer that depends on it while the dependency is already cached in
depResults (so it is never rebuilt), and asserts the consumer build leaves the
dependency's module `compiled` flag intact. Fails deterministically on the old
recursive reset; passes with the fix.
Resolves #16
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 11:32:46 +00:00
// (incremental-rebuild test scenarios).
//
// Reset ONLY this configuration's own modules — never recurse into
// dependencies. Configuration objects are shared across the dependency DAG
// (diamond deps point at the same Configuration*), and every config in the
// tree gets its own Build() call: the per-PcmDir builder registered in
// depResults resets its own state here, at the top of its own Build(),
// sequenced before that config's compile threads spawn. Recursing into
// dependencies from here would re-clear a shared dependency's `compiled`
// atomic from a parent/sibling thread *while that dependency's own Build()
// is concurrently compiling it* — after its module-compile thread set the
// flag true and exited but before its impl/partition waiter ran
// compiled.wait(false). The waiter would then block forever on a flag
// nothing re-signals: the build freezes mid-compile, idle, with no compiler
// process alive (issue #16). Cross-config module state is consulted only via
// PCM file mtimes and the depResults futures, never via these flags, so the
// narrower reset is correct.
for ( auto & iface : config . interfaces ) {
iface - > checked = false ;
iface - > compiled . store ( false ) ;
for ( auto & part : iface - > partitions ) {
part - > checked = false ;
part - > compiled . store ( false ) ;
}
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-07-30 17:12:24 +00:00
// Sources were scanned when they were declared, which for most callers is
// before `dependencies` exists — AddTest resolves tests/<name>/main.cpp and
// only then hands back a builder whose .Dependencies() supplies the library.
// Any `import <DepModule>;` in such a TU resolved to nothing and so carried
// no staleness edge, which meant a member added to a dependency's interface
// rebuilt the library, relinked the consumer, and silently kept the
// consumer's object compiled against the *old* class layout (issue #27).
// Re-resolving here — the last point before mtimes are compared, with the
// DAG fully wired — closes that window for every caller rather than for the
// ones that remember to declare in the right order.
config . ResolvePendingImports ( ) ;
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
// Auto-detect the WASI sysroot before any compile step runs so BuildStdPcm
// and the main compile command see the same value. Linux-only — Windows
// users supply cfg.sysroot pointing at their wasi-sdk install. Covers all
// wasm32-* triples (wasi, wasip1, wasip2, ...); the sysroot's per-triple
// subdirs handle the differences.
# ifdef CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_pc_linux_gnu
if ( config . sysroot . empty ( ) & & config . target . starts_with ( " wasm32 " ) ) {
config . sysroot = " /usr/share/wasi-sysroot " ;
}
# endif
2026-04-30 02:20:19 +02:00
fs : : path buildDir = config . BuildDir ( ) ;
fs : : path outputDir = config . BinDir ( ) ;
2026-04-23 01:57:25 +02:00
if ( ! fs : : exists ( buildDir ) ) {
fs : : create_directories ( buildDir ) ;
}
if ( ! fs : : exists ( outputDir ) ) {
fs : : create_directories ( outputDir ) ;
}
2026-05-19 16:53:24 +02:00
BuildResult buildResult { } ;
2026-04-27 07:04:42 +02:00
2026-05-02 21:08:51 +02:00
// glslang #include search paths for every shader compiled in this
// configuration: each transitive (incl. self) buildFiles entry's parent
// dir, or the entry itself if it points at a directory. Collected once
// up front so the per-shader threads can capture the resulting span by
// reference. No file copy involved — glslang reads the includes in
// place from the dep's source tree.
std : : vector < fs : : path > shaderIncludeDirs ;
{
std : : unordered_set < std : : string > seenDirs ;
std : : unordered_set < const Configuration * > seenCfg ;
std : : function < void ( const Configuration * ) > collect = [ & ] ( const Configuration * c ) {
if ( ! seenCfg . insert ( c ) . second ) return ;
for ( const fs : : path & bf : c - > buildFiles ) {
fs : : path dir = fs : : is_directory ( bf ) ? bf : bf . parent_path ( ) ;
if ( seenDirs . insert ( dir . string ( ) ) . second ) {
shaderIncludeDirs . push_back ( std : : move ( dir ) ) ;
}
}
for ( const Configuration * sub : c - > dependencies ) collect ( sub ) ;
} ;
collect ( & config ) ;
}
2026-04-23 01:57:25 +02:00
std : : vector < std : : thread > threads ;
threads . reserve ( config . shaders . size ( ) + 1 + config . interfaces . size ( ) + config . implementations . size ( ) ) ;
std : : string buildError ;
std : : atomic < bool > buildCancelled { false } ;
for ( const Shader & shader : config . shaders ) {
if ( shader . Check ( outputDir ) ) continue ;
2026-05-02 21:08:51 +02:00
threads . emplace_back ( [ & shader , & outputDir , & shaderIncludeDirs , & buildError , & buildCancelled ] ( ) {
2026-04-29 03:27:11 +02:00
Progress : : Task task ( std : : format ( " Compiling shader {} " , shader . path . filename ( ) . string ( ) ) ) ;
2026-04-23 01:57:25 +02:00
if ( buildCancelled . load ( std : : memory_order_relaxed ) ) return ;
2026-05-02 21:08:51 +02:00
std : : string result = shader . Compile ( outputDir , shaderIncludeDirs ) ;
2026-04-23 01:57:25 +02:00
if ( result . empty ( ) ) return ;
bool expected = false ;
if ( buildCancelled . compare_exchange_strong ( expected , true ) ) {
buildError = std : : move ( result ) ;
}
} ) ;
}
2026-05-12 03:44:14 +02:00
// Asset compilation: each cfg.assets entry is either a single .png/.obj
// file (flat output: outputDir/<filename>.ctex/cmesh — preserves the
// original behavior) or a directory (recursed, with the relative tree
// mirrored under outputDir/<dirname>/; .png/.obj are compressed, every
// other file is copied through unchanged). Directory mode lets mod/map
// trees keep their nested layout so mod.json paths like
// "cannon/base.cmesh" resolve correctly at runtime.
// Skipped per-file if the output is newer than the source. Each
// compress runs in its own thread; passthrough copies are bundled into
// a single thread to match the cfg.files pattern.
auto compressedName = [ ] ( const fs : : path & src ) - > std : : optional < fs : : path > {
std : : string ext = src . extension ( ) . string ( ) ;
2026-07-23 01:24:42 +02:00
for ( char & c : ext ) c = static_cast < char > ( std : : tolower ( static_cast < std : : uint8_t > ( c ) ) ) ;
2026-05-19 00:50:06 +02:00
// stb_image (used by CompressAsset → TextureAsset::LoadPNG) handles
// png/tga/jpg/bmp; all map to .ctex.
if ( ext = = " .png " | | ext = = " .tga " | | ext = = " .jpg " | | ext = = " .jpeg " | | ext = = " .bmp " ) {
return fs : : path ( src . filename ( ) ) . replace_extension ( " .ctex " ) ;
}
2026-05-12 03:44:14 +02:00
if ( ext = = " .obj " ) return fs : : path ( src . filename ( ) ) . replace_extension ( " .cmesh " ) ;
2026-05-12 01:16:40 +02:00
return std : : nullopt ;
} ;
2026-05-12 03:44:14 +02:00
auto submitCompress = [ & ] ( fs : : path sourcePath , fs : : path outRelative ) {
fs : : path out = outputDir / outRelative ;
if ( fs : : exists ( out ) & & fs : : exists ( sourcePath ) & & fs : : last_write_time ( sourcePath ) < = fs : : last_write_time ( out ) ) return ;
threads . emplace_back ( [ sourcePath = std : : move ( sourcePath ) , out = std : : move ( out ) , & buildError , & buildCancelled ] ( ) {
Progress : : Task task ( std : : format ( " Compressing asset {} " , sourcePath . filename ( ) . string ( ) ) ) ;
2026-05-12 01:16:40 +02:00
if ( buildCancelled . load ( std : : memory_order_relaxed ) ) return ;
2026-05-12 03:44:14 +02:00
std : : error_code ec ;
fs : : create_directories ( out . parent_path ( ) , ec ) ;
std : : string result = CompressAsset ( sourcePath , out ) ;
2026-05-12 01:16:40 +02:00
if ( result . empty ( ) ) return ;
bool expected = false ;
if ( buildCancelled . compare_exchange_strong ( expected , true ) ) {
buildError = std : : move ( result ) ;
}
} ) ;
2026-05-12 03:44:14 +02:00
} ;
std : : vector < std : : pair < fs : : path , fs : : path > > assetPassthroughs ;
for ( const fs : : path & asset : config . assets ) {
if ( fs : : is_directory ( asset ) ) {
fs : : path topName = asset . filename ( ) ;
for ( const auto & entry : fs : : recursive_directory_iterator ( asset ) ) {
if ( ! entry . is_regular_file ( ) ) continue ;
fs : : path rel = fs : : relative ( entry . path ( ) , asset ) ;
if ( std : : optional < fs : : path > compName = compressedName ( entry . path ( ) ) ) {
submitCompress ( entry . path ( ) , topName / rel . parent_path ( ) / * compName ) ;
} else {
assetPassthroughs . emplace_back ( entry . path ( ) , topName / rel ) ;
}
}
} else {
std : : optional < fs : : path > outName = compressedName ( asset ) ;
if ( ! outName ) {
buildCancelled . store ( true ) ;
2026-05-19 00:50:06 +02:00
buildError = std : : format ( " {}: unsupported asset extension (expected .png/.tga/.jpg/.bmp/.obj, or a directory) " , asset . string ( ) ) ;
2026-05-12 03:44:14 +02:00
break ;
}
submitCompress ( asset , * outName ) ;
}
}
if ( ! assetPassthroughs . empty ( ) ) {
threads . emplace_back ( [ passthroughs = std : : move ( assetPassthroughs ) , & outputDir , & buildCancelled , & buildError ] ( ) {
Progress : : Task task ( " Copying asset passthrough files " ) ;
if ( buildCancelled . load ( std : : memory_order_relaxed ) ) return ;
try {
for ( const auto & [ src , rel ] : passthroughs ) {
fs : : path dst = outputDir / rel ;
fs : : create_directories ( dst . parent_path ( ) ) ;
if ( ! fs : : exists ( dst ) ) {
fs : : copy_file ( src , dst ) ;
} else if ( fs : : last_write_time ( src ) > fs : : last_write_time ( dst ) ) {
fs : : copy_file ( src , dst , fs : : copy_options : : overwrite_existing ) ;
}
}
} catch ( const std : : exception & e ) {
bool expected = false ;
if ( buildCancelled . compare_exchange_strong ( expected , true ) ) {
buildError = e . what ( ) ;
}
}
} ) ;
2026-05-12 01:16:40 +02:00
}
2026-04-23 01:57:25 +02:00
threads . emplace_back ( [ & config , & outputDir , & buildCancelled , & buildError ] ( ) {
2026-04-29 03:27:11 +02:00
Progress : : Task task ( std : : format ( " Copying files for {} " , config . name ) ) ;
2026-04-23 01:57:25 +02:00
if ( buildCancelled . load ( std : : memory_order_relaxed ) ) return ;
try {
2026-05-02 21:08:51 +02:00
for ( const fs : : path & additionalFile : config . files ) {
2026-04-23 01:57:25 +02:00
fs : : path destination = outputDir / additionalFile . filename ( ) ;
if ( fs : : is_directory ( additionalFile ) ) {
for ( const auto & entry : fs : : recursive_directory_iterator ( additionalFile ) ) {
const fs : : path & sourcePath = entry . path ( ) ;
fs : : path relativePath = fs : : relative ( sourcePath , additionalFile ) ;
fs : : path destPath = destination / relativePath ;
if ( entry . is_directory ( ) ) {
2026-05-02 21:08:51 +02:00
if ( ! fs : : exists ( destPath ) ) fs : : create_directories ( destPath ) ;
2026-04-23 01:57:25 +02:00
} else if ( entry . is_regular_file ( ) ) {
fs : : create_directories ( destPath . parent_path ( ) ) ;
if ( ! fs : : exists ( destPath ) ) {
fs : : copy_file ( sourcePath , destPath ) ;
2026-05-02 21:08:51 +02:00
} else if ( fs : : last_write_time ( sourcePath ) > fs : : last_write_time ( destPath ) ) {
2026-04-23 01:57:25 +02:00
fs : : copy_file ( sourcePath , destPath , fs : : copy_options : : overwrite_existing ) ;
}
}
}
} else {
if ( ! fs : : exists ( destination ) ) {
fs : : copy_file ( additionalFile , destination ) ;
2026-05-02 21:08:51 +02:00
} else if ( fs : : last_write_time ( additionalFile ) > fs : : last_write_time ( destination ) ) {
2026-04-23 01:57:25 +02:00
fs : : copy_file ( additionalFile , destination , fs : : copy_options : : overwrite_existing ) ;
}
}
}
} catch ( std : : exception & e ) {
bool expected = false ;
if ( buildCancelled . compare_exchange_strong ( expected , true ) ) {
buildError = e . what ( ) ;
}
}
} ) ;
2026-04-27 07:04:42 +02:00
std : : vector < ExternalBuildResult > externalResults ( config . externalDependencies . size ( ) ) ;
std : : vector < std : : thread > externalThreads ;
externalThreads . reserve ( config . externalDependencies . size ( ) ) ;
for ( std : : size_t i = 0 ; i < config . externalDependencies . size ( ) ; + + i ) {
externalThreads . emplace_back ( [ & , i ] ( ) {
2026-04-29 03:27:11 +02:00
Progress : : Task task ( std : : format ( " Building external dep {} " , config . externalDependencies [ i ] . name ) ) ;
2026-04-27 07:04:42 +02:00
if ( buildCancelled . load ( std : : memory_order_relaxed ) ) return ;
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
externalResults [ i ] = BuildExternal ( config . externalDependencies [ i ] , config . target , buildCancelled ) ;
2026-04-27 07:04:42 +02:00
if ( ! externalResults [ i ] . error . empty ( ) ) {
bool expected = false ;
if ( buildCancelled . compare_exchange_strong ( expected , true ) ) {
buildError = externalResults [ i ] . error ;
}
}
} ) ;
}
refactor: extract GetCompileCommand and StdPcmDir out of Build
The clang invocation was assembled inline across four regions of Build,
interleaved with the dependency-graph walk, so nothing else could ask "what
flags does this Configuration compile with". The linter's AST layer needs
exactly that, and it cannot approximate it: a precompiled module is rejected
outright by a translation unit whose target features differ from the one that
wrote it. Dropping just -march=native produces hundreds of "compiled with the
target feature '+avx512bw' but the current translation unit is not" errors and
no usable parse, so reconstructed flags fail hard rather than degrade.
GetCompileCommand is the config-pure part: target, arch, standard,
configuration defines, module search paths, includes, user compileFlags,
optimisation and LTO. Build appends only what depends on work having happened
— dependency public flags and external dependency flags. The sub-strings it
also needs on their own (includes, defines, user flags, LTO) come back as
struct members, so the .c compile path is unchanged.
Verified by probing `command` at the equivalent point before and after and
diffing: byte-identical across all 23 configurations exercised by a full build
plus the test suite.
Two incidental simplifications fell out. pcmDir was recomputing what
Configuration::PcmDir() already returns, and cmakeBuildType is now a
one-liner. GetCompileCommand is also most of what a compile_commands.json
would need, which this repo lacks.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 23:15:20 +02:00
fs : : path stdPcmDir = StdPcmDir ( config ) ;
2026-04-23 01:57:25 +02:00
if ( ! fs : : exists ( stdPcmDir ) ) {
fs : : create_directories ( stdPcmDir ) ;
}
2026-04-29 03:27:11 +02:00
std : : string stdPcmResult ;
{
Progress : : Task task ( std : : format ( " Building std PCM ({}-{}) " , config . target , config . march ) ) ;
stdPcmResult = BuildStdPcm ( config , stdPcmDir / " std.pcm " ) ;
}
2026-04-23 01:57:25 +02:00
if ( ! stdPcmResult . empty ( ) ) {
2026-04-27 07:04:42 +02:00
buildCancelled . store ( true ) ;
for ( std : : thread & thread : threads ) thread . join ( ) ;
for ( std : : thread & thread : externalThreads ) thread . join ( ) ;
return { stdPcmResult , false , { } } ;
2026-04-23 01:57:25 +02:00
}
refactor: extract GetCompileCommand and StdPcmDir out of Build
The clang invocation was assembled inline across four regions of Build,
interleaved with the dependency-graph walk, so nothing else could ask "what
flags does this Configuration compile with". The linter's AST layer needs
exactly that, and it cannot approximate it: a precompiled module is rejected
outright by a translation unit whose target features differ from the one that
wrote it. Dropping just -march=native produces hundreds of "compiled with the
target feature '+avx512bw' but the current translation unit is not" errors and
no usable parse, so reconstructed flags fail hard rather than degrade.
GetCompileCommand is the config-pure part: target, arch, standard,
configuration defines, module search paths, includes, user compileFlags,
optimisation and LTO. Build appends only what depends on work having happened
— dependency public flags and external dependency flags. The sub-strings it
also needs on their own (includes, defines, user flags, LTO) come back as
struct members, so the .c compile path is unchanged.
Verified by probing `command` at the equivalent point before and after and
diffing: byte-identical across all 23 configurations exercised by a full build
plus the test suite.
Two incidental simplifications fell out. pcmDir was recomputing what
Configuration::PcmDir() already returns, and cmakeBuildType is now a
one-liner. GetCompileCommand is also most of what a compile_commands.json
would need, which this repo lacks.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 23:15:20 +02:00
fs : : path pcmDir = config . PcmDir ( ) ;
2026-04-23 01:57:25 +02:00
2026-04-27 07:04:42 +02:00
fs : : copy_file ( stdPcmDir / " std.pcm " , pcmDir / " std.pcm " , fs : : copy_options : : update_existing ) ;
refactor: extract GetCompileCommand and StdPcmDir out of Build
The clang invocation was assembled inline across four regions of Build,
interleaved with the dependency-graph walk, so nothing else could ask "what
flags does this Configuration compile with". The linter's AST layer needs
exactly that, and it cannot approximate it: a precompiled module is rejected
outright by a translation unit whose target features differ from the one that
wrote it. Dropping just -march=native produces hundreds of "compiled with the
target feature '+avx512bw' but the current translation unit is not" errors and
no usable parse, so reconstructed flags fail hard rather than degrade.
GetCompileCommand is the config-pure part: target, arch, standard,
configuration defines, module search paths, includes, user compileFlags,
optimisation and LTO. Build appends only what depends on work having happened
— dependency public flags and external dependency flags. The sub-strings it
also needs on their own (includes, defines, user flags, LTO) come back as
struct members, so the .c compile path is unchanged.
Verified by probing `command` at the equivalent point before and after and
diffing: byte-identical across all 23 configurations exercised by a full build
plus the test suite.
Two incidental simplifications fell out. pcmDir was recomputing what
Configuration::PcmDir() already returns, and cmakeBuildType is now a
one-liner. GetCompileCommand is also most of what a compile_commands.json
would need, which this repo lacks.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 23:15:20 +02:00
// Everything about the compile command that is a pure function of the
// Configuration is assembled by GetCompileCommand, so that anything which
// needs to PARSE these sources with the same flags — the linter's AST
// layer — cannot drift from what actually built the PCMs. Build appends
// only what depends on work having happened: dependency public flags and
// external dependency flags, further down.
CompileCommand compile = GetCompileCommand ( config ) ;
std : : string command = compile . command ;
const std : : string & includeFlags = compile . includeFlags ;
const std : : string & defineFlags = compile . defineFlags ;
const std : : string & userFlags = compile . userFlags ;
const std : : string & ltoCompileFlags = compile . ltoCompileFlags ;
const std : : string & ltoLinkFlags = compile . ltoLinkFlags ;
const bool isWasm = config . target . starts_with ( " wasm32 " ) ;
const bool useLto = compile . useLto ;
2026-04-23 01:57:25 +02:00
std : : string files ;
std : : unordered_set < std : : string > libSet ;
2026-05-01 19:02:14 +02:00
std : : unordered_set < std : : string > publicFlagSet ;
2026-04-23 01:57:25 +02:00
std : : mutex fileMutex ;
std : : vector < std : : thread > depThreads ;
2026-04-27 07:04:42 +02:00
depThreads . reserve ( config . dependencies . size ( ) ) ;
2026-04-23 01:57:25 +02:00
std : : atomic < bool > repack ( false ) ;
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
for ( Configuration * dep : config . dependencies ) {
2026-04-27 07:04:42 +02:00
depThreads . emplace_back ( [ & , dep ] ( ) {
try {
if ( buildCancelled . load ( std : : memory_order_relaxed ) ) return ;
std : : shared_ptr < std : : promise < BuildResult > > promise ;
std : : shared_future < BuildResult > resultFuture ;
bool isBuilder = false ;
2026-04-23 01:57:25 +02:00
depMutex . lock ( ) ;
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
fs : : path cacheKey = dep - > PcmDir ( ) ;
auto it = depResults . find ( cacheKey ) ;
2026-04-27 07:04:42 +02:00
if ( it = = depResults . end ( ) ) {
isBuilder = true ;
promise = std : : make_shared < std : : promise < BuildResult > > ( ) ;
resultFuture = promise - > get_future ( ) . share ( ) ;
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
depResults . emplace ( cacheKey , resultFuture ) ;
2026-04-27 07:04:42 +02:00
} else {
resultFuture = it - > second ;
}
depMutex . unlock ( ) ;
if ( isBuilder ) {
BuildResult built ;
try {
built = Build ( * dep , depResults , depMutex ) ;
} catch ( . . . ) {
promise - > set_exception ( std : : current_exception ( ) ) ;
throw ;
2026-04-23 01:57:25 +02:00
}
2026-04-27 07:04:42 +02:00
promise - > set_value ( std : : move ( built ) ) ;
}
const BuildResult & result = resultFuture . get ( ) ;
fileMutex . lock ( ) ;
for ( const std : : string & lib : result . libs ) libSet . insert ( lib ) ;
2026-05-01 19:02:14 +02:00
for ( const std : : string & f : result . publicCompileFlags ) publicFlagSet . insert ( f ) ;
2026-04-27 07:04:42 +02:00
fileMutex . unlock ( ) ;
if ( ! result . result . empty ( ) ) {
bool expected = false ;
if ( buildCancelled . compare_exchange_strong ( expected , true ) ) {
buildError = result . result ;
2026-04-23 01:57:25 +02:00
}
2026-04-27 07:04:42 +02:00
}
if ( result . repack ) {
repack = true ;
}
} catch ( const std : : exception & e ) {
bool expected = false ;
if ( buildCancelled . compare_exchange_strong ( expected , true ) ) {
buildError = std : : format ( " dep build for {} threw: {} " , dep - > path . string ( ) , e . what ( ) ) ;
2026-04-23 01:57:25 +02:00
}
}
2026-04-27 07:04:42 +02:00
} ) ;
2026-04-23 01:57:25 +02:00
}
refactor: extract GetCompileCommand and StdPcmDir out of Build
The clang invocation was assembled inline across four regions of Build,
interleaved with the dependency-graph walk, so nothing else could ask "what
flags does this Configuration compile with". The linter's AST layer needs
exactly that, and it cannot approximate it: a precompiled module is rejected
outright by a translation unit whose target features differ from the one that
wrote it. Dropping just -march=native produces hundreds of "compiled with the
target feature '+avx512bw' but the current translation unit is not" errors and
no usable parse, so reconstructed flags fail hard rather than degrade.
GetCompileCommand is the config-pure part: target, arch, standard,
configuration defines, module search paths, includes, user compileFlags,
optimisation and LTO. Build appends only what depends on work having happened
— dependency public flags and external dependency flags. The sub-strings it
also needs on their own (includes, defines, user flags, LTO) come back as
struct members, so the .c compile path is unchanged.
Verified by probing `command` at the equivalent point before and after and
diffing: byte-identical across all 23 configurations exercised by a full build
plus the test suite.
Two incidental simplifications fell out. pcmDir was recomputing what
Configuration::PcmDir() already returns, and cmakeBuildType is now a
one-liner. GetCompileCommand is also most of what a compile_commands.json
would need, which this repo lacks.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 23:15:20 +02:00
// Only the CMake build type is still derived here; the compile flags it
// used to sit alongside now come from GetCompileCommand.
const std : : string cmakeBuildType = config . debug ? " Debug " : " Release " ;
2026-04-23 01:57:25 +02:00
2026-05-18 05:23:11 +02:00
// Same target-aware setup as the C++ compile path (line 459-): wasm32
// rejects -march, silently ignores -mtune, and needs --sysroot to find
// wasi-libc headers. Build the prefix once so all C compiles share it.
const bool cIsWasm = config . target . starts_with ( " wasm32 " ) ;
std : : string cArchFlags = cIsWasm
? std : : string ( )
: std : : format ( " -march={} -mtune={} " , config . march , config . mtune ) ;
if ( ! config . sysroot . empty ( ) ) {
cArchFlags + = std : : format ( " --sysroot={} " , config . sysroot ) ;
}
if ( cIsWasm ) {
// Matches the C++ path's wasi flag set so any libc shim defines
// (e.g. _WASI_EMULATED_SIGNAL → signal.h shims) are visible to
// C dependencies that drag in signal.h transitively.
cArchFlags + = " -D_WASI_EMULATED_SIGNAL " ;
}
2026-04-27 07:04:42 +02:00
for ( const fs : : path & cFile : config . cFiles ) {
2026-04-23 01:57:25 +02:00
files + = std : : format ( " {}_source.o " , ( buildDir / cFile . filename ( ) ) . string ( ) ) ;
2026-07-23 01:24:42 +02:00
const std : : string objPath = std : : format ( " {}_source.o " , ( buildDir / cFile . filename ( ) ) . string ( ) ) ;
const std : : string srcPath = std : : format ( " {}.c " , cFile . string ( ) ) ;
2026-04-30 02:20:19 +02:00
if ( ! fs : : exists ( objPath ) | | ( fs : : exists ( srcPath ) & & fs : : last_write_time ( srcPath ) > fs : : last_write_time ( objPath ) ) ) {
2026-06-08 19:28:20 +02:00
threads . emplace_back ( [ & cFile , & buildDir , & buildError , & buildCancelled , & config , & includeFlags , & defineFlags , & userFlags , & cArchFlags , & ltoCompileFlags ] ( ) {
2026-04-29 03:27:11 +02:00
Progress : : Task task ( std : : format ( " Compiling {}.c " , cFile . filename ( ) . string ( ) ) ) ;
2026-04-23 01:57:25 +02:00
if ( buildCancelled . load ( std : : memory_order_relaxed ) ) return ;
2026-06-08 19:28:20 +02:00
std : : string result = RunCommand ( std : : format ( " clang {}.c --target={}{} -O3{} -c{}{}{} -o {}_source.o " , cFile . string ( ) , config . target , cArchFlags , ltoCompileFlags , includeFlags , defineFlags , userFlags , ( buildDir / cFile . filename ( ) ) . string ( ) ) ) ;
2026-04-23 01:57:25 +02:00
if ( result . empty ( ) ) return ;
bool expected = false ;
if ( buildCancelled . compare_exchange_strong ( expected , true ) ) {
buildError = std : : move ( result ) ;
}
} ) ;
}
}
for ( const fs : : path & cFile : config . cuda ) {
files + = std : : format ( " {}_source.o " , ( buildDir / cFile . filename ( ) ) . string ( ) ) ;
2026-07-23 01:24:42 +02:00
const std : : string objPath = std : : format ( " {}_source.o " , ( buildDir / cFile . filename ( ) ) . string ( ) ) ;
const std : : string srcPath = std : : format ( " {}.cu " , cFile . string ( ) ) ;
2026-04-30 02:20:19 +02:00
if ( ! fs : : exists ( objPath ) | | ( fs : : exists ( srcPath ) & & fs : : last_write_time ( srcPath ) > fs : : last_write_time ( objPath ) ) ) {
2026-04-23 01:57:25 +02:00
threads . emplace_back ( [ & cFile , & buildDir , & buildError , & buildCancelled ] ( ) {
2026-04-29 03:27:11 +02:00
Progress : : Task task ( std : : format ( " Compiling {}.cu " , cFile . filename ( ) . string ( ) ) ) ;
2026-04-23 01:57:25 +02:00
if ( buildCancelled . load ( std : : memory_order_relaxed ) ) return ;
std : : string result = RunCommand ( std : : format ( " nvcc {}.cu -c -o {}_source.o -O3 -arch=sm_89 " , cFile . string ( ) , ( buildDir / cFile . filename ( ) ) . string ( ) ) ) ;
if ( result . empty ( ) ) return ;
bool expected = false ;
if ( buildCancelled . compare_exchange_strong ( expected , true ) ) {
buildError = std : : move ( result ) ;
}
} ) ;
}
}
for ( std : : thread & thread : depThreads ) {
thread . join ( ) ;
}
2026-04-27 07:04:42 +02:00
for ( std : : thread & thread : externalThreads ) {
thread . join ( ) ;
}
2026-04-23 01:57:25 +02:00
if ( buildCancelled . load ( ) ) {
2026-04-27 07:04:42 +02:00
for ( std : : thread & thread : threads ) thread . join ( ) ;
2026-04-23 01:57:25 +02:00
return { buildError , false , { } } ;
}
2026-05-02 21:08:51 +02:00
// Ship runtime artifacts from transitive deps' bin dirs alongside the
// executable: compiled .spv files (cfg.shaders) and asset files/dirs
// (cfg.files). The lib already mirrored these into its own bin dir
// during its build, but a consumer exe loads them from its own dir at
// runtime. Only an exe is a deployment unit; intermediate libs don't
// need to forward since the exe walks all transitive deps. Runs outside
// the repack gate because the relink mtime check above only watches
// .so/.dll/.a, so a shader/file-only change in a dep wouldn't trigger
// repack but still needs the new artifact copied across.
// (cfg.buildFiles use a different mechanism — they're exposed to shader
// compiles as #include search paths in place, no copy.)
2026-05-01 19:16:13 +02:00
if ( config . type = = ConfigurationType : : Executable ) {
try {
2026-05-02 21:08:51 +02:00
auto copyTree = [ ] ( const fs : : path & src , const fs : : path & dest ) {
if ( fs : : is_directory ( src ) ) {
for ( const auto & entry : fs : : recursive_directory_iterator ( src ) ) {
fs : : path rel = fs : : relative ( entry . path ( ) , src ) ;
fs : : path destPath = dest / rel ;
if ( entry . is_directory ( ) ) {
if ( ! fs : : exists ( destPath ) ) fs : : create_directories ( destPath ) ;
} else if ( entry . is_regular_file ( ) ) {
fs : : create_directories ( destPath . parent_path ( ) ) ;
if ( ! fs : : exists ( destPath ) ) {
fs : : copy_file ( entry . path ( ) , destPath ) ;
} else if ( fs : : last_write_time ( entry . path ( ) ) > fs : : last_write_time ( destPath ) ) {
fs : : copy_file ( entry . path ( ) , destPath , fs : : copy_options : : overwrite_existing ) ;
}
}
}
} else {
if ( ! fs : : exists ( dest ) ) {
fs : : copy_file ( src , dest ) ;
} else if ( fs : : last_write_time ( src ) > fs : : last_write_time ( dest ) ) {
2026-05-01 19:16:13 +02:00
fs : : copy_file ( src , dest , fs : : copy_options : : overwrite_existing ) ;
}
}
} ;
2026-05-02 21:08:51 +02:00
std : : unordered_set < Configuration * > seen ;
std : : function < void ( Configuration * ) > forwardDepArtifacts = [ & ] ( Configuration * dep ) {
if ( ! seen . insert ( dep ) . second ) return ;
fs : : path depBinDir = dep - > BinDir ( ) ;
for ( const Shader & shader : dep - > shaders ) {
fs : : path src = depBinDir / shader . path . filename ( ) . replace_extension ( " spv " ) ;
if ( ! fs : : exists ( src ) ) continue ;
copyTree ( src , outputDir / src . filename ( ) ) ;
}
for ( const fs : : path & additionalFile : dep - > files ) {
fs : : path src = depBinDir / additionalFile . filename ( ) ;
if ( ! fs : : exists ( src ) ) continue ;
copyTree ( src , outputDir / additionalFile . filename ( ) ) ;
}
2026-05-12 01:16:40 +02:00
for ( const fs : : path & asset : dep - > assets ) {
2026-05-12 03:44:14 +02:00
// Directory entry: the dep already mirrored the
// (compressed + passthrough) tree under
// depBinDir/<asset.filename()>/. Forward it wholesale
// so our bin dir gets the same layout.
if ( fs : : is_directory ( asset ) ) {
fs : : path src = depBinDir / asset . filename ( ) ;
if ( ! fs : : exists ( src ) ) continue ;
copyTree ( src , outputDir / asset . filename ( ) ) ;
continue ;
}
2026-05-12 01:16:40 +02:00
std : : string ext = asset . extension ( ) . string ( ) ;
2026-07-23 01:24:42 +02:00
for ( char & c : ext ) c = static_cast < char > ( std : : tolower ( static_cast < std : : uint8_t > ( c ) ) ) ;
2026-05-12 01:16:40 +02:00
fs : : path srcName = asset . filename ( ) ;
2026-05-19 00:50:06 +02:00
if ( ext = = " .png " | | ext = = " .tga " | | ext = = " .jpg " | | ext = = " .jpeg " | | ext = = " .bmp " ) {
srcName . replace_extension ( " .ctex " ) ;
} else if ( ext = = " .obj " ) {
srcName . replace_extension ( " .cmesh " ) ;
} else {
continue ;
}
2026-05-12 01:16:40 +02:00
fs : : path src = depBinDir / srcName ;
if ( ! fs : : exists ( src ) ) continue ;
copyTree ( src , outputDir / srcName ) ;
}
2026-05-02 21:08:51 +02:00
for ( Configuration * sub : dep - > dependencies ) forwardDepArtifacts ( sub ) ;
} ;
for ( Configuration * dep : config . dependencies ) forwardDepArtifacts ( dep ) ;
2026-05-01 19:16:13 +02:00
} catch ( const fs : : filesystem_error & e ) {
for ( std : : thread & thread : threads ) thread . join ( ) ;
return { e . what ( ) , false , { } } ;
}
}
2026-04-27 07:04:42 +02:00
if ( repack . load ( ) ) {
buildResult . repack = true ;
2026-04-23 01:57:25 +02:00
}
2026-04-27 07:04:42 +02:00
buildResult . libs = std : : move ( libSet ) ;
for ( const std : : string & flag : config . linkFlags ) {
buildResult . libs . insert ( flag ) ;
}
2026-05-01 19:02:14 +02:00
// Public compile flags propagated from sub-deps. Add them to this build's
// command so config sees the headers its deps expose, and re-publish them
// so config's own consumers see them transitively.
2026-07-23 01:24:42 +02:00
for ( const std : : string & flag : publicFlagSet ) command + = std : : format ( " {} " , flag ) ;
2026-05-01 19:02:14 +02:00
buildResult . publicCompileFlags = std : : move ( publicFlagSet ) ;
2026-04-27 07:04:42 +02:00
fs : : file_time_type externalFloor = fs : : file_time_type : : min ( ) ;
for ( const ExternalBuildResult & ext : externalResults ) {
for ( const std : : string & flag : ext . compileFlags ) {
2026-07-23 01:24:42 +02:00
command + = std : : format ( " {} " , flag ) ;
2026-05-01 19:02:14 +02:00
// Headers a dep links via ExternalDependency are part of its
// public surface (its modules can include them in declarations
// visible to consumers), so propagate the -I to consumers.
buildResult . publicCompileFlags . insert ( flag ) ;
2026-04-23 01:57:25 +02:00
}
2026-04-27 07:04:42 +02:00
for ( const std : : string & flag : ext . linkFlags ) {
buildResult . libs . insert ( flag ) ;
2026-04-23 01:57:25 +02:00
}
2026-04-27 07:04:42 +02:00
if ( ext . latestArtifact > externalFloor ) externalFloor = ext . latestArtifact ;
2026-04-23 01:57:25 +02:00
}
fix: track what a primary module interface imports
A primary module interface unit — `export module Widget;`, no partitions —
recorded nothing about what it imported. GetInterfacesAndImplementations
registered the Module and then erased the file from the scan list, so the
import pass only ever saw partitions, and Module had no vectors to hold an
edge anyway.
Two consequences, both reported as issue #26:
Module::Check consulted only its own .cppm and its partitions. A data
member added to an imported module left Widget.pcm, Widget.o and every
consumer object untouched while the imported library rebuilt and both
binaries relinked — one executable holding two class layouts, no
diagnostic, and a crash somewhere unrelated. Wiping build/ was the only
cure, so `crafter-build test` could not be trusted straight after an
interface edit.
Module::Compile waited on nothing. Two modules in one Configuration
compile on concurrent threads, so a primary interface importing a
sibling was a coin flip between working and "module 'Base' not found".
Partitions never had either problem — they carry the same three vectors and
Check/Compile honour them — which is why the gap only surfaced on a module
whose interface is one flat unit.
Module now carries moduleDependencies, externalModuleDependencies and
pendingImports with the same meanings as on ModulePartition; primary units
stay in the scan list so their imports land there; Check sees through them;
Compile orders itself behind a local sibling; and ResolvePendingImports
sweeps them so an edge survives dependencies being wired up afterwards.
Build() now Checks every interface before spawning any compile thread — the
`compiled` flag a waiter blocks on is raised either by a Compile that runs
or by the Check that decides none is needed, so a Check still pending while
another module's thread waits would have hung the build.
Resolves #26
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-30 18:19:04 +00:00
// Check every interface before compiling any of them. Module::Compile waits
// on the `compiled` flag of each sibling module it imports, and that flag is
// set either by a Compile that runs or by the Check that decides none is
// needed — so a Check still pending while another module's thread is already
// waiting would block on a flag nothing goes on to raise.
std : : vector < Module * > staleInterfaces ;
2026-04-27 07:04:42 +02:00
for ( std : : unique_ptr < Module > & interface : config . interfaces ) {
if ( interface - > Check ( pcmDir , externalFloor ) ) {
fix: track what a primary module interface imports
A primary module interface unit — `export module Widget;`, no partitions —
recorded nothing about what it imported. GetInterfacesAndImplementations
registered the Module and then erased the file from the scan list, so the
import pass only ever saw partitions, and Module had no vectors to hold an
edge anyway.
Two consequences, both reported as issue #26:
Module::Check consulted only its own .cppm and its partitions. A data
member added to an imported module left Widget.pcm, Widget.o and every
consumer object untouched while the imported library rebuilt and both
binaries relinked — one executable holding two class layouts, no
diagnostic, and a crash somewhere unrelated. Wiping build/ was the only
cure, so `crafter-build test` could not be trusted straight after an
interface edit.
Module::Compile waited on nothing. Two modules in one Configuration
compile on concurrent threads, so a primary interface importing a
sibling was a coin flip between working and "module 'Base' not found".
Partitions never had either problem — they carry the same three vectors and
Check/Compile honour them — which is why the gap only surfaced on a module
whose interface is one flat unit.
Module now carries moduleDependencies, externalModuleDependencies and
pendingImports with the same meanings as on ModulePartition; primary units
stay in the scan list so their imports land there; Check sees through them;
Compile orders itself behind a local sibling; and ResolvePendingImports
sweeps them so an edge survives dependencies being wired up afterwards.
Build() now Checks every interface before spawning any compile thread — the
`compiled` flag a waiter blocks on is raised either by a Compile that runs
or by the Check that decides none is needed, so a Check still pending while
another module's thread waits would have hung the build.
Resolves #26
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-30 18:19:04 +00:00
staleInterfaces . push_back ( interface . get ( ) ) ;
2026-04-23 01:57:25 +02:00
buildResult . repack = true ;
}
2026-04-27 07:04:42 +02:00
files + = std : : format ( " {}/{}.o " , buildDir . string ( ) , interface - > path . filename ( ) . string ( ) ) ;
for ( std : : unique_ptr < ModulePartition > & part : interface - > partitions ) {
2026-04-23 01:57:25 +02:00
files + = std : : format ( " {}/{}.o " , buildDir . string ( ) , part - > path . filename ( ) . string ( ) ) ;
}
}
fix: track what a primary module interface imports
A primary module interface unit — `export module Widget;`, no partitions —
recorded nothing about what it imported. GetInterfacesAndImplementations
registered the Module and then erased the file from the scan list, so the
import pass only ever saw partitions, and Module had no vectors to hold an
edge anyway.
Two consequences, both reported as issue #26:
Module::Check consulted only its own .cppm and its partitions. A data
member added to an imported module left Widget.pcm, Widget.o and every
consumer object untouched while the imported library rebuilt and both
binaries relinked — one executable holding two class layouts, no
diagnostic, and a crash somewhere unrelated. Wiping build/ was the only
cure, so `crafter-build test` could not be trusted straight after an
interface edit.
Module::Compile waited on nothing. Two modules in one Configuration
compile on concurrent threads, so a primary interface importing a
sibling was a coin flip between working and "module 'Base' not found".
Partitions never had either problem — they carry the same three vectors and
Check/Compile honour them — which is why the gap only surfaced on a module
whose interface is one flat unit.
Module now carries moduleDependencies, externalModuleDependencies and
pendingImports with the same meanings as on ModulePartition; primary units
stay in the scan list so their imports land there; Check sees through them;
Compile orders itself behind a local sibling; and ResolvePendingImports
sweeps them so an edge survives dependencies being wired up afterwards.
Build() now Checks every interface before spawning any compile thread — the
`compiled` flag a waiter blocks on is raised either by a Compile that runs
or by the Check that decides none is needed, so a Check still pending while
another module's thread waits would have hung the build.
Resolves #26
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-30 18:19:04 +00:00
for ( Module * mod : staleInterfaces ) {
threads . emplace_back ( [ mod , & command , & pcmDir , & buildDir , & buildCancelled , & buildError ] ( ) {
Progress : : Task task ( std : : format ( " Compiling interface {} " , mod - > path . filename ( ) . string ( ) ) ) ;
try {
mod - > Compile ( command , pcmDir , buildDir , buildCancelled , buildError ) ;
} catch ( const std : : exception & e ) {
bool expected = false ;
if ( buildCancelled . compare_exchange_strong ( expected , true ) ) {
buildError = std : : format ( " Module::Compile threw: {} " , e . what ( ) ) ;
}
}
} ) ;
}
2026-04-27 07:04:42 +02:00
for ( Implementation & implementation : config . implementations ) {
if ( implementation . Check ( buildDir , pcmDir , externalFloor ) ) {
2026-04-23 01:57:25 +02:00
buildResult . repack = true ;
2026-04-27 07:04:42 +02:00
Implementation * impl = & implementation ;
threads . emplace_back ( [ impl , & command , & buildDir , & buildCancelled , & buildError ] ( ) {
2026-04-29 03:27:11 +02:00
Progress : : Task task ( std : : format ( " Compiling {}.cpp " , impl - > path . filename ( ) . string ( ) ) ) ;
2026-04-27 07:04:42 +02:00
try {
impl - > Compile ( command , buildDir , buildCancelled , buildError ) ;
} catch ( const std : : exception & e ) {
bool expected = false ;
if ( buildCancelled . compare_exchange_strong ( expected , true ) ) {
buildError = std : : format ( " Implementation::Compile threw: {} " , e . what ( ) ) ;
}
}
} ) ;
2026-04-23 01:57:25 +02:00
}
2026-04-27 07:04:42 +02:00
files + = std : : format ( " {}/{}_impl.o " , buildDir . string ( ) , implementation . path . filename ( ) . string ( ) ) ;
2026-04-23 01:57:25 +02:00
}
for ( std : : thread & thread : threads ) {
thread . join ( ) ;
}
if ( buildCancelled . load ( ) ) {
return { buildError , false , { } } ;
}
2026-04-27 07:04:42 +02:00
std : : string linkExtras ;
for ( const std : : string & flag : buildResult . libs ) {
2026-07-23 01:24:42 +02:00
linkExtras + = std : : format ( " {} " , flag ) ;
2026-04-27 07:04:42 +02:00
}
2026-06-08 19:28:20 +02:00
// Link-only LTO/section flags (empty in Debug and for wasm). Every exe/lib
// link below ends with linkExtras, so this reaches them all; the static-lib
// archive path uses ar instead and is handled separately.
linkExtras + = ltoLinkFlags ;
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
// mingw uses libstdc++; C++26 std::print/format extras live in libstdc++exp.
// libstdc++ on mingw uses winpthreads for std::atomic_wait /
// counting_semaphore / stop_token, so -lpthread is required as soon as
// those primitives appear (they do, transitively, in any non-trivial std
// import). -static-libstdc++ bundles libstdc++ into the exe so we don't
// chase libstdc++-6.dll TLS symbol mismatches across mingw versions and
// the resulting binary stands alone. Auto-link so user projects don't
// carry boilerplate.
if ( config . target = = " x86_64-w64-mingw32 " ) {
Cross-compiled mingw artifact: full DLL+launcher pattern + MSVC target
Linux→mingw cross-compile now produces the same architectural shape as
build.cmd (DLL + import lib + launcher exe) instead of a single static
binary. The CI Windows artifact becomes a first-class drop-in: a user
on Windows can run crafter-build.exe against any project.cpp and have
it produce real Windows binaries — for either mingw or MSVC ABI.
What changed:
project.cpp: when target=mingw or target=msvc, crafter.build-lib is
built as LibraryDynamic instead of LibraryStatic so the link emits a
DLL + import lib (matching what build.cmd produces natively).
Crafter.Build-Clang.cpp Build():
- LibraryDynamic now branches per target — mingw emits <name>.dll +
lib<name>.dll.a via lld --out-implib; msvc emits <name>.dll +
<name>.lib via /IMPLIB; unix unchanged.
- expectedOutputFor returns .dll for Windows-target dynamic libs.
- Executable on Windows host now branches per target: mingw target
uses simple link (no -lc++/-nostdlib++/LIBCXX_DIR), msvc target keeps
the existing path. Both auto-copy LibraryDynamic dep DLLs + import
libs alongside the launcher exe (Windows resolves DLLs from the exe's
own directory at load time).
- Mingw-target Executables get -D CRAFTER_BUILD_DLL_IMPORT so
CRAFTER_API resolves to dllimport in their PCMs.
- mingw link adds -static-libstdc++ -static-libgcc -Wl,-Bstatic
-lpthread so produced .exe/.dll don't depend on a particular
libstdc++-6.dll / libwinpthread-1.dll being on the consumer's PATH
(avoids the Arch UCRT vs msys2 UCRT vs msys2 MSVCRT ABI rabbit hole).
Drops the old auto-copy of /usr/x86_64-w64-mingw32/bin/*.dll which
is now dead weight.
- -r flag resolves to an absolute path before std::system, otherwise
cmd.exe rejects "./bin/..." with "'.' is not recognized...".
Crafter.Build-Platform.cpp:
- Split the Windows-host block into shared shell helpers (#if MSVC ||
MINGW) plus separate #if MSVC and #if MINGW blocks for LoadProject /
EnsureCrafterBuildPcms / GetBaseCommand / BuildStdPcm.
- Mingw-host LoadProject compiles project.cpp with --target=mingw,
--sysroot=C:\msys64\ucrt64 (default; override with CRAFTER_MINGW_DIR),
-femulated-tls, -Wl,--export-all-symbols (mingw-lld doesn't accept
/EXPORT:NAME), and links against libcrafter-build.dll.a from the
launcher's directory.
- Mingw-host GetBaseCommand and BuildStdPcm dispatch on config.target
so a mingw-host crafter-build can also build msvc-target outputs
(uses LIBCXX_DIR + libc++ headers, same as native build.cmd) when
the user sets cfg.target = "x86_64-pc-windows-msvc".
README adds a Quick start (Windows) section covering both build paths
(native MSVC via build.cmd and the cross-compiled mingw artifact),
documenting the msys2 UCRT toolchain prerequisite.
Verified end-to-end on the winvm:
- mingw target: cross-compiled crafter-build.exe builds hello-world's
project.cpp, compiles main.cpp, links a hello.exe that runs without
any custom PATH (only Windows system DLLs needed).
- msvc target: same crafter-build.exe builds an MSVC-ABI hello.exe
linked against c++.dll (auto-copied from LIBCXX_DIR), runs cleanly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 02:23:42 +02:00
// Static-link libstdc++/libgcc/libwinpthread so the resulting .exe
// or .dll doesn't depend on a specific runtime DLL being on the
// consumer's PATH. The mingw runtime ABI varies subtly between
// distributions (Arch UCRT vs msys2 UCRT vs msys2 MSVCRT) and
// STATUS_ENTRYPOINT_NOT_FOUND at LoadLibrary time is the symptom
// of a mismatch. Static linkage trades binary size for portability.
// The -Bstatic/-Bdynamic bracketing forces -lpthread to resolve
// against libwinpthread.a rather than the import lib; everything
// else (KERNEL32, UCRT) stays dynamic. -lstdc++exp adds C++26
// std::print/format extras. -femulated-tls is already on the
// compile so __once_callable et al resolve in static libstdc++.
linkExtras + = " -lstdc++exp -static-libstdc++ -static-libgcc -Wl,-Bstatic -lpthread -Wl,-Bdynamic " ;
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
}
// Force a relink if the expected output is missing or older than any dep
// artifact. Missing covers: previous build produced a different outputName,
// or the binary was deleted by hand. Older-than-dep covers: dep's library
// was rebuilt by an earlier run (so dep.repack is false this time around)
// but the consumer was never relinked against the new dep.
{
auto expectedOutputFor = [ ] ( const Configuration & c ) - > fs : : path {
2026-04-30 02:20:19 +02:00
fs : : path dir = c . BinDir ( ) ;
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
if ( c . type = = ConfigurationType : : Executable ) {
2026-07-23 01:24:42 +02:00
if ( c . target . starts_with ( " wasm32 " ) ) return dir / ( std : : format ( " {}.wasm " , c . outputName ) ) ;
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
return c . target = = " x86_64-w64-mingw32 "
2026-07-23 01:24:42 +02:00
? dir / ( std : : format ( " {}.exe " , c . outputName ) )
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
: dir / c . outputName ;
}
if ( c . type = = ConfigurationType : : LibraryStatic ) {
return c . target = = " x86_64-w64-mingw32 " | | c . target = = " x86_64-pc-windows-msvc "
? dir / std : : format ( " {}.lib " , c . outputName )
: dir / std : : format ( " lib{}.a " , c . outputName ) ;
}
Cross-compiled mingw artifact: full DLL+launcher pattern + MSVC target
Linux→mingw cross-compile now produces the same architectural shape as
build.cmd (DLL + import lib + launcher exe) instead of a single static
binary. The CI Windows artifact becomes a first-class drop-in: a user
on Windows can run crafter-build.exe against any project.cpp and have
it produce real Windows binaries — for either mingw or MSVC ABI.
What changed:
project.cpp: when target=mingw or target=msvc, crafter.build-lib is
built as LibraryDynamic instead of LibraryStatic so the link emits a
DLL + import lib (matching what build.cmd produces natively).
Crafter.Build-Clang.cpp Build():
- LibraryDynamic now branches per target — mingw emits <name>.dll +
lib<name>.dll.a via lld --out-implib; msvc emits <name>.dll +
<name>.lib via /IMPLIB; unix unchanged.
- expectedOutputFor returns .dll for Windows-target dynamic libs.
- Executable on Windows host now branches per target: mingw target
uses simple link (no -lc++/-nostdlib++/LIBCXX_DIR), msvc target keeps
the existing path. Both auto-copy LibraryDynamic dep DLLs + import
libs alongside the launcher exe (Windows resolves DLLs from the exe's
own directory at load time).
- Mingw-target Executables get -D CRAFTER_BUILD_DLL_IMPORT so
CRAFTER_API resolves to dllimport in their PCMs.
- mingw link adds -static-libstdc++ -static-libgcc -Wl,-Bstatic
-lpthread so produced .exe/.dll don't depend on a particular
libstdc++-6.dll / libwinpthread-1.dll being on the consumer's PATH
(avoids the Arch UCRT vs msys2 UCRT vs msys2 MSVCRT ABI rabbit hole).
Drops the old auto-copy of /usr/x86_64-w64-mingw32/bin/*.dll which
is now dead weight.
- -r flag resolves to an absolute path before std::system, otherwise
cmd.exe rejects "./bin/..." with "'.' is not recognized...".
Crafter.Build-Platform.cpp:
- Split the Windows-host block into shared shell helpers (#if MSVC ||
MINGW) plus separate #if MSVC and #if MINGW blocks for LoadProject /
EnsureCrafterBuildPcms / GetBaseCommand / BuildStdPcm.
- Mingw-host LoadProject compiles project.cpp with --target=mingw,
--sysroot=C:\msys64\ucrt64 (default; override with CRAFTER_MINGW_DIR),
-femulated-tls, -Wl,--export-all-symbols (mingw-lld doesn't accept
/EXPORT:NAME), and links against libcrafter-build.dll.a from the
launcher's directory.
- Mingw-host GetBaseCommand and BuildStdPcm dispatch on config.target
so a mingw-host crafter-build can also build msvc-target outputs
(uses LIBCXX_DIR + libc++ headers, same as native build.cmd) when
the user sets cfg.target = "x86_64-pc-windows-msvc".
README adds a Quick start (Windows) section covering both build paths
(native MSVC via build.cmd and the cross-compiled mingw artifact),
documenting the msys2 UCRT toolchain prerequisite.
Verified end-to-end on the winvm:
- mingw target: cross-compiled crafter-build.exe builds hello-world's
project.cpp, compiles main.cpp, links a hello.exe that runs without
any custom PATH (only Windows system DLLs needed).
- msvc target: same crafter-build.exe builds an MSVC-ABI hello.exe
linked against c++.dll (auto-copied from LIBCXX_DIR), runs cleanly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 02:23:42 +02:00
// LibraryDynamic — point at the .dll on Windows targets so the
// mtime check sees the newly-built DLL; on Unix the .so suffices.
if ( c . target = = " x86_64-w64-mingw32 " | | c . target = = " x86_64-pc-windows-msvc " ) {
return dir / std : : format ( " {}.dll " , c . outputName ) ;
}
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
return dir / std : : format ( " lib{}.so " , c . outputName ) ;
} ;
fs : : path expected = expectedOutputFor ( config ) ;
if ( ! fs : : exists ( expected ) ) {
buildResult . repack = true ;
} else {
auto consumerMtime = fs : : last_write_time ( expected ) ;
for ( Configuration * dep : config . dependencies ) {
fs : : path depArtifact = expectedOutputFor ( * dep ) ;
if ( fs : : exists ( depArtifact ) & & fs : : last_write_time ( depArtifact ) > consumerMtime ) {
buildResult . repack = true ;
break ;
}
}
2026-05-19 16:53:24 +02:00
// Also relink if any .o this archive bundles is newer than the
// archive itself. Covers a build that compiled the .o but never
// reached the link step (interrupt, crash, or a source touched
// after compile but before link): on the next run the .cpp is
// already ≤ .o so Implementation/Module Check returns false and
// nothing else would notice the archive is stale.
if ( ! buildResult . repack ) {
auto objNewer = [ & ] ( const fs : : path & obj ) {
std : : error_code ec ;
auto t = fs : : last_write_time ( obj , ec ) ;
return ! ec & & t > consumerMtime ;
} ;
for ( const std : : unique_ptr < Module > & iface : config . interfaces ) {
2026-07-23 01:24:42 +02:00
if ( objNewer ( buildDir / std : : format ( " {}.o " , iface - > path . filename ( ) . string ( ) ) ) ) {
2026-05-19 16:53:24 +02:00
buildResult . repack = true ;
break ;
}
bool partHit = false ;
for ( const std : : unique_ptr < ModulePartition > & part : iface - > partitions ) {
2026-07-23 01:24:42 +02:00
if ( objNewer ( buildDir / std : : format ( " {}.o " , part - > path . filename ( ) . string ( ) ) ) ) {
2026-05-19 16:53:24 +02:00
partHit = true ;
break ;
}
}
if ( partHit ) { buildResult . repack = true ; break ; }
}
if ( ! buildResult . repack ) {
for ( const Implementation & impl : config . implementations ) {
2026-07-23 01:24:42 +02:00
if ( objNewer ( buildDir / std : : format ( " {}_impl.o " , impl . path . filename ( ) . string ( ) ) ) ) {
2026-05-19 16:53:24 +02:00
buildResult . repack = true ;
break ;
}
}
}
}
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-27 07:04:42 +02:00
2026-04-23 01:57:25 +02:00
if ( buildResult . repack ) {
2026-04-29 03:27:11 +02:00
Progress : : Task task ( std : : format ( " Linking {} " , config . outputName ) ) ;
2026-04-27 07:04:42 +02:00
if ( config . type = = ConfigurationType : : Executable ) {
2026-04-23 01:57:25 +02:00
# ifdef CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_pc_linux_gnu
2026-04-27 07:04:42 +02:00
if ( config . target = = " x86_64-w64-mingw32 " ) {
try {
Cross-compiled mingw artifact: full DLL+launcher pattern + MSVC target
Linux→mingw cross-compile now produces the same architectural shape as
build.cmd (DLL + import lib + launcher exe) instead of a single static
binary. The CI Windows artifact becomes a first-class drop-in: a user
on Windows can run crafter-build.exe against any project.cpp and have
it produce real Windows binaries — for either mingw or MSVC ABI.
What changed:
project.cpp: when target=mingw or target=msvc, crafter.build-lib is
built as LibraryDynamic instead of LibraryStatic so the link emits a
DLL + import lib (matching what build.cmd produces natively).
Crafter.Build-Clang.cpp Build():
- LibraryDynamic now branches per target — mingw emits <name>.dll +
lib<name>.dll.a via lld --out-implib; msvc emits <name>.dll +
<name>.lib via /IMPLIB; unix unchanged.
- expectedOutputFor returns .dll for Windows-target dynamic libs.
- Executable on Windows host now branches per target: mingw target
uses simple link (no -lc++/-nostdlib++/LIBCXX_DIR), msvc target keeps
the existing path. Both auto-copy LibraryDynamic dep DLLs + import
libs alongside the launcher exe (Windows resolves DLLs from the exe's
own directory at load time).
- Mingw-target Executables get -D CRAFTER_BUILD_DLL_IMPORT so
CRAFTER_API resolves to dllimport in their PCMs.
- mingw link adds -static-libstdc++ -static-libgcc -Wl,-Bstatic
-lpthread so produced .exe/.dll don't depend on a particular
libstdc++-6.dll / libwinpthread-1.dll being on the consumer's PATH
(avoids the Arch UCRT vs msys2 UCRT vs msys2 MSVCRT ABI rabbit hole).
Drops the old auto-copy of /usr/x86_64-w64-mingw32/bin/*.dll which
is now dead weight.
- -r flag resolves to an absolute path before std::system, otherwise
cmd.exe rejects "./bin/..." with "'.' is not recognized...".
Crafter.Build-Platform.cpp:
- Split the Windows-host block into shared shell helpers (#if MSVC ||
MINGW) plus separate #if MSVC and #if MINGW blocks for LoadProject /
EnsureCrafterBuildPcms / GetBaseCommand / BuildStdPcm.
- Mingw-host LoadProject compiles project.cpp with --target=mingw,
--sysroot=C:\msys64\ucrt64 (default; override with CRAFTER_MINGW_DIR),
-femulated-tls, -Wl,--export-all-symbols (mingw-lld doesn't accept
/EXPORT:NAME), and links against libcrafter-build.dll.a from the
launcher's directory.
- Mingw-host GetBaseCommand and BuildStdPcm dispatch on config.target
so a mingw-host crafter-build can also build msvc-target outputs
(uses LIBCXX_DIR + libc++ headers, same as native build.cmd) when
the user sets cfg.target = "x86_64-pc-windows-msvc".
README adds a Quick start (Windows) section covering both build paths
(native MSVC via build.cmd and the cross-compiled mingw artifact),
documenting the msys2 UCRT toolchain prerequisite.
Verified end-to-end on the winvm:
- mingw target: cross-compiled crafter-build.exe builds hello-world's
project.cpp, compiles main.cpp, links a hello.exe that runs without
any custom PATH (only Windows system DLLs needed).
- msvc target: same crafter-build.exe builds an MSVC-ABI hello.exe
linked against c++.dll (auto-copied from LIBCXX_DIR), runs cleanly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 02:23:42 +02:00
// Copy any LibraryDynamic dependency DLLs alongside
// the launcher exe — Windows resolves DLLs from the exe's
// own directory at load time, so this is the simplest
// equivalent of rpath $ORIGIN.
std : : unordered_set < Configuration * > dllSeen ;
std : : function < void ( Configuration * ) > copyDepDlls = [ & ] ( Configuration * dep ) {
if ( ! dllSeen . insert ( dep ) . second ) return ;
if ( dep - > type = = ConfigurationType : : LibraryDynamic & & dep - > target = = " x86_64-w64-mingw32 " ) {
2026-04-30 02:20:19 +02:00
fs : : path depDir = dep - > BinDir ( ) ;
Cross-compiled mingw artifact: full DLL+launcher pattern + MSVC target
Linux→mingw cross-compile now produces the same architectural shape as
build.cmd (DLL + import lib + launcher exe) instead of a single static
binary. The CI Windows artifact becomes a first-class drop-in: a user
on Windows can run crafter-build.exe against any project.cpp and have
it produce real Windows binaries — for either mingw or MSVC ABI.
What changed:
project.cpp: when target=mingw or target=msvc, crafter.build-lib is
built as LibraryDynamic instead of LibraryStatic so the link emits a
DLL + import lib (matching what build.cmd produces natively).
Crafter.Build-Clang.cpp Build():
- LibraryDynamic now branches per target — mingw emits <name>.dll +
lib<name>.dll.a via lld --out-implib; msvc emits <name>.dll +
<name>.lib via /IMPLIB; unix unchanged.
- expectedOutputFor returns .dll for Windows-target dynamic libs.
- Executable on Windows host now branches per target: mingw target
uses simple link (no -lc++/-nostdlib++/LIBCXX_DIR), msvc target keeps
the existing path. Both auto-copy LibraryDynamic dep DLLs + import
libs alongside the launcher exe (Windows resolves DLLs from the exe's
own directory at load time).
- Mingw-target Executables get -D CRAFTER_BUILD_DLL_IMPORT so
CRAFTER_API resolves to dllimport in their PCMs.
- mingw link adds -static-libstdc++ -static-libgcc -Wl,-Bstatic
-lpthread so produced .exe/.dll don't depend on a particular
libstdc++-6.dll / libwinpthread-1.dll being on the consumer's PATH
(avoids the Arch UCRT vs msys2 UCRT vs msys2 MSVCRT ABI rabbit hole).
Drops the old auto-copy of /usr/x86_64-w64-mingw32/bin/*.dll which
is now dead weight.
- -r flag resolves to an absolute path before std::system, otherwise
cmd.exe rejects "./bin/..." with "'.' is not recognized...".
Crafter.Build-Platform.cpp:
- Split the Windows-host block into shared shell helpers (#if MSVC ||
MINGW) plus separate #if MSVC and #if MINGW blocks for LoadProject /
EnsureCrafterBuildPcms / GetBaseCommand / BuildStdPcm.
- Mingw-host LoadProject compiles project.cpp with --target=mingw,
--sysroot=C:\msys64\ucrt64 (default; override with CRAFTER_MINGW_DIR),
-femulated-tls, -Wl,--export-all-symbols (mingw-lld doesn't accept
/EXPORT:NAME), and links against libcrafter-build.dll.a from the
launcher's directory.
- Mingw-host GetBaseCommand and BuildStdPcm dispatch on config.target
so a mingw-host crafter-build can also build msvc-target outputs
(uses LIBCXX_DIR + libc++ headers, same as native build.cmd) when
the user sets cfg.target = "x86_64-pc-windows-msvc".
README adds a Quick start (Windows) section covering both build paths
(native MSVC via build.cmd and the cross-compiled mingw artifact),
documenting the msys2 UCRT toolchain prerequisite.
Verified end-to-end on the winvm:
- mingw target: cross-compiled crafter-build.exe builds hello-world's
project.cpp, compiles main.cpp, links a hello.exe that runs without
any custom PATH (only Windows system DLLs needed).
- msvc target: same crafter-build.exe builds an MSVC-ABI hello.exe
linked against c++.dll (auto-copied from LIBCXX_DIR), runs cleanly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 02:23:42 +02:00
// The DLL itself (Windows resolves it from the
// exe's directory at load time) and the mingw
// import lib (so a downstream `crafter-build.exe`
// can link a fresh project.dll against it without
// hunting through sibling output dirs).
2026-07-23 01:24:42 +02:00
for ( auto fname : { std : : format ( " {}.dll " , dep - > outputName ) , std : : format ( " lib{}.dll.a " , dep - > outputName ) } ) {
Cross-compiled mingw artifact: full DLL+launcher pattern + MSVC target
Linux→mingw cross-compile now produces the same architectural shape as
build.cmd (DLL + import lib + launcher exe) instead of a single static
binary. The CI Windows artifact becomes a first-class drop-in: a user
on Windows can run crafter-build.exe against any project.cpp and have
it produce real Windows binaries — for either mingw or MSVC ABI.
What changed:
project.cpp: when target=mingw or target=msvc, crafter.build-lib is
built as LibraryDynamic instead of LibraryStatic so the link emits a
DLL + import lib (matching what build.cmd produces natively).
Crafter.Build-Clang.cpp Build():
- LibraryDynamic now branches per target — mingw emits <name>.dll +
lib<name>.dll.a via lld --out-implib; msvc emits <name>.dll +
<name>.lib via /IMPLIB; unix unchanged.
- expectedOutputFor returns .dll for Windows-target dynamic libs.
- Executable on Windows host now branches per target: mingw target
uses simple link (no -lc++/-nostdlib++/LIBCXX_DIR), msvc target keeps
the existing path. Both auto-copy LibraryDynamic dep DLLs + import
libs alongside the launcher exe (Windows resolves DLLs from the exe's
own directory at load time).
- Mingw-target Executables get -D CRAFTER_BUILD_DLL_IMPORT so
CRAFTER_API resolves to dllimport in their PCMs.
- mingw link adds -static-libstdc++ -static-libgcc -Wl,-Bstatic
-lpthread so produced .exe/.dll don't depend on a particular
libstdc++-6.dll / libwinpthread-1.dll being on the consumer's PATH
(avoids the Arch UCRT vs msys2 UCRT vs msys2 MSVCRT ABI rabbit hole).
Drops the old auto-copy of /usr/x86_64-w64-mingw32/bin/*.dll which
is now dead weight.
- -r flag resolves to an absolute path before std::system, otherwise
cmd.exe rejects "./bin/..." with "'.' is not recognized...".
Crafter.Build-Platform.cpp:
- Split the Windows-host block into shared shell helpers (#if MSVC ||
MINGW) plus separate #if MSVC and #if MINGW blocks for LoadProject /
EnsureCrafterBuildPcms / GetBaseCommand / BuildStdPcm.
- Mingw-host LoadProject compiles project.cpp with --target=mingw,
--sysroot=C:\msys64\ucrt64 (default; override with CRAFTER_MINGW_DIR),
-femulated-tls, -Wl,--export-all-symbols (mingw-lld doesn't accept
/EXPORT:NAME), and links against libcrafter-build.dll.a from the
launcher's directory.
- Mingw-host GetBaseCommand and BuildStdPcm dispatch on config.target
so a mingw-host crafter-build can also build msvc-target outputs
(uses LIBCXX_DIR + libc++ headers, same as native build.cmd) when
the user sets cfg.target = "x86_64-pc-windows-msvc".
README adds a Quick start (Windows) section covering both build paths
(native MSVC via build.cmd and the cross-compiled mingw artifact),
documenting the msys2 UCRT toolchain prerequisite.
Verified end-to-end on the winvm:
- mingw target: cross-compiled crafter-build.exe builds hello-world's
project.cpp, compiles main.cpp, links a hello.exe that runs without
any custom PATH (only Windows system DLLs needed).
- msvc target: same crafter-build.exe builds an MSVC-ABI hello.exe
linked against c++.dll (auto-copied from LIBCXX_DIR), runs cleanly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 02:23:42 +02:00
fs : : path src = depDir / fname ;
if ( ! fs : : exists ( src ) ) continue ;
fs : : path dest = outputDir / src . filename ( ) ;
if ( ! fs : : exists ( dest ) | | fs : : last_write_time ( src ) > fs : : last_write_time ( dest ) ) {
fs : : copy ( src , dest , fs : : copy_options : : overwrite_existing ) ;
}
2026-04-27 07:04:42 +02:00
}
}
Cross-compiled mingw artifact: full DLL+launcher pattern + MSVC target
Linux→mingw cross-compile now produces the same architectural shape as
build.cmd (DLL + import lib + launcher exe) instead of a single static
binary. The CI Windows artifact becomes a first-class drop-in: a user
on Windows can run crafter-build.exe against any project.cpp and have
it produce real Windows binaries — for either mingw or MSVC ABI.
What changed:
project.cpp: when target=mingw or target=msvc, crafter.build-lib is
built as LibraryDynamic instead of LibraryStatic so the link emits a
DLL + import lib (matching what build.cmd produces natively).
Crafter.Build-Clang.cpp Build():
- LibraryDynamic now branches per target — mingw emits <name>.dll +
lib<name>.dll.a via lld --out-implib; msvc emits <name>.dll +
<name>.lib via /IMPLIB; unix unchanged.
- expectedOutputFor returns .dll for Windows-target dynamic libs.
- Executable on Windows host now branches per target: mingw target
uses simple link (no -lc++/-nostdlib++/LIBCXX_DIR), msvc target keeps
the existing path. Both auto-copy LibraryDynamic dep DLLs + import
libs alongside the launcher exe (Windows resolves DLLs from the exe's
own directory at load time).
- Mingw-target Executables get -D CRAFTER_BUILD_DLL_IMPORT so
CRAFTER_API resolves to dllimport in their PCMs.
- mingw link adds -static-libstdc++ -static-libgcc -Wl,-Bstatic
-lpthread so produced .exe/.dll don't depend on a particular
libstdc++-6.dll / libwinpthread-1.dll being on the consumer's PATH
(avoids the Arch UCRT vs msys2 UCRT vs msys2 MSVCRT ABI rabbit hole).
Drops the old auto-copy of /usr/x86_64-w64-mingw32/bin/*.dll which
is now dead weight.
- -r flag resolves to an absolute path before std::system, otherwise
cmd.exe rejects "./bin/..." with "'.' is not recognized...".
Crafter.Build-Platform.cpp:
- Split the Windows-host block into shared shell helpers (#if MSVC ||
MINGW) plus separate #if MSVC and #if MINGW blocks for LoadProject /
EnsureCrafterBuildPcms / GetBaseCommand / BuildStdPcm.
- Mingw-host LoadProject compiles project.cpp with --target=mingw,
--sysroot=C:\msys64\ucrt64 (default; override with CRAFTER_MINGW_DIR),
-femulated-tls, -Wl,--export-all-symbols (mingw-lld doesn't accept
/EXPORT:NAME), and links against libcrafter-build.dll.a from the
launcher's directory.
- Mingw-host GetBaseCommand and BuildStdPcm dispatch on config.target
so a mingw-host crafter-build can also build msvc-target outputs
(uses LIBCXX_DIR + libc++ headers, same as native build.cmd) when
the user sets cfg.target = "x86_64-pc-windows-msvc".
README adds a Quick start (Windows) section covering both build paths
(native MSVC via build.cmd and the cross-compiled mingw artifact),
documenting the msys2 UCRT toolchain prerequisite.
Verified end-to-end on the winvm:
- mingw target: cross-compiled crafter-build.exe builds hello-world's
project.cpp, compiles main.cpp, links a hello.exe that runs without
any custom PATH (only Windows system DLLs needed).
- msvc target: same crafter-build.exe builds an MSVC-ABI hello.exe
linked against c++.dll (auto-copied from LIBCXX_DIR), runs cleanly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 02:23:42 +02:00
for ( Configuration * sub : dep - > dependencies ) copyDepDlls ( sub ) ;
} ;
for ( Configuration * dep : config . dependencies ) copyDepDlls ( dep ) ;
2026-04-27 07:04:42 +02:00
} catch ( const fs : : filesystem_error & e ) {
return { e . what ( ) , false , { } } ;
}
}
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
if ( config . target . starts_with ( " wasm32 " ) ) {
buildResult . result = RunCommand ( std : : format ( " {}{} -o {}.wasm -fuse-ld=lld{} " , command , files , ( outputDir / config . outputName ) . string ( ) , linkExtras ) ) ;
} else {
buildResult . result = RunCommand ( std : : format ( " {}{} -o {} -fuse-ld=lld{} " , command , files , ( outputDir / config . outputName ) . string ( ) , linkExtras ) ) ;
}
2026-04-23 01:57:25 +02:00
# endif
# if defined(CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_pc_windows_msvc) || defined(CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_w64_mingw32)
Cross-compiled mingw artifact: full DLL+launcher pattern + MSVC target
Linux→mingw cross-compile now produces the same architectural shape as
build.cmd (DLL + import lib + launcher exe) instead of a single static
binary. The CI Windows artifact becomes a first-class drop-in: a user
on Windows can run crafter-build.exe against any project.cpp and have
it produce real Windows binaries — for either mingw or MSVC ABI.
What changed:
project.cpp: when target=mingw or target=msvc, crafter.build-lib is
built as LibraryDynamic instead of LibraryStatic so the link emits a
DLL + import lib (matching what build.cmd produces natively).
Crafter.Build-Clang.cpp Build():
- LibraryDynamic now branches per target — mingw emits <name>.dll +
lib<name>.dll.a via lld --out-implib; msvc emits <name>.dll +
<name>.lib via /IMPLIB; unix unchanged.
- expectedOutputFor returns .dll for Windows-target dynamic libs.
- Executable on Windows host now branches per target: mingw target
uses simple link (no -lc++/-nostdlib++/LIBCXX_DIR), msvc target keeps
the existing path. Both auto-copy LibraryDynamic dep DLLs + import
libs alongside the launcher exe (Windows resolves DLLs from the exe's
own directory at load time).
- Mingw-target Executables get -D CRAFTER_BUILD_DLL_IMPORT so
CRAFTER_API resolves to dllimport in their PCMs.
- mingw link adds -static-libstdc++ -static-libgcc -Wl,-Bstatic
-lpthread so produced .exe/.dll don't depend on a particular
libstdc++-6.dll / libwinpthread-1.dll being on the consumer's PATH
(avoids the Arch UCRT vs msys2 UCRT vs msys2 MSVCRT ABI rabbit hole).
Drops the old auto-copy of /usr/x86_64-w64-mingw32/bin/*.dll which
is now dead weight.
- -r flag resolves to an absolute path before std::system, otherwise
cmd.exe rejects "./bin/..." with "'.' is not recognized...".
Crafter.Build-Platform.cpp:
- Split the Windows-host block into shared shell helpers (#if MSVC ||
MINGW) plus separate #if MSVC and #if MINGW blocks for LoadProject /
EnsureCrafterBuildPcms / GetBaseCommand / BuildStdPcm.
- Mingw-host LoadProject compiles project.cpp with --target=mingw,
--sysroot=C:\msys64\ucrt64 (default; override with CRAFTER_MINGW_DIR),
-femulated-tls, -Wl,--export-all-symbols (mingw-lld doesn't accept
/EXPORT:NAME), and links against libcrafter-build.dll.a from the
launcher's directory.
- Mingw-host GetBaseCommand and BuildStdPcm dispatch on config.target
so a mingw-host crafter-build can also build msvc-target outputs
(uses LIBCXX_DIR + libc++ headers, same as native build.cmd) when
the user sets cfg.target = "x86_64-pc-windows-msvc".
README adds a Quick start (Windows) section covering both build paths
(native MSVC via build.cmd and the cross-compiled mingw artifact),
documenting the msys2 UCRT toolchain prerequisite.
Verified end-to-end on the winvm:
- mingw target: cross-compiled crafter-build.exe builds hello-world's
project.cpp, compiles main.cpp, links a hello.exe that runs without
any custom PATH (only Windows system DLLs needed).
- msvc target: same crafter-build.exe builds an MSVC-ABI hello.exe
linked against c++.dll (auto-copied from LIBCXX_DIR), runs cleanly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 02:23:42 +02:00
if ( config . target = = " x86_64-w64-mingw32 " ) {
// Windows host, mingw target: same shape as the Linux→mingw
// path (no LIBCXX_DIR / -lc++ / -nostdlib++ — those are MSVC
// libc++ flags). Copy LibraryDynamic dep DLLs + import libs
// alongside the launcher exe so Windows resolves them from
// the exe's own directory at load time. Runtime DLLs (libstdc++,
// libgcc, libwinpthread) come from msys2 on PATH.
std : : unordered_set < Configuration * > dllSeen ;
std : : function < void ( Configuration * ) > copyDepDlls = [ & ] ( Configuration * dep ) {
if ( ! dllSeen . insert ( dep ) . second ) return ;
if ( dep - > type = = ConfigurationType : : LibraryDynamic & & dep - > target = = " x86_64-w64-mingw32 " ) {
2026-04-30 02:20:19 +02:00
fs : : path depDir = dep - > BinDir ( ) ;
2026-07-23 01:24:42 +02:00
for ( auto fname : { std : : format ( " {}.dll " , dep - > outputName ) , std : : format ( " lib{}.dll.a " , dep - > outputName ) } ) {
Cross-compiled mingw artifact: full DLL+launcher pattern + MSVC target
Linux→mingw cross-compile now produces the same architectural shape as
build.cmd (DLL + import lib + launcher exe) instead of a single static
binary. The CI Windows artifact becomes a first-class drop-in: a user
on Windows can run crafter-build.exe against any project.cpp and have
it produce real Windows binaries — for either mingw or MSVC ABI.
What changed:
project.cpp: when target=mingw or target=msvc, crafter.build-lib is
built as LibraryDynamic instead of LibraryStatic so the link emits a
DLL + import lib (matching what build.cmd produces natively).
Crafter.Build-Clang.cpp Build():
- LibraryDynamic now branches per target — mingw emits <name>.dll +
lib<name>.dll.a via lld --out-implib; msvc emits <name>.dll +
<name>.lib via /IMPLIB; unix unchanged.
- expectedOutputFor returns .dll for Windows-target dynamic libs.
- Executable on Windows host now branches per target: mingw target
uses simple link (no -lc++/-nostdlib++/LIBCXX_DIR), msvc target keeps
the existing path. Both auto-copy LibraryDynamic dep DLLs + import
libs alongside the launcher exe (Windows resolves DLLs from the exe's
own directory at load time).
- Mingw-target Executables get -D CRAFTER_BUILD_DLL_IMPORT so
CRAFTER_API resolves to dllimport in their PCMs.
- mingw link adds -static-libstdc++ -static-libgcc -Wl,-Bstatic
-lpthread so produced .exe/.dll don't depend on a particular
libstdc++-6.dll / libwinpthread-1.dll being on the consumer's PATH
(avoids the Arch UCRT vs msys2 UCRT vs msys2 MSVCRT ABI rabbit hole).
Drops the old auto-copy of /usr/x86_64-w64-mingw32/bin/*.dll which
is now dead weight.
- -r flag resolves to an absolute path before std::system, otherwise
cmd.exe rejects "./bin/..." with "'.' is not recognized...".
Crafter.Build-Platform.cpp:
- Split the Windows-host block into shared shell helpers (#if MSVC ||
MINGW) plus separate #if MSVC and #if MINGW blocks for LoadProject /
EnsureCrafterBuildPcms / GetBaseCommand / BuildStdPcm.
- Mingw-host LoadProject compiles project.cpp with --target=mingw,
--sysroot=C:\msys64\ucrt64 (default; override with CRAFTER_MINGW_DIR),
-femulated-tls, -Wl,--export-all-symbols (mingw-lld doesn't accept
/EXPORT:NAME), and links against libcrafter-build.dll.a from the
launcher's directory.
- Mingw-host GetBaseCommand and BuildStdPcm dispatch on config.target
so a mingw-host crafter-build can also build msvc-target outputs
(uses LIBCXX_DIR + libc++ headers, same as native build.cmd) when
the user sets cfg.target = "x86_64-pc-windows-msvc".
README adds a Quick start (Windows) section covering both build paths
(native MSVC via build.cmd and the cross-compiled mingw artifact),
documenting the msys2 UCRT toolchain prerequisite.
Verified end-to-end on the winvm:
- mingw target: cross-compiled crafter-build.exe builds hello-world's
project.cpp, compiles main.cpp, links a hello.exe that runs without
any custom PATH (only Windows system DLLs needed).
- msvc target: same crafter-build.exe builds an MSVC-ABI hello.exe
linked against c++.dll (auto-copied from LIBCXX_DIR), runs cleanly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 02:23:42 +02:00
fs : : path src = depDir / fname ;
if ( ! fs : : exists ( src ) ) continue ;
fs : : path dest = outputDir / src . filename ( ) ;
if ( ! fs : : exists ( dest ) | | fs : : last_write_time ( src ) > fs : : last_write_time ( dest ) ) {
fs : : copy ( src , dest , fs : : copy_options : : overwrite_existing ) ;
}
}
}
for ( Configuration * sub : dep - > dependencies ) copyDepDlls ( sub ) ;
} ;
for ( Configuration * dep : config . dependencies ) copyDepDlls ( dep ) ;
2026-07-23 01:24:42 +02:00
buildResult . result = RunCommand ( std : : format ( " {}{} -o {} -fuse-ld=lld{} " , command , files , ( outputDir / config . outputName ) . string ( ) , linkExtras ) ) ;
Cross-compiled mingw artifact: full DLL+launcher pattern + MSVC target
Linux→mingw cross-compile now produces the same architectural shape as
build.cmd (DLL + import lib + launcher exe) instead of a single static
binary. The CI Windows artifact becomes a first-class drop-in: a user
on Windows can run crafter-build.exe against any project.cpp and have
it produce real Windows binaries — for either mingw or MSVC ABI.
What changed:
project.cpp: when target=mingw or target=msvc, crafter.build-lib is
built as LibraryDynamic instead of LibraryStatic so the link emits a
DLL + import lib (matching what build.cmd produces natively).
Crafter.Build-Clang.cpp Build():
- LibraryDynamic now branches per target — mingw emits <name>.dll +
lib<name>.dll.a via lld --out-implib; msvc emits <name>.dll +
<name>.lib via /IMPLIB; unix unchanged.
- expectedOutputFor returns .dll for Windows-target dynamic libs.
- Executable on Windows host now branches per target: mingw target
uses simple link (no -lc++/-nostdlib++/LIBCXX_DIR), msvc target keeps
the existing path. Both auto-copy LibraryDynamic dep DLLs + import
libs alongside the launcher exe (Windows resolves DLLs from the exe's
own directory at load time).
- Mingw-target Executables get -D CRAFTER_BUILD_DLL_IMPORT so
CRAFTER_API resolves to dllimport in their PCMs.
- mingw link adds -static-libstdc++ -static-libgcc -Wl,-Bstatic
-lpthread so produced .exe/.dll don't depend on a particular
libstdc++-6.dll / libwinpthread-1.dll being on the consumer's PATH
(avoids the Arch UCRT vs msys2 UCRT vs msys2 MSVCRT ABI rabbit hole).
Drops the old auto-copy of /usr/x86_64-w64-mingw32/bin/*.dll which
is now dead weight.
- -r flag resolves to an absolute path before std::system, otherwise
cmd.exe rejects "./bin/..." with "'.' is not recognized...".
Crafter.Build-Platform.cpp:
- Split the Windows-host block into shared shell helpers (#if MSVC ||
MINGW) plus separate #if MSVC and #if MINGW blocks for LoadProject /
EnsureCrafterBuildPcms / GetBaseCommand / BuildStdPcm.
- Mingw-host LoadProject compiles project.cpp with --target=mingw,
--sysroot=C:\msys64\ucrt64 (default; override with CRAFTER_MINGW_DIR),
-femulated-tls, -Wl,--export-all-symbols (mingw-lld doesn't accept
/EXPORT:NAME), and links against libcrafter-build.dll.a from the
launcher's directory.
- Mingw-host GetBaseCommand and BuildStdPcm dispatch on config.target
so a mingw-host crafter-build can also build msvc-target outputs
(uses LIBCXX_DIR + libc++ headers, same as native build.cmd) when
the user sets cfg.target = "x86_64-pc-windows-msvc".
README adds a Quick start (Windows) section covering both build paths
(native MSVC via build.cmd and the cross-compiled mingw artifact),
documenting the msys2 UCRT toolchain prerequisite.
Verified end-to-end on the winvm:
- mingw target: cross-compiled crafter-build.exe builds hello-world's
project.cpp, compiles main.cpp, links a hello.exe that runs without
any custom PATH (only Windows system DLLs needed).
- msvc target: same crafter-build.exe builds an MSVC-ABI hello.exe
linked against c++.dll (auto-copied from LIBCXX_DIR), runs cleanly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 02:23:42 +02:00
} else {
std : : system ( std : : format ( " copy \" %LIBCXX_DIR% \\ lib \\ c++.dll \" \" {} \\ c++.dll \" " , outputDir . string ( ) ) . c_str ( ) ) ;
buildResult . result = RunCommand ( std : : format ( " {}{} -o {}.exe -fuse-ld=lld -L %LIBCXX_DIR% \\ lib -lc++ -nostdinc++ -nostdlib++{} " , command , files , ( outputDir / config . outputName ) . string ( ) , linkExtras ) ) ;
}
2026-04-23 01:57:25 +02:00
# endif
2026-04-27 07:04:42 +02:00
} else if ( config . type = = ConfigurationType : : LibraryStatic ) {
2026-04-23 01:57:25 +02:00
# ifdef CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_pc_linux_gnu
2026-06-08 19:28:20 +02:00
// ThinLTO emits LLVM bitcode objects; plain `ar` writes an archive
// index that omits their symbols, so a consumer's lld link can't
// pull the needed members. llvm-ar writes a bitcode-aware index.
buildResult . result = RunCommand ( std : : format ( " {} rcs {}.a {} " , useLto ? " llvm-ar " : " ar " , ( outputDir / fs : : path ( std : : string ( " lib " ) + config . outputName ) ) . string ( ) , files ) ) ;
2026-04-23 01:57:25 +02:00
# endif
# if defined(CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_pc_windows_msvc) || defined(CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_w64_mingw32)
2026-04-27 07:04:42 +02:00
buildResult . result = RunCommand ( std : : format ( " llvm-lib.exe {} /OUT:{}.lib " , files , ( outputDir / fs : : path ( config . outputName ) ) . string ( ) ) ) ;
2026-04-23 01:57:25 +02:00
# endif
} else {
Cross-compiled mingw artifact: full DLL+launcher pattern + MSVC target
Linux→mingw cross-compile now produces the same architectural shape as
build.cmd (DLL + import lib + launcher exe) instead of a single static
binary. The CI Windows artifact becomes a first-class drop-in: a user
on Windows can run crafter-build.exe against any project.cpp and have
it produce real Windows binaries — for either mingw or MSVC ABI.
What changed:
project.cpp: when target=mingw or target=msvc, crafter.build-lib is
built as LibraryDynamic instead of LibraryStatic so the link emits a
DLL + import lib (matching what build.cmd produces natively).
Crafter.Build-Clang.cpp Build():
- LibraryDynamic now branches per target — mingw emits <name>.dll +
lib<name>.dll.a via lld --out-implib; msvc emits <name>.dll +
<name>.lib via /IMPLIB; unix unchanged.
- expectedOutputFor returns .dll for Windows-target dynamic libs.
- Executable on Windows host now branches per target: mingw target
uses simple link (no -lc++/-nostdlib++/LIBCXX_DIR), msvc target keeps
the existing path. Both auto-copy LibraryDynamic dep DLLs + import
libs alongside the launcher exe (Windows resolves DLLs from the exe's
own directory at load time).
- Mingw-target Executables get -D CRAFTER_BUILD_DLL_IMPORT so
CRAFTER_API resolves to dllimport in their PCMs.
- mingw link adds -static-libstdc++ -static-libgcc -Wl,-Bstatic
-lpthread so produced .exe/.dll don't depend on a particular
libstdc++-6.dll / libwinpthread-1.dll being on the consumer's PATH
(avoids the Arch UCRT vs msys2 UCRT vs msys2 MSVCRT ABI rabbit hole).
Drops the old auto-copy of /usr/x86_64-w64-mingw32/bin/*.dll which
is now dead weight.
- -r flag resolves to an absolute path before std::system, otherwise
cmd.exe rejects "./bin/..." with "'.' is not recognized...".
Crafter.Build-Platform.cpp:
- Split the Windows-host block into shared shell helpers (#if MSVC ||
MINGW) plus separate #if MSVC and #if MINGW blocks for LoadProject /
EnsureCrafterBuildPcms / GetBaseCommand / BuildStdPcm.
- Mingw-host LoadProject compiles project.cpp with --target=mingw,
--sysroot=C:\msys64\ucrt64 (default; override with CRAFTER_MINGW_DIR),
-femulated-tls, -Wl,--export-all-symbols (mingw-lld doesn't accept
/EXPORT:NAME), and links against libcrafter-build.dll.a from the
launcher's directory.
- Mingw-host GetBaseCommand and BuildStdPcm dispatch on config.target
so a mingw-host crafter-build can also build msvc-target outputs
(uses LIBCXX_DIR + libc++ headers, same as native build.cmd) when
the user sets cfg.target = "x86_64-pc-windows-msvc".
README adds a Quick start (Windows) section covering both build paths
(native MSVC via build.cmd and the cross-compiled mingw artifact),
documenting the msys2 UCRT toolchain prerequisite.
Verified end-to-end on the winvm:
- mingw target: cross-compiled crafter-build.exe builds hello-world's
project.cpp, compiles main.cpp, links a hello.exe that runs without
any custom PATH (only Windows system DLLs needed).
- msvc target: same crafter-build.exe builds an MSVC-ABI hello.exe
linked against c++.dll (auto-copied from LIBCXX_DIR), runs cleanly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 02:23:42 +02:00
// LibraryDynamic. Output names follow each target's convention so
// consumers can link via the standard linker search:
// mingw: <name>.dll + lib<name>.dll.a (lld --out-implib)
// msvc: <name>.dll + <name>.lib (lld /IMPLIB)
// unix: lib<name>.so (rpath $ORIGIN)
if ( config . target = = " x86_64-w64-mingw32 " ) {
fs : : path dll = outputDir / std : : format ( " {}.dll " , config . outputName ) ;
fs : : path implib = outputDir / std : : format ( " lib{}.dll.a " , config . outputName ) ;
2026-07-23 01:24:42 +02:00
buildResult . result = RunCommand ( std : : format ( " {}{} -shared -o {} -Wl,--out-implib,{} -fuse-ld=lld{} " , command , files , dll . string ( ) , implib . string ( ) , linkExtras ) ) ;
Cross-compiled mingw artifact: full DLL+launcher pattern + MSVC target
Linux→mingw cross-compile now produces the same architectural shape as
build.cmd (DLL + import lib + launcher exe) instead of a single static
binary. The CI Windows artifact becomes a first-class drop-in: a user
on Windows can run crafter-build.exe against any project.cpp and have
it produce real Windows binaries — for either mingw or MSVC ABI.
What changed:
project.cpp: when target=mingw or target=msvc, crafter.build-lib is
built as LibraryDynamic instead of LibraryStatic so the link emits a
DLL + import lib (matching what build.cmd produces natively).
Crafter.Build-Clang.cpp Build():
- LibraryDynamic now branches per target — mingw emits <name>.dll +
lib<name>.dll.a via lld --out-implib; msvc emits <name>.dll +
<name>.lib via /IMPLIB; unix unchanged.
- expectedOutputFor returns .dll for Windows-target dynamic libs.
- Executable on Windows host now branches per target: mingw target
uses simple link (no -lc++/-nostdlib++/LIBCXX_DIR), msvc target keeps
the existing path. Both auto-copy LibraryDynamic dep DLLs + import
libs alongside the launcher exe (Windows resolves DLLs from the exe's
own directory at load time).
- Mingw-target Executables get -D CRAFTER_BUILD_DLL_IMPORT so
CRAFTER_API resolves to dllimport in their PCMs.
- mingw link adds -static-libstdc++ -static-libgcc -Wl,-Bstatic
-lpthread so produced .exe/.dll don't depend on a particular
libstdc++-6.dll / libwinpthread-1.dll being on the consumer's PATH
(avoids the Arch UCRT vs msys2 UCRT vs msys2 MSVCRT ABI rabbit hole).
Drops the old auto-copy of /usr/x86_64-w64-mingw32/bin/*.dll which
is now dead weight.
- -r flag resolves to an absolute path before std::system, otherwise
cmd.exe rejects "./bin/..." with "'.' is not recognized...".
Crafter.Build-Platform.cpp:
- Split the Windows-host block into shared shell helpers (#if MSVC ||
MINGW) plus separate #if MSVC and #if MINGW blocks for LoadProject /
EnsureCrafterBuildPcms / GetBaseCommand / BuildStdPcm.
- Mingw-host LoadProject compiles project.cpp with --target=mingw,
--sysroot=C:\msys64\ucrt64 (default; override with CRAFTER_MINGW_DIR),
-femulated-tls, -Wl,--export-all-symbols (mingw-lld doesn't accept
/EXPORT:NAME), and links against libcrafter-build.dll.a from the
launcher's directory.
- Mingw-host GetBaseCommand and BuildStdPcm dispatch on config.target
so a mingw-host crafter-build can also build msvc-target outputs
(uses LIBCXX_DIR + libc++ headers, same as native build.cmd) when
the user sets cfg.target = "x86_64-pc-windows-msvc".
README adds a Quick start (Windows) section covering both build paths
(native MSVC via build.cmd and the cross-compiled mingw artifact),
documenting the msys2 UCRT toolchain prerequisite.
Verified end-to-end on the winvm:
- mingw target: cross-compiled crafter-build.exe builds hello-world's
project.cpp, compiles main.cpp, links a hello.exe that runs without
any custom PATH (only Windows system DLLs needed).
- msvc target: same crafter-build.exe builds an MSVC-ABI hello.exe
linked against c++.dll (auto-copied from LIBCXX_DIR), runs cleanly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 02:23:42 +02:00
} else if ( config . target = = " x86_64-pc-windows-msvc " ) {
fs : : path dll = outputDir / std : : format ( " {}.dll " , config . outputName ) ;
fs : : path implib = outputDir / std : : format ( " {}.lib " , config . outputName ) ;
2026-07-23 01:24:42 +02:00
buildResult . result = RunCommand ( std : : format ( " {}{} -shared -o {} -Wl,/IMPLIB:{} -fuse-ld=lld{} " , command , files , dll . string ( ) , implib . string ( ) , linkExtras ) ) ;
Cross-compiled mingw artifact: full DLL+launcher pattern + MSVC target
Linux→mingw cross-compile now produces the same architectural shape as
build.cmd (DLL + import lib + launcher exe) instead of a single static
binary. The CI Windows artifact becomes a first-class drop-in: a user
on Windows can run crafter-build.exe against any project.cpp and have
it produce real Windows binaries — for either mingw or MSVC ABI.
What changed:
project.cpp: when target=mingw or target=msvc, crafter.build-lib is
built as LibraryDynamic instead of LibraryStatic so the link emits a
DLL + import lib (matching what build.cmd produces natively).
Crafter.Build-Clang.cpp Build():
- LibraryDynamic now branches per target — mingw emits <name>.dll +
lib<name>.dll.a via lld --out-implib; msvc emits <name>.dll +
<name>.lib via /IMPLIB; unix unchanged.
- expectedOutputFor returns .dll for Windows-target dynamic libs.
- Executable on Windows host now branches per target: mingw target
uses simple link (no -lc++/-nostdlib++/LIBCXX_DIR), msvc target keeps
the existing path. Both auto-copy LibraryDynamic dep DLLs + import
libs alongside the launcher exe (Windows resolves DLLs from the exe's
own directory at load time).
- Mingw-target Executables get -D CRAFTER_BUILD_DLL_IMPORT so
CRAFTER_API resolves to dllimport in their PCMs.
- mingw link adds -static-libstdc++ -static-libgcc -Wl,-Bstatic
-lpthread so produced .exe/.dll don't depend on a particular
libstdc++-6.dll / libwinpthread-1.dll being on the consumer's PATH
(avoids the Arch UCRT vs msys2 UCRT vs msys2 MSVCRT ABI rabbit hole).
Drops the old auto-copy of /usr/x86_64-w64-mingw32/bin/*.dll which
is now dead weight.
- -r flag resolves to an absolute path before std::system, otherwise
cmd.exe rejects "./bin/..." with "'.' is not recognized...".
Crafter.Build-Platform.cpp:
- Split the Windows-host block into shared shell helpers (#if MSVC ||
MINGW) plus separate #if MSVC and #if MINGW blocks for LoadProject /
EnsureCrafterBuildPcms / GetBaseCommand / BuildStdPcm.
- Mingw-host LoadProject compiles project.cpp with --target=mingw,
--sysroot=C:\msys64\ucrt64 (default; override with CRAFTER_MINGW_DIR),
-femulated-tls, -Wl,--export-all-symbols (mingw-lld doesn't accept
/EXPORT:NAME), and links against libcrafter-build.dll.a from the
launcher's directory.
- Mingw-host GetBaseCommand and BuildStdPcm dispatch on config.target
so a mingw-host crafter-build can also build msvc-target outputs
(uses LIBCXX_DIR + libc++ headers, same as native build.cmd) when
the user sets cfg.target = "x86_64-pc-windows-msvc".
README adds a Quick start (Windows) section covering both build paths
(native MSVC via build.cmd and the cross-compiled mingw artifact),
documenting the msys2 UCRT toolchain prerequisite.
Verified end-to-end on the winvm:
- mingw target: cross-compiled crafter-build.exe builds hello-world's
project.cpp, compiles main.cpp, links a hello.exe that runs without
any custom PATH (only Windows system DLLs needed).
- msvc target: same crafter-build.exe builds an MSVC-ABI hello.exe
linked against c++.dll (auto-copied from LIBCXX_DIR), runs cleanly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 02:23:42 +02:00
} else {
2026-07-23 01:24:42 +02:00
buildResult . result = RunCommand ( std : : format ( " {}{} -shared -o {}.so -Wl,-rpath,'$ORIGIN' -fuse-ld=lld{} " , command , files , ( outputDir / ( std : : string ( " lib " ) + config . outputName ) ) . string ( ) , linkExtras ) ) ;
Cross-compiled mingw artifact: full DLL+launcher pattern + MSVC target
Linux→mingw cross-compile now produces the same architectural shape as
build.cmd (DLL + import lib + launcher exe) instead of a single static
binary. The CI Windows artifact becomes a first-class drop-in: a user
on Windows can run crafter-build.exe against any project.cpp and have
it produce real Windows binaries — for either mingw or MSVC ABI.
What changed:
project.cpp: when target=mingw or target=msvc, crafter.build-lib is
built as LibraryDynamic instead of LibraryStatic so the link emits a
DLL + import lib (matching what build.cmd produces natively).
Crafter.Build-Clang.cpp Build():
- LibraryDynamic now branches per target — mingw emits <name>.dll +
lib<name>.dll.a via lld --out-implib; msvc emits <name>.dll +
<name>.lib via /IMPLIB; unix unchanged.
- expectedOutputFor returns .dll for Windows-target dynamic libs.
- Executable on Windows host now branches per target: mingw target
uses simple link (no -lc++/-nostdlib++/LIBCXX_DIR), msvc target keeps
the existing path. Both auto-copy LibraryDynamic dep DLLs + import
libs alongside the launcher exe (Windows resolves DLLs from the exe's
own directory at load time).
- Mingw-target Executables get -D CRAFTER_BUILD_DLL_IMPORT so
CRAFTER_API resolves to dllimport in their PCMs.
- mingw link adds -static-libstdc++ -static-libgcc -Wl,-Bstatic
-lpthread so produced .exe/.dll don't depend on a particular
libstdc++-6.dll / libwinpthread-1.dll being on the consumer's PATH
(avoids the Arch UCRT vs msys2 UCRT vs msys2 MSVCRT ABI rabbit hole).
Drops the old auto-copy of /usr/x86_64-w64-mingw32/bin/*.dll which
is now dead weight.
- -r flag resolves to an absolute path before std::system, otherwise
cmd.exe rejects "./bin/..." with "'.' is not recognized...".
Crafter.Build-Platform.cpp:
- Split the Windows-host block into shared shell helpers (#if MSVC ||
MINGW) plus separate #if MSVC and #if MINGW blocks for LoadProject /
EnsureCrafterBuildPcms / GetBaseCommand / BuildStdPcm.
- Mingw-host LoadProject compiles project.cpp with --target=mingw,
--sysroot=C:\msys64\ucrt64 (default; override with CRAFTER_MINGW_DIR),
-femulated-tls, -Wl,--export-all-symbols (mingw-lld doesn't accept
/EXPORT:NAME), and links against libcrafter-build.dll.a from the
launcher's directory.
- Mingw-host GetBaseCommand and BuildStdPcm dispatch on config.target
so a mingw-host crafter-build can also build msvc-target outputs
(uses LIBCXX_DIR + libc++ headers, same as native build.cmd) when
the user sets cfg.target = "x86_64-pc-windows-msvc".
README adds a Quick start (Windows) section covering both build paths
(native MSVC via build.cmd and the cross-compiled mingw artifact),
documenting the msys2 UCRT toolchain prerequisite.
Verified end-to-end on the winvm:
- mingw target: cross-compiled crafter-build.exe builds hello-world's
project.cpp, compiles main.cpp, links a hello.exe that runs without
any custom PATH (only Windows system DLLs needed).
- msvc target: same crafter-build.exe builds an MSVC-ABI hello.exe
linked against c++.dll (auto-copied from LIBCXX_DIR), runs cleanly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 02:23:42 +02:00
}
2026-04-23 01:57:25 +02:00
}
}
2026-04-27 07:04:42 +02:00
if ( config . type = = ConfigurationType : : LibraryStatic | | config . type = = ConfigurationType : : LibraryDynamic ) {
buildResult . libs . insert ( std : : format ( " -L{} " , outputDir . string ( ) ) ) ;
buildResult . libs . insert ( std : : format ( " -l{} " , config . outputName ) ) ;
}
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
// Browser-wasm feature-detected variants. The baseline outputName.wasm is
// now built (above). For each declared variant, recompile the entire build
// graph with the variant's extra codegen flags and drop the result next to
// the baseline as outputName.<label>.wasm. Relaxed-SIMD (and friends) is
// per-translation-unit codegen, not a link switch, so a full rebuild — incl.
// dep libs and the std PCM — is the only correct way to produce it.
//
// Guard: only fires on the top-level config that *declared* wasmVariants
// (deps leave it empty) and never on a variant pass itself (those carry
// wasmVariantFlags), so there's no recursion. Skipped on build error.
2026-07-23 01:24:42 +02:00
if ( buildResult . result . empty ( ) & & config . target . starts_with ( " wasm32 " ) & & config . type = = ConfigurationType : : Executable & & ! config . wasmVariants . empty ( ) & & config . wasmVariantFlags . empty ( ) ) {
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
fs : : path baselineBin = config . BinDir ( ) ;
// The whole transitive graph, so the variant flags reach every TU.
std : : vector < Configuration * > graph ;
{
std : : unordered_set < Configuration * > graphSeen ;
std : : function < void ( Configuration * ) > collect = [ & ] ( Configuration * c ) {
if ( ! c | | ! graphSeen . insert ( c ) . second ) return ;
graph . push_back ( c ) ;
for ( Configuration * dep : c - > dependencies ) collect ( dep ) ;
} ;
collect ( & config ) ;
}
for ( const WasmVariant & variant : config . wasmVariants ) {
// A labelless or flagless "variant" is just the baseline under
// another name — nothing extra to compile.
if ( variant . label . empty ( ) | | variant . flags . empty ( ) ) continue ;
2026-07-23 01:24:42 +02:00
Progress : : Task task ( std : : format ( " Building wasm variant {} ({}) " , variant . label , config . outputName ) ) ;
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
for ( Configuration * c : graph ) c - > wasmVariantFlags = variant . flags ;
fs : : path variantBin = config . BinDir ( ) ;
std : : unordered_map < fs : : path , std : : shared_future < BuildResult > > variantDeps ;
std : : mutex variantMutex ;
BuildResult vr = Build ( config , variantDeps , variantMutex ) ;
for ( Configuration * c : graph ) c - > wasmVariantFlags . clear ( ) ;
if ( ! vr . result . empty ( ) ) {
2026-07-23 01:24:42 +02:00
buildResult . result = std : : format ( " wasm variant '{}' build failed: {} " , variant . label , vr . result ) ;
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
break ;
}
std : : error_code ec ;
2026-07-23 01:24:42 +02:00
fs : : path src = variantBin / ( std : : format ( " {}.wasm " , config . outputName ) ) ;
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
fs : : path dst = baselineBin / std : : format ( " {}.{}.wasm " , config . outputName , variant . label ) ;
fs : : copy_file ( src , dst , fs : : copy_options : : overwrite_existing , ec ) ;
if ( ec ) {
2026-07-23 01:24:42 +02:00
buildResult . result = std : : format ( " copy wasm variant '{}' {} -> {}: {} " , variant . label , src . string ( ) , dst . string ( ) , ec . message ( ) ) ;
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
break ;
}
}
}
2026-04-23 01:57:25 +02:00
return buildResult ;
2026-04-27 07:04:42 +02: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
void Crafter : : EnableWasiBrowserRuntime ( Configuration & cfg ) {
fs : : path runtimeDir = GetCrafterBuildHome ( ) / " wasi-runtime " ;
fs : : path runtimeJs = runtimeDir / " runtime.js " ;
fs : : path htmlTemplate = runtimeDir / " index.html.in " ;
if ( ! fs : : exists ( runtimeJs ) | | ! fs : : exists ( htmlTemplate ) ) {
2026-07-23 01:24:42 +02:00
throw std : : runtime_error ( std : : format ( " wasi-runtime assets missing under {} (set CRAFTER_BUILD_HOME or reinstall) " , runtimeDir . string ( ) ) ) ;
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 htmlOutDir = cfg . path / " build " / " wasi-runtime " / cfg . name ;
fs : : create_directories ( htmlOutDir ) ;
fs : : path htmlPath = htmlOutDir / " index.html " ;
2026-05-18 05:23:11 +02:00
// Walk the dep graph for env-style JS bridges that need to load BEFORE
// runtime.js so they can populate `window.crafter_webbuild_env`. Any
// `*.js` entry in a (transitive) dep's `cfg.files` qualifies — the
// file is already going to be copied into the consumer's bin dir, we
// just need its basename for a `<script src=...>` tag.
std : : vector < std : : string > envScripts ;
std : : unordered_set < Configuration * > seen ;
std : : function < void ( Configuration * ) > walk = [ & ] ( Configuration * c ) {
if ( ! c | | ! seen . insert ( c ) . second ) return ;
for ( const fs : : path & f : c - > files ) {
if ( f . extension ( ) = = " .js " & & f . filename ( ) ! = " runtime.js " ) {
std : : string name = f . filename ( ) . string ( ) ;
if ( std : : find ( envScripts . begin ( ) , envScripts . end ( ) , name ) = = envScripts . end ( ) ) {
envScripts . push_back ( std : : move ( name ) ) ;
}
}
}
for ( Configuration * dep : c - > dependencies ) walk ( dep ) ;
} ;
walk ( & cfg ) ;
2026-05-26 22:50:08 +02:00
// Per-build cache-busting token. Stamped onto every script src + the
// wasm URL so a regular browser reload sees fresh files even though
// the dev server (python -m http.server) sends no Cache-Control
// headers. Using ms-since-epoch is enough to be unique per build
// without invoking any version-control machinery.
2026-07-23 01:24:42 +02:00
const std : : string buildId = std : : to_string ( std : : chrono : : duration_cast < std : : chrono : : milliseconds > ( std : : chrono : : system_clock : : now ( ) . time_since_epoch ( ) ) . count ( ) ) ;
2026-05-26 22:50:08 +02:00
2026-05-18 05:23:11 +02:00
std : : string envScriptTags ;
for ( const std : : string & name : envScripts ) {
2026-07-23 01:24:42 +02:00
envScriptTags + = std : : format ( " <script src= \" {}?v={} \" type= \" module \" ></script> \n " , name , buildId ) ;
2026-05-18 05:23:11 +02:00
}
// Walk the dep graph again for non-JS assets — these get pre-loaded by
// runtime.js into an in-memory VFS so the wasm's std::ifstream et al.
// can actually read them (the wasi-runtime in this repo otherwise
2026-05-19 03:28:27 +02:00
// stubs every fd syscall to zero).
//
// The manifest lists *relative paths* (e.g. "assets/Inter.ttf") so
// runtime.js's fetch() resolves against the bin-dir layout the asset
// copy step actually emits. The VFS is keyed by basename — path_open
// strips to basename on lookup, so subdir layouts collapse on the
// wasm side. Basename collisions across subdirs aren't supported on
// the wasi runtime today; if two assets share a basename, the last
// one preloaded wins. Avoid collisions in the source tree.
auto compressedExt = [ ] ( const fs : : path & src ) - > std : : optional < std : : string > {
2026-05-19 00:50:06 +02:00
std : : string ext = src . extension ( ) . string ( ) ;
2026-07-23 01:24:42 +02:00
for ( char & c : ext ) c = static_cast < char > ( std : : tolower ( static_cast < std : : uint8_t > ( c ) ) ) ;
2026-05-19 00:50:06 +02:00
if ( ext = = " .png " | | ext = = " .tga " | | ext = = " .jpg " | | ext = = " .jpeg " | | ext = = " .bmp " ) {
2026-05-19 03:28:27 +02:00
return std : : string ( " .ctex " ) ;
2026-05-19 00:50:06 +02:00
}
2026-05-19 03:28:27 +02:00
if ( ext = = " .obj " ) return std : : string ( " .cmesh " ) ;
return std : : nullopt ;
} ;
auto compressedRel = [ & ] ( const fs : : path & rel ) - > fs : : path {
if ( auto ext = compressedExt ( rel ) ) {
fs : : path out = rel ;
out . replace_extension ( * ext ) ;
return out ;
}
return rel ;
2026-05-19 00:50:06 +02:00
} ;
2026-05-18 05:23:11 +02:00
std : : vector < std : : string > assetFiles ;
2026-05-19 03:28:27 +02:00
auto pushUnique = [ & ] ( std : : string name ) {
if ( name . empty ( ) ) return ;
if ( std : : find ( assetFiles . begin ( ) , assetFiles . end ( ) , name ) = = assetFiles . end ( ) ) {
assetFiles . push_back ( std : : move ( name ) ) ;
}
} ;
2026-05-18 05:23:11 +02:00
seen . clear ( ) ;
std : : function < void ( Configuration * ) > walkAssets = [ & ] ( Configuration * c ) {
if ( ! c | | ! seen . insert ( c ) . second ) return ;
for ( const fs : : path & f : c - > files ) {
std : : string ext = f . extension ( ) . string ( ) ;
if ( ext = = " .js " | | ext = = " .html " ) continue ;
if ( f . filename ( ) = = " runtime.js " ) continue ;
2026-05-19 03:28:27 +02:00
// cfg.files lands flat next to the .wasm by `name = filename()`.
pushUnique ( f . filename ( ) . string ( ) ) ;
2026-05-18 05:23:11 +02:00
}
2026-05-19 03:28:27 +02:00
// cfg.assets — mirror the bin-dir layout the build emits: a
// directory entry becomes <topName>/<rel inside dir>, single
// files land flat at the bin root. .png/.obj are compressed in
// place; everything else passes through under its original name.
2026-05-19 00:50:06 +02:00
for ( const fs : : path & a : c - > assets ) {
if ( fs : : is_directory ( a ) ) {
2026-05-19 03:28:27 +02:00
const fs : : path topName = a . filename ( ) ;
2026-05-19 00:50:06 +02:00
std : : error_code ec ;
for ( const auto & entry : fs : : recursive_directory_iterator ( a , ec ) ) {
if ( ec ) break ;
if ( ! entry . is_regular_file ( ) ) continue ;
2026-05-19 03:28:27 +02:00
fs : : path rel = fs : : relative ( entry . path ( ) , a ) ;
pushUnique ( ( topName / compressedRel ( rel ) ) . generic_string ( ) ) ;
2026-05-19 00:50:06 +02:00
}
} else {
2026-05-19 03:28:27 +02:00
pushUnique ( compressedRel ( a . filename ( ) ) . generic_string ( ) ) ;
2026-05-19 00:50:06 +02:00
}
}
2026-05-18 05:23:11 +02:00
for ( Configuration * dep : c - > dependencies ) walkAssets ( dep ) ;
} ;
walkAssets ( & cfg ) ;
fs : : path manifestPath = htmlOutDir / " files.json " ;
{
std : : ofstream m ( manifestPath ) ;
m < < " [ " ;
for ( std : : size_t i = 0 ; i < assetFiles . size ( ) ; + + i ) {
if ( i ) m < < " , " ;
m < < " \" " < < assetFiles [ i ] < < " \" " ;
}
m < < " ] " ;
}
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
// Variant manifest. runtime.js fetches this, runs each entry's probes in
// order, and instantiates the first whose probes all pass — so the
// preferred (most feature-rich) variants come first and the baseline last
// as the universal fallback (empty probes always pass). A variant with a
// non-empty label maps to outputName.<label>.wasm (produced by Build's
// variant driver); the baseline maps to outputName.wasm. Even with no
// declared variants we still emit a one-entry manifest so the runtime has a
// single, uniform code path.
auto jsonEscape = [ ] ( std : : string_view s ) {
std : : string out ;
for ( char c : s ) {
if ( c = = ' " ' | | c = = ' \\ ' ) out + = ' \\ ' ;
out + = c ;
}
return out ;
} ;
fs : : path variantsPath = htmlOutDir / " variants.json " ;
{
std : : ofstream v ( variantsPath ) ;
v < < " [ " ;
bool first = true ;
auto emit = [ & ] ( const std : : string & label , const std : : vector < std : : string > & probes ) {
if ( ! first ) v < < " , " ;
first = false ;
std : : string url = label . empty ( )
2026-07-23 01:24:42 +02:00
? std : : format ( " {}.wasm " , cfg . outputName )
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
: std : : format ( " {}.{}.wasm " , cfg . outputName , label ) ;
v < < " { \" label \" : \" " < < jsonEscape ( label ) < < " \" "
< < " , \" url \" : \" " < < jsonEscape ( url ) < < " \" "
< < " , \" probes \" :[ " ;
for ( std : : size_t i = 0 ; i < probes . size ( ) ; + + i ) {
if ( i ) v < < " , " ;
v < < " \" " < < jsonEscape ( probes [ i ] ) < < " \" " ;
}
v < < " ]} " ;
} ;
for ( const WasmVariant & variant : cfg . wasmVariants ) {
if ( variant . label . empty ( ) ) continue ; // baseline emitted below
emit ( variant . label , variant . probes ) ;
}
emit ( " " , { } ) ; // baseline fallback, always last
v < < " ] " ;
}
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 : : ifstream in ( htmlTemplate ) ;
std : : stringstream buf ;
buf < < in . rdbuf ( ) ;
2026-05-18 05:23:11 +02:00
std : : string html = buf . str ( ) ;
2026-07-23 01:24:42 +02:00
html = std : : regex_replace ( html , std : : regex ( R " ( \ { \ {WASM \ } \ }) " ) , std : : format ( " {}.wasm " , cfg . outputName ) ) ;
2026-05-18 05:23:11 +02:00
html = std : : regex_replace ( html , std : : regex ( R " ( \ { \ {ENV_SCRIPTS \ } \ }) " ) , envScriptTags ) ;
2026-05-26 22:50:08 +02:00
html = std : : regex_replace ( html , std : : regex ( R " ( \ { \ {BUILDID \ } \ }) " ) , buildId ) ;
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 : : ofstream out ( htmlPath ) ;
out < < html ;
out . close ( ) ;
cfg . files . push_back ( runtimeJs ) ;
cfg . files . push_back ( htmlPath ) ;
2026-05-18 05:23:11 +02:00
cfg . files . push_back ( manifestPath ) ;
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
cfg . files . push_back ( variantsPath ) ;
}
void Crafter : : EnableWasiRelaxedSimdVariant ( Configuration & cfg ) {
for ( const WasmVariant & v : cfg . wasmVariants ) {
if ( v . label = = " relaxed-simd " ) return ; // idempotent
}
cfg . wasmVariants . push_back ( WasmVariant {
. label = " relaxed-simd " ,
. flags = { " -mrelaxed-simd " } ,
. probes = { " relaxed-simd " } ,
} ) ;
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-29 18:59:01 +02:00
std : : string Crafter : : HostTarget ( ) {
2026-07-23 01:24:42 +02:00
static const std : : string Cached = [ ] ( ) - > std : : string {
2026-04-29 18:59:01 +02:00
CommandResult r = RunCommandChecked ( " clang++ -print-target-triple " ) ;
if ( r . exitCode ! = 0 ) return { } ;
std : : string out = std : : move ( r . output ) ;
while ( ! out . empty ( ) & & ( out . back ( ) = = ' \n ' | | out . back ( ) = = ' \r ' ) ) out . pop_back ( ) ;
return out ;
} ( ) ;
2026-07-23 01:24:42 +02:00
return Cached ;
2026-04-29 18:59:01 +02:00
}
2026-04-30 04:15:29 +02:00
bool Crafter : : ArgQuery : : Has ( std : : string_view flag ) const {
for ( std : : string_view a : args ) if ( a = = flag ) return true ;
return false ;
}
std : : optional < std : : string > Crafter : : ArgQuery : : Get ( std : : string_view prefix ) const {
for ( std : : string_view a : args ) {
if ( a . starts_with ( prefix ) ) return std : : string ( a . substr ( prefix . size ( ) ) ) ;
}
return std : : nullopt ;
}
2026-04-30 02:20:19 +02:00
ArgQuery Crafter : : ApplyStandardArgs ( Configuration & cfg , std : : span < const std : : string_view > args ) {
2026-04-29 18:59:01 +02:00
if ( const char * envMarch = std : : getenv ( " CRAFTER_BUILD_MARCH " ) ; envMarch & & * envMarch ) {
cfg . march = envMarch ;
}
if ( const char * envMtune = std : : getenv ( " CRAFTER_BUILD_MTUNE " ) ; envMtune & & * envMtune ) {
cfg . mtune = envMtune ;
}
2026-07-23 01:24:42 +02:00
bool sawLib = false ;
bool sawShared = false ;
2026-04-29 18:59:01 +02:00
for ( std : : string_view a : args ) {
if ( a = = " --debug " ) cfg . debug = true ;
2026-04-30 02:20:19 +02:00
else if ( a = = " --lib " ) sawLib = true ;
else if ( a = = " --shared " ) sawShared = true ;
2026-04-29 18:59:01 +02:00
else if ( a . starts_with ( " --target= " ) ) cfg . target = std : : string ( a . substr ( std : : string_view ( " --target= " ) . size ( ) ) ) ;
else if ( a . starts_with ( " --march= " ) ) cfg . march = std : : string ( a . substr ( std : : string_view ( " --march= " ) . size ( ) ) ) ;
else if ( a . starts_with ( " --mtune= " ) ) cfg . mtune = std : : string ( a . substr ( std : : string_view ( " --mtune= " ) . size ( ) ) ) ;
2026-07-22 18:25:39 +02:00
else if ( a . starts_with ( " --sysroot= " ) ) cfg . sysroot = std : : string ( a . substr ( std : : string_view ( " --sysroot= " ) . size ( ) ) ) ;
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
// Anything else is the project's own flag. Its effect on the output is
// opaque to the framework, so it has to key into VariantId or two flag
// settings share a bin dir and leave a bundle matching neither.
else cfg . projectArgs . emplace_back ( a ) ;
2026-04-29 18:59:01 +02:00
}
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
// Sorted and deduplicated so the identity depends on the set of flags, not
// on how they were ordered or repeated.
std : : ranges : : sort ( cfg . projectArgs ) ;
cfg . projectArgs . erase ( std : : ranges : : unique ( cfg . projectArgs ) . begin ( ) , cfg . projectArgs . end ( ) ) ;
2026-04-30 02:20:19 +02:00
if ( sawLib & & cfg . type = = ConfigurationType : : Executable ) cfg . type = ConfigurationType : : LibraryStatic ;
if ( sawShared & & cfg . type = = ConfigurationType : : LibraryStatic ) cfg . type = ConfigurationType : : LibraryDynamic ;
2026-05-18 05:23:11 +02:00
// WASI sysroot autodetect, applied at config-load time so the VariantId
// includes it. (Build() also runs this once more for callers that bypassed
// ApplyStandardArgs, but doing it here makes dep PcmDirs consistent
// between the consumer's command-construction and the dep's own build.)
# ifdef CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_pc_linux_gnu
if ( cfg . sysroot . empty ( ) & & cfg . target . starts_with ( " wasm32 " ) ) {
cfg . sysroot = " /usr/share/wasi-sysroot " ;
}
# endif
2026-04-30 02:20:19 +02:00
return ArgQuery { args } ;
2026-04-29 18:59:01 +02:00
}
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
std : : vector < fs : : path > Crafter : : CleanProject ( const fs : : path & projectFile ) {
fs : : path projectDir = fs : : absolute ( projectFile ) . lexically_normal ( ) . parent_path ( ) ;
std : : vector < fs : : path > removed ;
for ( std : : string_view name : { " bin " , " build " } ) {
fs : : path dir = projectDir / name ;
std : : error_code ec ;
if ( ! fs : : is_directory ( dir , ec ) ) continue ;
if ( fs : : remove_all ( dir , ec ) = = static_cast < std : : uintmax_t > ( - 1 ) | | ec ) {
throw std : : runtime_error ( std : : format ( " could not remove {}: {} " , dir . string ( ) , ec . message ( ) ) ) ;
}
removed . push_back ( std : : move ( dir ) ) ;
}
return removed ;
}
2026-04-29 04:00:07 +02:00
static void PrintHelp ( std : : string_view argv0 ) {
std : : println (
R " (Usage:
{ 0 } [ options ] [ - - project - args . . . ] Build the project in the current directory
{ 0 } test [ test - options ] [ globs . . . ] Build and run the project ' s tests
2026-07-23 01:24:42 +02:00
{ 0 } lint [ lint - options ] [ globs . . . ] Run the project ' s lint rules over its sources
{ 0 } format [ format - options ] [ globs . . . ] Apply the project ' s transform rules ( rewrites files )
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
{ 0 } clean Delete the project ' s bin / and build / trees
2026-04-29 04:00:07 +02:00
{ 0 } help | - h | - - help Show this help
Loads . / project . cpp ( override with - - project = < path > ) , compiles it to a shared
object , and invokes its CrafterBuildProject ( ) to obtain a Configuration that
drives the build . Outputs land at bin / < name > - < target > - < march > / , intermediates
at build / < name > - < target > - < march > / .
Build options :
- r Run the produced executable after a successful build
( host targets only ; libraries cannot be run ) .
- v , - - verbose Verbose progress output .
- q , - - quiet Suppress progress output .
- - project = < path > Path to the project file ( default : . / project . cpp ) .
Test options ( after the ` test ` subcommand ) :
- - list Enumerate matching tests without running them .
- - jobs = < N > Parallel job count ( default : hardware_concurrency ) .
- - timeout = < seconds > Per - test timeout override .
- - runner = < spec > Override the test runner for this run . Specs :
local
cmd : < command > ( e . g . cmd : wine )
2026-05-27 19:45:05 +02:00
- - target = < triple > Filter to tests whose cfg . target matches . Default :
sweep across every distinct target declared by the
project ' s tests plus the host triple .
2026-04-29 04:00:07 +02:00
< glob > One or more name globs to filter tests ( e . g . ' Unit * ' ) .
2026-07-23 01:24:42 +02:00
Lint options ( after the ` lint ` subcommand ) :
- - list Enumerate matching lint rules without running them .
feat(lint): AST layer over libclang cursors
Adds LintContext::Decls() — clang's view of the declarations written in the
file — plus AddAstLintRule to register a rule that reads it. No rule uses it
yet; the three that will are migrated separately.
The declarations come back as a flat vector with parent indices rather than an
opaque cursor handle: no lifetimes cross the project-DLL boundary, no callback
hops back into project.so per node, and "is this at namespace scope or inside a
function?" becomes an index lookup instead of a hand-rolled brace stack.
Three things had to be solved for this to work at all on this codebase.
libclang cannot see through `export`. A C++20 export declaration has no
CXCursorKind, so `export namespace Crafter { … }` arrives as a childless
CXCursor_UnexposedDecl and clang_visitChildren does not descend. Five of the
eleven interfaces here are written that way — 677 lines, including
Configuration and LintContext, yielding zero usable cursors. A plain
`namespace` IS descended into, so the fix is to blank the keyword before
parsing, byte-length preserving so every line and column still lands on the
original file. `export module` is left alone or the unit stops being a module
interface. Verified end-to-end against a fixture whose asserted line numbers
match the unblanked file.
PCMs are flag-locked, so each file has to parse with the flags that built it.
CollectConfigSources now records which Configuration owns each source instead
of flattening to a set, because three regimes coexist: the library, each test
(carrying its own target, defines and -march), and project.cpp, which Build
never touches and which therefore gets no command at all.
libclang resolves its builtin headers relative to its own install path, which
need not match the clang++ that wrote the PCMs. When it doesn't, every parse
dies on "'stddef.h' file not found", so -resource-dir is passed explicitly
from `clang++ -print-resource-dir`.
Two flags on each declaration replace what would otherwise become more
substring denylists: isExternC, and isForeignApi for a declaration that binds
to an entity declared outside the project root — resolved through
clang_getCursorReferenced and the same inside-the-root test the dependency
walk already uses. Parameters and fields inherit it, so an exemption covers a
whole signature rather than the one node that named the foreign entity.
Failure is never silent. A fatal diagnostic leaves a fragment that is
indistinguishable from a file declaring nothing, so it is reported as an error
instead: the rule is skipped, a finding explains why, and summary.errors makes
the run fail. --no-ast opts out deliberately and exits normally.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 23:30:41 +02:00
- - no - ast Skip rules that need an AST instead of building the
module PCMs they require .
2026-07-23 01:24:42 +02:00
< glob > One or more name globs to filter rules ( e . g . ' spdx * ' ) .
Lint rules are defined in project . cpp via cfg . AddLintRule ( name , callback ) —
C + + callbacks run once per source file . No rules ship by default .
feat(lint): AST layer over libclang cursors
Adds LintContext::Decls() — clang's view of the declarations written in the
file — plus AddAstLintRule to register a rule that reads it. No rule uses it
yet; the three that will are migrated separately.
The declarations come back as a flat vector with parent indices rather than an
opaque cursor handle: no lifetimes cross the project-DLL boundary, no callback
hops back into project.so per node, and "is this at namespace scope or inside a
function?" becomes an index lookup instead of a hand-rolled brace stack.
Three things had to be solved for this to work at all on this codebase.
libclang cannot see through `export`. A C++20 export declaration has no
CXCursorKind, so `export namespace Crafter { … }` arrives as a childless
CXCursor_UnexposedDecl and clang_visitChildren does not descend. Five of the
eleven interfaces here are written that way — 677 lines, including
Configuration and LintContext, yielding zero usable cursors. A plain
`namespace` IS descended into, so the fix is to blank the keyword before
parsing, byte-length preserving so every line and column still lands on the
original file. `export module` is left alone or the unit stops being a module
interface. Verified end-to-end against a fixture whose asserted line numbers
match the unblanked file.
PCMs are flag-locked, so each file has to parse with the flags that built it.
CollectConfigSources now records which Configuration owns each source instead
of flattening to a set, because three regimes coexist: the library, each test
(carrying its own target, defines and -march), and project.cpp, which Build
never touches and which therefore gets no command at all.
libclang resolves its builtin headers relative to its own install path, which
need not match the clang++ that wrote the PCMs. When it doesn't, every parse
dies on "'stddef.h' file not found", so -resource-dir is passed explicitly
from `clang++ -print-resource-dir`.
Two flags on each declaration replace what would otherwise become more
substring denylists: isExternC, and isForeignApi for a declaration that binds
to an entity declared outside the project root — resolved through
clang_getCursorReferenced and the same inside-the-root test the dependency
walk already uses. Parameters and fields inherit it, so an exemption covers a
whole signature rather than the one node that named the foreign entity.
Failure is never silent. A fatal diagnostic leaves a fragment that is
indistinguishable from a file declaring nothing, so it is reported as an error
instead: the rule is skipped, a finding explains why, and summary.errors makes
the run fail. --no-ast opts out deliberately and exits normally.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 23:30:41 +02:00
cfg . AddAstLintRule registers a rule that reads ctx . Decls ( ) , clang ' s view of
the declarations in the file . Those need the module PCMs , which the run
builds if they are missing ; a file whose AST cannot be produced is an error ,
never a silent pass . - - no - ast skips them instead .
2026-07-23 01:24:42 +02:00
Format options ( after the ` format ` subcommand ) :
- - check Dry run : list files that would change , exit 1 if any .
- - list Enumerate matching rules without running them .
feat(lint): AST layer over libclang cursors
Adds LintContext::Decls() — clang's view of the declarations written in the
file — plus AddAstLintRule to register a rule that reads it. No rule uses it
yet; the three that will are migrated separately.
The declarations come back as a flat vector with parent indices rather than an
opaque cursor handle: no lifetimes cross the project-DLL boundary, no callback
hops back into project.so per node, and "is this at namespace scope or inside a
function?" becomes an index lookup instead of a hand-rolled brace stack.
Three things had to be solved for this to work at all on this codebase.
libclang cannot see through `export`. A C++20 export declaration has no
CXCursorKind, so `export namespace Crafter { … }` arrives as a childless
CXCursor_UnexposedDecl and clang_visitChildren does not descend. Five of the
eleven interfaces here are written that way — 677 lines, including
Configuration and LintContext, yielding zero usable cursors. A plain
`namespace` IS descended into, so the fix is to blank the keyword before
parsing, byte-length preserving so every line and column still lands on the
original file. `export module` is left alone or the unit stops being a module
interface. Verified end-to-end against a fixture whose asserted line numbers
match the unblanked file.
PCMs are flag-locked, so each file has to parse with the flags that built it.
CollectConfigSources now records which Configuration owns each source instead
of flattening to a set, because three regimes coexist: the library, each test
(carrying its own target, defines and -march), and project.cpp, which Build
never touches and which therefore gets no command at all.
libclang resolves its builtin headers relative to its own install path, which
need not match the clang++ that wrote the PCMs. When it doesn't, every parse
dies on "'stddef.h' file not found", so -resource-dir is passed explicitly
from `clang++ -print-resource-dir`.
Two flags on each declaration replace what would otherwise become more
substring denylists: isExternC, and isForeignApi for a declaration that binds
to an entity declared outside the project root — resolved through
clang_getCursorReferenced and the same inside-the-root test the dependency
walk already uses. Parameters and fields inherit it, so an exemption covers a
whole signature rather than the one node that named the foreign entity.
Failure is never silent. A fatal diagnostic leaves a fragment that is
indistinguishable from a file declaring nothing, so it is reported as an error
instead: the rule is skipped, a finding explains why, and summary.errors makes
the run fail. --no-ast opts out deliberately and exits normally.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 23:30:41 +02:00
- - no - ast Skip rules that need an AST ( see ` lint - - no - ast ` ) .
2026-07-23 01:24:42 +02:00
< glob > One or more name globs to filter rules .
Rules are shared with lint — a rule that calls ctx . SetContent is a
transform . ` format ` rewrites changed files in place ; ` lint ` reports the
same transforms as would - reformat findings without writing .
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
Clean ( after the ` clean ` subcommand ) :
Removes bin / and build / next to the project file . Does not load project . cpp ,
so it works when the project itself no longer compiles . Every target , variant
and dependency artifact under those trees goes with it .
2026-04-29 04:00:07 +02:00
Project args :
Any flag not consumed above is forwarded verbatim to CrafterBuildProject as
part of its ` args ` span . Project - specific flags ( e . g . - - target = , custom
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
feature toggles ) live there . Flags ApplyStandardArgs does not itself
interpret are folded into the variant hash , so switching one lands in its own
bin / and build / directory instead of overwriting the other setting ' s .
2026-04-29 04:00:07 +02:00
Environment :
CRAFTER_BUILD_MARCH Override - march ( default : native ) .
CRAFTER_BUILD_MTUNE Override - mtune ( default : native ) .
CRAFTER_BUILD_RUNNER_ < TARGET > Default test runner for a target triple .
Replace ' - ' and ' . ' with ' _ ' in the
triple . CLI - - runner = overrides this .
CRAFTER_MINGW_DIR Override mingw - w64 sysroot auto - detect .
LIBCXX_DIR Windows libc + + install ( MSVC ABI builds ) .
Exit status :
2026-07-23 01:24:42 +02:00
0 success / all non - skipped tests passed / lint clean / format applied
1 build failure , one or more tests failed , lint findings , no rules defined ,
format write errors , or ` format - - check ` found files that would change
2026-04-29 04:00:07 +02:00
) " , argv0);
}
2026-04-27 07:04:42 +02:00
int Crafter : : Run ( int argc , char * * argv ) {
try {
2026-04-29 04:00:07 +02:00
std : : string_view argv0 = argc > 0 ? argv [ 0 ] : " crafter-build " ;
2026-04-27 07:04:42 +02:00
fs : : path projectFile = " ./project.cpp " ;
std : : vector < std : : string_view > projectArgs ;
projectArgs . reserve ( argc ) ;
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
bool runTests = false ;
2026-07-23 01:24:42 +02:00
bool runLint = false ;
bool runFormat = false ;
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
bool runClean = false ;
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
bool runAfterBuild = false ;
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
RunTestsOptions testOpts ;
2026-07-23 01:24:42 +02:00
RunLintOptions lintOpts ;
2026-04-29 03:27:11 +02:00
Progress : : Verbosity verbosity = Progress : : Verbosity : : Default ;
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
2026-04-27 07:04:42 +02:00
for ( int i = 1 ; i < argc ; + + i ) {
std : : string_view arg = argv [ i ] ;
2026-07-23 01:24:42 +02:00
if ( arg = = " -h " | | arg = = " --help " | | ( ! runTests & & ! runLint & & ! runFormat & & arg = = " help " ) ) {
2026-04-29 04:00:07 +02:00
PrintHelp ( argv0 ) ;
return 0 ;
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
} else if ( ! runLint & & ! runFormat & & ! runClean & & arg = = " test " ) {
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
runTests = true ;
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
} else if ( ! runTests & & ! runFormat & & ! runClean & & arg = = " lint " ) {
2026-07-23 01:24:42 +02:00
runLint = true ;
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
} else if ( ! runTests & & ! runLint & & ! runClean & & arg = = " clean " ) {
runClean = true ;
} else if ( ! runTests & & ! runLint & & ! runClean & & arg = = " format " ) {
2026-07-23 01:24:42 +02:00
runFormat = true ;
lintOpts . mode = LintMode : : Apply ;
} else if ( runFormat & & arg = = " --check " ) {
lintOpts . mode = LintMode : : Check ;
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
} else if ( arg = = " -r " ) {
runAfterBuild = true ;
2026-04-29 03:27:11 +02:00
} else if ( arg = = " -v " | | arg = = " --verbose " ) {
verbosity = Progress : : Verbosity : : Verbose ;
} else if ( arg = = " -q " | | arg = = " --quiet " ) {
verbosity = Progress : : Verbosity : : Quiet ;
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
} else if ( arg . starts_with ( " --project= " ) ) {
2026-04-27 07:04:42 +02:00
projectFile = arg . substr ( std : : string_view ( " --project= " ) . size ( ) ) ;
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
} else if ( runTests & & arg . starts_with ( " --jobs= " ) ) {
testOpts . jobs = std : : stoi ( std : : string ( arg . substr ( std : : string_view ( " --jobs= " ) . size ( ) ) ) ) ;
} else if ( runTests & & arg . starts_with ( " --timeout= " ) ) {
testOpts . timeoutOverride = std : : chrono : : seconds ( std : : stoi ( std : : string ( arg . substr ( std : : string_view ( " --timeout= " ) . size ( ) ) ) ) ) ;
} else if ( runTests & & arg = = " --list " ) {
testOpts . listOnly = true ;
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
} else if ( runTests & & arg . starts_with ( " --runner= " ) ) {
testOpts . runnerOverride = std : : string ( arg . substr ( std : : string_view ( " --runner= " ) . size ( ) ) ) ;
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
} else if ( runTests & & ! arg . starts_with ( " - " ) ) {
testOpts . globs . emplace_back ( arg ) ;
2026-07-23 01:24:42 +02:00
} else if ( ( runLint | | runFormat ) & & arg = = " --list " ) {
lintOpts . listOnly = true ;
feat(lint): AST layer over libclang cursors
Adds LintContext::Decls() — clang's view of the declarations written in the
file — plus AddAstLintRule to register a rule that reads it. No rule uses it
yet; the three that will are migrated separately.
The declarations come back as a flat vector with parent indices rather than an
opaque cursor handle: no lifetimes cross the project-DLL boundary, no callback
hops back into project.so per node, and "is this at namespace scope or inside a
function?" becomes an index lookup instead of a hand-rolled brace stack.
Three things had to be solved for this to work at all on this codebase.
libclang cannot see through `export`. A C++20 export declaration has no
CXCursorKind, so `export namespace Crafter { … }` arrives as a childless
CXCursor_UnexposedDecl and clang_visitChildren does not descend. Five of the
eleven interfaces here are written that way — 677 lines, including
Configuration and LintContext, yielding zero usable cursors. A plain
`namespace` IS descended into, so the fix is to blank the keyword before
parsing, byte-length preserving so every line and column still lands on the
original file. `export module` is left alone or the unit stops being a module
interface. Verified end-to-end against a fixture whose asserted line numbers
match the unblanked file.
PCMs are flag-locked, so each file has to parse with the flags that built it.
CollectConfigSources now records which Configuration owns each source instead
of flattening to a set, because three regimes coexist: the library, each test
(carrying its own target, defines and -march), and project.cpp, which Build
never touches and which therefore gets no command at all.
libclang resolves its builtin headers relative to its own install path, which
need not match the clang++ that wrote the PCMs. When it doesn't, every parse
dies on "'stddef.h' file not found", so -resource-dir is passed explicitly
from `clang++ -print-resource-dir`.
Two flags on each declaration replace what would otherwise become more
substring denylists: isExternC, and isForeignApi for a declaration that binds
to an entity declared outside the project root — resolved through
clang_getCursorReferenced and the same inside-the-root test the dependency
walk already uses. Parameters and fields inherit it, so an exemption covers a
whole signature rather than the one node that named the foreign entity.
Failure is never silent. A fatal diagnostic leaves a fragment that is
indistinguishable from a file declaring nothing, so it is reported as an error
instead: the rule is skipped, a finding explains why, and summary.errors makes
the run fail. --no-ast opts out deliberately and exits normally.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 23:30:41 +02:00
} else if ( ( runLint | | runFormat ) & & arg = = " --no-ast " ) {
lintOpts . noAst = true ;
2026-07-23 01:24:42 +02:00
} else if ( ( runLint | | runFormat ) & & ! arg . starts_with ( " - " ) ) {
lintOpts . globs . emplace_back ( arg ) ;
2026-04-27 07:04:42 +02:00
} else {
projectArgs . push_back ( arg ) ;
}
}
2026-04-29 03:27:11 +02:00
Progress : : SetVerbosity ( verbosity ) ;
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
// The test run is target-scoped: only tests whose cfg.target equals
// testOpts.targetFilter are included. Default = host triple, so a
// bare `crafter-build test` runs everything that targets host.
for ( auto & a : projectArgs ) {
if ( a . starts_with ( " --target= " ) ) {
testOpts . targetFilter = std : : string ( a . substr ( std : : string_view ( " --target= " ) . size ( ) ) ) ;
}
}
2026-04-27 07:04:42 +02:00
if ( ! fs : : exists ( projectFile ) ) {
std : : println ( std : : cerr , " No project file at {} " , projectFile . string ( ) ) ;
return 1 ;
}
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
// Ahead of LoadProject on purpose — see CleanProject.
if ( runClean ) {
std : : vector < fs : : path > removed = CleanProject ( projectFile ) ;
if ( removed . empty ( ) ) {
std : : println ( " Nothing to clean " ) ;
} else {
for ( const fs : : path & dir : removed ) std : : println ( " Removed {} " , dir . string ( ) ) ;
}
return 0 ;
}
2026-04-27 07:04:42 +02:00
Configuration config = LoadProject ( projectFile , projectArgs ) ;
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
SetParentProject ( & config ) ;
2026-04-27 07:04:42 +02:00
2026-07-23 01:24:42 +02:00
if ( runLint | | runFormat ) {
// lexically_normal: fs::absolute keeps a "./" component, which
// would leak into displayed paths and the project-root derivation.
lintOpts . projectFile = fs : : absolute ( projectFile ) . lexically_normal ( ) ;
LintSummary lintSummary = RunLint ( config , lintOpts ) ;
if ( ! runFormat ) return lintSummary . Clean ( ) ? 0 : 1 ;
if ( lintSummary . noRulesDefined | | lintSummary . errors > 0 ) return 1 ;
// Apply: formatting files is success (gofmt convention).
// Check: a would-change file is the CI failure signal.
return lintOpts . mode = = LintMode : : Check & & ! lintSummary . changedFiles . empty ( ) ? 1 : 0 ;
}
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 ( runTests ) {
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
TestSummary summary = RunTests ( config , testOpts , projectArgs ) ;
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
return summary . AllPassed ( ) ? 0 : 1 ;
}
2026-04-27 07:04:42 +02:00
std : : unordered_map < fs : : path , std : : shared_future < BuildResult > > depResults ;
std : : mutex depMutex ;
BuildResult result = Build ( config , depResults , depMutex ) ;
if ( ! result . result . empty ( ) ) {
2026-04-29 03:27:11 +02:00
Progress : : Clear ( ) ;
2026-04-27 07:04:42 +02:00
std : : println ( std : : cerr , " {} " , result . result ) ;
return 1 ;
}
2026-04-29 03:27:11 +02:00
Progress : : Finalize ( ) ;
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
if ( runAfterBuild ) {
if ( config . type ! = ConfigurationType : : Executable ) {
std : : println ( std : : cerr , " -r: cannot run a library " ) ;
return 1 ;
}
2026-04-30 02:20:19 +02:00
fs : : path dir = config . BinDir ( ) ;
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 artifact = dir / config . outputName ;
if ( config . target . starts_with ( " wasm32 " ) ) {
artifact + = " .wasm " ;
} else if ( config . target = = " x86_64-w64-mingw32 " | | config . target = = " x86_64-pc-windows-msvc " ) {
artifact + = " .exe " ;
}
2026-05-18 05:23:11 +02:00
artifact = fs : : absolute ( artifact ) ;
fs : : path absDir = fs : : absolute ( dir ) ;
// wasm targets need either a wasm runtime (wasi-cli) or an HTTP
// server (browser build with index.html). std::system on the
// .wasm path goes nowhere useful — replace with detection.
if ( config . target . starts_with ( " wasm32 " ) ) {
bool browserBuild = fs : : exists ( absDir / " index.html " ) ;
auto have = [ ] ( std : : string_view exe ) {
# ifdef _WIN32
std : : string probe = std : : format ( " where {} > NUL 2>&1 " , exe ) ;
# else
std : : string probe = std : : format ( " command -v {} > /dev/null 2>&1 " , exe ) ;
# endif
return std : : system ( probe . c_str ( ) ) = = 0 ;
} ;
if ( browserBuild ) {
2026-05-19 00:50:06 +02:00
// Probe-bind to find a free port starting at 8080 — if the
// user has another dev server running we shift up rather
// than letting caddy/python exit with EADDRINUSE.
2026-07-23 01:24:42 +02:00
auto findFreePort = [ ] ( std : : int32_t basePort , std : : int32_t span ) - > std : : int32_t {
2026-05-19 00:50:06 +02:00
# if defined(CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_pc_windows_msvc) || defined(CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_w64_mingw32)
2026-07-23 01:24:42 +02:00
static const bool WsaInit = [ ] { WSADATA d ; return WSAStartup ( MAKEWORD ( 2 , 2 ) , & d ) = = 0 ; } ( ) ;
if ( ! WsaInit ) return basePort ;
using Sock = SOCKET ;
const Sock invalid = INVALID_SOCKET ;
auto closesock = [ ] ( Sock s ) { closesocket ( s ) ; } ;
2026-05-19 00:50:06 +02:00
# else
2026-07-23 01:24:42 +02:00
using Sock = std : : int32_t ;
const Sock invalid = - 1 ;
auto closesock = [ ] ( Sock s ) { : : close ( s ) ; } ;
2026-05-19 00:50:06 +02:00
# endif
2026-07-23 01:24:42 +02:00
for ( std : : int32_t p = basePort ; p < basePort + span ; + + p ) {
Sock s = : : socket ( AF_INET , SOCK_STREAM , 0 ) ;
2026-05-19 00:50:06 +02:00
if ( s = = invalid ) return basePort ;
sockaddr_in addr { } ;
addr . sin_family = AF_INET ;
addr . sin_addr . s_addr = htonl ( INADDR_ANY ) ;
2026-05-27 03:15:19 +00:00
addr . sin_port = htons ( static_cast < std : : uint16_t > ( p ) ) ;
2026-05-19 00:50:06 +02:00
bool ok = : : bind ( s , reinterpret_cast < sockaddr * > ( & addr ) , sizeof ( addr ) ) = = 0 ;
closesock ( s ) ;
if ( ok ) return p ;
}
return basePort ;
} ;
2026-07-23 01:24:42 +02:00
const std : : int32_t port = findFreePort ( 8080 , 16 ) ;
2026-05-26 22:50:08 +02:00
// Cross-origin isolation: the browser coarsens
// performance.now() (and chrono::steady_clock under wasi)
// to ~0.1ms unless the response carries COOP/COEP, which
// floors the --timing overlay's sub-ms phase counters to
// zero. Emitting same-origin / require-corp drops the
// resolution to ~5µs and also unlocks SharedArrayBuffer
// for any future threading work. CORP keeps the local
// asset fetches passing under require-corp.
auto writeFile = [ ] ( const fs : : path & p , std : : string_view contents ) {
std : : ofstream f ( p , std : : ios : : binary | std : : ios : : trunc ) ;
f . write ( contents . data ( ) , static_cast < std : : streamsize > ( contents . size ( ) ) ) ;
} ;
2026-05-18 05:23:11 +02:00
std : : string cmd ;
std : : string_view picked ;
2026-05-26 22:50:08 +02:00
bool isolated = false ;
2026-05-18 05:23:11 +02:00
if ( have ( " caddy " ) ) {
picked = " caddy " ;
2026-05-26 22:50:08 +02:00
isolated = true ;
// caddy file-server has no --header flag; write a
// Caddyfile next to the build output. Adapter is
// inferred from the .caddyfile extension when run
// via `caddy run`.
fs : : path cf = absDir / " Caddyfile.coi " ;
writeFile ( cf , std : : format (
" :{} {{ \n "
" root * {} \n "
" header Cross-Origin-Opener-Policy \" same-origin \" \n "
" header Cross-Origin-Embedder-Policy \" require-corp \" \n "
" header Cross-Origin-Resource-Policy \" same-origin \" \n "
" header Cache-Control \" no-store \" \n "
2026-07-30 23:01:10 +02:00
// Every EnableWasiBrowserRuntime consumer is a
// single-page wasm app by construction: the
// generated index.html ships an empty <body> and
// the module builds the DOM at runtime. So an app
// that routes on window.location has no file on
// disk for any path but "/", and a bare
// file_server 404s every deep link, refresh and
// shared URL during development.
//
// try_files falls through to index.html only for
// paths that are not real files, so static assets
// still serve normally and a genuinely missing
// asset becomes a visible wrong-content-type
// rather than a silent 404 the app can't see.
" try_files {{path}} /index.html \n "
2026-05-26 22:50:08 +02:00
" file_server \n "
" }} \n " ,
port , absDir . string ( ) ) ) ;
2026-07-23 01:24:42 +02:00
cmd = std : : format ( " caddy run --config {} --adapter caddyfile " , cf . string ( ) ) ;
2026-05-26 22:50:08 +02:00
} else if ( have ( " python3 " ) | | have ( " python " ) ) {
std : : string_view py = have ( " python3 " ) ? " python3 " : " python " ;
picked = py ;
isolated = true ;
// Inline a tiny SimpleHTTPRequestHandler subclass
// that appends the COI headers on every response.
// Lives in absDir so the user can re-run it manually
// (`python3 absDir/.serve-coi.py 8080`).
fs : : path sp = absDir / " .serve-coi.py " ;
writeFile ( sp ,
" import http.server, socketserver, sys, os \n "
" class H(http.server.SimpleHTTPRequestHandler): \n "
" def end_headers(self): \n "
" self.send_header('Cross-Origin-Opener-Policy', 'same-origin') \n "
" self.send_header('Cross-Origin-Embedder-Policy', 'require-corp') \n "
" self.send_header('Cross-Origin-Resource-Policy', 'same-origin') \n "
" self.send_header('Cache-Control', 'no-store') \n "
" super().end_headers() \n "
" socketserver.TCPServer.allow_reuse_address = True \n "
" port = int(sys.argv[1]) if len(sys.argv) > 1 else 8080 \n "
" os.chdir(os.path.dirname(os.path.abspath(__file__))) \n "
" with socketserver.TCPServer(('', port), H) as s: \n "
" s.serve_forever() \n " ) ;
cmd = std : : format ( " {} {} {} " , py , sp . string ( ) , port ) ;
2026-05-18 05:23:11 +02:00
} else if ( have ( " php " ) ) {
picked = " php " ;
2026-05-26 22:50:08 +02:00
// php -S supports a router script — emit one that
// sets COI headers then delegates to the built-in
// static-file handler by returning false.
fs : : path rp = absDir / " .serve-coi.php " ;
writeFile ( rp ,
" <?php \n "
" header('Cross-Origin-Opener-Policy: same-origin'); \n "
" header('Cross-Origin-Embedder-Policy: require-corp'); \n "
" header('Cross-Origin-Resource-Policy: same-origin'); \n "
" header('Cache-Control: no-store'); \n "
" return false; \n " ) ;
isolated = true ;
2026-07-23 01:24:42 +02:00
cmd = std : : format ( " php -S 0.0.0.0:{} -t {} {} " , port , absDir . string ( ) , rp . string ( ) ) ;
2026-05-18 05:23:11 +02:00
} else if ( have ( " ruby " ) ) {
picked = " ruby " ;
cmd = std : : format ( " ruby -run -e httpd {} -p{} " , absDir . string ( ) , port ) ;
} else if ( have ( " busybox " ) ) {
picked = " busybox httpd " ;
cmd = std : : format ( " busybox httpd -f -p {} -h {} " , port , absDir . string ( ) ) ;
} else if ( have ( " npx " ) ) {
picked = " npx http-server " ;
cmd = std : : format ( " npx --yes http-server {} -p {} --silent " , absDir . string ( ) , port ) ;
} else {
2026-07-23 01:24:42 +02:00
std : : println ( std : : cerr , " -r wasm: no HTTP server found in PATH. Install one of: " " caddy, python3, python, php, ruby, busybox, npx (Node.js). " ) ;
2026-05-18 05:23:11 +02:00
return 1 ;
}
2026-05-31 17:23:34 +02:00
std : : println ( " listening on port :{} " , port ) ;
if ( ! isolated ) {
2026-07-23 01:24:42 +02:00
std : : println ( std : : cerr , " warning: {} does not emit COOP/COEP — performance.now() will be coarse " " (~0.1ms). Install caddy, python3, or php for cross-origin isolation. " , picked ) ;
2026-05-26 22:50:08 +02:00
}
2026-05-31 17:23:34 +02:00
// Silence the backend's own banner/request logs so the
// experience is identical regardless of which server is
// picked — the line above is the only output.
# ifdef _WIN32
cmd + = " > NUL 2>&1 " ;
# else
cmd + = " > /dev/null 2>&1 " ;
# endif
2026-05-18 05:23:11 +02:00
return std : : system ( cmd . c_str ( ) ) = = 0 ? 0 : 1 ;
}
// wasi-cli wasm — needs a standalone runtime.
if ( have ( " wasmtime " ) ) {
return std : : system ( std : : format ( " wasmtime {} " , artifact . string ( ) ) . c_str ( ) ) = = 0 ? 0 : 1 ;
}
if ( have ( " wasmer " ) ) {
return std : : system ( std : : format ( " wasmer run {} " , artifact . string ( ) ) . c_str ( ) ) = = 0 ? 0 : 1 ;
}
2026-07-23 01:24:42 +02:00
std : : println ( std : : cerr , " -r wasm: no wasm runtime found in PATH. Install wasmtime or wasmer, " " or call EnableWasiBrowserRuntime(cfg) in project.cpp for a browser build. " ) ;
2026-05-18 05:23:11 +02:00
return 1 ;
}
Cross-compiled mingw artifact: full DLL+launcher pattern + MSVC target
Linux→mingw cross-compile now produces the same architectural shape as
build.cmd (DLL + import lib + launcher exe) instead of a single static
binary. The CI Windows artifact becomes a first-class drop-in: a user
on Windows can run crafter-build.exe against any project.cpp and have
it produce real Windows binaries — for either mingw or MSVC ABI.
What changed:
project.cpp: when target=mingw or target=msvc, crafter.build-lib is
built as LibraryDynamic instead of LibraryStatic so the link emits a
DLL + import lib (matching what build.cmd produces natively).
Crafter.Build-Clang.cpp Build():
- LibraryDynamic now branches per target — mingw emits <name>.dll +
lib<name>.dll.a via lld --out-implib; msvc emits <name>.dll +
<name>.lib via /IMPLIB; unix unchanged.
- expectedOutputFor returns .dll for Windows-target dynamic libs.
- Executable on Windows host now branches per target: mingw target
uses simple link (no -lc++/-nostdlib++/LIBCXX_DIR), msvc target keeps
the existing path. Both auto-copy LibraryDynamic dep DLLs + import
libs alongside the launcher exe (Windows resolves DLLs from the exe's
own directory at load time).
- Mingw-target Executables get -D CRAFTER_BUILD_DLL_IMPORT so
CRAFTER_API resolves to dllimport in their PCMs.
- mingw link adds -static-libstdc++ -static-libgcc -Wl,-Bstatic
-lpthread so produced .exe/.dll don't depend on a particular
libstdc++-6.dll / libwinpthread-1.dll being on the consumer's PATH
(avoids the Arch UCRT vs msys2 UCRT vs msys2 MSVCRT ABI rabbit hole).
Drops the old auto-copy of /usr/x86_64-w64-mingw32/bin/*.dll which
is now dead weight.
- -r flag resolves to an absolute path before std::system, otherwise
cmd.exe rejects "./bin/..." with "'.' is not recognized...".
Crafter.Build-Platform.cpp:
- Split the Windows-host block into shared shell helpers (#if MSVC ||
MINGW) plus separate #if MSVC and #if MINGW blocks for LoadProject /
EnsureCrafterBuildPcms / GetBaseCommand / BuildStdPcm.
- Mingw-host LoadProject compiles project.cpp with --target=mingw,
--sysroot=C:\msys64\ucrt64 (default; override with CRAFTER_MINGW_DIR),
-femulated-tls, -Wl,--export-all-symbols (mingw-lld doesn't accept
/EXPORT:NAME), and links against libcrafter-build.dll.a from the
launcher's directory.
- Mingw-host GetBaseCommand and BuildStdPcm dispatch on config.target
so a mingw-host crafter-build can also build msvc-target outputs
(uses LIBCXX_DIR + libc++ headers, same as native build.cmd) when
the user sets cfg.target = "x86_64-pc-windows-msvc".
README adds a Quick start (Windows) section covering both build paths
(native MSVC via build.cmd and the cross-compiled mingw artifact),
documenting the msys2 UCRT toolchain prerequisite.
Verified end-to-end on the winvm:
- mingw target: cross-compiled crafter-build.exe builds hello-world's
project.cpp, compiles main.cpp, links a hello.exe that runs without
any custom PATH (only Windows system DLLs needed).
- msvc target: same crafter-build.exe builds an MSVC-ABI hello.exe
linked against c++.dll (auto-copied from LIBCXX_DIR), runs cleanly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 02:23:42 +02:00
// Resolve to absolute — cmd.exe on Windows mishandles a leading
// "./" by trying to interpret it as a command. system() invokes
// through cmd /c, so the relative-prefixed path makes cmd error
// with "'.' is not recognized as an internal or external command".
2026-05-01 19:02:14 +02:00
// Run from the artifact's own directory so relative file opens
// (shaders, assets copied alongside the exe via cfg.files) resolve
// against the bin dir rather than the user's cwd. We exit the
// process immediately after, so no cwd restore needed.
fs : : current_path ( dir ) ;
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
return std : : system ( artifact . string ( ) . c_str ( ) ) = = 0 ? 0 : 1 ;
}
2026-04-27 07:04:42 +02:00
return 0 ;
} catch ( const std : : exception & e ) {
2026-04-29 03:27:11 +02:00
Progress : : Clear ( ) ;
2026-04-27 07:04:42 +02:00
std : : println ( std : : cerr , " {} " , e . what ( ) ) ;
return 1 ;
}
}