Gate desktop-only deps for Tauri Android compilation

Make keyring optional behind keyring-storage feature in onyx-core.
Make notify/notify-debouncer-mini optional behind desktop feature in Tauri.
Gate all file watcher code behind #[cfg(not(target_os = "android"))].
Provide env-var-only credential fallbacks when keyring is disabled.
This commit is contained in:
Tristan Michael 2026-04-01 17:35:56 -07:00 committed by GitButler
parent 6aa87b6df9
commit 326ebd83d8
4 changed files with 57 additions and 4 deletions

View file

@ -19,14 +19,16 @@ tauri-plugin-dialog = "2"
tauri-plugin-os = "2" tauri-plugin-os = "2"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_json = "1" serde_json = "1"
onyx-core = { path = "../../../crates/onyx-core" } onyx-core = { path = "../../../crates/onyx-core", default-features = false }
tokio = { version = "1", features = ["full"] } tokio = { version = "1", features = ["full"] }
uuid = { version = "1", features = ["serde", "v4"] } uuid = { version = "1", features = ["serde", "v4"] }
chrono = { version = "0.4", features = ["serde"] } chrono = { version = "0.4", features = ["serde"] }
notify = "7" notify = { version = "7", optional = true }
notify-debouncer-mini = "0.5" notify-debouncer-mini = { version = "0.5", optional = true }
[package.metadata.tauri] [package.metadata.tauri]
[features] [features]
default = ["desktop"]
desktop = ["notify", "notify-debouncer-mini", "onyx-core/keyring-storage"]
custom-protocol = ["tauri/custom-protocol"] custom-protocol = ["tauri/custom-protocol"]

View file

