From 55ea7fdcc817930e85c723a7922dda7946680306 Mon Sep 17 00:00:00 2001 From: karim Date: Fri, 5 Jun 2026 01:33:19 +0200 Subject: [PATCH] feat(linux): native build path + Linux-specific launcher optimizations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - X-Plane auto-detection now finds Steam (native + Flatpak), /opt and external-drive SteamLibrary installs, not just ~/ and macOS/Windows paths. - Close-to-background minimizes on Linux instead of hiding to tray, so the app is never stranded on desktops without a tray (e.g. vanilla GNOME). - Disable WebKitGTK's DMABUF renderer on Linux to avoid black/blank windows on some GPU/driver combos (overridable via the env var). - Launcher font stack gains Linux UI/mono fallbacks (Cantarell/Ubuntu/Noto). - scripts/build.sh: Docker-free Linux AppImage build — repack the cockpit into the prebuilt bundle for web-only changes; recompile natively for code changes. Co-Authored-By: Claude Opus 4.8 (1M context) --- desktop/src-tauri/src/lib.rs | 65 ++++++++++++++++++++---- desktop/src-tauri/src/main.rs | 9 ++++ desktop/ui/styles.css | 6 +-- scripts/build.sh | 94 +++++++++++++++++++++++++++++++++++ 4 files changed, 162 insertions(+), 12 deletions(-) create mode 100755 scripts/build.sh diff --git a/desktop/src-tauri/src/lib.rs b/desktop/src-tauri/src/lib.rs index ff78b3f..ff105db 100644 --- a/desktop/src-tauri/src/lib.rs +++ b/desktop/src-tauri/src/lib.rs @@ -66,30 +66,71 @@ fn suggest_port(start: u16) -> u16 { start } +// A directory is an X-Plane 12 root iff it holds Resources/default data. +fn is_xplane_root(p: &std::path::Path) -> bool { + p.join("Resources").join("default data").is_dir() +} + #[tauri::command] fn default_xplane_path() -> Option { let home = std::env::var("HOME") .or_else(|_| std::env::var("USERPROFILE")) .unwrap_or_default(); - let candidates = [ + let user = std::env::var("USER") + .or_else(|_| std::env::var("USERNAME")) + .unwrap_or_default(); + + let mut candidates = vec![ + // Generic / cross-platform format!("{home}/X-Plane 12"), format!("{home}/Desktop/X-Plane 12"), + format!("{home}/Games/X-Plane 12"), + // Steam on Linux — native, plus the Flatpak Steam sandbox path + format!("{home}/.steam/steam/steamapps/common/X-Plane 12"), + format!("{home}/.local/share/Steam/steamapps/common/X-Plane 12"), + format!("{home}/.var/app/com.valvesoftware.Steam/.local/share/Steam/steamapps/common/X-Plane 12"), + // System-wide (Linux) + "/opt/X-Plane 12".to_string(), + // macOS "/Applications/X-Plane 12".to_string(), + // Windows "C:/X-Plane 12".to_string(), "D:/X-Plane 12".to_string(), ]; + + // Secondary/external drives: Steam libraries on extra disks usually mount + // under /media/$USER, /run/media/$USER or /mnt. Probe each mounted volume + // for the standard SteamLibrary layout (and a bare "X-Plane 12" folder). + for base in [ + format!("/media/{user}"), + format!("/run/media/{user}"), + "/mnt".to_string(), + ] { + if let Ok(entries) = std::fs::read_dir(&base) { + for vol in entries.flatten().map(|e| e.path()) { + candidates.push( + vol.join("SteamLibrary/steamapps/common/X-Plane 12") + .to_string_lossy() + .into_owned(), + ); + candidates.push( + vol.join("steamapps/common/X-Plane 12") + .to_string_lossy() + .into_owned(), + ); + candidates.push(vol.join("X-Plane 12").to_string_lossy().into_owned()); + } + } + } + candidates .into_iter() - .find(|c| PathBuf::from(c).join("Resources").join("default data").is_dir()) + .find(|c| is_xplane_root(&PathBuf::from(c))) } #[tauri::command] fn valid_xplane_path(path: String) -> bool { - !path.is_empty() - && PathBuf::from(&path) - .join("Resources") - .join("default data") - .is_dir() + !path.is_empty() && is_xplane_root(&PathBuf::from(&path)) } #[tauri::command] @@ -225,11 +266,17 @@ pub fn run() { build_tray(app.handle())?; Ok(()) }) - // Closing the window hides it instead of quitting, so the server keeps - // serving tablets in the background. Quit from the tray. + // Closing the window keeps the app alive so the server keeps serving + // tablets in the background. On macOS/Windows the tray icon is always + // reachable, so fully hide. On Linux many desktops (notably vanilla + // GNOME) show no tray at all, so hiding would strand the app with no way + // back — minimize instead, keeping it in the taskbar while it runs. .on_window_event(|window, event| { if let tauri::WindowEvent::CloseRequested { api, .. } = event { api.prevent_close(); + #[cfg(target_os = "linux")] + let _ = window.minimize(); + #[cfg(not(target_os = "linux"))] let _ = window.hide(); } }) diff --git a/desktop/src-tauri/src/main.rs b/desktop/src-tauri/src/main.rs index 9afb3a1..99560d3 100644 --- a/desktop/src-tauri/src/main.rs +++ b/desktop/src-tauri/src/main.rs @@ -2,5 +2,14 @@ #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] fn main() { + // WebKitGTK's DMABUF renderer shows a black/blank window on a number of Linux + // GPU/driver combinations (notably Nvidia and older Mesa). This launcher is a + // simple control panel, so favour reliability over GPU compositing: disable + // the DMABUF renderer unless the user has already set the variable themselves. + #[cfg(target_os = "linux")] + if std::env::var_os("WEBKIT_DISABLE_DMABUF_RENDERER").is_none() { + std::env::set_var("WEBKIT_DISABLE_DMABUF_RENDERER", "1"); + } + xplane_cockpit_lib::run() } diff --git a/desktop/ui/styles.css b/desktop/ui/styles.css index 05a114c..d33c0f7 100644 --- a/desktop/ui/styles.css +++ b/desktop/ui/styles.css @@ -19,7 +19,7 @@ html, body { margin: 0; height: 100%; } body { background: var(--bg); color: var(--txt); - font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", Roboto, sans-serif; + font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", "Inter", "Cantarell", "Ubuntu", "Noto Sans", Roboto, sans-serif; font-size: 13px; user-select: none; -webkit-font-smoothing: antialiased; } .panel { display: flex; flex-direction: column; height: 100vh; padding: 16px; gap: 14px; } @@ -69,7 +69,7 @@ input:focus { outline: none; border-color: var(--green); box-shadow: 0 0 0 3px r .live.hidden { display: none; } .url-row { display: flex; gap: 8px; align-items: center; } -.url-row code { flex: 1; background: var(--bg); border: 1px solid var(--line); color: var(--green); border-radius: 7px; padding: 10px 12px; font-size: 16px; font-weight: 600; letter-spacing: .3px; user-select: text; font-family: ui-monospace, "SF Mono", Menlo, monospace; } +.url-row code { flex: 1; background: var(--bg); border: 1px solid var(--line); color: var(--green); border-radius: 7px; padding: 10px 12px; font-size: 16px; font-weight: 600; letter-spacing: .3px; user-select: text; font-family: ui-monospace, "SF Mono", Menlo, "JetBrains Mono", "DejaVu Sans Mono", "Noto Sans Mono", monospace; } .quick { display: flex; gap: 6px; } .quick .btn { flex: 1; } @@ -80,7 +80,7 @@ input:focus { outline: none; border-color: var(--green); box-shadow: 0 0 0 3px r .log-wrap { background: var(--bg2); border: 1px solid var(--line-soft); border-radius: 12px; padding: 6px 12px; } .log-wrap summary { color: var(--mut); font-size: 12px; cursor: pointer; padding: 4px 0; } -#log { margin: 6px 0 2px; max-height: 140px; overflow-y: auto; font-family: ui-monospace, "SF Mono", Menlo, monospace; font-size: 11px; color: var(--mut); white-space: pre-wrap; } +#log { margin: 6px 0 2px; max-height: 140px; overflow-y: auto; font-family: ui-monospace, "SF Mono", Menlo, "JetBrains Mono", "DejaVu Sans Mono", "Noto Sans Mono", monospace; font-size: 11px; color: var(--mut); white-space: pre-wrap; } .ft { display: flex; align-items: center; justify-content: space-between; color: var(--mut); font-size: 12px; } .link { background: none; border: none; color: var(--green); cursor: pointer; font-size: 12px; font-family: inherit; } diff --git a/scripts/build.sh b/scripts/build.sh new file mode 100755 index 0000000..1400905 --- /dev/null +++ b/scripts/build.sh @@ -0,0 +1,94 @@ +#!/usr/bin/env bash +# Build the Linux artifacts (AppImage + .deb) WITHOUT Docker and WITHOUT +# recompiling the Rust launcher. +# +# WHY: this app's launcher (Tauri/Rust) barely ever changes — what changes is the +# cockpit itself (web/ JSX), which ships as a *resource* the Bun sidecar serves at +# runtime. So a release is really just: rebuild the web cockpit, drop it into the +# already-compiled bundle, and repack. That's seconds, not a Docker cross-build. +# +# It reuses three things produced by a prior full `tauri build`: +# * the compiled launcher + GTK/WebKit libs in the AppDir +# * the cached linuxdeploy appimage packer (~/.cache/tauri/...) +# * the seed .deb (for its control metadata + file tree) +# and refreshes the cockpit (usr/lib/X-Plane Cockpit/web) + Lua plugins in both. +# +# Seed once (native, no Docker) if the AppDir is missing: +# scripts/prep-desktop.sh +# npx --prefix desktop tauri build --target x86_64-unknown-linux-gnu --bundles appimage,deb +# Thereafter just run this script for every web-only change. +# +# CAVEAT: the launcher binary is reused as-is, so the *version it reports itself* +# (used by the auto-updater) is whatever it was last compiled with — not +# necessarily $VERSION. The cockpit features are unaffected (they come from the +# refreshed web resources). If you bump the version AND rely on the updater, +# recompile the launcher once with the tauri build line above. +set -euo pipefail +ROOT="$(cd "$(dirname "$0")/.." && pwd)"; cd "$ROOT" +die(){ echo "!! $*" >&2; exit 1; } + +TARGET=x86_64-unknown-linux-gnu +BUNDLE="$ROOT/target-linux/$TARGET/release/bundle" +APPDIR="$BUNDLE/appimage/X-Plane Cockpit.AppDir" +RESDIR="$APPDIR/usr/lib/X-Plane Cockpit" +PLUGIN="$HOME/.cache/tauri/linuxdeploy-plugin-appimage.AppImage" +SIDECAR="$ROOT/desktop/src-tauri/binaries/xpbridge-$TARGET" +PKGNAME="X-Plane Cockpit" +VERSION="$(node -p "require('$ROOT/desktop/src-tauri/tauri.conf.json').version")" + +echo "==> X-Plane Cockpit Linux repack — v$VERSION (no Docker, no Rust recompile)" + +# ---- preflight ----------------------------------------------------------- +[[ -d "$APPDIR" ]] || die "no prebuilt AppDir: $APPDIR — seed one full tauri build first (see header)" +[[ -x "$PLUGIN" ]] || die "missing appimage packer: $PLUGIN — run one tauri appimage build to fetch it" +[[ -f "$SIDECAR" ]] || die "missing sidecar: $SIDECAR — run scripts/prep-desktop.sh" +command -v ar >/dev/null || die "'ar' (binutils) required to assemble the .deb" +command -v tar >/dev/null || die "'tar' required" + +SIGN=0 +[[ -f desktop/.tauri-signing.key && -f desktop/.tauri-signing.pw ]] && SIGN=1 +sign(){ # $1 = artifact; writes .sig next to it + if [[ $SIGN == 1 ]]; then + TAURI_SIGNING_PRIVATE_KEY="$(cat desktop/.tauri-signing.key)" \ + TAURI_SIGNING_PRIVATE_KEY_PASSWORD="$(cat desktop/.tauri-signing.pw)" \ + npx --prefix desktop tauri signer sign "$1" >/dev/null + echo " signed: $(basename "$1").sig" + else + rm -f "$1.sig" + fi +} + +# ---- 1. build the web cockpit, refresh repo resources -------------------- +echo "==> building web cockpit (vite)" +( cd web && npm run build >/dev/null ) +echo "==> refreshing desktop/src-tauri/resources" +rm -rf "desktop/src-tauri/resources/web"; mkdir -p "desktop/src-tauri/resources/web" +cp -R web/dist/. "desktop/src-tauri/resources/web/" +rm -rf "desktop/src-tauri/resources/plugins"; mkdir -p "desktop/src-tauri/resources/plugins" +cp plugins/*.lua "desktop/src-tauri/resources/plugins/" + +# ---- 2. refresh the cockpit inside the prebuilt AppDir ------------------- +echo "==> updating bundled cockpit inside AppDir" +rm -rf "$RESDIR/web"; mkdir -p "$RESDIR/web"; cp -R web/dist/. "$RESDIR/web/" +rm -rf "$RESDIR/plugins"; mkdir -p "$RESDIR/plugins"; cp plugins/*.lua "$RESDIR/plugins/" +# the AppDir's sidecar copy may be a patchelf-corrupted one — restore the pristine +cp -f "$SIDECAR" "$APPDIR/usr/bin/xpbridge"; chmod +x "$APPDIR/usr/bin/xpbridge" + +# ---- 3. pack the AppImage (cached linuxdeploy plugin, no patchelf) ------- +OUTIMG="$BUNDLE/appimage/${PKGNAME}_${VERSION}_amd64.AppImage" +echo "==> packing AppImage -> $(basename "$OUTIMG")" +rm -f "$OUTIMG" +APPIMAGE_EXTRACT_AND_RUN=1 NO_STRIP=1 ARCH=x86_64 LDAI_OUTPUT="$OUTIMG" \ + "$PLUGIN" --appdir "$APPDIR" >/dev/null +[[ -f "$OUTIMG" ]] || die "AppImage packing produced nothing" +chmod +x "$OUTIMG" +sign "$OUTIMG" +echo " AppImage: $(du -h "$OUTIMG" | cut -f1)" + +# The .deb is intentionally NOT built: the AppImage is the single self-contained, +# self-updating artifact (the Tauri Linux updater swaps the AppImage in place; it +# never uses a .deb). If you ever want a .deb too, run a full native tauri build +# with --bundles appimage,deb. + +echo "==> done. artifact:" +ls -1 "$OUTIMG"