From b240341b1c8074cf6caf020472b3a41866677376 Mon Sep 17 00:00:00 2001 From: Tony <68118705+Legend-Master@users.noreply.github.com> Date: Thu, 30 Apr 2026 17:06:17 +0800 Subject: [PATCH 1/2] fix(deps): update specta in lockfile (#15303) Co-authored-by: Oscar Beaumont --- Cargo.lock | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b4d6c8dd625c..80f882b31ec7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8325,20 +8325,20 @@ dependencies = [ [[package]] name = "specta" -version = "2.0.0-rc.20" +version = "2.0.0-rc.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ccbb212565d2dc177bc15ecb7b039d66c4490da892436a4eee5b394d620c9bc" +checksum = "f320c7dd82008b6958f43f6257c95319c407d1c17ade43686e50ea520c28bb26" dependencies = [ "paste", + "rustc_version", "specta-macros", - "thiserror 1.0.69", ] [[package]] name = "specta-macros" -version = "2.0.0-rc.17" +version = "2.0.0-rc.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68999d29816965eb9e5201f60aec02a76512139811661a7e8e653abc810b8f72" +checksum = "153f185d0051a64d81977bab5012809d5c9d9db8792406a0997352e05494f711" dependencies = [ "Inflector", "proc-macro2", From e55492df6e518475886ef04897fb0c4b55e1eab8 Mon Sep 17 00:00:00 2001 From: Andrew de Waal Date: Thu, 30 Apr 2026 02:55:06 -0700 Subject: [PATCH 2/2] feat: Add support for Android build variants (feat #14777) (#14886) * feat: Add support for Android build variants (feat #14777) * synchronize with build.gradle.kts * update cargo-mobile2 * change to a applicationIdSuffix map to support more variants * Revert "change to a applicationIdSuffix map to support more variants" This reverts commit d251c31fe629ce5a907b12e46d9e7ff0d6de3140. * do not apply .debug suffix by default for existing projects * kotlin raw string support --------- Co-authored-by: Lucas Nogueira --- Cargo.lock | 4 +- crates/tauri-cli/Cargo.toml | 2 +- crates/tauri-cli/config.schema.json | 7 + crates/tauri-cli/schema.json | 7 + crates/tauri-cli/src/mobile/android/build.rs | 3 +- crates/tauri-cli/src/mobile/android/dev.rs | 29 +- crates/tauri-cli/src/mobile/android/mod.rs | 366 ++++++++++++++++++ crates/tauri-cli/src/mobile/android/run.rs | 22 +- crates/tauri-cli/src/mobile/init.rs | 7 + crates/tauri-cli/tauri.config.schema.json | 9 +- .../mobile/android/app/build.gradle.kts | 3 + crates/tauri-cli/templates/tauri.conf.json | 5 +- .../schemas/config.schema.json | 7 + crates/tauri-utils/src/config.rs | 7 + 14 files changed, 464 insertions(+), 14 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 80f882b31ec7..11cd62da6e0d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1055,9 +1055,9 @@ dependencies = [ [[package]] name = "cargo-mobile2" -version = "0.22.3" +version = "0.22.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4409d8c4087e66ff08bae7497ef311dbdedcf9903049d66228056f99ebd2a19b" +checksum = "caa0060b6b7e1c0f14312c8ccebf28f5713b3045ffaae033a180622122a361dc" dependencies = [ "colored", "core-foundation 0.10.0", diff --git a/crates/tauri-cli/Cargo.toml b/crates/tauri-cli/Cargo.toml index 9e216f6c55a3..fa460e15a68a 100644 --- a/crates/tauri-cli/Cargo.toml +++ b/crates/tauri-cli/Cargo.toml @@ -36,7 +36,7 @@ name = "cargo-tauri" path = "src/main.rs" [target."cfg(any(target_os = \"linux\", target_os = \"dragonfly\", target_os = \"freebsd\", target_os = \"openbsd\", target_os = \"netbsd\", target_os = \"windows\", target_os = \"macos\"))".dependencies] -cargo-mobile2 = { version = "0.22.3", default-features = false } +cargo-mobile2 = { version = "0.22.4", default-features = false } [dependencies] jsonrpsee = { version = "0.24", features = ["server"] } diff --git a/crates/tauri-cli/config.schema.json b/crates/tauri-cli/config.schema.json index 6878c2b2adc8..6b0ec229bd96 100644 --- a/crates/tauri-cli/config.schema.json +++ b/crates/tauri-cli/config.schema.json @@ -3919,6 +3919,13 @@ "description": "Whether to automatically increment the `versionCode` on each build.\n\n - If `true`, the generator will try to read the last `versionCode` from\n `tauri.properties` and increment it by 1 for every build.\n - If `false` or not set, it falls back to `version_code` or semver-derived logic.\n\n Note that to use this feature, you should remove `/tauri.properties` from `src-tauri/gen/android/app/.gitignore` so the current versionCode is committed to the repository.", "default": false, "type": "boolean" + }, + "debugApplicationIdSuffix": { + "description": "Application ID suffix to append for debug builds.\n This allows installing debug and release versions side-by-side on the same device.\n Example: \".debug\" will make debug builds use \"com.example.app.debug\" as the application ID.", + "type": [ + "string", + "null" + ] } }, "additionalProperties": false diff --git a/crates/tauri-cli/schema.json b/crates/tauri-cli/schema.json index 6bd2b3bc87bf..903f05633e63 100644 --- a/crates/tauri-cli/schema.json +++ b/crates/tauri-cli/schema.json @@ -3095,6 +3095,13 @@ "format": "uint32", "maximum": 2100000000.0, "minimum": 1.0 + }, + "debugApplicationIdSuffix": { + "description": "Application ID suffix to append for debug builds.\n This allows installing debug and release versions side-by-side on the same device.\n Example: \".debug\" will make debug builds use \"com.example.app.debug\" as the application ID.", + "type": [ + "string", + "null" + ] } }, "additionalProperties": false diff --git a/crates/tauri-cli/src/mobile/android/build.rs b/crates/tauri-cli/src/mobile/android/build.rs index 3a4369402a8a..7d5f39c8d076 100644 --- a/crates/tauri-cli/src/mobile/android/build.rs +++ b/crates/tauri-cli/src/mobile/android/build.rs @@ -4,7 +4,7 @@ use super::{ configure_cargo, delete_codegen_vars, ensure_init, env, get_app, get_config, inject_resources, - log_finished, open_and_wait, MobileTarget, OptionsHandle, + log_finished, open_and_wait, sync_debug_application_id_suffix, MobileTarget, OptionsHandle, }; use crate::{ build::Options as BuildOptions, @@ -192,6 +192,7 @@ pub fn run( configure_cargo(&mut env, &config)?; generate_tauri_properties(&config, tauri_config, false)?; + sync_debug_application_id_suffix(&config, tauri_config)?; crate::build::setup(&interface, &mut build_options, tauri_config, dirs, true)?; diff --git a/crates/tauri-cli/src/mobile/android/dev.rs b/crates/tauri-cli/src/mobile/android/dev.rs index 93692f6d0454..dec1eea932a7 100644 --- a/crates/tauri-cli/src/mobile/android/dev.rs +++ b/crates/tauri-cli/src/mobile/android/dev.rs @@ -4,7 +4,7 @@ use super::{ configure_cargo, delete_codegen_vars, device_prompt, ensure_init, env, get_app, get_config, - inject_resources, open_and_wait, MobileTarget, + inject_resources, open_and_wait, sync_debug_application_id_suffix, MobileTarget, }; use crate::{ dev::Options as DevOptions, @@ -274,6 +274,7 @@ fn run_dev( configure_cargo(&mut env, config)?; generate_tauri_properties(config, &tauri_config, true)?; + sync_debug_application_id_suffix(config, &tauri_config)?; let installed_targets = crate::interface::rust::installation::installed_targets().unwrap_or_default(); @@ -339,7 +340,15 @@ fn run_dev( if open { open_and_wait(config, &env) } else if let Some(device) = &device { - match run(device, options, config, &env, metadata, noise_level) { + match run( + device, + options, + config, + &env, + metadata, + noise_level, + tauri_config, + ) { Ok(c) => Ok(Box::new(c) as Box), Err(e) => { crate::dev::kill_before_dev_process(); @@ -361,6 +370,7 @@ fn run( env: &Env, metadata: &AndroidMetadata, noise_level: NoiseLevel, + tauri_config: &tauri_utils::config::Config, ) -> crate::Result { let profile = if options.debug { Profile::Debug @@ -370,8 +380,18 @@ fn run( let build_app_bundle = metadata.asset_packs().is_some(); + let application_id_suffix = if profile == Profile::Debug { + tauri_config + .bundle + .android + .debug_application_id_suffix + .clone() + } else { + None + }; + device - .run( + .run_with_application_id_suffix( config, env, noise_level, @@ -383,7 +403,8 @@ fn run( }), build_app_bundle, false, - ".MainActivity".into(), + format!("{}.MainActivity", config.app().identifier()), + application_id_suffix, ) .map(DevChild::new) .context("failed to run Android app") diff --git a/crates/tauri-cli/src/mobile/android/mod.rs b/crates/tauri-cli/src/mobile/android/mod.rs index 8ea6490d02c6..fc7406832660 100644 --- a/crates/tauri-cli/src/mobile/android/mod.rs +++ b/crates/tauri-cli/src/mobile/android/mod.rs @@ -25,6 +25,7 @@ use std::{ io::Cursor, path::{Path, PathBuf}, process::{exit, Command}, + sync::OnceLock, thread::sleep, time::Duration, }; @@ -188,6 +189,247 @@ pub fn get_config( (config, metadata) } +fn sync_debug_application_id_suffix( + config: &AndroidConfig, + tauri_config: &TauriConfig, +) -> Result<()> { + let build_gradle_path = config.project_dir().join("app").join("build.gradle.kts"); + let build_gradle = std::fs::read_to_string(&build_gradle_path).fs_context( + "failed to read Android Gradle build file", + build_gradle_path.clone(), + )?; + let Some(updated_build_gradle) = set_debug_application_id_suffix( + &build_gradle, + tauri_config + .bundle + .android + .debug_application_id_suffix + .as_deref(), + ) else { + crate::error::bail!( + "Could not find the Android debug build type in {}. Add a `getByName(\"debug\")` build type or run `tauri android init` to regenerate the Android project.", + build_gradle_path.display() + ); + }; + + if updated_build_gradle != build_gradle { + write(&build_gradle_path, updated_build_gradle).fs_context( + "failed to write Android Gradle build file", + build_gradle_path, + )?; + } + + Ok(()) +} + +fn set_debug_application_id_suffix(build_gradle: &str, suffix: Option<&str>) -> Option { + static DEBUG_BUILD_TYPE_RE: OnceLock = OnceLock::new(); + + let debug_build_type_re = DEBUG_BUILD_TYPE_RE.get_or_init(|| { + regex::Regex::new(r#"(?m)(?:\bgetByName\(\s*"debug"\s*\)|\bdebug\b)\s*\{"#) + .expect("valid debug build type regex") + }); + + for build_type_match in debug_build_type_re.find_iter(build_gradle) { + let Some(opening_brace) = build_gradle[build_type_match.start()..] + .find('{') + .map(|index| build_type_match.start() + index) + else { + continue; + }; + let Some(closing_brace) = find_matching_brace(build_gradle, opening_brace) else { + continue; + }; + + let debug_block = &build_gradle[opening_brace..closing_brace]; + let updated_debug_block = set_application_id_suffix_in_block(debug_block, suffix); + let mut updated_build_gradle = + String::with_capacity(build_gradle.len() + updated_debug_block.len()); + updated_build_gradle.push_str(&build_gradle[..opening_brace]); + updated_build_gradle.push_str(&updated_debug_block); + updated_build_gradle.push_str(&build_gradle[closing_brace..]); + return Some(updated_build_gradle); + } + + None +} + +fn set_application_id_suffix_in_block(debug_block: &str, suffix: Option<&str>) -> String { + static APPLICATION_ID_SUFFIX_RE: OnceLock = OnceLock::new(); + + let application_id_suffix_re = APPLICATION_ID_SUFFIX_RE.get_or_init(|| { + regex::Regex::new(r#"(?m)^[ \t]*applicationIdSuffix\s*=.*(?:\r?\n)?"#) + .expect("valid applicationIdSuffix regex") + }); + + if let Some(application_id_suffix_match) = application_id_suffix_re.find(debug_block) { + let mut updated_debug_block = String::with_capacity(debug_block.len()); + updated_debug_block.push_str(&debug_block[..application_id_suffix_match.start()]); + if let Some(suffix) = suffix { + let indentation = debug_block + [application_id_suffix_match.start()..application_id_suffix_match.end()] + .chars() + .take_while(|character| *character == ' ' || *character == '\t') + .collect::(); + updated_debug_block.push_str(&format!( + "{indentation}applicationIdSuffix = \"{}\"\n", + escape_kotlin_string(suffix) + )); + } + updated_debug_block.push_str(&debug_block[application_id_suffix_match.end()..]); + return updated_debug_block; + } + + let Some(suffix) = suffix else { + return debug_block.to_string(); + }; + + let indentation = debug_block_indentation(debug_block); + let application_id_suffix = format!( + "{indentation}applicationIdSuffix = \"{}\"\n", + escape_kotlin_string(suffix) + ); + + if let Some(first_newline) = debug_block.find('\n') { + let mut updated_debug_block = + String::with_capacity(debug_block.len() + application_id_suffix.len()); + updated_debug_block.push_str(&debug_block[..=first_newline]); + updated_debug_block.push_str(&application_id_suffix); + updated_debug_block.push_str(&debug_block[first_newline + 1..]); + updated_debug_block + } else { + format!("{{\n{application_id_suffix}") + } +} + +fn debug_block_indentation(debug_block: &str) -> &str { + debug_block + .lines() + .skip(1) + .find_map(|line| { + if line.trim().is_empty() { + None + } else { + Some(line.trim_end().trim_end_matches(line.trim_start())) + } + }) + .unwrap_or(" ") +} + +fn find_matching_brace(content: &str, opening_brace: usize) -> Option { + let mut depth = 0u32; + let mut in_line_comment = false; + let mut in_block_comment = false; + let mut in_string = false; + let mut in_raw_string = false; + let mut string_quote = '\0'; + let mut escaped = false; + let mut previous = '\0'; + let mut chars = content[opening_brace..].char_indices().peekable(); + + while let Some((relative_index, character)) = chars.next() { + let index = opening_brace + relative_index; + + if in_line_comment { + if character == '\n' { + in_line_comment = false; + } + previous = character; + continue; + } + + if in_block_comment { + if previous == '*' && character == '/' { + in_block_comment = false; + } + previous = character; + continue; + } + + if in_raw_string { + if content[index..].starts_with("\"\"\"") { + // Consume the remaining two quotes in the Kotlin raw string delimiter. + let _ = chars.next(); + let _ = chars.next(); + in_raw_string = false; + } + previous = character; + continue; + } + + if in_string { + if escaped { + escaped = false; + } else if character == '\\' { + escaped = true; + } else if character == string_quote { + in_string = false; + } + previous = character; + continue; + } + + if character == '/' && chars.peek().is_some_and(|(_, next)| *next == '/') { + in_line_comment = true; + previous = character; + continue; + } + + if character == '/' && chars.peek().is_some_and(|(_, next)| *next == '*') { + in_block_comment = true; + previous = character; + continue; + } + + if content[index..].starts_with("\"\"\"") { + // Consume the remaining two quotes in the Kotlin raw string delimiter. + let _ = chars.next(); + let _ = chars.next(); + in_raw_string = true; + previous = character; + continue; + } + + if character == '"' || character == '\'' { + in_string = true; + string_quote = character; + previous = character; + continue; + } + + if character == '{' { + depth = depth.saturating_add(1); + } else if character == '}' { + depth = depth.saturating_sub(1); + if depth == 0 { + return Some(index); + } + } + + previous = character; + } + + None +} + +fn escape_kotlin_string(value: &str) -> String { + let mut output = String::with_capacity(value.len()); + + for character in value.chars() { + match character { + '"' => output.push_str("\\\""), + '\\' => output.push_str("\\\\"), + '$' => output.push_str("\\$"), + '\n' => output.push_str("\\n"), + '\r' => output.push_str("\\r"), + '\t' => output.push_str("\\t"), + other => output.push(other), + } + } + + output +} + pub fn env(non_interactive: bool) -> Result { let env = super::env().context("failed to setup Android environment")?; ensure_env(non_interactive).context("failed to ensure Android environment")?; @@ -676,3 +918,127 @@ fn generate_tauri_properties( Ok(()) } + +#[cfg(test)] +mod tests { + use super::{find_matching_brace, set_debug_application_id_suffix}; + + #[test] + fn writes_debug_application_id_suffix() { + let build_gradle = r#" +android { + buildTypes { + getByName("debug") { + manifestPlaceholders["usesCleartextTraffic"] = "true" + } + } +} +"#; + + let updated = set_debug_application_id_suffix(build_gradle, Some(".debug")).unwrap(); + + assert!(updated.contains( + r#" getByName("debug") { + applicationIdSuffix = ".debug" + manifestPlaceholders["usesCleartextTraffic"] = "true""# + )); + } + + #[test] + fn replaces_debug_application_id_suffix() { + let build_gradle = r#" +android { + buildTypes { + getByName("debug") { + applicationIdSuffix = ".old" + manifestPlaceholders["usesCleartextTraffic"] = "true" + } + } +} +"#; + + let updated = set_debug_application_id_suffix(build_gradle, Some(".internal")).unwrap(); + + assert!(updated.contains(r#" applicationIdSuffix = ".internal""#)); + assert!(!updated.contains(r#".old"#)); + } + + #[test] + fn removes_debug_application_id_suffix() { + let build_gradle = r#" +android { + buildTypes { + getByName("debug") { + applicationIdSuffix = ".debug" + } + getByName("release") { + applicationIdSuffix = ".release" + } + } +} +"#; + + let updated = set_debug_application_id_suffix(build_gradle, None).unwrap(); + + assert!(!updated.contains(r#"applicationIdSuffix = ".debug""#)); + assert!(updated.contains(r#"applicationIdSuffix = ".release""#)); + } + + #[test] + fn writes_debug_suffix_before_nested_blocks() { + let build_gradle = r#" +android { + buildTypes { + debug { + packaging { + jniLibs.keepDebugSymbols.add("*/arm64-v8a/*.so") + } + } + } +} +"#; + + let updated = set_debug_application_id_suffix(build_gradle, Some(".internal")).unwrap(); + + assert!(updated.contains( + r#" debug { + applicationIdSuffix = ".internal" + packaging {"# + )); + } + + #[test] + fn ignores_braces_inside_kotlin_raw_strings() { + let build_gradle = r#" +android { + buildTypes { + debug { + val proguardRules = """ + -if class ** { + public *; + } + """ + manifestPlaceholders["usesCleartextTraffic"] = "true" + } + } +} +"#; + + let opening_brace = build_gradle + .find("debug {") + .and_then(|index| build_gradle[index..].find('{').map(|brace| index + brace)) + .unwrap(); + let closing_brace = find_matching_brace(build_gradle, opening_brace).unwrap(); + + assert!(build_gradle[opening_brace..closing_brace] + .contains(r#"manifestPlaceholders["usesCleartextTraffic"] = "true""#)); + + let updated = set_debug_application_id_suffix(build_gradle, Some(".debug")).unwrap(); + + assert!(updated.contains( + r#" debug { + applicationIdSuffix = ".debug" + val proguardRules = """"# + )); + } +} diff --git a/crates/tauri-cli/src/mobile/android/run.rs b/crates/tauri-cli/src/mobile/android/run.rs index 2e6c7ba012fe..f459a88e1297 100644 --- a/crates/tauri-cli/src/mobile/android/run.rs +++ b/crates/tauri-cli/src/mobile/android/run.rs @@ -10,7 +10,7 @@ use cargo_mobile2::{ use clap::{ArgAction, Parser}; use std::path::PathBuf; -use super::{configure_cargo, device_prompt, env}; +use super::{configure_cargo, device_prompt, env, sync_debug_application_id_suffix}; use crate::{ error::Context, helpers::config::ConfigMetadata, @@ -125,9 +125,22 @@ pub fn command(options: Options, noise_level: NoiseLevel) -> Result<()> { if let Some(device) = device { let config = built_application.config.clone(); let release = options.release; - let runner = move |_tauri_config: &ConfigMetadata| { + + let runner = move |tauri_config: &ConfigMetadata| { + sync_debug_application_id_suffix(&config, tauri_config)?; + + let application_id_suffix = if !release { + tauri_config + .bundle + .android + .debug_application_id_suffix + .clone() + } else { + None + }; + device - .run( + .run_with_application_id_suffix( &config, &env, noise_level, @@ -143,7 +156,8 @@ pub fn command(options: Options, noise_level: NoiseLevel) -> Result<()> { }), false, false, - ".MainActivity".into(), + format!("{}.MainActivity", config.app().identifier()), + application_id_suffix, ) .map(|c| Box::new(DevChild::new(c)) as Box) .context("failed to run Android app") diff --git a/crates/tauri-cli/src/mobile/init.rs b/crates/tauri-cli/src/mobile/init.rs index 7ace53b512a4..db7fa52f5d29 100644 --- a/crates/tauri-cli/src/mobile/init.rs +++ b/crates/tauri-cli/src/mobile/init.rs @@ -140,6 +140,13 @@ fn exec( let (config, metadata) = super::android::get_config(&app, &tauri_config, &[], &Default::default()); map.insert("android", &config); + + // Add application_id_suffix to the map for template access + // The template will access it via a helper or we'll modify template to use root context + if let Some(suffix) = &tauri_config.bundle.android.debug_application_id_suffix { + map.insert("android-debug-application-id-suffix", suffix); + } + super::android::project::gen( &config, &metadata, diff --git a/crates/tauri-cli/tauri.config.schema.json b/crates/tauri-cli/tauri.config.schema.json index b6c64b09b99b..ea575a22234f 100644 --- a/crates/tauri-cli/tauri.config.schema.json +++ b/crates/tauri-cli/tauri.config.schema.json @@ -3081,7 +3081,7 @@ "additionalProperties": false }, "AndroidConfig": { - "description": "General configuration for the iOS target.", + "description": "General configuration for the Android target.", "type": "object", "properties": { "minSdkVersion": { @@ -3100,6 +3100,13 @@ "format": "uint32", "maximum": 2100000000.0, "minimum": 1.0 + }, + "debugApplicationIdSuffix": { + "description": "Application ID suffix to append for debug builds.\n This allows installing debug and release versions side-by-side on the same device.\n Example: \".debug\" will make debug builds use \"com.example.app.debug\" as the application ID.", + "type": [ + "string", + "null" + ] } }, "additionalProperties": false diff --git a/crates/tauri-cli/templates/mobile/android/app/build.gradle.kts b/crates/tauri-cli/templates/mobile/android/app/build.gradle.kts index b1485863abf9..0f73195fa008 100644 --- a/crates/tauri-cli/templates/mobile/android/app/build.gradle.kts +++ b/crates/tauri-cli/templates/mobile/android/app/build.gradle.kts @@ -28,6 +28,9 @@ android { } buildTypes { getByName("debug") { + {{#if android-debug-application-id-suffix}} + applicationIdSuffix = "{{android-debug-application-id-suffix}}" + {{/if}} manifestPlaceholders["usesCleartextTraffic"] = "true" isDebuggable = true isJniDebuggable = true diff --git a/crates/tauri-cli/templates/tauri.conf.json b/crates/tauri-cli/templates/tauri.conf.json index 6b8354f78464..048fc0f00d8c 100644 --- a/crates/tauri-cli/templates/tauri.conf.json +++ b/crates/tauri-cli/templates/tauri.conf.json @@ -32,6 +32,9 @@ "icons/128x128@2x.png", "icons/icon.icns", "icons/icon.ico" - ] + ], + "android": { + "debugApplicationIdSuffix": ".debug" + } } } diff --git a/crates/tauri-schema-generator/schemas/config.schema.json b/crates/tauri-schema-generator/schemas/config.schema.json index 6878c2b2adc8..6b0ec229bd96 100644 --- a/crates/tauri-schema-generator/schemas/config.schema.json +++ b/crates/tauri-schema-generator/schemas/config.schema.json @@ -3919,6 +3919,13 @@ "description": "Whether to automatically increment the `versionCode` on each build.\n\n - If `true`, the generator will try to read the last `versionCode` from\n `tauri.properties` and increment it by 1 for every build.\n - If `false` or not set, it falls back to `version_code` or semver-derived logic.\n\n Note that to use this feature, you should remove `/tauri.properties` from `src-tauri/gen/android/app/.gitignore` so the current versionCode is committed to the repository.", "default": false, "type": "boolean" + }, + "debugApplicationIdSuffix": { + "description": "Application ID suffix to append for debug builds.\n This allows installing debug and release versions side-by-side on the same device.\n Example: \".debug\" will make debug builds use \"com.example.app.debug\" as the application ID.", + "type": [ + "string", + "null" + ] } }, "additionalProperties": false diff --git a/crates/tauri-utils/src/config.rs b/crates/tauri-utils/src/config.rs index 78da42a4945d..a54c619e3f6f 100644 --- a/crates/tauri-utils/src/config.rs +++ b/crates/tauri-utils/src/config.rs @@ -3232,6 +3232,12 @@ pub struct AndroidConfig { /// Note that to use this feature, you should remove `/tauri.properties` from `src-tauri/gen/android/app/.gitignore` so the current versionCode is committed to the repository. #[serde(alias = "auto-increment-version-code", default)] pub auto_increment_version_code: bool, + + /// Application ID suffix to append for debug builds. + /// This allows installing debug and release versions side-by-side on the same device. + /// Example: ".debug" will make debug builds use "com.example.app.debug" as the application ID. + #[serde(alias = "debug-application-id-suffix")] + pub debug_application_id_suffix: Option, } impl Default for AndroidConfig { @@ -3240,6 +3246,7 @@ impl Default for AndroidConfig { min_sdk_version: default_min_sdk_version(), version_code: None, auto_increment_version_code: false, + debug_application_id_suffix: None, } } }