max / alloy
| 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 | |
| 49 | |
| 50 | HERE="" |
| 51 | REPO_ROOT="" |
| 52 | |
| 53 | # shellcheck source=build/privilege.sh |
| 54 | |
| 55 | |
| 56 | |
| 57 | |
| 58 | |
| 59 | BINARY="" |
| 60 | COMPONENT="alloy" |
| 61 | OUT="/out" |
| 62 | while [; do |
| 63 | case "" in |
| 64 | --binary) BINARY=""; ;; |
| 65 | --component) COMPONENT=""; ;; |
| 66 | --out) OUT=""; ;; |
| 67 | *) ;; |
| 68 | esac |
| 69 | done |
| 70 | |
| 71 | case "" in |
| 72 | alloy|shop) ;; |
| 73 | *) ;; |
| 74 | esac |
| 75 | [ || [ \ |
| 76 | || |
| 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 [; then |
| 88 | VERSION="" |
| 89 | [ || |
| 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 | [ || |
| 95 | else |
| 96 | [ || |
| 97 | VERSION="" |
| 98 | [ || |
| 99 | fi |
| 100 | |
| 101 | if [; then |
| 102 | |
| 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 | ( && ) |
| 115 | BINARY="/target/release/alloy" |
| 116 | fi |
| 117 | [ || |
| 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="" |
| 128 | [ \ |
| 129 | || |
| 130 | |
| 131 | |
| 132 | |
| 133 | |
| 134 | |
| 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 | |
| 142 | |
| 143 | -v ":/spec:ro,z" \ |
| 144 | -v ":/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 '_version ' \ |
| 150 | -bb /spec/.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 | |
| 159 | |
| 160 | |
| 161 | |
| 162 | |
| 163 | |
| 164 | |
| 165 | |
| 166 | |
| 167 | |
| 168 | |
| 169 | |
| 170 | |
| 171 | |
| 172 |