Эх сурвалжийг харах

speed up asset inclusion on debug mode (fixes #1394) (#1430)

Co-authored-by: Lucas Nogueira <lucas@tauri.studio>
chip 4 жил өмнө
parent
commit
8584e667cd

+ 10 - 0
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")))]
 #[cfg_attr(doc_cfg, doc(cfg(feature = "codegen")))]
 #[derive(Debug)]
 #[derive(Debug)]
 pub struct CodegenContext {
 pub struct CodegenContext {
+  dev: bool,
   config_path: PathBuf,
   config_path: PathBuf,
   out_file: PathBuf,
   out_file: PathBuf,
 }
 }
@@ -22,6 +23,7 @@ pub struct CodegenContext {
 impl Default for CodegenContext {
 impl Default for CodegenContext {
   fn default() -> Self {
   fn default() -> Self {
     Self {
     Self {
+      dev: false,
       config_path: PathBuf::from("tauri.conf.json"),
       config_path: PathBuf::from("tauri.conf.json"),
       out_file: PathBuf::from("tauri-build-context.rs"),
       out_file: PathBuf::from("tauri-build-context.rs"),
     }
     }
@@ -60,6 +62,13 @@ impl CodegenContext {
     self
     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.
   /// 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
   /// 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<PathBuf> {
   pub fn try_build(self) -> Result<PathBuf> {
     let (config, config_parent) = tauri_codegen::get_config(&self.config_path)?;
     let (config, config_parent) = tauri_codegen::get_config(&self.config_path)?;
     let code = context_codegen(ContextData {
     let code = context_codegen(ContextData {
+      dev: self.dev,
       config,
       config,
       config_parent,
       config_parent,
       // it's very hard to have a build script for unit tests, so assume this is always called from
       // it's very hard to have a build script for unit tests, so assume this is always called from

+ 2 - 1
core/tauri-codegen/Cargo.toml

@@ -10,6 +10,7 @@ description = "code generation meant to be consumed inside of `tauri` through `t
 edition = "2018"
 edition = "2018"
 
 
 [dependencies]
 [dependencies]
+blake3 = { version = "0.3", features = ["rayon"] }
 proc-macro2 = "1"
 proc-macro2 = "1"
 quote = "1"
 quote = "1"
 serde = { version = "1", features = ["derive"] }
 serde = { version = "1", features = ["derive"] }
@@ -17,4 +18,4 @@ serde_json = "1"
 tauri-api = { path = "../../tauri-api", features = ["build"] }
 tauri-api = { path = "../../tauri-api", features = ["build"] }
 thiserror = "1"
 thiserror = "1"
 walkdir = "2"
 walkdir = "2"
-zstd = "0.6"
+zstd = "0.7"

+ 18 - 4
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.
 /// Necessary data needed by [`codegen_context`] to generate code for a Tauri application context.
 pub struct ContextData {
 pub struct ContextData {
+  pub dev: bool,
   pub config: Config,
   pub config: Config,
   pub config_parent: PathBuf,
   pub config_parent: PathBuf,
   pub context_path: TokenStream,
   pub context_path: TokenStream,
@@ -14,15 +15,28 @@ pub struct ContextData {
 /// Build an `AsTauriContext` implementation for including in application code.
 /// Build an `AsTauriContext` implementation for including in application code.
 pub fn context_codegen(data: ContextData) -> Result<TokenStream, EmbeddedAssetsError> {
 pub fn context_codegen(data: ContextData) -> Result<TokenStream, EmbeddedAssetsError> {
   let ContextData {
   let ContextData {
-    mut config,
+    dev,
+    config,
     config_parent,
     config_parent,
     context_path,
     context_path,
   } = data;
   } = 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
   // 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
   // handle default window icons for Windows targets
   let default_window_icon = if cfg!(windows) {
   let default_window_icon = if cfg!(windows) {

+ 69 - 24
core/tauri-codegen/src/embedded_assets.rs

@@ -2,17 +2,21 @@ use proc_macro2::TokenStream;
 use quote::{quote, ToTokens, TokenStreamExt};
 use quote::{quote, ToTokens, TokenStreamExt};
 use std::{
 use std::{
   collections::HashMap,
   collections::HashMap,
-  env::var,
   fs::File,
   fs::File,
-  io::BufReader,
   path::{Path, PathBuf},
   path::{Path, PathBuf},
 };
 };
 use tauri_api::assets::AssetKey;
 use tauri_api::assets::AssetKey;
 use thiserror::Error;
 use thiserror::Error;
 use walkdir::WalkDir;
 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))
 /// (key, (original filepath, compressed bytes))
-type Asset = (AssetKey, (String, Vec<u8>));
+type Asset = (AssetKey, (PathBuf, PathBuf));
 
 
 /// All possible errors while reading and compressing an [`EmbeddedAssets`] directory
 /// All possible errors while reading and compressing an [`EmbeddedAssets`] directory
 #[derive(Debug, Error)]
 #[derive(Debug, Error)]
@@ -37,6 +41,9 @@ pub enum EmbeddedAssetsError {
     path: PathBuf,
     path: PathBuf,
     error: walkdir::Error,
     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.
 /// 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`]
 /// 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
 /// through [`ToTokens`]. The generated code is meant to be injected into an application to include
 /// the compressed assets in that application's binary.
 /// the compressed assets in that application's binary.
-pub struct EmbeddedAssets(HashMap<AssetKey, (String, Vec<u8>)>);
+#[derive(Default)]
+pub struct EmbeddedAssets(HashMap<AssetKey, (PathBuf, PathBuf)>);
 
 
 impl EmbeddedAssets {
 impl EmbeddedAssets {
   /// Compress a directory of assets, ready to be generated into a [`tauri_api::assets::Assets`].
   /// 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
   /// Use highest compression level for release, the fastest one for everything else
   fn compression_level() -> i32 {
   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.
   /// Compress a file and spit out the information in a [`HashMap`] friendly form.
   fn compress_file(prefix: &Path, path: &Path) -> Result<Asset, EmbeddedAssetsError> {
   fn compress_file(prefix: &Path, path: &Path) -> Result<Asset, EmbeddedAssetsError> {
-    let reader =
-      File::open(&path)
-        .map(BufReader::new)
-        .map_err(|error| EmbeddedAssetsError::AssetRead {
-          path: path.to_owned(),
-          error,
-        })?;
+    let input = std::fs::read(path).map_err(|error| EmbeddedAssetsError::AssetRead {
+      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,
+    // 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::<blake3::join::RayonJoin>(&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,
+        }
+      })?;
+    }
 
 
     // get a key to the asset path without the asset directory prefix
     // get a key to the asset path without the asset directory prefix
     let key = path
     let key = path
@@ -108,20 +151,22 @@ impl EmbeddedAssets {
         path: path.to_owned(),
         path: path.to_owned(),
       })?;
       })?;
 
 
-    Ok((key, (path.display().to_string(), bytes)))
+    Ok((key, (path.into(), out_path)))
   }
   }
 }
 }
 
 
 impl ToTokens for EmbeddedAssets {
 impl ToTokens for EmbeddedAssets {
   fn to_tokens(&self, tokens: &mut TokenStream) {
   fn to_tokens(&self, tokens: &mut TokenStream) {
     let mut map = TokenStream::new();
     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 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
       // add original asset as a compiler dependency, rely on dead code elimination to clean it up
       map.append_all(quote!(#key => {
       map.append_all(quote!(#key => {
-        const _: &[u8] = include_bytes!(#original);
-        &[#(#bytes),*]
+        const _: &[u8] = include_bytes!(#input);
+        include_bytes!(#output)
       },));
       },));
     }
     }
 
 

+ 3 - 0
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"]

+ 7 - 3
examples/api/src-tauri/tauri.conf.json

@@ -1,7 +1,7 @@
 {
 {
   "build": {
   "build": {
     "distDir": "../public",
     "distDir": "../public",
-    "devPath": "http://localhost:5000",
+    "devPath": "../public",
     "beforeDevCommand": "yarn dev",
     "beforeDevCommand": "yarn dev",
     "beforeBuildCommand": "yarn build"
     "beforeBuildCommand": "yarn build"
   },
   },
@@ -24,7 +24,11 @@
           "name": "theme",
           "name": "theme",
           "takesValue": true,
           "takesValue": true,
           "description": "App theme",
           "description": "App theme",
-          "possibleValues": ["light", "dark", "system"]
+          "possibleValues": [
+            "light",
+            "dark",
+            "system"
+          ]
         },
         },
         {
         {
           "short": "v",
           "short": "v",
@@ -77,4 +81,4 @@
       "csp": "default-src blob: data: filesystem: ws: http: https: 'unsafe-eval' 'unsafe-inline'"
       "csp": "default-src blob: data: filesystem: ws: http: https: 'unsafe-eval' 'unsafe-inline'"
     }
     }
   }
   }
-}
+}

+ 3 - 0
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"]

+ 3 - 0
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"]

+ 3 - 0
tauri-macros/Cargo.toml

@@ -17,3 +17,6 @@ proc-macro2 = "1"
 quote = "1"
 quote = "1"
 syn = { version = "1", features = [ "full" ] }
 syn = { version = "1", features = [ "full" ] }
 tauri-codegen = { path = "../core/tauri-codegen" }
 tauri-codegen = { path = "../core/tauri-codegen" }
+
+[features]
+custom-protocol = []

+ 1 - 0
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)
   let context = get_config(&context.config_file)
     .map_err(|e| e.to_string())
     .map_err(|e| e.to_string())
     .map(|(config, config_parent)| ContextData {
     .map(|(config, config_parent)| ContextData {
+      dev: cfg!(not(feature = "custom-protocol")),
       config,
       config,
       config_parent,
       config_parent,
       context_path: context.context_path.to_token_stream(),
       context_path: context.context_path.to_token_stream(),

+ 1 - 1
tauri-utils/Cargo.toml

@@ -14,7 +14,7 @@ serde_json = "1.0"
 sysinfo = "0.10"
 sysinfo = "0.10"
 thiserror = "1.0.19"
 thiserror = "1.0.19"
 phf = { version = "0.8", features = ["macros"] }
 phf = { version = "0.8", features = ["macros"] }
-zstd = "0.6"
+zstd = "0.7"
 
 
 # build feature only
 # build feature only
 proc-macro2 = { version = "1.0", optional = true }
 proc-macro2 = { version = "1.0", optional = true }

+ 1 - 1
tauri/Cargo.toml

@@ -43,7 +43,7 @@ serde = { version = "1.0", features = [ "derive" ] }
 
 
 [features]
 [features]
 cli = [ "tauri-api/cli" ]
 cli = [ "tauri-api/cli" ]
-custom-protocol = [ ]
+custom-protocol = [ "tauri-macros/custom-protocol" ]
 api-all = [ "tauri-api/notification", "tauri-api/global-shortcut", "updater" ]
 api-all = [ "tauri-api/notification", "tauri-api/global-shortcut", "updater" ]
 updater = [ "tauri-updater" ]
 updater = [ "tauri-updater" ]
 
 

+ 1 - 13
tauri/src/runtime/manager.rs

@@ -106,19 +106,7 @@ where
     if self.inner.config.build.dev_path.starts_with("http") {
     if self.inner.config.build.dev_path.starts_with("http") {
       self.inner.config.build.dev_path.clone()
       self.inner.config.build.dev_path.clone()
     } else {
     } 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)
     }
     }
   }
   }