use itertools::Itertools;
use json_patch::merge;
use serde_json::Value as JsonValue;
use tauri_utils::acl::REMOVE_UNUSED_COMMANDS_ENV_VAR;
pub use tauri_utils::{config::*, platform::Target};
use std::{
collections::HashMap,
env::{current_dir, set_current_dir, set_var},
ffi::{OsStr, OsString},
path::Path,
process::exit,
sync::OnceLock,
};
use crate::error::Context;
pub const MERGE_CONFIG_EXTENSION_NAME: &str = "--config";
pub struct ConfigMetadata {
target: Target,
original_identifier: Option<String>,
inner: Config,
extensions: HashMap<OsString, JsonValue>,
}
impl std::ops::Deref for ConfigMetadata {
type Target = Config;
#[inline(always)]
fn deref(&self) -> &Config {
&self.inner
}
}
impl ConfigMetadata {
pub fn original_identifier(&self) -> Option<&str> {
self.original_identifier.as_deref()
}
pub fn find_bundle_identifier_overwriter(&self) -> Option<OsString> {
for (ext, config) in &self.extensions {
if let Some(identifier) = config
.as_object()
.and_then(|bundle_config| bundle_config.get("identifier")?.as_str())
{
if identifier == self.inner.identifier {
return Some(ext.clone());
}
}
}
None
}
}
pub fn wix_settings(config: WixConfig) -> tauri_bundler::WixSettings {
tauri_bundler::WixSettings {
version: config.version,
upgrade_code: config.upgrade_code,
fips_compliant: std::env::var_os("TAURI_BUNDLER_WIX_FIPS_COMPLIANT")
.map(|v| v == "true")
.unwrap_or(config.fips_compliant),
language: tauri_bundler::WixLanguage(match config.language {
WixLanguage::One(lang) => vec![(lang, Default::default())],
WixLanguage::List(languages) => languages
.into_iter()
.map(|lang| (lang, Default::default()))
.collect(),
WixLanguage::Localized(languages) => languages
.into_iter()
.map(|(lang, config)| {
(
lang,
tauri_bundler::WixLanguageConfig {
locale_path: config.locale_path.map(Into::into),
},
)
})
.collect(),
}),
template: config.template,
fragment_paths: config.fragment_paths,
component_group_refs: config.component_group_refs,
component_refs: config.component_refs,
feature_group_refs: config.feature_group_refs,
feature_refs: config.feature_refs,
merge_refs: config.merge_refs,
enable_elevated_update_task: config.enable_elevated_update_task,
banner_path: config.banner_path,
dialog_image_path: config.dialog_image_path,
}
}
pub fn nsis_settings(config: NsisConfig) -> tauri_bundler::NsisSettings {
tauri_bundler::NsisSettings {
template: config.template,
header_image: config.header_image,
sidebar_image: config.sidebar_image,
installer_icon: config.installer_icon,
install_mode: config.install_mode,
languages: config.languages,
custom_language_files: config.custom_language_files,
display_language_selector: config.display_language_selector,
compression: config.compression,
start_menu_folder: config.start_menu_folder,
installer_hooks: config.installer_hooks,
minimum_webview2_version: config.minimum_webview2_version,
}
}
pub fn custom_sign_settings(
config: CustomSignCommandConfig,
) -> tauri_bundler::CustomSignCommandSettings {
match config {
CustomSignCommandConfig::Command(command) => {
let mut tokens = command.split(' ');
tauri_bundler::CustomSignCommandSettings {
cmd: tokens.next().unwrap().to_string(), args: tokens.map(String::from).collect(),
}
}
CustomSignCommandConfig::CommandWithOptions { cmd, args } => {
tauri_bundler::CustomSignCommandSettings { cmd, args }
}
}
}
fn config_schema_validator() -> &'static jsonschema::Validator {
static CONFIG_SCHEMA_VALIDATOR: OnceLock<jsonschema::Validator> = OnceLock::new();
CONFIG_SCHEMA_VALIDATOR.get_or_init(|| {
let schema: JsonValue = serde_json::from_str(include_str!("../../config.schema.json"))
.expect("Failed to parse config schema bundled in the tauri-cli");
jsonschema::validator_for(&schema).expect("Config schema bundled in the tauri-cli is invalid")
})
}
fn load_config(
merge_configs: &[&serde_json::Value],
reload: bool,
target: Target,
tauri_dir: &Path,
) -> crate::Result<ConfigMetadata> {
let (mut config, config_path) =
tauri_utils::config::parse::parse_value(target, tauri_dir.join("tauri.conf.json"))
.context("failed to parse config")?;
let config_file_name = config_path.file_name().unwrap();
let mut extensions = HashMap::new();
let original_identifier = config
.as_object()
.and_then(|config| config.get("identifier")?.as_str())
.map(ToString::to_string);
if let Some((platform_config, config_path)) =
tauri_utils::config::parse::read_platform(target, tauri_dir)
.context("failed to parse platform config")?
{
merge(&mut config, &platform_config);
extensions.insert(config_path.file_name().unwrap().into(), platform_config);
}
if !merge_configs.is_empty() {
let mut merge_config = serde_json::Value::Object(Default::default());
for conf in merge_configs {
merge_patches(&mut merge_config, conf);
}
let merge_config_str = serde_json::to_string(&merge_config).unwrap();
set_var("TAURI_CONFIG", merge_config_str);
merge(&mut config, &merge_config);
extensions.insert(MERGE_CONFIG_EXTENSION_NAME.into(), merge_config);
}
if config_path.extension() == Some(OsStr::new("json"))
|| config_path.extension() == Some(OsStr::new("json5"))
{
let mut errors = config_schema_validator().iter_errors(&config).peekable();
if errors.peek().is_some() {
for error in errors {
let path = error.instance_path.into_iter().join(" > ");
if path.is_empty() {
log::error!("`{config_file_name:?}` error: {error}");
} else {
log::error!("`{config_file_name:?}` error on `{path}`: {error}");
}
}
if !reload {
exit(1);
}
}
}
let current_dir = current_dir().context("failed to resolve current directory")?;
set_current_dir(config_path.parent().unwrap()).context("failed to set current directory")?;
let config: Config = serde_json::from_value(config).context("failed to parse config")?;
set_current_dir(current_dir).context("failed to set current directory")?;
for (plugin, conf) in &config.plugins.0 {
set_var(
format!(
"TAURI_{}_PLUGIN_CONFIG",
plugin.to_uppercase().replace('-', "_")
),
serde_json::to_string(&conf).context("failed to serialize config")?,
);
}
if config.build.remove_unused_commands {
std::env::set_var(REMOVE_UNUSED_COMMANDS_ENV_VAR, tauri_dir);
}
Ok(ConfigMetadata {
target,
original_identifier,
inner: config,
extensions,
})
}
pub fn get_config(
target: Target,
merge_configs: &[&serde_json::Value],
tauri_dir: &Path,
) -> crate::Result<ConfigMetadata> {
load_config(merge_configs, false, target, tauri_dir)
}
pub fn reload_config(
config: &mut ConfigMetadata,
merge_configs: &[&serde_json::Value],
tauri_dir: &Path,
) -> crate::Result<()> {
let target = config.target;
*config = load_config(merge_configs, true, target, tauri_dir)?;
Ok(())
}
pub fn merge_config_with(
config: &mut ConfigMetadata,
merge_configs: &[&serde_json::Value],
) -> crate::Result<()> {
if merge_configs.is_empty() {
return Ok(());
}
let mut merge_config = serde_json::Value::Object(Default::default());
for conf in merge_configs {
merge_patches(&mut merge_config, conf);
}
let merge_config_str = serde_json::to_string(&merge_config).unwrap();
set_var("TAURI_CONFIG", merge_config_str);
let mut value =
serde_json::to_value(config.inner.clone()).context("failed to serialize config")?;
merge(&mut value, &merge_config);
config.inner = serde_json::from_value(value).context("failed to parse config")?;
Ok(())
}
fn merge_patches(doc: &mut serde_json::Value, patch: &serde_json::Value) {
use serde_json::{Map, Value};
if !patch.is_object() {
*doc = patch.clone();
return;
}
if !doc.is_object() {
*doc = Value::Object(Map::new());
}
let map = doc.as_object_mut().unwrap();
for (key, value) in patch.as_object().unwrap() {
merge_patches(map.entry(key.as_str()).or_insert(Value::Null), value);
}
}
#[cfg(test)]
mod tests {
#[test]
fn merge_patches() {
let mut json = serde_json::Value::Object(Default::default());
super::merge_patches(
&mut json,
&serde_json::json!({
"app": {
"withGlobalTauri": true,
"windows": []
},
"plugins": {
"test": "tauri"
},
"build": {
"devUrl": "http://localhost:8080"
}
}),
);
super::merge_patches(
&mut json,
&serde_json::json!({
"app": { "withGlobalTauri": null }
}),
);
super::merge_patches(
&mut json,
&serde_json::json!({
"app": { "windows": null }
}),
);
super::merge_patches(
&mut json,
&serde_json::json!({
"plugins": { "updater": {
"endpoints": ["https://tauri.app"]
} }
}),
);
assert_eq!(
json,
serde_json::json!({
"app": {
"withGlobalTauri": null,
"windows": null
},
"plugins": {
"test": "tauri",
"updater": {
"endpoints": ["https://tauri.app"]
}
},
"build": {
"devUrl": "http://localhost:8080"
}
})
)
}
}