From bdeb2e12d21d60ca23398889e19824846942e150 Mon Sep 17 00:00:00 2001 From: Jordon Date: Mon, 23 Mar 2026 18:43:08 +0000 Subject: [PATCH 1/4] Fix plugin issues --- Backend/Cargo.toml | 2 +- Backend/src/lib.rs | 41 +------ Backend/src/plugin_bundles.rs | 110 ++++++++++++------- Backend/src/plugin_paths.rs | 144 ++++++++++++++++++------ Backend/src/plugin_vcs_backends.rs | 171 +---------------------------- Backend/src/plugins.rs | 32 +++--- Cargo.lock | 22 +--- docs/plugin architecture.md | 11 +- docs/plugins.md | 20 +++- 9 files changed, 232 insertions(+), 321 deletions(-) diff --git a/Backend/Cargo.toml b/Backend/Cargo.toml index 91d444a..fa36082 100644 --- a/Backend/Cargo.toml +++ b/Backend/Cargo.toml @@ -43,7 +43,7 @@ serde_json = "1.0" time = { version = "0.3", features = ["local-offset"] } zip = "7.2" tar = "0.4" -xz2 = "0.1" +flate2 = "1" shlex = "1.2" sha2 = "0.10" hex = "0.4" diff --git a/Backend/src/lib.rs b/Backend/src/lib.rs index 664dcd4..5be2df1 100644 --- a/Backend/src/lib.rs +++ b/Backend/src/lib.rs @@ -6,7 +6,6 @@ //! plugin discovery, and startup behavior. use log::warn; -use std::path::PathBuf; use std::sync::Arc; use tauri::path::BaseDirectory; use tauri::WindowEvent; @@ -107,36 +106,6 @@ fn try_reopen_last_repo(app_handle: &tauri::AppHandle) { } } -/// Resolves a development fallback path for the bundled Node runtime. -/// -/// In `cargo tauri dev`, the generated runtime is placed under -/// `target/openvcs/node-runtime`, while Tauri resource resolution can point at -/// `target/debug/node-runtime`. This helper probes the generated location. -/// -/// # Returns -/// - `Some(PathBuf)` when the dev bundled node binary exists. -/// - `None` when the path cannot be derived or does not exist. -fn resolve_dev_bundled_node_fallback() -> Option { - let exe = std::env::current_exe().ok()?; - let exe_dir = exe.parent()?; - let target_dir = exe_dir.parent()?; - let node_name = if cfg!(windows) { "node.exe" } else { "node" }; - let candidate = target_dir - .join("openvcs") - .join("node-runtime") - .join(node_name); - if candidate.is_file() { - return Some(candidate); - } - let nested = exe_dir - .join("_up_") - .join("target") - .join("openvcs") - .join("node-runtime") - .join(node_name); - nested.is_file().then_some(nested) -} - /// Starts the OpenVCS backend runtime and Tauri application. /// /// This configures logging, plugin bundle synchronization, startup restore @@ -187,17 +156,13 @@ pub fn run() { ); } } - let node_name = if cfg!(windows) { "node.exe" } else { "node" }; - let mut node_candidates: Vec = Vec::new(); if let Ok(node_runtime_dir) = app.path().resolve("node-runtime", BaseDirectory::Resource) { - node_candidates.push(node_runtime_dir.join(node_name)); - } - if let Some(dev_fallback) = resolve_dev_bundled_node_fallback() { - if !node_candidates.iter().any(|p| p == &dev_fallback) { - node_candidates.push(dev_fallback); + if let Some(parent) = node_runtime_dir.parent() { + crate::plugin_paths::set_resource_dir(parent.to_path_buf()); } } + let node_candidates = crate::plugin_paths::bundled_node_candidate_paths(); if let Some(bundled_node) = node_candidates.iter().find(|path| path.is_file()) { crate::plugin_paths::set_node_executable_path(bundled_node.to_path_buf()); diff --git a/Backend/src/plugin_bundles.rs b/Backend/src/plugin_bundles.rs index 4041226..b9dfd83 100644 --- a/Backend/src/plugin_bundles.rs +++ b/Backend/src/plugin_bundles.rs @@ -4,17 +4,60 @@ use crate::logging::LogTimer; use crate::plugin_paths::{built_in_plugin_dirs, ensure_dir, plugins_dir, PLUGIN_MANIFEST_NAME}; +use flate2::read::GzDecoder; use log::{debug, error, info, trace, warn}; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; use std::collections::{BTreeMap, HashSet}; use std::fs; -use std::io::{Read, Write}; +use std::io::{Read, Seek, SeekFrom, Write}; use std::path::{Component, Path, PathBuf}; use std::sync::OnceLock; -use xz2::read::XzDecoder; const MODULE: &str = "plugin_bundles"; +const GZIP_MAGIC: [u8; 2] = [0x1F, 0x8B]; + +/// Opens a plugin bundle as a decompressed tar reader. +/// +/// `.ovcsp` bundles are gzip-compressed tar archives. +/// +/// # Parameters +/// - `bundle_path`: Bundle file path. +/// +/// # Returns +/// - `Ok(Box)` with a decompressed tar stream. +/// - `Err(String)` when the bundle cannot be opened or has an unknown format. +fn open_bundle_reader(bundle_path: &Path) -> Result, String> { + let mut file = + fs::File::open(bundle_path).map_err(|e| format!("open {}: {e}", bundle_path.display()))?; + let mut magic = [0u8; 6]; + let read = file + .read(&mut magic) + .map_err(|e| format!("read {}: {e}", bundle_path.display()))?; + file.seek(SeekFrom::Start(0)) + .map_err(|e| format!("seek {}: {e}", bundle_path.display()))?; + + if read >= GZIP_MAGIC.len() && magic[..GZIP_MAGIC.len()] == GZIP_MAGIC { + return Ok(Box::new(GzDecoder::new(file))); + } + Err(format!( + "unsupported bundle compression in {}", + bundle_path.display() + )) +} + +/// Opens a plugin bundle as a tar archive. +/// +/// # Parameters +/// - `bundle_path`: Bundle file path. +/// +/// # Returns +/// - `Ok(tar::Archive<...>)` when the bundle format is supported. +/// - `Err(String)` when the bundle cannot be decoded. +fn open_bundle_archive(bundle_path: &Path) -> Result>, String> { + let reader = open_bundle_reader(bundle_path)?; + Ok(tar::Archive::new(reader)) +} /// Safety and resource limits enforced during bundle extraction. #[derive(Debug, Clone, Copy)] @@ -326,7 +369,7 @@ impl PluginBundleStore { &bundle_sha256[..12] ); - let (manifest_bundle_path, manifest) = locate_manifest_tar_xz(bundle_path)?; + let (manifest_bundle_path, manifest) = locate_manifest_in_bundle(bundle_path)?; let plugin_id = manifest.id.trim().to_string(); if plugin_id.is_empty() { error!("install_ovcsp_with_limits: manifest id is empty",); @@ -385,10 +428,7 @@ impl PluginBundleStore { .map_err(|e| format!("canonicalize {}: {e}", staging_version_dir.display()))?; // Extract all entries under `/...` into the staging version directory. - let f = fs::File::open(bundle_path) - .map_err(|e| format!("open {}: {e}", bundle_path.display()))?; - let decoder = XzDecoder::new(f); - let mut tar = tar::Archive::new(decoder); + let mut tar = open_bundle_archive(bundle_path)?; for entry in tar.entries().map_err(|e| format!("read tar: {e}"))? { let mut entry = entry.map_err(|e| format!("tar entry: {e}"))?; @@ -696,7 +736,7 @@ impl PluginBundleStore { trace!("ensure_built_in_bundle: {}", bundle_path.display()); let bundle_sha256 = sha256_hex_file(bundle_path)?; - let (_manifest_path, manifest) = locate_manifest_tar_xz(bundle_path)?; + let (_manifest_path, manifest) = locate_manifest_in_bundle(bundle_path)?; let plugin_id = manifest.id.trim(); if plugin_id.is_empty() { error!("ensure_built_in_bundle: bundle manifest id is empty",); @@ -1171,7 +1211,7 @@ fn read_built_in_plugin_ids() -> HashSet { let mut out: HashSet = HashSet::new(); for bundle_path in builtin_bundle_paths() { - let (_manifest_path, manifest) = match locate_manifest_tar_xz(&bundle_path) { + let (_manifest_path, manifest) = match locate_manifest_in_bundle(&bundle_path) { Ok(v) => v, Err(err) => { warn!( @@ -1240,7 +1280,7 @@ fn derive_install_version(manifest: &PluginManifest, bundle_sha256: &str) -> Str .unwrap_or_else(|| format!("sha256-{}", &bundle_sha256[..12])) } -/// Finds and parses `openvcs.plugin.json` from a tar.xz bundle. +/// Finds and parses `openvcs.plugin.json` from a plugin bundle archive. /// /// # Parameters /// - `bundle_path`: Bundle file path. @@ -1248,11 +1288,8 @@ fn derive_install_version(manifest: &PluginManifest, bundle_sha256: &str) -> Str /// # Returns /// - `Ok((PathBuf, PluginManifest))` manifest path inside archive and manifest payload. /// - `Err(String)` on read/parse/validation failure. -fn locate_manifest_tar_xz(bundle_path: &Path) -> Result<(PathBuf, PluginManifest), String> { - let file = - fs::File::open(bundle_path).map_err(|e| format!("open {}: {e}", bundle_path.display()))?; - let decoder = XzDecoder::new(file); - let mut tar = tar::Archive::new(decoder); +fn locate_manifest_in_bundle(bundle_path: &Path) -> Result<(PathBuf, PluginManifest), String> { + let mut tar = open_bundle_archive(bundle_path)?; let mut manifest_path: Option = None; let mut manifest_json: Option> = None; @@ -1382,9 +1419,9 @@ fn validate_entrypoint(version_dir: &Path, exec: Option<&str>, label: &str) -> R #[cfg(test)] mod tests { use super::*; - use std::io::Cursor; + use flate2::write::GzEncoder; + use flate2::Compression; use tempfile::tempdir; - use xz2::write::XzEncoder; /// Synthetic tar entry kind used by bundle-construction helpers. enum TarEntryKind { @@ -1406,16 +1443,15 @@ mod tests { kind: TarEntryKind, } - /// Builds a tar.xz bundle from synthetic test entries. + /// Builds a tar.gz bundle from synthetic test entries. /// /// # Parameters /// - `entries`: Tar entries to include. /// /// # Returns - /// - Encoded tar.xz bytes. - fn make_tar_xz_bundle(entries: Vec) -> Vec { - let cursor = Cursor::new(Vec::::new()); - let encoder = XzEncoder::new(cursor, 6); + /// - Encoded tar.gz bytes. + fn make_tar_gz_bundle(entries: Vec) -> Vec { + let encoder = GzEncoder::new(Vec::::new(), Compression::default()); let mut tar = tar::Builder::new(encoder); for e in entries { @@ -1446,17 +1482,17 @@ mod tests { } let encoder = tar.into_inner().unwrap(); - encoder.finish().unwrap().into_inner() + encoder.finish().unwrap() } - /// Builds a minimal raw tar.xz bundle from `(path, bytes)` tuples. + /// Builds a minimal raw tar.gz bundle from `(path, bytes)` tuples. /// /// # Parameters /// - `entries`: Raw path/data entries. /// /// # Returns - /// - Encoded tar.xz bytes. - fn make_raw_tar_xz_bundle(entries: Vec<(String, Vec)>) -> Vec { + /// - Encoded tar.gz bytes. + fn make_raw_tar_gz_bundle(entries: Vec<(String, Vec)>) -> Vec { /// Writes an octal tar header field. /// /// # Parameters @@ -1524,7 +1560,7 @@ mod tests { tar_bytes.resize(tar_bytes.len() + 1024, 0u8); let mut out = Vec::::new(); - let mut enc = XzEncoder::new(&mut out, 6); + let mut enc = GzEncoder::new(&mut out, Compression::default()); enc.write_all(&tar_bytes).unwrap(); enc.finish().unwrap(); out @@ -1576,7 +1612,7 @@ mod tests { kind: TarEntryKind::File, }); } - let bundle = make_tar_xz_bundle(entries); + let bundle = make_tar_gz_bundle(entries); let (_tmp, bundle_path) = write_bundle_to_temp(&bundle); let store_root = tempdir().unwrap(); @@ -1598,7 +1634,7 @@ mod tests { /// # Returns /// - `()`. fn install_requires_manifest_at_expected_location() { - let bundle = make_tar_xz_bundle(vec![TarEntry { + let bundle = make_tar_gz_bundle(vec![TarEntry { name: "test.plugin/other.json".into(), data: b"{}".to_vec(), unix_mode: None, @@ -1619,7 +1655,7 @@ mod tests { /// # Returns /// - `()`. fn install_validates_declared_entrypoints_exist() { - let bundle = make_tar_xz_bundle(vec![TarEntry { + let bundle = make_tar_gz_bundle(vec![TarEntry { name: "test.plugin/openvcs.plugin.json".into(), data: basic_manifest( "test.plugin", @@ -1643,7 +1679,7 @@ mod tests { /// # Returns /// - `()`. fn install_rejects_functions_component() { - let bundle = make_tar_xz_bundle(vec![TarEntry { + let bundle = make_tar_gz_bundle(vec![TarEntry { name: "test.plugin/openvcs.plugin.json".into(), data: basic_manifest("test.plugin", ",\"functions\":{\"exec\":\"legacy.mjs\"}"), unix_mode: None, @@ -1662,12 +1698,12 @@ mod tests { } #[test] - /// Verifies installer accepts valid tar.xz bundles. + /// Verifies installer accepts valid tar.gz bundles. /// /// # Returns /// - `()`. - fn install_accepts_tar_xz_bundles() { - let bundle = make_tar_xz_bundle(vec![ + fn install_accepts_tar_gz_bundles() { + let bundle = make_tar_gz_bundle(vec![ TarEntry { name: "test.plugin/openvcs.plugin.json".into(), data: basic_manifest( @@ -1700,7 +1736,7 @@ mod tests { /// # Returns /// - `()`. fn install_rejects_tar_zipslip_parent_dir() { - let bundle = make_raw_tar_xz_bundle(vec![ + let bundle = make_raw_tar_gz_bundle(vec![ ( "test.plugin/openvcs.plugin.json".into(), basic_manifest("test.plugin", ""), @@ -1722,7 +1758,7 @@ mod tests { /// # Returns /// - `()`. fn install_rejects_tar_symlink_entries() { - let bundle = make_tar_xz_bundle(vec![ + let bundle = make_tar_gz_bundle(vec![ TarEntry { name: "test.plugin/openvcs.plugin.json".into(), data: basic_manifest("test.plugin", ""), @@ -1754,7 +1790,7 @@ mod tests { /// - `()`. fn install_rejects_tar_suspicious_compression_ratio() { let big = vec![0u8; 2 * 1024 * 1024]; - let bundle = make_tar_xz_bundle(vec![ + let bundle = make_tar_gz_bundle(vec![ TarEntry { name: "test.plugin/openvcs.plugin.json".into(), data: basic_manifest("test.plugin", ""), diff --git a/Backend/src/plugin_paths.rs b/Backend/src/plugin_paths.rs index 70593cf..09ea9b2 100644 --- a/Backend/src/plugin_paths.rs +++ b/Backend/src/plugin_paths.rs @@ -17,6 +17,8 @@ use std::{ pub const PLUGIN_MANIFEST_NAME: &str = "openvcs.plugin.json"; /// Directory name used for built-in plugin bundles. pub const BUILT_IN_PLUGINS_DIR_NAME: &str = "built-in-plugins"; +/// Directory name used for the bundled Node runtime. +pub const NODE_RUNTIME_DIR_NAME: &str = "node-runtime"; // If the Tauri runtime resolves a resource directory at startup, we store // it here so plugin discovery can include resources embedded in the @@ -50,35 +52,105 @@ pub fn ensure_dir(path: &Path) { } } -/// Returns discovered built-in plugin directories that currently exist. +/// Appends a path when it has not already been recorded. +/// +/// # Parameters +/// - `paths`: Candidate path list. +/// - `path`: Candidate to append. /// /// # Returns -/// - Existing filesystem directories searched for built-in plugins. -pub fn built_in_plugin_dirs() -> Vec { +/// - `()`. +fn push_unique_path(paths: &mut Vec, path: PathBuf) { + if !paths.iter().any(|existing| existing == &path) { + paths.push(path); + } +} + +/// Adds Linux package resource roots installed under sibling `lib` directories. +/// +/// Tauri Linux packages commonly install the executable in `.../bin/` and the +/// mapped resources in `.../lib//`. This helper discovers those app +/// directories only when they already contain the requested resource directory. +/// +/// # Parameters +/// - `paths`: Candidate base directory list. +/// - `exe_dir`: Directory containing the executable. +/// - `resource_dir_name`: Resource subdirectory to check for. +/// +/// # Returns +/// - `()`. +#[cfg(target_os = "linux")] +fn push_linux_package_resource_bases( + paths: &mut Vec, + exe_dir: &Path, + resource_dir_name: &str, +) { + let Some(prefix_dir) = exe_dir.parent() else { + return; + }; + + for lib_dir_name in ["lib", "lib64"] { + let lib_dir = prefix_dir.join(lib_dir_name); + let entries = match std::fs::read_dir(&lib_dir) { + Ok(entries) => entries, + Err(_) => continue, + }; + for entry in entries.flatten() { + let app_dir = entry.path(); + if !app_dir.is_dir() { + continue; + } + if app_dir.join(resource_dir_name).is_dir() { + push_unique_path(paths, app_dir); + } + } + } +} + +/// Returns candidate resource base directories derived from the executable path. +/// +/// # Parameters +/// - `resource_dir_name`: Resource directory that should exist under packaged roots. +/// +/// # Returns +/// - Candidate base directories that may contain the requested resource. +fn bundled_resource_base_dirs(resource_dir_name: &str) -> Vec { let mut candidates: Vec = Vec::new(); if let Ok(exe) = env::current_exe() { if let Some(dir) = exe.parent() { - // Some installers place resources next to the executable, either - // in a `resources` subdirectory or directly alongside the exe. - candidates.push(dir.join("resources").join(BUILT_IN_PLUGINS_DIR_NAME)); - candidates.push(dir.join(BUILT_IN_PLUGINS_DIR_NAME)); - // Dev builds can generate built-in bundles under `target/openvcs/`. + push_unique_path(&mut candidates, dir.join("resources")); + push_unique_path(&mut candidates, dir.to_path_buf()); if let Some(target_dir) = dir.parent() { - candidates.push(target_dir.join("openvcs").join(BUILT_IN_PLUGINS_DIR_NAME)); - } - #[cfg(target_os = "macos")] - if let Some(parent) = dir.parent() { - candidates.push(parent.join("Resources").join(BUILT_IN_PLUGINS_DIR_NAME)); + push_unique_path(&mut candidates, target_dir.join("openvcs")); + #[cfg(target_os = "macos")] + push_unique_path(&mut candidates, target_dir.join("Resources")); } + #[cfg(target_os = "linux")] + push_linux_package_resource_bases(&mut candidates, dir, resource_dir_name); + push_unique_path( + &mut candidates, + dir.join("_up_").join("target").join("openvcs"), + ); } } - // If the Tauri runtime resolved a resource directory at startup, include - // its built-in-plugins subdirectory as a candidate. This is set by the - // application during `tauri::Builder::setup` via `set_resource_dir`. - if let Some(rp) = RESOURCE_DIR.get() { - candidates.push(rp.join(BUILT_IN_PLUGINS_DIR_NAME)); + if let Some(resource_dir) = RESOURCE_DIR.get() { + push_unique_path(&mut candidates, resource_dir.clone()); + } + + candidates +} + +/// Returns discovered built-in plugin directories that currently exist. +/// +/// # Returns +/// - Existing filesystem directories searched for built-in plugins. +pub fn built_in_plugin_dirs() -> Vec { + let mut candidates: Vec = Vec::new(); + + for base_dir in bundled_resource_base_dirs(BUILT_IN_PLUGINS_DIR_NAME) { + push_unique_path(&mut candidates, base_dir.join(BUILT_IN_PLUGINS_DIR_NAME)); } // On Windows installers the per-user AppData Local folder is commonly @@ -87,28 +159,14 @@ pub fn built_in_plugin_dirs() -> Vec { // plugins shipped by the installer are discovered. #[cfg(target_os = "windows")] if let Ok(local_appdata) = env::var("LOCALAPPDATA") { - candidates.push( + push_unique_path( + &mut candidates, PathBuf::from(local_appdata) .join("OpenVCS") .join(BUILT_IN_PLUGINS_DIR_NAME), ); } - // Some Tauri bundle targets (AppImage, RPM, DEB) preserve the source - // build-tree path under an `_up_` symlink. Include that nested location - // as a fallback so packages built before the resource-mapping fix are - // still functional. - if let Ok(exe) = env::current_exe() { - if let Some(dir) = exe.parent() { - candidates.push( - dir.join("_up_") - .join("target") - .join("openvcs") - .join(BUILT_IN_PLUGINS_DIR_NAME), - ); - } - } - let mut seen = std::collections::HashSet::new(); let result: Vec = candidates .into_iter() @@ -143,6 +201,22 @@ pub fn built_in_plugin_dirs() -> Vec { result } +/// Returns candidate bundled Node executable paths. +/// +/// # Returns +/// - Ordered candidate paths for the bundled Node binary. +pub fn bundled_node_candidate_paths() -> Vec { + let node_name = if cfg!(windows) { "node.exe" } else { "node" }; + let mut candidates = Vec::new(); + for base_dir in bundled_resource_base_dirs(NODE_RUNTIME_DIR_NAME) { + push_unique_path( + &mut candidates, + base_dir.join(NODE_RUNTIME_DIR_NAME).join(node_name), + ); + } + candidates +} + /// Set the resolved Tauri resource directory so the plugin discovery can /// include resources embedded inside the application bundle. Call this from /// the Tauri `setup` callback with `app.path().resolve("built-in-plugins", BaseDirectory::Resource)`. @@ -172,7 +246,7 @@ pub fn set_node_executable_path(path: PathBuf) { /// /// # Returns /// - `Some(PathBuf)` when a bundled runtime was resolved. -/// - `None` when host should fall back to `node` on PATH. +/// - `None` when no bundled runtime has been resolved yet. pub fn node_executable_path() -> Option { NODE_EXECUTABLE.get().cloned() } diff --git a/Backend/src/plugin_vcs_backends.rs b/Backend/src/plugin_vcs_backends.rs index 2713b4d..a0c7d9d 100644 --- a/Backend/src/plugin_vcs_backends.rs +++ b/Backend/src/plugin_vcs_backends.rs @@ -4,20 +4,14 @@ use crate::core::{BackendId, Result as VcsResult, Vcs, VcsError}; use crate::logging::LogTimer; -use crate::plugin_bundles::{PluginBundleStore, PluginManifest, VcsBackendProvide}; -use crate::plugin_paths::{built_in_plugin_dirs, PLUGIN_MANIFEST_NAME}; +use crate::plugin_bundles::PluginBundleStore; use crate::plugin_runtime::instance::PluginRuntimeInstance; use crate::plugin_runtime::runtime_select::create_node_runtime_instance; use crate::plugin_runtime::settings_store; use crate::plugin_runtime::{vcs_proxy::PluginVcsProxy, PluginRuntimeManager}; use crate::settings::AppConfig; use log::{debug, error, info, trace, warn}; -use std::{ - collections::BTreeMap, - fs, - path::{Path, PathBuf}, - sync::Arc, -}; +use std::{collections::BTreeMap, path::Path, sync::Arc}; const MODULE: &str = "plugin_vcs_backends"; @@ -60,84 +54,11 @@ pub struct PluginBackendDescriptor { pub plugin_name: Option, } -/// Reads a plugin manifest from a plugin directory. +/// Lists VCS backends currently available from installed plugins. /// -/// # Parameters -/// - `plugin_dir`: Plugin directory path. -/// -/// # Returns -/// - `Some(PluginManifest)` on success. -/// - `None` on read/parse failure. -fn load_manifest_from_dir(plugin_dir: &Path) -> Option { - let manifest_path = plugin_dir.join(PLUGIN_MANIFEST_NAME); - trace!( - "load_manifest_from_dir: loading from {}", - manifest_path.display() - ); - - let text = fs::read_to_string(&manifest_path).ok()?; - let manifest: PluginManifest = serde_json::from_str(&text).ok()?; - - debug!( - "load_manifest_from_dir: loaded manifest for plugin '{}'", - manifest.id - ); - Some(manifest) -} - -/// Lists manifests from built-in plugin directories. -/// -/// # Returns -/// - Directory/manifest pairs for readable built-in plugins. -fn builtin_plugin_manifests() -> Vec<(PathBuf, PluginManifest)> { - let _timer = LogTimer::new(MODULE, "builtin_plugin_manifests"); - trace!("builtin_plugin_manifests: scanning built-in plugin dirs",); - - let mut out = Vec::new(); - let dirs = built_in_plugin_dirs(); - debug!( - "builtin_plugin_manifests: found {} built-in plugin directories", - dirs.len() - ); - - for root in dirs { - if !root.is_dir() { - trace!( - "builtin_plugin_manifests: {} is not a directory", - root.display() - ); - continue; - } - let entries = match fs::read_dir(&root) { - Ok(entries) => entries, - Err(e) => { - warn!( - "builtin_plugin_manifests: failed to read {}: {}", - root.display(), - e - ); - continue; - } - }; - for entry in entries.flatten() { - let plugin_dir = entry.path(); - if !plugin_dir.is_dir() { - continue; - } - if let Some(manifest) = load_manifest_from_dir(&plugin_dir) { - out.push((plugin_dir, manifest)); - } - } - } - - debug!( - "builtin_plugin_manifests: found {} built-in manifests", - out.len() - ); - out -} - -/// Lists VCS backends currently available from installed and built-in plugins. +/// Built-in plugin bundles are synchronized into the installed plugin store at +/// startup, so backend discovery must use installed component metadata instead +/// of treating bundled `.ovcsp` archives as unpacked plugin directories. /// /// # Returns /// - `Ok(Vec)` containing discovered backend descriptors. @@ -210,86 +131,6 @@ pub fn list_plugin_vcs_backends() -> Result, String } } - for (plugin_dir, manifest) in builtin_plugin_manifests() { - let plugin_id = manifest.id.trim(); - if plugin_id.is_empty() { - warn!( - "list_plugin_vcs_backends: manifest has empty id at {}", - plugin_dir.display() - ); - continue; - } - if !is_plugin_enabled_in_settings(plugin_id, manifest.default_enabled) { - trace!( - "list_plugin_vcs_backends: built-in plugin {} is disabled", - plugin_id - ); - continue; - } - let Some(module) = &manifest.module else { - trace!( - "list_plugin_vcs_backends: built-in plugin {} has no module", - plugin_id - ); - continue; - }; - let Some(exec_name) = module - .exec - .as_deref() - .map(str::trim) - .filter(|s| !s.is_empty()) - else { - trace!( - "list_plugin_vcs_backends: built-in plugin {} has no exec", - plugin_id - ); - continue; - }; - let exec_path = plugin_dir.join("bin").join(exec_name); - if !exec_path.is_file() { - warn!( - "list_plugin_vcs_backends: built-in plugin {} is missing module exec {}", - plugin_id, - exec_path.display() - ); - continue; - } - - debug!( - "list_plugin_vcs_backends: processing built-in plugin {} at {}", - plugin_id, - plugin_dir.display() - ); - - let plugin_name = manifest.name.clone(); - for provide in &module.vcs_backends { - let (id, label) = match provide { - VcsBackendProvide::Id(id) => (id.clone(), None), - VcsBackendProvide::Named { id, name } => (id.clone(), name.clone()), - }; - let backend_id = BackendId::from(id.as_str()); - let key = backend_id.as_ref().to_string(); - if map.contains_key(&key) { - trace!( - "list_plugin_vcs_backends: backend {} already registered", - backend_id - ); - continue; - } - debug!( - "list_plugin_vcs_backends: registering built-in backend '{}' from plugin '{}'", - backend_id, plugin_id - ); - let candidate = PluginBackendDescriptor { - backend_id: backend_id.clone(), - backend_name: label, - plugin_id: plugin_id.to_string(), - plugin_name: plugin_name.clone(), - }; - map.insert(key, candidate); - } - } - let result: Vec<_> = map.into_values().collect(); info!( "list_plugin_vcs_backends: discovered {} VCS backends", diff --git a/Backend/src/plugins.rs b/Backend/src/plugins.rs index eab6143..06c27c8 100644 --- a/Backend/src/plugins.rs +++ b/Backend/src/plugins.rs @@ -1,7 +1,7 @@ // Copyright © 2025-2026 OpenVCS Contributors // SPDX-License-Identifier: GPL-3.0-or-later use crate::plugin_bundles::PluginBundleStore; -use crate::plugin_paths::{built_in_plugin_dirs, ensure_dir, plugins_dir, PLUGIN_MANIFEST_NAME}; +use crate::plugin_paths::{ensure_dir, plugins_dir, PLUGIN_MANIFEST_NAME}; use log::{debug, warn}; use notify::{Config, Event, RecommendedWatcher, RecursiveMode, Watcher}; use serde::{Deserialize, Serialize}; @@ -217,7 +217,7 @@ impl PluginCache { let mut summaries: Vec = Vec::new(); let mut entries: HashMap = HashMap::new(); - for (root, origin) in plugin_roots() { + for root in installed_plugin_roots() { match fs::read_dir(&root) { Ok(iter) => { for entry in iter.flatten() { @@ -243,7 +243,7 @@ impl PluginCache { let effective_origin = if is_built_in { PluginOrigin::BuiltIn } else { - origin + PluginOrigin::User }; let summary = manifest_to_summary(&resolved, manifest.clone(), effective_origin); @@ -286,7 +286,7 @@ impl PluginCache { } }; - for (root, _) in plugin_roots() { + for root in installed_plugin_roots() { if let Err(err) = watcher.watch(&root, RecursiveMode::Recursive) { warn!("plugins: failed to watch {}: {}", root.display(), err); } @@ -303,25 +303,23 @@ fn plugin_cache() -> &'static Arc { PLUGIN_CACHE.get_or_init(PluginCache::initialize) } -/// Resolves plugin root directories (user + built-in). +/// Resolves installed plugin root directories. +/// +/// Built-in bundles are synchronized into the writable plugin store during +/// startup, so plugin listing and theme discovery should read only from the +/// installed plugin roots instead of treating bundled `.ovcsp` archives as +/// unpacked plugin directories. /// /// # Returns -/// - Unique list of plugin root paths with origin metadata. -fn plugin_roots() -> Vec<(PathBuf, PluginOrigin)> { - let mut roots: Vec<(PathBuf, PluginOrigin)> = Vec::new(); +/// - Unique list of installed plugin root paths. +fn installed_plugin_roots() -> Vec { + let mut roots: Vec = Vec::new(); let mut seen = HashSet::new(); let dir = plugins_dir(); ensure_dir(&dir); if seen.insert(dir.clone()) { - roots.push((dir, PluginOrigin::User)); - } - - for path in built_in_plugin_dirs() { - if !seen.insert(path.clone()) { - continue; - } - roots.push((path, PluginOrigin::BuiltIn)); + roots.push(dir); } roots @@ -716,7 +714,7 @@ pub fn plugin_theme_dirs() -> Vec { let mut out = Vec::new(); let mut seen = HashSet::new(); - for (root, _) in plugin_roots() { + for root in installed_plugin_roots() { let entries = match fs::read_dir(&root) { Ok(entries) => entries, Err(err) => { diff --git a/Cargo.lock b/Cargo.lock index 921ae1c..22e5d16 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2304,17 +2304,6 @@ dependencies = [ "sha2", ] -[[package]] -name = "lzma-sys" -version = "0.1.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fda04ab3764e6cde78b9974eec4f779acaba7c4e84b36eca3cf77c581b85d27" -dependencies = [ - "cc", - "libc", - "pkg-config", -] - [[package]] name = "mac" version = "0.1.1" @@ -2725,6 +2714,7 @@ dependencies = [ "directories", "dirs", "env_logger", + "flate2", "hex", "log", "notify", @@ -2746,7 +2736,6 @@ dependencies = [ "time", "tokio", "toml 1.0.7+spec-1.1.0", - "xz2", "zip 7.2.0", ] @@ -6132,15 +6121,6 @@ dependencies = [ "rustix", ] -[[package]] -name = "xz2" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "388c44dc09d76f1536602ead6d325eb532f5c122f17782bd57fb47baeeb767e2" -dependencies = [ - "lzma-sys", -] - [[package]] name = "yoke" version = "0.8.1" diff --git a/docs/plugin architecture.md b/docs/plugin architecture.md index 3c5124a..20e21bf 100644 --- a/docs/plugin architecture.md +++ b/docs/plugin architecture.md @@ -43,7 +43,8 @@ Core plugin->host notifications: - `host.status_set` - `host.event_emit` - `vcs.event` -- Plugin runtime requires the app-bundled Node binary (`node-runtime/node` or `node.exe`); no system `node` fallback. In dev runs, the backend also probes the generated bundled path under `target/openvcs/node-runtime/`. +- Plugin runtime requires the app-bundled Node binary (`node-runtime/node` or `node.exe`); no system `node` fallback. +- The backend resolves bundled Node from the Tauri `node-runtime` resource first, then probes packaged filesystem layouts including executable-adjacent `node-runtime/` and Linux `lib//node-runtime/`, and finally the generated dev path under `target/openvcs/node-runtime/`. ## Plugin types @@ -58,7 +59,7 @@ Core plugin->host notifications: ## Bundle format (`.ovcsp`) -Plugins are installed from `.ovcsp` tar.xz archives. Layout: +Plugins are installed from `.ovcsp` tar.gz archives. Layout: ```text / @@ -71,6 +72,12 @@ Plugins are installed from `.ovcsp` tar.xz archives. Layout: node_modules/ (optional; pre-bundled npm dependencies) ``` +Built-in plugins ship in the bundled `built-in-plugins/` resource directory as +`.ovcsp` archives too. On startup, the backend synchronizes those built-in +bundles into the writable installed plugin store before plugin, theme, and VCS +backend discovery runs. Linux package targets may place those resources under +`lib//built-in-plugins/` instead of next to the executable. + ## Manifest (`openvcs.plugin.json`) The host currently consumes: diff --git a/docs/plugins.md b/docs/plugins.md index f3a7d92..7ea6275 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -6,14 +6,17 @@ Plugins may include themes, a Node.js module, or both. ## Where plugins live -OpenVCS discovers plugins from two places: +OpenVCS discovers installed plugins from the user plugins directory in the +OpenVCS config dir: `plugins/`. -- User plugins directory (in the OpenVCS config dir): `plugins/` -- Built-in plugins directory (bundled with the app): `built-in-plugins/` +Built-in plugins ship with the app as `.ovcsp` archives in +`built-in-plugins/`. During startup, OpenVCS synchronizes those bundled archives +into the installed `plugins/` store before plugin, theme, and VCS backend +discovery runs. ## Bundle format (`.ovcsp`) -An `.ovcsp` is a tar.xz archive with this layout: +An `.ovcsp` is a tar.gz archive with this layout: ```text / @@ -77,7 +80,10 @@ Before a plugin module can start, the installed version must be marked `approved` in plugin installation metadata. Plugin modules run only with the app-bundled Node runtime; OpenVCS does not -fall back to `node` from system PATH. +fall back to `node` from system PATH. The backend resolves bundled Node from the +packaged `node-runtime/` resource, packaged filesystem layouts such as +`node-runtime/` next to the executable or Linux `lib//node-runtime/`, +or the generated dev runtime under `target/openvcs/node-runtime/`. Install only plugins you trust. @@ -109,6 +115,10 @@ npx openvcs dist --plugin-dir /path/to/plugin --out /path/to/dist `openvcs dist` runs the build step automatically unless `--no-build` is passed. +If bundled runtime code imports npm packages at execution time, declare those +packages in `dependencies` rather than `devDependencies` so `openvcs dist` +includes them in the shipped `node_modules/` tree. + The desktop client's built-in plugin bundler follows the same contract: it runs `npm install`/`npm ci` as needed and then `npm run dist` for every built-in plugin. Plugins that do not ship their own `package.json` get a transient npm From 93a362abeedf2c5c8ef4a01fd7057918c20c4434 Mon Sep 17 00:00:00 2001 From: Jordon Date: Mon, 23 Mar 2026 18:43:12 +0000 Subject: [PATCH 2/4] Update Git --- Backend/built-in-plugins/Git | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Backend/built-in-plugins/Git b/Backend/built-in-plugins/Git index 9fc13a8..5bb4357 160000 --- a/Backend/built-in-plugins/Git +++ b/Backend/built-in-plugins/Git @@ -1 +1 @@ -Subproject commit 9fc13a8743cc5a64db4f9580c9af11bc968f0161 +Subproject commit 5bb4357ef710c5b0239ccb318d973eabc8108519 From b8ab6b9aea5ad048542d854e6e427e19651c9a75 Mon Sep 17 00:00:00 2001 From: Jordon Date: Mon, 23 Mar 2026 18:56:31 +0000 Subject: [PATCH 3/4] Fixed issues --- Backend/src/lib.rs | 5 +++ Backend/src/plugin_bundles.rs | 6 ++- Backend/src/plugin_paths.rs | 61 +++++++++++++++++++++++++++--- Backend/src/plugin_vcs_backends.rs | 11 ++++++ docs/plugin architecture.md | 9 +++-- docs/plugins.md | 5 ++- 6 files changed, 85 insertions(+), 12 deletions(-) diff --git a/Backend/src/lib.rs b/Backend/src/lib.rs index 5be2df1..694f9b4 100644 --- a/Backend/src/lib.rs +++ b/Backend/src/lib.rs @@ -158,10 +158,15 @@ pub fn run() { } if let Ok(node_runtime_dir) = app.path().resolve("node-runtime", BaseDirectory::Resource) { + crate::plugin_paths::set_node_runtime_resource_dir(node_runtime_dir.clone()); if let Some(parent) = node_runtime_dir.parent() { crate::plugin_paths::set_resource_dir(parent.to_path_buf()); } } + // Keep resource lookup state populated before resolving bundled Node + // candidates. `bundled_node_candidate_paths()` uses both the generic + // RESOURCE_DIR base and the exact Tauri-resolved `node-runtime` + // directory captured above, so future refactors must preserve this order. let node_candidates = crate::plugin_paths::bundled_node_candidate_paths(); if let Some(bundled_node) = node_candidates.iter().find(|path| path.is_file()) { diff --git a/Backend/src/plugin_bundles.rs b/Backend/src/plugin_bundles.rs index b9dfd83..08af8b4 100644 --- a/Backend/src/plugin_bundles.rs +++ b/Backend/src/plugin_bundles.rs @@ -428,7 +428,8 @@ impl PluginBundleStore { .map_err(|e| format!("canonicalize {}: {e}", staging_version_dir.display()))?; // Extract all entries under `/...` into the staging version directory. - let mut tar = open_bundle_archive(bundle_path)?; + let mut tar = open_bundle_archive(bundle_path) + .map_err(|e| format!("open bundle archive {}: {e}", bundle_path.display()))?; for entry in tar.entries().map_err(|e| format!("read tar: {e}"))? { let mut entry = entry.map_err(|e| format!("tar entry: {e}"))?; @@ -1289,7 +1290,8 @@ fn derive_install_version(manifest: &PluginManifest, bundle_sha256: &str) -> Str /// - `Ok((PathBuf, PluginManifest))` manifest path inside archive and manifest payload. /// - `Err(String)` on read/parse/validation failure. fn locate_manifest_in_bundle(bundle_path: &Path) -> Result<(PathBuf, PluginManifest), String> { - let mut tar = open_bundle_archive(bundle_path)?; + let mut tar = open_bundle_archive(bundle_path) + .map_err(|e| format!("open bundle archive {}: {e}", bundle_path.display()))?; let mut manifest_path: Option = None; let mut manifest_json: Option> = None; diff --git a/Backend/src/plugin_paths.rs b/Backend/src/plugin_paths.rs index 09ea9b2..c687afc 100644 --- a/Backend/src/plugin_paths.rs +++ b/Backend/src/plugin_paths.rs @@ -3,7 +3,7 @@ //! Path resolution helpers for installed and built-in plugins. use directories::ProjectDirs; -use log::{info, warn}; +use log::{info, trace, warn}; use std::{ env, path::{Path, PathBuf}, @@ -19,11 +19,15 @@ pub const PLUGIN_MANIFEST_NAME: &str = "openvcs.plugin.json"; pub const BUILT_IN_PLUGINS_DIR_NAME: &str = "built-in-plugins"; /// Directory name used for the bundled Node runtime. pub const NODE_RUNTIME_DIR_NAME: &str = "node-runtime"; +/// Known Linux package directory names owned by OpenVCS. +#[cfg(target_os = "linux")] +const LINUX_PACKAGE_APP_DIR_NAMES: [&str; 2] = ["OpenVCS", "openvcs"]; // If the Tauri runtime resolves a resource directory at startup, we store // it here so plugin discovery can include resources embedded in the // application bundle. static RESOURCE_DIR: OnceLock = OnceLock::new(); +static NODE_RUNTIME_RESOURCE_DIR: OnceLock = OnceLock::new(); static NODE_EXECUTABLE: OnceLock = OnceLock::new(); static LOGGED_BUILTIN_DIRS: AtomicBool = AtomicBool::new(false); @@ -66,11 +70,26 @@ fn push_unique_path(paths: &mut Vec, path: PathBuf) { } } +/// Returns whether a Linux package app directory name belongs to OpenVCS. +/// +/// # Parameters +/// - `app_dir`: Candidate packaged app directory. +/// +/// # Returns +/// - `true` when the directory name matches an OpenVCS-owned package layout. +#[cfg(target_os = "linux")] +fn is_known_linux_package_app_dir(app_dir: &Path) -> bool { + app_dir + .file_name() + .and_then(|name| name.to_str()) + .is_some_and(|name| LINUX_PACKAGE_APP_DIR_NAMES.contains(&name)) +} + /// Adds Linux package resource roots installed under sibling `lib` directories. /// -/// Tauri Linux packages commonly install the executable in `.../bin/` and the -/// mapped resources in `.../lib//`. This helper discovers those app -/// directories only when they already contain the requested resource directory. +/// Only OpenVCS-owned app directories are accepted so resource lookup does not +/// wander into unrelated package trees that happen to contain the same +/// subdirectory names. /// /// # Parameters /// - `paths`: Candidate base directory list. @@ -93,13 +112,27 @@ fn push_linux_package_resource_bases( let lib_dir = prefix_dir.join(lib_dir_name); let entries = match std::fs::read_dir(&lib_dir) { Ok(entries) => entries, - Err(_) => continue, + Err(err) => { + trace!( + "plugins: skipping Linux package resource root {}: {}", + lib_dir.display(), + err + ); + continue; + } }; for entry in entries.flatten() { let app_dir = entry.path(); if !app_dir.is_dir() { continue; } + if !is_known_linux_package_app_dir(&app_dir) { + trace!( + "plugins: skipping non-OpenVCS Linux package dir {}", + app_dir.display() + ); + continue; + } if app_dir.join(resource_dir_name).is_dir() { push_unique_path(paths, app_dir); } @@ -208,6 +241,9 @@ pub fn built_in_plugin_dirs() -> Vec { pub fn bundled_node_candidate_paths() -> Vec { let node_name = if cfg!(windows) { "node.exe" } else { "node" }; let mut candidates = Vec::new(); + if let Some(node_runtime_dir) = NODE_RUNTIME_RESOURCE_DIR.get() { + push_unique_path(&mut candidates, node_runtime_dir.join(node_name)); + } for base_dir in bundled_resource_base_dirs(NODE_RUNTIME_DIR_NAME) { push_unique_path( &mut candidates, @@ -231,6 +267,21 @@ pub fn set_resource_dir(path: PathBuf) { let _ = RESOURCE_DIR.set(path); } +/// Sets the exact `node-runtime` resource directory resolved by Tauri. +/// +/// This is tracked separately from the generic resource base because packaged +/// builds may resolve built-in plugin bundles and `node-runtime` from different +/// roots. Callers should provide the resolved `node-runtime` directory itself. +/// +/// # Parameters +/// - `path`: Exact runtime directory path resolved by Tauri at runtime. +/// +/// # Returns +/// - `()`. +pub fn set_node_runtime_resource_dir(path: PathBuf) { + let _ = NODE_RUNTIME_RESOURCE_DIR.set(path); +} + /// Sets the resolved bundled Node executable path used by plugin runtime. /// /// # Parameters diff --git a/Backend/src/plugin_vcs_backends.rs b/Backend/src/plugin_vcs_backends.rs index a0c7d9d..ecd0b94 100644 --- a/Backend/src/plugin_vcs_backends.rs +++ b/Backend/src/plugin_vcs_backends.rs @@ -60,6 +60,11 @@ pub struct PluginBackendDescriptor { /// startup, so backend discovery must use installed component metadata instead /// of treating bundled `.ovcsp` archives as unpacked plugin directories. /// +/// Discovery also performs a best-effort built-in sync before listing +/// components. This keeps packaged backends visible even if startup sync ran +/// before bundled resources were fully ready or if the installed store later +/// needs repair. +/// /// # Returns /// - `Ok(Vec)` containing discovered backend descriptors. /// - `Err(String)` if installed plugin components cannot be loaded. @@ -68,6 +73,12 @@ pub fn list_plugin_vcs_backends() -> Result, String info!("list_plugin_vcs_backends: discovering VCS backends",); let store = PluginBundleStore::new_default(); + if let Err(err) = store.sync_built_in_plugins() { + warn!( + "list_plugin_vcs_backends: built-in sync failed before discovery: {}", + err + ); + } let plugins = store.list_current_components().map_err(|e| { error!("list_plugin_vcs_backends: failed to list components: {}", e); e diff --git a/docs/plugin architecture.md b/docs/plugin architecture.md index 20e21bf..b1dd3d5 100644 --- a/docs/plugin architecture.md +++ b/docs/plugin architecture.md @@ -44,7 +44,7 @@ Core plugin->host notifications: - `host.event_emit` - `vcs.event` - Plugin runtime requires the app-bundled Node binary (`node-runtime/node` or `node.exe`); no system `node` fallback. -- The backend resolves bundled Node from the Tauri `node-runtime` resource first, then probes packaged filesystem layouts including executable-adjacent `node-runtime/` and Linux `lib//node-runtime/`, and finally the generated dev path under `target/openvcs/node-runtime/`. +- The backend resolves bundled Node from the exact Tauri `node-runtime` resource first, then probes packaged filesystem layouts including executable-adjacent `node-runtime/` and OpenVCS-owned Linux `lib//node-runtime/` directories, and finally the generated dev path under `target/openvcs/node-runtime/`. ## Plugin types @@ -75,8 +75,11 @@ Plugins are installed from `.ovcsp` tar.gz archives. Layout: Built-in plugins ship in the bundled `built-in-plugins/` resource directory as `.ovcsp` archives too. On startup, the backend synchronizes those built-in bundles into the writable installed plugin store before plugin, theme, and VCS -backend discovery runs. Linux package targets may place those resources under -`lib//built-in-plugins/` instead of next to the executable. +backend discovery runs. VCS backend discovery also performs a best-effort +re-sync before listing installed backends so packaged built-ins still appear if +startup sync previously failed. Linux package targets may place those resources +under OpenVCS-owned `lib//built-in-plugins/` directories instead of +next to the executable. ## Manifest (`openvcs.plugin.json`) diff --git a/docs/plugins.md b/docs/plugins.md index 7ea6275..3d4227d 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -82,8 +82,9 @@ Before a plugin module can start, the installed version must be marked Plugin modules run only with the app-bundled Node runtime; OpenVCS does not fall back to `node` from system PATH. The backend resolves bundled Node from the packaged `node-runtime/` resource, packaged filesystem layouts such as -`node-runtime/` next to the executable or Linux `lib//node-runtime/`, -or the generated dev runtime under `target/openvcs/node-runtime/`. +`node-runtime/` next to the executable or OpenVCS-owned Linux +`lib//node-runtime/` directories, or the generated dev runtime under +`target/openvcs/node-runtime/`. Install only plugins you trust. From 7b28d202609517fbccb2b2f683fe21bab4f995a0 Mon Sep 17 00:00:00 2001 From: Jordon Date: Mon, 23 Mar 2026 18:58:17 +0000 Subject: [PATCH 4/4] Update Cargo.lock --- Cargo.lock | 114 ++++++++++++++++++++++++++++++++--------------------- 1 file changed, 68 insertions(+), 46 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 89faabf..f03b261 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -54,9 +54,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.21" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" dependencies = [ "anstyle", "anstyle-parse", @@ -75,9 +75,9 @@ checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] name = "anstyle-parse" -version = "0.2.7" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" dependencies = [ "utf8parse", ] @@ -763,9 +763,9 @@ dependencies = [ [[package]] name = "deflate64" -version = "0.1.11" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "807800ff3288b621186fe0a8f3392c4652068257302709c24efd918c3dffcdc2" +checksum = "ac6b926516df9c60bfa16e107b21086399f8285a44ca9711344b9e553c5146e2" [[package]] name = "deranged" @@ -1009,9 +1009,9 @@ dependencies = [ [[package]] name = "env_filter" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a1c3cc8e57274ec99de65301228b537f1e4eedc1b8e0f9411c6caac8ae7308f" +checksum = "32e90c2accc4b07a8456ea0debdc2e7587bdd890680d71173a15d4ae604f6eef" dependencies = [ "log", "regex", @@ -1019,9 +1019,9 @@ dependencies = [ [[package]] name = "env_logger" -version = "0.11.9" +version = "0.11.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2daee4ea451f429a58296525ddf28b45a3b64f1acf6587e2067437bb11e218d" +checksum = "0621c04f2196ac3f488dd583365b9c09be011a4ab8b9f37248ffcc8f6198b56a" dependencies = [ "anstream", "anstyle", @@ -1995,9 +1995,9 @@ checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" [[package]] name = "iri-string" -version = "0.7.10" +version = "0.7.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" +checksum = "d8e7418f59cc01c88316161279a7f665217ae316b388e58a0d10e29f54f1e5eb" dependencies = [ "memchr", "serde", @@ -2030,9 +2030,9 @@ checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" [[package]] name = "itoa" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "javascriptcore-rs" @@ -2090,7 +2090,7 @@ dependencies = [ "cesu8", "cfg-if", "combine", - "jni-sys", + "jni-sys 0.3.1", "log", "thiserror 1.0.69", "walkdir", @@ -2099,9 +2099,31 @@ dependencies = [ [[package]] name = "jni-sys" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.117", +] [[package]] name = "jobserver" @@ -2413,7 +2435,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" dependencies = [ "bitflags 2.11.0", - "jni-sys", + "jni-sys 0.3.1", "log", "ndk-sys", "num_enum", @@ -2433,7 +2455,7 @@ version = "0.6.0+11769913" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" dependencies = [ - "jni-sys", + "jni-sys 0.3.1", ] [[package]] @@ -2716,8 +2738,8 @@ dependencies = [ "thiserror 2.0.18", "time", "tokio", - "toml 1.0.7+spec-1.1.0", - "zip 7.2.0", + "toml 1.1.0+spec-1.1.0", + "zip 8.4.0", ] [[package]] @@ -3185,7 +3207,7 @@ version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" dependencies = [ - "toml_edit 0.25.5+spec-1.1.0", + "toml_edit 0.25.7+spec-1.1.0", ] [[package]] @@ -3591,9 +3613,9 @@ checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" [[package]] name = "rustls-webpki" -version = "0.103.9" +version = "0.103.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" +checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef" dependencies = [ "ring", "rustls-pki-types", @@ -3839,9 +3861,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "1.0.4" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776" +checksum = "876ac351060d4f882bb1032b6369eb0aef79ad9df1ea8bc404874d8cc3d0cd98" dependencies = [ "serde_core", ] @@ -4179,9 +4201,9 @@ dependencies = [ [[package]] name = "tao" -version = "0.34.6" +version = "0.34.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e06d52c379e63da659a483a958110bbde891695a0ecb53e48cc7786d5eda7bb" +checksum = "9103edf55f2da3c82aea4c7fab7c4241032bfeea0e71fa557d98e00e7ce7cc20" dependencies = [ "bitflags 2.11.0", "block2", @@ -4228,9 +4250,9 @@ dependencies = [ [[package]] name = "tar" -version = "0.4.44" +version = "0.4.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d863878d212c87a19c1a610eb53bb01fe12951c0501cf5a0d65f724914a667a" +checksum = "22692a6476a21fa75fdfc11d452fda482af402c008cdbaf3476414e122040973" dependencies = [ "filetime", "libc", @@ -4745,7 +4767,7 @@ checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" dependencies = [ "indexmap 2.13.0", "serde_core", - "serde_spanned 1.0.4", + "serde_spanned 1.1.0", "toml_datetime 0.7.5+spec-1.1.0", "toml_parser", "toml_writer", @@ -4754,14 +4776,14 @@ dependencies = [ [[package]] name = "toml" -version = "1.0.7+spec-1.1.0" +version = "1.1.0+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd28d57d8a6f6e458bc0b8784f8fdcc4b99a437936056fa122cb234f18656a96" +checksum = "f8195ca05e4eb728f4ba94f3e3291661320af739c4e43779cbdfae82ab239fcc" dependencies = [ "indexmap 2.13.0", "serde_core", - "serde_spanned 1.0.4", - "toml_datetime 1.0.1+spec-1.1.0", + "serde_spanned 1.1.0", + "toml_datetime 1.1.0+spec-1.1.0", "toml_parser", "toml_writer", "winnow 1.0.0", @@ -4787,9 +4809,9 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "1.0.1+spec-1.1.0" +version = "1.1.0+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b320e741db58cac564e26c607d3cc1fdc4a88fd36c879568c07856ed83ff3e9" +checksum = "97251a7c317e03ad83774a8752a7e81fb6067740609f75ea2b585b569a59198f" dependencies = [ "serde_core", ] @@ -4820,30 +4842,30 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.25.5+spec-1.1.0" +version = "0.25.7+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ca1a40644a28bce036923f6a431df0b34236949d111cc07cb6dca830c9ef2e1" +checksum = "d15b06e6c39068c203e7c1d0bc3944796d867449e7668ef7fa5ea43727cb846e" dependencies = [ "indexmap 2.13.0", - "toml_datetime 1.0.1+spec-1.1.0", + "toml_datetime 1.1.0+spec-1.1.0", "toml_parser", "winnow 1.0.0", ] [[package]] name = "toml_parser" -version = "1.0.10+spec-1.1.0" +version = "1.1.0+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7df25b4befd31c4816df190124375d5a20c6b6921e2cad937316de3fccd63420" +checksum = "2334f11ee363607eb04df9b8fc8a13ca1715a72ba8662a26ac285c98aabb4011" dependencies = [ "winnow 1.0.0", ] [[package]] name = "toml_writer" -version = "1.0.7+spec-1.1.0" +version = "1.1.0+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f17aaa1c6e3dc22b1da4b6bba97d066e354c7945cac2f7852d4e4e7ca7a6b56d" +checksum = "d282ade6016312faf3e41e57ebbba0c073e4056dab1232ab1cb624199648f8ed" [[package]] name = "tower" @@ -6214,9 +6236,9 @@ dependencies = [ [[package]] name = "zip" -version = "8.3.1" +version = "8.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c546feb4481b0fbafb4ef0d79b6204fc41c6f9884b1b73b1d73f82442fc0845" +checksum = "7756d0206d058333667493c4014f545f4b9603c4330ccd6d9b3f86dcab59f7d9" dependencies = [ "aes", "bzip2",