From 8a56ea7222401a5b69027e543aec1c80ed382ee8 Mon Sep 17 00:00:00 2001 From: WSH032 <614337162@qq.com> Date: Mon, 2 Jun 2025 21:34:29 +0800 Subject: [PATCH] feat(tauri): support patch context at runtime --- crates/tauri-utils/src/config/parse.rs | 2 +- crates/tauri/Cargo.toml | 5 + crates/tauri/src/ipc/authority.rs | 2 +- crates/tauri/src/ipc/capability_builder.rs | 10 ++ crates/tauri/src/lib.rs | 152 +++++++++++++++++- examples/precompiled/README.md | 3 + examples/precompiled/Tauri.toml | 21 +++ examples/precompiled/main.rs | 26 +++ .../precompiled/src-tauri/frontend/index.html | 34 ++++ .../precompiled/src-tauri/tauri.conf.json | 30 ++++ 10 files changed, 280 insertions(+), 5 deletions(-) create mode 100644 examples/precompiled/README.md create mode 100644 examples/precompiled/Tauri.toml create mode 100644 examples/precompiled/main.rs create mode 100644 examples/precompiled/src-tauri/frontend/index.html create mode 100644 examples/precompiled/src-tauri/tauri.conf.json diff --git a/crates/tauri-utils/src/config/parse.rs b/crates/tauri-utils/src/config/parse.rs index f904d5f2546c..7a4565c77cd6 100644 --- a/crates/tauri-utils/src/config/parse.rs +++ b/crates/tauri-utils/src/config/parse.rs @@ -4,7 +4,7 @@ use crate::config::Config; use crate::platform::Target; -use json_patch::merge; +pub use json_patch::merge; use serde::de::DeserializeOwned; use serde_json::Value; use std::ffi::OsStr; diff --git a/crates/tauri/Cargo.toml b/crates/tauri/Cargo.toml index be5f2fe6a506..402672bffdd1 100644 --- a/crates/tauri/Cargo.toml +++ b/crates/tauri/Cargo.toml @@ -257,3 +257,8 @@ path = "../../examples/streaming/main.rs" name = "isolation" path = "../../examples/isolation/main.rs" required-features = ["isolation"] + +[[example]] +name = "precompiled" +path = "../../examples/precompiled/main.rs" +required-features = ["config-toml"] diff --git a/crates/tauri/src/ipc/authority.rs b/crates/tauri/src/ipc/authority.rs index 8bb81e398f26..dd07d4a8b368 100644 --- a/crates/tauri/src/ipc/authority.rs +++ b/crates/tauri/src/ipc/authority.rs @@ -152,7 +152,7 @@ impl RuntimeAuthority { } #[cfg(feature = "dynamic-acl")] - fn add_capability_inner(&mut self, capability: CapabilityFile) -> crate::Result<()> { + pub(crate) fn add_capability_inner(&mut self, capability: CapabilityFile) -> crate::Result<()> { let mut capabilities = BTreeMap::new(); match capability { CapabilityFile::Capability(c) => { diff --git a/crates/tauri/src/ipc/capability_builder.rs b/crates/tauri/src/ipc/capability_builder.rs index 62b5113bd42e..ae9224789ab6 100644 --- a/crates/tauri/src/ipc/capability_builder.rs +++ b/crates/tauri/src/ipc/capability_builder.rs @@ -17,12 +17,22 @@ pub trait RuntimeCapability { fn build(self) -> CapabilityFile; } +/// we should remove this implementation, use `::from_str` instead impl> RuntimeCapability for T { fn build(self) -> CapabilityFile { self.as_ref().parse().expect("invalid capability") } } +// // TODO: Because we have already `impl> RuntimeCapability for T`, +// // we cant implement `RuntimeCapability` for `CapabilityFile` anymore. +// +// impl RuntimeCapability for CapabilityFile { +// fn build(self) -> CapabilityFile { +// self +// } +// } + /// A builder for a [`Capability`]. pub struct CapabilityBuilder(Capability); diff --git a/crates/tauri/src/lib.rs b/crates/tauri/src/lib.rs index 2a8a0fea616f..1929deafbbb2 100644 --- a/crates/tauri/src/lib.rs +++ b/crates/tauri/src/lib.rs @@ -191,11 +191,13 @@ pub use tauri_runtime_wry::{tao, wry}; /// A task to run on the main thread. pub type SyncTask = Box; +use anyhow::Context as _; use serde::Serialize; use std::{ borrow::Cow, collections::HashMap, - fmt::{self, Debug}, + fmt::{self, Debug, Display}, + path::PathBuf, sync::MutexGuard, }; use utils::assets::{AssetKey, CspHash, EmbeddedAssets}; @@ -354,10 +356,19 @@ pub trait Assets: Send + Sync + 'static { fn get(&self, key: &AssetKey) -> Option>; /// Iterator for the assets. - fn iter(&self) -> Box>; + /// + /// This is for optimization purposes only; + /// Implementers may return an empty iterator even if [Assets::get] returns [Some]. + fn iter(&self) -> Box> { + Box::new(std::iter::empty()) + } /// Gets the hashes for the CSP tag of the HTML on the given path. - fn csp_hashes(&self, html_path: &AssetKey) -> Box> + '_>; + /// + /// Implementers may return an empty iterator. + fn csp_hashes(&self, _html_path: &AssetKey) -> Box> + '_> { + Box::new(std::iter::empty()) + } } impl Assets for EmbeddedAssets { @@ -374,6 +385,42 @@ impl Assets for EmbeddedAssets { } } +/// A simple `Assets` implementation that reads files from disk directory. +struct DirectoryAssets(PathBuf); + +impl DirectoryAssets { + fn new(dir: PathBuf) -> Self { + Self(dir) + } +} + +// TODO: [tauri_codegen::embedded_assets::AssetOptions] +impl Assets for DirectoryAssets { + fn get(&self, key: &AssetKey) -> Option> { + fn unwrap_and_log( + result: std::result::Result, + key: &AssetKey, + ) -> Option { + match result { + Ok(value) => Some(value), + Err(e) => { + log::info!("Failed to get asset from path '{}': {}", key.as_ref(), e); + None + } + } + } + + // We need to skip the first character (i.e., `/`) of the key. + let path = self.0.join(&key.as_ref()[1..]); + // prevents path traversal. + let safe_path = unwrap_and_log(path::SafePathBuf::new(path), key)?; + // TODO: LRU cache + let asset = unwrap_and_log(std::fs::read(&safe_path), key)?; + + Some(Cow::Owned(asset)) + } +} + /// User supplied data required inside of a Tauri application. /// /// # Stability @@ -524,6 +571,105 @@ impl Context { } } + // ref: + #[cfg(all(desktop, feature = "dynamic-acl"))] + pub fn load_runtime_context( + // NOTE: if error, this `Context` has already been modified (tainted), so we do not allow it to be reused + mut self, + src_tauri_dir: &std::path::Path, + tauri_config: Option, + ) -> Result { + if !src_tauri_dir.is_absolute() { + return Err( + anyhow::anyhow!( + "The `src_tauri_dir` path must be absolute, got: {}", + src_tauri_dir.display() + ) + .into(), + ); + } + + let dev = is_dev(); + let target = utils::platform::Target::current(); + + // Load config from file dynamically. + // ref: + let mut src_config = utils::config::parse::read_from(target, src_tauri_dir) + .with_context(|| "Failed to read tauri config")? + .0; + if let Some(tauri_config) = tauri_config { + utils::config::parse::merge(&mut src_config, &serde_json::to_value(tauri_config)?); + } + let mut new_config = serde_json::to_value(self.config)?; + utils::config::parse::merge(&mut new_config, &src_config); + // NOTE: unlike [tauri_codegen::get_config], we don't change CWD here, + // because it will change the runtime-behavior of user's application. + self.config = serde_json::from_value(new_config)?; + + // Patch `package_info` from `config`. + // ref: + if let Some(product_name) = &self.config.product_name { + self.package_info.name = product_name.clone(); + } + if let Some(version) = &self.config.version { + self.package_info.version = version.parse().with_context(|| "Failed to parse version")?; + } + // TODO: PackageInfo::{authors, description, crate_name} + + // Supply custom Assets from disk dynamically. + // ref: + let mut new_asset: Option>> = None; + if let Some(frontend_dist) = &self.config.build.frontend_dist { + match frontend_dist { + utils::config::FrontendDist::Url(_) => { + // do nothing, we don't need supply custom Assets for URL frontend_dist, + // because tauri will fetch the frontend from the URL. + } + utils::config::FrontendDist::Directory(dir) => { + new_asset = Some(Box::new(DirectoryAssets::new(src_tauri_dir.join(dir)))); + } + other => { + return Err(anyhow::anyhow!("Unsupported frontend_dist: {:?}", other).into()); + } + } + } + if let Some(assets) = new_asset { + self.assets = assets; + } + + // Load capabilities from disk dynamically. + // ref: + let capabilities_pattern_path = src_tauri_dir.join("./capabilities/**/*"); + let capabilities_pattern = capabilities_pattern_path.to_str().with_context(|| { + format!( + "Failed to convert capabilities pattern path to str: {}", + capabilities_pattern_path.display() + ) + })?; + let capabilities_from_files = utils::acl::build::parse_capabilities(capabilities_pattern) + .with_context(|| { + format!( + "Failed to parse capabilities from pattern: {}", + capabilities_pattern + ) + })?; + + // Patch `capabilities` from `config`. + // ref: + // + let capabilities = utils::acl::get_capabilities(&self.config, capabilities_from_files, None)?; + + // Add capabilities to `ctx`. + // TODO: maybe we should clear the existing capabilities first? + self + .runtime_authority + .add_capability_inner(utils::acl::capability::CapabilityFile::List( + capabilities.into_values().collect(), + ))?; + + Ok(self) + } + #[cfg(dev)] #[doc(hidden)] pub fn with_config_parent(&mut self, config_parent: impl AsRef) { diff --git a/examples/precompiled/README.md b/examples/precompiled/README.md new file mode 100644 index 000000000000..764ccdf03e4d --- /dev/null +++ b/examples/precompiled/README.md @@ -0,0 +1,3 @@ +# Precompiled B--features isolation` + +To execute run the following on the root directory of the repository: `cargo run --example precompiled --features="config-toml"`. diff --git a/examples/precompiled/Tauri.toml b/examples/precompiled/Tauri.toml new file mode 100644 index 000000000000..7c29b1ea8f40 --- /dev/null +++ b/examples/precompiled/Tauri.toml @@ -0,0 +1,21 @@ +# TODO, FIXME: The following is the basic necessary configuration, +# otherwise `tauri-build` will report an error. + +"$schema" = "https://schema.tauri.app/config/2" +productName = "Precompiled Tauri" +identifier = "com.precompiled-tauri.dev" + +[app] +# NOTE: IMPORTANT! We must enable `withGlobalTauri` when compile-time, +# because it can't be enabled dynamically at runtime, +# see: +withGlobalTauri = true + +[bundle] +icon = [ + "../.icons/32x32.png", + "../.icons/128x128.png", + "../.icons/128x128@2x.png", + "../.icons/icon.icns", + "../.icons/icon.ico" +] diff --git a/examples/precompiled/main.rs b/examples/precompiled/main.rs new file mode 100644 index 000000000000..ce5b45a4ef68 --- /dev/null +++ b/examples/precompiled/main.rs @@ -0,0 +1,26 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + +use std::env::current_dir; + +#[tauri::command] +fn greet(name: &str) -> String { + format!("Hello {name}, You have been greeted from Rust!") +} + +fn main() { + let current_dir = current_dir().expect("failed to get current directory"); + + let context = tauri::generate_context!("../../examples/precompiled/Tauri.toml"); + let context = context + .load_runtime_context(¤t_dir.join("examples/precompiled/src-tauri"), None) + .expect("failed to load runtime context"); + + tauri::Builder::default() + .invoke_handler(tauri::generate_handler![greet]) + .run(context) + .expect("error while running tauri application"); +} diff --git a/examples/precompiled/src-tauri/frontend/index.html b/examples/precompiled/src-tauri/frontend/index.html new file mode 100644 index 000000000000..d2ea9d5f7cbd --- /dev/null +++ b/examples/precompiled/src-tauri/frontend/index.html @@ -0,0 +1,34 @@ + + + + + + Welcome to Tauri! + + +

Welcome to Tauri!

+ +
+ + +
+ +

+ + + + diff --git a/examples/precompiled/src-tauri/tauri.conf.json b/examples/precompiled/src-tauri/tauri.conf.json new file mode 100644 index 000000000000..b79878cc7dfe --- /dev/null +++ b/examples/precompiled/src-tauri/tauri.conf.json @@ -0,0 +1,30 @@ +{ + "$schema": "../../../crates/tauri-schema-generator/schemas/config.schema.json", + "productName": "Precompiled Tauri Example", + "version": "0.1.0", + "identifier": "com.precompiled-tauri-example.dev", + "build": { + "frontendDist": "frontend" + }, + "app": { + "withGlobalTauri": true, + "windows": [ + { + "title": "Welcome to Precompiled Tauri!", + "width": 800, + "height": 600, + "resizable": true, + "fullscreen": false + } + ] + }, + "bundle": { + "icon": [ + "../../.icons/32x32.png", + "../../.icons/128x128.png", + "../../.icons/128x128@2x.png", + "../../.icons/icon.icns", + "../../.icons/icon.ico" + ] + } +}