This guide walks through every step needed to set up a development workstation for ShellStation, from system dependencies to a fully working build. It covers Linux, macOS, and Windows 11, plus instructions for producing release-ready production builds.
Tested on: Ubuntu 22.04+, Debian 12+, Fedora 37+, Arch Linux, macOS 13 Ventura+, Windows 11 23H2+.
- System Dependencies
- Rust Toolchain
- Node.js and npm
- Tauri CLI
- Database Tooling
- VS Code Extensions
- Project Bootstrap
- Verify the Environment
- Common Issues (Linux)
- Prerequisites (macOS)
- Rust Toolchain (macOS)
- Node.js and npm (macOS)
- Tauri CLI and Database Tooling (macOS)
- Project Bootstrap and Verify (macOS)
- Common Issues (macOS)
- Prerequisites (Windows)
- Rust Toolchain (Windows)
- Node.js and npm (Windows)
- Tauri CLI and Database Tooling (Windows)
- Project Bootstrap and Verify (Windows)
- Common Issues (Windows)
Tauri 2.x uses WebKitGTK 4.1 on Linux for its webview. The following packages provide the build toolchain, WebKit rendering engine, GTK bindings, and supporting libraries.
sudo apt update
sudo apt install -y \
build-essential \
curl \
wget \
file \
pkg-config \
libssl-dev \
libgtk-3-dev \
libwebkit2gtk-4.1-dev \
libayatana-appindicator3-dev \
librsvg2-dev \
libdbus-1-dev \
libsoup-3.0-dev \
libjavascriptcoregtk-4.1-devsudo dnf check-update
sudo dnf install -y \
webkit2gtk4.1-devel \
openssl-devel \
curl \
wget \
file \
libappindicator-gtk3-devel \
librsvg2-devel
sudo dnf group install "C Development Tools and Libraries"sudo pacman -Syu
sudo pacman -S --needed \
webkit2gtk-4.1 \
base-devel \
curl \
wget \
file \
openssl \
appmenu-gtk-module \
gtk3 \
libappindicator-gtk3 \
librsvgInstall Rust via rustup (the official installer). Do not use distribution packages — they tend to lag behind and miss components like clippy and rustfmt.
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | shFollow the prompts and select the default installation. Then reload your shell:
source "$HOME/.cargo/env"rustc --version
cargo --version
rustup --versionShellStation needs Rust 1.94 or newer, the rust-version in src-tauri/Cargo.toml. Cargo refuses to build it with an older toolchain; rustup update fixes that.
rustup component add clippy rustfmt- clippy — the Rust linter. ShellStation enforces zero clippy warnings.
- rustfmt — the standard Rust formatter.
cargo install cargo-watch # auto-rebuild on file changes during development
cargo install sqlx-cli --no-default-features --features native-tls,sqlite,postgres- cargo-watch — runs
cargo check(or any command) on every save, useful for fast feedback during development. - sqlx-cli — manages database migrations for both SQLite and PostgreSQL. The feature flags here enable both backends.
rustup updateRun this periodically (or set up a cron job) to stay on the latest stable toolchain.
ShellStation's frontend is built with React + TypeScript + Vite, which requires Node.js. Use Node 20 LTS or later.
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt install -y nodejsIf you prefer to manage multiple Node versions:
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash
source ~/.bashrc
nvm install 20
nvm use 20node --version # should be v20.x or later
npm --version # should be 10.x or latersudo npm install -g typescriptThe Tauri CLI handles project scaffolding, development server, and bundling. Install it via cargo:
cargo install tauri-cli --version "^2"This installs the cargo tauri subcommand. Verify it with:
cargo tauri --versionAlternatively, you can use the npm-based CLI wrapper (useful if you prefer npx-style invocation):
sudo npm install -g @tauri-apps/cli@latestTauri ships a built-in diagnostic command that validates your system dependencies:
cargo tauri infoReview the output. Every line should show a checkmark. If anything is marked with ✘, install the missing dependency before continuing.
ShellStation supports SQLite (local) and PostgreSQL (shared/team). You need the development libraries for both at build time.
SQLite is typically already available on most Linux systems. Ensure the development headers are present:
sudo apt install -y libsqlite3-dev sqlite3Verify:
sqlite3 --versionIf you want to develop and test against a real PostgreSQL instance locally:
sudo apt install -y postgresql postgresql-client libpq-devStart the service and create a development database:
sudo systemctl start postgresql
sudo systemctl enable postgresql
# Create a development user and database
sudo -u postgres createuser --interactive --pwprompt shellstation
sudo -u postgres createdb --owner=shellstation shellstation_devSet the database URL as an environment variable (add this to your ~/.bashrc or ~/.zshrc):
export DATABASE_URL="postgres://shellstation:<your-password>@localhost/shellstation_dev"If you prefer not to install PostgreSQL system-wide:
docker run -d \
--name shellstation-postgres \
-e POSTGRES_USER=shellstation \
-e POSTGRES_PASSWORD=devpassword \
-e POSTGRES_DB=shellstation_dev \
-p 5432:5432 \
postgres:16-alpineThen set:
export DATABASE_URL="postgres://shellstation:devpassword@localhost/shellstation_dev"For day-to-day development without PostgreSQL, sqlx can operate against a local SQLite file. Set:
export DATABASE_URL="sqlite:///home/$USER/.config/shellstation/dev.db"Create the directory:
mkdir -pv /home/$USER/.config/shellstationsqlx-cli will create the file automatically when you run migrations.
The following extensions provide the best development experience for a Tauri + Rust + React/TypeScript project. Install them from the VS Code extensions panel or via the command line.
# Rust language support (rust-analyzer)
code --install-extension rust-lang.rust-analyzer
# Tauri integration
code --install-extension tauri-apps.tauri-vscode
# ESLint for TypeScript/React linting
code --install-extension dbaeumer.vscode-eslint
# Prettier for frontend formatting
code --install-extension esbenp.prettier-vscode# TypeScript/JavaScript language support (ships with VS Code but ensure latest)
code --install-extension ms-vscode.vscode-typescript-next
# Better TOML support (for Cargo.toml, tauri.conf.json, etc.)
code --install-extension tamasfe.even-better-toml
# Crates helper (shows latest versions inline in Cargo.toml)
code --install-extension serayuzgur.crates
# Error Lens (inline compiler/lint errors)
code --install-extension usernamehw.errorlens
# SQLite viewer (inspect local DB from VS Code)
code --install-extension alexcvzz.vscode-sqlite
# GitLens (enhanced Git integration)
code --install-extension eamodio.gitlensAdd these to your workspace .vscode/settings.json to align with ShellStation's quality standards:
{
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"[rust]": {
"editor.defaultFormatter": "rust-lang.rust-analyzer",
"editor.formatOnSave": true
},
"rust-analyzer.check.command": "clippy",
"rust-analyzer.check.extraArgs": ["--", "-D", "warnings"],
"eslint.validate": ["javascript", "javascriptreact", "typescript", "typescriptreact"],
"typescript.tsdk": "node_modules/typescript/lib",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
}
}This configures rust-analyzer to run clippy instead of the default cargo check (catching more issues in real time) and sets up ESLint auto-fix on save.
git clone https://git.fiedler.live/tux/shellstation.git
cd shellstation
npm installnpm install reads package.json and pulls all frontend dependencies including the Tauri CLI wrapper. Rust crates are resolved automatically by Cargo on the first build.
sqlx database create
sqlx migrate run(Requires the DATABASE_URL environment variable to be set — see Database Tooling.)
cargo tauri devThis compiles the Rust backend, starts the Vite dev server for the frontend, and opens the application window. The first build takes several minutes as it compiles all Rust dependencies.
The Log level under Application Logging in Settings applies to ShellStation's own code only; every third-party crate stays silent at any level. For more, set RUST_LOG, which replaces that filter entirely:
RUST_LOG=shellstation_lib=trace,russh=debug cargo tauri devOn Windows PowerShell, set $env:RUST_LOG = "shellstation_lib=trace,russh=debug" first. RUST_LOG is passed through as given, so third-party targets at debug or trace write whatever they log — sqlx every SQL statement, russh protocol detail — and with application logging enabled, that lands in the rotating log file too. Unset it before collecting a log to share.
Run the full quality check suite to confirm everything is wired up correctly:
cd src-tauri
# Linter (zero warnings enforced)
cargo clippy -- -D warnings
# Formatter check (no changes = pass)
cargo fmt -- --check
# Unit tests
cargo test# ESLint (zero errors/warnings)
npx eslint src/ --ext .ts,.tsx
# Prettier format check
npx prettier --check "src/**/*.{ts,tsx,css,json}"
# TypeScript type check
npx tsc --noEmit
# Unit tests
npx vitest runcargo tauri infoIf all commands pass without errors, your development environment is ready.
Tauri 2.x requires WebKitGTK 4.1, which is only available in Ubuntu 22.04 and later. If you are on an older release, you must upgrade your distribution or use a Docker-based build environment.
Usually means a -dev package is missing. Read the error message — it will name the .pc file it cannot find. Search for the package that provides it:
apt-file search missing-package.pcInstall apt-file first if needed: sudo apt install apt-file && sudo apt-file update.
sqlx verifies queries at compile time against a real database. If you don't have a running database, you can use sqlx's offline mode:
cargo sqlx prepareThis generates a .sqlx/ directory with cached query metadata, allowing compilation without a live database connection.
The initial cargo build downloads and compiles all Rust crate dependencies. This is normal and can take 5–10 minutes depending on your hardware. Subsequent builds are incremental and much faster. Using cargo-watch avoids full rebuilds during development.
The keyring crate requires a running D-Bus session and a secret service (GNOME Keyring or KWallet). If you are developing in a headless environment or minimal desktop:
sudo apt install -y gnome-keyring libsecret-1-devThen ensure a keyring daemon is running in your session.
Some Linux environments (particularly VMs or Wayland sessions) may not expose WebGL to the webview. xterm.js will automatically fall back to the canvas renderer. This is not a bug — the application handles it gracefully.
The window opens with its title bar and native menu (Edit, Help), but the content area stays white. WebKitGTK's DMABUF renderer fails on some GPU drivers, and virtual GPUs are a common case (e.g. VMware SVGA with vmwgfx, seen with WebKitGTK 2.52). The app itself is fine; WebKitGTK simply draws nothing.
Disable the DMABUF renderer for one run to confirm:
WEBKIT_DISABLE_DMABUF_RENDERER=1 npm run tauri devIf the UI appears, set it permanently for your session and log out and back in:
echo 'export WEBKIT_DISABLE_DMABUF_RENDERER=1' >> ~/.profileNot every desktop session reads ~/.profile. If echo "$WEBKIT_DISABLE_DMABUF_RENDERER" in a new terminal prints nothing after logging back in, use a systemd user environment file instead:
mkdir -p ~/.config/environment.d
echo 'WEBKIT_DISABLE_DMABUF_RENDERER=1' >> ~/.config/environment.d/webkit.confEither way the variable applies to every WebKitGTK application in your session, not just ShellStation. To limit it to ShellStation, prefix the launch command with it as in the test above.
ShellStation deliberately does not set this variable itself: it costs every Linux user the zero-copy GPU path to fix a problem only some drivers have. If the window is still blank, try WEBKIT_DISABLE_COMPOSITING_MODE=1 instead, which turns off accelerated compositing entirely.
Tauri 2.x on macOS uses the system WebKit framework (WKWebView) — no additional webview runtime is needed. The primary build dependency is the Xcode Command Line Tools.
Open Terminal and run:
xcode-select --installThis installs clang, make, git, and the macOS SDK headers. You do not need the full Xcode IDE unless you plan to do code signing or work with Xcode projects directly.
Verify the installation:
xcode-select -p
# Should print: /Library/Developer/CommandLineTools or /Applications/Xcode.app/Contents/DeveloperHomebrew is the standard package manager for macOS development dependencies:
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"Follow the post-install instructions to add Homebrew to your PATH. Then verify:
brew --versioncurl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | shFollow the prompts and select the default installation. Then reload your shell:
source "$HOME/.cargo/env"rustc --version
cargo --version
rustup --versionrustup component add clippy rustfmtcargo install cargo-watch
cargo install sqlx-cli --no-default-features --features native-tls,sqlite,postgresrustup updatebrew install node@20Homebrew installs Node into a keg-only path. Follow the post-install output to add it to your PATH, or link it:
brew link --overwrite node@20curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash
source ~/.zshrc
nvm install 20
nvm use 20node --version # should be v20.x or later
npm --version # should be 10.x or laternpm install -g typescriptcargo install tauri-cli --version "^2"Verify:
cargo tauri --versionRun the environment diagnostic:
cargo tauri infoReview the output. Every line should show a checkmark. If anything is marked with a cross, install the missing dependency before continuing.
macOS ships with SQLite pre-installed. Verify:
sqlite3 --versionIf you need a newer version:
brew install sqliteFor local PostgreSQL development:
brew install postgresql@16
brew services start postgresql@16Create the development database:
createuser --interactive --pwprompt shellstation
createdb --owner=shellstation shellstation_devSet the database URL (add to your ~/.zshrc):
export DATABASE_URL="postgres://shellstation:<your-password>@localhost/shellstation_dev"docker run -d \
--name shellstation-postgres \
-e POSTGRES_USER=shellstation \
-e POSTGRES_PASSWORD=devpassword \
-e POSTGRES_DB=shellstation_dev \
-p 5432:5432 \
postgres:16-alpineThen set:
export DATABASE_URL="postgres://shellstation:devpassword@localhost/shellstation_dev"export DATABASE_URL="sqlite://$HOME/Library/Application Support/shellstation/dev.db"
mkdir -p "$HOME/Library/Application Support/shellstation"git clone https://git.fiedler.live/tux/shellstation.git
cd shellstation
npm installsqlx database create
sqlx migrate runcargo tauri devThe first build compiles all Rust dependencies and takes several minutes. Subsequent builds are incremental.
Rust checks:
cd src-tauri
cargo clippy -- -D warnings
cargo fmt -- --check
cargo test
cd ..Frontend checks:
npx eslint src/ --ext .ts,.tsx
npx prettier --check "src/**/*.{ts,tsx,css,json}"
npx tsc --noEmit
npx vitest runTauri environment check:
cargo tauri infoIf all commands pass without errors, your macOS development environment is ready.
This means Xcode Command Line Tools are not installed or need updating:
xcode-select --installIf already installed but still failing after a macOS upgrade, reset the path:
sudo xcode-select --resetSome Rust crates use pkg-config to locate system libraries. Install it via Homebrew:
brew install pkg-configSame as Linux — sqlx verifies queries at compile time. Use offline mode if no database is available:
cargo sqlx prepareNormal on macOS as well. The initial cargo build can take 5-10 minutes. Subsequent builds are incremental. Using cargo-watch avoids full rebuilds during development.
The keyring crate uses macOS Keychain by default. If running in a CI environment or headless context, Keychain access may be restricted. For local development, Keychain access should work without additional configuration. If prompted, allow the shellstation binary to access the keychain.
Rust natively supports aarch64-apple-darwin. The default rustup installation on Apple Silicon installs the ARM toolchain. No Rosetta or special configuration is needed. All dependencies (including Homebrew, Node.js, and SQLite) run natively on ARM.
If you need to cross-compile for Intel Macs:
rustup target add x86_64-apple-darwin
cargo tauri build --target x86_64-apple-darwinSome macOS configurations (particularly in VMs) may not expose WebGL to the webview. xterm.js falls back to the canvas renderer automatically. This is not a bug.
Tauri 2.x on Windows uses Microsoft Edge WebView2 (pre-installed on Windows 11) for its webview. The primary build dependency is the Microsoft C++ Build Tools.
Download Visual Studio 2022 Build Tools from https://visualstudio.microsoft.com/visual-cpp-build-tools/.
During installation, select the following workload:
- Desktop development with C++
Under "Individual components", ensure these are checked (they should be by default with the workload):
- MSVC v143 — VS 2022 C++ x64/x86 build tools (Latest)
- Windows 11 SDK (10.0.22621.0 or later)
- C++ CMake tools for Windows
This provides
cl.exe,link.exe, the Windows SDK headers, and the CRT libraries that Rust's MSVC toolchain requires. You do not need the full Visual Studio IDE — the Build Tools installer is sufficient.
WebView2 ships with Windows 11. Confirm it is present:
Get-ItemProperty -Path "HKLM:\SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" -Name pvIf this returns a version string, WebView2 is installed. If not, download the Evergreen Bootstrapper from https://developer.microsoft.com/en-us/microsoft-edge/webview2/.
If not already installed:
winget install Git.GitAfter installation, open a new terminal and verify:
git --versionNASM is required because aws-lc-sys (pulled in transitively via russh → rustls → aws-lc-rs) assembles its crypto primitives from .asm sources at build time on Windows. Without nasm.exe on PATH, the Rust build panics with NASM command not found! Build cannot continue.
winget install --id NASM.NASM -eThe winget package does not add NASM to PATH automatically. Add it manually:
[Environment]::SetEnvironmentVariable(
"Path",
[Environment]::GetEnvironmentVariable("Path", "Machine") + ";C:\Program Files\NASM",
"Machine"
)Restart your terminal, then verify:
nasm --versionWindows Terminal provides a better experience than the default console. Install via:
winget install Microsoft.WindowsTerminalOpen PowerShell and run:
winget install Rustlang.RustupOr download the installer from https://rustup.rs/.
Follow the prompts and select the default installation (which uses the stable-x86_64-pc-windows-msvc toolchain). Close and reopen your terminal after installation.
rustc --version
cargo --version
rustup --versionrustup component add clippy rustfmtcargo install cargo-watch
cargo install sqlx-cli --no-default-features --features native-tls,sqlite,postgresrustup updatewinget install OpenJS.NodeJS.LTSClose and reopen your terminal, then verify:
node --version
npm --versionnpm install -g typescriptcargo install tauri-cli --version "^2"Verify:
cargo tauri --versionRun the environment diagnostic:
cargo tauri infoReview the output. Every line should show a checkmark. If anything is marked with a cross, install the missing dependency before continuing.
SQLite is compiled from source by the sqlx crate via the bundled sqlite3 feature — no separate installation is needed on Windows. However, if you want the sqlite3 CLI for inspecting databases:
winget install SQLite.SQLiteFor local PostgreSQL development:
winget install PostgreSQL.PostgreSQLFollow the installer prompts to set a superuser password and create the default cluster. Then open a new terminal and create the development database:
createuser --interactive --pwprompt shellstation
createdb --owner=shellstation shellstation_devSet the environment variable (add to your PowerShell profile or system environment variables):
$env:DATABASE_URL = "postgres://shellstation:<your-password>@localhost/shellstation_dev"To make it permanent, use System Settings > Environment Variables, or add the line to your $PROFILE file.
$env:DATABASE_URL = "sqlite:///$env:APPDATA/shellstation/dev.db"
New-Item -ItemType Directory -Force -Path "$env:APPDATA\shellstation"git clone https://git.fiedler.live/tux/shellstation.git
cd shellstation
npm installsqlx database create
sqlx migrate runcargo tauri devThe first build compiles all Rust dependencies and takes several minutes. Subsequent builds are incremental.
Rust checks:
cd src-tauri
cargo clippy -- -D warnings
cargo fmt -- --check
cargo test
cd ..Frontend checks:
npx eslint src/ --ext .ts,.tsx
npx prettier --check "src/**/*.{ts,tsx,css,json}"
npx tsc --noEmit
npx vitest runTauri environment check:
cargo tauri infoIf all commands pass without errors, your Windows development environment is ready.
The Rust MSVC toolchain requires the Visual Studio Build Tools. If you see linker errors, ensure you installed the "Desktop development with C++" workload and are using a terminal that has the MSVC environment loaded. Opening Developer PowerShell for VS 2022 from the Start menu guarantees the correct paths are set.
Some crates probe for system libraries using pkg-config, which is not natively available on Windows. Most ShellStation dependencies use vendored/bundled C libraries (e.g., SQLite via sqlx), so this should not be an issue. If a crate does require it, install via:
winget install bloodrock.pkg-config-liteEnable long path support if you encounter MAX_PATH (260 character) errors:
# Run PowerShell as Administrator
New-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\FileSystem" -Name "LongPathsEnabled" -Value 1 -PropertyType DWORD -ForceAlso configure Git:
git config --global core.longpaths trueWindows Defender real-time scanning can significantly slow Rust compilation. Add exclusions for:
- Your project directory (e.g.,
C:\Users\<you>\Projects\shellstation) - The Cargo registry and build cache (
%USERPROFILE%\.cargo) - The Rust toolchain directory (
%USERPROFILE%\.rustup)
Open Windows Security > Virus & threat protection > Manage settings > Exclusions > Add or remove exclusions and add each path as a folder exclusion.
If the application window opens but shows a blank white screen, WebView2 may need reinstalling. Download the Evergreen Standalone Installer from the Microsoft Edge WebView2 page and run it.
Windows aggressively caches executable icons. After rebuilding with a new or updated icon, Task Manager (and sometimes Explorer) may continue showing a generic icon even though the taskbar displays the correct one. This is a Windows icon cache issue, not a build problem.
To force a refresh, run the following in an elevated PowerShell and then reboot:
Stop-Process -Name explorer -Force
Remove-Item "$env:LOCALAPPDATA\IconCache.db" -ErrorAction SilentlyContinue
Remove-Item "$env:LOCALAPPDATA\Microsoft\Windows\Explorer\iconcache_*" -Force -ErrorAction SilentlyContinue
Start-Process explorer.exeIf the icon still does not appear after rebooting, verify it is embedded in the executable: right-click the .exe in Explorer → Properties — the custom icon should be visible on the General tab. If it shows a generic icon there as well, the issue is in the build (check that icon.ico in src-tauri/icons/ contains the expected sizes and that tauri.conf.json references it).
This section covers producing optimized, distributable binaries for all platforms.
Ensure all quality checks pass before building a release:
# Rust
cd src-tauri && cargo clippy -- -D warnings && cargo fmt -- --check && cargo test && cd ..
# Frontend
npx eslint src/ --ext .ts,.tsx
npx prettier --check "src/**/*.{ts,tsx,css,json}"
npx tsc --noEmit
npx vitest runcargo tauri buildThis command:
- Runs
npm run build(TypeScript type check + Vite production build with minification). - Compiles the Rust backend in release mode (
--release) with full optimizations. - Produces platform-specific installers in
src-tauri/target/release/bundle/.
| Platform | Artifacts | Location |
|---|---|---|
| Windows | .msi installer, .exe (NSIS installer) |
src-tauri/target/release/bundle/msi/ and nsis/ |
| macOS | .dmg disk image, .app bundle |
src-tauri/target/release/bundle/dmg/ and macos/ |
| Linux | .deb package, .AppImage |
src-tauri/target/release/bundle/deb/ and appimage/ |
The tauri.conf.json bundle.targets field controls which formats are built. The current setting "all" builds every format available on the host OS. To build only specific formats, pass --bundles on the command line:
cargo tauri build -- --bundles deb,appimage # Linux
cargo tauri build -- --bundles msi,nsis # Windows
cargo tauri build -- --bundles dmg,app # macOSValid targets: msi, nsis, dmg, app, deb, appimage, rpm.
The version-bumping checklist and end-to-end release flow live in RELEASING.md. The release profile (strip, lto, codegen-units, opt-level) is already configured in src-tauri/Cargo.toml.
ShellStation is built and released via Forgejo Actions on a self-hosted runner setup. The pipeline lives at .forgejo/workflows/release.yml.
| Job | Runner label | Outputs |
|---|---|---|
build-linux |
linux |
.deb, .AppImage |
build-windows |
windows |
.msi, .exe (NSIS) |
build-macos-x64 |
macos |
.dmg (Intel) |
build-macos-arm64 |
macos |
.dmg (Apple Silicon) |
publish-release |
linux |
Forgejo release with all artifacts |
The macOS jobs run on an Apple Silicon runner and cross-compile for Intel via --target x86_64-apple-darwin. Both architectures produce a .dmg named ShellStation-macos-{x64,arm64}-<version>.dmg.
The pipeline fires on any tag matching v*:
git tag v0.9.0
git push origin v0.9.0| Secret name | Purpose |
|---|---|
RELEASE_TOKEN |
Forgejo access token with write:repository scope. Used by the publish-release job to create the release and upload artifacts. |
GH_RELEASE_TOKEN |
GitHub personal access token with Contents: write on the mirror repo. Used to mirror the release to GitHub. |
TAURI_SIGNING_PRIVATE_KEY |
Contents of ~/.tauri/shellstation_updater.key (the Ed25519 private key used to sign updater artifacts). See §23.x below. |
TAURI_SIGNING_PRIVATE_KEY_PASSWORD |
Password chosen at key-generation time. Required by tauri build when the private key is encrypted (which it should always be). |
Generate the Forgejo token at https://git.fiedler.live/user/settings/applications, the GitHub token at https://github.com/settings/tokens, and add them under repo Settings → Actions → Secrets.
First-install bundles (the .deb, .dmg, .msi, NSIS .exe) are currently unsigned. Windows users see a SmartScreen "Unknown publisher" warning on first launch; macOS users must right-click the .app and choose Open (or run xattr -d com.apple.quarantine ShellStation.app). This is acceptable for early releases. If first-install code signing becomes a requirement later, the recommended path is SignPath.io's free OSS program for Windows and Apple's Developer ID for macOS (requires an Apple Developer account).
Updater artifacts are signed independently via the Tauri updater's Ed25519 keypair — this is a different signing scheme from OS-level code signing and is mandatory for the in-app update flow to work. See §23.x below for the one-time setup, and DESIGN.md §4.7 for the architectural overview.
The in-app updater verifies every downloaded artifact against a public key compiled into the binary. The matching private key lives only on the CI runner (as the TAURI_SIGNING_PRIVATE_KEY secret) and on whichever developer machine generated it. Without this keypair in place, the updater cannot ship — every check would fail signature verification with a confusing error. A CI preflight job rejects releases that still carry the placeholder pubkey.
Run these once per repo, on the maintainer's machine:
# Generate the Ed25519 keypair. You'll be prompted for a password — pick
# a strong one and store it somewhere safe; you'll need it for the CI
# secret below. The private key is written encrypted with this password.
npx tauri signer generate -w ~/.tauri/shellstation_updater.key
# Display the PUBLIC key (small, ~120 chars of base64). Copy this value.
cat ~/.tauri/shellstation_updater.key.pubPut the public key into src-tauri/tauri.conf.json under plugins.updater.pubkey, replacing whatever was there. Commit and push:
git add src-tauri/tauri.conf.json
git commit -m "Set updater public key"Add the matching secrets to both Forgejo Actions and (if mirroring) GitHub Actions:
| Where | Field | Value |
|---|---|---|
Forgejo: Settings → Actions → Secrets on git.fiedler.live/tux/shellstation |
TAURI_SIGNING_PRIVATE_KEY |
Full contents of ~/.tauri/shellstation_updater.key |
| Forgejo: same page | TAURI_SIGNING_PRIVATE_KEY_PASSWORD |
The password you chose at generation time |
Never commit the private key, and never paste it into chat with an assistant. The file is encrypted at rest with your password, but a copy outside the secret store is one breach away from full updater takeover (an attacker who holds the key can sign arbitrary "updates" that the running app will trust).
After pushing the public key change, push a throwaway tag like v0.0.0-test to confirm the pipeline produces signed artifacts:
git tag v0.0.0-test
git push origin v0.0.0-testIn the Forgejo workflow log:
- The
preflightjob should pass (no placeholder pubkey). - Each build job should print
Signing <path>.AppImage (or .exe, or .app.tar.gz)duringtauri build. - The
Generate latest.json (updater manifest)step should print a non-emptyplatforms:list. If the list is empty, the secrets are not reaching the build step (check spelling, runner scope, etc.).
Delete the throwaway release and tag afterwards:
git tag -d v0.0.0-test
git push origin :refs/tags/v0.0.0-test
# also delete the Forgejo + GitHub releases through the web UIEvery install carries the embedded public key forever. To rotate, you need a transition release signed by the old private key whose binary contains the new public key. Procedure:
- Generate a new keypair (do not overwrite the old one yet).
- In a new commit, replace
plugins.updater.pubkeywith the new public key. - Ship that commit as a release using the old
TAURI_SIGNING_PRIVATE_KEYsecret. Users update via the existing flow; the running binary now contains the new public key. - Update the CI secret to the new private key.
- The next release will be signed with the new private key, which existing installs trust because they took the transition release in step 3.
Skipping the transition release leaves existing installs stranded — they can never accept any update again. Users would have to download a fresh first-install bundle manually.
cargo tauri dev does not exercise the updater unless you explicitly trigger a check. The auto-check on app launch runs in production-style builds and against the configured endpoint (the GitHub Releases URL), which always returns the latest real release manifest. If you want to test the updater UI against a fake manifest:
- Spin up a local HTTPS server with a self-signed cert and serve a hand-crafted
latest.jsonplus a signed artifact. - Temporarily change
plugins.updater.endpoints[0]intauri.conf.jsonto your local URL. - Do not commit either change. Revert before pushing.
Easier alternative for UI work: stub out the dialog by setting phase to "available" directly in UpdateDialog.tsx and reloading. This skips the network/signature path entirely and lets you iterate on the dialog presentation without infrastructure.
The pipeline needs three self-hosted runners: Linux, macOS, and Windows. All run the forgejo-runner daemon and register against https://git.fiedler.live.
Any modern Debian/Ubuntu host works. Recommended: Ubuntu 22.04+ or Debian 12+.
The pipeline runs in host mode (no container), so every tool the build needs must be preinstalled on this machine — the workflow deliberately does not run apt install or setup-node at job time. Steps 1 and 2 below must be completed before the runner will build successfully.
1. Install the build prerequisites (matches what the pipeline expects to find):
sudo apt update
sudo apt install -y \
build-essential curl wget file pkg-config \
libssl-dev libgtk-3-dev libwebkit2gtk-4.1-dev \
libayatana-appindicator3-dev librsvg2-dev \
libdbus-1-dev libsoup-3.0-dev libjavascriptcoregtk-4.1-dev2. Install Node.js 20 LTS and Rust stable as a dedicated runner user:
sudo useradd -m -s /bin/bash forgejo-runner
sudo -iu forgejo-runner bash -c '
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
curl --proto "=https" --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable
'
sudo apt install -y nodejs3. Download the Forgejo runner binary:
sudo -iu forgejo-runner bash -c '
cd ~
curl -L -o forgejo-runner https://code.forgejo.org/forgejo/runner/releases/download/v6.2.2/forgejo-runner-6.2.2-linux-amd64
chmod +x forgejo-runner
./forgejo-runner generate-config > config.yml
'(Check https://code.forgejo.org/forgejo/runner/releases for the latest version.)
4. Register against Forgejo. Get a registration token from https://git.fiedler.live/-/admin/actions/runners (instance-wide) or your repo's Settings → Actions → Runners (repo-scoped):
sudo -iu forgejo-runner bash -c '
cd ~
./forgejo-runner register --no-interactive \
--instance https://git.fiedler.live \
--token <REGISTRATION_TOKEN> \
--name linux-builder \
--labels linux:host
'The linux label matches runs-on: linux in the workflow. The :host suffix is critical — it tells the runner to execute jobs directly on the host OS instead of inside a Docker container. Without it, the runner will fail to start with daemon Docker Engine socket not found unless Docker is installed. Host mode is what you want for a Tauri build runner: the host already has Rust, Node, and the GTK dev libs installed, so there's no reason to add a container layer.
5. Run as a systemd service:
sudo tee /etc/systemd/system/forgejo-runner.service >/dev/null <<'EOF'
[Unit]
Description=Forgejo Actions Runner
After=network.target
[Service]
Type=simple
User=forgejo-runner
WorkingDirectory=/home/forgejo-runner
Environment="PATH=/home/forgejo-runner/.cargo/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
ExecStart=/home/forgejo-runner/forgejo-runner daemon
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable --now forgejo-runner
sudo systemctl status forgejo-runnerVerify the runner is online at https://git.fiedler.live/-/admin/actions/runners.
Recommended: Apple Silicon Mac (M1 or later) running macOS 13 Ventura+. The runner cross-compiles for Intel (x86_64-apple-darwin) via Rosetta-capable toolchain — no Intel hardware needed.
1. Install build prerequisites (if not already present from development setup):
xcode-select --install
brew install node@20
brew link --overwrite node@20
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable
source "$HOME/.cargo/env"
rustup target add x86_64-apple-darwin aarch64-apple-darwin2. Download the Forgejo runner binary:
cd ~
curl -L -o forgejo-runner https://code.forgejo.org/forgejo/runner/releases/download/v6.2.2/forgejo-runner-6.2.2-darwin-arm64
chmod +x forgejo-runner
./forgejo-runner generate-config > config.yml(Check https://code.forgejo.org/forgejo/runner/releases for the latest version. Use darwin-amd64 on Intel Macs.)
3. Register against Forgejo:
./forgejo-runner register --no-interactive \
--instance https://git.fiedler.live \
--token <REGISTRATION_TOKEN> \
--name macos-builder \
--labels macos:hostThe macos label matches runs-on: macos in the workflow. The :host suffix is critical — it tells the runner to execute jobs directly on the host instead of inside a container.
4. Run as a launchd service:
cat > ~/Library/LaunchAgents/com.forgejo.runner.plist <<'EOF'
<?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>Label</key>
<string>com.forgejo.runner</string>
<key>ProgramArguments</key>
<array>
<string>/Users/YOUR_USER/forgejo-runner</string>
<string>daemon</string>
</array>
<key>WorkingDirectory</key>
<string>/Users/YOUR_USER</string>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>EnvironmentVariables</key>
<dict>
<key>PATH</key>
<string>/Users/YOUR_USER/.cargo/bin:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
</dict>
<key>StandardOutPath</key>
<string>/Users/YOUR_USER/forgejo-runner.log</string>
<key>StandardErrorPath</key>
<string>/Users/YOUR_USER/forgejo-runner.err</string>
</dict>
</plist>
EOF
# Replace YOUR_USER with the actual username, then load:
launchctl load ~/Library/LaunchAgents/com.forgejo.runner.plistVerify the runner appears as macos-builder at https://git.fiedler.live/-/admin/actions/runners.
5. Prevent sleep so the runner stays online when the display is off:
sudo pmset -a sleep 0 disablesleep 1Recommended OS: Windows 11 Pro 23H2 or 24H2 (x64). Reasons:
- Matches what end users have, so platform-specific bugs surface during the build
- WebView2 Runtime is preinstalled (Tauri requires it)
- Modern PowerShell, winget, and proper symlink support
- Pro (not Home) gives Remote Desktop, Group Policy, Hyper-V
Avoid Windows Server (different WebView2 story), Windows 10 (EOL October 2025), and Windows 11 LTSC (no WebView2 by default).
1. Base OS prep:
-
Install Windows 11 Pro, fully patch it, set a static hostname
-
Create a dedicated local account for the runner service (e.g.
forgejo-svc) with a real password — not a Microsoft account and not PIN-only login. Service logon requires an NTLM password blob that MS-account/PIN accounts do not have, and attempting to use one producesRUNAS ERROR: Unable to acquire user passwordand service start failures with event ID 7000 "Access is denied". Create it viaNew-LocalUserin an elevated PowerShell:$pw = Read-Host -AsSecureString "Password" New-LocalUser -Name "forgejo-svc" -Password $pw -PasswordNeverExpires -AccountNeverExpires -UserMayNotChangePassword Add-LocalGroupMember -Group "Administrators" -Member "forgejo-svc"
-
Install the build toolchain (winget steps below) while logged in as this service account, so Rust and Node land in that account's profile where the service will later find them.
-
Disable sleep and hibernate so the runner stays online:
powercfg /change standby-timeout-ac 0
2. Install build prerequisites (elevated PowerShell):
winget install --id Git.Git -e
winget install --id OpenJS.NodeJS.LTS -e
winget install --id Rustlang.Rustup -e
winget install --id Microsoft.VisualStudio.2022.BuildTools -e --override "--quiet --wait --add Microsoft.VisualStudio.Workload.VCTools --add Microsoft.VisualStudio.Component.Windows11SDK.22621 --includeRecommended"
winget install --id Microsoft.EdgeWebView2Runtime -e
winget install --id NASM.NASM -eAfter installing NASM, add its install directory to the Machine PATH so the runner service picks it up (the winget package does not do this automatically):
[Environment]::SetEnvironmentVariable(
"Path",
[Environment]::GetEnvironmentVariable("Path", "Machine") + ";C:\Program Files\NASM",
"Machine"
)NASM is required because aws-lc-sys (pulled in transitively via russh → rustls → aws-lc-rs) assembles its crypto primitives from .asm sources at build time on Windows. Without nasm.exe on PATH, the Rust build panics with NASM command not found! Build cannot continue. Restart the runner service (or reboot) after changing the Machine PATH so the new value is inherited.
Open a fresh terminal and verify:
git --version
node --version
rustup default stable
rustc --version
cargo --version3. Install the Forgejo runner binary:
- Download
forgejo-runner-<ver>-windows-amd64.exefrom https://code.forgejo.org/forgejo/runner/releases - Place it at
C:\forgejo-runner\forgejo-runner.exe - In PowerShell:
cd C:\forgejo-runner
.\forgejo-runner.exe generate-config > config.yml4. Register against Forgejo (same token source as the Linux runner):
.\forgejo-runner.exe register --no-interactive `
--instance https://git.fiedler.live `
--token <REGISTRATION_TOKEN> `
--name windows-builder `
--labels windows:hostThe windows label matches runs-on: windows in the workflow. The :host suffix is critical — it tells the runner to execute jobs directly on Windows instead of inside a Docker container. Without it, the service will fail to start with daemon Docker Engine socket not found. If you already registered without :host and hit that error, stop the service, delete C:\forgejo-runner\.runner, grab a fresh registration token from the Forgejo admin UI, and re-run the register command with the corrected label.
5. Run as a Windows service using NSSM so it survives reboots:
winget install --id NSSM.NSSM -e
nssm install ForgejoRunner "C:\forgejo-runner\forgejo-runner.exe" "daemon"
nssm set ForgejoRunner AppDirectory "C:\forgejo-runner"
nssm set ForgejoRunner Start SERVICE_AUTO_START
nssm set ForgejoRunner ObjectName ".\<your-username>" "<password>"
nssm start ForgejoRunnerThe ObjectName line is critical. NSSM defaults to running services as LocalSystem, which has its own empty profile and cannot see rustup, cargo, or node installed into C:\Users\<you>\.cargo\bin and %APPDATA%\npm. Running the service as the user that installed the toolchain inherits that user's PATH and profile, so the build just works. The alternative — installing Rust machine-wide under C:\Rust with RUSTUP_HOME and CARGO_HOME set in the Machine environment — also works but is more ceremony.
Note the .\ prefix on ObjectName. It tells Windows the account is local to this machine rather than a domain account; without it, the logon will fail even for a valid local user.
Before starting the service, the account also needs the Log on as a service user right. By default even local admins do not have it. Grant it via secpol.msc → Local Policies → User Rights Assignment → Log on as a service → Add User or Group. Without this right, nssm start ForgejoRunner returns START: Access is denied.
Verify the runner appears as windows-builder at https://git.fiedler.live/-/admin/actions/runners.
6. Smoke test: push a throwaway tag (git tag v0.9.0-test1 && git push origin v0.9.0-test1) and watch both runners pick up their jobs in the Actions tab. Delete the tag after the test runs.
Keep dependencies up-to-date periodically (at minimum before each release) to pick up security patches and bug fixes.
# Check for outdated crates (install cargo-outdated if not present)
cargo install cargo-outdated
cd src-tauri && cargo outdated
# Update all crates to the latest version allowed by Cargo.toml constraints
cd src-tauri && cargo update
# To upgrade past semver-pinned versions, edit src-tauri/Cargo.toml
# then run cargo update again. Review changelogs for breaking changes.
# Audit for known security vulnerabilities (install cargo-audit if not present)
cargo install cargo-audit
cd src-tauri && cargo auditcargo audit reports the vulnerable crate name, its version, a RUSTSEC advisory ID, and a link to the full advisory description.
Step 1 — Find why the crate is in your tree:
# Shows which of your dependencies pulls in the vulnerable crate
cd src-tauri && cargo tree -i <vulnerable-crate>Step 2 — Fix it:
- Direct dependency (listed in
Cargo.toml): bump the version inCargo.tomlto one that resolves the advisory, then runcargo update. - Transitive dependency (pulled in by another crate): run
cargo update -p <vulnerable-crate>to pull a patched version within the existing semver range. If no patched version exists, update the parent crate that depends on it. - No fix available yet: check the advisory details. If the vulnerability does not apply to your usage (e.g., an unused feature or code path), you can temporarily suppress it with
cargo audit --ignore RUSTSEC-XXXX-XXXXand track the upstream issue until a fix is released.
After updating, verify the build and lint pass:
cd src-tauri && cargo build && cargo clippy -- -D warnings && cargo test# Check for outdated packages
npm outdated
# Update all packages within the version ranges in package.json
npm update
# To upgrade to new major versions beyond the pinned ranges, use:
npx npm-check-updates -u # rewrites package.json to latest versions
npm install # install the updated versions
# Audit for known security vulnerabilities
npm audit
# Auto-fix vulnerabilities where possible
npm audit fixAfter updating, verify the frontend build and lint pass:
npx tsc --noEmit && npx vite build
npx eslint src/ --ext .ts,.tsx
npx prettier --check "src/**/*.{ts,tsx,css,json}"The Tauri CLI is listed as a devDependency in package.json (@tauri-apps/cli) and is updated along with npm packages. Ensure the CLI version stays compatible with the tauri and tauri-build crate versions in Cargo.toml — they should track the same major.minor release line.
- Create a dedicated branch:
git checkout -b deps/update-YYYY-MM-DD - Update Rust crates (
cargo update) and npm packages (npm updateornpx npm-check-updates -u && npm install). - Run
cargo auditandnpm auditto check for remaining vulnerabilities. - Run the full lint and test suite (see Verify the Environment).
- Test the application manually (connect via SSH/Telnet, open local terminal).
- Commit and open a PR for review.
To test the functionality of connecting to a remote system via SSH or Telnet, including with the use of a jumphost, see the Readme in the directory ssh-testlab. The setup allows tests on a small footprint docker container construct, providing an SSH jumphost, an SSH target, and a Telnet target.