Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .changes/auto-increment-android-version-code.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"tauri-cli": minor:feat
"@tauri-apps/cli": minor:feat
"tauri-build": minor:feat
"tauri-utils": minor:feat
---

Add `tauri.conf.json > bundle > android > autoIncrementVersionCode` config option to automatically increment the Android version code.
2 changes: 1 addition & 1 deletion crates/tauri-build/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -499,7 +499,7 @@ pub fn try_build(attributes: Attributes) -> Result<()> {
println!("cargo:rustc-env=TAURI_ANDROID_PACKAGE_NAME_PREFIX={android_package_prefix}");

if let Some(project_dir) = env::var_os("TAURI_ANDROID_PROJECT_PATH").map(PathBuf::from) {
mobile::generate_gradle_files(project_dir, &config)?;
mobile::generate_gradle_files(project_dir)?;
}

cfg_alias("dev", is_dev());
Expand Down
57 changes: 3 additions & 54 deletions crates/tauri-build/src/mobile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,21 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT

use std::{fs::write, path::PathBuf};
use std::path::PathBuf;

use anyhow::{Context, Result};
use semver::Version;
use tauri_utils::{config::Config, write_if_changed};
use tauri_utils::write_if_changed;

use crate::is_dev;

pub fn generate_gradle_files(project_dir: PathBuf, config: &Config) -> Result<()> {
pub fn generate_gradle_files(project_dir: PathBuf) -> Result<()> {
let gradle_settings_path = project_dir.join("tauri.settings.gradle");
let app_build_gradle_path = project_dir.join("app").join("tauri.build.gradle.kts");
let app_tauri_properties_path = project_dir.join("app").join("tauri.properties");

let mut gradle_settings =
"// THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.\n".to_string();
let mut app_build_gradle = "// THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
val implementation by configurations
dependencies {"
.to_string();
let mut app_tauri_properties = Vec::new();

for (env, value) in std::env::vars_os() {
let env = env.to_string_lossy();
Expand Down Expand Up @@ -54,61 +49,15 @@ dependencies {"

app_build_gradle.push_str("\n}");

if let Some(version) = config.version.as_ref() {
app_tauri_properties.push(format!("tauri.android.versionName={version}"));
if let Some(version_code) = config.bundle.android.version_code.as_ref() {
app_tauri_properties.push(format!("tauri.android.versionCode={version_code}"));
} else if let Ok(version) = Version::parse(version) {
let mut version_code = version.major * 1000000 + version.minor * 1000 + version.patch;

if is_dev() {
version_code = version_code.clamp(1, 2100000000);
}

if version_code == 0 {
return Err(anyhow::anyhow!(
"You must change the `version` in `tauri.conf.json`. The default value `0.0.0` is not allowed for Android package and must be at least `0.0.1`."
));
} else if version_code > 2100000000 {
return Err(anyhow::anyhow!(
"Invalid version code {}. Version code must be between 1 and 2100000000. You must change the `version` in `tauri.conf.json`.",
version_code
));
}

app_tauri_properties.push(format!("tauri.android.versionCode={version_code}"));
}
}

// Overwrite only if changed to not trigger rebuilds
write_if_changed(&gradle_settings_path, gradle_settings)
.context("failed to write tauri.settings.gradle")?;

write_if_changed(&app_build_gradle_path, app_build_gradle)
.context("failed to write tauri.build.gradle.kts")?;

if !app_tauri_properties.is_empty() {
let app_tauri_properties_content = format!(
"// THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.\n{}",
app_tauri_properties.join("\n")
);
if std::fs::read_to_string(&app_tauri_properties_path)
.map(|o| o != app_tauri_properties_content)
.unwrap_or(true)
{
write(&app_tauri_properties_path, app_tauri_properties_content)
.context("failed to write tauri.properties")?;
}
}

println!("cargo:rerun-if-changed={}", gradle_settings_path.display());
println!("cargo:rerun-if-changed={}", app_build_gradle_path.display());
if !app_tauri_properties.is_empty() {
println!(
"cargo:rerun-if-changed={}",
app_tauri_properties_path.display()
);
}

Ok(())
}
7 changes: 7 additions & 0 deletions crates/tauri-cli/config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@
"default": {
"active": false,
"android": {
"autoIncrementVersionCode": false,
"minSdkVersion": 24
},
"createUpdaterArtifacts": false,
Expand Down Expand Up @@ -2282,6 +2283,7 @@
"android": {
"description": "Android configuration.",
"default": {
"autoIncrementVersionCode": false,
"minSdkVersion": 24
},
"allOf": [
Expand Down Expand Up @@ -3826,6 +3828,11 @@
"format": "uint32",
"maximum": 2100000000.0,
"minimum": 1.0
},
"autoIncrementVersionCode": {
"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"
}
},
"additionalProperties": false
Expand Down
8 changes: 7 additions & 1 deletion crates/tauri-cli/src/mobile/android/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ use crate::{
flock,
},
interface::{AppInterface, Interface, Options as InterfaceOptions},
mobile::{write_options, CliOptions, TargetDevice},
mobile::{android::generate_tauri_properties, write_options, CliOptions, TargetDevice},
ConfigValue, Error, Result,
};
use clap::{ArgAction, Parser};
Expand Down Expand Up @@ -178,6 +178,12 @@ pub fn command(options: Options, noise_level: NoiseLevel) -> Result<BuiltApplica
let mut env = env(options.ci)?;
configure_cargo(&mut env, &config)?;

generate_tauri_properties(
&config,
tauri_config.lock().unwrap().as_ref().unwrap(),
false,
)?;

crate::build::setup(&interface, &mut build_options, tauri_config.clone(), true)?;

let installed_targets =
Expand Down
6 changes: 4 additions & 2 deletions crates/tauri-cli/src/mobile/android/dev.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ use crate::{
},
interface::{AppInterface, Interface, MobileOptions, Options as InterfaceOptions},
mobile::{
use_network_address_for_dev_url, write_options, CliOptions, DevChild, DevHost, DevProcess,
TargetDevice,
android::generate_tauri_properties, use_network_address_for_dev_url, write_options, CliOptions,
DevChild, DevHost, DevProcess, TargetDevice,
},
ConfigValue, Error, Result,
};
Expand Down Expand Up @@ -271,6 +271,8 @@ fn run_dev(

configure_cargo(&mut env, config)?;

generate_tauri_properties(config, tauri_config.lock().unwrap().as_ref().unwrap(), true)?;

let installed_targets =
crate::interface::rust::installation::installed_targets().unwrap_or_default();

Expand Down
64 changes: 64 additions & 0 deletions crates/tauri-cli/src/mobile/android/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ use cargo_mobile2::{
util::prompt,
};
use clap::{Parser, Subcommand};
use semver::Version;
use std::{
env::set_var,
fs::{create_dir, create_dir_all, read_dir, write},
Expand Down Expand Up @@ -620,3 +621,66 @@ fn configure_cargo(env: &mut Env, config: &AndroidConfig) -> Result<()> {

Ok(())
}

pub fn generate_tauri_properties(
config: &AndroidConfig,
tauri_config: &TauriConfig,
dev: bool,
) -> Result<()> {
let app_tauri_properties_path = config.project_dir().join("app").join("tauri.properties");

let mut app_tauri_properties = Vec::new();
if let Some(version) = tauri_config.version.as_ref() {
app_tauri_properties.push(format!("tauri.android.versionName={version}"));
if tauri_config.bundle.android.auto_increment_version_code {
let last_version_code = std::fs::read_to_string(&app_tauri_properties_path)
.ok()
.and_then(|content| {
content
.lines()
.find(|line| line.starts_with("tauri.android.versionCode="))
.and_then(|line| line.split('=').nth(1))
.and_then(|s| s.trim().parse::<u32>().ok())
});
let new_version_code = last_version_code.map(|v| v.saturating_add(1)).unwrap_or(1);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Bug: Auto-incremented version code has no Android range validation

The auto-increment path (lines 635-646) computes new_version_code via saturating_add(1) but never validates that the result is within Android's allowed range [1, 2_100_000_000].

In contrast, the semver-derived path (lines 649-667) explicitly validates the range and returns clear error messages.

If a user has been building frequently (or the tauri.properties file was manually edited with a high value), the auto-incremented version code can exceed 2,100,000,000. This will produce a tauri.properties with an invalid version code that Android/Google Play will reject, with no helpful error from Tauri.

Additionally, saturating_add on u32::MAX returns u32::MAX itself, which means the version code silently stops incrementing without any warning.

Suggested fix: Add range validation after computing new_version_code, similar to the semver path:

let new_version_code = last_version_code.map(|v| v.saturating_add(1)).unwrap_or(1);
if new_version_code > 2100000000 {
    crate::error::bail!(
        "Auto-incremented version code {} exceeds Android's maximum of 2100000000. \
         Please reset the versionCode in tauri.properties.",
        new_version_code
    );
}

Was this helpful? React with 👍 / 👎

Suggested change
let new_version_code = last_version_code.map(|v| v.saturating_add(1)).unwrap_or(1);
let new_version_code = last_version_code.map(|v| v.saturating_add(1)).unwrap_or(1);
if new_version_code > 2100000000 {
crate::error::bail!(
"Auto-incremented version code {} exceeds Android's maximum of 2100000000. Please reset the versionCode in tauri.properties.",
new_version_code
);
}
app_tauri_properties.push(format!("tauri.android.versionCode={new_version_code}"));
  • Apply suggested fix

app_tauri_properties.push(format!("tauri.android.versionCode={new_version_code}"));
} else if let Some(version_code) = tauri_config.bundle.android.version_code.as_ref() {
app_tauri_properties.push(format!("tauri.android.versionCode={version_code}"));
} else if let Ok(version) = Version::parse(version) {
let mut version_code = version.major * 1000000 + version.minor * 1000 + version.patch;

if version_code == 0 {
crate::error::bail!(
"You must change the `version` in `tauri.conf.json`. The default value `0.0.0` is not allowed for Android package and must be at least `0.0.1`."
);
} else if version_code > 2100000000 {
crate::error::bail!(
"Invalid version code {}. Version code must be between 1 and 2100000000. You must change the `version` in `tauri.conf.json`.",
version_code
);
}

if dev {
version_code = version_code.clamp(1, 2100000000);
}

app_tauri_properties.push(format!("tauri.android.versionCode={version_code}"));
}
}

if !app_tauri_properties.is_empty() {
let app_tauri_properties_content = format!(
"// THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.\n{}",
app_tauri_properties.join("\n")
);
if std::fs::read_to_string(&app_tauri_properties_path)
.map(|o| app_tauri_properties_content != o)
.unwrap_or(true)
{
write(&app_tauri_properties_path, app_tauri_properties_content)
.context("failed to write tauri.properties")?;
}
}

Ok(())
}
7 changes: 7 additions & 0 deletions crates/tauri-schema-generator/schemas/config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@
"default": {
"active": false,
"android": {
"autoIncrementVersionCode": false,
"minSdkVersion": 24
},
"createUpdaterArtifacts": false,
Expand Down Expand Up @@ -2282,6 +2283,7 @@
"android": {
"description": "Android configuration.",
"default": {
"autoIncrementVersionCode": false,
"minSdkVersion": 24
},
"allOf": [
Expand Down Expand Up @@ -3826,6 +3828,11 @@
"format": "uint32",
"maximum": 2100000000.0,
"minimum": 1.0
},
"autoIncrementVersionCode": {
"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"
}
},
"additionalProperties": false
Expand Down
11 changes: 11 additions & 0 deletions crates/tauri-utils/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2929,13 +2929,24 @@ pub struct AndroidConfig {
#[serde(alias = "version-code")]
#[cfg_attr(feature = "schema", validate(range(min = 1, max = 2_100_000_000)))]
pub version_code: Option<u32>,

/// Whether to automatically increment the `versionCode` on each build.
///
/// - If `true`, the generator will try to read the last `versionCode` from
/// `tauri.properties` and increment it by 1 for every build.
/// - If `false` or not set, it falls back to `version_code` or semver-derived logic.
///
/// 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,
}

impl Default for AndroidConfig {
fn default() -> Self {
Self {
min_sdk_version: default_min_sdk_version(),
version_code: None,
auto_increment_version_code: false,
}
}
}
Expand Down