diff --git a/.changes/exit-with-code.md b/.changes/exit-with-code.md new file mode 100644 index 000000000000..dc405b21021a --- /dev/null +++ b/.changes/exit-with-code.md @@ -0,0 +1,6 @@ +--- +'tauri': 'patch:bug' +'tauri-runtime-wry': 'patch:bug' +--- + +Transfer the exit code from the `window.app_handle().exit(1)` call to the `run_return()` result instead of always returning 0. diff --git a/.changes/fix-deterministic-config-serialization.md b/.changes/fix-deterministic-config-serialization.md new file mode 100644 index 000000000000..7363bfa85909 --- /dev/null +++ b/.changes/fix-deterministic-config-serialization.md @@ -0,0 +1,5 @@ +--- +'tauri-utils': 'patch:bug' +--- + +Serialize the CSP directive map, header source maps and plugin config with sorted keys so writing the processed config (e.g. the `tauri.conf.json` embedded in Android/iOS projects) is deterministic across builds. diff --git a/.changes/fix-deterministic-embedded-assets-codegen.md b/.changes/fix-deterministic-embedded-assets-codegen.md new file mode 100644 index 000000000000..d66d3d6d103c --- /dev/null +++ b/.changes/fix-deterministic-embedded-assets-codegen.md @@ -0,0 +1,5 @@ +--- +'tauri-codegen': 'patch:bug' +--- + +Emit embedded assets and CSP script/style hashes in sorted order so `generate_context!` output no longer depends on the filesystem walk order, which varies across machines and broke reproducible builds. diff --git a/Cargo.lock b/Cargo.lock index d8a741ef02af..c2e7d5cc1c9f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3174,9 +3174,9 @@ dependencies = [ [[package]] name = "glob" -version = "0.3.2" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" +checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" [[package]] name = "globset" diff --git a/crates/tauri-codegen/src/embedded_assets.rs b/crates/tauri-codegen/src/embedded_assets.rs index f3d1bd2757cb..d3763ef8d3d7 100644 --- a/crates/tauri-codegen/src/embedded_assets.rs +++ b/crates/tauri-codegen/src/embedded_assets.rs @@ -7,7 +7,7 @@ use proc_macro2::TokenStream; use quote::{quote, ToTokens, TokenStreamExt}; use sha2::{Digest, Sha256}; use std::{ - collections::HashMap, + collections::BTreeMap, fs::File, path::{Path, PathBuf}, }; @@ -76,7 +76,7 @@ pub type EmbeddedAssetsResult = Result; /// the compressed assets in that application's binary. #[derive(Default)] pub struct EmbeddedAssets { - assets: HashMap, + assets: BTreeMap, csp_hashes: CspHashes, } @@ -158,7 +158,7 @@ pub struct CspHashes { /// Scripts that are part of the asset collection (JS or MJS files). pub(crate) scripts: Vec, /// Inline scripts (``). Maps a HTML path to a list of hashes. - pub(crate) inline_scripts: HashMap>, + pub(crate) inline_scripts: BTreeMap>, /// A list of hashes of the contents of all `style` elements. pub(crate) styles: Vec, } @@ -266,13 +266,13 @@ impl EmbeddedAssets { struct CompressState { csp_hashes: CspHashes, - assets: HashMap, + assets: BTreeMap, } let CompressState { assets, csp_hashes } = paths.into_iter().try_fold( CompressState { csp_hashes, - assets: HashMap::new(), + assets: BTreeMap::new(), }, move |mut state, (prefix, entry)| { let (key, asset) = @@ -302,7 +302,7 @@ impl EmbeddedAssets { settings } - /// Compress a file and spit out the information in a [`HashMap`] friendly form. + /// Compress a file and spit out the information in a [`BTreeMap`] friendly form. fn compress_file( prefix: &Path, path: &Path, @@ -404,12 +404,18 @@ impl ToTokens for EmbeddedAssets { } let mut global_hashes = TokenStream::new(); - for script_hash in &self.csp_hashes.scripts { + // Sort the hashes so the generated code does not depend on the filesystem + // walk order the assets were collected in, which varies across machines + let mut script_hashes: Vec<_> = self.csp_hashes.scripts.iter().collect(); + script_hashes.sort(); + for script_hash in script_hashes { let hash = script_hash.as_str(); global_hashes.append_all(quote!(CspHash::Script(#hash),)); } - for style_hash in &self.csp_hashes.styles { + let mut style_hashes: Vec<_> = self.csp_hashes.styles.iter().collect(); + style_hashes.sort(); + for style_hash in style_hashes { let hash = style_hash.as_str(); global_hashes.append_all(quote!(CspHash::Style(#hash),)); } diff --git a/crates/tauri-runtime-wry/src/lib.rs b/crates/tauri-runtime-wry/src/lib.rs index d6a1c6f9a40a..f92fd48e28e1 100644 --- a/crates/tauri-runtime-wry/src/lib.rs +++ b/crates/tauri-runtime-wry/src/lib.rs @@ -4304,7 +4304,7 @@ fn handle_event_loop( let should_prevent = matches!(recv, Ok(ExitRequestedEventAction::Prevent)); if !should_prevent { - *control_flow = ControlFlow::Exit; + *control_flow = ControlFlow::ExitWithCode(code); } } Message::Window(id, WindowMessage::Close) => { diff --git a/crates/tauri-utils/src/config.rs b/crates/tauri-utils/src/config.rs index 357e63d07eec..57fd3dcd3a54 100644 --- a/crates/tauri-utils/src/config.rs +++ b/crates/tauri-utils/src/config.rs @@ -39,7 +39,7 @@ use serde_with::skip_serializing_none; use url::Url; use std::{ - collections::{HashMap, HashSet}, + collections::{BTreeMap, HashMap, HashSet}, fmt::{self, Display}, fs::read_to_string, path::PathBuf, @@ -2521,7 +2521,7 @@ impl CspDirectiveSources { /// A Content-Security-Policy definition. /// See . -#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] +#[derive(Debug, PartialEq, Eq, Clone, Deserialize)] #[cfg_attr(feature = "schema", derive(JsonSchema))] #[serde(rename_all = "camelCase", untagged)] pub enum Csp { @@ -2531,6 +2531,24 @@ pub enum Csp { DirectiveMap(HashMap), } +impl Serialize for Csp { + fn serialize(&self, serializer: S) -> std::result::Result + where + S: Serializer, + { + match self { + Self::Policy(policy) => serializer.serialize_str(policy), + Self::DirectiveMap(map) => { + // Serialize through `BTreeMap` so the output is deterministic + // see: https://github.com/tauri-apps/tauri/issues/14978 + // TODO: Remove this in v3, use a BTreeMap instead of a HashMap + let btree_map: BTreeMap<_, _> = map.iter().collect(); + btree_map.serialize(serializer) + } + } + } +} + impl From> for Csp { fn from(map: HashMap) -> Self { Self::DirectiveMap(map) @@ -2684,7 +2702,7 @@ pub struct AssetProtocolConfig { /// definition of a header source /// /// The header value to a header name -#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] +#[derive(Debug, PartialEq, Eq, Clone, Deserialize)] #[cfg_attr(feature = "schema", derive(JsonSchema))] #[serde(rename_all = "camelCase", untagged)] pub enum HeaderSource { @@ -2696,6 +2714,25 @@ pub enum HeaderSource { Map(HashMap), } +impl Serialize for HeaderSource { + fn serialize(&self, serializer: S) -> std::result::Result + where + S: Serializer, + { + match self { + Self::Inline(s) => serializer.serialize_str(s), + Self::List(l) => l.serialize(serializer), + Self::Map(m) => { + // Serialize through `BTreeMap` so the output is deterministic + // see: https://github.com/tauri-apps/tauri/issues/14978 + // TODO: Remove this in v3, use a BTreeMap instead of a HashMap + let btree_map: BTreeMap<_, _> = m.iter().collect(); + btree_map.serialize(serializer) + } + } + } +} + impl Display for HeaderSource { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { @@ -3765,10 +3802,23 @@ pub struct Config { /// The plugin configs holds a HashMap mapping a plugin name to its configuration object. /// /// See more: -#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize, Serialize)] +#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize)] #[cfg_attr(feature = "schema", derive(JsonSchema))] pub struct PluginConfig(pub HashMap); +impl Serialize for PluginConfig { + fn serialize(&self, serializer: S) -> std::result::Result + where + S: Serializer, + { + // Serialize through `BTreeMap` so the output is deterministic + // see: https://github.com/tauri-apps/tauri/issues/14978 + // TODO: Remove this in v3, use a BTreeMap instead of a HashMap + let btree_map: BTreeMap<_, _> = self.0.iter().collect(); + btree_map.serialize(serializer) + } +} + /// Implement `ToTokens` for all config structs, allowing a literal `Config` to be built. /// /// This allows for a build script to output the values in a `Config` to a `TokenStream`, which can diff --git a/crates/tests/acl/fixtures/snapshots/acl_tests__tests__basic-ping.snap b/crates/tests/acl/fixtures/snapshots/acl_tests__tests__basic-ping.snap index 9d385d122283..3102d70dca71 100644 --- a/crates/tests/acl/fixtures/snapshots/acl_tests__tests__basic-ping.snap +++ b/crates/tests/acl/fixtures/snapshots/acl_tests__tests__basic-ping.snap @@ -26,6 +26,7 @@ Resolved { ), ], is_recursive: false, + has_metachars: false, }, ], webviews: [], diff --git a/crates/tests/acl/fixtures/snapshots/acl_tests__tests__file-explorer-remote.snap b/crates/tests/acl/fixtures/snapshots/acl_tests__tests__file-explorer-remote.snap index 9eba3e347416..6ef4943406ed 100644 --- a/crates/tests/acl/fixtures/snapshots/acl_tests__tests__file-explorer-remote.snap +++ b/crates/tests/acl/fixtures/snapshots/acl_tests__tests__file-explorer-remote.snap @@ -191,6 +191,7 @@ Resolved { ), ], is_recursive: false, + has_metachars: false, }, ], webviews: [], @@ -383,6 +384,7 @@ Resolved { ), ], is_recursive: false, + has_metachars: false, }, ], webviews: [], diff --git a/crates/tests/acl/fixtures/snapshots/acl_tests__tests__file-explorer.snap b/crates/tests/acl/fixtures/snapshots/acl_tests__tests__file-explorer.snap index 03f795b43793..ac4b3921dd4b 100644 --- a/crates/tests/acl/fixtures/snapshots/acl_tests__tests__file-explorer.snap +++ b/crates/tests/acl/fixtures/snapshots/acl_tests__tests__file-explorer.snap @@ -26,6 +26,7 @@ Resolved { ), ], is_recursive: false, + has_metachars: false, }, ], webviews: [], @@ -53,6 +54,7 @@ Resolved { ), ], is_recursive: false, + has_metachars: false, }, ], webviews: [], diff --git a/crates/tests/acl/fixtures/snapshots/acl_tests__tests__multiwebview.snap b/crates/tests/acl/fixtures/snapshots/acl_tests__tests__multiwebview.snap index 772b8f7d6a0b..364a0e3c254c 100644 --- a/crates/tests/acl/fixtures/snapshots/acl_tests__tests__multiwebview.snap +++ b/crates/tests/acl/fixtures/snapshots/acl_tests__tests__multiwebview.snap @@ -26,6 +26,7 @@ Resolved { ), ], is_recursive: false, + has_metachars: false, }, ], webviews: [ @@ -52,6 +53,7 @@ Resolved { ), ], is_recursive: false, + has_metachars: false, }, Pattern { original: "child2", @@ -76,6 +78,7 @@ Resolved { ), ], is_recursive: false, + has_metachars: false, }, ], scope_id: None, diff --git a/crates/tests/acl/fixtures/snapshots/acl_tests__tests__multiwindow.snap b/crates/tests/acl/fixtures/snapshots/acl_tests__tests__multiwindow.snap index 95ba3490dccf..b6f66ad4c3f0 100644 --- a/crates/tests/acl/fixtures/snapshots/acl_tests__tests__multiwindow.snap +++ b/crates/tests/acl/fixtures/snapshots/acl_tests__tests__multiwindow.snap @@ -26,6 +26,7 @@ Resolved { ), ], is_recursive: false, + has_metachars: false, }, ], webviews: [], @@ -55,6 +56,7 @@ Resolved { ), ], is_recursive: false, + has_metachars: false, }, ], webviews: [], @@ -82,6 +84,7 @@ Resolved { ), ], is_recursive: false, + has_metachars: false, }, ], webviews: [], @@ -109,6 +112,7 @@ Resolved { ), ], is_recursive: false, + has_metachars: false, }, ], webviews: [], @@ -146,6 +150,7 @@ Resolved { ), ], is_recursive: false, + has_metachars: false, }, ], webviews: [], @@ -173,6 +178,7 @@ Resolved { ), ], is_recursive: false, + has_metachars: false, }, ], webviews: [], @@ -200,6 +206,7 @@ Resolved { ), ], is_recursive: false, + has_metachars: false, }, ], webviews: [], @@ -239,6 +246,7 @@ Resolved { ), ], is_recursive: false, + has_metachars: false, }, ], webviews: [], diff --git a/crates/tests/acl/fixtures/snapshots/acl_tests__tests__scope-extended.snap b/crates/tests/acl/fixtures/snapshots/acl_tests__tests__scope-extended.snap index 7f1606fae65c..1b1db0240ebe 100644 --- a/crates/tests/acl/fixtures/snapshots/acl_tests__tests__scope-extended.snap +++ b/crates/tests/acl/fixtures/snapshots/acl_tests__tests__scope-extended.snap @@ -26,6 +26,7 @@ Resolved { ), ], is_recursive: false, + has_metachars: false, }, ], webviews: [], @@ -55,6 +56,7 @@ Resolved { ), ], is_recursive: false, + has_metachars: false, }, ], webviews: [], @@ -82,6 +84,7 @@ Resolved { ), ], is_recursive: false, + has_metachars: false, }, ], webviews: [], @@ -109,6 +112,7 @@ Resolved { ), ], is_recursive: false, + has_metachars: false, }, ], webviews: [], @@ -136,6 +140,7 @@ Resolved { ), ], is_recursive: false, + has_metachars: false, }, ], webviews: [], @@ -163,6 +168,7 @@ Resolved { ), ], is_recursive: false, + has_metachars: false, }, ], webviews: [], diff --git a/crates/tests/acl/fixtures/snapshots/acl_tests__tests__scope.snap b/crates/tests/acl/fixtures/snapshots/acl_tests__tests__scope.snap index b74d043814d3..e9ddb4b9d822 100644 --- a/crates/tests/acl/fixtures/snapshots/acl_tests__tests__scope.snap +++ b/crates/tests/acl/fixtures/snapshots/acl_tests__tests__scope.snap @@ -26,6 +26,7 @@ Resolved { ), ], is_recursive: false, + has_metachars: false, }, ], webviews: [], @@ -55,6 +56,7 @@ Resolved { ), ], is_recursive: false, + has_metachars: false, }, ], webviews: [], @@ -80,6 +82,7 @@ Resolved { ), ], is_recursive: false, + has_metachars: false, }, ], webviews: [], @@ -107,6 +110,7 @@ Resolved { ), ], is_recursive: false, + has_metachars: false, }, ], webviews: [], @@ -134,6 +138,7 @@ Resolved { ), ], is_recursive: false, + has_metachars: false, }, ], webviews: [], @@ -159,6 +164,7 @@ Resolved { ), ], is_recursive: false, + has_metachars: false, }, ], webviews: [], diff --git a/crates/tests/acl/fixtures/snapshots/linux/acl_tests__tests__platform-specific-permissions.snap b/crates/tests/acl/fixtures/snapshots/linux/acl_tests__tests__platform-specific-permissions.snap index 7fff64b5af9b..91d74b34fb34 100644 --- a/crates/tests/acl/fixtures/snapshots/linux/acl_tests__tests__platform-specific-permissions.snap +++ b/crates/tests/acl/fixtures/snapshots/linux/acl_tests__tests__platform-specific-permissions.snap @@ -26,6 +26,7 @@ Resolved { ), ], is_recursive: false, + has_metachars: false, }, ], webviews: [], @@ -53,6 +54,7 @@ Resolved { ), ], is_recursive: false, + has_metachars: false, }, ], webviews: [], diff --git a/crates/tests/acl/fixtures/snapshots/macOS/acl_tests__tests__platform-specific-permissions.snap b/crates/tests/acl/fixtures/snapshots/macOS/acl_tests__tests__platform-specific-permissions.snap index 73f393b23331..ecfe26d1e293 100644 --- a/crates/tests/acl/fixtures/snapshots/macOS/acl_tests__tests__platform-specific-permissions.snap +++ b/crates/tests/acl/fixtures/snapshots/macOS/acl_tests__tests__platform-specific-permissions.snap @@ -26,6 +26,7 @@ Resolved { ), ], is_recursive: false, + has_metachars: false, }, ], webviews: [], diff --git a/crates/tests/acl/fixtures/snapshots/windows/acl_tests__tests__platform-specific-permissions.snap b/crates/tests/acl/fixtures/snapshots/windows/acl_tests__tests__platform-specific-permissions.snap index 3c1197d8166d..aabb509b3aa9 100644 --- a/crates/tests/acl/fixtures/snapshots/windows/acl_tests__tests__platform-specific-permissions.snap +++ b/crates/tests/acl/fixtures/snapshots/windows/acl_tests__tests__platform-specific-permissions.snap @@ -26,6 +26,7 @@ Resolved { ), ], is_recursive: false, + has_metachars: false, }, ], webviews: [],