Replaces the linter's hand-rolled character scanning with clang's lexer.
StripComments could not see raw string literals, which was documented as a
v1 limitation but was worse than that: an odd number of quotes inside a raw
string desynchronised it for the rest of the file, hiding real code and
leaving the contents of later literals standing as if they were code. There
are 33 raw strings across 6 files here, including the two largest.
The reflow guards had the same class of bug from the other direction. They
were substring probes, so Line(n).contains("//") fired on // inside a string
literal and contains("R\"") fired on the characters R" inside a literal while
missing raw strings opened on an earlier line. Both misfire on this repo:
"MARKER" ends in R". They are now LineHasComment() and
LineHasMultiLineToken(), and two wrapped call sites in tests/Lint that had
been silently unjoinable join as a result.
libclang is dlopen'd rather than linked, so the mingw and MSVC cross-builds
keep linking and the release tarballs stay self-contained. Tokens need no
PCMs, no build flags and no prior build, and they cover preprocessor branches
that are inactive for the host — which is why the layout rules belong on
tokens rather than on an AST that would only ever see one platform's slice.
No rule behaviour was intended to change beyond the two guard fixes; lint
over this repo produced byte-identical output across the CommentStripped
swap.
A primary module interface unit — `export module Widget;`, no partitions —
recorded nothing about what it imported. GetInterfacesAndImplementations
registered the Module and then erased the file from the scan list, so the
import pass only ever saw partitions, and Module had no vectors to hold an
edge anyway.
Two consequences, both reported as issue #26:
Module::Check consulted only its own .cppm and its partitions. A data
member added to an imported module left Widget.pcm, Widget.o and every
consumer object untouched while the imported library rebuilt and both
binaries relinked — one executable holding two class layouts, no
diagnostic, and a crash somewhere unrelated. Wiping build/ was the only
cure, so `crafter-build test` could not be trusted straight after an
interface edit.
Module::Compile waited on nothing. Two modules in one Configuration
compile on concurrent threads, so a primary interface importing a
sibling was a coin flip between working and "module 'Base' not found".
Partitions never had either problem — they carry the same three vectors and
Check/Compile honour them — which is why the gap only surfaced on a module
whose interface is one flat unit.
Module now carries moduleDependencies, externalModuleDependencies and
pendingImports with the same meanings as on ModulePartition; primary units
stay in the scan list so their imports land there; Check sees through them;
Compile orders itself behind a local sibling; and ResolvePendingImports
sweeps them so an edge survives dependencies being wired up afterwards.
Build() now Checks every interface before spawning any compile thread — the
`compiled` flag a waiter blocks on is raised either by a Compile that runs
or by the Check that decides none is needed, so a Check still pending while
another module's thread waits would have hung the build.
Resolves#26
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Three follow-ons to the stale-build report, all cases of an identity not
capturing something that changes the output.
The host PCM cache under <cache>/crafter.build/<target>-<march>/ is shared by
every crafter-build on the machine, and freshness was a per-file mtime
comparison. That cannot tell "this PCM is newer than my source" from "this PCM
was built from different sources that happen to be newer", so a package install
and a working checkout — or two checkouts of different versions — silently
compiled their project.cpp against each other's declarations. Invalidation now
keys on a stamp over the bytes of every module source, which also covers the
case one file's mtime never could: the cached PCMs import each other, so a
change to :Interface invalidates :Clang's PCM with Crafter.Build-Clang.cppm
untouched.
Project args ApplyStandardArgs does not itself interpret are now folded into
VariantId. Such a flag typically decides what gets compiled or bundled — the
report's example is --no-webgpu dropping entries from cfg.files — and without it
both settings shared one bin dir and interleaved their outputs there, leaving a
bundle matching neither. Sorted and deduplicated so flag order doesn't split the
cache, and inherited by test Configurations.
`crafter-build clean` removes the project's bin/ and build/ trees. It
deliberately does not load project.cpp: cleaning is most often reached when
something is already wrong, and a clean that first needs the project to compile
is useless exactly then.
Adding a data member to a class in a module interface did not rebuild every
object compiled against the old layout. The build succeeded with no error or
warning and the resulting binary mixed both layouts, surfacing later as a
SIGSEGV in a destructor.
GetInterfacesAndImplementations scans a TU's `import X;` statements when the
source is declared. An import that matches neither a module in the
Configuration nor one reachable through `dependencies` was dropped on the
floor, leaving that TU with no staleness edge to the interface it consumes.
`dependencies` is frequently assigned *after* the scan — AddTest does exactly
that, resolving tests/<name>/main.cpp and only then returning a builder whose
.Dependencies() supplies the library — so consumers of a dependency's modules
routinely carried no edge at all. A layout change then rebuilt the library,
relinked the consumer, and kept the consumer's object as it was.
Unresolved names are now remembered on the partition/implementation as
pendingImports, and Configuration::ResolvePendingImports retries them against
the dependency DAG as it stands. Build() calls it immediately before comparing
mtimes, which closes the window for every caller rather than only the ones that
declare in the right order; TestBuilder::Dependencies also calls it so the
Configuration is coherent for anyone inspecting it before the build.
Resolves#27
The transforms guard themselves against comments and raw strings before
joining or rewriting a line, because pulling text up past a `//` buries it
and reflowing a multi-line literal changes the string. Those guards were
substring probes over the raw line, so they answered the wrong question:
Line(n).contains("//") fires on // inside a string literal
Line(n).contains("R\"") fires on the characters R" inside a literal, and
MISSES a raw string opened on an earlier line
Both misfire on this repo's own sources. "MARKER" ends in R", and any string
mentioning a lint-disable directive contains //. Two wrapped call sites in
tests/Lint were being left unjoined for exactly these reasons; they join now,
and the results are in this commit.
Replaced by LineHasComment() (added with the token layer) and a new
LineHasMultiLineToken(), which reports whether any token actually covering
that line spans a line boundary — a raw string or a block comment. Backed by
a per-line bitmap derived from the token cache and invalidated with it.
The guards themselves stay: joining across a real comment or a real
multi-line literal is still unsafe, and there are tests for both. What
changes is that they now fire on comments and literals rather than on the
characters that spell them.
format-concat gets narrower as a result. It used to refuse any line
containing R" and ask for a manual fix; now only a literal that genuinely
spans lines does that, because a single-line raw string reduces to R"…" in
the stripped view, fails the plain-literal test, and travels through as an
argument with its spelling intact.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
StripComments hand-scanned five states over raw characters and could not see
a raw string literal, which was documented as a v1 limitation. It was worse
than "does not recognise them": an odd number of quotes inside a raw string
desynchronised the scanner for the rest of the file. Simulated on the new
fixture, the old output was
auto banner = R" "hi)" \n \n ... "MARKER text"
— the code after the raw string blanked away, and the *contents* of a later
string literal left standing as if it were code. Every rule reading
CommentStripped() saw that. There are 33 raw strings across 6 files here,
including the two largest.
The replacement projects the token stream onto a copy of the buffer, blanking
comment tokens whole and the bodies of string/character literals. Editing a
copy in place makes the length-preserving contract structural rather than
something each branch has to remember, and the token extents make raw
strings, escapes, encoding prefixes and '"' all fall out for free. A raw
string reduces to R"…" so rules that bracket a literal by counting quotes
keep working. Digit separators (1'000) are excluded by requiring the text
before the quote to be an encoding prefix.
The public contract is unchanged, so no rule needed editing, and `lint` over
this repo produces byte-identical output to the previous implementation.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adds LintContext::Tokens() and friends, backed by clang_tokenize, as the
substrate the rules will move onto. Nothing consumes it yet.
libclang is dlopen'd rather than linked: -lclang would break the mingw and
MSVC cross-builds at link time and would put a libclang.so.NN runtime
dependency into the otherwise self-contained release tarballs. The clang-c
header is used for its declarations only, and the function-pointer table is
typed with decltype so the signatures cannot drift from the real API.
Three properties this buys that the hand-rolled scanners could not have:
- a raw string literal or block comment is ONE token, so the documented
"raw string literals are not recognized" limitation goes away;
- `//` inside a literal is not a comment, so LineHasComment() replaces the
Line(n).contains("//") probes that false-positive on it;
- tokens cover preprocessor branches that are inactive for the host, since
clang_tokenize lexes rather than evaluates #if. Token rules therefore
keep seeing every platform's code, which an AST could not offer.
The parse backing the tokenizer is expected to fail on module units — no
PCMs, no build flags — and that is fine, because lexing has no semantic
prerequisites. Verified in the new tests.
LintSummary::Clean() now counts `errors`. It previously ignored them, so an
infrastructure failure that produced no findings reported clean and exited
0; a missing libclang would have been exactly that.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Canonical LGPL-3.0-only text, GPL-3.0 companion, SPDX headers on all
first-party sources, MIT for examples/. Vendored lib/ untouched.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The browser wasm pipeline hardcoded -msimd128 for every wasm32 target and
baked a single wasm URL into index.html, so newer codegen features that
aren't yet baseline across engines (relaxed SIMD today; threads, future SIMD
revisions later) couldn't be adopted without dropping the browsers that lack
them.
Add a general, feature-parameterized mechanism owned entirely by
Crafter.Build:
- Configuration::wasmVariants declares N codegen variants (label, extra -m
flags, runtime probes). Build() compiles the baseline plus one
outputName.<label>.wasm per variant, recompiling the whole graph (incl.
dep libs + std PCM) with the variant's flags — relaxed-SIMD is per-TU
codegen, not a link switch. wasmVariantFlags folds into VariantId so each
variant's objects/PCMs land in their own build+bin dir.
- EnableWasiBrowserRuntime emits a variants.json manifest (label -> url +
probes), preferred-first with the baseline as the universal fallback.
- The shipped runtime.js runs inlined wasm-feature-detect probes
(relaxed-simd, simd, tail-call, bulk-memory, exception-handling, threads),
picks the first variant whose probes all pass, and falls back to the single
baked CRAFTER_WASM_URL when no manifest is present (backward compatible).
- EnableWasiRelaxedSimdVariant registers the relaxed-SIMD variant — the
motivating case (Chrome 114+/Firefox 120+ enable it by default; Safari
still flag-gates it as of mid-2026).
Verified end to end: a wasm32-wasip1 build emits both wasi-hello.wasm and
wasi-hello.relaxed-simd.wasm + variants.json; Firefox selects the
relaxed-simd variant and runs it.
Resolves#24
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
cmake --build was invoked with no --parallel, so the default Unix
Makefiles generator compiled external deps (DPP, msquic, glslang, …)
one translation unit at a time, leaving all but one core idle.
Pass an explicit --parallel N using hardware_concurrency() so dep
builds use the available cores. An explicit count (not a bare
--parallel) avoids an unbounded make -j fork bomb on the Makefiles
generator.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
When crafter-build's stdout is not a TTY (redirected to a file or pipe) the
C runtime defaults to full (block) buffering. The progress path is TTY-aware
and the in-place redraw flushes explicitly, but the non-TTY append path
(`[N/M]` lines), Finalize()'s `Built N steps` line and the `-r` server's
`listening on port :N` line all go through block-buffered stdout with no
flush. They accumulate in the buffer and only spill at ~4KB boundaries.
On a normal build this is hidden because the C runtime flushes stdout at
exit. Under `-r` the process never exits — it blocks in its serve loop — so
the trailing buffer is never flushed: a redirected log freezes mid-build (or
sits at 0 bytes) even though the build finished and the server is already
answering. Any tooling that polls the log for `Built …` / `listening …` /
`[N/N]` hangs forever. This is the real cause of the frozen log misdiagnosed
as a build deadlock in #16.
Fix: switch stdout to line buffering at the very top of main(), before any
output, only when stdout is not a terminal. Every `\n` then flushes, so the
markers reach a redirected log immediately. No behaviour change on a TTY.
Kept self-contained in main.cpp using system headers (isatty + setvbuf)
rather than a new Crafter::Progress export: the self-hosting exe build
compiles main.cpp against the installed/cached Crafter.Build module BMIs,
which shadow the freshly built local ones, so a new interface symbol would
not be visible without reinstalling crafter-build first.
Resolves#18
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Build() resets each Module/ModulePartition's per-build `compiled`/`checked`
flags so a reused Configuration re-evaluates mtimes. That reset recursed into
cfg.dependencies — but dependency Configurations are shared across the build
DAG and each is compiled concurrently by its own Build() call.
A parent/sibling's recursive reset could therefore clear a shared dependency's
module `compiled` atomic *after* that dependency's module-compile thread had
set it true and exited, but before an intra-config waiter (its impl, or a
dependent partition) ran compiled.wait(false). The waiter then blocked forever
on a flag nothing would re-signal: the build froze mid-compile, idle, with no
compiler process alive — exactly the hang in issue #16.
Reset only the current configuration's own modules. Every config in the tree
already gets its own Build() call (the per-PcmDir builder registered in
depResults), which resets its own state at the top of that call, sequenced
before its compile threads spawn. Cross-config module state is consulted only
via PCM file mtimes and the depResults futures, never via these flags, so the
narrower reset is correct and removes the data race entirely.
Adds ConcurrentDependencyReset: builds a static-lib dependency fully, then
builds a consumer that depends on it while the dependency is already cached in
depResults (so it is never rebuilt), and asserts the consumer build leaves the
dependency's module `compiled` flag intact. Fails deterministically on the old
recursive reset; passes with the fix.
Resolves#16
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two crafter-build invocations sharing XDG_CACHE_HOME used to clobber each
other's writes to <cache>/<target>-<march>/std.pcm and the
Crafter.Build-*.pcm modules: each LoadProject path wrote directly to the
final path, so a reader could see a half-written file and die with
"malformed or corrupted precompiled file: 'can't skip to bit X from Y'"
(issue #14). Every BuildStdPcm / EnsureCrafterBuildPcms write now goes via
<final>.tmp.<pid>.<seq> and atomic-renames into place; concurrent writers
always see either the old or the new file, never torn bytes. The mingw-on-
Linux std.cppm copy is per-PID for the same reason. Adds a regression test
(ConcurrentCacheRace) that races four LoadProject() calls against a cold
scratch cache — reproduces the race 5/5 without the fix and passes 5/5
with it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
With test.toml + ForTarget covering the cross-arch + Windows-on-Linux
cases, the env-var-driven transport runners are dead weight. This commit
removes them and the retired tests that exercised the env-var plumbing:
- TestRunner::Ssh / SshWin / Wsl factories and their copy/exec/cleanup
template machinery.
- TestRunner::Shell enum (Host/Sh/Cmd) and the ShellQuoteSh helper —
only Host shell quoting is needed once the remote shells are gone.
- TestRunner::copy / cleanup / remoteDir / argsShell fields.
- WindowsPathToWsl and the {remote_bundle}/{bin_win}/{bundle_wsl}
placeholder substitution in RunSingleTest's transport branch.
- ParseRunnerSpec narrowed from {local, cmd, ssh, sshwin, wsl} to
{local, cmd} — the override hatch is preserved, just simpler.
- tests/SshRunner, tests/WindowsViaSsh, tests/QemuUser: these tested
the CRAFTER_BUILD_RUNNER_<target> → runner plumbing that has been
replaced by ForTarget. The runner derivation is exercised every
time CrossArchAarch64 / Wasi / WindowsViaWine runs.
- tests/UnitLib: ssh/sshwin spec assertions become "throws on bogus
spec" assertions.
Refs issue #8.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Vendors toml++ v3.4.0 as lib/toml.hpp and wires it into Crafter.Build-Test
to parse a declarative test.toml manifest (target/march/mtune/sysroot/
requires/timeout/args/defines). Test discovery now treats project.cpp and
test.toml as mutually exclusive: project.cpp stays the escape hatch for
outer-driver tests, test.toml gives downstream test authors a no-boilerplate
path.
Adds:
- TestRunner::Wine() and TestRunner::ForTarget(cfg) — runner is now derived
from cfg.target (Local for host, Wine for Windows-on-Linux, wasmtime for
WASI, qemu-<arch> with QEMU_LD_PREFIX for non-host Linux). The env-var
override CRAFTER_BUILD_RUNNER_<target> still wins as a power-user escape
hatch via FromEnv.
- Declarative preconditions: tool:<name>, file:<path>, env:<VAR> are
evaluated before the build; missing preconditions Skip without paying
the compile cost.
- Hard-fail-unless-declared: when a derived runner's tool is missing AND
the test didn't declare 'tool:<that>' in requires, the missing runner
is a Fail instead of a silent Skip. Surfaces broken cross-arch CI
config that previously hid as "skipped".
- Multi-target sweep: bare `crafter-build test` (no --target=) now
iterates every distinct test.toml-declared target plus the host, so
cross-arch tests run by default without the user needing to know which
targets exist. `--target=X` bypasses the sweep.
Test struct gains a `requires_` vector so project.cpp users can declare
preconditions too (matching what test.toml writes there).
Existing tests, factories (Ssh/SshWin/Wsl/Cmd), and CRAFTER_BUILD_RUNNER_*
machinery remain intact — this commit only adds; migration and deletion
follow in subsequent commits.
Refs issue #8.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two Windows-only compile errors blocked the mingw cross-compile:
* `<winsock2.h>` transitively includes `<rpc.h>`, which defines
`#define interface struct`. The file uses `interface` as a loop
variable name in three for-loops, so on mingw it expanded into
`for(... struct : ...)` and cascaded into a wall of unrelated parse
errors. Undefine the macro right after the Windows headers in the
global module fragment.
* `static_cast<uint16_t>` in the port-probe helper failed because
mingw's `<stdint.h>` typedefs are in the global module fragment and
the C-namespace `::uint16_t` isn't anchored into the module purview
on this toolchain. `import std;` does export `std::uint16_t`, so
qualify the cast.
Verified by running `crafter-build --target=x86_64-w64-mingw32` end to
end and the full test suite (13 passed, 5 environment-skipped).
Two crafter-build invocations resolving to the same external dep both
cloned into <cache>/<name>-<hash>.tmp, corrupting each other's pack
tempfiles. Use a random per-invocation tmp suffix and treat a failed
rename whose destination now exists as the loser-of-the-race case —
discard the local clone and reuse the winner's.
Before LoadLibraryA on the project.dll, point Windows's loader at the
directory we already know holds the runtime DLLs the dll depends on
(libstdc++/libgcc/libwinpthread for mingw-host, c++.dll for msvc-host).
Lets a user run crafter-build.exe straight out of the release zip
without having to prepend C:\msys64\ucrt64\bin (or the libc++ bin
dir) to PATH first.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
In-class inline methods on a module-exported class get the @<module>
linkage attachment, and clang does not emit their bodies into
consumers; the resulting external reference fails to resolve when a
project.dll on Windows tries to call ArgQuery::Has after consuming
ApplyStandardArgs's return value. Move the bodies to the implementation
unit and dllexport them.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
cmd.exe treats single quotes as literal characters, so the existing
single-quote wrapping broke git/cmake invocations on a Windows host
("could not create leading directories of '<path>'").
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
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>
- 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>