Vectrum
Sign In
159 lines · 7.1 KB · Shell
#!/usr/bin/env bash
# Builds Vectrum Code in release mode and writes two artifacts to ./build:
#   build/vectrum-code                       plain binary
#   build/Vectrum_Code-<version>-<arch>.AppImage
#
# appimagetool is used from $PATH if present, otherwise downloaded once into
# build/.tools. Set APPIMAGETOOL=/path/to/appimagetool to use a specific one.
#
# ./build.sh --release (or -r) then publishes them as a release on Vectrum:
# it tags the commit with the version in Cargo.toml, pushes the tag, and
# uploads the AppImage, the binary and SHA256SUMS (the first upload
# publishes the release). Bump the version in Cargo.toml for each release.
#   VECTRUM_TOKEN       a read & write API token (vec_…); otherwise read from
#                       ~/.config/vectrum/token (or $VECTRUM_TOKEN_FILE)
#   VECTRUM_URL         the server, https://vectrum.cc by default
#   VECTRUM_REPO        owner/repo, taken from the origin remote by default
set -euo pipefail

RELEASE=0
for arg in "$@"; do
    case "$arg" in
        -r|--release) RELEASE=1 ;;
        -h|--help) sed -n '2,/^set -euo/p' "$0" | sed '$d; s/^# \{0,1\}//'; exit 0 ;;
        *) printf 'Unknown option: %s (try --help)\n' "$arg" >&2; exit 2 ;;
    esac
done

ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
BUILD="$ROOT/build"
TOOLS="$BUILD/.tools"
APPDIR="$BUILD/AppDir"

NAME="vectrum-code"
PRETTY="Vectrum Code"
VERSION="$(sed -n 's/^version *= *"\(.*\)"/\1/p' "$ROOT/Cargo.toml" | head -n1)"
ARCH="$(uname -m)"

log() { printf '\033[1;34m==>\033[0m %s\n' "$*"; }
die() { printf '\033[1;31mxx\033[0m %s\n' "$*" >&2; exit 1; }

