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.
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>
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 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>
Master's last two runs (#368 and #370) failed in the Build step with
The state dir (/root/.cache/flatpak-builder) is not on the same
filesystem as the target dir (/workspace/Catcrafts/Crafter.Build)
while #369, the 2.2.0 tag run on the same commit with the same
flatpak-builder, passed. The difference was the runner. Since 14 October
the Hetzner box has a second privileged runner, cx33-org-flatpak, which
serves every Catcrafts repository under the label `flatpak` so their
package jobs can run crafter-build as a Flatpak. It mounts no host
volumes, deliberately, since any repository in the organisation can send
it jobs. Whenever it was idle first it took this job too, logged our four
volumes as "not a valid volume, will be ignored", and left the
flatpak-builder state on the container's overlay while the workspace sat
on a Docker volume. Even past that check it would have had no build
cache, no web root and no signing key.
The org runner's label is right for the jobs it was made for, and giving
it our volumes would hand the signing key to every repository in the
organisation. So the job asks for a label only its own runner carries:
`crafter-flatpak`, after the volumes and key directory that runner alone
allows. The runner on the host is relabelled to match
(/etc/forgejo-runner/config-flatpak.yaml, previous copy kept as
config-flatpak.yaml.bak-2026-10-31-label), so until this reaches master
a push there with the old `runs-on: flatpak` goes only to the org runner
and fails the same way.
AGENTS.md now says the label must stay unique and quotes the error a job
on the wrong runner stops with, so the next collision is found from the
log.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
flatpak-builder printed "Building module llvm" on every CI run instead
of "Cache hit". The build environment, CRAFTER_BUILD_VERSION included,
sat in the manifest's top-level build options, and those are part of
every module's cache key; the version changes with each commit, so no
module ever hit. Runs still took twelve minutes rather than eighty only
because ccache made the LLVM rebuild cheap.
The variables only matter to crafter-build's own module, so they move
there, and CI rewrites the version on that module. LLVM, the WASI
sysroot and wasmtime are now keyed on nothing that changes between
commits.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Every non-tag build called itself 0.0.0-dev+<hash>, a placeholder that
said nothing about which release it came after and put a `+` in the
bundle's download URL. A master build now uses `git describe`, as a
local checkout already does: 2.0.0-3-g21746b1 is three commits past
2.0.0. The placeholder remains only for history that has no tag yet.
Three more things the tag path needed before its first run. The
forgejo-release action takes the notes text in `release-notes` and a
path in `release-notes-file`; the workflow passed a path to the former,
so the release body would have been the file name. The tag filter is
spelled with globs so it reads the same as a workflow pattern or a plain
glob. Python, used to extract the notes from the metainfo, was only in
the container as somebody else's dependency and is now listed. The
2.0.0 entry carries the date it is tagged on.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The publish step created the OSTree repo only when the directory was
missing. On the runner host the web root exists before the first
publish, as an empty directory, so the init was skipped and
build-commit-from failed with `opendir(objects)`. The check now looks
for the repo's config file.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The runner is our own machine, so the repo signing key does not need to
pass through the forge as a secret. It is generated once under
/etc/crafter-flatpak on the host and bind-mounted read-only into the job
container, where each run imports it into a throwaway keyring. One less
secret to manage, and the private key never leaves the box that uses it.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
One job replaces the tarball pipeline. On a pull request it builds the
Flatpak with flatpak-builder, which runs the bootstrap, the self-host
build and the test suite inside the sandbox, and smoke-tests the result
with `crafter-build --version` and the hello-world example. On a push to
master it also bundles the build, promotes it into the live repo as the
`beta` branch and refreshes the rolling `latest` prerelease. On a tag
X.Y.Z it promotes as `stable`, refuses to proceed unless the metainfo's
newest release is that version, and publishes a release whose notes are
that entry's description.
The job runs on the `flatpak` runner label: the privileged forgejo-runner
on the Hetzner box, with the Sdk install, the flatpak-builder state and
the repo's web root as volumes. The state volume is what keeps a normal
run short: flatpak-builder rebuilds the LLVM and WASI modules only when
their sources change. The live repo is canonical and never built into;
builds are promoted with build-commit-from, then build-update-repo signs
and prunes it and generates static deltas.
The musl job, the per-march tarballs, the toolchain packages the runner
used to install and the PKGBUILD go: the Flatpak is the one artifact.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
README and AGENTS.md stop documenting build.cmd, LIBCXX_DIR, the
DLL-plus-launcher design rule and the cmd-over-SSH gotcha. The runner
section also drops Ssh, SshWin and Wsl, which were removed from the
code earlier but still had README entries, and the stale pointer to a
deleted cross-arch test. ci.yaml loses the commented-out Windows build,
staging and upload steps and the comments that counted them.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The Linux host code paths were gated on the x86_64_pc_linux_gnu target
define alone, and LoadProject hardcoded --target=x86_64-pc-linux-gnu when
compiling project.cpp and the module PCMs. Both break on musl hosts: the
glibc release launcher cannot run there at all (no glibc loader; gcompat
lacks libgcc_s and the __isoc23_* symbols), and a musl-built launcher
would still compile project.cpp for the wrong libc.
- The host gates test clang's __linux__ instead of one triple's define:
they mean "Linux host", whatever the libc (or, later, the CPU).
- LoadProject derives the host triple from HostTarget() (clang's
-print-target-triple), which is also more correct on glibc distros
whose triple isn't x86_64-pc-linux-gnu.
- project.cpp / the cmake libc++ flags treat any *-linux-gnu or
*-linux-musl target as Linux.
- build.sh targets the host toolchain's triple (CRAFTER_BUILD_TARGET
overrides) and derives the per-target define from it. On Arch this is
byte-for-byte the previous behaviour.
- CI: a release-musl job bootstraps on alpine:edge, runs the tests, and
attaches crafter-build-linux-x86_64-musl-v2.tar.gz to the rolling
latest release (via the API, so the glibc assets survive). Dynamic
against musl libc++: the launcher dlopens project.so, so it cannot be
fully static.
First consumer: imsd's package CI, which cross-compiles for aarch64 from
an Alpine container.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
PRs now stop after bootstrap + tests. The per-march variant builds,
packaging, and upload-artifact steps reuse the same guard the rolling
'latest' tag/release steps already had, so artifacts are only produced
on push to master (or manual workflow_dispatch).
Replaces the single Linux + single Windows tarball with six self-
contained variants — three µarch levels × two OSes. End users on the
Releases page see them as six individually-downloadable archives:
crafter-build-{linux,windows}-x86_64-{v2,v3,v4}.{tar.gz,zip}
Pick the v-level your CPU supports:
v2: SSE4.2 baseline (every x86_64 since ~2011)
v3: AVX2 + FMA + BMI (Intel Haswell+, AMD Excavator+, ~2013+)
v4: AVX-512 (Skylake-X+, recent server-class)
How: the bootstrap binary stays at v2 because the CI SBC (N5105 / Tremont,
no AVX) can't execute v3 or v4 instructions and would crash before it
could rebuild itself for higher levels. Once bootstrap's done, the same
v2 binary is re-invoked with CRAFTER_BUILD_MARCH overridden per variant
to produce v2/v3/v4 outputs. Same again with --target=x86_64-w64-mingw32
for the Windows variants. ExternalDependency cache keys on (url, target,
march) already so glslang gets built per variant; subsequent CI runs hit
the cache.
Also drops the wrapper zip from workflow artifacts: instead of one
upload-artifact call producing crafter-build.zip containing six archives,
there's now one call per archive. The PR / run page shows six small
downloads named for what they are.
forgejo-release continues to upload release-dir/* as individual assets,
so the Releases page already has the right shape — this just brings the
workflow artifact UX in line.
Bumped the glslang cache key to v2 (one-time invalidation) since the
cache layout now needs space for six glslang builds rather than two.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Setting CRAFTER_BUILD_MARCH=x86-64-v2 made crafter-build emit the
mingw cross-compile to bin/crafter.build-exe-x86_64-w64-mingw32-x86-64-v2/
but the package step still hardcoded -native, causing cp to fail.
Use ${CRAFTER_BUILD_MARCH} in the path so workflow env and packaging
stay in sync.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two CI fixes from run #276 (got all the way through tests + mingw cross-
compile + packaging, only failed on artifact upload):
march: workflow now sets CRAFTER_BUILD_MARCH=x86-64-v2 / MTUNE=generic;
project.cpp reads both and applies them to the lib + exe Configurations
so the self-rebuild and mingw cross-compile honor the same baseline.
v3 is unusable on the runner — Intel N5105 (Tremont) has no AVX2, so
a v3 bootstrap binary wouldn't even start. v2 (SSE4.2) runs on the SBC
and on every x86_64 CPU since ~2011.
upload-artifact: pinned to v3. v4+ uses a GHES-only API that Forgejo
Actions doesn't implement; the v3 action stays on the older API that
Forgejo supports.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
archlinux:latest slim image has no local pacman master key and an
unpopulated upstream keyring, so:
- the archlinux-keyring upgrade fails with "no secret key available to
sign with" because pacman can't sign the keyring it's rewriting
- falling through to -Syu hits the original "unknown trust" errors on
libseccomp and zip
Run pacman-key --init then --populate archlinux before any pacman -S.
This is the documented bootstrap for slim Arch CI containers.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
archlinux:latest container ships a snapshot keyring; packages signed by
keys added after the snapshot date fail PGP verification (zip-3.0-13 hit
this with a "signature from Robin Candau is unknown trust" error). Update
the keyring first via pacman -Sy archlinux-keyring, then -Syu the rest.
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>