From b32295de1804bc02ab34d60a7c9b688428149f91 Mon Sep 17 00:00:00 2001 From: chip Date: Fri, 2 Aug 2024 19:30:02 +0900 Subject: [PATCH] prevent unnecessary rebuilds when working in the cargo workspace (#10442) * hash codegen image cache output * remove left over dbg! statement * prevent info.plist from workspace rebuilds * prevent schema generation from workspace rebuilds * use new `Cached` struct in `CachedIcon` * fmt * use full import for cached plist * use `to_vec()` for raw icons --- core/tauri-acl-schema/build.rs | 44 ++-- core/tauri-codegen/src/context.rs | 61 +++--- core/tauri-codegen/src/embedded_assets.rs | 17 +- core/tauri-codegen/src/image.rs | 232 ++++++++++------------ core/tauri-codegen/src/lib.rs | 59 +++++- core/tauri-config-schema/build.rs | 22 +- core/tauri-macros/src/lib.rs | 13 +- core/tauri-utils/src/lib.rs | 17 ++ 8 files changed, 243 insertions(+), 222 deletions(-) diff --git a/core/tauri-acl-schema/build.rs b/core/tauri-acl-schema/build.rs index e5f40f80b..8a2f3d1bb 100644 --- a/core/tauri-acl-schema/build.rs +++ b/core/tauri-acl-schema/build.rs @@ -2,33 +2,33 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT -use std::{ - error::Error, - fs::File, - io::{BufWriter, Write}, - path::PathBuf, +use std::{error::Error, path::PathBuf}; + +use schemars::schema_for; +use tauri_utils::{ + acl::capability::Capability, + acl::{Permission, Scopes}, + write_if_changed, }; -use schemars::schema::RootSchema; +macro_rules! schema { + ($name:literal, $path:ty) => { + (concat!($name, "-schema.json"), schema_for!($path)) + }; +} pub fn main() -> Result<(), Box> { - let cap_schema = schemars::schema_for!(tauri_utils::acl::capability::Capability); - let perm_schema = schemars::schema_for!(tauri_utils::acl::Permission); - let scope_schema = schemars::schema_for!(tauri_utils::acl::Scopes); + let schemas = [ + schema!("capability", Capability), + schema!("permission", Permission), + schema!("scope", Scopes), + ]; - let crate_dir = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR")?); - - write_schema_file(cap_schema, crate_dir.join("capability-schema.json"))?; - write_schema_file(perm_schema, crate_dir.join("permission-schema.json"))?; - write_schema_file(scope_schema, crate_dir.join("scope-schema.json"))?; - - Ok(()) -} - -fn write_schema_file(schema: RootSchema, outpath: PathBuf) -> Result<(), Box> { - let schema_str = serde_json::to_string_pretty(&schema).unwrap(); - let mut schema_file = BufWriter::new(File::create(outpath)?); - write!(schema_file, "{schema_str}")?; + let out = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR")?); + for (filename, schema) in schemas { + let schema = serde_json::to_string_pretty(&schema)?; + write_if_changed(out.join(filename), schema)?; + } Ok(()) } diff --git a/core/tauri-codegen/src/context.rs b/core/tauri-codegen/src/context.rs index 3e6d4b782..fd06092f3 100644 --- a/core/tauri-codegen/src/context.rs +++ b/core/tauri-codegen/src/context.rs @@ -7,28 +7,28 @@ use std::convert::identity; use std::path::{Path, PathBuf}; use std::{ffi::OsStr, str::FromStr}; +use crate::{ + embedded_assets::{ + ensure_out_dir, AssetOptions, CspHashes, EmbeddedAssets, EmbeddedAssetsResult, + }, + image::CachedIcon, +}; use base64::Engine; use proc_macro2::TokenStream; use quote::quote; use sha2::{Digest, Sha256}; - use syn::Expr; -use tauri_utils::acl::capability::{Capability, CapabilityFile}; -use tauri_utils::acl::manifest::Manifest; -use tauri_utils::acl::resolved::Resolved; -use tauri_utils::assets::AssetKey; -use tauri_utils::config::{CapabilityEntry, Config, FrontendDist, PatternKind}; -use tauri_utils::html::{ - inject_nonce_token, parse as parse_html, serialize_node as serialize_html_node, NodeRef, +use tauri_utils::{ + acl::capability::{Capability, CapabilityFile}, + acl::manifest::Manifest, + acl::resolved::Resolved, + assets::AssetKey, + config::{CapabilityEntry, Config, FrontendDist, PatternKind}, + html::{inject_nonce_token, parse as parse_html, serialize_node as serialize_html_node, NodeRef}, + platform::Target, + plugin::GLOBAL_API_SCRIPT_FILE_LIST_PATH, + tokens::{map_lit, str_lit}, }; -use tauri_utils::platform::Target; -use tauri_utils::plugin::GLOBAL_API_SCRIPT_FILE_LIST_PATH; -use tauri_utils::tokens::{map_lit, str_lit}; - -use crate::embedded_assets::{ - ensure_out_dir, AssetOptions, CspHashes, EmbeddedAssets, EmbeddedAssetsResult, -}; -use crate::image::{ico_icon, image_icon, png_icon, raw_icon}; const ACL_MANIFESTS_FILE_NAME: &str = "acl-manifests.json"; const CAPABILITIES_FILE_NAME: &str = "capabilities.json"; @@ -221,8 +221,8 @@ pub fn context_codegen(data: ContextData) -> EmbeddedAssetsResult { "icons/icon.ico", ); if icon_path.exists() { - ico_icon(&root, &out_dir, &icon_path, "default-window-icon.png") - .map(|i| quote!(::std::option::Option::Some(#i)))? + let icon = CachedIcon::new(&root, &icon_path)?; + quote!(::std::option::Option::Some(#icon)) } else { let icon_path = find_icon( &config, @@ -230,8 +230,8 @@ pub fn context_codegen(data: ContextData) -> EmbeddedAssetsResult { |i| i.ends_with(".png"), "icons/icon.png", ); - png_icon(&root, &out_dir, &icon_path, "default-window-icon.png") - .map(|i| quote!(::std::option::Option::Some(#i)))? + let icon = CachedIcon::new(&root, &icon_path)?; + quote!(::std::option::Option::Some(#icon)) } } else { // handle default window icons for Unix targets @@ -241,8 +241,8 @@ pub fn context_codegen(data: ContextData) -> EmbeddedAssetsResult { |i| i.ends_with(".png"), "icons/icon.png", ); - png_icon(&root, &out_dir, &icon_path, "default-window-icon.png") - .map(|i| quote!(::std::option::Option::Some(#i)))? + let icon = CachedIcon::new(&root, &icon_path)?; + quote!(::std::option::Option::Some(#icon)) } }; @@ -261,7 +261,9 @@ pub fn context_codegen(data: ContextData) -> EmbeddedAssetsResult { "icons/icon.png", ); } - raw_icon(&out_dir, &icon_path, "dev-macos-icon.png")? + + let icon = CachedIcon::new_raw(&root, &icon_path)?; + quote!(::std::option::Option::Some(#icon.to_vec())) } else { quote!(::std::option::Option::None) }; @@ -290,8 +292,8 @@ pub fn context_codegen(data: ContextData) -> EmbeddedAssetsResult { let with_tray_icon_code = if target.is_desktop() { if let Some(tray) = &config.app.tray_icon { let tray_icon_icon_path = config_parent.join(&tray.icon_path); - image_icon(&root, &out_dir, &tray_icon_icon_path, "tray-icon") - .map(|i| quote!(context.set_tray_icon(Some(#i));))? + let icon = CachedIcon::new(&root, &tray_icon_icon_path)?; + quote!(context.set_tray_icon(::std::option::Option::Some(#icon));) } else { quote!() } @@ -319,8 +321,6 @@ pub fn context_codegen(data: ContextData) -> EmbeddedAssetsResult { } } - let plist_file = out_dir.join("Info.plist"); - let mut plist_contents = std::io::BufWriter::new(Vec::new()); info_plist .to_writer_xml(&mut plist_contents) @@ -328,12 +328,9 @@ pub fn context_codegen(data: ContextData) -> EmbeddedAssetsResult { let plist_contents = String::from_utf8_lossy(&plist_contents.into_inner().unwrap()).into_owned(); - if plist_contents != std::fs::read_to_string(&plist_file).unwrap_or_default() { - std::fs::write(&plist_file, &plist_contents).expect("failed to write Info.plist"); - } - + let plist = crate::Cached::try_from(plist_contents)?; quote!({ - tauri::embed_plist::embed_info_plist!(concat!(std::env!("OUT_DIR"), "/Info.plist")); + tauri::embed_plist::embed_info_plist!(#plist); }) } else { quote!(()) diff --git a/core/tauri-codegen/src/embedded_assets.rs b/core/tauri-codegen/src/embedded_assets.rs index 868635ae2..20b00a105 100644 --- a/core/tauri-codegen/src/embedded_assets.rs +++ b/core/tauri-codegen/src/embedded_assets.rs @@ -8,7 +8,6 @@ use quote::{quote, ToTokens, TokenStreamExt}; use sha2::{Digest, Sha256}; use std::{ collections::HashMap, - fmt::Write, fs::File, path::{Path, PathBuf}, }; @@ -48,7 +47,7 @@ pub enum EmbeddedAssetsError { #[error("invalid prefix {prefix} used while including path {path}")] PrefixInvalid { prefix: PathBuf, path: PathBuf }, - #[error("invalid extension {extension} used for image {path}, must be `ico` or `png`")] + #[error("invalid extension `{extension}` used for image {path}, must be `ico` or `png`")] InvalidImageExtension { extension: PathBuf, path: PathBuf }, #[error("failed to walk directory {path} because {error}")] @@ -341,19 +340,7 @@ impl EmbeddedAssets { 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 = crate::vendor::blake3_reference::Hasher::default(); - hasher.update(&input); - - let mut bytes = [0u8; 32]; - hasher.finalize(&mut bytes); - - let mut hex = String::with_capacity(2 * bytes.len()); - for b in bytes { - write!(hex, "{b:02x}").map_err(EmbeddedAssetsError::Hex)?; - } - hex - }; + let hash = crate::checksum(&input).map_err(EmbeddedAssetsError::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()) { diff --git a/core/tauri-codegen/src/image.rs b/core/tauri-codegen/src/image.rs index 2718f334d..49546082a 100644 --- a/core/tauri-codegen/src/image.rs +++ b/core/tauri-codegen/src/image.rs @@ -2,141 +2,117 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT -use crate::embedded_assets::{ensure_out_dir, EmbeddedAssetsError, EmbeddedAssetsResult}; -use proc_macro2::{Span, TokenStream}; -use quote::{quote, ToTokens}; -use std::path::Path; -use syn::{punctuated::Punctuated, Ident, PathArguments, PathSegment, Token}; +use crate::{ + embedded_assets::{EmbeddedAssetsError, EmbeddedAssetsResult}, + Cached, +}; +use proc_macro2::TokenStream; +use quote::{quote, ToTokens, TokenStreamExt}; +use std::{ffi::OsStr, io::Cursor, path::Path}; -pub fn include_image_codegen( - path: &Path, - out_file_name: &str, -) -> EmbeddedAssetsResult { - let out_dir = ensure_out_dir()?; +/// The format the Icon is consumed as. +pub(crate) enum IconFormat { + /// The image, completely unmodified. + Raw, - let mut segments = Punctuated::new(); - segments.push(PathSegment { - ident: Ident::new("tauri", Span::call_site()), - arguments: PathArguments::None, - }); - let root = syn::Path { - leading_colon: Some(Token![::](Span::call_site())), - segments, - }; - - image_icon(&root.to_token_stream(), &out_dir, path, out_file_name) + /// RGBA raw data, meant to be consumed by [`tauri::image::Image`]. + Image { width: u32, height: u32 }, } -pub(crate) fn image_icon( - root: &TokenStream, - out_dir: &Path, - path: &Path, - out_file_name: &str, -) -> EmbeddedAssetsResult { - let extension = path.extension().unwrap_or_default(); - if extension == "ico" { - ico_icon(root, out_dir, path, out_file_name) - } else if extension == "png" { - png_icon(root, out_dir, path, out_file_name) - } else { - Err(EmbeddedAssetsError::InvalidImageExtension { - extension: extension.into(), - path: path.to_path_buf(), - }) - } +pub struct CachedIcon { + cache: Cached, + format: IconFormat, + root: TokenStream, } -pub(crate) fn raw_icon( - out_dir: &Path, - path: &Path, - out_file_name: &str, -) -> EmbeddedAssetsResult { - let bytes = - std::fs::read(path).unwrap_or_else(|e| panic!("failed to read icon {}: {}", path.display(), e)); - - let out_path = out_dir.join(out_file_name); - write_if_changed(&out_path, &bytes).map_err(|error| EmbeddedAssetsError::AssetWrite { - path: path.to_owned(), - error, - })?; - - let icon = quote!(::std::option::Option::Some( - include_bytes!(concat!(std::env!("OUT_DIR"), "/", #out_file_name)).to_vec() - )); - Ok(icon) -} - -pub(crate) fn ico_icon( - root: &TokenStream, - out_dir: &Path, - path: &Path, - out_file_name: &str, -) -> EmbeddedAssetsResult { - let file = std::fs::File::open(path) - .unwrap_or_else(|e| panic!("failed to open icon {}: {}", path.display(), e)); - let icon_dir = ico::IconDir::read(file) - .unwrap_or_else(|e| panic!("failed to parse icon {}: {}", path.display(), e)); - let entry = &icon_dir.entries()[0]; - let rgba = entry - .decode() - .unwrap_or_else(|e| panic!("failed to decode icon {}: {}", path.display(), e)) - .rgba_data() - .to_vec(); - let width = entry.width(); - let height = entry.height(); - - let out_path = out_dir.join(out_file_name); - write_if_changed(&out_path, &rgba).map_err(|error| EmbeddedAssetsError::AssetWrite { - path: path.to_owned(), - error, - })?; - - let icon = quote!(#root::image::Image::new(include_bytes!(concat!(std::env!("OUT_DIR"), "/", #out_file_name)), #width, #height)); - Ok(icon) -} - -pub(crate) fn png_icon( - root: &TokenStream, - out_dir: &Path, - path: &Path, - out_file_name: &str, -) -> EmbeddedAssetsResult { - let file = std::fs::File::open(path) - .unwrap_or_else(|e| panic!("failed to open icon {}: {}", path.display(), e)); - let decoder = png::Decoder::new(file); - let mut reader = decoder - .read_info() - .unwrap_or_else(|e| panic!("failed to read icon {}: {}", path.display(), e)); - - let (color_type, _) = reader.output_color_type(); - - if color_type != png::ColorType::Rgba { - panic!("icon {} is not RGBA", path.display()); - } - - let mut buffer: Vec = Vec::new(); - while let Ok(Some(row)) = reader.next_row() { - buffer.extend(row.data()); - } - let width = reader.info().width; - let height = reader.info().height; - - let out_path = out_dir.join(out_file_name); - write_if_changed(&out_path, &buffer).map_err(|error| EmbeddedAssetsError::AssetWrite { - path: path.to_owned(), - error, - })?; - - let icon = quote!(#root::image::Image::new(include_bytes!(concat!(std::env!("OUT_DIR"), "/", #out_file_name)), #width, #height)); - Ok(icon) -} - -fn write_if_changed(out_path: &Path, data: &[u8]) -> std::io::Result<()> { - if let Ok(curr) = std::fs::read(out_path) { - if curr == data { - return Ok(()); +impl CachedIcon { + pub fn new(root: &TokenStream, icon: &Path) -> EmbeddedAssetsResult { + match icon.extension().map(OsStr::to_string_lossy).as_deref() { + Some("png") => Self::new_png(root, icon), + Some("ico") => Self::new_ico(root, icon), + unknown => Err(EmbeddedAssetsError::InvalidImageExtension { + extension: unknown.unwrap_or_default().into(), + path: icon.to_path_buf(), + }), } } - std::fs::write(out_path, data) + /// Cache the icon without any manipulation. + pub fn new_raw(root: &TokenStream, icon: &Path) -> EmbeddedAssetsResult { + let buf = Self::open(icon); + Cached::try_from(buf).map(|cache| Self { + cache, + root: root.clone(), + format: IconFormat::Raw, + }) + } + + /// Cache an ICO icon as RGBA data, see [`ImageFormat::Image`]. + pub fn new_ico(root: &TokenStream, icon: &Path) -> EmbeddedAssetsResult { + let buf = Self::open(icon); + + let icon_dir = ico::IconDir::read(Cursor::new(&buf)) + .unwrap_or_else(|e| panic!("failed to parse icon {}: {}", icon.display(), e)); + + let entry = &icon_dir.entries()[0]; + let rgba = entry + .decode() + .unwrap_or_else(|e| panic!("failed to decode icon {}: {}", icon.display(), e)) + .rgba_data() + .to_vec(); + + Cached::try_from(rgba).map(|cache| Self { + cache, + root: root.clone(), + format: IconFormat::Image { + width: entry.width(), + height: entry.height(), + }, + }) + } + + /// Cache a PNG icon as RGBA data, see [`ImageFormat::Image`]. + pub fn new_png(root: &TokenStream, icon: &Path) -> EmbeddedAssetsResult { + let buf = Self::open(icon); + let decoder = png::Decoder::new(Cursor::new(&buf)); + let mut reader = decoder + .read_info() + .unwrap_or_else(|e| panic!("failed to read icon {}: {}", icon.display(), e)); + + if reader.output_color_type().0 != png::ColorType::Rgba { + panic!("icon {} is not RGBA", icon.display()); + } + + let mut rgba = Vec::with_capacity(reader.output_buffer_size()); + while let Ok(Some(row)) = reader.next_row() { + rgba.extend(row.data()); + } + + Cached::try_from(rgba).map(|cache| Self { + cache, + root: root.clone(), + format: IconFormat::Image { + width: reader.info().width, + height: reader.info().height, + }, + }) + } + + fn open(path: &Path) -> Vec { + std::fs::read(path).unwrap_or_else(|e| panic!("failed to open icon {}: {}", path.display(), e)) + } +} + +impl ToTokens for CachedIcon { + fn to_tokens(&self, tokens: &mut TokenStream) { + let root = &self.root; + let cache = &self.cache; + let raw = quote!(::std::include_bytes!(#cache)); + tokens.append_all(match self.format { + IconFormat::Raw => raw, + IconFormat::Image { width, height } => { + quote!(#root::image::Image::new(#raw, #width, #height)) + } + }) + } } diff --git a/core/tauri-codegen/src/lib.rs b/core/tauri-codegen/src/lib.rs index 48a3033fd..67bff21ef 100644 --- a/core/tauri-codegen/src/lib.rs +++ b/core/tauri-codegen/src/lib.rs @@ -13,17 +13,21 @@ )] pub use self::context::{context_codegen, ContextData}; -pub use self::image::include_image_codegen; +use crate::embedded_assets::{ensure_out_dir, EmbeddedAssetsError}; +use proc_macro2::TokenStream; +use quote::{quote, ToTokens, TokenStreamExt}; use std::{ borrow::Cow, + fmt::{self, Write}, path::{Path, PathBuf}, }; pub use tauri_utils::config::{parse::ConfigError, Config}; use tauri_utils::platform::Target; +use tauri_utils::write_if_changed; mod context; pub mod embedded_assets; -mod image; +pub mod image; #[doc(hidden)] pub mod vendor; @@ -97,3 +101,54 @@ pub fn get_config(path: &Path) -> Result<(Config, PathBuf), CodegenConfigError> Ok((config, parent)) } + +/// Create a blake3 checksum of the passed bytes. +fn checksum(bytes: &[u8]) -> Result { + let mut hasher = vendor::blake3_reference::Hasher::default(); + hasher.update(bytes); + + let mut bytes = [0u8; 32]; + hasher.finalize(&mut bytes); + + let mut hex = String::with_capacity(2 * bytes.len()); + for b in bytes { + write!(hex, "{b:02x}")?; + } + Ok(hex) +} + +/// Cache the data to `$OUT_DIR`, only if it does not already exist. +/// +/// Due to using a checksum as the filename, an existing file should be the exact same content +/// as the data being checked. +struct Cached { + checksum: String, +} + +impl TryFrom for Cached { + type Error = EmbeddedAssetsError; + + fn try_from(value: String) -> Result { + Self::try_from(Vec::from(value)) + } +} + +impl TryFrom> for Cached { + type Error = EmbeddedAssetsError; + + fn try_from(content: Vec) -> Result { + let checksum = checksum(content.as_ref()).map_err(EmbeddedAssetsError::Hex)?; + let path = ensure_out_dir()?.join(&checksum); + + write_if_changed(&path, &content) + .map(|_| Self { checksum }) + .map_err(|error| EmbeddedAssetsError::AssetWrite { path, error }) + } +} + +impl ToTokens for Cached { + fn to_tokens(&self, tokens: &mut TokenStream) { + let path = &self.checksum; + tokens.append_all(quote!(::std::concat!(::std::env!("OUT_DIR"), "/", #path))) + } +} diff --git a/core/tauri-config-schema/build.rs b/core/tauri-config-schema/build.rs index cfb52844c..d9afa67a9 100644 --- a/core/tauri-config-schema/build.rs +++ b/core/tauri-config-schema/build.rs @@ -2,23 +2,15 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT -use std::{ - error::Error, - fs::File, - io::{BufWriter, Write}, - path::PathBuf, -}; +use std::{error::Error, path::PathBuf}; +use tauri_utils::{config::Config, write_if_changed}; pub fn main() -> Result<(), Box> { - let schema = schemars::schema_for!(tauri_utils::config::Config); - let schema_str = serde_json::to_string_pretty(&schema).unwrap(); - let crate_dir = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR")?); - for file in [ - crate_dir.join("schema.json"), - crate_dir.join("../../tooling/cli/schema.json"), - ] { - let mut schema_file = BufWriter::new(File::create(file)?); - write!(schema_file, "{schema_str}")?; + let schema = schemars::schema_for!(Config); + let schema = serde_json::to_string_pretty(&schema)?; + let out = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR")?); + for path in ["schema.json", "../../tooling/cli/schema.json"] { + write_if_changed(out.join(path), &schema)?; } Ok(()) diff --git a/core/tauri-macros/src/lib.rs b/core/tauri-macros/src/lib.rs index d7ccab5a4..89e5b83fe 100644 --- a/core/tauri-macros/src/lib.rs +++ b/core/tauri-macros/src/lib.rs @@ -15,8 +15,9 @@ use std::path::PathBuf; use crate::context::ContextItems; use proc_macro::TokenStream; -use quote::quote; +use quote::{quote, ToTokens}; use syn::{parse2, parse_macro_input, LitStr}; +use tauri_codegen::image::CachedIcon; mod command; mod menu; @@ -203,13 +204,9 @@ pub fn include_image(tokens: TokenStream) -> TokenStream { ); return quote!(compile_error!(#error_string)).into(); } - match tauri_codegen::include_image_codegen( - &resolved_path, - resolved_path.file_name().unwrap().to_str().unwrap(), - ) - .map_err(|error| error.to_string()) - { - Ok(output) => output, + + match CachedIcon::new("e!(::tauri), &resolved_path).map_err(|error| error.to_string()) { + Ok(icon) => icon.into_token_stream(), Err(error) => quote!(compile_error!(#error)), } .into() diff --git a/core/tauri-utils/src/lib.rs b/core/tauri-utils/src/lib.rs index e6cc76738..08434064b 100644 --- a/core/tauri-utils/src/lib.rs +++ b/core/tauri-utils/src/lib.rs @@ -380,3 +380,20 @@ pub fn display_path>(p: P) -> String { .display() .to_string() } + +/// Write the file only if the content of the existing file (if any) is different. +/// +/// This will always write unless the file exists with identical content. +pub fn write_if_changed(path: P, content: C) -> std::io::Result<()> +where + P: AsRef, + C: AsRef<[u8]>, +{ + if let Ok(existing) = std::fs::read(&path) { + if existing == content.as_ref() { + return Ok(()); + } + } + + std::fs::write(path, content) +}