From 33d079392ac4a5a153b7d8a6d82fefd6f54a2bdf Mon Sep 17 00:00:00 2001 From: Mohammad Hossein Bagheri <37915399+mhbagheri-99@users.noreply.github.com> Date: Sun, 13 Jul 2025 13:28:09 +0200 Subject: [PATCH] feat(cli): allow runner configuration to be an object with cwd and args (#13811) * Update config.schema.json * Add RunnerConfig for customizable build runner Replaces runner String with RunnerConfig in CLI and config, allowing advanced runner configuration via string or object with cmd, cwd, and args. Updates schema and usage to support new format, and adds tests for serialization, deserialization, and API. Enables more flexible build and run command customization. * Create runner-object-config.md * Remove unused RunnerConfig import in tests Cleaned up the test module in config.rs by removing the unused RunnerConfig import from two test functions. * Fix tests failing Updates related tests in tauri-utils to improve readability and maintain consistency. Minor import reordering in tauri-cli for clarity. * Move RunnerConfig enum and impls above BuildConfig Relocated the RunnerConfig enum and its associated implementations to appear before the BuildConfig definition. This improves code organization and logical grouping of configuration-related types. --- .changes/runner-object-config.md | 6 + crates/tauri-cli/config.schema.json | 49 ++- crates/tauri-cli/src/build.rs | 5 +- crates/tauri-cli/src/dev.rs | 13 +- crates/tauri-cli/src/interface/rust.rs | 4 +- .../tauri-cli/src/interface/rust/desktop.rs | 14 +- .../schemas/config.schema.json | 49 ++- crates/tauri-utils/src/config.rs | 306 +++++++++++++++++- 8 files changed, 428 insertions(+), 18 deletions(-) create mode 100644 .changes/runner-object-config.md diff --git a/.changes/runner-object-config.md b/.changes/runner-object-config.md new file mode 100644 index 000000000..60cf4aeab --- /dev/null +++ b/.changes/runner-object-config.md @@ -0,0 +1,6 @@ +--- +'tauri-cli': 'minor:feat' +'tauri-utils': 'minor:feat' +--- + +Allow runner configuration to be an object with cmd, cwd, and args properties. The runner can now be configured as `{ "cmd": "my_runner", "cwd": "/path", "args": ["--quiet"] }` while maintaining backwards compatibility with the existing string format. diff --git a/crates/tauri-cli/config.schema.json b/crates/tauri-cli/config.schema.json index 0d46677dd..e1868bb1c 100644 --- a/crates/tauri-cli/config.schema.json +++ b/crates/tauri-cli/config.schema.json @@ -1805,9 +1805,13 @@ "properties": { "runner": { "description": "The binary used to build and run the application.", - "type": [ - "string", - "null" + "anyOf": [ + { + "$ref": "#/definitions/RunnerConfig" + }, + { + "type": "null" + } ] }, "devUrl": { @@ -1880,6 +1884,45 @@ }, "additionalProperties": false }, + "RunnerConfig": { + "description": "The runner configuration.", + "anyOf": [ + { + "description": "A string specifying the binary to run.", + "type": "string" + }, + { + "description": "An object with advanced configuration options.", + "type": "object", + "required": [ + "cmd" + ], + "properties": { + "cmd": { + "description": "The binary to run.", + "type": "string" + }, + "cwd": { + "description": "The current working directory to run the command from.", + "type": [ + "string", + "null" + ] + }, + "args": { + "description": "Arguments to pass to the command.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + } + } + } + ] + }, "FrontendDist": { "description": "Defines the URL or assets to embed in the application.", "anyOf": [ diff --git a/crates/tauri-cli/src/build.rs b/crates/tauri-cli/src/build.rs index 443765e76..f31ab3b94 100644 --- a/crates/tauri-cli/src/build.rs +++ b/crates/tauri-cli/src/build.rs @@ -15,6 +15,7 @@ use crate::{ use anyhow::Context; use clap::{ArgAction, Parser}; use std::env::set_current_dir; +use tauri_utils::config::RunnerConfig; use tauri_utils::platform::Target; #[derive(Debug, Clone, Parser)] @@ -25,7 +26,7 @@ use tauri_utils::platform::Target; pub struct Options { /// Binary to use to build the application, defaults to `cargo` #[clap(short, long)] - pub runner: Option, + pub runner: Option, /// Builds with the debug flag #[clap(short, long)] pub debug: bool, @@ -210,7 +211,7 @@ pub fn setup( } if options.runner.is_none() { - options.runner.clone_from(&config_.build.runner); + options.runner = config_.build.runner.clone(); } options diff --git a/crates/tauri-cli/src/dev.rs b/crates/tauri-cli/src/dev.rs index 3d7ea098e..f802d4480 100644 --- a/crates/tauri-cli/src/dev.rs +++ b/crates/tauri-cli/src/dev.rs @@ -17,7 +17,7 @@ use crate::{ use anyhow::{bail, Context}; use clap::{ArgAction, Parser}; use shared_child::SharedChild; -use tauri_utils::platform::Target; +use tauri_utils::{config::RunnerConfig, platform::Target}; use std::{ env::set_current_dir, @@ -49,7 +49,7 @@ pub const TAURI_CLI_BUILTIN_WATCHER_IGNORE_FILE: &[u8] = pub struct Options { /// Binary to use to run the application #[clap(short, long)] - pub runner: Option, + pub runner: Option, /// Target triple to build against #[clap(short, long)] pub target: Option, @@ -224,9 +224,14 @@ pub fn setup(interface: &AppInterface, options: &mut Options, config: ConfigHand } if options.runner.is_none() { - options + options.runner = config + .lock() + .unwrap() + .as_ref() + .unwrap() + .build .runner - .clone_from(&config.lock().unwrap().as_ref().unwrap().build.runner); + .clone(); } let mut cargo_features = config diff --git a/crates/tauri-cli/src/interface/rust.rs b/crates/tauri-cli/src/interface/rust.rs index 2b046273e..b8990fb45 100644 --- a/crates/tauri-cli/src/interface/rust.rs +++ b/crates/tauri-cli/src/interface/rust.rs @@ -25,7 +25,7 @@ use tauri_bundler::{ IosSettings, MacOsSettings, PackageSettings, Position, RpmSettings, Size, UpdaterSettings, WindowsSettings, }; -use tauri_utils::config::{parse::is_configuration_file, DeepLinkProtocol, Updater}; +use tauri_utils::config::{parse::is_configuration_file, DeepLinkProtocol, RunnerConfig, Updater}; use super::{AppSettings, DevProcess, ExitReason, Interface}; use crate::{ @@ -47,7 +47,7 @@ use manifest::{rewrite_manifest, Manifest}; #[derive(Debug, Default, Clone)] pub struct Options { - pub runner: Option, + pub runner: Option, pub debug: bool, pub target: Option, pub features: Option>, diff --git a/crates/tauri-cli/src/interface/rust/desktop.rs b/crates/tauri-cli/src/interface/rust/desktop.rs index 41836be84..667f9cbbb 100644 --- a/crates/tauri-cli/src/interface/rust/desktop.rs +++ b/crates/tauri-cli/src/interface/rust/desktop.rs @@ -230,11 +230,21 @@ fn cargo_command( available_targets: &mut Option>, config_features: Vec, ) -> crate::Result { - let runner = options.runner.unwrap_or_else(|| "cargo".into()); + let runner_config = options.runner.unwrap_or_else(|| "cargo".into()); - let mut build_cmd = Command::new(runner); + let mut build_cmd = Command::new(runner_config.cmd()); build_cmd.arg(if dev { "run" } else { "build" }); + // Set working directory if specified + if let Some(cwd) = runner_config.cwd() { + build_cmd.current_dir(cwd); + } + + // Add runner-specific arguments first + if let Some(runner_args) = runner_config.args() { + build_cmd.args(runner_args); + } + if let Some(target) = &options.target { if available_targets.is_none() { *available_targets = fetch_available_targets(); diff --git a/crates/tauri-schema-generator/schemas/config.schema.json b/crates/tauri-schema-generator/schemas/config.schema.json index 0d46677dd..e1868bb1c 100644 --- a/crates/tauri-schema-generator/schemas/config.schema.json +++ b/crates/tauri-schema-generator/schemas/config.schema.json @@ -1805,9 +1805,13 @@ "properties": { "runner": { "description": "The binary used to build and run the application.", - "type": [ - "string", - "null" + "anyOf": [ + { + "$ref": "#/definitions/RunnerConfig" + }, + { + "type": "null" + } ] }, "devUrl": { @@ -1880,6 +1884,45 @@ }, "additionalProperties": false }, + "RunnerConfig": { + "description": "The runner configuration.", + "anyOf": [ + { + "description": "A string specifying the binary to run.", + "type": "string" + }, + { + "description": "An object with advanced configuration options.", + "type": "object", + "required": [ + "cmd" + ], + "properties": { + "cmd": { + "description": "The binary to run.", + "type": "string" + }, + "cwd": { + "description": "The current working directory to run the command from.", + "type": [ + "string", + "null" + ] + }, + "args": { + "description": "Arguments to pass to the command.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + } + } + } + ] + }, "FrontendDist": { "description": "Defines the URL or assets to embed in the application.", "anyOf": [ diff --git a/crates/tauri-utils/src/config.rs b/crates/tauri-utils/src/config.rs index 29db41d89..8ca8ff86b 100644 --- a/crates/tauri-utils/src/config.rs +++ b/crates/tauri-utils/src/config.rs @@ -2761,6 +2761,77 @@ pub enum HookCommand { }, } +/// The runner configuration. +#[skip_serializing_none] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "schema", derive(JsonSchema))] +#[serde(untagged)] +pub enum RunnerConfig { + /// A string specifying the binary to run. + String(String), + /// An object with advanced configuration options. + Object { + /// The binary to run. + cmd: String, + /// The current working directory to run the command from. + cwd: Option, + /// Arguments to pass to the command. + args: Option>, + }, +} + +impl Default for RunnerConfig { + fn default() -> Self { + RunnerConfig::String("cargo".to_string()) + } +} + +impl RunnerConfig { + /// Returns the command to run. + pub fn cmd(&self) -> &str { + match self { + RunnerConfig::String(cmd) => cmd, + RunnerConfig::Object { cmd, .. } => cmd, + } + } + + /// Returns the working directory. + pub fn cwd(&self) -> Option<&str> { + match self { + RunnerConfig::String(_) => None, + RunnerConfig::Object { cwd, .. } => cwd.as_deref(), + } + } + + /// Returns the arguments. + pub fn args(&self) -> Option<&[String]> { + match self { + RunnerConfig::String(_) => None, + RunnerConfig::Object { args, .. } => args.as_deref(), + } + } +} + +impl std::str::FromStr for RunnerConfig { + type Err = std::convert::Infallible; + + fn from_str(s: &str) -> Result { + Ok(RunnerConfig::String(s.to_string())) + } +} + +impl From<&str> for RunnerConfig { + fn from(s: &str) -> Self { + RunnerConfig::String(s.to_string()) + } +} + +impl From for RunnerConfig { + fn from(s: String) -> Self { + RunnerConfig::String(s) + } +} + /// The Build configuration object. /// /// See more: @@ -2770,7 +2841,7 @@ pub enum HookCommand { #[serde(rename_all = "camelCase", deny_unknown_fields)] pub struct BuildConfig { /// The binary used to build and run the application. - pub runner: Option, + pub runner: Option, /// The URL to load in development. /// /// This is usually an URL to a dev server, which serves your application assets with hot-reload and HMR. @@ -3436,11 +3507,34 @@ mod build { } } + impl ToTokens for RunnerConfig { + fn to_tokens(&self, tokens: &mut TokenStream) { + let prefix = quote! { ::tauri::utils::config::RunnerConfig }; + + tokens.append_all(match self { + Self::String(cmd) => { + let cmd = cmd.as_str(); + quote!(#prefix::String(#cmd.into())) + } + Self::Object { cmd, cwd, args } => { + let cmd = cmd.as_str(); + let cwd = opt_str_lit(cwd.as_ref()); + let args = opt_lit(args.as_ref().map(|v| vec_lit(v, str_lit)).as_ref()); + quote!(#prefix::Object { + cmd: #cmd.into(), + cwd: #cwd, + args: #args, + }) + } + }) + } + } + impl ToTokens for BuildConfig { fn to_tokens(&self, tokens: &mut TokenStream) { let dev_url = opt_lit(self.dev_url.as_ref().map(url_lit).as_ref()); let frontend_dist = opt_lit(self.frontend_dist.as_ref()); - let runner = quote!(None); + let runner = opt_lit(self.runner.as_ref()); let before_dev_command = quote!(None); let before_build_command = quote!(None); let before_bundle_command = quote!(None); @@ -3824,4 +3918,212 @@ mod test { assert_eq!(Color(0, 0, 0, 255), "#000000ff".parse().unwrap()); assert_eq!(Color(0, 255, 0, 255), "#00ff00ff".parse().unwrap()); } + + #[test] + fn test_runner_config_string_format() { + use super::RunnerConfig; + + // Test string format deserialization + let json = r#""cargo""#; + let runner: RunnerConfig = serde_json::from_str(json).unwrap(); + + assert_eq!(runner.cmd(), "cargo"); + assert_eq!(runner.cwd(), None); + assert_eq!(runner.args(), None); + + // Test string format serialization + let serialized = serde_json::to_string(&runner).unwrap(); + assert_eq!(serialized, r#""cargo""#); + } + + #[test] + fn test_runner_config_object_format_full() { + use super::RunnerConfig; + + // Test object format with all fields + let json = r#"{"cmd": "my_runner", "cwd": "/tmp/build", "args": ["--quiet", "--verbose"]}"#; + let runner: RunnerConfig = serde_json::from_str(json).unwrap(); + + assert_eq!(runner.cmd(), "my_runner"); + assert_eq!(runner.cwd(), Some("/tmp/build")); + assert_eq!( + runner.args(), + Some(&["--quiet".to_string(), "--verbose".to_string()][..]) + ); + + // Test object format serialization + let serialized = serde_json::to_string(&runner).unwrap(); + let deserialized: RunnerConfig = serde_json::from_str(&serialized).unwrap(); + assert_eq!(runner, deserialized); + } + + #[test] + fn test_runner_config_object_format_minimal() { + use super::RunnerConfig; + + // Test object format with only cmd field + let json = r#"{"cmd": "cross"}"#; + let runner: RunnerConfig = serde_json::from_str(json).unwrap(); + + assert_eq!(runner.cmd(), "cross"); + assert_eq!(runner.cwd(), None); + assert_eq!(runner.args(), None); + } + + #[test] + fn test_runner_config_default() { + use super::RunnerConfig; + + let default_runner = RunnerConfig::default(); + assert_eq!(default_runner.cmd(), "cargo"); + assert_eq!(default_runner.cwd(), None); + assert_eq!(default_runner.args(), None); + } + + #[test] + fn test_runner_config_from_str() { + use super::RunnerConfig; + + // Test From<&str> trait + let runner: RunnerConfig = "my_runner".into(); + assert_eq!(runner.cmd(), "my_runner"); + assert_eq!(runner.cwd(), None); + assert_eq!(runner.args(), None); + } + + #[test] + fn test_runner_config_from_string() { + use super::RunnerConfig; + + // Test From trait + let runner: RunnerConfig = "another_runner".to_string().into(); + assert_eq!(runner.cmd(), "another_runner"); + assert_eq!(runner.cwd(), None); + assert_eq!(runner.args(), None); + } + + #[test] + fn test_runner_config_from_str_parse() { + use super::RunnerConfig; + use std::str::FromStr; + + // Test FromStr trait + let runner = RunnerConfig::from_str("parsed_runner").unwrap(); + assert_eq!(runner.cmd(), "parsed_runner"); + assert_eq!(runner.cwd(), None); + assert_eq!(runner.args(), None); + } + + #[test] + fn test_runner_config_in_build_config() { + use super::BuildConfig; + + // Test string format in BuildConfig + let json = r#"{"runner": "cargo"}"#; + let build_config: BuildConfig = serde_json::from_str(json).unwrap(); + + let runner = build_config.runner.unwrap(); + assert_eq!(runner.cmd(), "cargo"); + assert_eq!(runner.cwd(), None); + assert_eq!(runner.args(), None); + } + + #[test] + fn test_runner_config_in_build_config_object() { + use super::BuildConfig; + + // Test object format in BuildConfig + let json = r#"{"runner": {"cmd": "cross", "cwd": "/workspace", "args": ["--target", "x86_64-unknown-linux-gnu"]}}"#; + let build_config: BuildConfig = serde_json::from_str(json).unwrap(); + + let runner = build_config.runner.unwrap(); + assert_eq!(runner.cmd(), "cross"); + assert_eq!(runner.cwd(), Some("/workspace")); + assert_eq!( + runner.args(), + Some( + &[ + "--target".to_string(), + "x86_64-unknown-linux-gnu".to_string() + ][..] + ) + ); + } + + #[test] + fn test_runner_config_in_full_config() { + use super::Config; + + // Test runner config in full Tauri config + let json = r#"{ + "productName": "Test App", + "version": "1.0.0", + "identifier": "com.test.app", + "build": { + "runner": { + "cmd": "my_custom_cargo", + "cwd": "/tmp/build", + "args": ["--quiet", "--verbose"] + } + } + }"#; + + let config: Config = serde_json::from_str(json).unwrap(); + let runner = config.build.runner.unwrap(); + + assert_eq!(runner.cmd(), "my_custom_cargo"); + assert_eq!(runner.cwd(), Some("/tmp/build")); + assert_eq!( + runner.args(), + Some(&["--quiet".to_string(), "--verbose".to_string()][..]) + ); + } + + #[test] + fn test_runner_config_equality() { + use super::RunnerConfig; + + let runner1 = RunnerConfig::String("cargo".to_string()); + let runner2 = RunnerConfig::String("cargo".to_string()); + let runner3 = RunnerConfig::String("cross".to_string()); + + assert_eq!(runner1, runner2); + assert_ne!(runner1, runner3); + + let runner4 = RunnerConfig::Object { + cmd: "cargo".to_string(), + cwd: Some("/tmp".to_string()), + args: Some(vec!["--quiet".to_string()]), + }; + let runner5 = RunnerConfig::Object { + cmd: "cargo".to_string(), + cwd: Some("/tmp".to_string()), + args: Some(vec!["--quiet".to_string()]), + }; + + assert_eq!(runner4, runner5); + assert_ne!(runner1, runner4); + } + + #[test] + fn test_runner_config_untagged_serialization() { + use super::RunnerConfig; + + // Test that serde untagged works correctly - string should serialize as string, not object + let string_runner = RunnerConfig::String("cargo".to_string()); + let string_json = serde_json::to_string(&string_runner).unwrap(); + assert_eq!(string_json, r#""cargo""#); + + // Test that object serializes as object + let object_runner = RunnerConfig::Object { + cmd: "cross".to_string(), + cwd: None, + args: None, + }; + let object_json = serde_json::to_string(&object_runner).unwrap(); + assert!(object_json.contains("\"cmd\":\"cross\"")); + // With skip_serializing_none, null values should not be included + assert!(object_json.contains("\"cwd\":null") || !object_json.contains("cwd")); + assert!(object_json.contains("\"args\":null") || !object_json.contains("args")); + } }