steid

@jamesgill /

steid/release.sh
9.0 KBCode·Blame·Raw
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-musl
33# ./release.sh --native # host target, local toolchain
34# ./release.sh --target x86_64-unknown-linux-gnu --version 0.1.0
35#
36set -euo pipefail
37
38# --- parameters -------------------------------------------------------------
39
40# glibc, not musl — measured, then decided.
41#
42# musl was tried and failed on `ring`: Debian's `musl-gcc` wrapper rejects the
43# `-m64` that cc-rs passes, so it would need a real cross toolchain rather than
44# `musl-tools`. But the decisive argument is not that it was awkward. **musl buys
45# a binary with no runtime dependencies, and Steid hard-requires `git` on PATH** —
46# anyone installing this already has a package manager and a distro, so the
47# portability is unusable. The musl path below still works if a cross toolchain
48# ever makes it worthwhile; nothing else in the script cares which is chosen.
49TARGET="${STEID_RELEASE_TARGET:-x86_64-unknown-linux-gnu}"
50
51# Bullseye pins the glibc floor at 2.31, which covers Debian 11+ and Ubuntu
52# 20.04+. Building on bookworm would need 2.36 and silently exclude Ubuntu 22.04,
53# which is still everywhere — and the failure lands on the user as
54# `GLIBC_2.36 not found`, at startup, with nothing pointing at the build.
55#
56# Pinned to 1.97 for the same reason the Dockerfile pins it: rustc >= 1.95 is a
57# hard floor for Topcoat 0.5, and on an older toolchain `topcoat` silently
58# resolves to an empty `v0.0.0` placeholder instead of failing.
59RUST_IMAGE="${STEID_RUST_IMAGE:-rust:1.97-slim-bullseye}"
60TOPCOAT_CLI_VERSION="${STEID_TOPCOAT_CLI_VERSION:-0.5.0}"
61
62VERSION=""
63OUT_DIR="dist"
64NATIVE=0
65
66usage() {
67 sed -n '2,32p' "$0" | sed 's/^#\{1,2\} \{0,1\}//'
68 exit "${1:-0}"
69}
70
71while [ $# -gt 0 ]; do
72 case "$1" in
73 --target) TARGET="${2:?--target needs a triple}"; shift 2 ;;
74 --version) VERSION="${2:?--version needs a value}"; shift 2 ;;
75 --out) OUT_DIR="${2:?--out needs a directory}"; shift 2 ;;
76 --native) NATIVE=1; shift ;;
77 -h|--help) usage 0 ;;
78 *) echo "release.sh: unknown argument: $1" >&2; usage 1 ;;
79 esac
80done
81
82REPO_ROOT="$(CDPATH='' cd -- "$(dirname -- "$0")" && pwd)"
83cd "$REPO_ROOT"
84
85# The version is the crate version unless overridden. Read with grep rather than
86# a TOML parser so this script has no dependencies of its own.
87if [ -z "$VERSION" ]; then
88 VERSION="$(grep -m1 '^version *= *"' Cargo.toml | sed 's/.*"\(.*\)".*/\1/')"
89fi
90[ -n "$VERSION" ] || { echo "release.sh: could not determine version" >&2; exit 1; }
91
92if [ "$NATIVE" = 1 ]; then
93 TARGET="$(rustc -vV | sed -n 's/^host: //p')"
94fi
95
96NAME="steid-${VERSION}-${TARGET}"
97STAGE="${OUT_DIR}/${NAME}"
98
99echo "release.sh: building ${NAME}"
100
101# --- build ------------------------------------------------------------------
102
103rm -rf "$STAGE"
104mkdir -p "$STAGE"
105
106if [ "$NATIVE" = 1 ]; then
107 command -v topcoat >/dev/null 2>&1 || {
108 echo "release.sh: topcoat CLI not found." >&2
109 echo " cargo install topcoat-cli --version ${TOPCOAT_CLI_VERSION} --locked" >&2
110 exit 1
111 }
112 echo "release.sh: local build (host toolchain) — NOT a release artefact"
113 topcoat asset bundle --release
114 cp target/release/steid "$STAGE/steid"
115 cp -R target/assets "$STAGE/assets"
116else
117 command -v docker >/dev/null 2>&1 || {
118 echo "release.sh: docker not found, and a Linux artefact cannot be built" >&2
119 echo " with the host toolchain. Install Docker, or run on Linux with" >&2
120 echo " --native, or use a cross toolchain." >&2
121 exit 1
122 }
123
124 case "$TARGET" in
125 x86_64-*) PLATFORM="linux/amd64" ;;
126 aarch64-*) PLATFORM="linux/arm64" ;;
127 *) echo "release.sh: don't know the Docker platform for ${TARGET}" >&2; exit 1 ;;
128 esac
129
130 # musl needs the cross-linker and the std for the target. On a glibc builder
131 # image this is a cross-compile even when the arch matches the host, which is
132 # the point: build.rs keeps running against glibc.
133 EXTRA_SETUP=""
134 BIN_PATH="target/release/steid"
135 case "$TARGET" in
136 *-musl)
137 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}"
138 BIN_PATH="target/${TARGET}/release/steid"
139 ;;
140 esac
141
142 echo "release.sh: building in ${RUST_IMAGE} on ${PLATFORM}"
143
144 # A throwaway image built from a heredoc rather than the repo Dockerfile: that
145 # one produces a runtime *image*, this one produces files to copy out. Keeping
146 # them separate means neither has to compromise for the other.
147 IMAGE_TAG="steid-release-build:${VERSION}-${TARGET}"
148 docker buildx build \
149 --platform "$PLATFORM" \
150 --load \
151 --tag "$IMAGE_TAG" \
152 --build-arg "TARGET=${TARGET}" \
153 --file - . <<EOF
154FROM ${RUST_IMAGE}
155ARG TARGET
156RUN cargo install topcoat-cli --version ${TOPCOAT_CLI_VERSION} --locked
157WORKDIR /src
158COPY . .
159RUN set -eux; ${EXTRA_SETUP:-true}; \\
160 topcoat asset bundle --release; \\
161 mkdir -p /out; \\
162 cp ${BIN_PATH} /out/steid; \\
163 cp -r target/assets /out/assets
164EOF
165
166 # `docker create` + `docker cp` rather than a bind mount: the build ran on a
167 # possibly-emulated platform and this needs no write access to the host tree.
168 CONTAINER="$(docker create --platform "$PLATFORM" "$IMAGE_TAG" /bin/true)"
169 trap 'docker rm -f "$CONTAINER" >/dev/null 2>&1 || true' EXIT
170 docker cp "${CONTAINER}:/out/steid" "$STAGE/steid"
171 docker cp "${CONTAINER}:/out/assets" "$STAGE/assets"
172 docker rm -f "$CONTAINER" >/dev/null
173 trap - EXIT
174fi
175
176chmod 755 "$STAGE/steid"
177
178# The README ships inside the tarball so an unpacked directory on a server is
179# self-explanatory without network access.
180cp README.md "$STAGE/README.md"
181
182# --- sanity checks ----------------------------------------------------------
183
184# The single failure mode worth guarding: an artefact whose assets are missing or
185# in the wrong place boots fine in CI and dies on the user's first request.
186[ -f "$STAGE/assets/manifest.toml" ] || {
187 echo "release.sh: assets/manifest.toml is missing — was this built with" >&2
188 echo " 'topcoat asset bundle' and not a bare 'cargo build'?" >&2
189 exit 1
190}
191
192# --- package ----------------------------------------------------------------
193
194TARBALL="${OUT_DIR}/${NAME}.tar.gz"
195# `--no-xattrs` and COPYFILE_DISABLE because macOS's bsdtar otherwise stores
196# Apple extended attributes, and GNU tar on the machine that extracts this then
197# prints a warning line per file: "Ignoring unknown extended header keyword
198# 'LIBARCHIVE.xattr.com.apple.provenance'". Harmless, and it makes a release look
199# broken in the first thirty seconds a stranger spends with it.
200COPYFILE_DISABLE=1 tar --no-xattrs -czf "$TARBALL" -C "$OUT_DIR" "$NAME" 2>/dev/null \
201 || COPYFILE_DISABLE=1 tar -czf "$TARBALL" -C "$OUT_DIR" "$NAME"
202
203# A checksum file per tarball, which is what install.sh fetches and verifies.
204# Written next to the tarball with a bare name inside it so `sha256sum -c` works
205# from the download directory.
206(
207 cd "$OUT_DIR"
208 if command -v sha256sum >/dev/null 2>&1; then
209 sha256sum "${NAME}.tar.gz" > "${NAME}.tar.gz.sha256"
210 else
211 # macOS has shasum, not sha256sum. Same output format.
212 shasum -a 256 "${NAME}.tar.gz" > "${NAME}.tar.gz.sha256"
213 fi
214)
215
216echo
217echo "release.sh: wrote"
218echo " ${TARBALL}"
219echo " ${TARBALL}.sha256"
220echo
221echo "Upload both to the release named v${VERSION}. install.sh expects exactly"
222echo "these filenames."