diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..bc00f43 --- /dev/null +++ b/.github/workflows/ci.yml @@ -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 diff --git a/.github/workflows/release-builds.yml b/.github/workflows/release-builds.yml index 57a1c7d..64fe0c6 100644 --- a/.github/workflows/release-builds.yml +++ b/.github/workflows/release-builds.yml @@ -17,118 +17,50 @@ jobs: fail-fast: false matrix: include: - - platform: darwin/amd64 - runner: macos-13 - os: darwin - arch: amd64 - archive_type: zip - - platform: darwin/arm64 - runner: macos-14 - os: darwin - arch: arm64 - archive_type: zip - - 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 - + - { platform: darwin/amd64, runner: macos-13 } + - { platform: darwin/arm64, runner: macos-14 } + - { platform: linux/amd64, runner: ubuntu-22.04 } + - { platform: linux/arm64, runner: ubuntu-22.04-arm } + - { platform: windows/amd64, runner: windows-latest } + - { platform: windows/arm64, runner: windows-11-arm } + defaults: + run: + shell: bash + working-directory: rust steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Set up Go - uses: actions/setup-go@v5 + - uses: actions/checkout@v4 with: - go-version-file: go.mod + fetch-depth: 0 - - name: Set up Node - uses: actions/setup-node@v4 - with: - node-version: "20" + - name: Rust toolchain (pinned by rust-toolchain.toml) + run: rustup show active-toolchain || rustup toolchain install - - name: Install Wails - run: go install github.com/wailsapp/wails/v2/cmd/wails@$(go list -m -f '{{.Version}}' github.com/wailsapp/wails/v2) - - - name: Install frontend dependencies - run: npm ci - working-directory: frontend - - - name: Install Linux build dependencies - if: matrix.os == 'linux' + - name: Linux build dependencies + if: runner.os == 'Linux' run: | 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 - if: matrix.os == 'windows' && matrix.cross - run: | - sudo apt-get update - sudo apt-get install -y zip + - name: Test + run: cargo test --locked --workspace - - name: Build (non-Windows runners) - if: matrix.runner != 'windows-latest' + - name: Build and package env: - CGO_ENABLED: "0" - run: | - wails build -platform "${{ matrix.platform }}" -ldflags "-s -w" + NEXUS_RELEASE_TARGETS: ${{ matrix.platform }} + NEXUS_RELEASE_ROOT: ${{ github.workspace }}/build/releases + run: ./release.sh - - name: Build (Windows runner) - 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 + - name: Upload artefacts uses: actions/upload-artifact@v4 with: - name: alchementary-link-${{ matrix.platform }} + name: nexus-terminal-${{ matrix.runner }} path: | - alchementary-link-${{ matrix.platform }}.zip - alchementary-link-${{ matrix.platform }}.tar.gz + build/releases/*/NexusTerminal-*.zip + build/releases/manifest-*.json + build/releases/version-*.txt - name: Attach to GitHub Release if: startsWith(github.ref, 'refs/tags/') uses: softprops/action-gh-release@v2 with: - files: | - alchementary-link-${{ matrix.platform }}.zip - alchementary-link-${{ matrix.platform }}.tar.gz + files: build/releases/*/NexusTerminal-*.zip diff --git a/rust/core/src/service.rs b/rust/core/src/service.rs index 2d385e7..89df85f 100644 --- a/rust/core/src/service.rs +++ b/rust/core/src/service.rs @@ -13,7 +13,11 @@ use std::path::{Path, PathBuf}; use std::sync::Arc; 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; pub struct Service { diff --git a/rust/packaging/linux/install.sh b/rust/packaging/linux/install.sh new file mode 100755 index 0000000..086869b --- /dev/null +++ b/rust/packaging/linux/install.sh @@ -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)" diff --git a/rust/packaging/linux/nexus-terminal.desktop b/rust/packaging/linux/nexus-terminal.desktop new file mode 100644 index 0000000..f902934 --- /dev/null +++ b/rust/packaging/linux/nexus-terminal.desktop @@ -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 diff --git a/rust/packaging/macos/Info.plist b/rust/packaging/macos/Info.plist new file mode 100644 index 0000000..1443493 --- /dev/null +++ b/rust/packaging/macos/Info.plist @@ -0,0 +1,36 @@ + + + + + CFBundleName + Nexus Terminal + CFBundleDisplayName + Nexus Terminal + CFBundleIdentifier + com.andydixon.nexusterminal + CFBundleExecutable + NexusTerminal + CFBundleIconFile + NexusTerminal + CFBundlePackageType + APPL + CFBundleShortVersionString + __VERSION__ + CFBundleVersion + __VERSION__ + CFBundleInfoDictionaryVersion + 6.0 + CFBundleDevelopmentRegion + en + LSApplicationCategoryType + public.app-category.developer-tools + LSMinimumSystemVersion + 11.0 + NSHighResolutionCapable + + NSSupportsAutomaticGraphicsSwitching + + NSHumanReadableCopyright + © Andy Dixon + + diff --git a/rust/packaging/macos/build-app.sh b/rust/packaging/macos/build-app.sh new file mode 100755 index 0000000..93ed8df --- /dev/null +++ b/rust/packaging/macos/build-app.sh @@ -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-.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" diff --git a/rust/release.sh b/rust/release.sh new file mode 100755 index 0000000..03dd4f9 --- /dev/null +++ b/rust/release.sh @@ -0,0 +1,109 @@ +#!/usr/bin/env bash +# release.sh — build and package Nexus Terminal release artefacts with cargo. +# +# Produces, under build/releases// (repo root): +# NexusTerminal--[.exe|.app] and NexusTerminal--.zip +# and next to them manifest--.json + version--.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" <