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
This commit is contained in:
chip 2024-08-02 19:30:02 +09:00 committed by GitHub
parent 6755af2302
commit b32295de18
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 243 additions and 222 deletions

View File

@ -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<dyn Error>> {
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<dyn Error>> {
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(())
}

View File

@ -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<TokenStream> {
"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<TokenStream> {
|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<TokenStream> {
|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<TokenStream> {
"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<TokenStream> {
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<TokenStream> {
}
}
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<TokenStream> {
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!(())

View File

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

View File

@ -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<TokenStream> {
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<TokenStream> {
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<TokenStream> {
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<TokenStream> {
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<TokenStream> {
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<u8> = 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<Self> {
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<Self> {
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<Self> {
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<Self> {
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<u8> {
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))
}
})
}
}

View File

@ -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<String, fmt::Error> {
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<String> for Cached {
type Error = EmbeddedAssetsError;
fn try_from(value: String) -> Result<Self, Self::Error> {
Self::try_from(Vec::from(value))
}
}
impl TryFrom<Vec<u8>> for Cached {
type Error = EmbeddedAssetsError;
fn try_from(content: Vec<u8>) -> Result<Self, Self::Error> {
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)))
}
}

View File

@ -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<dyn Error>> {
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(())

View File

@ -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(&quote!(::tauri), &resolved_path).map_err(|error| error.to_string()) {
Ok(icon) => icon.into_token_stream(),
Err(error) => quote!(compile_error!(#error)),
}
.into()

View File

@ -380,3 +380,20 @@ pub fn display_path<P: AsRef<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<P, C>(path: P, content: C) -> std::io::Result<()>
where
P: AsRef<Path>,
C: AsRef<[u8]>,
{
if let Ok(existing) = std::fs::read(&path) {
if existing == content.as_ref() {
return Ok(());
}
}
std::fs::write(path, content)
}