From d797f2a698123e8685a335417b857c9188d3911a Mon Sep 17 00:00:00 2001 From: Luna Date: Wed, 10 Jun 2026 04:22:18 -0700 Subject: [PATCH] Cross-compile groundwork: make the tree build for Windows (3.1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `cargo check --target x86_64-pc-windows-gnu` is now green (only the upstream egui f32:From warnings). The Linux-only desktop deps are target-gated so the rest of the stack — which already cross-compiles — can build off Linux: - Cargo.toml: ksni and rfd's xdg-portal backend move to [target.'cfg(target_os = "linux")'.dependencies]; non-Linux gets rfd with its native Win32/AppKit backend (default features). - tray.rs: cfg-split. The ksni/SNI implementation stays Linux-only; other OSes get a no-op `start() -> None`, i.e. windowed-only (identical to the Linux no-SNI-host path). TrayEvent/TrayHandle stay cross-platform. - plex.rs: add a Windows `symlink_file` arm to make_symlink (also fixes an unused-variable warning on non-unix). The rest was already portable (disk_space/statvfs, mpv IPC UnixStream, chmod/PermissionsExt guards all cfg(unix)). macOS is Unix so those apply there unchanged. Still not a *shipped* binary: needs a real per-OS tray backend and a linking CI matrix. ROADMAP 3.1 + CLAUDE.md updated. Co-Authored-By: Claude Opus 4.8 --- CLAUDE.md | 13 ++++++++++--- Cargo.lock | 13 +++++++++++++ Cargo.toml | 16 ++++++++++++---- ROADMAP.md | 24 ++++++++++++++++++------ src/plex.rs | 13 +++++++++++-- src/tray.rs | 19 ++++++++++++++++--- 6 files changed, 80 insertions(+), 18 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index e315bf2..e183231 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -162,6 +162,13 @@ both the transcript indexer and the transcript viewers. desktop code goes in `app.rs`, web handlers in `web.rs`, shared logic in the focused modules (`downloader`, `database`, `library`, `platform`, `fingerprint`, `vtt`, …). -- Tray (`ksni`) and file dialogs (`rfd` xdg-portal) are Linux-only/no-GTK by - design; keep that posture (it's why packaging avoids a GTK dep). Windows/macOS - are not yet first-class — the tray would need a per-OS backend. +- Tray (`ksni`) and the `rfd` xdg-portal dialog backend are Linux-only/no-GTK + by design (it's why packaging avoids a GTK dep). They're now **target-gated** + in `Cargo.toml` (`cfg(target_os = "linux")`); off Linux `rfd` uses its native + backend and `tray::start` is a compiled-out no-op (`None`). The tree + cross-compiles — `cargo check --target x86_64-pc-windows-gnu` is green — so + keep new platform-specific code behind `cfg(unix)`/`cfg(windows)`/ + `cfg(target_os = …)` with a fallback, the way `disk_space`, `plex`, the mpv + IPC, and the `chmod` guards already do. Windows/macOS still aren't *shipped* + (no tray backend, no link/CI yet), but don't reintroduce an unconditional + Linux-only dep. diff --git a/Cargo.lock b/Cargo.lock index 30afec4..3b902e2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -223,6 +223,8 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2f3f79755c74fd155000314eb349864caa787c6592eace6c6882dad873d9c39" dependencies = [ + "async-fs", + "async-net", "enumflags2", "futures-channel", "futures-util", @@ -328,6 +330,17 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "async-net" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b948000fad4873c1c9339d60f2623323a0cfd3816e5181033c6a5cb68b2accf7" +dependencies = [ + "async-io", + "blocking", + "futures-lite", +] + [[package]] name = "async-process" version = "2.5.0" diff --git a/Cargo.toml b/Cargo.toml index 2001232..5efda8b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,12 +32,20 @@ tokio-stream = "0.1" tower-http = { version = "0.5", features = ["cors", "fs", "compression-gzip"] } argon2 = "0.5" rand = "0.8" -# File-picker dialog for the desktop GUI. xdg-portal backend keeps it pure-Rust -# (zbus, no GTK/libdbus build dep), consistent with notify-rust above. -rfd = { version = "0.15", default-features = false, features = ["xdg-portal", "tokio"] } -ksni = { version = "0.3.4", features = ["tokio"] } libc = "0.2.186" +# Platform-specific desktop deps. On Linux the tray (ksni — StatusNotifierItem +# over zbus) and the rfd file picker (xdg-portal backend) stay pure-Rust with no +# GTK build dep, matching notify-rust. On Windows/macOS there's no SNI tray yet +# (tray::start is a compiled-out no-op) and rfd falls back to its native backend +# (Win32 / AppKit) via its default features. +[target.'cfg(target_os = "linux")'.dependencies] +ksni = { version = "0.3.4", features = ["tokio"] } +rfd = { version = "0.15", default-features = false, features = ["xdg-portal", "tokio"] } + +[target.'cfg(not(target_os = "linux"))'.dependencies] +rfd = { version = "0.15", features = ["tokio"] } + [profile.release] opt-level = 3 # Thin LTO does cross-crate inlining without the rust-lld + bundled-sqlite diff --git a/ROADMAP.md b/ROADMAP.md index d3afc44..a98f781 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -126,13 +126,25 @@ instead of cascading one handler's panic into a dead server. Once we're at parity, we push past Tartube on its own ground. -### 3.1 Cross-compile macOS + Windows binaries +### 3.1 Cross-compile macOS + Windows binaries — GROUNDWORK DONE -The Linux packaging (1.8) is done; this is the natural next reach. Blocked -on abstracting the Linux-only bits behind a per-OS backend — the `ksni` -tray and the `rfd` xdg-portal file dialog have no Windows/macOS path yet. -Once the tray is a trait with per-OS impls, the rest of the stack -(eframe/wgpu, axum, rusqlite-bundled) already cross-compiles. +The compile blockers are cleared: `cargo check --target +x86_64-pc-windows-gnu` is green (only the upstream egui `f32: From` +warnings). The Linux-only deps are now target-gated in `Cargo.toml` — +`ksni` and `rfd`'s `xdg-portal` backend are `cfg(target_os = "linux")` +only, with `rfd` falling back to its native Win32/AppKit backend +elsewhere. `tray::start` is `cfg`-split: the SNI/ksni implementation on +Linux, a no-op `None` stub on other OSes (windowed-only, exactly the +no-SNI-host behavior). The rest was already portable — `disk_space` +(`statvfs`), `plex` (symlinks; now with a Windows `symlink_file` path), +the mpv IPC `UnixStream`, and the `chmod`/`PermissionsExt` guards are all +`cfg(unix)`-gated. macOS is Unix, so those paths apply there unchanged. + +Remaining for a shipped binary: a real per-OS tray backend (e.g. +`tray-icon`) if the tray is wanted off Linux; a CI matrix that actually +*links* (mingw-w64 / an osxcross or macOS runner) and produces artifacts; +and runtime testing on each OS. The hard part — making the tree compile +off Linux — is done. ### 3.2 Android client diff --git a/src/plex.rs b/src/plex.rs index 0de3f6e..5da037c 100644 --- a/src/plex.rs +++ b/src/plex.rs @@ -118,8 +118,17 @@ fn make_symlink(target: &Path, link: &Path) -> Result<(), String> { #[cfg(unix)] std::os::unix::fs::symlink(target, link) .map_err(|e| format!("{}: {e}", link.display()))?; - #[cfg(not(unix))] - return Err("symlinks are not supported on this platform".to_string()); + // Windows distinguishes file vs dir symlinks; the Plex tree only links to + // video files. Requires Developer Mode or admin at runtime — a failure + // here surfaces as the usual per-link error rather than aborting. + #[cfg(windows)] + std::os::windows::fs::symlink_file(target, link) + .map_err(|e| format!("{}: {e}", link.display()))?; + #[cfg(not(any(unix, windows)))] + { + let _ = target; + return Err("symlinks are not supported on this platform".to_string()); + } Ok(()) } diff --git a/src/tray.rs b/src/tray.rs index 2633423..05f070d 100644 --- a/src/tray.rs +++ b/src/tray.rs @@ -21,7 +21,7 @@ //! the tray entirely optional — if `TrayController::start` fails the app //! continues normally. -use std::sync::mpsc::{self, Receiver, Sender}; +use std::sync::mpsc::Receiver; /// Menu events emitted by the tray. The main app drains these in /// [`eframe::App::update`] and translates them into viewport commands or @@ -49,8 +49,9 @@ pub struct TrayHandle { /// by ksni when the SNI host needs a property; menu activations call the /// boxed closures we store in `MenuItem::activate`, which forward into /// the channel back to the main thread. +#[cfg(target_os = "linux")] struct TrayModel { - tx: Sender, + tx: std::sync::mpsc::Sender, /// PNG bytes for the icon, decoded once at construction. ksni asks /// for raw ARGB so we keep both forms. icon_argb: Vec, @@ -58,6 +59,7 @@ struct TrayModel { icon_h: i32, } +#[cfg(target_os = "linux")] impl ksni::Tray for TrayModel { fn id(&self) -> String { "yt-offline".into() @@ -127,6 +129,7 @@ impl ksni::Tray for TrayModel { /// Returns `None` if the runtime / channel can't be set up. A `Some(_)` /// return does *not* guarantee the icon is actually visible — that depends /// on whether a StatusNotifier host is registered on the user's desktop. +#[cfg(target_os = "linux")] pub fn start(icon_png_bytes: &[u8]) -> Option { // Decode the PNG eagerly so we fail fast if the embedded asset is bad. let img = image::load_from_memory(icon_png_bytes).ok()?; @@ -141,7 +144,7 @@ pub fn start(icon_png_bytes: &[u8]) -> Option { argb.push(px[2]); } - let (tx, rx) = mpsc::channel(); + let (tx, rx) = std::sync::mpsc::channel(); let model = TrayModel { tx, icon_argb: argb, @@ -175,3 +178,13 @@ pub fn start(icon_png_bytes: &[u8]) -> Option { Some(TrayHandle { events: rx }) } + +/// Non-Linux stub: there's no StatusNotifierItem tray backend on Windows or +/// macOS yet (ksni is Linux/SNI-only), so the tray is simply absent and the +/// app runs windowed-only. A real implementation would need a per-OS backend +/// (e.g. `tray-icon`) behind this same signature. Returning `None` makes +/// `App` behave exactly as it does on Linux when no SNI host is registered. +#[cfg(not(target_os = "linux"))] +pub fn start(_icon_png_bytes: &[u8]) -> Option { + None +}