2026-04-23 01:57:25 +02:00
/*
Crafter ® Build
Copyright ( C ) 2026 Catcrafts ®
Catcrafts . net
This library is free software ; you can redistribute it and / or
modify it under the terms of the GNU Lesser General Public
License version 3.0 as published by the Free Software Foundation ;
This library is distributed in the hope that it will be useful ,
but WITHOUT ANY WARRANTY ; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE . See the GNU
Lesser General Public License for more details .
You should have received a copy of the GNU Lesser General Public
License along with this library ; if not , write to the Free Software
Foundation , Inc . , 51 Franklin Street , Fifth Floor , Boston , MA 02110 - 1301 USA
*/
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-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
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 ) - > bool {
for ( const std : : unique_ptr < Module > & interface : this - > interfaces ) {
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 ) {
fs : : path depPcmPath = ( depCfg - > PcmDir ( ) / depInterface - > path . filename ( ) ) . string ( ) + " .pcm " ;
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 ;
} ;
for ( Configuration * depCfg : this - > dependencies ) {
if ( walk ( depCfg ) ) return true ;
2026-04-27 07:04:42 +02:00
}
return false ;
} ;
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 } ;
}
std : : erase_if ( tempModulePaths , [ this ] ( std : : tuple < fs : : path , std : : string , ModulePartition * , Module * > & file ) {
std : : smatch match ;
if ( std : : regex_search ( std : : get < 1 > ( file ) , match , std : : regex ( R " (export module ([a-zA-Z0-9_ \ . \ -]+);) " ) ) ) {
std : : get < 0 > ( file ) . replace_extension ( " " ) ;
this - > interfaces . push_back ( std : : make_unique < Module > ( std : : move ( match [ 1 ] . str ( ) ) , std : : move ( std : : get < 0 > ( file ) ) ) ) ;
return true ;
} else {
return false ;
}
} ) ;
for ( std : : uint16_t i = 0 ; i < tempModulePaths . size ( ) ; i + + ) {
std : : smatch match ;
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 ;
}
}
throw std : : runtime_error ( std : : format ( " Module {} not found, referenced in {} " , match [ 1 ] . str ( ) , std : : get < 0 > ( tempModulePaths [ i ] ) . string ( ) ) ) ;
} else {
throw std : : runtime_error ( std : : format ( " No module declaration found in {} " , std : : get < 0 > ( tempModulePaths [ i ] ) . string ( ) ) ) ;
}
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 ) ;
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 ;
resolveImport ( match [ 1 ] . str ( ) , partition - > moduleDependencies , partition - > externalModuleDependencies ) ;
+ + 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 ] ) {
resolveImport ( match2 [ 1 ] . str ( ) , implementation . moduleDependencies , implementation . externalModuleDependencies ) ;
}
+ + 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 ;
resolveImport ( match2 [ 1 ] . str ( ) , implementation . moduleDependencies , implementation . externalModuleDependencies ) ;
+ + currentMatch ;
}
}
}
}
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
// (incremental-rebuild test scenarios). Walks cfg.dependencies recursively
// with a seen-set so diamond deps don't loop.
{
std : : unordered_set < Configuration * > resetSeen ;
std : : function < void ( Configuration * ) > reset = [ & ] ( Configuration * c ) {
if ( ! resetSeen . insert ( c ) . second ) return ;
for ( auto & iface : c - > interfaces ) {
iface - > checked = false ;
iface - > compiled . store ( false ) ;
for ( auto & part : iface - > partitions ) {
part - > checked = false ;
part - > compiled . store ( false ) ;
}
}
for ( Configuration * dep : c - > dependencies ) {
reset ( dep ) ;
}
} ;
reset ( & config ) ;
}
// 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-04-27 07:04:42 +02:00
BuildResult buildResult ;
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 ( ) ;
if ( ext = = " .png " ) return fs : : path ( src . filename ( ) ) . replace_extension ( " .ctex " ) ;
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 ) ;
buildError = std : : format ( " {}: unsupported asset extension (expected .png or .obj, or a directory) " , asset . string ( ) ) ;
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 ;
}
}
} ) ;
}
fs : : path stdPcmDir = GetCacheDir ( ) / ( config . target + " - " + config . march ) ;
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
}
fs : : path pcmDir ;
if ( config . type ! = ConfigurationType : : Executable ) {
pcmDir = outputDir ;
} else {
pcmDir = buildDir ;
}
2026-04-27 07:04:42 +02:00
fs : : copy_file ( stdPcmDir / " std.pcm " , pcmDir / " std.pcm " , fs : : copy_options : : update_existing ) ;
2026-04-23 01:57:25 +02:00
2026-04-27 07:04:42 +02:00
std : : string editedTarget = config . target ;
std : : replace ( editedTarget . begin ( ) , editedTarget . end ( ) , ' - ' , ' _ ' ) ;
V2: WASI, -r flag, CI pipeline, examples & tests cleanup
WASI / wasm32 target support
- Auto-detect /usr/share/wasi-sysroot on Linux when target starts_with("wasm32")
- Skip -march/-mtune for wasm (clang rejects them)
- Apply -fno-exceptions -fno-c++-static-destructors -mllvm -wasm-enable-sjlj
-D_WASI_EMULATED_SIGNAL to wasm builds (compile + std PCM, kept in sync)
- .wasm output extension in expectedOutputFor and link command
- EnableWasiBrowserRuntime(cfg): opt-in helper that drops index.html +
runtime.js next to the .wasm; runtime.js reads window.CRAFTER_WASM_URL
set in the templated index.html so a single shim handles any output name
-r run flag in the CLI: build then exec the artifact (host targets only;
rejects libraries; auto .exe/.wasm extension handling)
CI pipeline (.forgejo/workflows/ci.yaml)
- Triggers: PR/push to master + manual dispatch
- Single arch-latest container job: install deps, bootstrap, self-rebuild,
run tests, cross-compile mingw, package both archives, upload artifacts
- Rolling 'latest' release published only on push/dispatch to master
mingw cross-compile from Linux now works end-to-end:
- ExternalDependency cache key includes target so per-target glslang builds
don't collide; CMAKE_BUILD_TYPE=Release pinned (otherwise glslang appends
'd' to lib names and breaks linking); cross-compile cmake flags
(CMAKE_SYSTEM_NAME=Windows, CMAKE_*_COMPILER_TARGET=...)
- project.cpp accepts --target=<triple>; Linux-only -Wl,--export-dynamic
and -ldl are gated; mingw glslang skips the standalone exe (its libgcc_eh
link pulls pthread which mingw doesn't link by default)
- mingw compile uses -femulated-tls so std::__once_callable etc reference
the same emutls symbols libstdc++ provides
- mingw link auto-adds -lstdc++exp -lpthread
GetCrafterBuildHome() exposed from the Platform module; LoadProject (Linux
+ Windows) now both use it instead of duplicating the resolution.
Examples reorg: hello-world, library, with-module, wasi, tests — each with
its own README. Tests reorg: per-test directory with inner/ fixture, no
shared tests/fixtures/ tree. New Wasi test verifies .wasm magic bytes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 23:24:46 +02:00
// wasm32 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 ) ;
std : : string 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 , stdPcmDir . string ( ) , pcmDir . string ( ) ) ;
2026-04-27 07:04:42 +02:00
test runner, cross-target runners, lib/exe split
- subprocess-isolated test runner (replaces V1 dlopen-RunTest);
Pass/Fail/Crash/Timeout/Skipped outcomes via :Test partition
- TestRunner abstraction with command templates: Local, Ssh,
SshWin (cmd.exe-shell), QemuUser, FromEnv; probe-based skip
when runner unreachable
- transitive PCM-path propagation in Build(); resolveImport
walks deps recursively; depResults cache keyed by PcmDir()
so per-target builds don't collide
- cfg.sysroot threaded through BuildStdPcm + base compile/link
command (enables aarch64 cross via Arch Linux ARM rootfs)
- lib + exe split: project.cpp defines crafterBuildLib
(LibraryStatic) + crafterBuildExe (Executable depending on
it); build.sh produces lib/libcrafter-build.a alongside
bin/crafter-build for downstream static-link consumers
- Windows DLL+launcher: CRAFTER_API macro, /EXPORT flag for
project.dll's CrafterBuildProject; Crafter::Run as the real
entry point with main.cpp as a thin wrapper
- 18 tests: HelloWorld/WithModule/Defines/CrossProjectModule/
Diamond × (Linux + sshwin:winvm), plus Incremental,
BuildError, Libraries, RunnerClassification, QemuUser,
SshRunner, WindowsViaSsh, CrossArchAarch64
- single ./bin/crafter-build test runs everything; Windows
variants skip gracefully if winvm SSH alias unreachable
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-27 22:32:19 +02:00
if ( ! config . sysroot . empty ( ) ) {
command + = std : : format ( " --sysroot={} " , config . sysroot ) ;
}
V2: WASI, -r flag, CI pipeline, examples & tests cleanup
WASI / wasm32 target support
- Auto-detect /usr/share/wasi-sysroot on Linux when target starts_with("wasm32")
- Skip -march/-mtune for wasm (clang rejects them)
- Apply -fno-exceptions -fno-c++-static-destructors -mllvm -wasm-enable-sjlj
-D_WASI_EMULATED_SIGNAL to wasm builds (compile + std PCM, kept in sync)
- .wasm output extension in expectedOutputFor and link command
- EnableWasiBrowserRuntime(cfg): opt-in helper that drops index.html +
runtime.js next to the .wasm; runtime.js reads window.CRAFTER_WASM_URL
set in the templated index.html so a single shim handles any output name
-r run flag in the CLI: build then exec the artifact (host targets only;
rejects libraries; auto .exe/.wasm extension handling)
CI pipeline (.forgejo/workflows/ci.yaml)
- Triggers: PR/push to master + manual dispatch
- Single arch-latest container job: install deps, bootstrap, self-rebuild,
run tests, cross-compile mingw, package both archives, upload artifacts
- Rolling 'latest' release published only on push/dispatch to master
mingw cross-compile from Linux now works end-to-end:
- ExternalDependency cache key includes target so per-target glslang builds
don't collide; CMAKE_BUILD_TYPE=Release pinned (otherwise glslang appends
'd' to lib names and breaks linking); cross-compile cmake flags
(CMAKE_SYSTEM_NAME=Windows, CMAKE_*_COMPILER_TARGET=...)
- project.cpp accepts --target=<triple>; Linux-only -Wl,--export-dynamic
and -ldl are gated; mingw glslang skips the standalone exe (its libgcc_eh
link pulls pthread which mingw doesn't link by default)
- mingw compile uses -femulated-tls so std::__once_callable etc reference
the same emutls symbols libstdc++ provides
- mingw link auto-adds -lstdc++exp -lpthread
GetCrafterBuildHome() exposed from the Platform module; LoadProject (Linux
+ Windows) now both use it instead of duplicating the resolution.
Examples reorg: hello-world, library, with-module, wasi, tests — each with
its own README. Tests reorg: per-test directory with inner/ fixture, no
shared tests/fixtures/ tree. New Wasi test verifies .wasm magic bytes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 23:24:46 +02:00
if ( config . target . starts_with ( " wasm32 " ) ) {
// -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.
command + = " -fno-exceptions -fno-c++-static-destructors -mllvm -wasm-enable-sjlj -D_WASI_EMULATED_SIGNAL -Wno-unused-command-line-argument " ;
}
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.
command + = " -femulated-tls -Wno-unused-command-line-argument " ;
}
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
if ( config . type = = ConfigurationType : : LibraryDynamic ) {
2026-04-23 01:57:25 +02:00
# ifdef CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_pc_linux_gnu
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)
command + = " -D CRAFTER_BUILD_CONFIGURATION_TYPE_SHARED_LIBRARY " ;
# endif
2026-04-27 07:04:42 +02:00
} else if ( config . type = = ConfigurationType : : Executable ) {
2026-04-23 01:57:25 +02:00
command + = " -D CRAFTER_BUILD_CONFIGURATION_TYPE_EXECUTABLE " ;
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
// 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 " ) {
command + = " -D CRAFTER_BUILD_DLL_IMPORT " ;
}
2026-04-27 07:04:42 +02:00
} else {
command + = " -D CRAFTER_BUILD_CONFIGURATION_TYPE_LIBRARY " ;
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 ) ;
2026-05-12 01:16:40 +02:00
// -I propagation that's valid for both C and C++ compiles. Module-only
// bits (-fprebuilt-module-path) stay on `command` only.
std : : string includeFlags ;
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 < 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 " ) {
2026-05-12 01:16:40 +02:00
includeFlags + = " -I " + entry . path ( ) . string ( ) ;
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-23 01:57:25 +02:00
}
2026-05-12 01:16:40 +02:00
includeFlags + = std : : format ( " -I{} " , dep - > path . string ( ) ) ;
command + = std : : format ( " -fprebuilt-module-path={} " , dep - > PcmDir ( ) . string ( ) ) ;
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 : dep - > dependencies ) {
addFlags ( sub ) ;
}
} ;
for ( Configuration * dep : config . dependencies ) {
addFlags ( dep ) ;
2026-04-23 01:57:25 +02:00
}
test runner, cross-target runners, lib/exe split
- subprocess-isolated test runner (replaces V1 dlopen-RunTest);
Pass/Fail/Crash/Timeout/Skipped outcomes via :Test partition
- TestRunner abstraction with command templates: Local, Ssh,
SshWin (cmd.exe-shell), QemuUser, FromEnv; probe-based skip
when runner unreachable
- transitive PCM-path propagation in Build(); resolveImport
walks deps recursively; depResults cache keyed by PcmDir()
so per-target builds don't collide
- cfg.sysroot threaded through BuildStdPcm + base compile/link
command (enables aarch64 cross via Arch Linux ARM rootfs)
- lib + exe split: project.cpp defines crafterBuildLib
(LibraryStatic) + crafterBuildExe (Executable depending on
it); build.sh produces lib/libcrafter-build.a alongside
bin/crafter-build for downstream static-link consumers
- Windows DLL+launcher: CRAFTER_API macro, /EXPORT flag for
project.dll's CrafterBuildProject; Crafter::Run as the real
entry point with main.cpp as a thin wrapper
- 18 tests: HelloWorld/WithModule/Defines/CrossProjectModule/
Diamond × (Linux + sshwin:winvm), plus Incremental,
BuildError, Libraries, RunnerClassification, QemuUser,
SshRunner, WindowsViaSsh, CrossArchAarch64
- single ./bin/crafter-build test runs everything; Windows
variants skip gracefully if winvm SSH alias unreachable
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-27 22:32:19 +02:00
}
2026-05-12 01:16:40 +02:00
command + = includeFlags ;
2026-04-23 01:57:25 +02:00
test runner, cross-target runners, lib/exe split
- subprocess-isolated test runner (replaces V1 dlopen-RunTest);
Pass/Fail/Crash/Timeout/Skipped outcomes via :Test partition
- TestRunner abstraction with command templates: Local, Ssh,
SshWin (cmd.exe-shell), QemuUser, FromEnv; probe-based skip
when runner unreachable
- transitive PCM-path propagation in Build(); resolveImport
walks deps recursively; depResults cache keyed by PcmDir()
so per-target builds don't collide
- cfg.sysroot threaded through BuildStdPcm + base compile/link
command (enables aarch64 cross via Arch Linux ARM rootfs)
- lib + exe split: project.cpp defines crafterBuildLib
(LibraryStatic) + crafterBuildExe (Executable depending on
it); build.sh produces lib/libcrafter-build.a alongside
bin/crafter-build for downstream static-link consumers
- Windows DLL+launcher: CRAFTER_API macro, /EXPORT flag for
project.dll's CrafterBuildProject; Crafter::Run as the real
entry point with main.cpp as a thin wrapper
- 18 tests: HelloWorld/WithModule/Defines/CrossProjectModule/
Diamond × (Linux + sshwin:winvm), plus Incremental,
BuildError, Libraries, RunnerClassification, QemuUser,
SshRunner, WindowsViaSsh, CrossArchAarch64
- single ./bin/crafter-build test runs everything; Windows
variants skip gracefully if winvm SSH alias unreachable
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-27 22:32:19 +02:00
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-23 01:57:25 +02:00
} else {
2026-04-27 07:04:42 +02:00
resultFuture = it - > second ;
}
depMutex . unlock ( ) ;
if ( isBuilder ) {
BuildResult built ;
try {
built = Build ( * dep , depResults , depMutex ) ;
} catch ( . . . ) {
promise - > set_exception ( std : : current_exception ( ) ) ;
throw ;
}
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 ;
}
}
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
}
2026-05-12 01:16:40 +02:00
// Defines belong on both C and C++ compiles so vendored C dependencies
// can see configuration-level macros consistently with module sources.
std : : string defineFlags ;
2026-04-23 01:57:25 +02:00
for ( const Define & define : config . defines ) {
if ( define . value . empty ( ) ) {
2026-05-12 01:16:40 +02:00
defineFlags + = std : : format ( " -D {} " , define . name ) ;
2026-04-23 01:57:25 +02:00
} else {
2026-05-12 01:16:40 +02:00
defineFlags + = std : : format ( " -D {}={} " , define . name , define . value ) ;
2026-04-23 01:57:25 +02:00
}
}
2026-05-12 01:16:40 +02:00
command + = defineFlags ;
2026-04-23 01:57:25 +02:00
2026-05-12 01:16:40 +02:00
// Track caller-provided compileFlags separately so the .c compile can
// pick them up too (vendored C deps usually need -I from this set).
std : : string userFlags ;
2026-04-27 07:04:42 +02:00
for ( const std : : string & flag : config . compileFlags ) {
2026-05-12 01:16:40 +02:00
userFlags + = " " + flag ;
2026-04-27 07:04:42 +02:00
}
2026-05-12 01:16:40 +02:00
command + = userFlags ;
2026-04-27 07:04:42 +02:00
2026-04-23 01:57:25 +02:00
std : : string cmakeBuildType ;
if ( config . debug ) {
cmakeBuildType = " Debug " ;
command + = " -g -D CRAFTER_BUILD_CONFIGURATION_DEBUG " ;
} else {
cmakeBuildType = " Release " ;
command + = " -O3 " ;
}
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 ( ) ) ;
const std : : string objPath = ( buildDir / cFile . filename ( ) ) . string ( ) + " _source.o " ;
const std : : string srcPath = cFile . string ( ) + " .c " ;
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-05-12 01:16:40 +02:00
threads . emplace_back ( [ & cFile , & buildDir , & buildError , & buildCancelled , & config , & includeFlags , & defineFlags , & userFlags ] ( ) {
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-05-12 01:16:40 +02:00
std : : string result = RunCommand ( std : : format ( " clang {}.c --target={} -march={} -mtune={} -O3 -c{}{}{} -o {}_source.o " , cFile . string ( ) , config . target , config . march , config . mtune , 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 ( ) ) ;
const std : : string objPath = ( buildDir / cFile . filename ( ) ) . string ( ) + " _source.o " ;
const std : : string srcPath = cFile . string ( ) + " .cu " ;
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 ( ) ;
fs : : path srcName = asset . filename ( ) ;
if ( ext = = " .png " ) srcName . replace_extension ( " .ctex " ) ;
else if ( ext = = " .obj " ) srcName . replace_extension ( " .cmesh " ) ;
else continue ;
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 ;
}
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.
for ( const std : : string & flag : publicFlagSet ) command + = " " + flag ;
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 ) {
command + = " " + 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-27 07:04:42 +02:00
}
for ( const std : : string & flag : ext . linkFlags ) {
buildResult . libs . insert ( flag ) ;
}
if ( ext . latestArtifact > externalFloor ) externalFloor = ext . latestArtifact ;
2026-04-23 01:57:25 +02:00
}
2026-04-27 07:04:42 +02:00
for ( std : : unique_ptr < Module > & interface : config . interfaces ) {
if ( interface - > Check ( pcmDir , externalFloor ) ) {
Module * mod = interface . get ( ) ;
threads . emplace_back ( [ mod , & command , & pcmDir , & buildDir , & buildCancelled , & buildError ] ( ) {
2026-04-29 03:27:11 +02:00
Progress : : Task task ( std : : format ( " Compiling interface {} " , mod - > path . filename ( ) . string ( ) ) ) ;
2026-04-27 07:04:42 +02:00
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-23 01:57:25 +02:00
}
2026-04-27 07:04:42 +02:00
} ) ;
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 ( ) ) ;
}
}
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 ) {
linkExtras + = " " + flag ;
}
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 ) {
if ( c . target . starts_with ( " wasm32 " ) )
return dir / ( c . outputName + " .wasm " ) ;
return c . target = = " x86_64-w64-mingw32 "
? dir / ( c . outputName + " .exe " )
: 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-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).
for ( auto fname : { std : : format ( " {}.dll " , dep - > outputName ) ,
std : : format ( " lib{}.dll.a " , dep - > outputName ) } ) {
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 ( ) ;
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 ( auto fname : { std : : format ( " {}.dll " , dep - > outputName ) ,
std : : format ( " lib{}.dll.a " , dep - > outputName ) } ) {
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 ) ;
buildResult . result = RunCommand ( std : : format (
" {}{} -o {} -fuse-ld=lld{} " ,
command , files , ( outputDir / config . outputName ) . string ( ) , linkExtras ) ) ;
} 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-04-27 07:04:42 +02:00
buildResult . result = RunCommand ( std : : format ( " ar rcs {}.a {} " , ( 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 ) ;
buildResult . result = RunCommand ( std : : format (
" {}{} -shared -o {} -Wl,--out-implib,{} -fuse-ld=lld{} " ,
command , files , dll . string ( ) , implib . string ( ) , linkExtras ) ) ;
} 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 ) ;
buildResult . result = RunCommand ( std : : format (
" {}{} -shared -o {} -Wl,/IMPLIB:{} -fuse-ld=lld{} " ,
command , files , dll . string ( ) , implib . string ( ) , linkExtras ) ) ;
} else {
buildResult . result = RunCommand ( std : : format (
" {}{} -shared -o {}.so -Wl,-rpath,'$ORIGIN' -fuse-ld=lld{} " ,
command , files , ( outputDir / ( std : : string ( " lib " ) + config . outputName ) ) . string ( ) , linkExtras ) ) ;
}
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 ) ) ;
}
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 ) ) {
throw std : : runtime_error ( std : : format (
" wasi-runtime assets missing under {} (set CRAFTER_BUILD_HOME or reinstall) " ,
runtimeDir . string ( ) ) ) ;
}
fs : : path htmlOutDir = cfg . path / " build " / " wasi-runtime " / cfg . name ;
fs : : create_directories ( htmlOutDir ) ;
fs : : path htmlPath = htmlOutDir / " index.html " ;
std : : ifstream in ( htmlTemplate ) ;
std : : stringstream buf ;
buf < < in . rdbuf ( ) ;
std : : string html = std : : regex_replace ( buf . str ( ) , std : : regex ( R " ( \ { \ {WASM \ } \ }) " ) , cfg . outputName + " .wasm " ) ;
std : : ofstream out ( htmlPath ) ;
out < < html ;
out . close ( ) ;
cfg . files . push_back ( runtimeJs ) ;
cfg . files . push_back ( htmlPath ) ;
}
2026-04-29 18:59:01 +02:00
std : : string Crafter : : HostTarget ( ) {
static const std : : string cached = [ ] ( ) - > std : : string {
CommandResult r = RunCommandChecked ( " clang++ -print-target-triple " ) ;
if ( r . exitCode ! = 0 ) return { } ;
std : : string out = std : : move ( r . output ) ;
while ( ! out . empty ( ) & & ( out . back ( ) = = ' \n ' | | out . back ( ) = = ' \r ' ) ) out . pop_back ( ) ;
return out ;
} ( ) ;
return cached ;
}
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-04-30 02:20:19 +02:00
bool sawLib = false , 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-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 ;
return ArgQuery { args } ;
2026-04-29 18:59:01 +02:00
}
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
{ 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 )
ssh : < host > [ : < remoteDir > ]
sshwin : < host > [ : < remoteDir > ]
wsl [ : < remoteDir > ]
- - target = < triple > Filter to tests whose cfg . target matches ; this is
also forwarded to project - args so per - target tests
build for that triple . Default : host triple .
< glob > One or more name globs to filter tests ( e . g . ' Unit * ' ) .
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
feature toggles ) live there .
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 :
0 success / all non - skipped tests passed
1 build failure or one or more tests failed
) " , 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 ;
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-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-04-29 04:00:07 +02:00
if ( arg = = " -h " | | arg = = " --help " | | ( ! runTests & & arg = = " help " ) ) {
PrintHelp ( argv0 ) ;
return 0 ;
} else if ( 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 ;
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-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 ;
}
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
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 " ;
}
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".
artifact = fs : : absolute ( artifact ) ;
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 ;
}
}