Skip to main content

max / alloy

Add a js arm to LANGS, so the image can supply node and npm Off by default: nothing in the image is JavaScript. What asks for it is the build-host role, where Sando's code_smoke gate runs npm over MNW's two frontends and MNW's build.rs downgrades a missing npm to a warning, so a fresh worktree with no node produces no bundle at all. Installs with install_weak_deps=False, leaving nodejs-full-i18n behind. The console's Lang enum carries the arm too, since the mint choice is the builder's.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-08-21 21:19 UTC
Commit: f5d92a5190587787ab0e38cd39cde6f048a73302
Parent: f8acd78
3 files changed, +45 insertions, -8 deletions
M Containerfile +40 -6
@@ -796,6 +796,22 @@
796 796 #
797 797 # python and zig stay off for the reason go now is: nothing in the image
798 798 # requires them.
799 + #
800 + # js is the newest arm and is off by the same rule. Nothing in the image
801 + # is JavaScript. What asks for it is the build-host role: Sando's
802 + # code_smoke gate runs `npm run build` over MNW's two frontends before it
803 + # creates a database or boots anything, and MNW's own build.rs downgrades
804 + # a missing npm to a cargo warning, so without node a fresh worktree
805 + # produces no bundle at all rather than a stale one. The gate is what
806 + # catches it, fatally and on purpose. So the arm exists and the default
807 + # does not carry it: a machine that builds MNW asks for `LANGS=rust,js`,
808 + # and every other mint pays nothing.
809 + #
810 + # Weight, measured 2026-08-21 with `dnf install --assumeno nodejs npm`:
811 + # five packages, 216 MiB installed, of which nodejs-full-i18n is 31.6.
812 + # The arm installs with install_weak_deps=False, which is what leaves the
813 + # i18n split behind; verify the two bundles still build before treating
814 + # that as settled.
799 815 ARG LANGS=rust
800 816
801 817 # TRIM is the one builder choice about the *base* rather than about Alloy.
@@ -857,8 +873,8 @@
857 873 esac; \
858 874 for lang in $(echo "$LANGS" | tr ',' ' '); do \
859 875 case "$lang" in \
860 - rust|go|python|zig) ;; \
861 - *) echo "unknown language '$lang'; the builder offers rust, go, python and zig" >&2; exit 1 ;; \
876 + rust|go|python|zig|js) ;; \
877 + *) echo "unknown language '$lang'; the builder offers rust, go, python, zig and js" >&2; exit 1 ;; \
862 878 esac; \
863 879 done; \
864 880 case "$TRIM" in \
@@ -1314,6 +1330,19 @@
1314 1330 # into. Base because yazi is base; the GTK file-chooser portal that
1315 1331 # was the other consumer is client-only.
1316 1332 xdg-user-dirs \
1333 + # xdg-utils, for /usr/bin/xdg-mime and /usr/bin/xdg-open. Two roles,
1334 + # and only one of them is a desktop's. The desktop one is yazi's
1335 + # primary action, which is `xdg-open` on Enter and on Reveal.
1336 + # The other is the build host's: tauri-bundler shells out to both
1337 + # binaries at those hard-coded absolute paths while writing a Linux
1338 + # bundle, so a profile without them cannot bundle a Tauri app at
1339 + # all. That is the whole argument for base rather than the
1340 + # client block where this used to sit: headless IS the build-host
1341 + # profile, so the one profile that most needs to bundle the apps was
1342 + # the one profile that could not. The mime database the desktop half
1343 + # resolves against stays client-side; the binaries do not need it to
1344 + # exist for the bundler's sake.
1345 + xdg-utils \
1317 1346 # jq. Already present, and that is exactly the problem: it comes from
1318 1347 # fedora-bootc rather than from any line here, and the sway config's
1319 1348 # Ctrl+Print binding (active-window screenshot) pipes swaymsg through it.
@@ -1621,12 +1650,14 @@
1621 1650 # said so in writing since 2026-07-21, when it was proven by
1622 1651 # reading the uid 1000 journal off a booted install.
1623 1652 sway-systemd \
1624 - # xdg-open, and the database it resolves against.
1653 + # The database xdg-open resolves against. xdg-utils itself moved
1654 + # to the base block on 2026-08-21 (see the note beside it there);
1655 + # these two are the half that only a desktop reads.
1625 1656 # etc/skel/.config/yazi/yazi.toml binds `xdg-open` to Enter and to
1626 - # Reveal, so without these the file manager's primary action is a
1627 - # command not found. mailcap is the /etc/mime.types half of the
1657 + # Reveal, and without a mime database it opens the wrong handler
1658 + # rather than none. mailcap is the /etc/mime.types half of the
1628 1659 # same answer.
1629 - xdg-utils desktop-file-utils mailcap \
1660 + desktop-file-utils mailcap \
1630 1661 # Vulkan, which docs/IMAGE.md#size argues about at length and
1631 1662 # treats as a stack decision rather than an accident: six of its
1632 1663 # twelve ICDs drive hardware this image boots on. It has no rpm
@@ -1800,6 +1831,7 @@
1800 1831 go) dnf install -y golang ;; \
1801 1832 python) dnf install -y python3 python3-pip ;; \
1802 1833 zig) dnf install -y zig ;; \
1834 + js) dnf install -y --setopt=install_weak_deps=False nodejs npm ;; \
1803 1835 esac; \
1804 1836 done; \
1805 1837 dnf clean all; \
@@ -1809,6 +1841,8 @@
1809 1841 go) command -v go >/dev/null || { echo "go was asked for and the go binary is not in the image" >&2; exit 1; } ;; \
1810 1842 python) command -v python3 >/dev/null || { echo "python was asked for and python3 is not in the image" >&2; exit 1; } ;; \
1811 1843 zig) command -v zig >/dev/null || { echo "zig was asked for and the zig binary is not in the image" >&2; exit 1; } ;; \
1844 + js) command -v node >/dev/null || { echo "js was asked for and node is not in the image" >&2; exit 1; }; \
1845 + command -v npm >/dev/null || { echo "js was asked for and npm is not in the image" >&2; exit 1; } ;; \
1812 1846 esac; \
1813 1847 done; \
1814 1848 echo "languages: ${LANGS:-none}"
@@ -56,7 +56,7 @@
56 56
57 57 **sccache is absent, and the tree-wide cargo config makes it mandatory.** `~/Code/.cargo/config.toml` sets `rustc-wrapper = "sccache"` unconditionally. A missing wrapper is fatal at the version probe, before any compilation starts, so on a fresh install every cargo command under `~/Code` dies instantly. Measured by pointing the wrapper at a nonexistent binary. sccache is a cargo-installed binary in `~/.cargo/bin`, not a system package, so this is a `$HOME` restore rather than an image change. The config's own comment says the two lines are removable, so dropping sccache is a real option rather than a forced dependency.
58 58
59 - **node and npm are absent, and no `LANGS` value supplies them.** Sando's `code_smoke` gate builds two frontends with `npm run build` before it creates a database or boots anything, and the config text says outright that this is the one place a frontend failure is fatal. It is fatal there because the MNW server's `build.rs` deliberately is not: with no node it emits a warning and lets the Rust build succeed against whatever `static/dist/` already holds, which would silently ship the previous build's JavaScript. The `LANGS` validator accepts only rust, go, python and zig, so this is not a mint-time flag away.
59 + **node and npm are absent from a default mint, and `LANGS=rust,js` is what supplies them.** Sando's `code_smoke` gate builds two frontends with `npm run build` before it creates a database or boots anything, and the config text says outright that this is the one place a frontend failure is fatal. It is fatal there because the MNW server's `build.rs` deliberately is not: with no node it emits a warning and lets the Rust build succeed against whatever `static/dist/` already holds, which would silently ship the previous build's JavaScript. Closed 2026-08-21: the `LANGS` validator carries a `js` arm now, so a machine in the build-host role mints with `LANGS=rust,js` and every other mint pays nothing. What LANGS a given machine starts from is the builder's choice, not a default this file sets. The arm installs with `install_weak_deps=False`, which leaves `nodejs-full-i18n` behind; Fedora 43's nodejs is 22.22.2 against fw13's current 24.18.0, and neither frontend declares an `engines` field, so it is still unverified that the two bundles build under 22.
60 60
61 61 **No PostgreSQL server or client.** Sando's `migration_dry_run` and `cargo_test` gates restore prod dumps into a local scratch cluster over the unix socket. Alloy ships neither the server nor `psql`.
62 62
@@ -259,10 +259,11 @@
259 259 Go,
260 260 Python,
261 261 Zig,
262 + Js,
262 263 }
263 264
264 265 impl Lang {
265 - const ALL: [Lang; 4] = [Lang::Rust, Lang::Go, Lang::Python, Lang::Zig];
266 + const ALL: [Lang; 5] = [Lang::Rust, Lang::Go, Lang::Python, Lang::Zig, Lang::Js];
266 267
267 268 const fn value(self) -> &'static str {
268 269 match self {
@@ -270,6 +271,7 @@
270 271 Lang::Go => "go",
271 272 Lang::Python => "python",
272 273 Lang::Zig => "zig",
274 + Lang::Js => "js",
273 275 }
274 276 }
275 277
@@ -289,6 +291,7 @@
289 291 Lang::Go => "go (356 MB; rebuilds syncthing, restic, tailscale)",
290 292 Lang::Python => "python",
291 293 Lang::Zig => "zig",
294 + Lang::Js => "js (216 MiB; node and npm, for building MNW's frontends)",
292 295 }
293 296 }
294 297