feat(cli): allow merging multiple configuration values (#12970)

* feat(cli): allow merging multiple configuration values

Currently the dev/build/bundle commands can only merge a single Tauri configuration value (file or raw JSON string), which imposes a limitation in scenarios where you need more flexibility (like multiple app flavors and environments). This changes the config CLI option to allow multiple values, letting you merge multiple Tauri config files with the main one.

* fix ios build
This commit is contained in:
Lucas Fernandes Nogueira
2025-03-14 12:04:58 -03:00
committed by GitHub
parent f67a4a6bfe
commit d91bfa5cb9
16 changed files with 161 additions and 90 deletions

View File

@@ -0,0 +1,6 @@
---
"@tauri-apps/cli": minor:feat
"tauri-cli": minor:feat
---
Allow merging multiple configuration values on `tauri dev`, `tauri build`, `tauri bundle`, `tauri android dev`, `tauri android build`, `tauri ios dev` and `tauri ios build`.

View File

@@ -47,9 +47,15 @@ pub struct Options {
/// Skip the bundling step even if `bundle > active` is `true` in tauri config.
#[clap(long)]
pub no_bundle: bool,
/// JSON string or path to JSON file to merge with tauri.conf.json
/// JSON strings or path to JSON files to merge with the default configuration file
///
/// Configurations are merged in the order they are provided, which means a particular value overwrites previous values when a config key-value pair conflicts.
///
/// Note that a platform-specific file is looked up and merged with the default file by default
/// (tauri.macos.conf.json, tauri.linux.conf.json, tauri.windows.conf.json, tauri.android.conf.json and tauri.ios.conf.json)
/// but you can use this for more specific use cases such as different build flavors.
#[clap(short, long)]
pub config: Option<ConfigValue>,
pub config: Vec<ConfigValue>,
/// Command line arguments passed to the runner. Use `--` to explicitly mark the start of the arguments.
pub args: Vec<String>,
/// Skip prompting for values
@@ -68,7 +74,10 @@ pub fn command(mut options: Options, verbosity: u8) -> Result<()> {
.map(Target::from_triple)
.unwrap_or_else(Target::current);
let config = get_config(target, options.config.as_ref().map(|c| &c.0))?;
let config = get_config(
target,
&options.config.iter().map(|c| &c.0).collect::<Vec<_>>(),
)?;
let mut interface = AppInterface::new(
config.lock().unwrap().as_ref().unwrap(),

View File

@@ -67,9 +67,15 @@ pub struct Options {
/// Note that the `updater` bundle is not automatically added so you must specify it if the updater is enabled.
#[clap(short, long, action = ArgAction::Append, num_args(0..), value_delimiter = ',')]
pub bundles: Option<Vec<BundleFormat>>,
/// JSON string or path to JSON file to merge with tauri.conf.json
/// JSON strings or path to JSON files to merge with the default configuration file
///
/// Configurations are merged in the order they are provided, which means a particular value overwrites previous values when a config key-value pair conflicts.
///
/// Note that a platform-specific file is looked up and merged with the default file by default
/// (tauri.macos.conf.json, tauri.linux.conf.json, tauri.windows.conf.json, tauri.android.conf.json and tauri.ios.conf.json)
/// but you can use this for more specific use cases such as different build flavors.
#[clap(short, long)]
pub config: Option<ConfigValue>,
pub config: Vec<ConfigValue>,
/// Space or comma separated list of features, should be the same features passed to `tauri build` if any.
#[clap(short, long, action = ArgAction::Append, num_args(0..))]
pub features: Option<Vec<String>>,
@@ -109,7 +115,10 @@ pub fn command(options: Options, verbosity: u8) -> crate::Result<()> {
.map(Target::from_triple)
.unwrap_or_else(Target::current);
let config = get_config(target, options.config.as_ref().map(|c| &c.0))?;
let config = get_config(
target,
&options.config.iter().map(|c| &c.0).collect::<Vec<_>>(),
)?;
let interface = AppInterface::new(
config.lock().unwrap().as_ref().unwrap(),

View File

@@ -59,9 +59,15 @@ pub struct Options {
/// Exit on panic
#[clap(short, long)]
pub exit_on_panic: bool,
/// JSON string or path to JSON file to merge with tauri.conf.json
/// JSON strings or path to JSON files to merge with the default configuration file
///
/// Configurations are merged in the order they are provided, which means a particular value overwrites previous values when a config key-value pair conflicts.
///
/// Note that a platform-specific file is looked up and merged with the default file by default
/// (tauri.macos.conf.json, tauri.linux.conf.json, tauri.windows.conf.json, tauri.android.conf.json and tauri.ios.conf.json)
/// but you can use this for more specific use cases such as different build flavors.
#[clap(short, long)]
pub config: Option<ConfigValue>,
pub config: Vec<ConfigValue>,
/// Run the code in release mode
#[clap(long = "release")]
pub release_mode: bool,
@@ -104,7 +110,10 @@ fn command_internal(mut options: Options) -> Result<()> {
.map(Target::from_triple)
.unwrap_or_else(Target::current);
let config = get_config(target, options.config.as_ref().map(|c| &c.0))?;
let config = get_config(
target,
&options.config.iter().map(|c| &c.0).collect::<Vec<_>>(),
)?;
let mut interface = AppInterface::new(
config.lock().unwrap().as_ref().unwrap(),
@@ -262,26 +271,13 @@ pub fn setup(interface: &AppInterface, options: &mut Options, config: ConfigHand
let server_url = format!("http://{server_url}");
dev_url = Some(server_url.parse().unwrap());
if let Some(c) = &mut options.config {
if let Some(build) = c
.0
.as_object_mut()
.and_then(|root| root.get_mut("build"))
.and_then(|build| build.as_object_mut())
{
build.insert("devUrl".into(), server_url.into());
options.config.push(crate::ConfigValue(serde_json::json!({
"build": {
"devUrl": server_url
}
} else {
options
.config
.replace(crate::ConfigValue(serde_json::json!({
"build": {
"devUrl": server_url
}
})));
}
})));
reload_config(options.config.as_ref().map(|c| &c.0))?;
reload_config(&options.config.iter().map(|c| &c.0).collect::<Vec<_>>())?;
}
}
}

View File

@@ -138,7 +138,7 @@ fn config_handle() -> &'static ConfigHandle {
/// Gets the static parsed config from `tauri.conf.json`.
fn get_internal(
merge_config: Option<&serde_json::Value>,
merge_configs: &[&serde_json::Value],
reload: bool,
target: Target,
) -> crate::Result<ConfigHandle> {
@@ -162,12 +162,17 @@ fn get_internal(
);
}
if let Some(merge_config) = merge_config {
if !merge_configs.is_empty() {
let mut merge_config = serde_json::Value::Object(Default::default());
for conf in merge_configs {
merge(&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);
merge(&mut config, &merge_config);
extensions.insert(MERGE_CONFIG_EXTENSION_NAME.into(), merge_config.clone());
};
}
if config_path.extension() == Some(OsStr::new("json"))
|| config_path.extension() == Some(OsStr::new("json5"))
@@ -217,35 +222,42 @@ fn get_internal(
Ok(config_handle().clone())
}
pub fn get(
target: Target,
merge_config: Option<&serde_json::Value>,
) -> crate::Result<ConfigHandle> {
get_internal(merge_config, false, target)
pub fn get(target: Target, merge_configs: &[&serde_json::Value]) -> crate::Result<ConfigHandle> {
get_internal(merge_configs, false, target)
}
pub fn reload(merge_config: Option<&serde_json::Value>) -> crate::Result<ConfigHandle> {
pub fn reload(merge_configs: &[&serde_json::Value]) -> crate::Result<ConfigHandle> {
let target = config_handle()
.lock()
.unwrap()
.as_ref()
.map(|conf| conf.target);
if let Some(target) = target {
get_internal(merge_config, true, target)
get_internal(merge_configs, true, target)
} else {
Err(anyhow::anyhow!("config not loaded"))
}
}
/// merges the loaded config with the given value
pub fn merge_with(merge_config: &serde_json::Value) -> crate::Result<ConfigHandle> {
pub fn merge_with(merge_configs: &[&serde_json::Value]) -> crate::Result<ConfigHandle> {
let handle = config_handle();
if merge_configs.is_empty() {
return Ok(handle.clone());
}
if let Some(config_metadata) = &mut *handle.lock().unwrap() {
let merge_config_str = serde_json::to_string(merge_config).unwrap();
let mut merge_config = serde_json::Value::Object(Default::default());
for conf in merge_configs {
merge(&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_metadata.inner.clone())?;
merge(&mut value, merge_config);
merge(&mut value, &merge_config);
config_metadata.inner = serde_json::from_value(value)?;
Ok(handle.clone())

View File

@@ -13,7 +13,7 @@ use tauri_utils::platform::Target;
pub fn items(frontend_dir: Option<&PathBuf>, tauri_dir: Option<&Path>) -> Vec<SectionItem> {
let mut items = Vec::new();
if tauri_dir.is_some() {
if let Ok(config) = crate::helpers::config::get(Target::current(), None) {
if let Ok(config) = crate::helpers::config::get(Target::current(), &[]) {
let config_guard = config.lock().unwrap();
let config = config_guard.as_ref().unwrap();

View File

@@ -31,7 +31,7 @@ fn wix_upgrade_code() -> Result<()> {
crate::helpers::app_paths::resolve();
let target = tauri_utils::platform::Target::Windows;
let config = crate::helpers::config::get(target, None)?;
let config = crate::helpers::config::get(target, &[])?;
let interface = AppInterface::new(config.lock().unwrap().as_ref().unwrap(), None)?;

View File

@@ -51,7 +51,7 @@ pub struct Options {
pub target: Option<String>,
pub features: Option<Vec<String>>,
pub args: Vec<String>,
pub config: Option<ConfigValue>,
pub config: Vec<ConfigValue>,
pub no_watch: bool,
}
@@ -101,7 +101,7 @@ pub struct MobileOptions {
pub debug: bool,
pub features: Option<Vec<String>>,
pub args: Vec<String>,
pub config: Option<ConfigValue>,
pub config: Vec<ConfigValue>,
pub no_watch: bool,
}
@@ -207,14 +207,14 @@ impl Interface for Rust {
rx.recv().unwrap();
Ok(())
} else {
let config = options.config.clone().map(|c| c.0);
let merge_configs = options.config.iter().map(|c| &c.0).collect::<Vec<_>>();
let run = Arc::new(|rust: &mut Rust| {
let on_exit = on_exit.clone();
rust.run_dev(options.clone(), run_args.clone(), move |status, reason| {
on_exit(status, reason)
})
});
self.run_dev_watcher(config, run)
self.run_dev_watcher(&merge_configs, run)
}
}
@@ -236,9 +236,9 @@ impl Interface for Rust {
runner(options)?;
Ok(())
} else {
let config = options.config.clone().map(|c| c.0);
let merge_configs = options.config.iter().map(|c| &c.0).collect::<Vec<_>>();
let run = Arc::new(|_rust: &mut Rust| runner(options.clone()));
self.run_dev_watcher(config, run)
self.run_dev_watcher(&merge_configs, run)
}
}
@@ -487,7 +487,7 @@ impl Rust {
fn run_dev_watcher<F: Fn(&mut Rust) -> crate::Result<Box<dyn DevProcess + Send>>>(
&mut self,
config: Option<serde_json::Value>,
merge_configs: &[&serde_json::Value],
run: Arc<F>,
) -> crate::Result<()> {
let child = run(self)?;
@@ -537,7 +537,7 @@ impl Rust {
if let Some(event_path) = event.paths.first() {
if !ignore_matcher.is_ignore(event_path, event_path.is_dir()) {
if is_configuration_file(self.app_settings.target, event_path) {
if let Ok(config) = reload_config(config.as_ref()) {
if let Ok(config) = reload_config(merge_configs) {
let (manifest, modified) =
rewrite_manifest(config.lock().unwrap().as_ref().unwrap())?;
if modified {

View File

@@ -46,7 +46,7 @@ pub fn command(options: Options) -> Result<()> {
Profile::Debug
};
let tauri_config = get_tauri_config(tauri_utils::platform::Target::Android, None)?;
let tauri_config = get_tauri_config(tauri_utils::platform::Target::Android, &[])?;
let (config, metadata, cli_options) = {
let tauri_config_guard = tauri_config.lock().unwrap();
@@ -72,8 +72,14 @@ pub fn command(options: Options) -> Result<()> {
MobileTarget::Android,
)?;
if let Some(config) = &cli_options.config {
crate::helpers::config::merge_with(&config.0)?;
if !cli_options.config.is_empty() {
crate::helpers::config::merge_with(
&cli_options
.config
.iter()
.map(|conf| &conf.0)
.collect::<Vec<_>>(),
)?;
}
let env = env()?;

View File

@@ -49,9 +49,15 @@ pub struct Options {
/// List of cargo features to activate
#[clap(short, long, action = ArgAction::Append, num_args(0..))]
pub features: Option<Vec<String>>,
/// JSON string or path to JSON file to merge with tauri.conf.json
/// JSON strings or path to JSON files to merge with the default configuration file
///
/// Configurations are merged in the order they are provided, which means a particular value overwrites previous values when a config key-value pair conflicts.
///
/// Note that a platform-specific file is looked up and merged with the default file by default
/// (tauri.macos.conf.json, tauri.linux.conf.json, tauri.windows.conf.json, tauri.android.conf.json and tauri.ios.conf.json)
/// but you can use this for more specific use cases such as different build flavors.
#[clap(short, long)]
pub config: Option<ConfigValue>,
pub config: Vec<ConfigValue>,
/// Whether to split the APKs and AABs per ABIs.
#[clap(long)]
pub split_per_abi: bool,
@@ -105,7 +111,11 @@ pub fn command(options: Options, noise_level: NoiseLevel) -> Result<()> {
let tauri_config = get_tauri_config(
tauri_utils::platform::Target::Android,
options.config.as_ref().map(|c| &c.0),
&options
.config
.iter()
.map(|conf| &conf.0)
.collect::<Vec<_>>(),
)?;
let (interface, config, metadata) = {
let tauri_config_guard = tauri_config.lock().unwrap();

View File

@@ -48,9 +48,15 @@ pub struct Options {
/// Exit on panic
#[clap(short, long)]
exit_on_panic: bool,
/// JSON string or path to JSON file to merge with tauri.conf.json
/// JSON strings or path to JSON files to merge with the default configuration file
///
/// Configurations are merged in the order they are provided, which means a particular value overwrites previous values when a config key-value pair conflicts.
///
/// Note that a platform-specific file is looked up and merged with the default file by default
/// (tauri.macos.conf.json, tauri.linux.conf.json, tauri.windows.conf.json, tauri.android.conf.json and tauri.ios.conf.json)
/// but you can use this for more specific use cases such as different build flavors.
#[clap(short, long)]
pub config: Option<ConfigValue>,
pub config: Vec<ConfigValue>,
/// Run the code in release mode
#[clap(long = "release")]
pub release_mode: bool,
@@ -126,7 +132,11 @@ fn run_command(options: Options, noise_level: NoiseLevel) -> Result<()> {
let tauri_config = get_tauri_config(
tauri_utils::platform::Target::Android,
options.config.as_ref().map(|c| &c.0),
&options
.config
.iter()
.map(|conf| &conf.0)
.collect::<Vec<_>>(),
)?;
let env = env()?;

View File

@@ -43,7 +43,7 @@ pub fn exec(
#[allow(unused_variables)] reinstall_deps: bool,
skip_targets_install: bool,
) -> Result<App> {
let tauri_config = get_tauri_config(target.platform_target(), None)?;
let tauri_config = get_tauri_config(target.platform_target(), &[])?;
let tauri_config_guard = tauri_config.lock().unwrap();
let tauri_config_ = tauri_config_guard.as_ref().unwrap();

View File

@@ -60,9 +60,15 @@ pub struct Options {
/// List of cargo features to activate
#[clap(short, long, action = ArgAction::Append, num_args(0..))]
pub features: Option<Vec<String>>,
/// JSON string or path to JSON file to merge with tauri.conf.json
/// JSON strings or path to JSON files to merge with the default configuration file
///
/// Configurations are merged in the order they are provided, which means a particular value overwrites previous values when a config key-value pair conflicts.
///
/// Note that a platform-specific file is looked up and merged with the default file by default
/// (tauri.macos.conf.json, tauri.linux.conf.json, tauri.windows.conf.json, tauri.android.conf.json and tauri.ios.conf.json)
/// but you can use this for more specific use cases such as different build flavors.
#[clap(short, long)]
pub config: Option<ConfigValue>,
pub config: Vec<ConfigValue>,
/// Build number to append to the app version.
#[clap(long)]
pub build_number: Option<u32>,
@@ -145,7 +151,7 @@ pub fn command(options: Options, noise_level: NoiseLevel) -> Result<()> {
let tauri_config = get_tauri_config(
tauri_utils::platform::Target::Ios,
options.config.as_ref().map(|c| &c.0),
&options.config.iter().map(|c| &c.0).collect::<Vec<_>>(),
)?;
let (interface, mut config) = {
let tauri_config_guard = tauri_config.lock().unwrap();

View File

@@ -54,9 +54,15 @@ pub struct Options {
/// Exit on panic
#[clap(short, long)]
exit_on_panic: bool,
/// JSON string or path to JSON file to merge with tauri.conf.json
/// JSON strings or path to JSON files to merge with the default configuration file
///
/// Configurations are merged in the order they are provided, which means a particular value overwrites previous values when a config key-value pair conflicts.
///
/// Note that a platform-specific file is looked up and merged with the default file by default
/// (tauri.macos.conf.json, tauri.linux.conf.json, tauri.windows.conf.json, tauri.android.conf.json and tauri.ios.conf.json)
/// but you can use this for more specific use cases such as different build flavors.
#[clap(short, long)]
pub config: Option<ConfigValue>,
pub config: Vec<ConfigValue>,
/// Run the code in release mode
#[clap(long = "release")]
pub release_mode: bool,
@@ -148,7 +154,7 @@ fn run_command(options: Options, noise_level: NoiseLevel) -> Result<()> {
let tauri_config = get_tauri_config(
tauri_utils::platform::Target::Ios,
options.config.as_ref().map(|c| &c.0),
&options.config.iter().map(|c| &c.0).collect::<Vec<_>>(),
)?;
let (interface, config) = {
let tauri_config_guard = tauri_config.lock().unwrap();

View File

@@ -78,7 +78,7 @@ pub fn command(options: Options) -> Result<()> {
let profile = profile_from_configuration(&options.configuration);
let macos = macos_from_platform(&options.platform);
let tauri_config = get_tauri_config(tauri_utils::platform::Target::Ios, None)?;
let tauri_config = get_tauri_config(tauri_utils::platform::Target::Ios, &[])?;
let (config, metadata, cli_options) = {
let tauri_config_guard = tauri_config.lock().unwrap();
@@ -103,8 +103,14 @@ pub fn command(options: Options) -> Result<()> {
MobileTarget::Ios,
)?;
if let Some(config) = &cli_options.config {
crate::helpers::config::merge_with(&config.0)?;
if !cli_options.config.is_empty() {
crate::helpers::config::merge_with(
&cli_options
.config
.iter()
.map(|conf| &conf.0)
.collect::<Vec<_>>(),
)?;
}
let env = env()?.explicit_env_vars(cli_options.vars);

View File

@@ -187,7 +187,7 @@ pub struct CliOptions {
pub args: Vec<String>,
pub noise_level: NoiseLevel,
pub vars: HashMap<String, OsString>,
pub config: Option<ConfigValue>,
pub config: Vec<ConfigValue>,
pub target_device: Option<TargetDevice>,
}
@@ -199,7 +199,7 @@ impl Default for CliOptions {
args: vec!["--lib".into()],
noise_level: Default::default(),
vars: Default::default(),
config: None,
config: Vec::new(),
target_device: None,
}
}
@@ -292,26 +292,21 @@ fn use_network_address_for_dev_url(
url.path()
))?;
if let Some(c) = &mut dev_options.config {
if let Some(build) = c
.0
.as_object_mut()
.and_then(|root| root.get_mut("build"))
.and_then(|build| build.as_object_mut())
{
build.insert("devUrl".into(), url.to_string().into());
}
} else {
let mut build = serde_json::Map::new();
build.insert("devUrl".into(), url.to_string().into());
dev_options
.config
.push(crate::ConfigValue(serde_json::json!({
"build": {
"devUrl": url
}
})));
dev_options
reload_config(
&dev_options
.config
.replace(crate::ConfigValue(serde_json::json!({
"build": build
})));
}
reload_config(dev_options.config.as_ref().map(|c| &c.0))?;
.iter()
.map(|conf| &conf.0)
.collect::<Vec<_>>(),
)?;
Some(ip)
} else {