# ── 0. Release checks, before spending time on a build ───────────────────────
if (( RELEASE )); then
    TAG="$VERSION"
    SERVER="${VECTRUM_URL:-https://vectrum.cc}"
    SERVER="${SERVER%/}"
    TOKEN_FILE="${VECTRUM_TOKEN_FILE:-$HOME/.config/vectrum/token}"
    TOKEN="${VECTRUM_TOKEN:-}"
    if [[ -z "$TOKEN" && -r "$TOKEN_FILE" ]]; then
        TOKEN="$(tr -d '[:space:]' < "$TOKEN_FILE")"
    fi
    [[ -n "$TOKEN" ]] || die "No API token: set VECTRUM_TOKEN or save a read & write token to $TOKEN_FILE"
    # ssh://git@git.vectrum.cc:2222/vectrum/code.git, https://…/vectrum/code → vectrum/code
    REPO="${VECTRUM_REPO:-$(git -C "$ROOT" remote get-url origin | sed -E 's#\.git$##; s#^.*[:/]([^/:]+/[^/:]+)$#\1#')}"
    [[ "$REPO" == */* ]] || die "Couldn't tell the repository from the origin remote; set VECTRUM_REPO=owner/repo"
    [[ -z "$(git -C "$ROOT" status --porcelain)" ]] || die "Commit or stash your changes first: a release is built from a commit"
    HEAD="$(git -C "$ROOT" rev-parse HEAD)"
    REMOTE_TAG="$(git -C "$ROOT" ls-remote origin "refs/tags/$TAG^{}" "refs/tags/$TAG" | awk 'NR==1 || /\^\{\}$/ {sha=$1} END {print sha}')"
    LOCAL_TAG="$(git -C "$ROOT" rev-parse -q --verify "refs/tags/$TAG^{commit}" || true)"
    for tagged in "$REMOTE_TAG" "$LOCAL_TAG"; do
        [[ -z "$tagged" || "$tagged" == "$HEAD" ]] ||
            die "$TAG is already tagged at ${tagged:0:7}; bump the version in Cargo.toml for a new release"
    done
    log "Releasing $TAG of $REPO on $SERVER from ${HEAD:0:7}"
fi

# ── 1. Binary ────────────────────────────────────────────────────────────────
log "Building $NAME $VERSION (release, $ARCH)"
cargo build --release --manifest-path "$ROOT/Cargo.toml"

TARGET_DIR="$(cargo metadata --format-version 1 --no-deps --manifest-path "$ROOT/Cargo.toml" |
    sed -n 's/.*"target_directory":"\([^"]*\)".*/\1/p')"
mkdir -p "$BUILD"
install -m755 "$TARGET_DIR/release/$NAME" "$BUILD/$NAME"
log "Binary: build/$NAME"

# ── 2. AppDir ────────────────────────────────────────────────────────────────
rm -rf "$APPDIR"
mkdir -p "$APPDIR/usr/bin" \
         "$APPDIR/usr/share/applications" \
         "$APPDIR/usr/share/icons/hicolor/scalable/apps"

install -m755 "$BUILD/$NAME" "$APPDIR/usr/bin/$NAME"

cat > "$APPDIR/usr/share/applications/$NAME.desktop" <<EOF
[Desktop Entry]
Type=Application
Name=$PRETTY
Comment=A T3 Code-style GUI for Claude Code
Exec=$NAME
Icon=$NAME
Terminal=false
Categories=Development;IDE;
StartupWMClass=$NAME
EOF

install -m644 "$ROOT/assets/icons/$NAME.svg" "$APPDIR/usr/share/icons/hicolor/scalable/apps/$NAME.svg"

cat > "$APPDIR/AppRun" <<'EOF'
#!/bin/sh
HERE="$(dirname "$(readlink -f "$0")")"
exec "$HERE/usr/bin/vectrum-code" "$@"
EOF
chmod +x "$APPDIR/AppRun"

ln -sf "usr/share/applications/$NAME.desktop" "$APPDIR/$NAME.desktop"
ln -sf "usr/share/icons/hicolor/scalable/apps/$NAME.svg" "$APPDIR/$NAME.svg"
ln -sf "$NAME.svg" "$APPDIR/.DirIcon"

# ── 3. appimagetool ──────────────────────────────────────────────────────────
APPIMAGETOOL="${APPIMAGETOOL:-$(command -v appimagetool || true)}"
if [[ -z "$APPIMAGETOOL" ]]; then
    APPIMAGETOOL="$TOOLS/appimagetool-$ARCH.AppImage"
    if [[ ! -x "$APPIMAGETOOL" ]]; then
        log "Downloading appimagetool"
        mkdir -p "$TOOLS"
        curl -fL --progress-bar -o "$APPIMAGETOOL.part" \
            "https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-$ARCH.AppImage"
        chmod +x "$APPIMAGETOOL.part"
        mv "$APPIMAGETOOL.part" "$APPIMAGETOOL"
    fi
fi

# ── 4. AppImage ──────────────────────────────────────────────────────────────
OUT="$BUILD/${PRETTY// /_}-$VERSION-$ARCH.AppImage"
log "Packaging AppImage"
# Extract-and-run so this works on systems without FUSE (containers, CI).
APPIMAGE_EXTRACT_AND_RUN=1 ARCH="$ARCH" "$APPIMAGETOOL" --no-appstream "$APPDIR" "$OUT"
rm -rf "$APPDIR"

log "AppImage: ${OUT#"$ROOT"/}"

# ── 5. Release ───────────────────────────────────────────────────────────────
if (( RELEASE )); then
    STAGE="$BUILD/release-$TAG"
    rm -rf "$STAGE"
    mkdir -p "$STAGE"
    cp "$OUT" "$STAGE/"
    cp "$BUILD/$NAME" "$STAGE/$NAME-$VERSION-$ARCH"
    (cd "$STAGE" && sha256sum -- * > SHA256SUMS)

    if [[ -z "$LOCAL_TAG" ]]; then
        git -C "$ROOT" tag -a "$TAG" -m "$PRETTY $VERSION" "$HEAD"
    fi
    if [[ -z "$REMOTE_TAG" ]]; then
        log "Pushing tag $TAG"
        git -C "$ROOT" push -q origin "refs/tags/$TAG"
    fi

    for file in "$STAGE"/*; do
        name="$(basename "$file")"
        log "Uploading $name"
        # The token goes in through a file descriptor, so it never shows in ps.
        if ! reply="$(curl -sS --fail-with-body -T "$file" -H @<(printf 'Authorization: Bearer %s\n' "$TOKEN") \
            "$SERVER/$REPO/releases/download/$TAG/$name")"; then
            die "Uploading $name failed: $reply"
        fi
    done
    log "Released: $SERVER/$REPO/releases/tag/$TAG"
fi