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" <