#!/usr/bin/env bash set -euo pipefail ROOT="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" cd "$ROOT" DIST="$ROOT/dist" CARGO_TARGET_DIR="${CARGO_TARGET_DIR:-$ROOT/target}" export CARGO_TARGET_DIR fail() { printf 'error: %s\n' "$*" >&2 exit 1 } need() { command -v "$1" >/dev/null 2>&1 || fail "$1 is required" } need cargo need rustc need python3 need sha256sum need tar need git python3 -c 'import sys; sys.exit(0 if sys.version_info >= (3,10) else 1)' \ || fail 'Python 3.10+ is required' cargo fmt --version >/dev/null 2>&1 \ || fail 'rustfmt component is required (rustup component add rustfmt)' cargo clippy --version >/dev/null 2>&1 \ || fail 'clippy component is required (rustup component add clippy)' [[ -f Cargo.toml ]] || fail 'run this script from the RustPad project root' [[ -f Cargo.lock ]] || fail 'Cargo.lock is required for a reproducible release' [[ -f make_zip.py ]] || fail 'make_zip.py is missing' [[ -f scripts/update_browser_libs.py ]] || fail 'scripts/update_browser_libs.py is missing' [[ -f scripts/browser-libs.lock.json ]] || fail 'scripts/browser-libs.lock.json is missing' [[ -d static ]] || fail 'static directory is missing' [[ -d migrations ]] || fail 'migrations directory is missing' git rev-parse --is-inside-work-tree >/dev/null 2>&1 \ || fail 'release source archive requires a Git working tree' METADATA="$(cargo metadata --locked --no-deps --format-version 1)" read -r PACKAGE VERSION REQUIRED_RUST < <( printf '%s' "$METADATA" | python3 -c ' import json, sys data = json.load(sys.stdin) packages = data.get("packages", []) pkg = next((p for p in packages if p.get("name") == "rustpad"), None) if pkg is None: raise SystemExit("rustpad package not found in Cargo metadata") print(pkg["name"], pkg["version"], pkg.get("rust_version") or "0") ' ) [[ "$PACKAGE" == "rustpad" ]] || fail 'Cargo package rustpad was not found' [[ -n "$VERSION" ]] || fail 'could not determine project version' RUSTC_VERSION="$(rustc -V | awk '{print $2}')" HOST="$(rustc -vV | sed -n 's/^host: //p')" [[ -n "$RUSTC_VERSION" ]] || fail 'could not determine rustc version' [[ -n "$HOST" ]] || fail 'could not determine Rust host target' python3 - "$RUSTC_VERSION" "$REQUIRED_RUST" <<'PY' \ || fail "Rust $REQUIRED_RUST+ is required (found $RUSTC_VERSION)" import re import sys def version_tuple(value: str) -> tuple[int, int, int]: match = re.match(r"^(\d+)\.(\d+)(?:\.(\d+))?", value) if not match: raise SystemExit(1) major, minor, patch = match.groups(default="0") return int(major), int(minor), int(patch) raise SystemExit(0 if version_tuple(sys.argv[1]) >= version_tuple(sys.argv[2]) else 1) PY printf 'RustPad %s release build for %s\n' "$VERSION" "$HOST" printf 'Preparing locked browser libraries...\n' python3 scripts/update_browser_libs.py --locked --strict printf '\nChecking formatting...\n' cargo fmt --all -- --check printf '\nRunning tests...\n' cargo test --locked --all-targets printf '\nRunning clippy...\n' cargo clippy --locked --all-targets -- -D warnings printf '\nBuilding release binary...\n' rm -rf "$CARGO_TARGET_DIR/release" cargo build --locked --release --bin rustpad BIN="$CARGO_TARGET_DIR/release/rustpad" [[ -x "$BIN" ]] || fail "release binary was not created: $BIN" ACTUAL_VERSION="$("$BIN" --version)" EXPECTED_VERSION="rustpad $VERSION" [[ "$ACTUAL_VERSION" == "$EXPECTED_VERSION" ]] \ || fail "built binary reports '$ACTUAL_VERSION', expected '$EXPECTED_VERSION'" printf 'Built: %s\n' "$ACTUAL_VERSION" rm -rf "$DIST" mkdir -p "$DIST" # Source archive. dist/ is ignored by Git, so generated release files are not included. SOURCE_ZIP="$DIST/RustPad-${VERSION}-source.zip" python3 make_zip.py "$SOURCE_ZIP" [[ -s "$SOURCE_ZIP" ]] || fail "source archive was not created: $SOURCE_ZIP" sha256sum "$SOURCE_ZIP" > "$SOURCE_ZIP.sha256" BINARY_ARTIFACT="rustpad-${VERSION}-${HOST}" cp "$BIN" "$DIST/$BINARY_ARTIFACT" chmod 0755 "$DIST/$BINARY_ARTIFACT" sha256sum "$DIST/$BINARY_ARTIFACT" > "$DIST/$BINARY_ARTIFACT.sha256" # RustPad needs static assets and SQL migrations at runtime. BUNDLE_NAME="RustPad-${VERSION}-${HOST}" BUNDLE_DIR="$DIST/$BUNDLE_NAME" BUNDLE_ARCHIVE="$DIST/$BUNDLE_NAME.tar.gz" mkdir -p "$BUNDLE_DIR/data/db" "$BUNDLE_DIR/data/files" cp "$BIN" "$BUNDLE_DIR/rustpad" chmod 0755 "$BUNDLE_DIR/rustpad" cp -a static "$BUNDLE_DIR/static" cp -a migrations "$BUNDLE_DIR/migrations" [[ -d systemd ]] && cp -a systemd "$BUNDLE_DIR/systemd" [[ -f README.md ]] && cp README.md "$BUNDLE_DIR/README.md" [[ -f LICENSE.md ]] && cp LICENSE.md "$BUNDLE_DIR/LICENSE.md" [[ -f .env.example ]] && cp .env.example "$BUNDLE_DIR/.env.example" cat > "$BUNDLE_DIR/RELEASE.txt" < "$BUNDLE_ARCHIVE.sha256" rm -rf "$BUNDLE_DIR" printf '\nRelease ready:\n' printf ' binary: %s\n' "$DIST/$BINARY_ARTIFACT" printf ' checksum: %s\n' "$DIST/$BINARY_ARTIFACT.sha256" printf ' runtime: %s\n' "$BUNDLE_ARCHIVE" printf ' checksum: %s\n' "$BUNDLE_ARCHIVE.sha256" printf ' source zip: %s\n' "$SOURCE_ZIP" printf ' checksum: %s\n' "$SOURCE_ZIP.sha256"