rust: packaging (Linux desktop entry, macOS bundle), cargo release script with update manifests, CI

This commit is contained in:
Andy Dixon
2026-10-11 16:53:19 +01:00
parent ea09aeafe8
commit 328507c57b
8 changed files with 294 additions and 98 deletions
+28
View File
@@ -0,0 +1,28 @@
name: CI
on:
push:
branches: ["**"]
pull_request:
jobs:
test:
name: Test (${{ matrix.os }})
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-22.04, macos-14, windows-latest]
defaults:
run:
shell: bash
working-directory: rust
steps:
- uses: actions/checkout@v4
- name: Linux dependencies (GUI libs, and sshd for the SSH integration tests)
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install -y libxkbcommon-dev libgl1-mesa-dev libx11-dev libxcursor-dev libxrandr-dev libxi-dev libwayland-dev libgtk-3-dev openssh-server
- run: cargo test --locked --workspace
- run: cargo clippy --locked --workspace -- -D warnings
+29 -97
View File
@@ -17,118 +17,50 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
include: include:
- platform: darwin/amd64 - { platform: darwin/amd64, runner: macos-13 }
runner: macos-13 - { platform: darwin/arm64, runner: macos-14 }
os: darwin - { platform: linux/amd64, runner: ubuntu-22.04 }
arch: amd64 - { platform: linux/arm64, runner: ubuntu-22.04-arm }
archive_type: zip - { platform: windows/amd64, runner: windows-latest }
- platform: darwin/arm64 - { platform: windows/arm64, runner: windows-11-arm }
runner: macos-14 defaults:
os: darwin run:
arch: arm64 shell: bash
archive_type: zip working-directory: rust
- platform: linux/amd64
runner: ubuntu-latest
os: linux
arch: amd64
archive_type: tar
- platform: linux/arm64
runner: ubuntu-24.04-arm64
os: linux
arch: arm64
archive_type: tar
- platform: windows/amd64
runner: windows-latest
os: windows
arch: amd64
archive_type: zip
- platform: windows/arm64
runner: ubuntu-latest
os: windows
arch: arm64
archive_type: zip
cross: true
steps: steps:
- name: Checkout - uses: actions/checkout@v4
uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with: with:
go-version-file: go.mod fetch-depth: 0
- name: Set up Node - name: Rust toolchain (pinned by rust-toolchain.toml)
uses: actions/setup-node@v4 run: rustup show active-toolchain || rustup toolchain install
with:
node-version: "20"
- name: Install Wails - name: Linux build dependencies
run: go install github.com/wailsapp/wails/v2/cmd/wails@$(go list -m -f '{{.Version}}' github.com/wailsapp/wails/v2) if: runner.os == 'Linux'
- name: Install frontend dependencies
run: npm ci
working-directory: frontend
- name: Install Linux build dependencies
if: matrix.os == 'linux'
run: | run: |
sudo apt-get update sudo apt-get update
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev sudo apt-get install -y libxkbcommon-dev libgl1-mesa-dev libx11-dev libxcursor-dev libxrandr-dev libxi-dev libwayland-dev libgtk-3-dev zip
- name: Install Windows packaging tools - name: Test
if: matrix.os == 'windows' && matrix.cross run: cargo test --locked --workspace
run: |
sudo apt-get update
sudo apt-get install -y zip
- name: Build (non-Windows runners) - name: Build and package
if: matrix.runner != 'windows-latest'
env: env:
CGO_ENABLED: "0" NEXUS_RELEASE_TARGETS: ${{ matrix.platform }}
run: | NEXUS_RELEASE_ROOT: ${{ github.workspace }}/build/releases
wails build -platform "${{ matrix.platform }}" -ldflags "-s -w" run: ./release.sh
- name: Build (Windows runner) - name: Upload artefacts
if: matrix.runner == 'windows-latest'
shell: pwsh
env:
CGO_ENABLED: "0"
run: |
wails build -platform "${{ matrix.platform }}" -ldflags "-s -w"
- name: Package (zip)
if: matrix.archive_type == 'zip' && matrix.runner != 'windows-latest'
run: |
archive="alchementary-link-${{ matrix.platform }}.zip"
cd build/bin
zip -r "../../${archive}" .
- name: Package (tar)
if: matrix.archive_type == 'tar'
run: |
archive="alchementary-link-${{ matrix.platform }}.tar.gz"
tar -czf "${archive}" -C build/bin .
- name: Package (Windows zip)
if: matrix.archive_type == 'zip' && matrix.runner == 'windows-latest'
shell: pwsh
run: |
$archive = "alchementary-link-${{ matrix.platform }}.zip"
Compress-Archive -Path "build/bin/*" -DestinationPath $archive
- name: Upload artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: alchementary-link-${{ matrix.platform }} name: nexus-terminal-${{ matrix.runner }}
path: | path: |
alchementary-link-${{ matrix.platform }}.zip build/releases/*/NexusTerminal-*.zip
alchementary-link-${{ matrix.platform }}.tar.gz build/releases/manifest-*.json
build/releases/version-*.txt
- name: Attach to GitHub Release - name: Attach to GitHub Release
if: startsWith(github.ref, 'refs/tags/') if: startsWith(github.ref, 'refs/tags/')
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2
with: with:
files: | files: build/releases/*/NexusTerminal-*.zip
alchementary-link-${{ matrix.platform }}.zip
alchementary-link-${{ matrix.platform }}.tar.gz
+5 -1
View File
@@ -13,7 +13,11 @@ use std::path::{Path, PathBuf};
use std::sync::Arc; use std::sync::Arc;
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
pub const VERSION: &str = env!("CARGO_PKG_VERSION"); /// The release version (`NEXUS_VERSION` from release.sh, e.g. "v1.2.0-abc1234"), else the crate version.
pub const VERSION: &str = match option_env!("NEXUS_VERSION") {
Some(v) => v,
None => env!("CARGO_PKG_VERSION"),
};
const MAX_IMPORT_JSON_BYTES: usize = 5 * 1024 * 1024; const MAX_IMPORT_JSON_BYTES: usize = 5 * 1024 * 1024;
pub struct Service { pub struct Service {
+14
View File
@@ -0,0 +1,14 @@
#!/bin/sh
# Install Nexus Terminal for the current user so it appears in the app launcher.
# Usage: rust/packaging/linux/install.sh [path/to/nexus-terminal] (default: target/release/nexus-terminal)
set -eu
HERE=$(cd "$(dirname "$0")" && pwd)
BIN=${1:-"$HERE/../../target/release/nexus-terminal"}
PREFIX=${XDG_DATA_HOME:-$HOME/.local/share}
install -Dm755 "$BIN" "$HOME/.local/bin/nexus-terminal"
install -Dm644 "$HERE/nexus-terminal.desktop" "$PREFIX/applications/nexus-terminal.desktop"
install -Dm644 "$HERE/../../app/assets/icon/icon-256.png" "$PREFIX/icons/hicolor/256x256/apps/nexus-terminal.png"
install -Dm644 "$HERE/../../app/assets/icon/icon-512.png" "$PREFIX/icons/hicolor/512x512/apps/nexus-terminal.png"
update-desktop-database "$PREFIX/applications" 2>/dev/null || true
gtk-update-icon-cache -f -t "$PREFIX/icons/hicolor" 2>/dev/null || true
echo "installed: ~/.local/bin/nexus-terminal (make sure ~/.local/bin is on PATH)"
@@ -0,0 +1,11 @@
[Desktop Entry]
Type=Application
Name=Nexus Terminal
GenericName=Terminal and Remote Connections
Comment=SSH, Telnet, RAW TCP, Serial, VNC, IRC and GitHub sessions in one window
Exec=nexus-terminal
Icon=nexus-terminal
Terminal=false
Categories=System;TerminalEmulator;Network;RemoteAccess;
Keywords=ssh;telnet;serial;sftp;vnc;irc;terminal;
StartupWMClass=nexus-terminal
+36
View File
@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleName</key>
<string>Nexus Terminal</string>
<key>CFBundleDisplayName</key>
<string>Nexus Terminal</string>
<key>CFBundleIdentifier</key>
<string>com.andydixon.nexusterminal</string>
<key>CFBundleExecutable</key>
<string>NexusTerminal</string>
<key>CFBundleIconFile</key>
<string>NexusTerminal</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>__VERSION__</string>
<key>CFBundleVersion</key>
<string>__VERSION__</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>LSApplicationCategoryType</key>
<string>public.app-category.developer-tools</string>
<key>LSMinimumSystemVersion</key>
<string>11.0</string>
<key>NSHighResolutionCapable</key>
<true/>
<key>NSSupportsAutomaticGraphicsSwitching</key>
<true/>
<key>NSHumanReadableCopyright</key>
<string>© Andy Dixon</string>
</dict>
</plist>
+62
View File
@@ -0,0 +1,62 @@
#!/bin/sh
# Build NexusTerminal.app, a macOS application bundle. Run on a Mac with Xcode command-line tools:
#
# rust/packaging/macos/build-app.sh # universal (Apple Silicon + Intel) if both
# # targets are installed, else this Mac's arch
# rust/packaging/macos/build-app.sh --dmg # also wrap it in a .dmg
#
# Output: rust/target/macos/NexusTerminal.app (and NexusTerminal-<version>.dmg).
# Set NEXUS_BIN=/path/to/binary to bundle an existing build instead of compiling.
set -eu
HERE=$(cd "$(dirname "$0")" && pwd)
ROOT=$(cd "$HERE/../.." && pwd)
cd "$ROOT"
VERSION=${NEXUS_VERSION:-$(sed -n 's/^version = "\(.*\)"/\1/p' Cargo.toml | head -1)}
OUT="$ROOT/target/macos"
APP="$OUT/NexusTerminal.app"
if [ -n "${NEXUS_BIN:-}" ]; then
BIN="$NEXUS_BIN"
else
targets=""
for t in aarch64-apple-darwin x86_64-apple-darwin; do
if rustup target list --installed 2>/dev/null | grep -qx "$t"; then targets="$targets $t"; fi
done
[ -n "$targets" ] || targets=" $(rustc -vV | sed -n 's/^host: //p')"
bins=""
for t in $targets; do
echo "building $t"
cargo build --release -p nexus --target "$t"
bins="$bins target/$t/release/nexus-terminal"
done
mkdir -p "$OUT"
set -- $bins
if [ $# -gt 1 ]; then
lipo -create "$@" -output "$OUT/nexus-terminal-universal"
BIN="$OUT/nexus-terminal-universal"
else
BIN="$1"
fi
fi
rm -rf "$APP"
mkdir -p "$APP/Contents/MacOS" "$APP/Contents/Resources"
cp "$BIN" "$APP/Contents/MacOS/NexusTerminal"
chmod +x "$APP/Contents/MacOS/NexusTerminal"
cp "$HERE/NexusTerminal.icns" "$APP/Contents/Resources/NexusTerminal.icns"
sed "s/__VERSION__/$VERSION/g" "$HERE/Info.plist" > "$APP/Contents/Info.plist"
printf 'APPL????' > "$APP/Contents/PkgInfo"
if command -v plutil >/dev/null; then plutil -lint "$APP/Contents/Info.plist"; fi
# Ad-hoc sign so Apple Silicon runs it. For distribution, sign with a Developer ID and notarize:
# codesign --force --options runtime --sign "Developer ID Application: …" NexusTerminal.app
# xcrun notarytool submit … --wait && xcrun stapler staple NexusTerminal.app
if command -v codesign >/dev/null; then codesign --force --deep --sign - "$APP"; fi
if [ "${1:-}" = "--dmg" ] && command -v hdiutil >/dev/null; then
DMG="$OUT/NexusTerminal-$VERSION.dmg"
rm -f "$DMG"
hdiutil create -volname "Nexus Terminal" -srcfolder "$APP" -ov -format UDZO "$DMG"
echo "built $DMG"
fi
echo "built $APP"
+109
View File
@@ -0,0 +1,109 @@
#!/usr/bin/env bash
# release.sh — build and package Nexus Terminal release artefacts with cargo.
#
# Produces, under build/releases/<version>/ (repo root):
# NexusTerminal-<os>-<arch>[.exe|.app] and NexusTerminal-<os>-<arch>.zip
# and next to them manifest-<os>-<arch>.json + version-<os>-<arch>.txt for the update server
# (updates/index.php), in the same format the Wails build produced.
#
# Targets default to this machine's native one; set NEXUS_RELEASE_TARGETS to a comma list such as
# "linux/amd64,windows/amd64" (windows/* from Linux needs the mingw-w64 toolchain and rustup target).
# CI (.github/workflows/release-builds.yml) builds every platform natively.
set -euo pipefail
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT="$(cd "$HERE/.." && pwd)"
cd "$HERE"
DEFAULT_UPDATE_URL="https://alchementary.co.uk/download/link/manifest-{platform}-{arch}.json"
DEFAULT_RELEASE_BASE_URL="https://alchementary.co.uk/download/link"
log() { echo -e "\033[0;32m[nexus]\033[0m $1"; }
warn() { echo -e "\033[1;33m[nexus]\033[0m $1"; }
host_target() {
local os arch
case "$(uname -s)" in Darwin) os=darwin ;; Linux) os=linux ;; MINGW*|MSYS*|CYGWIN*) os=windows ;; *) os=unknown ;; esac
case "$(uname -m)" in x86_64|amd64) arch=amd64 ;; arm64|aarch64) arch=arm64 ;; *) arch=unknown ;; esac
echo "$os/$arch"
}
rust_triple() {
case "$1" in
linux/amd64) echo x86_64-unknown-linux-gnu ;;
linux/arm64) echo aarch64-unknown-linux-gnu ;;
darwin/amd64) echo x86_64-apple-darwin ;;
darwin/arm64) echo aarch64-apple-darwin ;;
windows/amd64) if [ "$(uname -s)" = Linux ]; then echo x86_64-pc-windows-gnu; else echo x86_64-pc-windows-msvc; fi ;;
windows/arm64) echo aarch64-pc-windows-msvc ;;
*) return 1 ;;
esac
}
VERSION="${NEXUS_VERSION:-$(git describe --tags --abbrev=0 2>/dev/null || echo dev)-$(git rev-parse --short HEAD 2>/dev/null || echo unknown)}"
export NEXUS_VERSION="$VERSION"
export NEXUS_UPDATE_URL="${NEXUS_UPDATE_URL:-$DEFAULT_UPDATE_URL}"
BASE_URL="${NEXUS_RELEASE_BASE_URL:-$DEFAULT_RELEASE_BASE_URL}"
RELEASE_ROOT="${NEXUS_RELEASE_ROOT:-$ROOT/build/releases}"
OUT="$RELEASE_ROOT/$VERSION"
mkdir -p "$OUT"
: > "$OUT/.release-files"
IFS=',' read -r -a TARGETS <<< "${NEXUS_RELEASE_TARGETS:-$(host_target)}"
errors=0
for target in "${TARGETS[@]}"; do
os="${target%%/*}"; arch="${target##*/}"
triple="$(rust_triple "$target")" || { warn "unknown target $target"; errors=$((errors+1)); continue; }
base="NexusTerminal-${os}-${arch}"
log "Building $target ($triple), version $VERSION"
rustup target add "$triple" >/dev/null 2>&1 || true
if ! cargo build --release --locked -p nexus --target "$triple"; then
warn "build failed for $target"; errors=$((errors+1)); continue
fi
bin="target/$triple/release/nexus-terminal"
case "$os" in
darwin)
NEXUS_BIN="$bin" packaging/macos/build-app.sh
rm -rf "$OUT/$base.app"
cp -R target/macos/NexusTerminal.app "$OUT/$base.app"
(cd "$OUT" && rm -f "$base.zip" && zip -qry "$base.zip" "$base.app")
;;
windows)
cp "$bin.exe" "$OUT/$base.exe"
(cd "$OUT" && rm -f "$base.zip" && zip -qj "$base.zip" "$base.exe")
;;
*)
cp "$bin" "$OUT/$base"
strip "$OUT/$base" 2>/dev/null || true
stage="$(mktemp -d)"
cp "$OUT/$base" "$stage/nexus-terminal"
cp packaging/linux/nexus-terminal.desktop packaging/linux/install.sh "$stage/"
mkdir -p "$stage/app/assets/icon" && cp app/assets/icon/icon-256.png app/assets/icon/icon-512.png "$stage/app/assets/icon/"
sed -i 's|\$HERE/../../app|$HERE/app|; s|\$HERE/../../target/release/nexus-terminal|$HERE/nexus-terminal|' "$stage/install.sh"
(cd "$stage" && rm -f "$OUT/$base.zip" && zip -qr "$OUT/$base.zip" .)
rm -rf "$stage"
;;
esac
echo "$os|$arch|$base.zip" >> "$OUT/.release-files"
done
published_at="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
while IFS='|' read -r os arch filename; do
[ -z "$os" ] && continue
sha="$(shasum -a 256 "$OUT/$filename" 2>/dev/null || sha256sum "$OUT/$filename")"; sha="${sha%% *}"
size="$(wc -c < "$OUT/$filename" | tr -d ' ')"
echo "$VERSION" > "$RELEASE_ROOT/version-${os}-${arch}.txt"
cat > "$RELEASE_ROOT/manifest-${os}-${arch}.json" <<JSON
{
"latestVersion": "${VERSION}",
"publishedAt": "${published_at}",
"releaseURL": "${BASE_URL}/",
"files": [
{"platform":"${os}","arch":"${arch}","filename":"${filename}","url":"${BASE_URL}/${filename}","sha256":"${sha}","size":${size}}
]
}
JSON
done < "$OUT/.release-files"
rm -f "$OUT/.release-files"
log "Artefacts in $OUT"
exit $errors