@ -4,6 +4,7 @@ use std::time::Instant;
use chrono::Utc; use chrono::Utc;
#[cfg(not(target_os = "android"))]
use notify_debouncer_mini::{new_debouncer, DebouncedEventKind}; use notify_debouncer_mini::{new_debouncer, DebouncedEventKind};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tauri::{Emitter, Manager, State}; use tauri::{Emitter, Manager, State};
@ -17,10 +18,12 @@ use onyx_core::{
webdav, webdav,
}; };
#[cfg(not(target_os = "android"))]
/// Active file watcher stored globally so it lives for the app lifetime. /// Active file watcher stored globally so it lives for the app lifetime.
static WATCHER: Mutex<Option<notify_debouncer_mini::Debouncer<notify::RecommendedWatcher>>> = static WATCHER: Mutex<Option<notify_debouncer_mini::Debouncer<notify::RecommendedWatcher>>> =
Mutex::new(None); Mutex::new(None);
#[cfg(not(target_os = "android"))]
/// Shared mute timestamp — set before writes, checked by the watcher. /// Shared mute timestamp — set before writes, checked by the watcher.
static LAST_WRITE: Mutex<Option<Instant>> = Mutex::new(None); static LAST_WRITE: Mutex<Option<Instant>> = Mutex::new(None);
@ -55,10 +58,14 @@ impl From<CoreSyncResult> for SyncResult {
} }
/// Suppress file watcher events for the next second (call before writes). /// Suppress file watcher events for the next second (call before writes).
#[cfg(not(target_os = "android"))]
fn mute_watcher(_state: &mut AppState) { fn mute_watcher(_state: &mut AppState) {
*LAST_WRITE.lock().unwrap() = Some(Instant::now()); *LAST_WRITE.lock().unwrap() = Some(Instant::now());
} }
#[cfg(target_os = "android")]
fn mute_watcher(_state: &mut AppState) {}
/// Helper: get or open a TaskRepository for the current workspace. /// Helper: get or open a TaskRepository for the current workspace.
fn ensure_repo(state: &mut AppState) -> Result<(), String> { fn ensure_repo(state: &mut AppState) -> Result<(), String> {
if state.repo.is_some() { if state.repo.is_some() {
@ -463,6 +470,7 @@ async fn sync_workspace(
// ── File watcher ──────────────────────────────────────────────────── // ── File watcher ────────────────────────────────────────────────────
#[cfg(not(target_os = "android"))]
fn start_watcher(handle: tauri::AppHandle, path: PathBuf) { fn start_watcher(handle: tauri::AppHandle, path: PathBuf) {
let handle = handle.clone(); let handle = handle.clone();
let debouncer = new_debouncer( let debouncer = new_debouncer(
@ -492,12 +500,19 @@ fn start_watcher(handle: tauri::AppHandle, path: PathBuf) {
} }
} }
#[cfg(not(target_os = "android"))]
#[tauri::command] #[tauri::command]
fn watch_workspace(path: String, app_handle: tauri::AppHandle) -> Result<(), String> { fn watch_workspace(path: String, app_handle: tauri::AppHandle) -> Result<(), String> {
start_watcher(app_handle, PathBuf::from(path)); start_watcher(app_handle, PathBuf::from(path));
Ok(()) Ok(())
} }
#[cfg(target_os = "android")]
#[tauri::command]
fn watch_workspace(_path: String, _app_handle: tauri::AppHandle) -> Result<(), String> {
Ok(())
}
// ── App entry ──────────────────────────────────────────────────────── // ── App entry ────────────────────────────────────────────────────────
#[cfg_attr(mobile, tauri::mobile_entry_point)] #[cfg_attr(mobile, tauri::mobile_entry_point)]
@ -517,6 +532,7 @@ pub fn run() {
let s = state.lock().unwrap(); let s = state.lock().unwrap();
s.config.get_current_workspace().ok().map(|(_, ws)| ws.path.clone()) s.config.get_current_workspace().ok().map(|(_, ws)| ws.path.clone())
}; };
#[cfg(not(target_os = "android"))]
if let Some(path) = workspace_path { if let Some(path) = workspace_path {
start_watcher(handle, path); start_watcher(handle, path);
} }

View file

@ -6,6 +6,10 @@ description = "Core library for local-first task management with markdown storag
license = "GPL-3.0-or-later" license = "GPL-3.0-or-later"
repository = "https://github.com/SteelDynamite/onyx" repository = "https://github.com/SteelDynamite/onyx"
[features]
default = ["keyring-storage"]
keyring-storage = ["keyring"]
[dependencies] [dependencies]
serde = { workspace = true } serde = { workspace = true }
serde_json = "1.0" serde_json = "1.0"
@ -17,7 +21,7 @@ reqwest = { workspace = true }
sha2 = { workspace = true } sha2 = { workspace = true }
quick-xml = { workspace = true } quick-xml = { workspace = true }
tokio = { workspace = true } tokio = { workspace = true }
keyring = { version = "3", features = ["apple-native", "windows-native", "sync-secret-service"] } keyring = { version = "3", features = ["apple-native", "windows-native", "sync-secret-service"], optional = true }
[dev-dependencies] [dev-dependencies]
tempfile = "3.0" tempfile = "3.0"

View file

@ -379,6 +379,7 @@ fn extract_relative_path(href: &str, base_url: &str, request_path: &str) -> Stri
// --- Credential Storage --- // --- Credential Storage ---
#[cfg(feature = "keyring-storage")]
/// Store WebDAV credentials in the platform keychain. /// Store WebDAV credentials in the platform keychain.
pub fn store_credentials(domain: &str, username: &str, password: &str) -> Result<()> { pub fn store_credentials(domain: &str, username: &str, password: &str) -> Result<()> {
let service = format!("com.onyx.webdav.{}", domain); let service = format!("com.onyx.webdav.{}", domain);
@ -396,6 +397,13 @@ pub fn store_credentials(domain: &str, username: &str, password: &str) -> Result
Ok(()) Ok(())
} }
#[cfg(not(feature = "keyring-storage"))]
/// Store WebDAV credentials (not available without keyring-storage feature).
pub fn store_credentials(_domain: &str, _username: &str, _password: &str) -> Result<()> {
Err(Error::Credential("Credential storage not available on this platform".into()))
}
#[cfg(feature = "keyring-storage")]
/// Load WebDAV credentials from the platform keychain, falling back to env vars. /// Load WebDAV credentials from the platform keychain, falling back to env vars.
pub fn load_credentials(domain: &str) -> Result<(String, String)> { pub fn load_credentials(domain: &str) -> Result<(String, String)> {
let service = format!("com.onyx.webdav.{}", domain); let service = format!("com.onyx.webdav.{}", domain);
@ -423,6 +431,23 @@ pub fn load_credentials(domain: &str) -> Result<(String, String)> {
))) )))
} }
#[cfg(not(feature = "keyring-storage"))]
/// Load WebDAV credentials from env vars only (keyring not available).
pub fn load_credentials(domain: &str) -> Result<(String, String)> {
if let (Ok(user), Ok(pass)) = (
std::env::var("ONYX_WEBDAV_USER"),
std::env::var("ONYX_WEBDAV_PASS"),
) {
return Ok((user, pass));
}
Err(Error::Credential(format!(
"No credentials found for '{}'. Set ONYX_WEBDAV_USER and ONYX_WEBDAV_PASS.",
domain
)))
}
#[cfg(feature = "keyring-storage")]
/// Delete WebDAV credentials from the platform keychain. /// Delete WebDAV credentials from the platform keychain.
pub fn delete_credentials(domain: &str) -> Result<()> { pub fn delete_credentials(domain: &str) -> Result<()> {
let service = format!("com.onyx.webdav.{}", domain); let service = format!("com.onyx.webdav.{}", domain);
@ -437,6 +462,12 @@ pub fn delete_credentials(domain: &str) -> Result<()> {
Ok(()) Ok(())
} }
#[cfg(not(feature = "keyring-storage"))]
/// Delete WebDAV credentials (no-op without keyring-storage feature).
pub fn delete_credentials(_domain: &str) -> Result<()> {
Ok(())
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;