#!/usr/bin/env bash # Build (and by default install) an Arch Linux package for BikeControl. # # Tauri's bundler has no pacman target, so we ship a hand-written PKGBUILD in # packaging/arch/ and build it with makepkg. makepkg is Arch-specific and # refuses to run as root, so run this as your normal user — the install step # calls sudo pacman itself. # # The version comes from git: the tag on master/main, the branch name elsewhere. # # Usage: # scripts/build-arch.sh # build, then pacman -U the result # scripts/build-arch.sh --no-install # build only (CI / packaging runs) # scripts/build-arch.sh --sync-deps # let makepkg -s pacman-install makedeps # scripts/build-arch.sh --print-version # show the version that would be built # OUTPUT_DIR=dist scripts/build-arch.sh # also copy the .pkg.tar.zst there set -euo pipefail REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" INSTALL=1 SYNC_DEPS=0 PRINT_ONLY=0 for arg in "$@"; do case "$arg" in --no-install) INSTALL=0 ;; --install) INSTALL=1 ;; --sync-deps) SYNC_DEPS=1 ;; --print-version) PRINT_ONLY=1 ;; -h|--help) sed -n '2,15p' "${BASH_SOURCE[0]}"; exit 0 ;; *) echo "❌ Unknown option: $arg (try --help)" >&2; exit 2 ;; esac done [[ $PRINT_ONLY -eq 1 ]] || command -v makepkg >/dev/null || { echo "❌ makepkg not found — this script must run on an Arch-based system." >&2 exit 1 } [[ $PRINT_ONLY -eq 1 || $EUID -ne 0 ]] || { echo "❌ Do not run this as root; makepkg refuses to build as root." >&2 exit 1 } # --- Version ----------------------------------------------------------------- # Release builds (master/main, or a detached checkout as CI does on a tag) take # their version from the git tag. Branch builds take it from the branch name, so # a package sitting in /var/cache/pacman or `pacman -Qi bikecontrol` says which # branch it came from. Both get an .r.g suffix so two builds of the # same branch/tag are distinguishable and sort in commit order. # # pacman forbids hyphens, colons, slashes and whitespace in a pkgver, so branch # and tag names are sanitised to underscores. Note that pacman's vercmp ranks a # leading digit above a leading letter, so installing a branch build on top of a # release build reads as a "downgrade" — harmless, and --noconfirm accepts it. _cargo_version() { sed -n '/^\[workspace\.package\]/,/^\[/ s/^version[[:space:]]*=[[:space:]]*"\([^"]*\)".*/\1/p' \ "$REPO_ROOT/Cargo.toml" | head -1 } _sanitize() { printf '%s' "$1" | sed 's/[^A-Za-z0-9._+]/_/g'; } # Sets the globals PKGVER and VERSION_SOURCE (rather than echoing, so the # "where did this version come from" note survives out of a subshell). derive_pkgver() { local cargover branch desc tag rest count sha cargover="$(_cargo_version)" [[ -n "$cargover" ]] || { echo "❌ Could not read version from Cargo.toml" >&2; exit 1; } if ! git -C "$REPO_ROOT" rev-parse --git-dir >/dev/null 2>&1; then VERSION_SOURCE="Cargo.toml (not a git checkout)" PKGVER="$cargover"; return fi count="$(git -C "$REPO_ROOT" rev-list --count HEAD 2>/dev/null || echo 0)" sha="g$(git -C "$REPO_ROOT" rev-parse --short HEAD 2>/dev/null || echo unknown)" # Empty on a detached HEAD — that is how CI checks out a tag, so treat it as a # release build rather than a branch build. branch="$(git -C "$REPO_ROOT" symbolic-ref --quiet --short HEAD 2>/dev/null || true)" if [[ -z "$branch" || "$branch" == master || "$branch" == main ]]; then if desc="$(git -C "$REPO_ROOT" describe --tags --long 2>/dev/null)"; then # v0.2.0-3-gabc1234 -> tag=v0.2.0, count=3. Strips the trailing two # hyphen-separated fields, so a tag containing hyphens survives intact. tag="${desc%-*-*}"; rest="${desc#"$tag"-}"; count="${rest%-*}"; sha="${rest#*-}" VERSION_SOURCE="git tag ${desc%-*-*}" tag="$(_sanitize "${tag#v}")" if [[ "$count" == 0 ]]; then PKGVER="$tag" else VERSION_SOURCE="$VERSION_SOURCE +$count commits" PKGVER="$tag.r$count.$sha" fi else VERSION_SOURCE="Cargo.toml (no tags yet)" PKGVER="$cargover.r$count.$sha" fi else VERSION_SOURCE="branch $branch" PKGVER="$(_sanitize "$branch").r$count.$sha" fi } # BIKECONTROL_PKGVER may be set by the caller (CI) to pin the version exactly. if [[ -n "${BIKECONTROL_PKGVER:-}" ]]; then PKGVER="$BIKECONTROL_PKGVER" VERSION_SOURCE="\$BIKECONTROL_PKGVER" else derive_pkgver fi if [[ $PRINT_ONLY -eq 1 ]]; then echo "$PKGVER" echo "(from $VERSION_SOURCE)" >&2 exit 0 fi echo "🚴 Building BikeControl Arch package $PKGVER" echo " version from: $VERSION_SOURCE" echo "=============================================" # Build straight out of the working tree; give cargo a writable, repo-local home # so a makepkg run never fights with the user's ~/.cargo permissions. export BIKECONTROL_SRC="$REPO_ROOT" export BIKECONTROL_PKGVER="$PKGVER" export CARGO_HOME="${CARGO_HOME:-$REPO_ROOT/.cargo-arch}" if [[ $SYNC_DEPS -eq 0 ]]; then # Without -s, makepkg only checks that the makedeps are present. rustup users # have cargo on PATH without the `rust` package installed, so warn rather than # drag in a duplicate toolchain. for tool in cargo npm node pkg-config; do command -v "$tool" >/dev/null || echo "⚠️ $tool not on PATH — build may fail (or rerun with --sync-deps)" done fi cd "$REPO_ROOT/packaging/arch" MAKEPKG_ARGS=(-f --noconfirm --nodeps) [[ $SYNC_DEPS -eq 1 ]] && MAKEPKG_ARGS=(-sf --noconfirm) makepkg "${MAKEPKG_ARGS[@]}" PKG="$(ls -1t ./*.pkg.tar.zst | head -1)" echo "" echo "✅ Built $PKG" if [[ -n "${OUTPUT_DIR:-}" ]]; then mkdir -p "$OUTPUT_DIR" cp -v "$PKG" "$OUTPUT_DIR/" echo "📦 Copied to $OUTPUT_DIR" fi if [[ $INSTALL -eq 1 ]]; then echo "" echo "📥 Installing $PKG (sudo pacman -U)" sudo pacman -U --noconfirm "$PKG" echo "" echo "✅ Installed. Run it with: bikecontrol" echo " Example profiles: /usr/share/bikecontrol/profiles/" else echo "" echo "ℹ️ Skipped install. To install manually: sudo pacman -U $PWD/${PKG#./}" fi