- Dockerfile 64.7%
- Shell 30.8%
- Just 4.5%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
The arm64 FVM release is a launcher script beside a Dart runtime and snapshot in src/, not a single binary like x64, so moving just `fvm` onto PATH broke the arm64 image build. The tarball now lives whole under /opt/fvm-bin with a symlink from /usr/local/bin/fvm, which the launcher resolves. Only the current stable Flutter is baked in now; Tempo's three pins are fetched per job by flutter-install and kept by the actions cache, which takes the image from 10.3 GB to 6.1 GB. The README shows the cache step. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NKEvJWsncYMn3zcp3CDc3t |
||
| .forgejo/workflows | ||
| scripts | ||
| .containerignore | ||
| Containerfile | ||
| justfile | ||
| README.md | ||
| run.sh | ||
toolbox
The shared build and CI toolchain image: one Debian container that runs the CI jobs of every project on the Forgejo instance and doubles as the local build toolchain, so a checkout builds on any host with podman and nothing else installed.
| Tool | Version | Where |
|---|---|---|
| Debian | bookworm (12) | base image |
| Node | 24 LTS, npm, corepack with pnpm | /opt/node, /usr/local/bin |
| Rust | rustup, stable pinned, rustfmt, clippy | /opt/rustup, /opt/cargo |
| Rust targets | aarch64-unknown-linux-gnu (amd64 image only), armv7-unknown-linux-gnueabihf, wasm32-unknown-unknown, wasm-bindgen-cli |
|
| Dart | stable standalone SDK | /opt/dart-sdk (first on PATH) |
| Flutter | FVM, with current stable preinstalled; other pins fetched per job and cached | /opt/fvm/versions/<ref>, default on PATH via /opt/fvm/default |
| Cross compilers | gcc/g++ for armhf, and for arm64 on the amd64 image | |
| Native libraries | GTK 3, libmpv, epoxy, ALSA (host), armhf multiarch DRM/GBM/EGL/GLES/input/udev/xkbcommon/systemd/ALSA/GStreamer | |
| Kernel and rootfs tools | bc bison flex dtc kmod cpio, debootstrap, qemu-user-static, e2fsprogs, parted | |
| Containers | podman, buildah, skopeo with fuse-overlayfs, for nested use in privileged jobs | |
| Misc | just, git-lfs, jq, rsync, cmake, ninja, clang, python3, zsh with Oh My Zsh |
Exact pins are the ARG lines at the top of each section in the
Containerfile; bump them there and rebuild.
Using it in CI
Point a job at the registry's copy and run steps as if the tools were on the runner:
jobs:
check:
runs-on: linux-amd64-container
container:
image: forge.example.net/you/toolbox:<sha12>
steps:
- uses: https://data.forgejo.org/actions/checkout@v7
- run: cargo test --workspace
- run: fvm flutter test
Pin the <sha12> tag (the first 12 characters of the toolbox commit that
built it), not latest; a toolbox change then reaches a project when you
choose. Every published image is also tagged latest and YYYYMMDD.
Because the image carries Node, the JavaScript actions (checkout,
upload-artifact, forgejo-release) run inside it without any provisioning.
Jobs run as root in a privileged container, so apt-get install for a
one-off dependency (Playwright's --with-deps, say) works in a step, and so
does building or running containers: the image carries podman, buildah and
skopeo with fuse-overlayfs, and a plain podman build in a step drives that
nested engine (no socket from the host is involved).
Publishing
.forgejo/workflows/image.yml builds the image natively on the amd64 and
arm64 container runners inside quay.io/podman/stable, pushes
toolbox:<arch>-<sha> from each, and a final job joins them with podman manifest into toolbox:<sha12>, :latest and :<yyyymmdd>. It needs write
access to the registry; the header of the workflow explains the one-time
Authorized Integration setup (a TOOLBOX_REGISTRY_AUDIENCE repository
variable) or the CI_FORGEJO_REGISTRY_* secrets, the same arrangement Tempo
and Cadence used for their own images. Each run builds from scratch (a job
container keeps no layer cache), which is about forty minutes on amd64 and
longer on the arm64 runners. The arm64 build has been exercised as far as
resolving its package set under QEMU; if it misbehaves, drop the arm64 entry
from the matrix and its line in the publish job, and everything else stands.
The registry refuses blobs over about a gigabyte, so each RUN that lays
down something big is split so no layer crosses it. just layers shows
them; the largest is under 400 MB compressed.
The same thing by hand: just build, then just push forge.example.net/you/toolbox:<tag> after podman login.
Using it locally
cd ~/Projects/some-project
~/Projects/toolbox/run.sh cargo build
~/Projects/toolbox/run.sh --shell
run.sh mounts the project (the git top level of the current directory) at
its real host path and runs the command there as your own user, with HOME
and the cargo home in a per-project directory under ~/.local/state/toolbox
so caches persist without touching the checkout. The pub cache is your
host's own, so package resolutions written inside are valid outside. It
builds localhost/toolbox:latest from this directory the first time; set
TOOLBOX_IMAGE to the registry's tag to use that instead.
just build, just shell, just versions, just layers and just push <target> wrap the same things; just build linux/arm64 builds the arm64
image under QEMU (slow, but it works with qemu-user-static on the host).
Inside the container TOOLBOX_CONTAINER=1 is set, and run.sh becomes a
plain exec, so project tooling can go through it unconditionally.
Flutter and FVM
The SDKs live in FVM's cache layout at /opt/fvm/versions/<ref>, so a
project's .fvmrc resolves against the image:
fvm flutter build linux # the project's pinned SDK
fvm dart run build_runner # its Dart
flutter --version # the global default (FLUTTER_STABLE)
Only the current stable is preinstalled (also reachable as stable, which
is what Cadence's .fvmrc names). Every other pin is fetched in the job by
scripts/flutter-install, which is on PATH in the image, and kept between
runs by the actions cache keyed on the pin:
- name: Restore the pinned Flutter SDK
uses: https://code.forgejo.org/actions/cache@v4
with:
path: /opt/fvm/versions/3.44.9
key: flutter-sdk-3.44.9-linux-${{ runner.arch }}
- name: Provision it if the cache missed
run: flutter-install 3.44.9
flutter-install is idempotent: with the cache restored it prints one line
and exits; otherwise it does a blobless partial clone
(--filter=blob:none) of the tag or commit, fetches the tool's Dart SDK and
snapshot, and precaches the Linux desktop artifacts, about 1.3 GB in
around five minutes on amd64. Android, iOS and web artifacts are not
fetched; flutter precache --android adds them if a project ever needs
them. FVM's own fvm install also works but does a full clone, since its
git cache (a complete mirror of flutter/flutter) is switched off in the
image's global config and by run.sh for your user.
To change the preinstalled stable, bump FLUTTER_STABLE in the
Containerfile. To bake in another version permanently, add a RUN flutter-install --stage ... group for it beside stable's; the image grows
by about 1.3 GB per version.
Tempo and Cadence
Both projects currently build their own toolchain image from a
Containerfile in the checkout and name it by a content digest. Everything
those images carry is in this one, so switching them is a matter of the
workflow and the run script:
- Cadence (
toolchain/Containerfile,toolchain/run.sh,.forgejo/workflows/ci.yml): thetoolchainandtoolchain-buildjobs go away andcontainer: image:becomes the toolbox tag. The workspace tool is no longer AOT-compiled into the image, so a job runs it withdart run tool/bin/cadence.dart(or compiles it in a first step and caches the binary). Its Dart pin moves from 3.13.2 to 3.13.3, a patch release; the Containerfile's warning about cross-build launchers deserves afile bundle/bin/cadencedcheck on each architecture the first time.CADENCE_TOOLCHAIN=1is set by the image for now, sorun.sh's in-container detection keeps working;run.shitself can become a one-line wrapper of this repository's. - Tempo (
platform/toolchain/Containerfile,.forgejo/workflows/ci.yml,.forgejo/sdks.sh): same job removal. TheDARTworkflow variable now points at/opt/dart-sdk/bin/dart(3.13.3 rather than 3.12.2 in/opt/toolbox-test). Its three Flutter pins are fetched per job: replacesdks.shwith a cache step per pin plusflutter-install <pin>as shown above, and point the build tool at the result (TEMPO_FLUTTER_SDK=/opt/fvm/versions/3.44.9, or teachFlutterSdk.discoverto look in$FVM_CACHE_PATH/versions/<version>). Alternatively keepsdks.shand itsbuild/sdkscache exactly as they are; they work unchanged in this image.TEMPO_TOOLCHAIN=1,ARCH=armandCROSS_COMPILE=arm-linux-gnueabihf-are set by the image because the kernel build reads them from the environment; moving them onto Tempo'smakeline lets the image drop them later.
The kernel and firmware jobs that need rootful podman, loop mounts and binfmt still cannot run on the container runners; the image carries the tools for when a VM-backed runner exists.
Why bookworm
Tempo's Y2 and Cadence's Raspberry Pi targets run Debian bookworm (glibc 2.36). The cross toolchains link against the image's own libc, so building on a newer Debian would produce binaries those devices refuse to run. Move to trixie together with the devices, not before. The Rust and Dart toolchains are downloaded from upstream and are current regardless of the base.
Adding a tool
Add an apt package to the relevant RUN apt-get block, or a downloaded tool
as its own section with a version ARG, a per-architecture sha256, and a
--version check at the end of the RUN. Anything that writes into its own
tree at run time (SDK caches, package managers) must end with chmod -R a+rwX, because locally the container runs as an arbitrary uid. Then just build, just versions, just layers to confirm no layer grew past a
gigabyte, and push to main to publish.