Skip to main content

max / alloy

Let the server profile build: the night skeleton it prunes was asserted present The server prune removes /usr/share/alloy/skel-night, and three instructions from the end the build asserted that tree exists, unconditionally. So PROFILE=server failed every time, and nothing said so until the first server image was built today, two days after the split landed. Every static check in the tree passed throughout. The assertion is now conditional, and the server branch asserts the absence rather than skipping — a branch that checks nothing is exactly the failure the split's own rule exists to prevent. The rule could not have caught this, because there was no conditional to inspect. So profile_split.rs grows the other half of it: nothing outside a $PROFILE conditional may read a path the prune deletes. Narrow on purpose — only the paths the prune names, whole-word, with the rust-build stage's own prefixes excluded. Checked against a negative control: with the fix reverted, it names the instruction and the path.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-08-03 23:19 UTC
Signed with PGP, not checked
Commit: 3dc66770296efe1d073b11256c3545096aa341c1
Parent: 8af8084
2 files changed, +83 insertions, -7 deletions
M Containerfile +21 -7
@@ -1957,14 +1957,28 @@
1957 1957 # that ships a skeleton file and by every COPY above. A night render whose day
1958 1958 # counterpart went missing between there and here is a file the pristine test
1959 1959 # can only decide by keeping, so the user stays on whatever they have.
1960 + #
1961 + # Conditional since 2026-08-03, and it had to become one: the server prune
1962 + # above deletes /usr/share/alloy/skel-night, so this test asserted the presence
1963 + # of something the same file had already removed and `PROFILE=server` could not
1964 + # build at all. Found by the first server build ever run, which is the whole
1965 + # argument for running one. The server branch asserts the absence rather than
1966 + # skipping, per the rule the split lives by — a branch that checks nothing is
1967 + # the failure mode conditionals were supposed to be worth risking.
1960 1968 RUN set -eux; \
1961 - test -n "$(find /usr/share/alloy/skel-night -type f -print -quit)" \
1962 - || { echo "the night skeleton did not arrive; alloy theme apply would have nothing to apply" >&2; exit 1; }; \
1963 - for applied in $(find /usr/share/alloy/skel-night -type f); do \
1964 - rel="${applied#/usr/share/alloy/skel-night/}"; \
1965 - [ -f "/etc/skel/$rel" ] \
1966 - || { echo "/etc/skel/$rel is gone; the night render of it has nothing to switch back to" >&2; exit 1; }; \
1967 - done
1969 + if [ "$PROFILE" = client ]; then \
1970 + test -n "$(find /usr/share/alloy/skel-night -type f -print -quit)" \
1971 + || { echo "the night skeleton did not arrive; alloy theme apply would have nothing to apply" >&2; exit 1; }; \
1972 + for applied in $(find /usr/share/alloy/skel-night -type f); do \
1973 + rel="${applied#/usr/share/alloy/skel-night/}"; \
1974 + [ -f "/etc/skel/$rel" ] \
1975 + || { echo "/etc/skel/$rel is gone; the night render of it has nothing to switch back to" >&2; exit 1; }; \
1976 + done; \
1977 + else \
1978 + test ! -e /usr/share/alloy/skel-night \
1979 + || { echo "profile=server still carries the night skeleton the prune removes" >&2; exit 1; }; \
1980 + echo "skel-night: pruned with the rest of the desktop skeleton"; \
1981 + fi
1968 1982
1969 1983 # Script coverage, and the two claims that come with it.
1970 1984 #
@@ -155,6 +155,68 @@
155 155 );
156 156 }
157 157
158 + /// Nothing outside a conditional may read a path the server prune deletes.
159 + ///
160 + /// The rule above catches a conditional with a silent half. This catches the
161 + /// other shape of the same mistake, which has no conditional to inspect: an
162 + /// unconditional assertion about a path that only one profile still has.
163 + ///
164 + /// It is not hypothetical. `/usr/share/alloy/skel-night` is removed by the
165 + /// prune and was then asserted present, unconditionally, three instructions
166 + /// from the end — so `PROFILE=server` failed every time, and nobody knew until
167 + /// the first server image was built on 2026-08-03, two days after the split
168 + /// landed. Every static check in the tree passed the whole time, this one
169 + /// included, because it did not exist.
170 + ///
171 + /// Deliberately narrow: only the paths the prune names, and only whole-word
172 + /// matches. A general "does this instruction hold on both profiles" is not
173 + /// something a text check can answer, and pretending otherwise would produce a
174 + /// test that fails on things that are fine.
175 + #[test]
176 + fn nothing_unconditional_reads_what_the_server_prune_removes() {
177 + let text = containerfile();
178 + let instructions = instructions(&text);
179 +
180 + let pruning = instructions
181 + .iter()
182 + .find(|i| i.contains("/etc/skel/.config/sway") && i.contains("rm -rf"))
183 + .expect("no server prune of the desktop skeleton");
184 +
185 + // The paths between `rm -rf` and the first `;` that follows it.
186 + let removed: Vec<&str> = pruning
187 + .split_once("rm -rf ")
188 + .expect("the prune has an rm -rf")
189 + .1
190 + .split(';')
191 + .next()
192 + .unwrap_or("")
193 + .split_whitespace()
194 + .filter(|word| word.starts_with('/'))
195 + .collect();
196 + assert!(!removed.is_empty(), "read no paths out of the server prune");
197 +
198 + for instruction in &instructions {
199 + // The prune itself, and anything that already branches on the profile,
200 + // are both accounted for: the branch is what the other test polices.
201 + if std::ptr::eq(instruction, pruning) || instruction.contains("$PROFILE\" =") {
202 + continue;
203 + }
204 + // The rust-build stage runs before the runtime image exists and stages
205 + // these trees under its own prefixes, which the prune never touches.
206 + if instruction.contains("/staged-skel") {
207 + continue;
208 + }
209 + for path in &removed {
210 + assert!(
211 + !instruction.contains(path),
212 + "`{path}` is removed by the server prune, but this instruction reads it \
213 + with no $PROFILE conditional, so it can only hold on client:\n {}",
214 + &instruction[..instruction.len().min(200)],
215 + );
216 + }
217 + }
218 + }
219 +
158 220 /// The one thing the brief insists survives on a headless machine: the bare
159 221 /// console palette is the only themed surface there, so a prune that took it
160 222 /// would take the whole design system with it.