diff --git a/.changes/attohttpc-default-client.md b/.changes/attohttpc-default-client.md new file mode 100644 index 000000000..6a6204d99 --- /dev/null +++ b/.changes/attohttpc-default-client.md @@ -0,0 +1,5 @@ +--- +"tauri": patch +--- + +Use `attohttpc` on the HTTP API by default for bundle size optimization. `reqwest` is implemented behind the `reqwest-client` feature flag. diff --git a/.changes/core-features.md b/.changes/core-features.md new file mode 100644 index 000000000..7ce2353e4 --- /dev/null +++ b/.changes/core-features.md @@ -0,0 +1,5 @@ +--- +"cli.rs": patch +--- + +Properly keep all `tauri` features that are not managed by the CLI. diff --git a/core/tauri/Cargo.toml b/core/tauri/Cargo.toml index e3b97630a..b3687c463 100644 --- a/core/tauri/Cargo.toml +++ b/core/tauri/Cargo.toml @@ -27,6 +27,9 @@ targets = [ "x86_64-apple-darwin" ] +[package.metadata.cargo-udeps.ignore] +normal = ["attohttpc"] # we ignore attohttpc because we can't remove it based on `not(feature = "reqwest-client")` + [dependencies] serde_json = { version = "1.0", features = [ "raw_value" ] } serde = { version = "1.0", features = [ "derive" ] } @@ -41,7 +44,6 @@ tauri-macros = { version = "1.0.0-beta.1", path = "../tauri-macros" } tauri-utils = { version = "1.0.0-beta.0", path = "../tauri-utils" } tauri-runtime-wry = { version = "0.1.1", path = "../tauri-runtime-wry", optional = true } rand = "0.8" -reqwest = { version = "0.11", features = [ "json", "multipart" ] } tempfile = "3" semver = "0.11" serde_repr = "0.1" @@ -53,7 +55,6 @@ tar = "0.4" flate2 = "1.0" rfd = "0.3.0" tinyfiledialogs = "3.3" -bytes = { version = "1", features = [ "serde" ] } http = "0.2" clap = { version = "=3.0.0-beta.2", optional = true } notify-rust = { version = "4.5.0", optional = true } @@ -65,6 +66,11 @@ minisign-verify = "0.1.8" state = "0.4" bincode = "1.3" +# HTTP +reqwest = { version = "0.11", features = [ "json", "multipart" ], optional = true } +bytes = { version = "1", features = [ "serde" ], optional = true } +attohttpc = { version = "0.17", features = [ "json", "form" ] } + [build-dependencies] cfg_aliases = "0.1.1" @@ -85,9 +91,10 @@ wry = [ "tauri-runtime-wry" ] cli = [ "clap" ] custom-protocol = [ "tauri-macros/custom-protocol" ] api-all = [ "notification-all", "global-shortcut-all", "updater" ] -updater = [ "reqwest/default-tls" ] +updater = [ ] menu = [ "tauri-runtime/menu", "tauri-runtime-wry/menu" ] system-tray = [ "tauri-runtime/system-tray", "tauri-runtime-wry/system-tray" ] +reqwest-client = [ "reqwest", "bytes" ] fs-all = [ ] fs-read-text-file = [ ] fs-read-binary-file = [ ] diff --git a/core/tauri/src/api/error.rs b/core/tauri/src/api/error.rs index ba883b166..3bbdfcddc 100644 --- a/core/tauri/src/api/error.rs +++ b/core/tauri/src/api/error.rs @@ -25,6 +25,11 @@ pub enum Error { #[error("user cancelled the dialog")] DialogCancelled, /// The network error. + #[cfg(not(feature = "reqwest-client"))] + #[error("Network Error: {0}")] + Network(#[from] attohttpc::Error), + /// The network error. + #[cfg(feature = "reqwest-client")] #[error("Network Error: {0}")] Network(#[from] reqwest::Error), /// HTTP method error. @@ -32,10 +37,10 @@ pub enum Error { HttpMethod(#[from] http::method::InvalidMethod), /// Invalid HTTO header. #[error("{0}")] - HttpHeader(#[from] reqwest::header::InvalidHeaderName), + HttpHeader(#[from] http::header::InvalidHeaderName), /// Failed to serialize header value as string. #[error("failed to convert response header value to string")] - HttpHeaderToString(#[from] reqwest::header::ToStrError), + HttpHeaderToString(#[from] http::header::ToStrError), /// HTTP form to must be an object. #[error("http form must be an object")] InvalidHttpForm, diff --git a/core/tauri/src/api/http.rs b/core/tauri/src/api/http.rs index 780bc30a8..4eaa3d048 100644 --- a/core/tauri/src/api/http.rs +++ b/core/tauri/src/api/http.rs @@ -2,8 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT -use bytes::Bytes; -use reqwest::{header::HeaderName, redirect::Policy, Method}; +use http::{header::HeaderName, Method}; use serde::{Deserialize, Serialize}; use serde_json::Value; use serde_repr::{Deserialize_repr, Serialize_repr}; @@ -11,7 +10,7 @@ use serde_repr::{Deserialize_repr, Serialize_repr}; use std::{collections::HashMap, path::PathBuf, time::Duration}; /// Client builder. -#[derive(Default, Deserialize)] +#[derive(Clone, Default, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ClientBuilder { /// Max number of redirections to follow @@ -38,12 +37,19 @@ impl ClientBuilder { self } - /// Builds the ClientOptions. + /// Builds the Client. + #[cfg(not(feature = "reqwest-client"))] + pub fn build(self) -> crate::api::Result { + Ok(Client(self)) + } + + /// Builds the Client. + #[cfg(feature = "reqwest-client")] pub fn build(self) -> crate::api::Result { let mut client_builder = reqwest::Client::builder(); if let Some(max_redirections) = self.max_redirections { - client_builder = client_builder.redirect(Policy::limited(max_redirections)) + client_builder = client_builder.redirect(reqwest::redirect::Policy::limited(max_redirections)) } if let Some(connect_timeout) = self.connect_timeout { @@ -56,16 +62,80 @@ impl ClientBuilder { } /// The HTTP client. +#[cfg(feature = "reqwest-client")] #[derive(Clone)] pub struct Client(reqwest::Client); +/// The HTTP client. +#[cfg(not(feature = "reqwest-client"))] +#[derive(Clone)] +pub struct Client(ClientBuilder); + +#[cfg(not(feature = "reqwest-client"))] impl Client { /// Executes an HTTP request /// /// The response will be transformed to String, - /// If reading the response as binary, the byte array will be serialized using serde_json + /// If reading the response as binary, the byte array will be serialized using serde_json. pub async fn send(&self, request: HttpRequestBuilder) -> crate::api::Result { let method = Method::from_bytes(request.method.to_uppercase().as_bytes())?; + + let mut request_builder = attohttpc::RequestBuilder::try_new(method, &request.url)?; + + if let Some(query) = request.query { + request_builder = request_builder.params(&query); + } + + if let Some(headers) = request.headers { + for (header, header_value) in headers.iter() { + request_builder = + request_builder.header(HeaderName::from_bytes(header.as_bytes())?, header_value); + } + } + + if let Some(timeout) = request.timeout { + request_builder = request_builder.timeout(Duration::from_secs(timeout)); + } + + let response = if let Some(body) = request.body { + match body { + Body::Bytes(data) => request_builder.body(attohttpc::body::Bytes(data)).send()?, + Body::Text(text) => request_builder.body(attohttpc::body::Bytes(text)).send()?, + Body::Json(json) => request_builder.json(&json)?.send()?, + Body::Form(form_body) => { + let mut form = Vec::new(); + for (name, part) in form_body.0 { + match part { + FormPart::Bytes(bytes) => form.push((name, serde_json::to_string(&bytes)?)), + FormPart::File(file_path) => form.push((name, serde_json::to_string(&file_path)?)), + FormPart::Text(text) => form.push((name, text)), + } + } + request_builder.form(&form)?.send()? + } + } + } else { + request_builder.send()? + }; + + let response = response.error_for_status()?; + Ok(Response( + request.response_type.unwrap_or(ResponseType::Json), + response, + request.url, + )) + } +} + +#[cfg(feature = "reqwest-client")] +impl Client { + /// Executes an HTTP request + /// + /// The response will be transformed to String, + /// If reading the response as binary, the byte array will be serialized using serde_json. + pub async fn send(&self, request: HttpRequestBuilder) -> crate::api::Result { + let method = Method::from_bytes(request.method.to_uppercase().as_bytes())?; + let mut request_builder = self.0.request(method, &request.url); if let Some(query) = request.query { @@ -85,8 +155,18 @@ impl Client { let response = if let Some(body) = request.body { match body { - Body::Bytes(data) => request_builder.body(Bytes::from(data)).send().await?, - Body::Text(text) => request_builder.body(Bytes::from(text)).send().await?, + Body::Bytes(data) => { + request_builder + .body(bytes::Bytes::from(data)) + .send() + .await? + } + Body::Text(text) => { + request_builder + .body(bytes::Bytes::from(text)) + .send() + .await? + } Body::Json(json) => request_builder.json(&json).send().await?, Body::Form(form_body) => { let mut form = Vec::new(); @@ -249,24 +329,50 @@ impl HttpRequestBuilder { } /// The HTTP response. +#[cfg(feature = "reqwest-client")] pub struct Response(ResponseType, reqwest::Response); +/// The HTTP response. +#[cfg(not(feature = "reqwest-client"))] +pub struct Response(ResponseType, attohttpc::Response, String); impl Response { + /// Reads the response as raw bytes. + pub async fn bytes(self) -> crate::api::Result { + let status = self.1.status().as_u16(); + #[cfg(feature = "reqwest-client")] + let data = self.1.bytes().await?.to_vec(); + #[cfg(not(feature = "reqwest-client"))] + let data = self.1.bytes()?; + Ok(RawResponse { status, data }) + } + /// Reads the response and returns its info. pub async fn read(self) -> crate::api::Result { + #[cfg(feature = "reqwest-client")] let url = self.1.url().to_string(); + #[cfg(not(feature = "reqwest-client"))] + let url = self.2; + let mut headers = HashMap::new(); for (name, value) in self.1.headers() { headers.insert(name.as_str().to_string(), value.to_str()?.to_string()); } let status = self.1.status().as_u16(); + #[cfg(feature = "reqwest-client")] let data = match self.0 { ResponseType::Json => self.1.json().await?, ResponseType::Text => Value::String(self.1.text().await?), ResponseType::Binary => Value::String(serde_json::to_string(&self.1.bytes().await?)?), }; + #[cfg(not(feature = "reqwest-client"))] + let data = match self.0 { + ResponseType::Json => self.1.json()?, + ResponseType::Text => Value::String(self.1.text()?), + ResponseType::Binary => Value::String(serde_json::to_string(&self.1.bytes()?)?), + }; + Ok(ResponseData { url, status, @@ -276,12 +382,26 @@ impl Response { } } +/// A response with raw bytes. +#[non_exhaustive] +pub struct RawResponse { + /// Response status code. + pub status: u16, + /// Response bytes. + pub data: Vec, +} + /// The response type. #[derive(Serialize)] #[serde(rename_all = "camelCase")] +#[non_exhaustive] pub struct ResponseData { - url: String, - status: u16, - headers: HashMap, - data: Value, + /// Response URL. Useful if it followed redirects. + pub url: String, + /// Response status code. + pub status: u16, + /// Response headers. + pub headers: HashMap, + /// Response data. + pub data: Value, } diff --git a/core/tauri/src/updater/core.rs b/core/tauri/src/updater/core.rs index 71b82617b..633b69326 100644 --- a/core/tauri/src/updater/core.rs +++ b/core/tauri/src/updater/core.rs @@ -5,16 +5,17 @@ use super::error::{Error, Result}; use crate::api::{file::Extract, version}; use base64::decode; +use http::StatusCode; use minisign_verify::{PublicKey, Signature}; -use reqwest::{self, header, StatusCode}; use std::{ + collections::HashMap, env, ffi::OsStr, fs::{read_dir, remove_file, File, OpenOptions}, io::{prelude::*, BufReader, Read}, path::{Path, PathBuf}, str::from_utf8, - time::{Duration, SystemTime, UNIX_EPOCH}, + time::{SystemTime, UNIX_EPOCH}, }; #[cfg(not(target_os = "macos"))] @@ -23,6 +24,8 @@ use std::process::Command; #[cfg(target_os = "macos")] use crate::api::file::Move; +use crate::api::http::{ClientBuilder, HttpRequestBuilder}; + #[cfg(target_os = "windows")] use std::process::exit; @@ -271,31 +274,33 @@ impl<'a> UpdateBuilder<'a> { ); // we want JSON only - let mut headers = header::HeaderMap::new(); - headers.insert(header::ACCEPT, "application/json".parse().unwrap()); + let mut headers = HashMap::new(); + headers.insert("Accept".into(), "application/json".into()); - let resp = reqwest::Client::new() - .get(&fixed_link) - .headers(headers) - // wait 20sec for the firewall - .timeout(Duration::from_secs(20)) - .send() + let resp = ClientBuilder::new() + .build()? + .send( + HttpRequestBuilder::new("GET", &fixed_link) + .headers(headers) + // wait 20sec for the firewall + .timeout(20), + ) .await; // If we got a success, we stop the loop // and we set our remote_release variable - if let Ok(ref res) = resp { + if let Ok(res) = resp { + let res = res.read().await?; // got status code 2XX - if res.status().is_success() { + if StatusCode::from_u16(res.status).unwrap().is_success() { // if we got 204 - if StatusCode::NO_CONTENT == res.status() { + if StatusCode::NO_CONTENT.as_u16() == res.status { // return with `UpToDate` error // we should catch on the client return Err(Error::UpToDate); }; - let json = resp?.json::().await?; // Convert the remote result to our local struct - let built_release = RemoteRelease::from_release(&json, &target); + let built_release = RemoteRelease::from_release(&res.data, &target); // make sure all went well and the remote data is compatible // with what we need locally match built_release { @@ -411,35 +416,32 @@ impl Update { let mut tmp_archive = File::create(&tmp_archive_path)?; // set our headers - let mut headers = header::HeaderMap::new(); - headers.insert(header::ACCEPT, "application/octet-stream".parse().unwrap()); - - // make sure we have a valid agent - if !headers.contains_key(header::USER_AGENT) { - headers.insert( - header::USER_AGENT, - "tauri/updater".parse().expect("invalid user-agent"), - ); - } + let mut headers = HashMap::new(); + headers.insert("Accept".into(), "application/octet-stream".into()); + headers.insert("User-Agent".into(), "tauri/updater".into()); // Create our request - let resp = reqwest::Client::new() - .get(&url) - // wait 20sec for the firewall - .timeout(Duration::from_secs(20)) - .headers(headers) - .send() + let resp = ClientBuilder::new() + .build()? + .send( + HttpRequestBuilder::new("GET", &url) + .headers(headers) + // wait 20sec for the firewall + .timeout(20), + ) + .await? + .bytes() .await?; // make sure it's success - if !resp.status().is_success() { + if !StatusCode::from_u16(resp.status).unwrap().is_success() { return Err(Error::Network(format!( "Download request failed with status: {}", - resp.status() + resp.status ))); } - tmp_archive.write_all(&resp.bytes().await?)?; + tmp_archive.write_all(&resp.data)?; // Validate signature ONLY if pubkey is available in tauri.conf.json if let Some(pub_key) = pub_key { diff --git a/core/tauri/src/updater/error.rs b/core/tauri/src/updater/error.rs index 451191e6d..2438d0f03 100644 --- a/core/tauri/src/updater/error.rs +++ b/core/tauri/src/updater/error.rs @@ -11,9 +11,6 @@ pub enum Error { /// IO Errors. #[error("`{0}`")] Io(#[from] std::io::Error), - /// Reqwest Errors. - #[error("Request error: {0}")] - Reqwest(#[from] reqwest::Error), /// Semver Errors. #[error("Unable to compare version: {0}")] Semver(#[from] semver::SemVerError), diff --git a/examples/api/src-tauri/Cargo.toml b/examples/api/src-tauri/Cargo.toml index 2728379e3..ed63682ed 100644 --- a/examples/api/src-tauri/Cargo.toml +++ b/examples/api/src-tauri/Cargo.toml @@ -11,7 +11,7 @@ tauri-build = { path = "../../../core/tauri-build" } [dependencies] serde_json = "1.0" serde = { version = "1.0", features = [ "derive" ] } -tauri = { path = "../../../core/tauri", features = ["api-all", "cli", "updater", "system-tray", "menu"] } +tauri = { path = "../../../core/tauri", features = ["api-all", "cli", "menu", "system-tray", "updater"] } [features] default = [ "custom-protocol" ] diff --git a/tooling/cli.rs/config_definition.rs b/tooling/cli.rs/config_definition.rs index 64ad594c4..03573f511 100644 --- a/tooling/cli.rs/config_definition.rs +++ b/tooling/cli.rs/config_definition.rs @@ -300,7 +300,7 @@ pub struct SecurityConfig { pub csp: Option, } -trait Allowlist { +pub trait Allowlist { fn to_features(&self) -> Vec<&str>; } @@ -314,31 +314,31 @@ macro_rules! check_feature { #[derive(Debug, Default, PartialEq, Clone, Deserialize, Serialize, JsonSchema)] #[serde(rename_all = "camelCase", deny_unknown_fields)] -struct FsAllowlistConfig { +pub struct FsAllowlistConfig { #[serde(default)] - all: bool, + pub all: bool, #[serde(default)] - read_text_file: bool, + pub read_text_file: bool, #[serde(default)] - read_binary_file: bool, + pub read_binary_file: bool, #[serde(default)] - write_file: bool, + pub write_file: bool, #[serde(default)] - write_binary_file: bool, + pub write_binary_file: bool, #[serde(default)] - read_dir: bool, + pub read_dir: bool, #[serde(default)] - copy_file: bool, + pub copy_file: bool, #[serde(default)] - create_dir: bool, + pub create_dir: bool, #[serde(default)] - remove_dir: bool, + pub remove_dir: bool, #[serde(default)] - remove_file: bool, + pub remove_file: bool, #[serde(default)] - rename_file: bool, + pub rename_file: bool, #[serde(default)] - path: bool, + pub path: bool, } impl Allowlist for FsAllowlistConfig { @@ -365,11 +365,11 @@ impl Allowlist for FsAllowlistConfig { #[derive(Debug, Default, PartialEq, Clone, Deserialize, Serialize, JsonSchema)] #[serde(rename_all = "camelCase", deny_unknown_fields)] -struct WindowAllowlistConfig { +pub struct WindowAllowlistConfig { #[serde(default)] - all: bool, + pub all: bool, #[serde(default)] - create: bool, + pub create: bool, } impl Allowlist for WindowAllowlistConfig { @@ -386,13 +386,13 @@ impl Allowlist for WindowAllowlistConfig { #[derive(Debug, Default, PartialEq, Clone, Deserialize, Serialize, JsonSchema)] #[serde(rename_all = "camelCase", deny_unknown_fields)] -struct ShellAllowlistConfig { +pub struct ShellAllowlistConfig { #[serde(default)] - all: bool, + pub all: bool, #[serde(default)] - execute: bool, + pub execute: bool, #[serde(default)] - open: bool, + pub open: bool, } impl Allowlist for ShellAllowlistConfig { @@ -410,13 +410,13 @@ impl Allowlist for ShellAllowlistConfig { #[derive(Debug, Default, PartialEq, Clone, Deserialize, Serialize, JsonSchema)] #[serde(rename_all = "camelCase", deny_unknown_fields)] -struct DialogAllowlistConfig { +pub struct DialogAllowlistConfig { #[serde(default)] - all: bool, + pub all: bool, #[serde(default)] - open: bool, + pub open: bool, #[serde(default)] - save: bool, + pub save: bool, } impl Allowlist for DialogAllowlistConfig { @@ -434,11 +434,11 @@ impl Allowlist for DialogAllowlistConfig { #[derive(Debug, Default, PartialEq, Clone, Deserialize, Serialize, JsonSchema)] #[serde(rename_all = "camelCase", deny_unknown_fields)] -struct HttpAllowlistConfig { +pub struct HttpAllowlistConfig { #[serde(default)] - all: bool, + pub all: bool, #[serde(default)] - request: bool, + pub request: bool, } impl Allowlist for HttpAllowlistConfig { @@ -455,9 +455,9 @@ impl Allowlist for HttpAllowlistConfig { #[derive(Debug, Default, PartialEq, Clone, Deserialize, Serialize, JsonSchema)] #[serde(rename_all = "camelCase", deny_unknown_fields)] -struct NotificationAllowlistConfig { +pub struct NotificationAllowlistConfig { #[serde(default)] - all: bool, + pub all: bool, } impl Allowlist for NotificationAllowlistConfig { @@ -472,9 +472,9 @@ impl Allowlist for NotificationAllowlistConfig { #[derive(Debug, Default, PartialEq, Clone, Deserialize, Serialize, JsonSchema)] #[serde(rename_all = "camelCase", deny_unknown_fields)] -struct GlobalShortcutAllowlistConfig { +pub struct GlobalShortcutAllowlistConfig { #[serde(default)] - all: bool, + pub all: bool, } impl Allowlist for GlobalShortcutAllowlistConfig { @@ -489,23 +489,23 @@ impl Allowlist for GlobalShortcutAllowlistConfig { #[derive(Debug, Default, PartialEq, Clone, Deserialize, Serialize, JsonSchema)] #[serde(rename_all = "camelCase", deny_unknown_fields)] -struct AllowlistConfig { +pub struct AllowlistConfig { #[serde(default)] - all: bool, + pub all: bool, #[serde(default)] - fs: FsAllowlistConfig, + pub fs: FsAllowlistConfig, #[serde(default)] - window: WindowAllowlistConfig, + pub window: WindowAllowlistConfig, #[serde(default)] - shell: ShellAllowlistConfig, + pub shell: ShellAllowlistConfig, #[serde(default)] - dialog: DialogAllowlistConfig, + pub dialog: DialogAllowlistConfig, #[serde(default)] - http: HttpAllowlistConfig, + pub http: HttpAllowlistConfig, #[serde(default)] - notification: NotificationAllowlistConfig, + pub notification: NotificationAllowlistConfig, #[serde(default)] - global_shortcut: GlobalShortcutAllowlistConfig, + pub global_shortcut: GlobalShortcutAllowlistConfig, } impl Allowlist for AllowlistConfig { diff --git a/tooling/cli.rs/src/build/rust.rs b/tooling/cli.rs/src/build/rust.rs index 727a989f0..0381e5eaa 100644 --- a/tooling/cli.rs/src/build/rust.rs +++ b/tooling/cli.rs/src/build/rust.rs @@ -383,7 +383,7 @@ fn tauri_config_to_bundle_settings( // provides `libwebkit2gtk-4.0.so.37` and all `4.0` versions have the -37 package name depends.push("libwebkit2gtk-4.0-37".to_string()); depends.push("libgtk-3-0".to_string()); - if manifest.features.contains(&"menu".into()) || system_tray_config.is_some() { + if manifest.features.contains("menu") || system_tray_config.is_some() { depends.push("libgtksourceview-3.0-1".to_string()); } } diff --git a/tooling/cli.rs/src/helpers/config.rs b/tooling/cli.rs/src/helpers/config.rs index b6b39f08d..6c7c52192 100644 --- a/tooling/cli.rs/src/helpers/config.rs +++ b/tooling/cli.rs/src/helpers/config.rs @@ -101,3 +101,44 @@ pub fn reload(merge_config: Option<&str>) -> crate::Result<()> { get_internal(merge_config, true)?; Ok(()) } + +pub fn all_allowlist_features() -> Vec<&'static str> { + AllowlistConfig { + all: true, + fs: FsAllowlistConfig { + all: true, + read_text_file: true, + read_binary_file: true, + write_file: true, + write_binary_file: true, + read_dir: true, + copy_file: true, + create_dir: true, + remove_dir: true, + remove_file: true, + rename_file: true, + path: true, + }, + window: WindowAllowlistConfig { + all: true, + create: true, + }, + shell: ShellAllowlistConfig { + all: true, + execute: true, + open: true, + }, + dialog: DialogAllowlistConfig { + all: true, + open: true, + save: true, + }, + http: HttpAllowlistConfig { + all: true, + request: true, + }, + notification: NotificationAllowlistConfig { all: true }, + global_shortcut: GlobalShortcutAllowlistConfig { all: true }, + } + .to_features() +} diff --git a/tooling/cli.rs/src/helpers/manifest.rs b/tooling/cli.rs/src/helpers/manifest.rs index 45a69ab88..4daa0342a 100644 --- a/tooling/cli.rs/src/helpers/manifest.rs +++ b/tooling/cli.rs/src/helpers/manifest.rs @@ -2,19 +2,23 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT -use super::{app_paths::tauri_dir, config::ConfigHandle}; +use super::{ + app_paths::tauri_dir, + config::{all_allowlist_features, ConfigHandle}, +}; use anyhow::Context; use toml_edit::{Array, Document, InlineTable, Item, Value}; use std::{ + collections::HashSet, fs::File, io::{Read, Write}, path::Path, }; pub struct Manifest { - pub features: Vec, + pub features: HashSet, } fn read_manifest(manifest_path: &Path) -> crate::Result { @@ -31,14 +35,14 @@ fn read_manifest(manifest_path: &Path) -> crate::Result { Ok(manifest) } -fn features_to_vec(features: &Array) -> Vec { - let mut string_features = Vec::new(); - for feat in features.iter() { - if let Value::String(feature) = feat { - string_features.push(feature.value().to_string()); - } +fn toml_array(features: &HashSet) -> Array { + let mut f = Array::default(); + let mut features: Vec = features.iter().map(|f| f.to_string()).collect(); + features.sort(); + for feature in features { + f.push(feature.as_str()).unwrap(); } - string_features + f } pub fn rewrite_manifest(config: ConfigHandle) -> crate::Result { @@ -56,32 +60,35 @@ pub fn rewrite_manifest(config: ConfigHandle) -> crate::Result { let config = config_guard.as_ref().unwrap(); let allowlist_features = config.tauri.features(); - let mut features = Array::default(); + let mut features = HashSet::new(); for feature in allowlist_features { - features.push(feature).unwrap(); + features.insert(feature.to_string()); } if config.tauri.cli.is_some() { - features.push("cli").unwrap(); + features.insert("cli".to_string()); } if config.tauri.updater.active { - features.push("updater").unwrap(); + features.insert("updater".to_string()); } if config.tauri.system_tray.is_some() { - features.push("system-tray").unwrap(); + features.insert("system-tray".to_string()); } + let mut cli_managed_features = all_allowlist_features(); + cli_managed_features.extend(vec!["cli", "updater", "system-tray"]); + if let Some(tauri) = tauri_entry.as_table_mut() { let manifest_features = tauri.entry("features"); if let Item::Value(Value::Array(f)) = &manifest_features { for feat in f.iter() { if let Value::String(feature) = feat { - if feature.value() == "menu" { - features.push("menu").unwrap(); + if !cli_managed_features.contains(&feature.value().as_str()) { + features.insert(feature.value().to_string()); } } } } - *manifest_features = Item::Value(Value::Array(features.clone())); + *manifest_features = Item::Value(Value::Array(toml_array(&features))); } else if let Some(tauri) = tauri_entry.as_value_mut() { match tauri { Value::InlineTable(table) => { @@ -89,13 +96,13 @@ pub fn rewrite_manifest(config: ConfigHandle) -> crate::Result { if let Value::Array(f) = &manifest_features { for feat in f.iter() { if let Value::String(feature) = feat { - if feature.value() == "menu" { - features.push("menu").unwrap(); + if !cli_managed_features.contains(&feature.value().as_str()) { + features.insert(feature.value().to_string()); } } } } - *manifest_features = Value::Array(features.clone()); + *manifest_features = Value::Array(toml_array(&features)); } Value::String(version) => { let mut def = InlineTable::default(); @@ -103,7 +110,7 @@ pub fn rewrite_manifest(config: ConfigHandle) -> crate::Result { "version", version.to_string().replace("\"", "").replace(" ", ""), ); - def.get_or_insert("features", Value::Array(features.clone())); + def.get_or_insert("features", Value::Array(toml_array(&features))); *tauri = Value::InlineTable(def); } _ => { @@ -113,9 +120,7 @@ pub fn rewrite_manifest(config: ConfigHandle) -> crate::Result { } } } else { - return Ok(Manifest { - features: features_to_vec(&features), - }); + return Ok(Manifest { features }); } let mut manifest_file = @@ -132,9 +137,7 @@ pub fn rewrite_manifest(config: ConfigHandle) -> crate::Result { )?; manifest_file.flush()?; - Ok(Manifest { - features: features_to_vec(&features), - }) + Ok(Manifest { features }) } pub fn get_workspace_members() -> crate::Result> {