| 81 |
81 |
|
RUN /src/target/release/alloy-skelgen --help | grep -q -- '--templates' \
|
| 82 |
82 |
|
|| { echo "built skelgen is the stub; the cache-split cleanup matched nothing" >&2; exit 1; }
|
| 83 |
83 |
|
|
|
84 |
+ |
# The one subcommand name that is spelled out in a shell script rather than
|
|
85 |
+ |
# resolved by the compiler. usr/bin/alloy-session runs `alloy theme apply` and
|
|
86 |
+ |
# swallows its exit code, because nothing before the exec is allowed to fail a
|
|
87 |
+ |
# login; the cost of that `|| true` is that renaming the verb makes the wrapper
|
|
88 |
+ |
# a no-op and leaves every account on the day render forever, with one line in
|
|
89 |
+ |
# the journal per login and no other symptom. clap answers `--help` before any
|
|
90 |
+ |
# dispatch, so this asks the binary whether the verb exists without needing a
|
|
91 |
+ |
# theme, a home directory, or a skeleton.
|
|
92 |
+ |
RUN /src/target/release/alloy theme apply --help >/dev/null \
|
|
93 |
+ |
|| { echo "the binary has no 'theme apply'; usr/bin/alloy-session would silently do nothing" >&2; exit 1; }
|
|
94 |
+ |
|
| 84 |
95 |
|
# The console's themes, staged at a path the runtime stage can name.
|
| 85 |
96 |
|
#
|
| 86 |
97 |
|
# theme.rs has no built-in palette on purpose (docs/TOKENS.md: no hex in
|
| 116 |
127 |
|
# mapping and no two of which agreed.
|
| 117 |
128 |
|
#
|
| 118 |
129 |
|
# skelgen fails the build on an unknown token or an empty render, so the
|
| 119 |
|
- |
# assertions the vtrgb and theme-glob steps needed are inside it. The one worth
|
| 120 |
|
- |
# keeping outside is the count: a template tree that silently matched nothing
|
| 121 |
|
- |
# would copy an empty overlay and every program in the image would fall back to
|
| 122 |
|
- |
# its own defaults, with the build log saying nothing at all.
|
|
130 |
+ |
# assertions the vtrgb and theme-glob steps needed are inside it. What has to
|
|
131 |
+ |
# stay out here is everything about the shape of the output tree, because a
|
|
132 |
+ |
# template tree that silently matched nothing, or a night set that silently went
|
|
133 |
+ |
# missing, copies a plausible-looking overlay and says nothing at all.
|
|
134 |
+ |
#
|
|
135 |
+ |
# Two renders per themed template now, the plain one and a `.night` sibling. The
|
|
136 |
+ |
# sibling is written next to its target, which is not where it belongs: left
|
|
137 |
+ |
# there, every new user gets a ~/.config/mako/config.night and a
|
|
138 |
+ |
# userChrome.css.night inside their Firefox profile. No program picks those up —
|
|
139 |
+ |
# helix scans for *.toml, sway reads `config` — so the failure is clutter rather
|
|
140 |
+ |
# than breakage, which is exactly the kind that ships. Hence the relocation
|
|
141 |
+ |
# below, and the assertion that it left nothing behind.
|
|
142 |
+ |
#
|
|
143 |
+ |
# Every loop is `for x in $(find ...)` rather than `find | while read`, and that
|
|
144 |
+ |
# is not a style choice: a piped `while` runs its body in a subshell, so an
|
|
145 |
+ |
# `exit 1` inside it exits the subshell and the RUN succeeds anyway. A build
|
|
146 |
+ |
# guard that cannot fail the build is worse than no guard. Word splitting is safe
|
|
147 |
+ |
# here because no path in templates/ contains whitespace, and the counterpart
|
|
148 |
+ |
# assertions below would catch it if one ever did.
|
|
149 |
+ |
#
|
|
150 |
+ |
# Four guards, none of them a hardcoded number, because the old `-ge 17` floor
|
|
151 |
+ |
# went stale the day the night set landed and a floor that passes by accident
|
|
152 |
+ |
# reads exactly like a floor that passes on purpose:
|
|
153 |
+ |
#
|
|
154 |
+ |
# 1. The file count is derived, not written down: one output per template, plus
|
|
155 |
+ |
# one more for each template whose first line is a `variants` directive.
|
|
156 |
+ |
# Equality rather than a floor, so a render that stops fanning out fails
|
|
157 |
+ |
# here, and adding a template needs no edit.
|
|
158 |
+ |
# 2. Both vt tables are checked, not only the day one. setvtrgb rejects
|
|
159 |
+ |
# anything that is not three rows of sixteen values, and it rejects it at
|
|
160 |
+ |
# boot on the greeter, where nobody is reading logs.
|
|
161 |
+ |
# 3. Every rendered file under /etc/skel must have a night render, with the two
|
|
162 |
+ |
# helix palettes named as the deliberate exceptions: those two files are the
|
|
163 |
+ |
# polarities themselves, picked by filename, so a `.night` sibling of either
|
|
164 |
+ |
# would be a second copy of one of them under the wrong name. This is the
|
|
165 |
+ |
# guard that catches a themed template losing its directive, which guard 1
|
|
166 |
+ |
# cannot: guard 1 derives its expectation from the same first lines.
|
|
167 |
+ |
# 4. Every night render must have a plain counterpart, which is what
|
|
168 |
+ |
# `alloy theme apply`'s pristine test compares against. Together with 3 it
|
|
169 |
+ |
# is a bijection, so a stray file on either side fails the build.
|
| 123 |
170 |
|
COPY templates/ /src/templates/
|
| 124 |
171 |
|
RUN set -eux; \
|
| 125 |
172 |
|
/src/target/release/alloy-skelgen \
|
| 127 |
174 |
|
--out /staged-skel \
|
| 128 |
175 |
|
--theme default=/staged-themes/akari-dawn.toml \
|
| 129 |
176 |
|
--theme night=/staged-themes/akari-night.toml; \
|
| 130 |
|
- |
[ "$(find /staged-skel -type f | wc -l)" -ge 17 ] \
|
| 131 |
|
- |
|| { echo "skelgen produced fewer files than the tree has templates" >&2; exit 1; }; \
|
| 132 |
|
- |
awk 'NF != 16 { exit 1 } END { if (NR != 3) exit 1 }' /staged-skel/usr/share/alloy/vtrgb
|
|
177 |
+ |
templates="$(find /src/templates -type f | wc -l)"; \
|
|
178 |
+ |
variants="$(find /src/templates -type f -exec awk 'FNR == 1 && /^[[:space:]]*@\{! *variants *=/ { print FILENAME }' {} + | wc -l)"; \
|
|
179 |
+ |
rendered="$(find /staged-skel -type f | wc -l)"; \
|
|
180 |
+ |
[ "$rendered" -eq "$((templates + variants))" ] \
|
|
181 |
+ |
|| { echo "skelgen wrote $rendered files; $templates templates of which $variants render twice should have produced $((templates + variants))" >&2; exit 1; }; \
|
|
182 |
+ |
for table in /staged-skel/usr/share/alloy/vtrgb /staged-skel/usr/share/alloy/vtrgb.night; do \
|
|
183 |
+ |
awk 'NF != 16 { exit 1 } END { if (NR != 3) exit 1 }' "$table" \
|
|
184 |
+ |
|| { echo "$table is not three rows of sixteen values; setvtrgb would reject it" >&2; exit 1; }; \
|
|
185 |
+ |
done; \
|
|
186 |
+ |
for night in $(find /staged-skel/etc/skel -type f -name '*.night'); do \
|
|
187 |
+ |
rel="${night#/staged-skel/etc/skel/}"; \
|
|
188 |
+ |
dest="/staged-skel/usr/share/alloy/skel-night/${rel%.night}"; \
|
|
189 |
+ |
mkdir -p "$(dirname "$dest")"; \
|
|
190 |
+ |
mv "$night" "$dest"; \
|
|
191 |
+ |
done; \
|
|
192 |
+ |
[ -z "$(find /staged-skel/etc/skel -name '*.night')" ] \
|
|
193 |
+ |
|| { echo "a night render was left in /etc/skel; every new user would get it" >&2; exit 1; }; \
|
|
194 |
+ |
for day in $(find /staged-skel/etc/skel -type f); do \
|
|
195 |
+ |
rel="${day#/staged-skel/etc/skel/}"; \
|
|
196 |
+ |
case "$rel" in .config/helix/themes/akari-dawn.toml|.config/helix/themes/akari-night.toml) continue;; esac; \
|
|
197 |
+ |
[ -f "/staged-skel/usr/share/alloy/skel-night/$rel" ] \
|
|
198 |
+ |
|| { echo "$rel has no night render; a night session would keep the light one forever" >&2; exit 1; }; \
|
|
199 |
+ |
done; \
|
|
200 |
+ |
for applied in $(find /staged-skel/usr/share/alloy/skel-night -type f); do \
|
|
201 |
+ |
rel="${applied#/staged-skel/usr/share/alloy/skel-night/}"; \
|
|
202 |
+ |
[ -f "/staged-skel/etc/skel/$rel" ] \
|
|
203 |
+ |
|| { echo "night render $rel has no plain counterpart in /etc/skel" >&2; exit 1; }; \
|
|
204 |
+ |
done
|
|
205 |
+ |
|
|
206 |
+ |
# The two skeleton sources must not overlap.
|
|
207 |
+ |
#
|
|
208 |
+ |
# The runtime stage lays the repo's etc/skel down first and this rendered tree
|
|
209 |
+ |
# second, so a path present in both silently resolves to the render and leaves a
|
|
210 |
+ |
# committed file in the repo that nothing in the image ever reads. That is how
|
|
211 |
+ |
# etc/skel/.config/helix/config.toml would come back: it moved into templates/
|
|
212 |
+ |
# to carry `theme = "@{meta.id}"`, and a restored copy would look like the
|
|
213 |
+ |
# source of truth while the image used the other one.
|
|
214 |
+ |
#
|
|
215 |
+ |
# etc/skel is copied in here rather than compared in the runtime stage because
|
|
216 |
+ |
# this is the only stage that holds both trees, and this stage is discarded.
|
|
217 |
+ |
COPY etc/skel/ /src/etc-skel/
|
|
218 |
+ |
RUN set -eux; \
|
|
219 |
+ |
for staged in $(find /src/etc-skel -type f); do \
|
|
220 |
+ |
rel="${staged#/src/etc-skel/}"; \
|
|
221 |
+ |
[ ! -e "/staged-skel/etc/skel/$rel" ] \
|
|
222 |
+ |
|| { echo "etc/skel/$rel is also rendered from templates/; one of the two is dead" >&2; exit 1; }; \
|
|
223 |
+ |
done
|
| 133 |
224 |
|
|
| 134 |
225 |
|
# =====================================================================
|
| 135 |
226 |
|
# Runtime image — the bootable container itself.
|
| 579 |
670 |
|
# tokens. The generated tree mirrors `/` the same way, so it lands by the same
|
| 580 |
671 |
|
# 1:1 rule and simply completes the tree rather than patching it: no file
|
| 581 |
672 |
|
# appears in both, and a stale copy of a themed file cannot shadow its
|
| 582 |
|
- |
# generated version because there is no copy to go stale.
|
|
673 |
+ |
# generated version because there is no copy to go stale. "No file appears in
|
|
674 |
+ |
# both" is checked in the rust-build stage rather than assumed here, since this
|
|
675 |
+ |
# is the stage where a collision would resolve quietly in the render's favor.
|
| 583 |
676 |
|
# =====================================================================
|
| 584 |
677 |
|
COPY etc/ /etc/
|
| 585 |
678 |
|
COPY usr/ /usr/
|
| 586 |
679 |
|
COPY --from=rust-build /staged-skel/ /
|
| 587 |
680 |
|
|
|
681 |
+ |
# =====================================================================
|
|
682 |
+ |
# The session wrapper — assert it can actually run.
|
|
683 |
+ |
#
|
|
684 |
+ |
# etc/greetd/config.toml points --cmd at alloy-session rather than at
|
|
685 |
+ |
# sway, which puts this one script on the boot path: missing,
|
|
686 |
+ |
# non-executable, or carrying a bad shebang, and nobody can log into the
|
|
687 |
+ |
# machine at all, with the virtual console as the only recovery. Both
|
|
688 |
+ |
# failures are silent at build time and total at runtime, so they are
|
|
689 |
+ |
# checked here for the same reason the polkit and setvtrgb steps check
|
|
690 |
+ |
# theirs.
|
|
691 |
+ |
#
|
|
692 |
+ |
# `sh -n` parses without running, which is the whole check worth making:
|
|
693 |
+ |
# the script's job is one alloy call and one exec, and a syntax error is
|
|
694 |
+ |
# the only way that fails before it has a chance to matter.
|
|
695 |
+ |
# =====================================================================
|
|
696 |
+ |
RUN test -x /usr/bin/alloy-session \
|
|
697 |
+ |
|| { echo "alloy-session is missing or not executable; greetd --cmd would fail" >&2; exit 1; }
|
|
698 |
+ |
RUN sh -n /usr/bin/alloy-session \
|
|
699 |
+ |
|| { echo "alloy-session does not parse; no user could log in" >&2; exit 1; }
|
|
700 |
+ |
RUN grep -q -- '--cmd alloy-session' /etc/greetd/config.toml \
|
|
701 |
+ |
|| { echo "greetd does not launch the session wrapper; the skeleton would never be applied" >&2; exit 1; }
|
|
702 |
+ |
|
| 588 |
703 |
|
# =====================================================================
|
| 589 |
704 |
|
# polkit rules — assert the grant is not inert.
|
| 590 |
705 |
|
#
|
| 785 |
900 |
|
RUN test -s /usr/share/alloy/vtrgb \
|
| 786 |
901 |
|
|| { echo "the console palette did not arrive with the rendered tree" >&2; exit 1; }
|
| 787 |
902 |
|
|
|
903 |
+ |
# vtrgb.night is the dark console table, and nothing reads it yet. It is here so
|
|
904 |
+ |
# that switching the greeter's palette is a unit change rather than a render
|
|
905 |
+ |
# change: alloy-vtrgb.service runs before any user exists, so the per-user mode
|
|
906 |
+ |
# file cannot reach it, and the system-scoped setting that will choose between
|
|
907 |
+ |
# these two tables is still to come. Asserted anyway, because an unread file is
|
|
908 |
+ |
# exactly the kind that quietly stops being produced. The note lives here rather
|
|
909 |
+ |
# than in the template: templates/usr/share/alloy/vtrgb.in renders to nothing but
|
|
910 |
+ |
# the table, and setvtrgb rejects any line that is not sixteen values.
|
|
911 |
+ |
RUN test -s /usr/share/alloy/vtrgb.night \
|
|
912 |
+ |
|| { echo "the dark console palette did not arrive; the greeter would have nothing to switch to" >&2; exit 1; }
|
|
913 |
+ |
|
|
914 |
+ |
# The dark half of the skeleton, which `alloy theme apply` reads its whole
|
|
915 |
+ |
# manifest from. An empty or missing tree makes the verb a no-op that says so
|
|
916 |
+ |
# once per login and leaves every user on the light render forever, so it is
|
|
917 |
+ |
# worth one test here rather than a report from a booted machine.
|
|
918 |
+ |
#
|
|
919 |
+ |
# The pairing was already proved in the rust-build stage; what this repeats it
|
|
920 |
+ |
# for is the finished image, where /etc/skel is also written by every package
|
|
921 |
+ |
# that ships a skeleton file and by every COPY above. A night render whose day
|
|
922 |
+ |
# counterpart went missing between there and here is a file the pristine test
|
|
923 |
+ |
# can only decide by keeping, so the user stays on whatever they have.
|
|
924 |
+ |
RUN set -eux; \
|
|
925 |
+ |
test -n "$(find /usr/share/alloy/skel-night -type f -print -quit)" \
|
|
926 |
+ |
|| { echo "the night skeleton did not arrive; alloy theme apply would have nothing to apply" >&2; exit 1; }; \
|
|
927 |
+ |
for applied in $(find /usr/share/alloy/skel-night -type f); do \
|
|
928 |
+ |
rel="${applied#/usr/share/alloy/skel-night/}"; \
|
|
929 |
+ |
[ -f "/etc/skel/$rel" ] \
|
|
930 |
+ |
|| { echo "/etc/skel/$rel is gone; the night render of it has nothing to switch back to" >&2; exit 1; }; \
|
|
931 |
+ |
done
|
|
932 |
+ |
|
| 788 |
933 |
|
# =====================================================================
|
| 789 |
934 |
|
# bootc validation — fails the build if the image isn't a valid
|
| 790 |
935 |
|
# bootable container.
|