diff --git a/core/tauri-build/src/codegen/context.rs b/core/tauri-build/src/codegen/context.rs index 37f086708..57c85db84 100644 --- a/core/tauri-build/src/codegen/context.rs +++ b/core/tauri-build/src/codegen/context.rs @@ -15,6 +15,7 @@ use tauri_codegen::{context_codegen, ContextData}; #[cfg_attr(doc_cfg, doc(cfg(feature = "codegen")))] #[derive(Debug)] pub struct CodegenContext { + dev: bool, config_path: PathBuf, out_file: PathBuf, } @@ -22,6 +23,7 @@ pub struct CodegenContext { impl Default for CodegenContext { fn default() -> Self { Self { + dev: false, config_path: PathBuf::from("tauri.conf.json"), out_file: PathBuf::from("tauri-build-context.rs"), } @@ -60,6 +62,13 @@ impl CodegenContext { self } + /// Run the codegen in a `dev` context, meaning that Tauri is using a dev server or local file for development purposes, + /// usually with the `tauri dev` CLI command. + pub fn dev(mut self) -> Self { + self.dev = true; + self + } + /// Generate the code and write it to the output file - returning the path it was saved to. /// /// Unless you are doing something special with this builder, you don't need to do anything with @@ -80,6 +89,7 @@ impl CodegenContext { pub fn try_build(self) -> Result { let (config, config_parent) = tauri_codegen::get_config(&self.config_path)?; let code = context_codegen(ContextData { + dev: self.dev, config, config_parent, // it's very hard to have a build script for unit tests, so assume this is always called from diff --git a/core/tauri-codegen/Cargo.toml b/core/tauri-codegen/Cargo.toml index 9a4b06e5e..4c288e940 100644 --- a/core/tauri-codegen/Cargo.toml +++ b/core/tauri-codegen/Cargo.toml @@ -10,6 +10,7 @@ description = "code generation meant to be consumed inside of `tauri` through `t edition = "2018" [dependencies] +blake3 = { version = "0.3", features = ["rayon"] } proc-macro2 = "1" quote = "1" serde = { version = "1", features = ["derive"] } @@ -17,4 +18,4 @@ serde_json = "1" tauri-api = { path = "../../tauri-api", features = ["build"] } thiserror = "1" walkdir = "2" -zstd = "0.6" +zstd = "0.7" diff --git a/core/tauri-codegen/src/context.rs b/core/tauri-codegen/src/context.rs index b4ae7af3d..d1341ef22 100644 --- a/core/tauri-codegen/src/context.rs +++ b/core/tauri-codegen/src/context.rs @@ -6,6 +6,7 @@ use tauri_api::config::Config; /// Necessary data needed by [`codegen_context`] to generate code for a Tauri application context. pub struct ContextData { + pub dev: bool, pub config: Config, pub config_parent: PathBuf, pub context_path: TokenStream, @@ -14,15 +15,28 @@ pub struct ContextData { /// Build an `AsTauriContext` implementation for including in application code. pub fn context_codegen(data: ContextData) -> Result { let ContextData { - mut config, + dev, + config, config_parent, context_path, } = data; - let dist_dir = config_parent.join(&config.build.dist_dir); - config.build.dist_dir = dist_dir.to_string_lossy().to_string(); + let assets_path = if dev { + // if dev_path is a dev server, we don't have any assets to embed + if config.build.dev_path.starts_with("http") { + None + } else { + Some(config_parent.join(&config.build.dev_path)) + } + } else { + Some(config_parent.join(&config.build.dist_dir)) + }; // generate the assets inside the dist dir into a perfect hash function - let assets = EmbeddedAssets::new(&dist_dir)?; + let assets = if let Some(assets_path) = assets_path { + EmbeddedAssets::new(&assets_path)? + } else { + Default::default() + }; // handle default window icons for Windows targets let default_window_icon = if cfg!(windows) { diff --git a/core/tauri-codegen/src/embedded_assets.rs b/core/tauri-codegen/src/embedded_assets.rs index 43fc7b84d..3ebd6302a 100644 --- a/core/tauri-codegen/src/embedded_assets.rs +++ b/core/tauri-codegen/src/embedded_assets.rs @@ -2,17 +2,21 @@ use proc_macro2::TokenStream; use quote::{quote, ToTokens, TokenStreamExt}; use std::{ collections::HashMap, - env::var, fs::File, - io::BufReader, path::{Path, PathBuf}, }; use tauri_api::assets::AssetKey; use thiserror::Error; use walkdir::WalkDir; +/// The subdirectory inside the target directory we want to place assets. +const TARGET_PATH: &str = "tauri-codegen-assets"; + +/// The minimum size needed for the hasher to use multiple threads. +const MULTI_HASH_SIZE_LIMIT: usize = 131_072; // 128KiB + /// (key, (original filepath, compressed bytes)) -type Asset = (AssetKey, (String, Vec)); +type Asset = (AssetKey, (PathBuf, PathBuf)); /// All possible errors while reading and compressing an [`EmbeddedAssets`] directory #[derive(Debug, Error)] @@ -37,6 +41,9 @@ pub enum EmbeddedAssetsError { path: PathBuf, error: walkdir::Error, }, + + #[error("OUT_DIR env var is not set, do you have a build script?")] + OutDir, } /// Represent a directory of assets that are compressed and embedded. @@ -48,7 +55,8 @@ pub enum EmbeddedAssetsError { /// The assets are compressed during this runtime, and can only be represented as a [`TokenStream`] /// through [`ToTokens`]. The generated code is meant to be injected into an application to include /// the compressed assets in that application's binary. -pub struct EmbeddedAssets(HashMap)>); +#[derive(Default)] +pub struct EmbeddedAssets(HashMap); impl EmbeddedAssets { /// Compress a directory of assets, ready to be generated into a [`tauri_api::assets::Assets`]. @@ -75,29 +83,64 @@ impl EmbeddedAssets { /// Use highest compression level for release, the fastest one for everything else fn compression_level() -> i32 { - match var("PROFILE").as_ref().map(String::as_str) { - Ok("release") => 22, - _ => -5, + let levels = zstd::compression_level_range(); + if cfg!(debug_assertions) { + *levels.start() + } else { + *levels.end() } } /// Compress a file and spit out the information in a [`HashMap`] friendly form. fn compress_file(prefix: &Path, path: &Path) -> Result { - let reader = - File::open(&path) - .map(BufReader::new) - .map_err(|error| EmbeddedAssetsError::AssetRead { + let input = std::fs::read(path).map_err(|error| EmbeddedAssetsError::AssetRead { + path: path.to_owned(), + error, + })?; + + // we must canonicalize the base of our paths to allow long paths on windows + let out_dir = std::env::var("OUT_DIR") + .map_err(|_| EmbeddedAssetsError::OutDir) + .map(PathBuf::from) + .and_then(|p| p.canonicalize().map_err(|_| EmbeddedAssetsError::OutDir)) + .map(|p| p.join(TARGET_PATH))?; + + // make sure that our output directory is created + std::fs::create_dir_all(&out_dir).map_err(|_| EmbeddedAssetsError::OutDir)?; + + // get a hash of the input - allows for caching existing files + let hash = { + let mut hasher = blake3::Hasher::new(); + if input.len() < MULTI_HASH_SIZE_LIMIT { + hasher.update(&input); + } else { + hasher.update_with_join::(&input); + } + hasher.finalize().to_hex() + }; + + // use the content hash to determine filename, keep extensions that exist + let out_path = if let Some(ext) = path.extension().and_then(|e| e.to_str()) { + out_dir.join(format!("{}.{}", hash, ext)) + } else { + out_dir.join(hash.to_string()) + }; + + // only compress and write to the file if it doesn't already exist. + if !out_path.exists() { + let out_file = File::create(&out_path).map_err(|error| EmbeddedAssetsError::AssetWrite { + path: out_path.clone(), + error, + })?; + + // entirely write input to the output file path with compression + zstd::stream::copy_encode(&*input, out_file, Self::compression_level()).map_err(|error| { + EmbeddedAssetsError::AssetWrite { path: path.to_owned(), error, - })?; - - // entirely read compressed asset into bytes - let bytes = zstd::encode_all(reader, Self::compression_level()).map_err(|error| { - EmbeddedAssetsError::AssetWrite { - path: path.to_owned(), - error, - } - })?; + } + })?; + } // get a key to the asset path without the asset directory prefix let key = path @@ -108,20 +151,22 @@ impl EmbeddedAssets { path: path.to_owned(), })?; - Ok((key, (path.display().to_string(), bytes))) + Ok((key, (path.into(), out_path))) } } impl ToTokens for EmbeddedAssets { fn to_tokens(&self, tokens: &mut TokenStream) { let mut map = TokenStream::new(); - for (key, (original, bytes)) in &self.0 { + for (key, (input, output)) in &self.0 { let key: &str = key.as_ref(); + let input = input.display().to_string(); + let output = output.display().to_string(); // add original asset as a compiler dependency, rely on dead code elimination to clean it up map.append_all(quote!(#key => { - const _: &[u8] = include_bytes!(#original); - &[#(#bytes),*] + const _: &[u8] = include_bytes!(#input); + include_bytes!(#output) },)); } diff --git a/examples/api/src-tauri/.cargo/config b/examples/api/src-tauri/.cargo/config new file mode 100644 index 000000000..c39e8d415 --- /dev/null +++ b/examples/api/src-tauri/.cargo/config @@ -0,0 +1,3 @@ +# using lld linker can make the compile time 30% of the default linker time. +[build] +rustflags = ["-C", "link-arg=-fuse-ld=lld"] diff --git a/examples/api/src-tauri/tauri.conf.json b/examples/api/src-tauri/tauri.conf.json index da944a3eb..18d56b410 100644 --- a/examples/api/src-tauri/tauri.conf.json +++ b/examples/api/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "build": { "distDir": "../public", - "devPath": "http://localhost:5000", + "devPath": "../public", "beforeDevCommand": "yarn dev", "beforeBuildCommand": "yarn build" }, @@ -24,7 +24,11 @@ "name": "theme", "takesValue": true, "description": "App theme", - "possibleValues": ["light", "dark", "system"] + "possibleValues": [ + "light", + "dark", + "system" + ] }, { "short": "v", @@ -77,4 +81,4 @@ "csp": "default-src blob: data: filesystem: ws: http: https: 'unsafe-eval' 'unsafe-inline'" } } -} +} \ No newline at end of file diff --git a/examples/helloworld/src-tauri/.cargo/config b/examples/helloworld/src-tauri/.cargo/config new file mode 100644 index 000000000..c39e8d415 --- /dev/null +++ b/examples/helloworld/src-tauri/.cargo/config @@ -0,0 +1,3 @@ +# using lld linker can make the compile time 30% of the default linker time. +[build] +rustflags = ["-C", "link-arg=-fuse-ld=lld"] diff --git a/examples/multiwindow/src-tauri/.cargo/config b/examples/multiwindow/src-tauri/.cargo/config new file mode 100644 index 000000000..c39e8d415 --- /dev/null +++ b/examples/multiwindow/src-tauri/.cargo/config @@ -0,0 +1,3 @@ +# using lld linker can make the compile time 30% of the default linker time. +[build] +rustflags = ["-C", "link-arg=-fuse-ld=lld"] diff --git a/tauri-macros/Cargo.toml b/tauri-macros/Cargo.toml index 631711853..07fee1fbf 100644 --- a/tauri-macros/Cargo.toml +++ b/tauri-macros/Cargo.toml @@ -17,3 +17,6 @@ proc-macro2 = "1" quote = "1" syn = { version = "1", features = [ "full" ] } tauri-codegen = { path = "../core/tauri-codegen" } + +[features] +custom-protocol = [] diff --git a/tauri-macros/src/context/mod.rs b/tauri-macros/src/context/mod.rs index 7b15ed4da..c8d138e7f 100644 --- a/tauri-macros/src/context/mod.rs +++ b/tauri-macros/src/context/mod.rs @@ -72,6 +72,7 @@ pub(crate) fn generate_context(context: ContextItems) -> TokenStream { let context = get_config(&context.config_file) .map_err(|e| e.to_string()) .map(|(config, config_parent)| ContextData { + dev: cfg!(not(feature = "custom-protocol")), config, config_parent, context_path: context.context_path.to_token_stream(), diff --git a/tauri-utils/Cargo.toml b/tauri-utils/Cargo.toml index 53d031e9e..39d0d5fe8 100644 --- a/tauri-utils/Cargo.toml +++ b/tauri-utils/Cargo.toml @@ -14,7 +14,7 @@ serde_json = "1.0" sysinfo = "0.10" thiserror = "1.0.19" phf = { version = "0.8", features = ["macros"] } -zstd = "0.6" +zstd = "0.7" # build feature only proc-macro2 = { version = "1.0", optional = true } diff --git a/tauri/Cargo.toml b/tauri/Cargo.toml index 072dd770a..222b58a0f 100644 --- a/tauri/Cargo.toml +++ b/tauri/Cargo.toml @@ -43,7 +43,7 @@ serde = { version = "1.0", features = [ "derive" ] } [features] cli = [ "tauri-api/cli" ] -custom-protocol = [ ] +custom-protocol = [ "tauri-macros/custom-protocol" ] api-all = [ "tauri-api/notification", "tauri-api/global-shortcut", "updater" ] updater = [ "tauri-updater" ] diff --git a/tauri/src/runtime/manager.rs b/tauri/src/runtime/manager.rs index eaa5a673a..5094c543f 100644 --- a/tauri/src/runtime/manager.rs +++ b/tauri/src/runtime/manager.rs @@ -106,19 +106,7 @@ where if self.inner.config.build.dev_path.starts_with("http") { self.inner.config.build.dev_path.clone() } else { - let path = "index.html"; - format!( - "data:text/html;base64,{}", - base64::encode( - self - .inner - .assets - .get(&path) - .ok_or_else(|| crate::Error::AssetNotFound(path.to_string())) - .map(Cow::into_owned) - .expect("Unable to find `index.html` under your devPath folder") - ) - ) + format!("tauri://{}", self.inner.config.tauri.bundle.identifier) } }