Skip to main content

max / alloy

7.9 KB · 172 lines History Blame Raw
1 #!/usr/bin/env bash
2 #
3 # build.sh — package a component as an RPM, and put it in a repo.
4 #
5 # build.sh build the console from the tree, package it
6 # build.sh --binary PATH package a binary somebody else already built
7 # build.sh --component shop \
8 # --binary PATH package the terminal
9 # build.sh --out DIR write somewhere other than build/rpm/out
10 #
11 # The console is the default because it is the one component this repo can
12 # build. shop lives in its own repo, so there is nothing here to compile and
13 # --binary is the only way to package it — normally the binary the image
14 # shipped, taken out of a build with `podman cp`, so the hotfix is byte-for-byte
15 # what was tested. The image builds both itself, from these same specs, in the
16 # Containerfile's rust-build stage.
17 #
18 # This is the supply side of the hotfix channel (GoingsOn task d866e125). Alloy
19 # is distributed as a builder rather than as an image, so a machine that is
20 # already installed has no way to receive a fix short of rebuilding an ISO and
21 # writing a drive. A signed repo carrying only Make Creative's own components,
22 # layered with rpm-ostree, is the answer; this produces what it serves.
23 #
24 # WHAT MUST NOT HAPPEN TO THIS PACKAGE. It must never end up in a base image.
25 # A component the base carries cannot be replaced client-side: `rpm-ostree
26 # install` fails to depsolve against it, and `override replace` records a
27 # request that never activates and silently survives a reboot as a no-op. So a
28 # base that ships the console is a machine that can never be sent a console
29 # fix. Measured, in build/layertest — read its README before changing where
30 # this package lands.
31 #
32 # Unsigned so far, and the decision is in: **both repos get signed**, the
33 # carried one included. Ruled by Max 2026-08-17 over the recommendation, which
34 # was to leave the carried repo explicitly unsigned on the grounds that it
35 # reaches no network and is only as trustworthy as the image around it. What
36 # the ruling buys is one code path: the carried repo and the network repo are
37 # built, configured and verified the same way, so no branch exists that only
38 # the network path exercises, and the key and its handling exist before
39 # anything is under pressure to ship.
40 #
41 # So `gpgcheck=0` in the generated .repo file is unfinished work, not a
42 # position. What is left is the key: it is generated under the same rule as
43 # the sops identities (never in the repo it signs, never synced by anything),
44 # and where it lives is settled before it is generated. Then this script signs
45 # what it builds, the public key ships in the image, the .repo snippets carry
46 # `gpgcheck=1` and a `gpgkey`, and a test fails the build if the carried repo
47 # does not verify. GoingsOn alloy d866e125.
48 set -euo pipefail
49
50 HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
51 REPO_ROOT="$(cd "$HERE/../.." && pwd)"
52
53 # shellcheck source=build/privilege.sh
54 . "$REPO_ROOT/build/privilege.sh"
55
56 die() { printf 'error: %s\n' "$*" >&2; exit 1; }
57 say() { printf '%s\n' "$*"; }
58
59 BINARY=""
60 COMPONENT="alloy"
61 OUT="$HERE/out"
62 while [ $# -gt 0 ]; do
63 case "$1" in
64 --binary) BINARY="${2:-}"; shift 2 ;;
65 --component) COMPONENT="${2:-}"; shift 2 ;;
66 --out) OUT="${2:-}"; shift 2 ;;
67 *) die "unknown argument $1" ;;
68 esac
69 done
70
71 case "$COMPONENT" in
72 alloy|shop) ;;
73 *) die "unknown component $COMPONENT (alloy, shop)" ;;
74 esac
75 [ "$COMPONENT" != shop ] || [ -n "$BINARY" ] \
76 || die "shop is a separate repo; package it with --binary PATH"
77
78 # Where the version comes from, and it is never an argument to this script. An
79 # RPM that claims a version the binary inside it does not report is invisible
80 # afterwards: it installs, rpm answers one thing, `--version` answers another,
81 # and `--version` is what a person quotes in a bug report.
82 #
83 # The console has a crate here to read, so that is the source and the binary is
84 # checked against it below. shop does not, so its own `--version` is the only
85 # statement of record — which is why it is asked before anything else happens
86 # rather than trusted after packaging.
87 if [ "$COMPONENT" = alloy ]; then
88 VERSION="$(grep -m1 '^version' "$REPO_ROOT/crates/alloy/Cargo.toml" | cut -d'"' -f2)"
89 [ -n "$VERSION" ] || die "no version in crates/alloy/Cargo.toml"
90 # 0.0.0 was the placeholder every console build carried until 2026-08-14.
91 # Kept as a guard rather than deleted with the placeholder: an RPM channel
92 # ships a HIGHER version of a component, and there is nothing for a hotfix to
93 # be higher than if this regresses.
94 [ "$VERSION" != "0.0.0" ] || die "the console is back at 0.0.0, so no hotfix can outrank it"
95 else
96 [ -x "$BINARY" ] || die "no shop binary at $BINARY"
97 VERSION="$("$BINARY" --version 2>/dev/null | head -1 | awk '{print $2}')"
98 [ -n "$VERSION" ] || die "$BINARY did not report a version"
99 fi
100
101 if [ -z "$BINARY" ]; then
102 say "building the console $VERSION"
103 # No --locked, unlike the Containerfile, and the difference is the build
104 # location rather than a difference in intent. This runs on the dev host,
105 # inside ~/Code, where `.cargo/config.toml`'s `[patch]` block redirects the
106 # cross-repo git deps to the working copies; under it every resolve rewrites
107 # the lock, so --locked fails on every machine that has the redirect rather
108 # than catching anything (CLAUDE.md states this tree-wide). The Containerfile
109 # builds in a container that has no such block, which is why it can pass it.
110 #
111 # What that costs: a hotfix built here takes whatever the sibling working
112 # copies currently hold, so build it from a clean tree, and prefer handing in
113 # a binary from the image build with --binary when the two must match.
114 (cd "$REPO_ROOT" && cargo build --release -p alloy)
115 BINARY="$REPO_ROOT/target/release/alloy"
116 fi
117 [ -x "$BINARY" ] || die "no $COMPONENT binary at $BINARY"
118
119 # Checked rather than trusted. Packaging a binary whose version disagrees with
120 # the spec's is the one mistake this script exists to make impossible, and it
121 # is invisible afterwards: the RPM installs, the machine reports the version
122 # rpm knows, and the binary reports another.
123 #
124 # For shop the two came from the same place a moment ago, so this is a re-read
125 # rather than a comparison. Left in the same path anyway: --binary can point at
126 # one file and the version have been taken from another only if this changes.
127 REPORTED="$("$BINARY" --version 2>/dev/null | head -1 | awk '{print $2}')"
128 [ "$REPORTED" = "$VERSION" ] \
129 || die "binary reports $REPORTED, expected $VERSION"
130
131 mkdir -p "$OUT"
132 rm -rf "${OUT:?}/rpmbuild" "${OUT:?}/repo"
133 mkdir -p "$OUT/rpmbuild/SOURCES" "$OUT/repo"
134 install -m 0755 "$BINARY" "$OUT/rpmbuild/SOURCES/$COMPONENT"
135
136 # In a container rather than on the host: rpmbuild and createrepo_c are not on
137 # a dev box by default, and the package has to carry the same %{dist} as the
138 # base it installs into. fedora:43 matches the image; a package built as .fc42
139 # lands in a repo the machine will resolve and then refuses to be what anyone
140 # asked for.
141 say "packaging $COMPONENT-$VERSION"
142 privc podman run --rm \
143 -v "$HERE:/spec:ro,z" \
144 -v "$OUT:/out:z" \
145 -w /out \
146 registry.fedoraproject.org/fedora:43 bash -c "
147 set -e
148 dnf -y install rpm-build createrepo_c >/dev/null 2>&1
149 rpmbuild --define '_topdir /out/rpmbuild' --define '${COMPONENT}_version $VERSION' \
150 -bb /spec/$COMPONENT.spec >/dev/null
151 cp /out/rpmbuild/RPMS/*/*.rpm /out/repo/
152 createrepo_c /out/repo >/dev/null
153 " >/dev/null
154
155 # podman writes as root through the bind mount, so the output is not the
156 # invoking user's without this. Left as a separate step rather than folded into
157 # the container command, which cannot know the uid outside it.
158 privc chown -R "$(id -u):$(id -g)" "$OUT"
159
160 say ""
161 say "built:"
162 find "$OUT/repo" -name '*.rpm' -printf ' %f\n'
163 say ""
164 say "repo metadata in $OUT/repo"
165 say "serve that directory and point a machine at it with a .repo file:"
166 say ""
167 say " [alloy]"
168 say " name=Alloy components"
169 say " baseurl=https://<host>/rpm/"
170 say " enabled=1"
171 say " gpgcheck=0 # until signing lands; both repos are to be signed"
172