The README and the spec comment said that a project two paths load with
different lists ends up in the program twice. They now say the build
stops, with an error that names both paths.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The README suggested giving one sibling a list without a flag only the
project reads, so that the sibling would not get a build directory of
its own. When another path loads the same sibling with the full list,
that filter is what gives it the second directory: two builds of one
project, both in the program (#41). It now says to build such a list
once and give it to every sibling. It also says the default is read
when the spec is made, so a spec made at namespace scope, on another
thread or in a script forwards nothing, and that cfg.projectArgs, the
part ApplyStandardArgs did not interpret, is no substitute: it drops
the target and the build type. The comments on ProjectArgs() and on
Configuration::projectArgs say the same.
The spec comment named --local as a flag the default carries down a
chain of siblings. Through a GitProject hop it breaks: the clone has no
checkouts beside it. The GitProject comment said the clone is keyed by
the args; only the Configuration is, and every arg list builds in the
one clone. AGENTS.md now notes that a stale harness gives no symptom
for a changed default member initialiser: project.cpp silently gets the
old default.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ProjectArgs() read args that LoadProject recorded when it pushed the
frame, so they were there for everything that ran on that frame, not
only for CrafterBuildProject. Two things went wrong with that.
The pragma pre-pass runs on the frame too, so both of its specs needed
a hand-written `.args = {}` to keep the consumer's args from reaching
host tooling, and anything else built there would have needed one as
well. And dlopen runs a project file's static initialisers only on the
first load of that file: a later load gets the same mapping back. A
spec made at namespace scope therefore kept the first load's args for
every later load of the file, a silently wrong variant.
The frame now gets the args just before CrafterBuildProject is called.
The pre-pass and the static initialisers see none, on every load, so
the two opt-outs go. The dependencies the pre-pass resolved move the
other way, to just before dlopen: they are the same on every load, and
ProjectDependency() called from a static initialiser used to find none
and terminate the process.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every project.cpp with a sibling dependency opened with the same line,
std::vector<std::string> depArgs(args.begin(), args.end());
and handed depArgs to each GitProject and LocalProject. CrafterBuildProject
receives a span of string_views and the specs wanted owned strings, and
without the copy the sibling got none of the project's args: it was built
for the host while the project cross-compiled, and in release while it
built --debug. GitProject then turned the copy straight back into
string_views for LoadProject.
Both specs now default their args to ProjectArgs(), the args LoadProject
was given for the project file whose CrafterBuildProject is running. It
reads the per-thread stack LoadProject already keeps for IsTopLevelProject
and ProjectDependency, whose frames now record the args. A default member
initializer rather than a new type or an optional: the member stays a
std::vector<std::string>, so every existing `.args = depArgs` keeps its
exact meaning (each of the ~50 calls across the Catcrafts projects passes
one), and `.args = {}` still forwards nothing. It is the shape of
Configuration::target = HostTarget(). A project.cpp that drops its .args
needs a crafter-build with this change: an older one compiles it and
forwards nothing.
ProjectArgs() is declared in :External, next to the defaults that call
it. :Platform, where it is defined, cannot be imported there: its
unexported `struct Configuration;` would make :External's exported one an
ill-formed redeclaration, which clang rejects.
The #pragma crafter dependency path now passes `.args = {}` explicitly.
Host tooling is built for the host whatever the consumer targets and was
loaded with no args; the default would otherwise hand it the consumer's
--target= and project flags.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`crafter-build test` builds a project's tests side by side, and a
project whose tests each copy its externalDependencies hands every one
of them the same unpinned dependency. Each test's Build() pulled the
shared clone. Before the clone had a lock, those pulls ran at the same
time and broke its .git/FETCH_HEAD, which failed every test of
Crafter.Graphics (issue #34). Since 653dd0b they wait for each other,
but every one is still a network round trip: 26 tests cost 26 pulls of
each dependency, one after another, and for a CMake dependency as many
configure checks and builds.
depResults already records one build pass, keyed by the PcmDir of every
dependency configuration built in it. An external dependency now goes
in the same map, keyed by the directory its build leaves
(ExternalBuildDir: the clone, or for CMake its build directory). The
first configuration that needs it fetches and builds it; the others
wait for that and take its outcome. Keying on the build directory
rather than the clone keeps a CMake declaration from taking the outcome
of a header-only declaration of the same clone, which only fetched it.
A fresh depResults is a fresh pass, so the next build still pulls, and
every caller of Build() gets this: RunTests, a plain build, and the
pragma dependencies LoadProject builds.
To make that possible BuildExternal is split. BuildExternal is now the
shared work, lock, fetch, configure and build, and returns the error.
ExternalOutputs computes what one declaration compiles and links with,
because two declarations of one clone can differ in includeDirs,
libDirs and libs; ExternalBuildResult loses its error field. The work
no longer stops between steps when the configuration that started it
is cancelled: others may be waiting on it, and a run cut short would
leave them a clone nobody fetched. A dependency configuration's Build()
already runs to completion for the same reason.
The promise and future the dependency threads used to build or wait for
a configuration are now one helper, OncePerPass, which both kinds use.
An exception thrown in BuildExternal used to escape its thread into
std::terminate; it is now a build error, as for a dependency.
With eight tests that share a local dependency, a test run fetched it
eight times before this and fetches it once now (SharedExternalFetch,
next commit).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
crafter-build is moving to a single Flatpak distribution, and there is
no Windows host in that picture: no build.cmd, no MSVC or mingw host
build, no DLL plus launcher. Delete every host-Windows path instead of
leaving it to rot unverified: the cmd.exe command runner and quoting,
the LOCALAPPDATA cache, the MSVC and mingw-host LoadProject variants
with their LIBCXX_DIR and CRAFTER_MINGW_DIR plumbing, the winsock port
probe, the LoadLibrary libclang loader, the console-width and isatty
shims, and the preprocessor guards around the Linux bodies.
The x86_64-w64-mingw32 cross-compile target stays. It is target logic
(output names, -femulated-tls, the libstdc++ link extras, copying a
dependency DLL next to a mingw exe, wine as the runner) and did not
depend on any of the removed code. The msvc target goes with the host
that was the only thing able to use it.
Three target-path warts fixed on the way: ExpectedOutput named a mingw
static library <name>.lib while the archive step wrote lib<name>.a, so
those libraries relinked on every run; the import lib is no longer
copied next to a mingw exe, since the dependency's own bin dir is
already on the link line; and llvm-ar now writes every archive, not
only the LTO ones, so Debug and Release archives are made the same way.
CRAFTER_API and Crafter.Build-Api.h existed only for the DLL boundary.
Nothing downstream used them, so they are removed rather than kept as
an empty macro nobody can verify.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The rule was `\bchar\s*\*` over the text minus a substring denylist — argv,
getenv, setenv, dlerror, c_str, .data(, reinterpret_cast, extern ". Every entry
was a patch for one interop site, the list could only grow as libraries
arrived, and each entry disabled the rule for the whole LINE it appeared on.
It now walks declarations and asks the question the denylist was approximating:
whose header dictates this spelling? A declaration with C language linkage, or
one whose initialiser binds to an entity declared outside the project root, is
somebody else's API and keeps its spelling. getenv, c_str and friends are
exempt because of where they are declared, not because they are named here, so
a new external library needs no new entry.
Two bugs found while testing this, both of which had made the rule silently
pass over the entire repository:
clang_getCursorLanguage cannot be used to detect extern "C". Its default answer
is CXLanguage_C for a plain function, variable or parameter even in a C++
translation unit, so isExternC was true almost everywhere and exempted
everything. Replaced by tracking CXCursor_LinkageSpec depth during the walk,
reading the extent text to tell extern "C" from extern "C++".
Attributing any foreign reference in a subtree to the enclosing declaration was
too broad: a function that merely touched libc++ somewhere in its body would
exempt its own signature. Narrowed to initialiser contexts — a variable, field
or parameter — which is where a binding to a foreign API actually occurs.
Also: functions now carry their RESULT type rather than the whole function
type, since the parameters arrive as their own declarations and would otherwise
be reported twice. main's parameters are exempt structurally, its signature
being fixed by the language rather than chosen here.
Two sites keep an explicit lint-disable, both Crafter::Run taking main's argv
verbatim. That is two visible, reasoned suppressions in place of a denylist
that silently disabled the rule for every line mentioning one of eight tokens.
ExternalCloneDir and ExternalIncludeFlags are now exposed from :External, so a
source that includes an external dependency's headers can be parsed without
running a build to discover where they are. BuildExternal derives its own
working directory through the same function, so the two cannot drift.
Crafter.Build-Shader.cpp needed this to parse at all.
A file with no compile command — project.cpp, which LoadProject builds with its
own flags — is not a translation unit of the build graph, so AST rules skip it
the way a rule self-filters by extension. That is distinct from a file that
should have parsed and did not, which stays an error.
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>
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>