@jpgilldev / steid

steid/release.sh
10.0 KBRaw
1#!/usr/bin/env bash
2#
3# Build a Steid release artefact: a tarball that extracts to a self-contained
4# directory containing the binary, the asset bundle beside it, and a README.
5#
6# steid-<version>-<target>/
7# steid
8# assets/ <- manifest.toml + content-hashed CSS
9# README.md
10#
11# Why a plain binary and not a container: Steid is meant to be installable on a
12# £4 VPS by someone who does not run Docker. The container image still exists
13# (see ./Dockerfile) and this script uses Docker as a *build* tool, but nothing
14# in the shipped artefact depends on it.
15#
16# THE BUILD COMMAND MATTERS. `cargo build --release` alone produces a binary
17# that will not boot: `main` calls `AssetBundle::load()`, which walks up from the
18# executable looking for `assets/manifest.toml`, and `build.rs` never writes one.
19# `topcoat asset bundle --release` runs `cargo build --release` itself, then
20# scans the linked binary for the assets it declares and writes them plus the
21# manifest to `target/assets`. That is the only supported way to build Steid.
22#
23# ON MACOS you cannot produce a Linux artefact with the host toolchain — there is
24# no linker for it and `build.rs` runs a platform-specific Tailwind binary. This
25# script therefore builds inside a container of the target platform by default
26# (Docker, with qemu emulation when the arch differs from the host). `--native`
27# skips all that and builds with the local toolchain, which is what you want for
28# a quick smoke test of the artefact layout, not for a release.
29#
30# Usage:
31# ./release.sh # default target, via Docker
32# ./release.sh --target aarch64-unknown-linux-gnu
33# ./release.sh --all # every target a published release carries
34# ./release.sh --native # host target, local toolchain
35# ./release.sh --target x86_64-unknown-linux-gnu --version 0.1.0
36#
37set -euo pipefail
38
39# --- parameters -------------------------------------------------------------
40
41# glibc, not musl — measured, then decided.
42#
43# musl was tried and failed on `ring`: Debian's `musl-gcc` wrapper rejects the
44# `-m64` that cc-rs passes, so it would need a real cross toolchain rather than
45# `musl-tools`. But the decisive argument is not that it was awkward. **musl buys
46# a binary with no runtime dependencies, and Steid hard-requires `git` on PATH** —
47# anyone installing this already has a package manager and a distro, so the
48# portability is unusable. The musl path below still works if a cross toolchain
49# ever makes it worthwhile; nothing else in the script cares which is chosen.
50TARGET="${STEID_RELEASE_TARGET:-x86_64-unknown-linux-gnu}"
51
52# Bullseye pins the glibc floor at 2.31, which covers Debian 11+ and Ubuntu
53# 20.04+. Building on bookworm would need 2.36 and silently exclude Ubuntu 22.04,
54# which is still everywhere — and the failure lands on the user as
55# `GLIBC_2.36 not found`, at startup, with nothing pointing at the build.
56#
57# Pinned to 1.97 for the same reason the Dockerfile pins it: rustc >= 1.95 is a
58# hard floor for Topcoat 0.5, and on an older toolchain `topcoat` silently
59# resolves to an empty `v0.0.0` placeholder instead of failing.
60RUST_IMAGE="${STEID_RUST_IMAGE:-rust:1.97-slim-bullseye}"
61TOPCOAT_CLI_VERSION="${STEID_TOPCOAT_CLI_VERSION:-0.5.0}"
62
63VERSION=""
64OUT_DIR="dist"
65NATIVE=0
66ALL=0
67
68# Every target a published release carries. `install.sh` picks between them from
69# `uname -m`, so this list and its architecture detection have to agree: adding a
70# target here without teaching the installer about it produces a tarball nobody
71# ever downloads.
72ALL_TARGETS="x86_64-unknown-linux-gnu aarch64-unknown-linux-gnu"
73
74usage() {
75 sed -n '2,32p' "$0" | sed 's/^#\{1,2\} \{0,1\}//'
76 exit "${1:-0}"
77}
78
79while [ $# -gt 0 ]; do
80 case "$1" in
81 --target) TARGET="${2:?--target needs a triple}"; shift 2 ;;
82 --version) VERSION="${2:?--version needs a value}"; shift 2 ;;
83 --out) OUT_DIR="${2:?--out needs a directory}"; shift 2 ;;
84 --native) NATIVE=1; shift ;;
85 --all) ALL=1; shift ;;
86 -h|--help) usage 0 ;;
87 *) echo "release.sh: unknown argument: $1" >&2; usage 1 ;;
88 esac
89done
90
91REPO_ROOT="$(CDPATH='' cd -- "$(dirname -- "$0")" && pwd)"
92cd "$REPO_ROOT"
93
94# --all re-runs this script once per target rather than looping inline, so a single
95# failure aborts the whole release instead of leaving a half-published set where some
96# architectures have today's build and others have last week's.
97if [ "$ALL" = 1 ]; then
98 for t in $ALL_TARGETS; do
99 "$0" --target "$t" ${VERSION:+--version "$VERSION"} --out "$OUT_DIR" || exit 1
100 done
101 echo
102 echo "release.sh: built every target"
103 find "$OUT_DIR" -maxdepth 1 -name '*.tar.gz' -exec echo ' {}' \;
104 exit 0
105fi
106
107# The version is the crate version unless overridden. Read with grep rather than
108# a TOML parser so this script has no dependencies of its own.
109if [ -z "$VERSION" ]; then
110 VERSION="$(grep -m1 '^version *= *"' Cargo.toml | sed 's/.*"\(.*\)".*/\1/')"
111fi
112[ -n "$VERSION" ] || { echo "release.sh: could not determine version" >&2; exit 1; }
113
114if [ "$NATIVE" = 1 ]; then
115 TARGET="$(rustc -vV | sed -n 's/^host: //p')"
116fi
117
118NAME="steid-${VERSION}-${TARGET}"
119STAGE="${OUT_DIR}/${NAME}"
120
121echo "release.sh: building ${NAME}"
122
123# --- build ------------------------------------------------------------------
124
125rm -rf "$STAGE"
126mkdir -p "$STAGE"
127
128if [ "$NATIVE" = 1 ]; then
129 command -v topcoat >/dev/null 2>&1 || {
130 echo "release.sh: topcoat CLI not found." >&2
131 echo " cargo install topcoat-cli --version ${TOPCOAT_CLI_VERSION} --locked" >&2
132 exit 1
133 }
134 echo "release.sh: local build (host toolchain) — NOT a release artefact"
135 topcoat asset bundle --release
136 cp target/release/steid "$STAGE/steid"
137 cp -R target/assets "$STAGE/assets"
138else
139 command -v docker >/dev/null 2>&1 || {
140 echo "release.sh: docker not found, and a Linux artefact cannot be built" >&2
141 echo " with the host toolchain. Install Docker, or run on Linux with" >&2
142 echo " --native, or use a cross toolchain." >&2
143 exit 1
144 }
145
146 case "$TARGET" in
147 x86_64-*) PLATFORM="linux/amd64" ;;
148 aarch64-*) PLATFORM="linux/arm64" ;;
149 *) echo "release.sh: don't know the Docker platform for ${TARGET}" >&2; exit 1 ;;
150 esac
151
152 # musl needs the cross-linker and the std for the target. On a glibc builder
153 # image this is a cross-compile even when the arch matches the host, which is
154 # the point: build.rs keeps running against glibc.
155 EXTRA_SETUP=""
156 BIN_PATH="target/release/steid"
157 case "$TARGET" in
158 *-musl)
159 EXTRA_SETUP="apt-get update && apt-get install -y --no-install-recommends musl-tools && rm -rf /var/lib/apt/lists/* && rustup target add ${TARGET} && export CARGO_BUILD_TARGET=${TARGET}"
160 BIN_PATH="target/${TARGET}/release/steid"
161 ;;
162 esac
163
164 echo "release.sh: building in ${RUST_IMAGE} on ${PLATFORM}"
165
166 # A throwaway image built from a heredoc rather than the repo Dockerfile: that
167 # one produces a runtime *image*, this one produces files to copy out. Keeping
168 # them separate means neither has to compromise for the other.
169 IMAGE_TAG="steid-release-build:${VERSION}-${TARGET}"
170 docker buildx build \
171 --platform "$PLATFORM" \
172 --load \
173 --tag "$IMAGE_TAG" \
174 --build-arg "TARGET=${TARGET}" \
175 --file - . <<EOF
176FROM ${RUST_IMAGE}
177ARG TARGET
178RUN cargo install topcoat-cli --version ${TOPCOAT_CLI_VERSION} --locked
179WORKDIR /src
180COPY . .
181RUN set -eux; ${EXTRA_SETUP:-true}; \\
182 topcoat asset bundle --release; \\
183 mkdir -p /out; \\
184 cp ${BIN_PATH} /out/steid; \\
185 cp -r target/assets /out/assets
186EOF
187
188 # `docker create` + `docker cp` rather than a bind mount: the build ran on a
189 # possibly-emulated platform and this needs no write access to the host tree.
190 CONTAINER="$(docker create --platform "$PLATFORM" "$IMAGE_TAG" /bin/true)"
191 trap 'docker rm -f "$CONTAINER" >/dev/null 2>&1 || true' EXIT
192 docker cp "${CONTAINER}:/out/steid" "$STAGE/steid"
193 docker cp "${CONTAINER}:/out/assets" "$STAGE/assets"
194 docker rm -f "$CONTAINER" >/dev/null
195 trap - EXIT
196fi
197
198chmod 755 "$STAGE/steid"
199
200# The README ships inside the tarball so an unpacked directory on a server is
201# self-explanatory without network access.
202cp README.md "$STAGE/README.md"
203
204# --- sanity checks ----------------------------------------------------------
205
206# The single failure mode worth guarding: an artefact whose assets are missing or
207# in the wrong place boots fine in CI and dies on the user's first request.
208[ -f "$STAGE/assets/manifest.toml" ] || {
209 echo "release.sh: assets/manifest.toml is missing — was this built with" >&2
210 echo " 'topcoat asset bundle' and not a bare 'cargo build'?" >&2
211 exit 1
212}
213
214# --- package ----------------------------------------------------------------
215
216TARBALL="${OUT_DIR}/${NAME}.tar.gz"
217# `--no-xattrs` and COPYFILE_DISABLE because macOS's bsdtar otherwise stores
218# Apple extended attributes, and GNU tar on the machine that extracts this then
219# prints a warning line per file: "Ignoring unknown extended header keyword
220# 'LIBARCHIVE.xattr.com.apple.provenance'". Harmless, and it makes a release look
221# broken in the first thirty seconds a stranger spends with it.
222COPYFILE_DISABLE=1 tar --no-xattrs -czf "$TARBALL" -C "$OUT_DIR" "$NAME" 2>/dev/null \
223 || COPYFILE_DISABLE=1 tar -czf "$TARBALL" -C "$OUT_DIR" "$NAME"
224
225# A checksum file per tarball, which is what install.sh fetches and verifies.
226# Written next to the tarball with a bare name inside it so `sha256sum -c` works
227# from the download directory.
228(
229 cd "$OUT_DIR"
230 if command -v sha256sum >/dev/null 2>&1; then
231 sha256sum "${NAME}.tar.gz" > "${NAME}.tar.gz.sha256"
232 else
233 # macOS has shasum, not sha256sum. Same output format.
234 shasum -a 256 "${NAME}.tar.gz" > "${NAME}.tar.gz.sha256"
235 fi
236)
237
238echo
239echo "release.sh: wrote"
240echo " ${TARBALL}"
241echo " ${TARBALL}.sha256"
242echo
243echo "Upload both to the release named v${VERSION}. install.sh expects exactly"
244echo "these filenames."