#!/usr/bin/env bash set -Eeuo pipefail # Build a self-contained release on the target Linux architecture. Native # addons (SQLite, Argon2 and image processing) must be installed on the same # architecture/libc as the artifact. ROOT=$(cd -- "$(dirname -- "$0")/.." && pwd -P) VERSION=${1:-} OUT_DIR=${2:-$ROOT/release} [[ "$(uname -s)" == "Linux" ]] || { printf 'release builds must run on Linux; detected %s\n' "$(uname -s)" >&2; exit 2; } if [[ -z "$VERSION" ]]; then VERSION=$(node -p 'require("./package.json").version') fi VERSION=${VERSION#v} [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?(\+[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$ ]] || { printf 'invalid version: %s\n' "$VERSION" >&2; exit 2; } case "$(uname -m)" in x86_64|amd64) ARCH=x64 ;; aarch64|arm64) ARCH=arm64 ;; armv7l|armv7|armhf) ARCH=armv7 ;; *) printf 'unsupported architecture: %s\n' "$(uname -m)" >&2; exit 2 ;; esac LIBC=glibc if command -v ldd >/dev/null 2>&1 && ldd --version 2>&1 | grep -qi musl; then LIBC=musl; fi cd "$ROOT" pnpm build stage=$(mktemp -d) trap 'rm -rf "$stage"' EXIT mkdir -p "$stage/dist" "$stage/migrations" "$stage/bin" "$stage/scripts" "$stage/systemd" "$stage/runtime/bin" # Copy only the production build outputs. In particular, do not carry a # stale dist/web-next directory from a previous local preview build. cp -a dist/server "$stage/dist/" cp -a dist/shared "$stage/dist/" cp -a dist/web "$stage/dist/" cp -a migrations/. "$stage/migrations/" cp package.json pnpm-lock.yaml "$stage/" cp -a bin/. "$stage/bin/" cp -a scripts/tallynote-update.sh scripts/tallynote-update-runner.sh "$stage/scripts/" cp uninstall.sh "$stage/uninstall.sh" cp -a systemd/tallynote.service systemd/tallynote-update.service systemd/tallynote-update.path systemd/tallynote.env.example "$stage/systemd/" node_path=$(command -v node) cp -L "$node_path" "$stage/runtime/bin/node" chmod 755 "$stage/bin/tallynote" "$stage/bin/tallynote-admin-init" "$stage/scripts"/*.sh "$stage/runtime/bin/node" "$stage/uninstall.sh" # pnpm's default linker creates symlinks. A release archive is deliberately # symlink-free so the installer can reject traversal links deterministically. (cd "$stage" && pnpm install --prod --node-linker=hoisted --frozen-lockfile) find "$stage" -type l -delete mkdir -p "$OUT_DIR" archive="$OUT_DIR/tallynote-${VERSION}-linux-${ARCH}-${LIBC}.tar.gz" tar -C "$stage" -czf "$archive" --owner=0 --group=0 --numeric-owner . # The application-only asset is used by the online updater. It deliberately # excludes the stable runtime (Node and production dependencies), systemd # helpers and installer files; the updater overlays it on the currently # installed, already-validated runtime before atomically switching releases. app_stage=$(mktemp -d) trap 'rm -rf "$stage" "$app_stage"' EXIT mkdir -p "$app_stage/dist" "$app_stage/migrations" "$app_stage/bin" "$app_stage/scripts" "$app_stage/systemd" cp -a "$stage/dist/server" "$app_stage/dist/" cp -a "$stage/dist/shared" "$app_stage/dist/" cp -a "$stage/dist/web" "$app_stage/dist/" cp -a "$stage/migrations/." "$app_stage/migrations/" cp -a "$stage/bin/." "$app_stage/bin/" cp -a "$stage/scripts/." "$app_stage/scripts/" cp -a "$stage/systemd/." "$app_stage/systemd/" cp "$stage/package.json" "$app_stage/package.json" cp "$stage/uninstall.sh" "$app_stage/uninstall.sh" runtime_hash=$(sha256sum pnpm-lock.yaml | awk '{print $1}') app_archive="$OUT_DIR/tallynote-${VERSION}-linux-${ARCH}-${LIBC}.update-${runtime_hash}.tar.gz" tar -C "$app_stage" -czf "$app_archive" --owner=0 --group=0 --numeric-owner . # Keep the sidecar useful when a caller builds more than one architecture into # the same directory. The publishing script recomputes this list immediately # before signing, so stale or hand-edited entries can never reach a Release. (cd "$OUT_DIR" && sha256sum ./*.tar.gz | sed 's#^\./##' | LC_ALL=C sort > SHA256SUMS) printf 'built %s\n' "$archive"