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

Co-authored-by: Lucas Nogueira <lucas@tauri.studio>
This commit is contained in:
chip
2021-04-05 21:15:53 -07:00
committed by GitHub
parent 836d9d0834
commit 8584e667cd
13 changed files with 122 additions and 47 deletions

View File

@@ -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<PathBuf> {
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

View File

@@ -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"

View File

@@ -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<TokenStream, EmbeddedAssetsError> {
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) {

View File

@@ -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<u8>));
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<AssetKey, (String, Vec<u8>)>);
#[derive(Default)]
pub struct EmbeddedAssets(HashMap<AssetKey, (PathBuf, PathBuf)>);
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<Asset, EmbeddedAssetsError> {
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::<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,
})?;
// 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)
},));
}

View File

@@ -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"]

View File

@@ -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'"
}
}
}
}

View File

@@ -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"]

View File

@@ -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"]

View File

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

View File

@@ -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(),

View File

@@ -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 }

View File

@@ -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" ]

View File

@@ -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)
}
}