Crafter.Review's workflow publishes its Flatpak into the same repo, the
web root's repo/, and holds an flock on .publish.lock in the web root
while it commits and updates the summary. Its comment says that this
protects both only once Crafter.Build's workflow takes the lock too, and
ours never did, so a publish of each could interleave: a
build-commit-from and a build-update-repo --generate-static-deltas
--prune of each workflow on one repo at the same time.
The Publish step now holds the same lock from its commit to the ref
files it writes into the web root, and the job installs util-linux for
flock, as Crafter.Review's jobs do.
AGENTS.template.md says that the repo is shared and the lock taken.
Neither it nor the workflow's header comment says any longer that the
web root and the signing key are this runner's alone, or that the
organisation-wide `flatpak` runner mounts none of them. That runner,
which serves every Catcrafts repository and refused all of this job's
volumes when it last took it, in October, now mounts the web root and
the key for Crafter.Review's publish job, and the volumes a runner
allows are open to every job it runs. The owner has ruled the sharing
intended. The header also stops saying that a job of ours that lands
there fails before building: that held while the runner refused every
volume, and AGENTS.template.md keeps it as the October history.
The Bundle step copied packaging/flatpak/net.catcrafts.CrafterBuild.flatpakref
into dist/ as the checkout has it, with GPGKey=GPG_KEY_BASE64_PLACEHOLDER,
so all six releases, 2.0.0 to 5.0.0, attach a .flatpakref that does not
install: `flatpak install --from` it stops with "GPG: Unable to export
keys: GPGME: No data" (Flatpak 1.18.4) before it adds the remote. The
copy the publish step writes to the web root has the key filled in, and
installing from it goes ahead.
The key is known only from the publish step on, so the release's copy,
and SHA256SUMS over it, are made there from the web root's file instead
of in the Bundle step.
crafter-review 1.0.1, the released build installed on this machine, and
the newer build installed beside it as //local refuse the config key
lensSections: a lens's repository section is named per lens, as
lenses.<name>.section, instead of being found by listing a directory.
With the key in place `crafter-review config` reports it as a problem,
and freeze and run stop at "preflight: config: unknown key lensSections".
The three files in .review-gates/lenses/ are named under lenses, one
entry each, the migration the Crafter.Review session gave; with them
`crafter-review config` reports no problem.
AGENTS.template.md had every change go through a pull request against
master. The owner has made pushing straight to master the default.
The commit and push guidelines say so: an approved range is pushed to
master as a fast-forward, with no merge commit, and a pull request is
opened only when the owner asks for one; when master has moved since
the freeze, the range is rebased, frozen and reviewed again. What
follows from that: the README changes in the same push; without a pull
request CI does not run on a range before it reaches master unless
someone starts it by hand, so the review's gate0 is the check before the
push; and the freeze exception for a merge commit applies only to a pull
request. Design rule 8, the testing note and the release steps lose
their pull-request wording, and the testing note no longer speaks of a
commented-out lint step: ci.yaml has no lint step at all.
AGENTS.template.md and project.cpp gave the first run flatpak as about
25 minutes on a 56-core machine and its state as about 2 GB, figures
written on 2026-10-06, before lldb and the Vulkan and SPIR-V modules
were in the manifest. A first run of this commit's manifest, in a fresh
worktree, took 30 min 36 s with 56 CPU threads, 14 min of it for LLVM
with lldb, and left build/flatpak at 4.1 GB, 3.3 GB of it state/.
The figures become those, and the line on later runs says that they
rebuild only the crafter-build module while the manifest is unchanged.
Crafter.Graphics' PushConstantRewrite and UIFusedShader tests require
tool:spirv-val, and the Sdk has no spirv-val, so inside the Flatpak they
are skipped. The spirv-tools module the validation layer needs built
only the libraries, and removed them afterwards.
It now builds the programs as well, spirv-val, spirv-opt, spirv-dis and
the rest, which link SPIRV-Tools statically, and ships them. The
headers, the static libraries, the CMake and pkg-config files and
libSPIRV-Tools-shared.so, which nothing here links, are still removed,
and a post-install step runs spirv-val --version.
flatpak run becomes bubblewrap, and without --die-with-parent the
sandbox outlives it. With crafter-build started through the alias in an
interactive bash or zsh on a terminal, SIGKILL to its flatpak run
process alone left a -r program running and a build writing into bin/
and build/. With the flag, both ended with it.
bubblewrap kills the sandbox when flatpak run or its parent dies, and
for crafter-build typed at the prompt, in the foreground or with &, that
parent is the shell: with the flag, the sandbox also ended when the
shell was SIGKILLed, or exited after a disown. In a (crafter-build -r &)
subshell, flatpak run outlives the subshell and is adopted by another
process, so its sandbox outlived the shell with the flag as well. Ctrl-C
ended the program either way.
The README's alias gets the flag, and the paragraph after it says what
that changes: a program started through the alias, as crafter-build ...
or crafter-build ... & at a bash or zsh prompt, ends with its shell,
even after a disown.
The README gets a Debugging section: the debugging tools the sandbox
carries, that a program to debug needs cfg.debug (which --debug sets
where the project calls ApplyStandardArgs), how to start the tools with
--command, why a debugger in one flatpak run cannot see a program that
another one started (each has process IDs of its own), and flatpak
enter as the way into a running program's sandbox, with the ptrace
opt-in that takes where ptrace_scope is 1. It warns off lldb's
gdb-remote from a second sandbox: when a process in lldb's own sandbox
has the remote program's process ID, lldb takes that process's
executable for the program's. And it shows which variables turn the
layer and GPU-AV on from the environment, and that VK_LAYER_PATH and
VK_ADD_LAYER_PATH do not reach the sandbox.
AGENTS.template.md gets the module layout and the rule that the layer
follows the Sdk's Vulkan loader. Its CI section says that a change to a
module's sources or build-commands builds it again and every module
after it, so that a new version of swig or libedit, which come before
LLVM, now rebuilds it too, and loses the cold-run time it gave, written
on 2026-10-03, before any of these modules were in the manifest.
Programs that -r and the test runner start run inside the Flatpak's
sandbox, and the Sdk it runs on has gdb but neither lldb nor the Khronos
validation layer: in the sandbox of beta 5.0.0, a vkCreateInstance that
asks for VK_LAYER_KHRONOS_validation returns VK_ERROR_LAYER_NOT_PRESENT.
flatpak run clears VK_LAYER_PATH and VK_ADD_LAYER_PATH, so the layer the
host has cannot stand in.
lldb becomes a project of the llvm module and brings lldb-server,
lldb-dap and lldb-mcp. Its Python, libedit, curses, libxml2 and lzma
support are set ON instead of being left to detection, so a dependency
that goes missing fails the configure step rather than shipping an lldb
without it. The Sdk has neither SWIG nor libedit, so both become modules
before llvm, SWIG as build-only.
The layer is Vulkan-ValidationLayers vulkan-sdk-1.4.357.0, the Vulkan
SDK release of the Sdk's loader (libvulkan.so.1.4.357), built from
Vulkan-Headers, Vulkan-Utility-Libraries, SPIRV-Headers and SPIRV-Tools
at the revisions that release's scripts/known_good.json names. Those
four are built into /app at those revisions and removed afterwards. All
five come after the LLVM and WASI modules, so that bumping them reuses
the cached LLVM and WASI builds. UPDATE_DEPS is turned off: it defaults
to ON, which has update_deps.py clone the dependencies, and of all the
modules only crafter-build's build-args share the network.
Crafter.Review reviews a frozen commit range before it is pushed: the
approach, a static proof of the code's bugs, and every claim in the
messages and in the documentation the range changes, one claude process
per lens and unit, or per whole-range pass, and the owner approves the
exact frozen hash before the push. This sets it up for this repository.
.review-gates/config.json turns on three lenses in the commits mode, at
xhigh: repo-better-solution, repo-bug-prover and repo-facts. The
functional lens is off: it exercises a web application through a
browser, and this repository's product is a command-line tool. The
security lens is off by the owner's ruling: crafter-build stores no
secret of its own, and while its Flatpak runs with the user's SSH agent
(--socket=ssh-auth) and the whole host filesystem (--filesystem=host),
everything it runs is code its user chose to run. The rules CI keeps
around its token and signing key go to the approach lens's section
instead.
gate0 lists the commands run by hand before a freeze. build.sh comes
first, because bin/crafter-build compiles project.cpp against the
interfaces it was bootstrapped with, and lint comes after the self-host
build, because lint's AST rules parse with the module PCMs a build
leaves and lint builds only the std PCM itself.
.review-gates/lenses/ holds this repository's section of each lens's
charter, appended to the text Crafter.Review ships: that AGENTS.md is
absent from the frozen tree, the layers and the consumers of
interfaces/, the execution contexts a bug can hide in, where libc++ and
clang are readable from the review's sandbox, and what a "passes the
suite" claim traces to.
The /prepush skill that crafter-review init writes stays untracked. It
is generated from the installed Crafter.Review and refreshed by every
init, so a committed copy would lag the binary it describes. One init
per checkout restores it, which AGENTS.template.md now says, together
with the rules freeze holds every commit of a range to: signed
(requireSigned), with the owner's address as its author and committer
(authors), and no AI attribution trailer (aiTrailers forbid).
The fix for #43, a mingw build that saw the host's glibc headers and
Linux libraries through a prefix like /usr as clang's sysroot, and what
the review of it brought along: units that recompile after a std module
rebuilt in place, CMake externals built again when their configure
inputs change, std modules keyed on every flag they are compiled with,
and lint that fails rather than passing files it could not parse. A
major, because Toolchain::mingwSysroot is now mingwPrefix and every
build directory moves once.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings in the fix for #43. The mingw target no longer gets a system
prefix (the host's /usr in the Flatpak, or any prefix whose include/
has a C library's features.h) as clang's --sysroot, but a view of its
mingw-w64 parts, made in the cache or in $XDG_RUNTIME_DIR; a prefix of
mingw-w64 alone stays the sysroot whole. From the review of the first
version: the std module's directory is keyed on every flag it is
compiled with and VariantId on that key, so every std PCM and build
directory moves once; units record the BMIs they import; a CMake
external whose configure inputs changed starts from an empty build
directory; lint fails a configuration that has no compile command.
Toolchain::mingwSysroot is now mingwPrefix.
The MingwSysroot test writes its stand-in std module through
std::format, because the module scanner reads string literals as code.
#45 replaces the scanner's regexes with clang-scan-deps.
The branch starts at master's tip, so the merge is a tree identical to
the branch's; that tree passes format, lint and the test suite, natively
and inside the Flatpak sandbox, and CI.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The README said every prefix reached clang as a view of its
x86_64-w64-mingw32/ and lib/gcc/, in ~/.cache. It now says a prefix is
the sysroot as it is unless it also holds a Linux system, names
lib64/gcc/ as well, gives the view's place inside the Flatpak and the
XDG_RUNTIME_DIR fallback, and says messages name headers by their path
in the prefix.
AGENTS.template.md describes MingwSysroot's two outcomes and
MingwViewToPrefix, has the std PCM keyed on its flags in the cache
rule, lists StdPcmKey among what VariantId hashes, and gains two
gotchas: anything that changes how the std module is compiled goes into
StdPcmFlags, and a CMake external whose recorded configure inputs
differ is built again from an empty directory.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A host configuration pointed at a sysroot without libc++ cannot get its
std module built, so its files cannot be parsed. With an AST rule
registered, the run has to skip the rule and fail instead of reporting
the files clean, as it did before the fix. The cache is redirected
around this run only: the std module's directory is made before its
build fails.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When GetCompileCommand threw or the std module failed to build, the
lint driver cleared the configuration's command, and an empty command
means "not a translation unit" (project.cpp, a header): every rule that
needs an AST skipped the files quietly and the run passed them as clean.
MingwSysroot made that reachable for every mingw configuration whose
view cannot be made. The reason is now kept per configuration and
reported as an AST failure for each of its files, which fails the run
and prints it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ExternalReconfigure builds a one-file C library from a local repository
as a CMake external, then plants a file in its build directory to stand
for what an old configure left. Built again with the same inputs, the
directory is kept. With its recorded inputs rewritten the way another
crafter-build's configure would have left them, the next build has to
start from an empty directory and build the library again. Before the
fix the planted file survived the reconfigure.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ConfigureCMake records the flags it injects (compiler, libc++, and for
the mingw target CMAKE_SYSROOT) and reconfigures when they differ, but
it did so in place. CMake then keeps what the old flags compiled: the
Makefiles generator writes --sysroot into build.make, which make does
not compare, so nothing was recompiled, and the check results the old
configure cached stay too. A mingw external built by a release that gave
clang the host's /usr as the sysroot kept its objects reading
__libc_single_threaded after the fix for #43, and only deleting
<cache>/external/<name>-<hash>/build by hand got rid of them.
A build directory with a CMakeCache.txt whose recorded inputs differ is
now removed before the configure. It keeps its name, since projects
point include directories at headers generated in it. The two
crafter-builds the Flatpak's branches are, sharing one cache, rebuild
such an external from nothing each time they take turns, where they
used to reuse each other's objects.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
StdPcmRebuild builds a project whose main.cpp imports std, then dates
the source before the object and the object before std.pcm, as a std
module rebuilt after the object leaves them, so that nothing else makes
the object stale. The next build has to compile main.cpp again, and the
idle build before it must compile nothing. Without -fmodule-file-deps
the object was only relinked. The test redirects the cache, since
redating a std PCM the real cache shares would rebuild its other users.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A .cpp unit's depfile listed its source and headers, never the BMIs it
imports: clang's driver adds -module-file-deps to --precompile only. A
std module rebuilt in its directory, which happens whenever its source
changes under one compiler and one set of flags (a libc++ update, or
mingw-w64's under the same prefix), left every object that imports it
as it was, and the next build only relinked them. Implementation units
are now compiled with -fmodule-file-deps, so the std PCM and the
project's own PCMs are prerequisites of their objects like headers.
Depfiles written before this lack the edge, so a std module that moves
directories still needs VariantId to move with it (StdPcmKey).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
StdPcmDir named the directory by target, march and toolchain, then
grew a case per target for the rest: the wasm flags, and with the first
fix for #43 the mingw prefix. Every other flag BuildStdPcm passes went
unkeyed, so configurations that differed only there took turns
rebuilding one std PCM in place, under the PCMs built against it: a
project with -mtune=generic against project.cpp's -mtune=native on every
run, or two sysroots for one target. And the mingw prefix was keyed only
when there was one, so a build without a prefix landed in the directory
an older release had filled with a CRAFTER_MINGW_DIR=/usr build, and
relinked its objects.
StdPcmFlags is now the one source of the flags BuildStdPcm compiles
with, and StdPcmKey names the directory by target, march and toolchain
followed by a hash of those flags and, for the mingw target, of the
prefix behind its --sysroot. VariantId hashes StdPcmKey in place of the
mingw prefix: units do not notice a std module that moved directories,
so a build has to move with it. Every std PCM directory and build
directory moves once, as after a compiler update; the old ones stay
until `clean` and the cache is cleared by hand.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The first fix showed clang every mingw prefix through a view of its
mingw-w64 parts. That broke a prefix that holds mingw-w64 alone, where
Windows libraries installed with --prefix land in include/ and lib/:
with CRAFTER_MINGW_DIR=/opt/mingw, <SDL2/SDL.h> and -lSDL2 were no
longer found. Such a prefix is clang's sysroot whole again. Only a
prefix that also holds a Linux system gets the view
(Toolchain::mingwPrefixIsSystem): the host's /usr inside the Flatpak,
whatever its include/ holds, and a prefix whose include/ has a C
library's features.h, which every glibc and musl header includes and
mingw-w64 has no use for, as CRAFTER_MINGW_DIR=/usr has.
MakeMingwSysroot makes the view in the cache, or in
$XDG_RUNTIME_DIR/crafter.build when the cache's filesystem takes no
symbolic links (vfat, exFAT, CIFS without unix extensions): the XDG
spec promises them there, and nothing else in the cache needs them.
Its path is absolute, because CMake runs the compiler from directories
of its own and a relative cache did not resolve from there. The prefix
is resolved once, absolute and lexically normal, so /opt/mingw/ and
/opt/mingw are one prefix and not two builds.
clang names a header by the path it found it at, so every diagnostic
from a viewed mingw-w64 header named a hashed cache directory.
RunBuildCommand now writes paths into the view as paths into the
prefix, as BuildStdPcm did for its manifest messages already.
Also from the review of PR #44: SysrootCompileFlags builds a mingw
command, which can make a view, so it redirects the cache like the
other tests; the manifest filter's comment names what it still keeps
out; the C path asks MingwSysroot() once.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The README says which directories of a mingw-w64 prefix clang sees, and
why the rest of a prefix like /usr is kept out of a Windows build.
AGENTS.template.md names MingwSysroot in the :Toolchain partition, with
the reason the prefix itself is never a sysroot, counts the views among
what goes in the cache, and adds the mingw prefix to what VariantId
hashes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MingwSysroot stages a prefix shaped like a Linux system's /usr with a
mingw-w64 cross toolchain in it, without needing a real one: glibc's
<sys/single_threaded.h> in include/, gcc's per-target directory, and a
stand-in mingw-w64 tree whose libstdc++ has an empty std module. A
static library for x86_64-w64-mingw32, so that nothing is linked,
compiles a C++ and a C source that stop with #error when they see the
prefix's include/ or miss mingw-w64's or libstdc++'s headers. Against
the library before the fix, the C++ source stopped on
<sys/single_threaded.h>. The test also checks that the std module is
not cached in the directory older releases built it in against the
prefix itself. It needs only clang, so it runs in CI too, where no
mingw-w64 is installed.
The stand-in std module's source is written through std::format: the
build's import scanner reads string literals as code, and the literal
"export module std;" made the test itself a unit of a module std.
VariantId checks that a prefix gives a mingw configuration a variant of
its own, that two prefixes give two, and that other targets ignore it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Inside the Flatpak the mingw-w64 prefix is the host's /usr, and it was
clang's --sysroot as is. clang's mingw driver takes a sysroot for a tree
that holds mingw-w64 alone, and adds its include/ to the include path
and its lib/ to the library path. For /usr those are the Linux system's
own: every header mingw-w64 lacks came from glibc, and libstdc++'s
<ext/atomicity.h>, finding <sys/single_threaded.h>, had shared_ptr read
__libc_single_threaded, which nothing on Windows defines. Every program
that used one failed to link, among them every Crafter.Graphics Win32
executable, through Crafter.Asset's GLB loader. CRAFTER_MINGW_DIR=/usr
did the same outside the Flatpak (#43).
The sysroot is now MingwSysroot(): a directory in the cache, one per
prefix, holding only links to the prefix's x86_64-w64-mingw32/, lib/gcc/
and lib64/gcc/ (where openSUSE keeps gcc). Through it clang finds
mingw-w64, gcc's version and libstdc++ exactly as it does through the
mingw gcc on PATH, and nothing else. It is made by the first mingw
compile, link or CMake configure of a process, so no other command
writes to the cache for it and a failure to make it is that build's
error. The Toolchain field is now mingwPrefix, since clang never gets
the prefix itself, and it is made absolute when the toolchain is
resolved, as it ends up in link targets and directory names. Messages
keep naming paths under the prefix rather than under the view.
The prefix is also part of the mingw target's VariantId and of its std
PCM directory. Units do not track the std PCM they import: when the
fixed release built in a directory a broken build had left, the std
module was rebuilt and the objects compiled against glibc's headers
were only relinked, failing as before. As with a compiler update, a new
directory is what gets them rebuilt. It also keeps two releases that
share a cache, as the Flatpak's branches do, from rebuilding one std PCM
under each other's PCMs, and two prefixes apart. Without a prefix both
stay as they were: clang's own discovery has not changed.
Two shorter ways do not work: --sysroot=<prefix>/x86_64-w64-mingw32
loses the directory gcc's version names, so <string> is not found, and
clang ignores --gcc-toolchain and --gcc-install-dir for this target.
Fixes#43
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every release page ended in the two flatpak commands that install
crafter-build, which the top of the README already gives. The notes
now stop at their last section.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
AGENTS.md written from a tracked AGENTS.template.md, so the dependency
notes no longer change a file the project commits. A major, because a
project that kept 3.0.0's notes section in its committed AGENTS.md
stops getting it updated until it moves to the template: the rule is a
major whenever the notes have a Breaking section, even one that no
build fails over.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings in the template: a project commits AGENTS.template.md, and
`crafter-build` and `crafter-build test` write a gitignored AGENTS.md
from it with the dependency notes merged in, so a change to a
dependency's notes no longer shows up in the consumer's diff. An
AGENTS.md no build wrote is never replaced, and without a template
AGENTS.md is left alone, with a warning when it still holds a 3.0.0
section. This repository moves its own AGENTS.md to the template.
The branch starts at master's tip, so the merge is a tree identical to
the branch's; that tree passes format --check, lint and the test suite.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This repository now uses the template itself. AGENTS.md is written by
the self-host build and `test` and is gitignored, so the notes
Crafter.Math ships reach the agents working here, and a change to them
no longer leaves a tracked file modified, which also made every build
afterwards a dirty-tree version, a new crafter.build-lib variant and a
full rebuild.
The gotcha that described the old arrangement now says where to edit,
that a fresh clone or worktree has no AGENTS.md until its first build
(./build.sh does not write it), and what an older harness does to the
generated file. The paths table and the never-commit list name both
files. The CI error for a missing signing key and a manifest comment
point at the template, since the forge no longer shows an AGENTS.md.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The dependency notes section now starts from the template: what to
commit and what to ignore, the two commands that move a project with a
committed AGENTS.md over, what a build writes, where the section goes,
and what becomes of an AGENTS.md no build wrote and of one that still
holds a section from 3.0.0.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The dependency notes were kept in a section of the AGENTS.md beside
project.cpp, the file a project commits, so a build could change a
tracked file at any time: a dependency editing its AGENTS.consumer.md,
an unpinned one moving on, a flag pulling a dependency in or out, each
showed up in the consumer's diff as a change nobody made. Users
reported it as the feature clobbering their diffs.
A project now commits its instructions as AGENTS.template.md, and a
build and a test run write AGENTS.md from it: a first line saying the
file is generated and where to edit, the template's text as written,
then the notes section, after it or where the template's marker lines
put it. AGENTS.md is build output and goes into .gitignore. It is
created when missing, since a fresh clone has none, and otherwise
written as before: only when its text changes, in place and under a
lock, so links, ownership and a read-only file are respected.
The first line is also how a build tells its own AGENTS.md from one
someone wrote. Beside a template the second is easily there, as a
committed AGENTS.md nobody has removed yet; it is never replaced, and
each build warns until it is deleted. An empty file counts as a
build's, since that is what a write cut short leaves of a new one. An
AGENTS.md linked to the template is refused: writing it would write
the committed file again. A template the notes cannot be placed in
leaves no empty AGENTS.md behind.
The in-place mode is removed rather than kept as the fallback for a
project without a template. It is the default that put generated text
into tracked files unasked, the moment any dependency started shipping
notes. Without a template a build no longer touches AGENTS.md. One
that still holds a section from 3.0.0 would go stale silently, under a
preamble promising that every build rewrites it, so it gets a warning
naming the move instead.
MergeDependencyNotes keeps its rules and now runs on the template. The
test changes in this commit rather than its own, because it pinned the
in-place behaviour this removes: it now covers the generated file's
shape, edits and markers in the template, the ownership check, a CRLF
template, a link to the template and the cases without one.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
C++ exceptions in wasm builds, scripts, dependency notes, the browser
runtime's exit and park, and mingw found where it is; the fixes since
2.2.0; and the two changes that need a look when upgrading: the wasm
sysroot and engine requirements, and siblings inheriting the loading
project's args. A major, because a wasm project.cpp with
-fno-exceptions or an older sysroot, and a project.cpp whose siblings
relied on getting no args, build differently or not at all on this
release.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The release page got the newest <release> block as the plain text of
its paragraphs, which fit two paragraphs of prose and nothing more.
From 3.0.0 the notes group the changes under New, Fixes and Breaking,
each in descending order of importance, and AppStream's description
markup has lists but no headings to carry that.
The metainfo stays the one source of the notes, since the Flatpak
installs it and the tag check reads it. A category is written there as
a paragraph that introduces a list, <p>New</p><ul>...</ul>, which is
how AppStream notes lead into a list anyway, and the CI step renders
such a paragraph as a Markdown heading over its items. A paragraph on
its own stays a paragraph, so the older entries render as before. The
step parses the XML with ElementTree instead of two regexes: entities
are decoded, <em> and <code> get their Markdown spelling, and a code
span keeps text like lib/<triple>/eh from being read as an HTML tag by
the forge.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
-mllvm -wasm-enable-sjlj, in every wasm compile because setjmp.h does
not compile without it, turns setjmp and longjmp into calls to
__wasm_setjmp, __wasm_setjmp_test and __wasm_longjmp and a throw of
the __c_longjmp tag, all of which live in wasi-libc's libsetjmp. The
driver does not link it, so a program that used setjmp, or a C library
that handles its errors that way, failed to link with undefined
__wasm_setjmp until the project put -lsetjmp in its linkFlags.
Build() adds it to every wasm link, next to -lunwind. A static archive
contributes only the members something references, so a program
without setjmp gets nothing from it, and every sysroot that passes the
exception check ships it. WasmExceptions drops its own -lsetjmp, and
its C fixture now fails to link without this.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The README called an exception escaping main a trap. It is an uncaught
WebAssembly exception, and nothing prints its type or what(); the
README says so now. It also lists what a wasm build refuses and why,
says that setjmp needs -lsetjmp and that a longjmp across a noexcept
function ends the program, that a wasmtime older than 47 needs
-W exceptions=y, and that an old wasm-opt on PATH breaks release links.
It documents exit and park in a page from EnableWasiBrowserRuntime,
with the declaration of the park import.
The wasi example still sent Arch users to wasi-libc++, which has no
exception-enabled libraries. It asks for a wasi-sdk 33 sysroot or the
Flatpak now, and names the browsers a page needs. AGENTS.md notes that
the runtime test needs node, so it runs in the native loop and not in
CI.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
WasmBuildRefusals checks that -flto in compileFlags and in linkFlags,
-fno-exceptions and a shared library are each refused with their
cause, and that none of them leaves bin/ or build/ behind. With the
check disabled, each of them failed on a missing std.cppm instead,
after the build directories had been made.
WasiSysrootWithoutExceptions also takes its sysroot through missing
and without the target's directory, each with a message of its own,
checks that a refused build leaves nothing behind, and that a static
library passes without libunwind.
WasmExceptions catches an exception thrown inside libc++.a (stoi).
vector::at throws from a header compiled into the fixture, so the
fixture also passed against the libc++ built without exceptions. Its C
source uses setjmp, which the old C flags did not compile. An
exception that escapes main has to end the run as wasmtime's uncaught
exception, where any nonzero exit used to pass, a trap or a timeout
included.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
-flto in a wasm configuration's compileFlags or linkFlags reached the
link, where LTO code generation runs without the backend options that
keep exception handling: every landing pad was dropped, and the build
succeeded with throws that escaped their catch blocks. -fno-exceptions
there made every `import std;` fail on the std module built with
exceptions, and a wasm shared library failed to link against the eh/
libc++, which is static archives built without -fPIC. Build() refuses
all three now, each with a line that says why, before it makes a
directory or starts a thread.
The sysroot check sat in BuildStdPcm, which the linter calls as well.
There the error only dropped the compile command, so lint ran no AST
rule on a wasm configuration and reported it clean. It also refused
static libraries, which never link, fired after the shader and
external threads had started, and called a missing sysroot, or one
without the target, "no C++ exception support". It is part of the same
early check now, which first says when the sysroot or the target's
directory is missing, and asks for libunwind only where something
links.
C sources got only the signal.h define. setjmp.h refused to compile in
C, and a C object given -wasm-enable-sjlj through compileFlags used the
legacy exception encoding next to C++ objects in the standard one, a
mix engines refuse to load. They get WasmCompileFlags now, like the C++
units, a variant pass's flags included.
The baseline probes include simd: V8 and SpiderMonkey switch SIMD off
on x86 CPUs without SSE4.1, which virtual machines often present, and
such an engine passed the exception-handling probe only to fail
compiling the module; the manifest tests expect it on every entry. The
Flatpak pins WASI_SDK_EXCEPTIONS=DUAL, which
wasi-sdk 34 defaults to only with clang 23 or newer, and checks that
the eh/ libunwind got installed: the wasm tests skip without it, so CI
would otherwise pass a Flatpak whose every wasm build fails.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
runtime.js had no test that runs it. WasiBrowserRuntimeExit runs the
real file under node, once per scenario, against a module assembled in
the harness. Between _start and the call that ends it the module has
the frames that caught the old exit: a destructor (catch_all_ref, then
throw_ref), a catch (...) and a noexcept function, each reporting when
it runs. An exit has to run none of them and record its status; a park
has to run none of them, nor the code after it, and leave the program
callable with its state intact; an exit from a later call has to end
the program too, a return from main has to give status 0, and a crash
or an exception nothing caught has to stay an error.
With the JS throw put back into proc_exit, the exit from main and the
exit from a later call both end in the noexcept frame's terminate.
It skips without node, or with a node whose engine lacks the
standardized exception handling. The Flatpak has no node, so CI does
not run it; the native loop does.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
runtime.js implemented proc_exit by throwing a JavaScript error through
the wasm stack, and the switch to -fwasm-exceptions made that error
catchable: every cleanup pad, catch (...) and noexcept terminate pad
catches a foreign exception. An exit therefore ran the destructors of
every frame it left, a catch (...) swallowed it and carried on, and
under a noexcept frame it ended in std::terminate. Crafter.Graphics
calls std::_Exit(0) in Window::StartSync to hand the thread to
requestAnimationFrame; once the exit's destructors had torn down
main's locals, every later frame read destroyed state.
proc_exit is now a function of a small helper module, bound directly
as the program's import, that reports to the runtime and then executes
unreachable. A trap is the one thing wasm code cannot catch, and raised
in wasm rather than from JS it does not depend on each engine keeping
a trap uncatchable when it passes back through a JS frame. Nothing of
the program runs on the way out and its shadow stack stays as it was,
which is what exit and _Exit require. window.crafter_wasi.exitCode
holds the status, also after a return from main, so that bridges which
call into the program on their own can stop.
Handing the thread to the browser for good is a separate import,
crafter_runtime.park. It ends the call the same way but leaves the
program alive, and main's locals valid for the calls that follow, so
StartSync keeps its contract that no caller code after it runs. Until
Crafter.Graphics moves to park, its _Exit behaves as before minus the
destructors: the runtime records an exit but cannot stop a bridge that
calls exports itself.
A WASI reactor build, where main returns and state lives in statics,
was the alternative. It breaks StartSync's promise never to return,
which the native targets rely on. JSPI would keep the whole stack
alive, but Safari does not have it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The README described the mingw target and said nothing about wasm. It
now says where the WASI sysroot comes from, that C++ exceptions work as
on the other targets and what that takes (-fwasm-exceptions in the
standardized form, the sysroot's exception-enabled libc++, -lunwind
added to the link), which engines run the result, and that an exception
escaping main ends the program without calling std::terminate.
AGENTS.md says where the wasm flags live, that VariantId and the std
PCM directory hash them, and that the wasm tests skip where the
distro's sysroot lacks the eh/ libraries, so they run in the Flatpak.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
WasmExceptions builds a fixture for wasm32-wasip1 and runs it under
wasmtime. It throws through four frames whose destructors have to run,
catches the exception, catches one that vector::at throws, and with an
argument lets one escape main, which has to fail the run. With the
previous flags the fixture does not compile ("cannot use 'throw' with
exceptions disabled"), and each part of turning exceptions on fails it
in its own way, from a BMI built without them to the legacy encoding
wasmtime refuses.
WasiSysrootWithoutExceptions takes a scratch sysroot through three
states, no libunwind, libunwind under eh/ and libunwind in the
triple's own directory, and checks that only the first stops the
build, with the message naming the library it looked for. It needs no
working wasm toolchain, so it also runs where the others skip.
WasmHelloRuns and WasmExceptions treat a sysroot without the
exception-enabled libraries as missing and skip. The manifest tests
expect the exception-handling probe on every entry.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
wasm builds were compiled with -fno-exceptions. A throw did not
compile, libc++'s own throws (vector::at, std::format, filesystem)
aborted the program, and code that relies on exceptions could not be
built for the browser at all.
Every wasm unit and the std module now get -fwasm-exceptions, and the
link adds -lunwind, which code built that way calls into and the driver
does not add. The flag also selects the libc++ under the sysroot's
lib/<triple>/eh, which wasi-sdk 33 and later ship next to the noeh
one. Those libraries use the standardized encoding (try_table, exnref)
while LLVM 23 still emits the legacy one by default, which wasmtime
refuses to load and which would put both in one binary, so the flags
ask for the standard form with -wasm-use-legacy-eh=false. That makes
the setjmp lowering loadable under wasmtime too.
The flags live in one constant, WasmBaselineFlags, which the compile
command and the std module both take through WasmCompileFlags: a BMI
is refused by a unit whose exception setting differs from its own.
VariantId and StdPcmDir hash it, so the first build after the update
starts fresh instead of importing PCMs built without exceptions, and
two releases sharing the Flatpak's cache don't take turns rebuilding
one std PCM. A sysroot without the exception-enabled libraries stops
the build with a line saying so, rather than at the link with "unable
to find library -lunwind".
The result needs an engine with exnref: Chrome 137, Firefox 131,
Safari 18.4, the bundled wasmtime. The runtime's exception-handling
probe tested the legacy encoding; it now tests try_table, every
variants.json entry carries it, and a browser without it is told which
feature it lacks instead of failing with a CompileError. There is no
switch back to -fno-exceptions: engines without exnref are old enough
not to keep a second build for them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PR #42, fixes#41. Build() now refuses a configuration whose transitive
dependencies hold two configurations exporting a module of the same
name, such as one project loaded along two paths with different arg
lists, before it builds anything, and names both paths. Until now both
builds ended up in the one program, which compiled cleanly and then
read garbage. Merged cleanly on top of claude/forward-project-args;
the suite, with claude/dependency-notes' fixtures, passes under the
new check.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PR #40. GitProject and LocalProject default their args to the loading
project's own (ProjectArgs()), this tree lints with LintRules' lite
set, and the fixes from the branch's review: a project file gets its
args only for the CrafterBuildProject call, host tooling gets project
cache entries of its own, and `--` ends crafter-build's options.
The conflicts with claude/dependency-notes are all places both sides
added to:
- Platform.cppm and Platform.cpp: PragmaDependencies() beside
ProjectArgs() and LoadingHostTooling(), all three kept.
- LoadProject: the record PragmaDependencies() reads moves up with the
frame's dependencies, which this branch sets before dlopen. Left
where it was, it would have copied preDeps.configs after the vector
was moved into the frame, and recorded nothing.
- README: the pragma section's new last sentence goes before the new
"Dependency notes for agents" section.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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 fixture is P -> M and P -> A -> M, where M's own flag --wide
changes a struct it exports. Loaded with --wide --split, P gives M its
args without --wide while A passes them on whole: Build(P) has to fail,
name the module and both paths, and leave no bin/ anywhere. Loaded with
--wide alone, both paths give M the same list, there is one M, and the
build has to go through. Before the fix the first case built cleanly.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A project that two paths load with different arg lists is two
configurations, each with a variant of its own, and Build() built both.
The consumer's compile got both PCM dirs on its -fprebuilt-module-path,
and clang resolved the module by name to the first for every import,
also for the import inside the other build's BMI consumers, with no
check that it was the build they had been compiled against. With
P -> M and P -> A -> M, where the two lists change a struct M exports,
P compiled against one layout, A against the other, and the program
built cleanly, then read garbage or crashed (#41).
Each configuration now checks, before it makes a directory or builds a
dependency, that no two configurations among it and its transitive
dependencies export a module of the same name, and otherwise fails with
both paths and what tells the builds apart. Going by module name rather
than by project file also catches a checkout on one path and a clone of
the same project on the other. Per configuration, not per invocation:
two builds of one project in different programs, such as a test and a
#pragma crafter dependency tool, are fine.
Picking one of the two builds was not an option: arg lists have no merge
rule, so whichever consumer lost would silently get a different build
from the one it asked for.
Fixes#41
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>
`run -- record -- a` has to run the record script with the args `--`
and `a`: the first `--` ends the options, the second comes after the
script's name and is the script's. The project args keep only what
stood before `run`. And `run -- -x` has to look for a script called
-x, since after `--` an option-like word is an operand. Before the fix
the first `--` showed up in the project args, and `-x` joined it there
while the bare `run` listed the scripts and succeeded.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The help has advertised `[options] [-- project-args...]`, but Run never
looked for `--`. It fell through to projectArgs as a word of its own,
and the words after it were still read as this tool's options and
verbs: `crafter-build -- -v` turned verbose output on. In projectArgs
`--` changed the project's variant hash, and now that siblings inherit
the args, the hash of every sibling down the chain, each building into
directories of its own that the project's clean never removes.
`--` now ends this tool's options and goes nowhere itself. Every later
word is an operand of the verb, as POSIX has it: a project arg for a
build, a glob for test, lint and format, the script's name and then its
args for run. A glob rather than a project arg after `test` because
`crafter-build test -- 'House*'` is how such a filter gets typed; a
project arg goes before the `--`.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The fixture app has `#pragma crafter dependency "../tool"` and loads
../shared for itself, which the tool loads too; the app is loaded with
no args, the case where the two cache keys used to match. With
CRAFTER_BUILD_MARCH=x86-64-v2, the app's copy of shared has to keep
that march and no -fPIC, and the tool's copy has to be host tooling.
Before the fix the app got the tool's copy back: one Configuration,
with march native and -fPIC.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>