Shared script libraries (linux/scripts/lib/)#

Sourceable, project-agnostic cores. Nothing about a specific project is hard-coded in any of them. The contract is always the same:

  1. A thin wrapper script sets that library’s *_DEFAULT_* / *_* variables — its project defaults.

  2. It optionally declares hook functions.

  3. It sources the library and calls the library’s *_main.

None of them set -e/-u/-o pipefail: sourcing must not change the caller’s shell options, and wrappers are expected to run under set -euo pipefail themselves. Anything the wrapper does not provide is discovered from the environment — logging from 01-core/logging.sh (or minimal fallbacks), job computation from 01-core/parallelism.sh, tool presence and the Vulkan environment from the caller’s own has_tool/require_tools/source_vulkan_env when it declares them.

Three have their own pages, because their topic is bigger than the library: code-quality.sh, slang-compile.sh, and the agentic-loop.sh + agentic-engines.sh pair, one executable loop in two files (agentic-loop-build-matrix.md).

The tree, as AGENTS.md drew it#

Moved out of AGENTS.md on 2026-09-15 (owner decision D10), and edited since only to keep it current. AGENTS.md keeps one line per top-level directory; the per-file detail, the deletion history and the consumer-surface notes are here.

linux/scripts/
├── 01-core/             shared utilities (62 as of 2026-09-01 — `ls linux/scripts/01-core/*.sh | wc -l`; the literal said 48 for long enough that README repeated it, so treat any count here as indicative: versions.env, logging, platform, cross-env, cross-gcc, cross-meson, cross-apt, compiler-resolution, tag-naming, stage-defs, digest-pinning, ancestry, build-helpers, cli-parsers, …)
├── 02-toolchain/        GCC, LLVM, Rust, Python, CMake, Vulkan builds
├── 03-media/            media library build scripts
│   ├── core/common.sh   single DRY bootstrap — sourced by every media script
│   ├── build/           per-library build scripts
│   │   ├── onnxruntime/   ONNX Runtime + GenAI (build/ steps, runtime/ pkgconfig, android/)
│   │   ├── litert/        LiteRT + TFLite C API (Critical Fix #2: abseil span.h copy in build-litert.sh)
│   │   ├── opencv/        OpenCV 5.x
│   │   ├── ffmpeg/        FFmpeg (build-ffmpeg.sh has fixed host compiler wrapper)
│   │   ├── gstreamer/     GStreamer monorepo (common/ has patch-gstreamer-sources.sh — Critical Fix #5)
│   │   ├── libcamera/     libcamera
│   │   ├── pyav/          PyAV wheel (`import av`), built in a stage layered on the FFmpeg it links against (Dockerfile.media `FROM ffmpeg AS pyav`); versions.env pinned PYAV_VERSION while nothing built it until 2026-08
│   │   ├── armnn/         Arm NN + Arm Compute Library — arm64 ONLY; other arches get empty /opt/armnn + /opt/acl
│   │   ├── hailo/         HailoRT + hailortcli + the hailonet element, built in the wrapper image (Dockerfile.torch); riscv64 skips
│   │   └── iree/          android/ ONLY (dispatched via android-dispatch.sh); the Linux-lane IREE is built by 05-frameworks/torch/build-app-wheelhouse.sh
│   └── runtime/         artifact collection, runtime config, wheel repair, verification, media-env.sh (canonical ENV)
├── 04-runtime/          entrypoint + env scripts (gstreamer-env.sh, etc.)
├── 05-frameworks/       TVM, Torch, Flutter
└── 06-packaging/        assembly + smoke tests (smoke-media.sh, smoke-common.sh)

Top-level orchestrators: build-cross-chain.sh, build-cross-compiler.sh, build-cross-stage.sh, build-runtime-manifest.sh, build-runtime-artifacts.sh. Verification: verify-cross-chain.sh, verify-critical-fixes.sh, verify-artifact-copy-parity.sh.

Beyond linux/scripts/:

linux/scripts/lib/       consumer-facing bash libraries: agentic-loop.sh +
                         agentic-engines.sh (one loop in two files),
                         app-packaging.sh (tar/deb/AppImage/Flatpak),
                         app-runner.sh (generic app launcher: arg parse, exe
                         discovery, LD_LIBRARY_PATH, per-profile hooks),
                         cmake-build.sh, code-quality.sh, coverage.sh,
                         compiler-llvm-tools.sh (a build tree's own LLVM
                         tools, first on PATH), dartdoc-build.sh (+
                         dartdoc-guides.py), linker-select.sh (opt-in
                         KATAGLYPHIS_LINKER=lld|mold), log-bootstrap.sh (the
                         logging block the others source), slang-compile.sh,
                         wasm-opt.sh, ctest-run.sh (ctest runner),
                         docs-build.sh (Sphinx build helper),
                         rust-toolchain.sh — the last three had NO doc entry
                         anywhere until the 2026-08-08 orphan sweep. A
                         consumer surface, reached through the consumer's
                         third_party/ANTfrastructure checkout: no image
                         ships lib/, and in-repo only preflight.sh and
                         05-frameworks/flutter/flutter_checks.sh source it
                         (code-quality.sh)
linux/scripts/02-toolchain/rust/   cargo_* helpers (test/bench/debug/
                         fmt+clippy/security/coverage/release/doc; test,
                         bench and debug run through _cargo_wrapper.sh) —
                         consumer surface COPY'd into the
                         toolchain/sdk/package images; nothing in-repo calls
                         them (the redundant zero-ref Build-Linux.sh duplicate
                         was deleted 2026-08-08)
linux/scripts/02-toolchain/python/ci_*.sh   Python CI helpers (tests, static
                         analysis, packaging, docs) — same consumer-surface
                         status as rust/
linux/scripts/01-core/setup-host-deps.sh    hand-run developer-onboarding
                         helper (host toolchains, compiler caches, analysis
                         and coverage tools); intentionally not wired into CI
                         or builds
linux/scripts/06-packaging/package_archive.sh   tar/deb/AppImage/Flatpak
                         assembly — consumer surface. Called from
                         OxidANT's reusable-linux.yml, which its Linux
                         lanes (linux-x64.yml, linux-arm64.yml) call;
                         rust_ubuntu26_04.yml until the 2026-09-24 rename.
                         Deleted by the 2026-08-08 orphan sweep as
                         "zero-reference" and restored 2026-08-11: the sweep
                         searched only THIS repo, so a consumer's CI lane was
                         broken silently. Grep the consumer repos before
                         deleting anything under lib/, rust/, python/ or
                         06-packaging/.
windows/scripts/         Windows lane, GROUPED since #108 (2026-08-20):
                         build/ (chain components: Build-*FromSource.ps1,
                         Build-*All.ps1 wrappers, Test-Container.ps1,
                         Import-Versions.ps1), host/ (Install-*/Set-*/
                         Repair-*/Reset-* + elevated maintenance),
                         diagnostics/ (Test-*/Get-*/Invoke-*/Measure-* probes
                         + the Invoke-DiagnosticProbe.ps1 runner). A settled
                         one-shot is DELETED, not archived: git history is the
                         record, and the diagnostics/archive/ facility that
                         used to hold them never worked as advertised —
                         `**/archive/` in .dockerignore strips it from every
                         build context, so the -ProbeScript archive/<name>.ps1
                         it promised could not solve. Container mounts
                         stay FLAT (C:\bkmnt, C:\temp\scripts) — the
                         $scriptAssetRoot resolver bridges both layouts and
                         is gated by ScriptAssetRoot.Parity.Tests.
                         Ungrouped residents BY DECISION (#131):
                         Invoke-Lint.ps1, entrypoint.cmd, cargo-retry.cmd
                         (consumer-CI suspect — never delete unverified),
                         certificates/ (MSIX cert generation + WebDAV
                         download_webdav_files.py — see its README.md),
                         python/ + rust/ (consumer CI-lane drivers).
                         Also ungrouped: patches/ (the static source-build
                         patches, catalogued in its README.md).
                         modules/*.psm1 (reusable PS modules: SourceBuild,
                         Build.Common, ContainerBuild.Reuse, AgenticLoop,
                         CMake, Config, Formatting, Msix.{Common,Signing},
                         WebDav, Uv, Scripts.Shared, Toolchain, CodeQL,
                         ContainerImage, CrossBundle, Flutter, Installer,
                         HostMaintenance, SmokeTest, GstPlugins, …),
                         tests/ (harness + suites), shims/
windows/upstream/        prepared upstream submissions (not build inputs), one
                         directory per submission = format-patch + PR.md. See
                         its README.md for the index, and
                         docs/upstream-windows-patches.md for the graded
                         register of EVERY local third-party change.
                         hcsshim-teardown-timeout/ is the one already FILED
                         (microsoft/hcsshim#2855) and also carries ISSUE.md,
                         the deployed 45min local patch and the rebuild
                         recipe; the other 14 are prepared and UNSENT - do
                         not post without the owner saying so.
shared/agentic-loop/     cross-platform data: prompts/*.md — the single source
                         for the default planner/refactor-planner/executor task
                         prompts read by BOTH WindowsAgenticLoop.Common.psm1
                         and linux/scripts/lib/agentic-loop.sh;
                         system-prompts/*.md, the role prompts both halves
                         compose (agentic-engines.sh on Linux); templates/,
                         the consumer config and runner wrappers
.github/actions/         12 composite actions consumers call @develop, incl.
                         cleanup-disk-space (Windows runners),
                         run-in-linux-container, run-in-windows-container;
                         full list in .github/actions/README.md

out/: generated build artifacts (OCI layouts, rootfs exports). Excluded from Docker context via .dockerignore.

Module loading order, as AGENTS.md carried it#

Moved out of AGENTS.md on 2026-09-15 (owner decision D10), and edited since only to keep it current. The RULES stayed there; this is the reference behind them.

artifact-common.sh sources 01-core modules in dependency order:

  1. common.sh 2. tag-naming.sh 3. stage-defs.sh 4. digest-pinning.sh 5. chain-verify.sh 6. ancestry.sh 7. build-helpers.sh 8. cross-stage-build.sh 9. context-management.sh 10. version-forwarding.sh 11. cli-parsers.sh 12. runtime-build-fns.sh 13. compiler-resolution.sh 14. parallel-loop.sh 15. path-helpers.sh. abseil-headers.sh is deliberately NOT in this loop (backlog A3, 2026-08-12): it has no host-side caller, and its in-image consumers load it via source_module.

runtime-flow-common.sh is sourced by lib-orchestrator.sh inside runtime_flow_preamble(); build-runtime-artifacts.sh and build-runtime-manifest.sh reach it by sourcing lib-orchestrator.sh.

lib-runtime-wheels.sh (RUNTIME_WHEELS_SOURCE) is sourced by lib-orchestrator.sh at top level, right after artifact-common.sh. It sits beside 01-core, not in it, because 01-core is in the compiler image’s closure and this file is host-only. append_wrapper_build_args calls into it only while RUNTIME_WHEELS_EXPORT_ROOT is set, which only its own runtime_wheels_setup does (both deliveries).

03-media/build/hailo/hailo-build-lib.sh is sourced there too, right after it, for one function: hailo_validate_knobs, which the chain and both runtime orchestrators call before they build anything. The same file is the Hailo build’s own library inside the wrapper image, so it sets no shell options and runs nothing at load, and a change to it re-keys only the wrapper’s Hailo RUN (two switches).

Which loader a NEW script should use (the dual-loader rule): scripts that also execute INSIDE containers (bind-mounted or COPY’d — base-image, 02-toolchain, 03-media, 06-packaging) load via modules.sh / source_module, which resolves both the repo layout and the /opt/scripts container layout. Host-only orchestration (build-cross-*.sh, runtime flows) sources artifact-common.sh directly. Do not mix: a container-capable script hard-sourcing repo paths breaks at /opt/scripts. (Known wart: modules.sh hardcodes a ../02-toolchain search path — a 01-core file encoding stage-2 layout; fold a fix into any future modules.sh touch. The layer order itself is frozen by tests/test-layer-order.sh.)

What holds the standalone contract#

Two suites, and they split the work. tests/test-lib-smoke.sh is the cheap half over every lib/*.sh: the module parses (bash -n), it sources cleanly under set -euo pipefail — a strict-mode consumer must not be killed by an unbound variable or a failing top-level command — and sourcing it defines at least one function, counted as a delta inside one shell so functions exported into the test environment cannot fake the number. A module that exports nothing is a gutted or early-returning copy, not a library. It also parses cmake_build_parse_args in isolation. No network, no cmake run.

tests/test-lib-modules.sh is the strict half described under The logging bootstrap: double-source safety, and that info/warn/err arrive from the real 01-core/logging.sh rather than a private fallback copy. It skips the agentic-*.sh pair, one executable loop split over two files rather than a source-library.

The logging bootstrap#

log-bootstrap.sh is the one owner of the block every other library needs before it can say anything: resolve ../01-core/logging.sh if the caller has not already defined info, and otherwise define the minimal info/warn/err that let the library run standalone. Each library sources it on the line after its own re-source guard; only cmake-build.sh and wasm-opt.sh keep a _*_CORE_DIR of their own, because they reach into 01-core for parallelism.sh, load-versions-env.sh and downloads.sh as well.

It is a separate file, and not an idiom pasted into each library, because nine hand-kept copies had already drifted twice, and both drifts were defects (complexity audit F-A): app-runner.sh carried no re-source guard and never attempted the real 01-core/logging.sh, so standalone consumers silently got the minimal fallbacks — no log, no die, different formatting — forever; rust-toolchain.sh had no guard either and defined no err, so an err call would have inherited whatever the caller happened to have, or exploded. No duplication gate could catch that: at nine owners every shingle of the block lands in verify_code_dupes’ suppressed as idiom at >6 owners bucket (MAX_OWNERS = 6), which is why the copies were free to rot.

Sourcing a sibling to get logging is not the bootstrap paradox it looks like. The block it replaced already sourced a file — ../01-core/logging.sh, one directory further away — and every consumer vendors the whole ANTfrastructure checkout (third_party/ANTfrastructure/linux/scripts/lib/<lib>.sh), so a missing file next to the library it serves is a broken checkout, not a supported state. tests/test-lib-modules.sh holds that line: every lib/*.sh must source cleanly standalone, define info/warn/err, survive a double source, and end up with the real logging module rather than the fallbacks.

cmake-build.sh — configure + build a CMake project in a container#

Variable

Meaning

Default

CMAKE_BUILD_DEFAULT_PRESET

CMake preset name

—

CMAKE_BUILD_DEFAULT_BUILD_DIR

build directory

build

CMAKE_BUILD_DEFAULT_CLEAN_BUILD_DIR

true to rm -rf the build dir first

false

CMAKE_BUILD_DEFAULT_SKIP_CONFIGURE

true to build without configuring

false

CMAKE_BUILD_DEFAULT_VULKAN_SETUP_SCRIPT

setup-env.sh sourced when it exists

—

CMAKE_BUILD_DEFAULT_MB_PER_JOB

peak RAM per compile job

4000

CMAKE_BUILD_DEFAULT_ALLOW_PREBUILD_FAILURE

true makes a failing pre-build hook non-fatal

false

CMAKE_BUILD_DEFAULT_CONFIGURE_ARGS

bash ARRAY of extra configure-step arguments

empty

CMAKE_BUILD_SAFE_DIRECTORY

path registered as a git safe.directory; empty disables

/workspace

CMAKE_BUILD_PREBUILD_LABEL

label logged around the pre-build hook

—

CMAKE_BUILD_USAGE_INTRO

one-line description shown in --help

—

Vulkan selection — _cmake_build_resolve_vulkan. Three sources can name a Vulkan SDK, and they are resolved in one place: an explicit --vulkan-version / --vulkan-setup-script / --vulkan-sdk flag overwrites whatever the image exported, and CMAKE_BUILD_DEFAULT_VULKAN_SETUP_SCRIPT is consulted last — only when nothing else set VULKAN_SETUP_SCRIPT and the file it names exists. That -f test is the load-bearing half: cmake_build_prepare_env sources VULKAN_SETUP_SCRIPT unconditionally once it is set, so adopting a default that is not on disk turns a missing SDK into a sourcing error much later. tests/test-lib-smoke.sh pins all four cases.

Extra configure arguments — --configure-arg. Repeatable, and the only way to put a -D on the configure command line. Each occurrence appends one argument, in order, to CMAKE_BUILD_CONFIGURE_ARGS; the list is reset per cmake_build_parse_args call and seeded from CMAKE_BUILD_DEFAULT_CONFIGURE_ARGS (an array, not an environment string — splitting one would break the first -D whose value contains a space, and --configure-arg '-DCMAKE_CXX_FLAGS=-O2 -g' must stay one argument). They reach the configure step only, never cmake --build. An empty value is fatal rather than dropped: cmake "" fails with a message about the source directory, nowhere near the caller. Without this, a wrapper that needed one -D had to run --skip-configure true and issue its own cmake -B … --preset …, i.e. call three library entry points where cmake_build_main would do.

Hook — cmake_build_prebuild_hook. Called only when the wrapper declares it. Runs after configure, immediately before cmake --build; use it for code or asset generation the build or the runtime depends on (shader precompilation, codegen). A non-zero return is fatal by default — see cmake_build_run() for why.

app-packaging.sh — the two flatpak entry points#

Every packager in the file obeys three rules. Staging trees are container-native, never inside the mounted workspace: a bind-mounted host drive refuses chmod/fchmod for the container uid, so dpkg-deb, appimagetool and flatpak-builder fail there, and only finished artifacts are copied into out/. flatpak-builder’s exit code is reported but never the verdict; ostree --repo=<repo> refs decides whether the app is committed. And nothing prints Created: without app_packaging_assert_artifact. Two entry points were added on 2026-09-15 for a consumer that packages a CMake install tree rather than a Flutter bundle; the finish-args block and the CON2 probe landed on 2026-09-17:

  • app_packaging_ensure_flatpak_runtime [arch] [runtime] [sdk] [version] — flathub plus the runtime/SDK pair, for a run that is not inside the family CI image. Deliberately not app_packaging_setup_dependencies_for_container: that one assumes the image already ships flatpak, installs missing packages through a privilege helper and installs under dbus-run-session. This one installs nothing with apt and asks first, so a warm dev box is a no-op. User installation first, system as the fallback, for the remote and for both refs — an unprivileged user cannot write the system installation, and pulling a second copy of a ref the machine already has system-wide costs 1-2 GB. app_packaging_setup_dependencies_for_container now probes both scopes before installing too (CON2, 2026-09-17): the CI image installs the pair system-wide as root while the container runs as uid 1001, so its unconditional --user install was pulling that second copy on every flatpak-packaging run.

  • app_packaging_flatpak_finish_args_block — the manifest’s finish-args list: the four generated entries (network, the two display sockets, DRI) plus whatever KATAGLYPHIS_FLATPAK_FINISH_ARGS adds, space-separated. Appended, never replaced (CON6, 2026-09-17). V4L2 capture needs --device=all (there is no per-device video grant in flatpak) and a user-chosen model path needs a --filesystem=; the knob exists because the omission is invisible to every gate — the bundle ships, the window opens, and only the camera stays silent.

  • app_packaging_package_cmake_install_flatpak <build_dir> <out_dir> <app_id> <project_name> <version_suffix> [runtime] [sdk] [version] [branch] [arch] — cmake --install into a container-native staging prefix, rename the PROJECT-named .desktop/.png/.appdata.xml to the APP ID, write the manifest, build, and copy only the finished bundle into <out_dir>. <out_dir> is routinely the build directory on a mounted workspace, which is exactly why nothing but the bundle is written there.

app_packaging_require_flatpak_tools, which both packagers call first, requires three binaries: flatpak, flatpak-builder and — since 2026-09-15 — ostree. The verdict above is ostree refs, but Debian’s and Ubuntu’s flatpak package depends on libostree and not on the ostree CLI, so a box with the two obvious tools installed passed the check and then reported is not in <repo> over an export that had succeeded. Each missing tool is reported with what the packaging step wanted it for, and all of them at once: one apt-get installs the set.

tests/test-app-packaging-flatpak.sh pins all of them against stubbed flatpak/flatpak-builder/ostree/cmake, including the two cases the exit code cannot see: a non-zero flatpak-builder whose app is committed still ships, and a zero exit with an empty repo fails.

06-packaging/package_archive.sh — one binary, four formats#

One release binary, a tar always, and by --package-types a deb, an AppImage and a flatpak. The types are comma- or space-separated, in any case. Its consumer is OxidANT’s Linux lane. Until 2026-10-06 it wrote the tar and stopped: its create_deb() was deleted on 2026-08-08 as unreachable, and --flatpak-manifest, --desktop-file and --appdata-file were checked for existence and then never read.

After the tar it stages the binary alone, under its package name, in ${KATAGLYPHIS_PACKAGING_WORKDIR:-/tmp/packaging-work}/bundle. It hands that tree to the three bundle packagers above, the ones a Flutter lane calls, through seven knobs. Unset, each keeps the Flutter default, so a Flutter lane packs what it did before:

Knob

Flutter default

package_archive.sh sets

APP_PACKAGING_BUNDLE_DIR

build/linux/<arch>/release/bundle

the staged tree

APP_PACKAGING_VERSION

pubspec.yaml’s version:

--version, minus a tag’s leading v

APP_PACKAGING_DESKTOP_FILE

a generated entry

--desktop-file

APP_PACKAGING_ICON_FILE

web/icons/Icon-512.png, then the fallbacks

--icon-file

APP_PACKAGING_OUT_DIR

out

--archive-dir

APP_PACKAGING_DEB_DEPENDS

libc6, libstdc++6, libgtk-3-0

libc6, libgcc-s1, unless the caller set it

APP_PACKAGING_APP_ID

<APP_PACKAGING_APP_ID_PREFIX>.<package>

--app-id, when given

Three details each format depends on:

  • The desktop entry is the project’s own. app_packaging_adapt_desktop_file keeps every line but Exec and Icon, and sets those two right after [Desktop Entry] for the format: the package name for the deb and the flatpak, AppRun for the AppImage.

  • The icon goes where its real size says. app_packaging_icon_size reads WxH from the PNG header, so a 128×128 logo lands in hicolor/128x128/apps. A file that is no PNG falls back to 512x512, the directory every icon used to go to.

  • The flatpak copies lib/ and data/ only when they exist. A Flutter bundle has both, a single binary has neither.

--flatpak-manifest and --appdata-file now stop the run with an error that says why, instead of being accepted and ignored. The flatpak manifest is generated from the staged bundle. An unknown package type stops the run before anything is built, and the three bundle formats need --arch.

tests/test-package-archive.sh pins the wiring against stubbed packagers. It also packs one real deb through the real library and checks its contents, Depends and desktop entry. Measured 2026-10-06 in :latest, OxidANT’s release kataglyphis_cli (1.9 MB; it links libc, libm and libgcc_s) gave:

  • a tar of 885 KB;

  • a deb of 732 KB, whose binary runs;

  • an AppImage of 1.7 MB, which runs with APPIMAGE_EXTRACT_AND_RUN=1;

  • a flatpak of 684 KB, which flatpak install --system --bundle installs and flatpak run --command=oxidant runs, with its desktop entry and 128×128 icon exported.

ctest-run.sh — run a CMake project’s test suite in a container#

The twin of cmake-build.sh for the test phase, and deliberately a separate library rather than another entry point inside it: CI configures and builds once, then runs ctest several times over different build trees (plain, ASan, TSan…). Sourcing the build driver for that would drag in its cargo/ccache/sccache writability fallbacks and its pre-build hook machinery, none of which a ctest run uses.

Variable

Meaning

Default

CTEST_RUN_DEFAULT_BUILD_DIR

build tree to cd into; empty means “stay here”

build

CTEST_RUN_DEFAULT_BUILD_TYPE

value for ctest -C

Debug

CTEST_RUN_DEFAULT_EXCLUDE

default ctest -E regex

none

CTEST_RUN_DEFAULT_ARGS

ctest flags when the caller does not override them

maximally loud on purpose — a container test run is only debuggable through its log

CTEST_RUN_SAFE_DIRECTORY

git safe.directory; empty disables

/workspace

CTEST_RUN_USAGE_INTRO

one-line description shown in --help

—

A GPU test suite needs the loader and the layers on the same terms the build had, which is why the Vulkan environment is resolved here too.

compiler-llvm-tools.sh — the LLVM tools of the compiler that built a tree#

Anything that reads clang’s output has to come from the same LLVM. That covers llvm-profdata and llvm-cov on a raw profile, and clang-tidy on module PCMs. The images put a source-built clang behind clang/clang++, while the bare tools on PATH can be an older distro LLVM, so the pairing fails with “no profile can be merged” or “uses a newer format that cannot be read”. The image after CON11 links clang’s own copies into /usr/local/bin (BACKLOG CON15); keep the helper for older images and for a tree another compiler configured.

  • compiler_llvm_tool <build-dir> <tool> prints the tool beside the compiler that configured the tree (CMAKE_CXX_COMPILER in its CMakeCache.txt, else clang++). It asks that compiler’s -print-prog-name, and fails when the answer is the bare name, which is clang’s way of saying it has none.

  • use_compiler_llvm_tools <build-dir> <tool>... puts that directory first on PATH, but only when it holds every named tool. A half set would mix two LLVMs in one report, so it warns and leaves PATH alone instead. Call it in a subshell when the directory holds a tool the caller must not swap, such as clang-format.

It came up from AccelerANTgine’s ci-common.sh once BeschleunigerBallett’s coverage lane hit the same “no profile can be merged” (2026-09-24). linux/scripts/tests/test-compiler-llvm-tools.sh pins it with two fake LLVM installs.

linker-select.sh — an opt-in linker#

KATAGLYPHIS_LINKER=default|lld|mold picks the linker for one build. Unset, or default, changes nothing: clang keeps GNU ld 2.46 and x64 Rust keeps rustc’s own rust-lld. linker_select_env applies it, and cmake_build_prepare_env and the cargo_test/cargo_bench/cargo_debug wrappers call it. A Flutter lane calls it itself, after it has set CC.

  • C/C++: it appends -fuse-ld=<x> to LDFLAGS. CMake folds that into every CMAKE_*_LINKER_FLAGS, but only at the first configure, so a reused build tree keeps its old linker. For mold under clang it also passes -Wl,-plugin,<clang prefix>/lib/LLVMgold.so. Without it mold stops on LTO bitcode archive members (“the -plugin option was not provided”). GCC’s collect2 passes its own plugin.

  • Rust: for the host triple only, it sets CARGO_TARGET_<HOST>_LINKER=$CC and appends -C link-arg=-fuse-ld=<x> to CARGO_TARGET_<HOST>_RUSTFLAGS. wasm32 and the Android targets keep their own linkers. Changing the value recompiles every crate, and sccache misses with it.

  • Loud, never a fallback. An unknown value returns 2. Three things return 1: a set RUSTFLAGS or CARGO_ENCODED_RUSTFLAGS (cargo would take either over the per-target flags without a word), a driver that cannot link a one-line probe with -fuse-ld=<x>, and a mold that cannot be had. USE_LLD in compiler-cache.sh falls back silently; this switch exists to measure, and a fallback would make the measurement lie.

  • mold comes from PATH, else the pinned release. No image ships it yet. linker_select_ensure_mold downloads the MOLD_LINUX_* pin in tool-pins.env (SHA256 per arch: x86_64, aarch64, riscv64) into a version-keyed cache (LINKER_SELECT_CACHE_DIR, default $TMPDIR). An unpinned architecture fails.

Which one to pick. Measured 2026-10-05 in :latest on 32 cores, across OxidANT, AccelerANTgine and the OmniAccelerANT Flutter app, and BeschleunigerBallett on 2026-10-06:

Build

GNU ld

lld 23

mold 3.0.0

AccelerANTgine Debug (ASan, UBSan), relink all 5 binaries

1.70 s

0.35 s

0.56 s

AccelerANTgine release, libAccelerANTgine.so (ThinLTO)

0.97 s

0.85 s

0.82 s

flutter build linux --release, full / touch one TU

49.3 / 3.9 s

47.6 / 3.8 s

49.5 / 4.3 s

Rust dev link, liboxidant.so (rust-lld default: 0.22 s)

0.87 s

0.14 s

0.33 s

BeschleunigerBallett linux-debug-clang (ASan, UBSan, coverage), relink commitTestSuite (110 MB)

1.55 s

0.20 s

0.32 s

Linking is about 2% of these builds, so lld is the useful value. It is already in the image, and it is the one real gain on Debug relinks. mold 3.0.0 links correctly, with the same exported symbols and passing tests, but is never faster than lld here. mold 2.x (Ubuntu’s 2.40.4, the last C++ release 2.42.1) does not link AccelerANTgine. It stops on its non-PIC spdlog (R_X86_64_PC32 … recompile with -fPIC). With spdlog made PIC it exports 619 extra symbols, and two Debug suites abort on an ASan odr-violation. That is why the pin is 3.x and why no apt package is used.

linux/scripts/tests/test-linker-select.sh pins it with stub drivers and a stubbed download.

riscv64-cross.sh — cross-build for riscv64, test under QEMU#

riscv64_cross_env turns the amd64 image into a riscv64 cross environment over a sysroot built from the image’s riscv64 child (RISCV64_SYSROOT, default /opt/riscv64-sysroot). It writes triple-named clang wrappers, exports the cc-rs, Cargo and pkg-config variables for riscv64gc-unknown-linux-gnu, names the CMake toolchain in RISCV64_CMAKE_TOOLCHAIN_FILE, sets the QEMU CPU model, and fails when a riscv64 binary does not execute. Why each piece is there, and what it was measured against: riscv64-cross-test-lanes.md. linux/scripts/tests/test-riscv64-cross.sh pins it.

docs-build.sh — build a Sphinx documentation tree#

Every project in this family builds its docs the same way: get a virtualenv with the docs requirements, pull whatever the C++/Doxygen side generated into _static, optionally run a diagram generator, then make html and make linkcheck with warnings promoted to errors.

Not 02-toolchain/python/ci_build_docs.sh, which is the docs step for pure-Python repositories (uv_sync_project over a pyproject, pytest/coverage report staging, no linkcheck). This one is for projects whose docs sit next to a C++/Rust build.

Variable

Meaning

Default

DOCS_BUILD_PROJECT_ROOT

project root

cwd

DOCS_BUILD_DOCS_DIR

directory holding the Sphinx Makefile

<root>/docs

DOCS_BUILD_SOURCE_DIR

Sphinx source dir

<docs>/source

DOCS_BUILD_STATIC_DIR

static asset dir

<source>/_static

DOCS_BUILD_VENV_DIR

virtualenv to activate

<root>/.venv

DOCS_BUILD_UV_VENV_CREATE_SCRIPT

script that creates the venv

—

DOCS_BUILD_UV_INSTALL_REQUIREMENTS_SCRIPT

script that installs its requirements

—

DOCS_BUILD_SVG_SOURCE_DIR

directory whose *.svg are copied into the static dir before the build; empty skips

—

DOCS_BUILD_GENERATOR_SCRIPT

Python script run with the source dir as cwd before Sphinx; empty skips

—

DOCS_BUILD_PYTHON

interpreter for that script

python

DOCS_BUILD_SPHINXOPTS

SPHINXOPTS for every target

-W --keep-going — warnings are errors, but the build reports all of them

DOCS_BUILD_TARGETS

array of make targets

html linkcheck

Both UV_* scripts run with the project root as cwd — the same contract as code-quality.sh’s pair, and both defer to 01-core/python_uv.sh. Unlike that pair, which falls back to the hub’s own bootstrap since 2026-09-15, both are required here: an unset one is fatal.

A missing SVG is fatal on purpose. An empty diagram set means the generating build did not run, and shipping docs with holes in them is worse than failing here.

dartdoc-build.sh — theme and enrich a dart doc site#

The Dart/Flutter counterpart of docs-build.sh. dart doc has no theme and no navigation hook, so the library works the only two seams it leaves: the generated doc/api/static-assets/styles.css, and the emitted HTML.

The theme sheet is generated, never hand-written. DARTDOC_BUILD_THEME_CSS points at style/dartdoc.css, which DocumANTation’s style/generate_style.py renders from style/brand.json — the same single source of truth the LaTeX, Pandoc and Sphinx consumers read. That file exists because the hand-written predecessor had drifted onto a Tailwind slate/sky palette (#0284c7 links, #22c55e hover) while the brand’s link colour was #0e7490, so one site in the family rendered a different brand from every other. Its two marker lines are load-bearing: dartdoc_build_apply_theme truncates a previous append at the first line, so rebuilding cannot stack copies of the sheet.

dartdoc-guides.py next to it renders the configured Markdown into dartdoc’s own index.html shell, so a guide page carries the same header, sidebars and theme as an API page, and rewrites every relative *.md link onto the guide page rendered from that file.

dartdoc emits TWO page shapes, and a third kind of file. Some pages carry a static left sidebar with a bare <ol>; the rest carry <div id="dartdoc-sidebar-left-content"></div>, filled at runtime from a *-sidebar.html fragment — measured on one consumer, 1030 of 1459 pages were the second kind. Those fragments are not documents at all: no <html, no </body>. So the renderer skips a page it cannot hang navigation on and skips a fragment it cannot attach a footer to, rather than aborting the run on the first one, and the END of the run is where vacuity is caught instead: a run that navigated no page, or that configured a footer and footered no page, fails. It prints both numbers (navigated X/N and footered Y/N) so “it worked” is a measurement.

Variable

Meaning

Default

DARTDOC_BUILD_PROJECT_ROOT

project root

cwd

DARTDOC_BUILD_DOC_ROOT

directory dart doc writes into

<root>/doc

DARTDOC_BUILD_CLEAN_CMD

array run before generation; unset skips

—

DARTDOC_BUILD_DOC_CMD

array that generates the site

dart doc

DARTDOC_BUILD_THEME_CSS

generated brand sheet appended to dartdoc’s stylesheet

— (required)

DARTDOC_BUILD_IMAGES_DIR

directory copied to doc/api/images; empty skips

—

DARTDOC_BUILD_GUIDES

array of <source markdown>|<slug>|<nav title>

—

DARTDOC_BUILD_FOOTER_LINKS

array of <label>|<url> for the page footer

—

DARTDOC_BUILD_FOOTER_TITLE

bold name in front of those links

—

DARTDOC_BUILD_TITLE_SUFFIX

appended to each guide page’s <title>

—

DARTDOC_BUILD_VENV_DIR

venv for the renderer

${TMPDIR:-/tmp}/kataglyphis-dartdoc-venv

DARTDOC_BUILD_REQUIREMENTS

its requirements file

lib/dartdoc-guides.requirements.txt

DARTDOC_BUILD_PYTHON

interpreter for the renderer

the venv’s, created on demand

A configured input that is missing is fatal. An absent DARTDOC_BUILD_IMAGES_DIR, guide source or theme sheet fails the build instead of being skipped: a docs site quietly missing its theme and half its pages is worse than a build that stops and says so. The same rule governs the CI ownership fix — the container writes doc/ as root over a bind mount, and a chown that fails leaves a tree the host user cannot rebuild.

The rustdoc theme sheet#

02-toolchain/rust/cargo_build_doc.sh styles cargo doc output with the same generated brand sheet the Sphinx and dartdoc builds use: DocumANTation’s style/generate_style.py renders it from style/brand.json and ships it inside the sphinx_kataglyphis package.

It did not always find it. Both probes that stood in that script named a hub path that resolves to nothing — the hub’s own docs/_static/css/custom.css was dropped in 28425115 (2026-07-15) as a stale fork of that very sheet, and the fallback pointed one level short, at linux/docs/_static/, which has never existed in this repository. The block therefore produced an empty EXT_CSS on every run and rustdoc got no theme at all, silently.

The path is now resolved from SCRIPT_DIR rather than the working directory, so it answers the same inside a consumer’s third_party/ANTfrastructure checkout — which is the case the cwd-relative probe existed for in the first place.

Consumer entry points that are not libraries#

The things below are executables a consumer runs, not cores it sources. They share one rule, and it is the rule the lint-secrets.sh and lint-workflows.sh repairs were both about: the consumer repo root is an explicit argument, never inferred from BASH_SOURCE. A consumer checks this repo out at third_party/ANTfrastructure/, so a self-derived root resolves to ANTfrastructure and the tool operates on the wrong tree — reporting green, having looked at nothing.

Gate aggregation (01-core/gates.sh)#

run_gate / gate_skip / assert_gates, the shell half of the fleet’s “run every gate, then fail once” idiom (the PowerShell half is Invoke-BuildGate / Add-BuildGateSkip / Assert-BuildGates in WindowsBuild.Common.psm1).

source "${CORE_DIR}/gates.sh"
gate_reset "static analysis"
run_gate "ruff check" ruff check --no-fix src
run_gate "ty"         ty check
if has_tool clang-tidy; then
  run_gate  "clang-tidy" clang-tidy src
else
  gate_skip "clang-tidy" "not installed in this image"
fi
assert_gates            # 0 when all passed; 1 naming every failure or skip

Every gate RUNS even after an earlier one fails, so one push names every finding instead of one per round trip. run_gate returns 0 for a failing gate — that is deliberate and it is only safe because assert_gates re-raises: a run_gate batch with no closing assert_gates is suppression, not aggregation. It is compatible with set -e (the command runs inside a || list). assert_gates also fails when no gate ran, because an aggregator whose list came out empty reporting success is the failure this mechanism exists to prevent.

The third bucket: a gate that could not RUN#

A gate can also be neither a pass nor a failure: its tool is not installed. Both ways of forcing that into the other two buckets are lies — counted as a pass it is the suppression this file exists to prevent, counted as a failure it is a red nobody can act on — so gate_skip <name> [why] records it as its own thing. The reason is part of the record on purpose: “skipped” without one reads exactly like a gate somebody quietly deleted.

Bucket

Recorded by

Counts as “a gate ran”?

Verdict

pass

run_gate, command exits 0

yes

green

failure

run_gate, command exits non-zero

yes

red, and named

skip

gate_skip

no

red, unless --tolerate-skips

A skip is RED BY DEFAULT, and that default is the inverse of the first cut. The earlier spelling was assert_gates --fail-on-skip: tolerance was what you got for free and strictness was the thing you had to remember, which is exactly the “allowed to fail” shape the fleet rule forbids. A driver that forgot the flag reported green over a tool that never ran, and nothing in the tree recorded which drivers those were. Inverted, tolerance is an explicit --tolerate-skips at a call site, so one grep -rn -- --tolerate-skips enumerates every place in the fleet where a missing tool is currently allowed to pass and the audit is finite. assert_gates returns 2 for any other argument, so a stale --fail-on-skip is a caller bug rather than a silently re-armed default.

A skip does not count towards _GATE_RAN, so a batch of nothing but skips is red even with --tolerate-skips: nothing was graded, so there is no result to tolerate. That is the no-gate-ran rule above, and it outranks the flag — as does a real failure sharing the batch.

run_gate runs its command in a SUBSHELL#

( "$@" ), not a bare "$@". Upstream check helpers report failure with err() (01-core/logging.sh), which ends in exit 1. Called directly that exit unwinds the driver, not just the gate: the findings already recorded are lost, the gates after it never run, and the batch never reaches assert_gates — “stop at the first failure” arriving through the back door, in the one file whose whole job is to prevent it. || does not catch exit; only a subshell does. The cost is that a gate can no longer export state back to the driver, which no caller wanted. linux/scripts/tests/test-lint-gates.sh grades this by running one driver against the shipped file and against a copy with the subshell removed: the copy has to die mid-batch, or the shipped file passing proves nothing.

Windows parity, and the one place the halves differ#

The third bucket is mirrored exactly — Add-BuildGateSkip -Context -Name [-Reason] and Assert-BuildGates [-TolerateSkips], where a [switch] is false unless passed, so the inverted default is the same fact on both halves.

The subshell is not mirrored, and that was measured rather than assumed: exit inside a gate scriptblock terminates the whole PowerShell driver through both & $Script and $Script.Invoke(), and the only containment left is a child runspace, which breaks the closures a gate scriptblock is written with. The hazard also does not arise there for the reason it does in bash. Invoke-BuildGate’s contract is that a gate fails by throwing, or by a non-zero exit propagated through Invoke-BuildExternal, and its try/catch already contains both; PowerShell has no equivalent of the fleet-wide err() helper that makes exit the normal way to report a failure. windows/scripts/tests/BuildGates.ThirdBucket.Tests.ps1 pins both halves of that paragraph, the child-pwsh measurement included, so the day PowerShell contains an exit this section goes red instead of quietly aging into a false claim.

Tool presence (01-core/tool-checks.sh)#

has_tool <cmd> and require_tools <cmd>..., the latter naming every missing tool rather than the first. Each is defined only when the caller has not already defined it, so a project common.sh still wins — that conditional shape is what the inline fallbacks in lib/code-quality.sh and lib/coverage.sh were, and those two now source this instead of carrying a copy each.

Python interpreter probe (01-core/python-probe.sh)#

preflight_python_require <caller> returns 0 with PREFLIGHT_PYTHON exported when that value (or, unset, python3) runs -c pass, and 1 naming the caller and the knob otherwise. Plain python3 is not trusted because on Windows Git Bash it is the Microsoft Store stub, which prints an install hint and exits non-zero. preflight.sh probes a candidate list and exports the winner; a gate run standalone inherits nothing, so lint-workflows.sh and run-lint-gates.sh call this before their Python steps instead of each carrying the check inline, which is where the second copy sat until 2026-09-14. lint-python.sh and this repo’s own pre-commit/pre-push hooks joined them on 2026-09-15. Expand the value unquoted, as preflight.sh does: it may be a command line such as uv run --no-project python, the very hint the failure message gives.

run-in-ci-image.sh — run a command in the CI image#

bash third_party/ANTfrastructure/linux/scripts/run-in-ci-image.sh . -- bash scripts/linux/build.sh

<repo-root> [--engine docker|nerdctl] [--platform ...] [--workdir ...] [--mount-hub-scripts] [--name N] [--keep] -- <command...>.

Every consumer README, every “reproducing CI locally” section and half the agent prompts carried the same hand-typed docker run, and they had all drifted: a different tag, a forgotten MSYS_NO_PATHCONV, a mount at a different path, no safe.directory. This is that line once. The image comes from ci-image-ref.sh, the root is mounted at /workspace and registered as a git safe.directory before the command runs (without it every git ls-files inside fails as “dubious ownership”, which breaks a format gate long before anything builds), and the engine defaults to nerdctl when present, else docker — the local box runs Rancher Desktop and CI runs docker, and neither should have to say so.

MSYS_NO_PATHCONV=1 MSYS2_ARG_CONV_EXCL='*' are exported by the script rather than documented for the reader: from Git Bash, MSYS rewrites anything that looks like a POSIX path into a Windows one, destroys every -v/-w argument, and fails with “expected an absolute path”. A note nobody reads is how that keeps being rediscovered.

CI workflow steps keep using the run-in-linux-container composite action; this is for everything that is not a workflow step.

ci-image-ref.sh — the family CI image reference#

Prints ${IMAGE_REGISTRY_PREFIX}:${CI_IMAGE_LINUX_TAG} (or …_WINDOWS_TAG with --windows, …_WINDOWS_ARM64_TAG with --windows-arm64 for the arm64 cross bundle) on stdout and nothing else, so it is safe in a command substitution.

docker run --rm -v "$PWD:/workspace" -w /workspace \
  "$(third_party/ANTfrastructure/linux/scripts/ci-image-ref.sh)" <cmd>

Workflow steps do not need it: the four container composite actions carry the same value as their image: input default. It is for the callers that cannot omit an input because they are not calling an action — a raw docker run, a local repro, a lane driver. It is the one entry point here that takes no consumer root, because the only file it reads is this repo’s versions.env whatever tree is being built; a root parameter would imply a per-consumer answer and there is none. Its PowerShell twin is Get-CiImageReference (WindowsContainerImage.Common.psm1). tests/test-ci-image-ref.sh holds the script to the same strings as verify_ci_image_refs.py, which grades the four action defaults (and the Windows pair’s image-arm64); ContainerImage.CiRef.Tests.ps1 holds the twin to versions.env.

The empty-scope rule#

_lint_gates_scope builds a gate’s file list from git ls-files under the graded root, minus the excluded top-level directories. Its third argument decides what an empty result means, and the two callers genuinely differ:

Mode

Meaning

Who uses it

refuse-empty (default)

a BROKEN SCOPE. lint-shell.sh with zero file arguments falls back to ANTfrastructure’s OWN tree and passes, so green over nothing is a lie.

the shell gate

allow-empty

a FACT about the repo.

the python gate

The python gate can allow it because it passes explicit absolute paths: with no paths there is no argument list to fall back from, so there is nothing to run and nothing to mis-grade. That distinction is not cosmetic — ANThology is a pure Dart package and OxidANT a Rust crate, neither has a single .py, and refusing an empty Python scope made both lanes exit 1 on every push for a reason nothing in either tree could change. A permanently red lane is a tolerated failure by construction.

run-lint-gates.sh — six gates over a consumer tree, plus the opt-in ratchets#

bash third_party/ANTfrastructure/linux/scripts/run-lint-gates.sh "$PWD"
bash third_party/ANTfrastructure/linux/scripts/run-lint-gates.sh "$PWD" --exclude vendor

shellcheck, actionlint (+ the CI image-ref check), gitleaks, ruff (error tier), the shared-config drift check and the consumer pin-forwarding check, in one command, every gate running even after one fails. Three consumers had grown their own copy — two of them as run: blocks inside a workflow, so the gate blocking their deploy could not be reproduced locally at all.

What the copies carried and this keeps: the git ls-files scope (a **/*.sh glob does not recurse without globstar, so it graded the directories somebody remembered), the empty-list guards (lint-shell.sh with zero file arguments falls back to ANTfrastructure’s own tree and exits 0), and the gitleaks self-test — a clean-tree positive control plus a planted-PAT canary matched by path, which is what tells “the gate ran and found nothing” from “the gate never started” and proves the scan root was honoured.

--exclude <dir> (default third_party) drops a vendored top-level directory from every scope while KEEPING the tracked plain files directly inside it: those are the consumer’s own, and dropping the whole prefix excluded them silently.

The pin preconditions the copies carried (“does the pinned lint-secrets.sh understand a scan root yet?”) are gone by construction: this script ships in the same commit as the gates it calls.

--ratchets (opt-in, 2026-09-14) adds the eight measurement gates that take --root — verify_code_size, verify_code_complexity, verify_dead_functions, verify_comment_size, verify_stdout_returns, verify_masked_assignments, verify_trailing_conditional and verify_shellcheck_warnings — over the consumer tree, exactly as the scan-root contract describes: the freeze files are read from <consumer-root>/<gate>.allow (function-size.allow, file-size.allow, code-complexity.allow, dead-functions.allow, comment-size.allow, masked-assignments.allow, trailing-conditional.allow, shellcheck-warnings.allow; verify_stdout_returns has none). It is opt-in because a tree with no freeze files is red on its first run — that first report is what seeds them. Seed, commit, then keep the flag on in the wrapper and the workflow.

The step also runs docs/scripts/verify_doc_links.py --root (2026-09-15), which is the ninth gate and the odd one out: it has no freeze file, so there is nothing to seed and it is safe the very first time the flag goes on. It grades every tracked Markdown page in the consumer — which is what puts a consumer’s own README.md into the cross-reference graph at all — and it runs even when the tree carries no shell, because a Dart or Python repo still has pages.

A consumer with no tracked shell at all — a pure Dart or Python repo — may still pass the flag: the step asks gate_scope.assert_non_empty once with on_empty="allow", prints ratchets: no tracked *.sh outside third_party under <root> - nothing to grade. and returns green. That decision belongs to the aggregator, not to the gates: rule 2 of the scan-root contract says an empty scan is a decision and never a default, and who pointed the gate at this tree is the only thing that knows which answer is right. Pointed at a tree by hand, a gate still refuses. A root that is not a usable checkout fails the step outright, with gate_scope’s own message.

The Python gates run under PREFLIGHT_PYTHON when it is set (the same contract preflight.sh and lint-workflows.sh document) and probe the interpreter first: on a Windows host plain python3 is the Microsoft Store stub, and the probe names the fix instead of letting a gate die inside its Python step.

05-frameworks/flutter/setup-sqlite3-wasm.sh#

Fetches the pinned sqlite3.wasm into <consumer-root>/web/, SHA256-verified through download_verified_file. Two consumers had copied the same unverified curl and had already drifted to different versions; the version and its digest now live in 01-core/versions.env (SQLITE3_WASM_VERSION / SQLITE3_WASM_SHA256). There is deliberately no version argument — the pin is the point.

05-frameworks/flutter/lane-prologue.sh#

flutter_lane_prepare_env [flutter_dir] is the Flutter twin of cmake-build.sh’s cmake_build_prepare_env, and exists for the same reason: two consumers had each grown their own prologue, they had drifted, and the differences were all work the IMAGE already does.

It asserts ${FLUTTER_DIR:-/opt/flutter}/bin/flutter, puts its bin/ on PATH once, registers a git safe.directory for the repo root only, defaults PUB_CACHE to <repo>/.pub-cache, and prints flutter --version — the tag is unpinned, so the version is a measurement rather than a constant.

Because that cache lands inside the repository, a consumer that also runs a tree-walking gate must keep it out of the walk: every dependency ships its own example/ sources, and a gate that grades them fails on code the repository neither owns nor can format. OmniAccelerANT measured 28 such CMake files — fl_chart’s example/linux/CMakeLists.txt among them — after one pub get. code_quality_find_cmake_files therefore excludes */.pub-cache/* by default (CODE_QUALITY_CMAKE_DEFAULT_EXCLUDES, added to whatever CODE_QUALITY_CMAKE_EXCLUDE_PATHS a consumer sets, never replacing it): the hub created the directory, so the hub excludes it. Any OTHER tree-walking gate a consumer runs — clang-format, a licence sweep, a secret scan over sources — has to exclude it in that gate’s own list.

Three things it does not do, and must not gain: a safe.directory for the SDK (setup-package-image.sh:556 registers /opt/flutter at --system level, so a --global copy is a no-op that reads like a requirement), sourcing ~/.bashrc to find flutter (Dockerfile.package’s ENV PATH already carries it, and a stock non-interactive .bashrc returns early with a meaningless status while hiding a broken rc file), and installing an SDK — a lane that installs one is testing a different toolchain from the one it ships.

flutter_build_web [--wasm] [--no-tree-shake-icons] [args...] is the optional second half: flutter build web --release with the two flags every consumer passes named rather than re-spelled, everything else forwarded untouched.

01-core/webdav-download.sh#

webdav_download_tree <remote> <local> [extension|all] pulls a WebDAV tree into a directory. Credentials come from WEBDAV_HOSTNAME / WEBDAV_USERNAME / WEBDAV_PASSWORD and are never arguments — a password on a command line is in every ps listing and in every CI log line that echoes the command.

It installs the client at WEBDAVCLIENT_REF from 01-core/versions.env, which is the point of the pin: the PowerShell twin (WindowsWebDav.Common.psm1) installs the same ref, so “which WebDavClient did this run use” has one answer instead of “whatever the default branch was that day”. The --python in the install is load-bearing — the image bakes a root-owned /opt/venv and exports UV_PYTHON at it, so a plain uv pip install inside an activated .venv still targets /opt/venv and dies with “Permission denied (os error 13)” as the uid-1001 build user.

The script it runs is 01-core/download-webdav-files.py. With no --extension (or all) it hands the whole walk to the client’s own download_all_files_iterative; a 140-line hand-rolled traversal used to live in its place, with its own URL joining, sub-path sanitising and streaming download, each a second implementation of something the client already did. The extension-filtered path stays for the early .pfx certificate fetch, which genuinely wants one file type out of a shared folder. A one-line shim remains at windows/scripts/certificates/download_webdav_files.py, because the PowerShell module resolves that path relative to itself.

01-core/http-readiness.sh#

wait_for_http <url> <who> [attempts] [sleep] polls until an endpoint answers. Three consumers had written this loop and every difference between them was an accident — attempt count, sleep length, and the one that matters: whether failure kills the caller. It returns non-zero rather than exiting, because the nginx caller has to dump docker logs before it dies and a helper that calls exit takes that away. Probes that fail while the server is still starting are the expected case and stay silent; only the verdict is printed, and it names who — a bare URL never told anyone which server failed to come up. Defaults are 50 attempts at 0.2s, the ten seconds the consumers converged on.

01-core/bind-mount-ownership.sh#

fix_bind_mount_ownership <target> <reference> hands a tree a container wrote back to whoever owns the bind mount. <reference> is a path the host user owns — normally the workspace root — and its uid:gid is what <target> is handed back to; nothing takes a uid as an argument, because a number typed at a call site is the thing that goes stale.

The three decisions around the chown are the whole reason this is shared:

  • Only the paths that actually differ, never a blanket chown -R. On a tree that is mostly already correct, recursing over it asks the kernel for a chown it refuses for a non-owner and turns a no-op into an error. -h so a symlink is retargeted rather than its destination, which may be outside the tree.

  • A failure as a non-root uid is explained and tolerated. Giving a file to a different uid needs CAP_CHOWN; as an unprivileged container user that is arithmetic, not a defect, and it was measured red on a Windows host with nothing an operator could do about it.

  • The same failure as root is fatal. There it means a read-only or broken mount, and the tree really is left in the state this step exists to prevent.

A missing <target> is not an error: a docs step that produced nothing has nothing to hand back. lib/dartdoc-build.sh’s dartdoc_build_fix_ownership is the older blanket chown -R form over one fixed tree and predates this; tests/test-bind-mount-ownership.sh pins all three decisions.