Project Operations
Operational guide for contributors and maintainers.
Daily Developer Flow
- Update branch and submodules.
- Run local checks (
flutter analyze, selected tests, docs generation). - Verify at least one target runtime (web, linux, windows, or android).
- Open pull request with clear scope and verification notes.
Quality Gates
Static checks
dart analyze
dart format --output=none --set-exit-if-changed $(git ls-files '*.dart')
Never dart format .. It ignores analysis_options.yaml entirely, so the
recursive walk reaches flutter/, third_party/ and build/ — directories
dart format cannot be told to skip. It is not theoretical: back when the lanes
installed the Flutter SDK inside the mounted workspace, run 33810449411
(2026-09-03) reported Formatted 7404 files (627 changed) with 604 of them
under flutter/. That alone fails --set-exit-if-changed, and it rewrote the
SDK on disk on the way.
Both lanes list tracked files instead — code_quality_find_dart_files on Linux,
Get-ProjectDartFiles on Windows, the same 60 files — which is what the command
above reproduces. Keep the tracked-file listing even now that the SDK comes from
/opt/flutter in the image: a stray flutter/ from an older run is git-ignored
and still on disk, and third_party/ and build/ would be walked regardless.
dart analyze is not affected and never was: it honours the
analyzer.exclude list in analysis_options.yaml, which already names
flutter/**, third_party/** and rust_builder/**. dart format ignores that
file entirely — which is the whole reason the file list has to be built outside
it, and why the two helpers use exactly those three exclusions.
Lint gates (shell, workflows, secrets)
bash scripts/linux/run-lint-gates.sh
The exact command the lint job of dart_on_native_linux.yml runs — shellcheck,
actionlint (plus the CI image-reference check) and the gitleaks secret scan, all
three bootstrapped pinned from ANTfrastructure, all three run even after one fails.
These used to exist only as run: blocks inside the workflow, so a failing merge
gate could not be reproduced locally at all. The gitleaks arm self-tests first: an
empty tree must scan clean and a planted token must be reported and must make the
gate exit non-zero, so "found nothing" cannot be confused with "never ran".
The shared-config drift check is one of that command's gates, not a separate
step: bash scripts/linux/run-lint-gates.sh runs it. The workflow's pwsh
Sync-SharedConfig.ps1 -Check step is the PowerShell twin of the same gate,
kept so both halves stay exercised and are required to agree.
The CMake format gate
The gate is fatal in the native-Linux lane and the Android lane, and
run_cmake_format_check takes no arguments at all: it used to accept a
strictness flag and ignore its own verdict when that flag was false, so it now
errors (exit 2) if handed one rather than letting a stale caller pass silently.
That is safe for a measured reason, not an optimistic one — the gate's 13 files
are already clean and the native-Linux lane has passed --strict-checks true
since fc8b65c, so the Android lane can only catch drift that already blocks the
merge on the other lane. The web lane builds no native CMake code and does not
run it.
The CMake format gate covers hand-maintained CMake only — 13 files today.
Both lanes build the same list (run_cmake_format_check in
scripts/linux/lib/container-steps.sh; the CMake Format Verification step in
Build-Windows.ps1) and both exclude, each verified generated or vendored:
third_party/ and build/; */flutter/CMakeLists.txt (header: "It should not
be edited"); */generated_plugins.cmake ("Generated file, do not edit");
*/ephemeral/ (rewritten on every pub get); */.cxx/ (Android Gradle's CMake
build trees); */.plugin_symlinks/ (pub's junction farm);
rust_builder/cargokit/ (vendored — its README opens with "copied from
Cargokit"); .venv/ (created by the gate's own bootstrap). Do not widen the
gate onto any of those: it would fight the generator or upstream.
.cmake-format.yaml at the root is the consumer copy of ANTfrastructure's
canonical config — shared/config/README.md owns why it is a copy. Refresh it
with pwsh -File third_party/ANTfrastructure/shared/config/Sync-SharedConfig.ps1 -RepoRoot . -Write (or bash third_party/ANTfrastructure/shared/config/sync-shared-config.sh --repo-root . --write) — no -Ignore: what this repo takes is declared in
.antfrastructure-shared.manifest, three rows,
and the scripts refuse -Ignore while that file exists. cmake-format itself
comes from PATH or a
uv venv fed by ANTfrastructure's pinned
third_party/ANTfrastructure/linux/scripts/cmake-format.requirements.txt — there
is no root requirements.txt (pyyaml sits in that pinned set because
cmake-format cannot read its own YAML config without it). Both bootstraps read
that one file: run_cmake_format_check in
scripts/linux/lib/container-steps.sh and the "CMake Format Verification" step
in scripts/windows/Build-Windows.ps1. The config's
line_ending: unix is why .gitattributes pins CMakeLists.txt and *.cmake
to LF — a core.autocrlf=true checkout would otherwise fail --check on every
file.
Tests
flutter test
flutter test integration_test/simple_test.dart
Documentation Workflow
Generate docs:
bash scripts/linux/generate-docs.sh
Preview docs:
dhttpd --path doc/api --host 127.0.0.1 --port 8080
There is no Sphinx site here
docs/source/ is Markdown source for exactly one consumer: the dart doc site
that scripts/linux/generate-docs.sh builds, which stages each guide listed in
that script's DARTDOC_BUILD_GUIDES array. The docs/Makefile, docs/make.bat,
docs/source/conf.py and docs/source/index.rst that used to sit beside it were
scaffolding from sphinx-quickstart that no lane, workflow or script ever
invoked; they were deleted on 2026-09-15 and git history keeps them. Add a guide
by adding the file and one array row, not by reviving a second doc builder.
If a Sphinx site is ever wanted, take the shared theme rather than the
standalone press theme the deleted conf.py named: it lives in
DocumANTation, consumers vendor
it as third_party/DocumANTation and install it through a docs-scoped
requirements file (-e ./third_party/DocumANTation/sphinx-kataglyphis-theme),
and conf.py then reduces to
from sphinx_kataglyphis import setup_theme; setup_theme(globals(), ...).
This repo carries no root requirements.txt: the only thing it ever fed was the
cmake-format gate, which now takes its pinned bootstrap set from
third_party/ANTfrastructure/linux/scripts/cmake-format.requirements.txt. A docs
requirements file would be docs-scoped for the same reason — an unpinned root
file next to a pinned shared one is exactly the drift that removal closed.
Large tracked binaries
Measured 2026-09-15: 148 MiB across 451 tracked files, of which roughly 141 MiB is binary assets. The inventory, so nobody has to re-derive it:
| What | Size | Why it is tracked |
|---|---|---|
dummy_assets/ |
79.7 MiB, 16 files | Fixture corpus mirroring the shape of assets/. 82.9 MiB of it is two PDFs, documents/thesis/{Master,Bachelor}_Thesis.pdf. Nothing references it — not pubspec.yaml's asset list, not lib/, not a test, not a script. It is sample content for trying the document pages against. |
assets/fonts/Noto_Sans/ |
47.9 MiB, 76 files | The complete Noto Sans family as shipped by Google Fonts: 2 variable fonts plus all 72 static faces. pubspec.yaml declares 4 of them (Regular, Italic, Bold, BoldItalic). The other 72 files are the download, not a requirement. |
assets/videos/funnyandsummy.mp4 |
12.3 MiB | Demo clip. Not in pubspec.yaml's asset list and not referenced from lib/. |
assets/icons/kataglyphis_app_icon.png |
1.9 MiB | Load-bearing: flutter_launcher_icons generates every platform icon set from it (pubspec.yaml names it five times), so it must stay at source resolution. |
images/overview.gif |
1.6 MiB | README illustration. |
web/sqlite3.wasm |
0.7 MiB | The sqlite3 WASM build the web app loads. Fetched and checksum-verified by ANTfrastructure's setup-sqlite3-wasm.sh; the committed copy is a convenience, and it has been wrong before (see Getting Started). |
The history is not being rewritten. No git filter-repo, no BFG, no LFS
migration. Four repositories pin this one by gitlink or consume it in a
recursive checkout; a rewrite changes every commit sha and every one of those
pins, plus every clone anyone holds, to save clone time nobody has complained
about. The decision is to document what is there and stop it growing.
Stopping it growing is .gitignore: media, archives, documents and model
weights are ignored by extension. Already-tracked files are unaffected —
.gitignore never untracks anything — so this changes nothing about the table
above. It changes the next one: a new asset the app genuinely ships is added
with git add -f <path> and earns a row here saying what it is for. Having to
type -f is the whole mechanism.
If the tree is ever trimmed, the order is obvious from the table and needs no
history rewrite to be worth doing: the 72 undeclared font faces, then
dummy_assets/, then the video. All three are deletions in a normal commit.
Serving the web build
Two servers, one contract. Both must send Cross-Origin-Opener-Policy: same-origin
and Cross-Origin-Embedder-Policy: require-corp, and both must serve .wasm as
application/wasm — the Stream page checks crossOriginIsolated before using
its SharedArrayBuffer, and WebAssembly.compileStreaming() refuses anything that
is not application/wasm.
| Where | Config |
|---|---|
| The cat-stream demo, any board | scripts/linux/cat-stream/serve.sh (generates its own nginx.conf) |
| The hosted site | nginx.conf + security-headers.conf |
There is no web/.htaccess any more. It was deleted on 2026-09-16: the site
is served by nginx, which never reads one, so every rule in it — the headers, the
wasm MIME type, the caching — was inert. Keeping it was worse than not having it,
because it read like the deployment's source of truth while changing nothing. Two
things it got wrong are worth recording so they are not reintroduced: it sent
Cross-Origin-Embedder-Policy: credentialless, which Safari does not implement,
and it matched \.(js|wasm|…)$ with max-age=31536000, immutable, which covers
main.dart.js, flutter_bootstrap.js and main.dart.wasm — none of which are
content-hashed, so a redeploy would never have reached a returning visitor.
Upstream's nginx config gets both right.
web/index.html also carries three <meta http-equiv> COOP/COEP tags' worth of
explanation and no tags: browsers honour those headers only as real HTTP
responses, never as <meta>, so the ones that used to sit there had never done
anything.
The web lane's CanvasKit source
scripts/linux/ci/ci-container-run-web-linux.sh builds with
--no-web-resources-cdn. The flag defaults to on in flutter build web,
and leaving it on bakes https://www.gstatic.com/flutter-canvaskit/<engineRevision>
into two independent places:
- the
flutter.jsloader's base URL — a single URL shared by the canvaskit and skwasm branches, so--wasmdoes not escape it; and - the
FLUTTER_WEB_CANVASKIT_URLdart-define compiled into the app.
The renderer files ship in build/web/canvaskit/ either way, so the default
buys nothing but a CDN round trip — and the whole point of the cat-stream
runbook is a Raspberry Pi serving build/web on a LAN. A host with no route to
gstatic.com renders a blank page with nothing in the console.
Verify it with the bootstrap key, not with grep:
grep -o '"useLocalCanvasKit":[a-z]*' build/web/flutter_bootstrap.js # => "useLocalCanvasKit":true
The key is emitted only when true, so its absence is the regression.
Do not verify by grepping for gstatic. flutter.js and
flutter_bootstrap.js each keep exactly one hit whether the flag is on or off,
because the URL is a string literal in the untaken arm of the loader's own
ternary:
e.engineRevision && !e.useLocalCanvasKit
? W("https://www.gstatic.com/flutter-canvaskit", e.engineRevision)
: "canvaskit"
useLocalCanvasKit flips that branch to the local canvaskit directory; it does
not delete the string. Measured 2026-09-16 on a --wasm build: 1 hit in each of
those two files before and after, 0 in main.dart.mjs. A "gstatic must be 0"
check would fail forever and teach the next reader that the flag does not work.
CI/CD Notes
- Linux native, Windows native, Web, and Android pipelines are available via GitHub Actions.
- Keep generated artifacts deterministic to reduce CI diffs and flaky builds.
- Prefer script-driven commands from
scripts/over ad-hoc commands for reproducibility.
Release Hygiene
- Keep dependency upgrades and feature changes in separate pull requests.
- Regenerate bridge code when Rust API signatures change.
- Update docs in the same pull request for any user-facing behavior changes.
Dependency upgrades, in detail
AGENTS.md § 5 (Build, run, test) has the commands and the report-first rule. These are the behaviours that surprise people.
Why the container still downloads its own Node. Renovate's engines.node
range excludes the image's Node, so the bootstrap pulls a checksum-pinned one
onto the kataglyphis-renovate-cache volume — the versions and the rationale
are ANTfrastructure's:
third_party/ANTfrastructure/docs/dependency-updates.md.
The runner passes gh's token as GITHUB_COM_TOKEN when gh is
authenticated. Without it Renovate's GitHub API lookups are rate-limited and it
can report stale GitHub Actions as up to date — DocumANTation's action majors
were invisible until a token was supplied.
Renovate is a local CLI and only detects — --platform=local cannot write —
so the --apply half is this repo's own code: git for gitlinks and a located
line rewrite for the manifests it reported. Managers default to every manager
whose file patterns match this tree (eight today), so --managers narrows the
run rather than enabling it. --apply needs the git that wrote the working
tree; the script sorts that out itself and refuses up front rather than
half-applying. Why any of it —
third_party/ANTfrastructure/docs/dependency-updates.md.
Troubleshooting
The Linux lane, locally
scripts/windows/Invoke-LinuxLane.ps1 runs the same image, script and arguments
as the Linux workflows; AGENTS.md § 5 (Build, run, test) holds the lane table and the rules. What
follows is the evidence behind those rules — each cost at least one run to find,
and each has a symptom that names something other than its cause.
arm64 locally needs QEMU registered once per VM boot. Rancher's VM starts
with no emulators at all — binfmt reports "emulators": null and only
linux/amd64 variants under supported, so an arm64 container would run
x86-64 binaries and die exactly as CI did before the image went multi-arch
(rustc: 1: ELF: not found). Register it with:
nerdctl run --rm --privileged tonistiigi/binfmt --install arm64
nerdctl run --rm --privileged tonistiigi/binfmt # verify: qemu-aarch64 listed
nerdctl run --rm --platform linux/arm64 alpine uname -m # verify: aarch64
Like the D: mount in containerd's namespace, this does not survive a VM
restart. The arm64 layers are a separate pull — about 6 GB over the wire, 30 GB
on disk next to the amd64 copy (nerdctl pull --platform linux/arm64 …) — and
every compile then runs under emulation, so expect it to be far slower than the
native x64 lane.
Emulated arm64 produces tar and deb, never flatpak or AppImage. Both fail
inside qemu-user, for reasons that have nothing to do with this repo or the
image, and both were verified 2026-09-05 after a full arm64 build that compiled
Rust and C++ without a single error:
bwrap: Creating new namespace failed, likely because the kernel does not support user namespaces— the kernel does support them (/proc/sys/user/max_user_namespacesis 123100) and--privilegedis passed; qemu-user simply does not carryunshare(CLONE_NEWUSER)through, and flatpak-builder sandboxes every module with bubblewrap./usr/local/bin/appimagetool: cannot execute binary file: Exec format error— the binary is the correct architecture (ELF aarch64, static-pie linked); qemu-user cannot load static-PIE executables.
CI is unaffected: its arm64 row runs on a real ubuntu-26.04-arm runner, so
neither restriction applies. Locally, treat a failing flatpak/AppImage step on
arm64 as expected and check the two messages above before investigating.
error: fchmod after Pruning cache is not the prune. That combination cost
hours. Pruning cache is merely flatpak-builder's last output line; it exits
0. The error underneath came from flatpak build-bundle, which chmods the file
it writes — and that file was out/…flatpak, on the host mount. The bundle is
now written under /tmp/flatpak-work and copied out afterwards. set -x around
the function answered this in one run, after three rounds of eliminating
plausible-looking causes had only moved the symptom.
The step also does not gate on flatpak-builder's exit code any more. It asks
ostree --repo=<repo> refs whether the app is committed, because the export can
be complete while a later stage fails. The exit code is reported in the warning,
never used as the verdict.
All four formats build locally on x64: tar, deb, flatpak and AppImage. The appimagetool mode-711 problem that used to break the last one is fixed in the image.
-v name:/path is not a named volume on Windows nerdctl. It is a bind of
$PWD/name, created silently, and nerdctl volume create beforehand changes
nothing — the volume is made and never mounted. Proof: after five lane runs,
%TEMP% held kataglyphis-lane-native-x64-workspace-build/ and four siblings,
729 MB each, while the volume of that name mounted through
--mount type=volume,… was empty. One run started from the repo root even left
a 151 MB directory of that name in the checkout. So the whole point of the
volumes — keeping the write-heavy build tree off drvfs — was never in effect
locally, and the failures it prevents were only avoided because the packaging
steps had already been moved to /tmp. Invoke-LinuxLane.ps1 now always uses
--mount type=volume,source=…,target=…, which nerdctl cannot reinterpret as a
path. CI is unaffected: there the Linux engine resolves the short form
correctly.
flutter clean then logs Failed to remove /workspace/build … Device or resource busy (errno 16) on every run and keeps going: it empties the
directory but cannot unlink the mount point itself. Cosmetic, and the direct
consequence of mounting build/ — not a failure to chase.
There is no separate host-side driver any more. The legacy
ci-dart-on-native-linux.sh / ci-dart-build-android-app.sh pair and their
ci-common.sh were removed on 2026-09-04: no workflow ever referenced them,
they carried a third copy of the CodeQL logic, and they re-implemented what CI
actually runs instead of invoking it. Use Invoke-LinuxLane.ps1 (§ 5), which
runs the very script CI runs.
**Flutter comes from the image, and this repo does not have an opinion about
Why it went: the lanes were re-running ANTfrastructure's setup-flutter.sh at
run time. That script is a build-stage script — its last step strips
bin/cache on purpose — so every Android run re-extracted Flutter over the
image's copy and then re-downloaded the 227 MB Dart SDK it had just deleted.
Upstream now returns early when the requested version is already bootstrapped,
and this repo no longer calls it at all.
That flip is safe for a measured reason, not an optimistic one: the gate's 13 files
are already clean under this repo's .cmake-format.yaml, and the native-Linux lane
has passed --strict-checks true since fc8b65c — so any drift the Android lane now
catches is drift that already blocks the merge on the other lane. Wrap the call in
run_gate when you want the batch to decide, rather than reaching for a flag.
--flutter-dir only says where to look; it defaults to /opt/flutter. There
is no --install-flutter and no --flutter-version — see Flutter comes from
the image below.
which version.** It used to: three lanes resolved FLUTTER_VERSION and
FLUTTER_SDK_SHA256 out of ANTfrastructure's versions.env, exported the sha, and
handed both to an installer. That machinery is gone — resolve_flutter_pin,
setup_flutter_sdk, install-flutter.sh and the --flutter-version /
--install-flutter flags with it. ANTfrastructure's
flutter_lane_prepare_env replaces all of it: it returns non-zero if
--flutter-dir holds no bin/flutter, and otherwise puts the SDK on PATH,
registers the workspace safe.directory, sets PUB_CACHE and prints the
flutter --version this run got. Until 2026-09-15 this repo did the same three
things itself, in scripts/linux/lib/container-steps.sh and in a different
order per lane.
To change the Flutter version, change the image.
flutter_rust_bridge Version Mismatch
If you encounter this error at runtime:
oxidant's codegen version (2.11.1) should be the same as runtime version (2.12.0)
Cause: The generated Dart binding files (in lib/src/rust/) are out of sync with the pubspec.yaml dependency version.
Fix: Regenerate the Flutter Rust Bridge bindings:
flutter_rust_bridge_codegen generate
It is a cargo binary baked into the build image, not a pub dependency — on a
bare host install it first, at the version this repo pins rather than at latest:
cargo install --locked --version 2.13.0 flutter_rust_bridge_codegen (the
flutter_rust_bridge pin in third_party/OxidANT/Cargo.toml; the image exports
the same number as FLUTTER_RUST_BRIDGE_VERSION). Installing latest is how the
mismatch above happens in the first place.
Then rebuild the project:
# For Windows
.\scripts\windows\Build-Windows.ps1 -BuildRootDir build
Prevention: Always regenerate bindings after updating flutter_rust_bridge version in pubspec.yaml or modifying Rust API signatures.
Contribution Checklist
- [ ] Scope is focused and documented.
- [ ] Build/test commands were run locally.
- [ ] Relevant docs were updated.
- [ ] No secrets, machine-specific paths, or temporary artifacts were committed.