Skip to main content

max / alloy

Read the profile split, which is an if and not a case The package-set parser tracked `case "$VAR" in` arms and nothing else, so every `if [ "$PROFILE" = client ]` install was attributed to the base. Nobody had noticed because fw13 is a client and the fw13 recipe is all anyone had run it against. Measured on astra, the first server recipe put through it: 114 packages against fw13's 115, differing by firefox alone, carrying sway, greetd, cups, flatpak and fontconfig. The probe then failed bootc's /var lint on paths declared by the client tmpfiles file a server mint correctly drops -- a finding against a mint that would have passed, which is the one failure this tool must not produce. astra now resolves to 57 packages and fw13 stays at 115, which is the proof that matters: the fix is inert on the path that was already right. The other half was fragment shape. `RUN if [ ... ]` and `then dnf install ...` arrive as single fragments because no semicolon separates the keyword from what follows it, so an anchored `^if` matched nothing at all.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session
https://claude.ai/code/session_013wvegQEzB5piwPowYQ3ZbV
Author: Max Johnson <me@maxj.phd> · 2026-09-04 20:43 UTC
Signed with PGP, not checked
Commit: d8f16576f898f7a27948b815b789c7fa92e4be55
Parent: 4be640d
1 file changed, +70 insertions, -1 deletion
@@ -82,9 +82,43 @@
82 82 dials.setdefault("LANGS", "")
83 83 dials.setdefault("DB", "none")
84 84 dials.setdefault("GUI", "none")
85 + dials.setdefault("TRIM", "unused")
85 86 return dials
86 87
87 88
89 + def eval_condition(frag, dials):
90 + """One `if` fragment against the recipe's dials: True, False, or None.
91 +
92 + Only the shape the Containerfile uses is read -- `[ "$VAR" = value ]` and its
93 + `!=`, joined by `&&`. Anything else returns None and is treated as taken,
94 + so a new guard shape costs a wide probe rather than a silent one.
95 + """
96 + if "||" in frag:
97 + return None
98 + tests = re.findall(r'\[\s*"\$(\w+)"\s*(=|!=)\s*([A-Za-z0-9._+-]+)\s*\]', frag)
99 + if not tests:
100 + return None
101 + # Every `[ ... ]` in the fragment has to be one this understands, or the
102 + # conjunction is being evaluated against half its terms.
103 + if len(tests) != frag.count("[ "):
104 + return None
105 + for var, op, want in tests:
106 + have = dials.get(var, "")
107 + if (have == want) != (op == "="):
108 + return False
109 + return True
110 +
111 +
112 + def branch_taken(if_stack):
113 + """Whether the fragment's enclosing if/else frames all lead here."""
114 + for cond, in_else in if_stack:
115 + if cond is None:
116 + continue
117 + if cond == in_else: # False and not in else, or True and in else
118 + return False
119 + return True
120 +
121 +
88 122 def install_sites(dials):
89 123 """Package names this mint installs, with the dial arm that guards each.
90 124
@@ -101,6 +135,15 @@
101 135 in force, so a package inside `postgres16)` is dropped when the recipe says
102 136 DB=none. Approximating this instead would report packages the mint does not
103 137 install, and a false failure is how an instrument stops being read.
138 +
139 + `if [ "$VAR" = value ]` is read the same way, and it is not a nicety: the
140 + profile split is written as an if/else rather than a case, so a parser that
141 + saw only `case` attributed every client-only package to the base. Measured
142 + 2026-09-04 on astra, the first server recipe anyone ran this against: its
143 + package set came back one package short of fw13's (firefox), carrying sway,
144 + greetd, cups and fontconfig, and the probe then failed bootc's /var lint on
145 + the client tmpfiles file a server mint correctly drops. A finding against a
146 + mint that would pass, which is the failure mode this file exists to avoid.
104 147 """
105 148 logical, buf = [], ""
106 149 for raw in open(os.path.join(REPO, "Containerfile")):
@@ -118,10 +161,36 @@
118 161 pkgs = {}
119 162 for line in logical:
120 163 case_stack, arm = [], None
164 + # if/else frames, innermost last. Each is [condition, in_else], where a
165 + # condition of None means a shape this does not read -- those count as
166 + # taken, because dropping packages on an unrecognised guard would hide
167 + # real findings, which is the one failure worse than reporting extra.
168 + if_stack = []
121 169 for frag in re.split(r'[;]', line):
122 170 f = frag.strip()
123 171 if not f:
124 172 continue
173 + # A fragment carries whatever keywords preceded it with no
174 + # semicolon between them: `RUN if [ ... ]` is one, and so is
175 + # `then dnf install ...`. Anchoring on the bare keyword found
176 + # nothing, which is how PROFILE=server kept every client package.
177 + kw = re.sub(r'^((RUN|then|do|else)\s+)+', '', f)
178 + if re.match(r'^(el)?if\s', kw):
179 + cond = eval_condition(kw, dials)
180 + if kw.startswith("elif"):
181 + if if_stack:
182 + if_stack[-1] = [cond, False]
183 + else:
184 + if_stack.append([cond, False])
185 + continue
186 + if re.match(r'^else\b', f) and if_stack:
187 + if_stack[-1][1] = True
188 + # `else` and the command it guards can share a fragment, so
189 + # this falls through rather than continuing.
190 + if re.match(r'^fi\b', f):
191 + if if_stack:
192 + if_stack.pop()
193 + continue
125 194 m = re.search(r'case\s+"\$(\w+)"\s+in', f)
126 195 if m:
127 196 case_stack.append(m.group(1))
@@ -147,7 +216,7 @@
147 216 # which is how `above`, `moved`, `layer` and `setting` ended up in a
148 217 # probe's install list. An odd number of quotes before the match
149 218 # means the match is inside a string.
150 - if idx >= 0 and f[:idx].count('"') % 2 == 0:
219 + if idx >= 0 and f[:idx].count('"') % 2 == 0 and branch_taken(if_stack):
151 220 tail = f[idx + len("dnf install"):]
152 221 tail = re.split(r'&&|\|\|', tail)[0]
153 222 guard = "base" if not case_stack else "%s=%s" % (case_stack[-1], arm)