From cdb2440a7148df5527920d25607702325bea346d Mon Sep 17 00:00:00 2001 From: tenderdeve Date: Sun, 26 Jul 2026 09:46:16 +0530 Subject: [PATCH 1/7] feat(cli): warn when Java is too new for the bundled Gradle Installing a very recent JDK (e.g. Java 25/26) while Tauri ships Gradle 8.14 makes Android builds fail with a cryptic 'Failed to assemble APK' error. Detect the active Java feature version during the Android env setup and warn upfront when it exceeds what the bundled Gradle can run, pointing at the Gradle/Java compatibility matrix. Closes #15385 --- .changes/android-java-gradle-compat-hint.md | 6 ++ crates/tauri-cli/src/mobile/android/mod.rs | 70 ++++++++++++++++++++- 2 files changed, 75 insertions(+), 1 deletion(-) create mode 100644 .changes/android-java-gradle-compat-hint.md diff --git a/.changes/android-java-gradle-compat-hint.md b/.changes/android-java-gradle-compat-hint.md new file mode 100644 index 000000000000..30a06e3baa19 --- /dev/null +++ b/.changes/android-java-gradle-compat-hint.md @@ -0,0 +1,6 @@ +--- +"tauri-cli": patch:enhance +"@tauri-apps/cli": patch:enhance +--- + +Warn during Android commands (`init`/`dev`/`build`) when the active Java version is too new for the Gradle version Tauri ships (e.g. Java 25+ against Gradle 8.14), instead of letting the build fail later with a cryptic error. The warning points to the Gradle/Java compatibility matrix and suggests a supported JDK. diff --git a/crates/tauri-cli/src/mobile/android/mod.rs b/crates/tauri-cli/src/mobile/android/mod.rs index c0add29e61bc..923e78d84bf7 100644 --- a/crates/tauri-cli/src/mobile/android/mod.rs +++ b/crates/tauri-cli/src/mobile/android/mod.rs @@ -50,6 +50,12 @@ mod run; const NDK_VERSION: &str = "29.0.13846066"; const SDK_VERSION: u8 = 36; +/// Highest Java feature version that the Gradle shipped in the Android template +/// (see `templates/mobile/android/gradle/wrapper/gradle-wrapper.properties`) can +/// run on. Gradle 8.14 supports running on Java up to 24 — bump this whenever the +/// template's Gradle version changes. +/// See . +const MAX_SUPPORTED_JAVA: u32 = 24; #[cfg(target_os = "macos")] const CMDLINE_TOOLS_URL: &str = @@ -486,9 +492,50 @@ fn ensure_java() -> Result<()> { } } + if let Some(java) = java_major_version() { + if java > MAX_SUPPORTED_JAVA { + log::warn!( + "Detected Java {java}, but the bundled Gradle can only run on Java up to {MAX_SUPPORTED_JAVA}. \ + Android builds will likely fail with a cryptic error. Install a supported JDK (the Java {MAX_SUPPORTED_JAVA} \ + or an LTS release such as Java 17 or 21) and point JAVA_HOME at it. \ + See https://docs.gradle.org/current/userguide/compatibility.html" + ); + } + } + Ok(()) } +/// Detects the major (feature) version of the active Java installation by +/// running `java -version`, preferring the binary in `JAVA_HOME` if set. +fn java_major_version() -> Option { + let java = std::env::var_os("JAVA_HOME") + .map(|home| PathBuf::from(home).join("bin").join("java")) + .filter(|path| path.exists()) + .unwrap_or_else(|| PathBuf::from("java")); + + // `java -version` writes the version banner to stderr. + let output = Command::new(java).arg("-version").output().ok()?; + parse_java_major(&String::from_utf8_lossy(&output.stderr)) +} + +/// Parses the major (feature) version out of a `java -version` banner, handling +/// both the legacy `1.x` scheme (`1.8.0_292` -> 8) and the modern scheme +/// (`21.0.1` -> 21). +fn parse_java_major(version_output: &str) -> Option { + let start = version_output.find('"')?; + let rest = &version_output[start + 1..]; + let end = rest.find('"')?; + let mut parts = rest[..end].split(['.', '_', '-']); + + let first = parts.next()?; + if first == "1" { + parts.next()?.parse().ok() + } else { + first.parse().ok() + } +} + fn ensure_sdk(non_interactive: bool) -> Result<()> { let android_home = std::env::var_os("ANDROID_HOME") .or_else(|| std::env::var_os("ANDROID_SDK_ROOT")) @@ -1081,7 +1128,28 @@ fn generate_tauri_properties( #[cfg(test)] mod tests { - use super::{find_matching_brace, set_debug_application_id_suffix}; + use super::{find_matching_brace, parse_java_major, set_debug_application_id_suffix}; + + #[test] + fn parses_java_major_version() { + // modern scheme + assert_eq!( + parse_java_major("openjdk version \"21.0.1\" 2023-10-17"), + Some(21) + ); + assert_eq!( + parse_java_major("openjdk version \"26\" 2026-03-17"), + Some(26) + ); + assert_eq!( + parse_java_major("java version \"17.0.9\" 2023-10-17 LTS"), + Some(17) + ); + // legacy 1.x scheme + assert_eq!(parse_java_major("java version \"1.8.0_292\""), Some(8)); + // garbage + assert_eq!(parse_java_major("no version here"), None); + } #[test] fn writes_debug_application_id_suffix() { From 91eb1e4874e8c78e94328d4946ca80fa77e7114a Mon Sep 17 00:00:00 2001 From: tenderdeve Date: Fri, 31 Jul 2026 15:15:35 +0530 Subject: [PATCH 2/7] refactor(cli): detect Java version without spawning a subprocess Read the major version from the JDK's `release` metadata file instead of running `java -version` on every Android command, so the compatibility warning no longer adds process-spawn overhead to init/dev/build. --- crates/tauri-cli/src/mobile/android/mod.rs | 36 ++++++++++++++-------- 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/crates/tauri-cli/src/mobile/android/mod.rs b/crates/tauri-cli/src/mobile/android/mod.rs index 923e78d84bf7..4751eac43c2a 100644 --- a/crates/tauri-cli/src/mobile/android/mod.rs +++ b/crates/tauri-cli/src/mobile/android/mod.rs @@ -507,21 +507,31 @@ fn ensure_java() -> Result<()> { } /// Detects the major (feature) version of the active Java installation by -/// running `java -version`, preferring the binary in `JAVA_HOME` if set. +/// reading the `release` metadata file shipped inside every JDK, preferring +/// `JAVA_HOME` and falling back to the `java` binary on `PATH`. This avoids +/// spawning a `java -version` subprocess on every Android command. fn java_major_version() -> Option { - let java = std::env::var_os("JAVA_HOME") - .map(|home| PathBuf::from(home).join("bin").join("java")) - .filter(|path| path.exists()) - .unwrap_or_else(|| PathBuf::from("java")); - - // `java -version` writes the version banner to stderr. - let output = Command::new(java).arg("-version").output().ok()?; - parse_java_major(&String::from_utf8_lossy(&output.stderr)) + let release = std::fs::read_to_string(java_home_dir()?.join("release")).ok()?; + release + .lines() + .find_map(|line| line.strip_prefix("JAVA_VERSION=")) + .and_then(parse_java_major) +} + +/// Resolves the active JDK home directory, preferring `JAVA_HOME` and otherwise +/// deriving it from the `java` binary on `PATH` (`/bin/java` -> ``). +fn java_home_dir() -> Option { + if let Some(home) = std::env::var_os("JAVA_HOME") { + return Some(PathBuf::from(home)); + } + let java = which::which("java").ok()?; + java.parent()?.parent().map(Path::to_path_buf) } -/// Parses the major (feature) version out of a `java -version` banner, handling -/// both the legacy `1.x` scheme (`1.8.0_292` -> 8) and the modern scheme -/// (`21.0.1` -> 21). +/// Parses the major (feature) version out of a quoted Java version string (the +/// `JAVA_VERSION="..."` line of a JDK `release` file, or a `java -version` +/// banner), handling both the legacy `1.x` scheme (`1.8.0_292` -> 8) and the +/// modern scheme (`21.0.1` -> 21). fn parse_java_major(version_output: &str) -> Option { let start = version_output.find('"')?; let rest = &version_output[start + 1..]; @@ -1147,6 +1157,8 @@ mod tests { ); // legacy 1.x scheme assert_eq!(parse_java_major("java version \"1.8.0_292\""), Some(8)); + // JDK `release` file line (the path java_major_version actually feeds in) + assert_eq!(parse_java_major("\"21.0.1\""), Some(21)); // garbage assert_eq!(parse_java_major("no version here"), None); } From 7a95ab7751c3d17ad7be3fbf150425050e2b625f Mon Sep 17 00:00:00 2001 From: tenderdeve Date: Wed, 12 Aug 2026 12:06:50 +0530 Subject: [PATCH 3/7] feat(cli): derive Java/Gradle compat from the project's Gradle version Read the Gradle version from the project's gradle-wrapper.properties instead of a hardcoded template constant, so the warning stays correct when users upgrade Gradle. Skip the warning when the project or Gradle version can't be determined (and for Gradle 9+, which tracks new Java quickly). --- crates/tauri-cli/src/mobile/android/mod.rs | 70 +++++++++++++++++----- 1 file changed, 56 insertions(+), 14 deletions(-) diff --git a/crates/tauri-cli/src/mobile/android/mod.rs b/crates/tauri-cli/src/mobile/android/mod.rs index 4751eac43c2a..0637987bbac0 100644 --- a/crates/tauri-cli/src/mobile/android/mod.rs +++ b/crates/tauri-cli/src/mobile/android/mod.rs @@ -50,12 +50,6 @@ mod run; const NDK_VERSION: &str = "29.0.13846066"; const SDK_VERSION: u8 = 36; -/// Highest Java feature version that the Gradle shipped in the Android template -/// (see `templates/mobile/android/gradle/wrapper/gradle-wrapper.properties`) can -/// run on. Gradle 8.14 supports running on Java up to 24 — bump this whenever the -/// template's Gradle version changes. -/// See . -const MAX_SUPPORTED_JAVA: u32 = 24; #[cfg(target_os = "macos")] const CMDLINE_TOOLS_URL: &str = @@ -492,20 +486,68 @@ fn ensure_java() -> Result<()> { } } - if let Some(java) = java_major_version() { - if java > MAX_SUPPORTED_JAVA { - log::warn!( - "Detected Java {java}, but the bundled Gradle can only run on Java up to {MAX_SUPPORTED_JAVA}. \ - Android builds will likely fail with a cryptic error. Install a supported JDK (the Java {MAX_SUPPORTED_JAVA} \ - or an LTS release such as Java 17 or 21) and point JAVA_HOME at it. \ - See https://docs.gradle.org/current/userguide/compatibility.html" - ); + // Read the Gradle version this project actually uses (the user may have upgraded it away from + // the template default) and only warn when the detected Java is too new for that Gradle. + if let (Some(java), Some(gradle_version)) = (java_major_version(), project_gradle_version()) { + if let Some(max_java) = gradle_max_supported_java(&gradle_version) { + if java > max_java { + log::warn!( + "Detected Java {java}, but Gradle {gradle_version} used by this project can only run on Java up to {max_java}. \ + Android builds will likely fail with a cryptic error. Install a supported JDK (Java {max_java} \ + or an LTS release such as Java 17 or 21) and point JAVA_HOME at it. \ + See https://docs.gradle.org/current/userguide/compatibility.html" + ); + } } } Ok(()) } +/// Reads the Gradle version this project is pinned to from its +/// `gradle/wrapper/gradle-wrapper.properties` `distributionUrl` (e.g. +/// `gradle-8.14-bin.zip` -> `8.14`). Returns `None` when the project isn't +/// generated yet or the file can't be parsed, in which case no warning is shown. +fn project_gradle_version() -> Option { + let project_path = std::env::var_os("TAURI_ANDROID_PROJECT_PATH")?; + let properties = std::fs::read_to_string( + Path::new(&project_path).join("gradle/wrapper/gradle-wrapper.properties"), + ) + .ok()?; + properties.lines().find_map(|line| { + let url = line.trim().strip_prefix("distributionUrl=")?; + let file = url.rsplit('/').next()?; + let version = file.strip_prefix("gradle-")?.split('-').next()?; + (!version.is_empty()).then(|| version.to_string()) + }) +} + +/// Highest Java feature version a given Gradle release can *run* on. +/// Only 8.x releases are mapped; for Gradle 9+ (which tracks new Java quickly) +/// we return `None` to avoid false warnings. See +/// . +fn gradle_max_supported_java(version: &str) -> Option { + let mut parts = version.split('.'); + let major: u32 = parts.next()?.parse().ok()?; + let minor: u32 = parts.next().and_then(|m| m.parse().ok()).unwrap_or(0); + + if major >= 9 { + return None; + } + + Some(match (major, minor) { + v if v >= (8, 14) => 24, + v if v >= (8, 10) => 23, + v if v >= (8, 8) => 22, + v if v >= (8, 5) => 21, + v if v >= (8, 3) => 20, + v if v >= (7, 6) => 19, + v if v >= (7, 5) => 18, + v if v >= (7, 3) => 17, + _ => return None, + }) +} + /// Detects the major (feature) version of the active Java installation by /// reading the `release` metadata file shipped inside every JDK, preferring /// `JAVA_HOME` and falling back to the `java` binary on `PATH`. This avoids From ec4f795109fce30fb1c513646c2ddbac9b0fd291 Mon Sep 17 00:00:00 2001 From: tenderdeve Date: Thu, 13 Aug 2026 12:10:16 +0530 Subject: [PATCH 4/7] refactor(cli): move Android Java/Gradle checks to a dedicated module Extract the Java detection and Java/Gradle compatibility checks out of android/mod.rs into android/java.rs, and recommend the exact supported Java ceiling in the warning instead of a stale LTS list. Addresses @Legend-Master's feedback. --- crates/tauri-cli/src/mobile/android/java.rs | 169 ++++++++++++++++++++ crates/tauri-cli/src/mobile/android/mod.rs | 147 +---------------- 2 files changed, 172 insertions(+), 144 deletions(-) create mode 100644 crates/tauri-cli/src/mobile/android/java.rs diff --git a/crates/tauri-cli/src/mobile/android/java.rs b/crates/tauri-cli/src/mobile/android/java.rs new file mode 100644 index 000000000000..fb19d345cdf6 --- /dev/null +++ b/crates/tauri-cli/src/mobile/android/java.rs @@ -0,0 +1,169 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +//! Java detection and Java/Gradle compatibility checks for Android builds. + +use std::path::{Path, PathBuf}; + +use crate::Result; + +pub fn ensure_java() -> Result<()> { + if std::env::var_os("JAVA_HOME").is_none() { + #[cfg(windows)] + let default_java_home = "C:\\Program Files\\Android\\Android Studio\\jbr"; + #[cfg(target_os = "macos")] + let default_java_home = "/Applications/Android Studio.app/Contents/jbr/Contents/Home"; + #[cfg(target_os = "linux")] + let default_java_home = "/opt/android-studio/jbr"; + + if Path::new(default_java_home).exists() { + log::info!("Using Android Studio's default Java installation: {default_java_home}"); + std::env::set_var("JAVA_HOME", default_java_home); + } else if which::which("java").is_err() { + crate::error::bail!("Java not found in PATH, default Android Studio Java installation not found at {default_java_home} and JAVA_HOME environment variable not set. Please install Java before proceeding"); + } + } + + // Read the Gradle version this project actually uses (the user may have upgraded it away from + // the template default) and only warn when the detected Java is too new for that Gradle. + if let (Some(java), Some(gradle_version)) = (java_major_version(), project_gradle_version()) { + if let Some(max_java) = gradle_max_supported_java(&gradle_version) { + if java > max_java { + log::warn!( + "Detected Java {java}, but Gradle {gradle_version} used by this project can only run on Java up to {max_java}. \ + Android builds will likely fail with a cryptic error. Install a JDK that Gradle {gradle_version} supports \ + (Java {max_java} or older) and point JAVA_HOME at it. \ + See https://docs.gradle.org/current/userguide/compatibility.html" + ); + } + } + } + + Ok(()) +} + +/// Reads the Gradle version this project is pinned to from its +/// `gradle/wrapper/gradle-wrapper.properties` `distributionUrl` (e.g. +/// `gradle-8.14-bin.zip` -> `8.14`). Returns `None` when the project isn't +/// generated yet or the file can't be parsed, in which case no warning is shown. +fn project_gradle_version() -> Option { + let project_path = std::env::var_os("TAURI_ANDROID_PROJECT_PATH")?; + let properties = std::fs::read_to_string( + Path::new(&project_path).join("gradle/wrapper/gradle-wrapper.properties"), + ) + .ok()?; + properties.lines().find_map(|line| { + let url = line.trim().strip_prefix("distributionUrl=")?; + let file = url.rsplit('/').next()?; + let version = file.strip_prefix("gradle-")?.split('-').next()?; + (!version.is_empty()).then(|| version.to_string()) + }) +} + +/// Highest Java feature version a given Gradle release can *run* on. +/// Only 8.x releases are mapped; for Gradle 9+ (which tracks new Java quickly) +/// we return `None` to avoid false warnings. See +/// . +fn gradle_max_supported_java(version: &str) -> Option { + let mut parts = version.split('.'); + let major: u32 = parts.next()?.parse().ok()?; + let minor: u32 = parts.next().and_then(|m| m.parse().ok()).unwrap_or(0); + + if major >= 9 { + return None; + } + + Some(match (major, minor) { + v if v >= (8, 14) => 24, + v if v >= (8, 10) => 23, + v if v >= (8, 8) => 22, + v if v >= (8, 5) => 21, + v if v >= (8, 3) => 20, + v if v >= (7, 6) => 19, + v if v >= (7, 5) => 18, + v if v >= (7, 3) => 17, + _ => return None, + }) +} + +/// Detects the major (feature) version of the active Java installation by +/// reading the `release` metadata file shipped inside every JDK, preferring +/// `JAVA_HOME` and falling back to the `java` binary on `PATH`. This avoids +/// spawning a `java -version` subprocess on every Android command. +fn java_major_version() -> Option { + let release = std::fs::read_to_string(java_home_dir()?.join("release")).ok()?; + release + .lines() + .find_map(|line| line.strip_prefix("JAVA_VERSION=")) + .and_then(parse_java_major) +} + +/// Resolves the active JDK home directory, preferring `JAVA_HOME` and otherwise +/// deriving it from the `java` binary on `PATH` (`/bin/java` -> ``). +fn java_home_dir() -> Option { + if let Some(home) = std::env::var_os("JAVA_HOME") { + return Some(PathBuf::from(home)); + } + let java = which::which("java").ok()?; + java.parent()?.parent().map(Path::to_path_buf) +} + +/// Parses the major (feature) version out of a quoted Java version string (the +/// `JAVA_VERSION="..."` line of a JDK `release` file, or a `java -version` +/// banner), handling both the legacy `1.x` scheme (`1.8.0_292` -> 8) and the +/// modern scheme (`21.0.1` -> 21). +fn parse_java_major(version_output: &str) -> Option { + let start = version_output.find('"')?; + let rest = &version_output[start + 1..]; + let end = rest.find('"')?; + let mut parts = rest[..end].split(['.', '_', '-']); + + let first = parts.next()?; + if first == "1" { + parts.next()?.parse().ok() + } else { + first.parse().ok() + } +} + +#[cfg(test)] +mod tests { + use super::{gradle_max_supported_java, parse_java_major}; + + #[test] + fn parses_java_major_version() { + // modern scheme + assert_eq!( + parse_java_major("openjdk version \"21.0.1\" 2023-10-17"), + Some(21) + ); + assert_eq!( + parse_java_major("openjdk version \"26\" 2026-03-17"), + Some(26) + ); + assert_eq!( + parse_java_major("java version \"17.0.9\" 2023-10-17 LTS"), + Some(17) + ); + // legacy 1.x scheme + assert_eq!(parse_java_major("java version \"1.8.0_292\""), Some(8)); + // JDK `release` file line (the path java_major_version actually feeds in) + assert_eq!(parse_java_major("\"21.0.1\""), Some(21)); + // garbage + assert_eq!(parse_java_major("no version here"), None); + } + + #[test] + fn maps_gradle_to_max_java() { + assert_eq!(gradle_max_supported_java("8.14"), Some(24)); + assert_eq!(gradle_max_supported_java("8.5"), Some(21)); + assert_eq!(gradle_max_supported_java("7.3"), Some(17)); + // pre-7.3 is unmapped + assert_eq!(gradle_max_supported_java("7.2"), None); + // Gradle 9+ tracks new Java quickly, so we don't warn + assert_eq!(gradle_max_supported_java("9.0"), None); + // unparsable + assert_eq!(gradle_max_supported_java("not-a-version"), None); + } +} diff --git a/crates/tauri-cli/src/mobile/android/mod.rs b/crates/tauri-cli/src/mobile/android/mod.rs index 0637987bbac0..2b46b0134e40 100644 --- a/crates/tauri-cli/src/mobile/android/mod.rs +++ b/crates/tauri-cli/src/mobile/android/mod.rs @@ -45,6 +45,7 @@ use crate::{ mod android_studio_script; mod build; mod dev; +mod java; pub(crate) mod project; mod run; @@ -463,131 +464,12 @@ fn download_cmdline_tools(extract_path: &Path) -> Result<()> { } fn ensure_env(non_interactive: bool) -> Result<()> { - ensure_java()?; + java::ensure_java()?; ensure_sdk(non_interactive)?; ensure_ndk(non_interactive)?; Ok(()) } -fn ensure_java() -> Result<()> { - if std::env::var_os("JAVA_HOME").is_none() { - #[cfg(windows)] - let default_java_home = "C:\\Program Files\\Android\\Android Studio\\jbr"; - #[cfg(target_os = "macos")] - let default_java_home = "/Applications/Android Studio.app/Contents/jbr/Contents/Home"; - #[cfg(target_os = "linux")] - let default_java_home = "/opt/android-studio/jbr"; - - if Path::new(default_java_home).exists() { - log::info!("Using Android Studio's default Java installation: {default_java_home}"); - std::env::set_var("JAVA_HOME", default_java_home); - } else if which::which("java").is_err() { - crate::error::bail!("Java not found in PATH, default Android Studio Java installation not found at {default_java_home} and JAVA_HOME environment variable not set. Please install Java before proceeding"); - } - } - - // Read the Gradle version this project actually uses (the user may have upgraded it away from - // the template default) and only warn when the detected Java is too new for that Gradle. - if let (Some(java), Some(gradle_version)) = (java_major_version(), project_gradle_version()) { - if let Some(max_java) = gradle_max_supported_java(&gradle_version) { - if java > max_java { - log::warn!( - "Detected Java {java}, but Gradle {gradle_version} used by this project can only run on Java up to {max_java}. \ - Android builds will likely fail with a cryptic error. Install a supported JDK (Java {max_java} \ - or an LTS release such as Java 17 or 21) and point JAVA_HOME at it. \ - See https://docs.gradle.org/current/userguide/compatibility.html" - ); - } - } - } - - Ok(()) -} - -/// Reads the Gradle version this project is pinned to from its -/// `gradle/wrapper/gradle-wrapper.properties` `distributionUrl` (e.g. -/// `gradle-8.14-bin.zip` -> `8.14`). Returns `None` when the project isn't -/// generated yet or the file can't be parsed, in which case no warning is shown. -fn project_gradle_version() -> Option { - let project_path = std::env::var_os("TAURI_ANDROID_PROJECT_PATH")?; - let properties = std::fs::read_to_string( - Path::new(&project_path).join("gradle/wrapper/gradle-wrapper.properties"), - ) - .ok()?; - properties.lines().find_map(|line| { - let url = line.trim().strip_prefix("distributionUrl=")?; - let file = url.rsplit('/').next()?; - let version = file.strip_prefix("gradle-")?.split('-').next()?; - (!version.is_empty()).then(|| version.to_string()) - }) -} - -/// Highest Java feature version a given Gradle release can *run* on. -/// Only 8.x releases are mapped; for Gradle 9+ (which tracks new Java quickly) -/// we return `None` to avoid false warnings. See -/// . -fn gradle_max_supported_java(version: &str) -> Option { - let mut parts = version.split('.'); - let major: u32 = parts.next()?.parse().ok()?; - let minor: u32 = parts.next().and_then(|m| m.parse().ok()).unwrap_or(0); - - if major >= 9 { - return None; - } - - Some(match (major, minor) { - v if v >= (8, 14) => 24, - v if v >= (8, 10) => 23, - v if v >= (8, 8) => 22, - v if v >= (8, 5) => 21, - v if v >= (8, 3) => 20, - v if v >= (7, 6) => 19, - v if v >= (7, 5) => 18, - v if v >= (7, 3) => 17, - _ => return None, - }) -} - -/// Detects the major (feature) version of the active Java installation by -/// reading the `release` metadata file shipped inside every JDK, preferring -/// `JAVA_HOME` and falling back to the `java` binary on `PATH`. This avoids -/// spawning a `java -version` subprocess on every Android command. -fn java_major_version() -> Option { - let release = std::fs::read_to_string(java_home_dir()?.join("release")).ok()?; - release - .lines() - .find_map(|line| line.strip_prefix("JAVA_VERSION=")) - .and_then(parse_java_major) -} - -/// Resolves the active JDK home directory, preferring `JAVA_HOME` and otherwise -/// deriving it from the `java` binary on `PATH` (`/bin/java` -> ``). -fn java_home_dir() -> Option { - if let Some(home) = std::env::var_os("JAVA_HOME") { - return Some(PathBuf::from(home)); - } - let java = which::which("java").ok()?; - java.parent()?.parent().map(Path::to_path_buf) -} - -/// Parses the major (feature) version out of a quoted Java version string (the -/// `JAVA_VERSION="..."` line of a JDK `release` file, or a `java -version` -/// banner), handling both the legacy `1.x` scheme (`1.8.0_292` -> 8) and the -/// modern scheme (`21.0.1` -> 21). -fn parse_java_major(version_output: &str) -> Option { - let start = version_output.find('"')?; - let rest = &version_output[start + 1..]; - let end = rest.find('"')?; - let mut parts = rest[..end].split(['.', '_', '-']); - - let first = parts.next()?; - if first == "1" { - parts.next()?.parse().ok() - } else { - first.parse().ok() - } -} - fn ensure_sdk(non_interactive: bool) -> Result<()> { let android_home = std::env::var_os("ANDROID_HOME") .or_else(|| std::env::var_os("ANDROID_SDK_ROOT")) @@ -1180,30 +1062,7 @@ fn generate_tauri_properties( #[cfg(test)] mod tests { - use super::{find_matching_brace, parse_java_major, set_debug_application_id_suffix}; - - #[test] - fn parses_java_major_version() { - // modern scheme - assert_eq!( - parse_java_major("openjdk version \"21.0.1\" 2023-10-17"), - Some(21) - ); - assert_eq!( - parse_java_major("openjdk version \"26\" 2026-03-17"), - Some(26) - ); - assert_eq!( - parse_java_major("java version \"17.0.9\" 2023-10-17 LTS"), - Some(17) - ); - // legacy 1.x scheme - assert_eq!(parse_java_major("java version \"1.8.0_292\""), Some(8)); - // JDK `release` file line (the path java_major_version actually feeds in) - assert_eq!(parse_java_major("\"21.0.1\""), Some(21)); - // garbage - assert_eq!(parse_java_major("no version here"), None); - } + use super::{find_matching_brace, set_debug_application_id_suffix}; #[test] fn writes_debug_application_id_suffix() { From 078319bc6700b5a5cacf83092bb0ad412716b3c2 Mon Sep 17 00:00:00 2001 From: Tony Date: Thu, 13 Aug 2026 15:50:59 +0800 Subject: [PATCH 5/7] revert ensure java --- crates/tauri-cli/src/mobile/android/mod.rs | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/crates/tauri-cli/src/mobile/android/mod.rs b/crates/tauri-cli/src/mobile/android/mod.rs index 2b46b0134e40..6073767aa2e6 100644 --- a/crates/tauri-cli/src/mobile/android/mod.rs +++ b/crates/tauri-cli/src/mobile/android/mod.rs @@ -464,12 +464,32 @@ fn download_cmdline_tools(extract_path: &Path) -> Result<()> { } fn ensure_env(non_interactive: bool) -> Result<()> { - java::ensure_java()?; + ensure_java()?; ensure_sdk(non_interactive)?; ensure_ndk(non_interactive)?; Ok(()) } +fn ensure_java() -> Result<()> { + if std::env::var_os("JAVA_HOME").is_none() { + #[cfg(windows)] + let default_java_home = "C:\\Program Files\\Android\\Android Studio\\jbr"; + #[cfg(target_os = "macos")] + let default_java_home = "/Applications/Android Studio.app/Contents/jbr/Contents/Home"; + #[cfg(target_os = "linux")] + let default_java_home = "/opt/android-studio/jbr"; + + if Path::new(default_java_home).exists() { + log::info!("Using Android Studio's default Java installation: {default_java_home}"); + std::env::set_var("JAVA_HOME", default_java_home); + } else if which::which("java").is_err() { + crate::error::bail!("Java not found in PATH, default Android Studio Java installation not found at {default_java_home} and JAVA_HOME environment variable not set. Please install Java before proceeding"); + } + } + + Ok(()) +} + fn ensure_sdk(non_interactive: bool) -> Result<()> { let android_home = std::env::var_os("ANDROID_HOME") .or_else(|| std::env::var_os("ANDROID_SDK_ROOT")) From 44874c510f3cdf6378a37d351164a49d2004b740 Mon Sep 17 00:00:00 2001 From: Tony Date: Thu, 13 Aug 2026 15:52:50 +0800 Subject: [PATCH 6/7] move the check to get_config the project path is only available after it --- ...{java.rs => check_java_gradle_versions.rs} | 40 +++++-------------- crates/tauri-cli/src/mobile/android/mod.rs | 5 ++- 2 files changed, 14 insertions(+), 31 deletions(-) rename crates/tauri-cli/src/mobile/android/{java.rs => check_java_gradle_versions.rs} (78%) diff --git a/crates/tauri-cli/src/mobile/android/java.rs b/crates/tauri-cli/src/mobile/android/check_java_gradle_versions.rs similarity index 78% rename from crates/tauri-cli/src/mobile/android/java.rs rename to crates/tauri-cli/src/mobile/android/check_java_gradle_versions.rs index fb19d345cdf6..40874d28347e 100644 --- a/crates/tauri-cli/src/mobile/android/java.rs +++ b/crates/tauri-cli/src/mobile/android/check_java_gradle_versions.rs @@ -6,41 +6,21 @@ use std::path::{Path, PathBuf}; -use crate::Result; - -pub fn ensure_java() -> Result<()> { - if std::env::var_os("JAVA_HOME").is_none() { - #[cfg(windows)] - let default_java_home = "C:\\Program Files\\Android\\Android Studio\\jbr"; - #[cfg(target_os = "macos")] - let default_java_home = "/Applications/Android Studio.app/Contents/jbr/Contents/Home"; - #[cfg(target_os = "linux")] - let default_java_home = "/opt/android-studio/jbr"; - - if Path::new(default_java_home).exists() { - log::info!("Using Android Studio's default Java installation: {default_java_home}"); - std::env::set_var("JAVA_HOME", default_java_home); - } else if which::which("java").is_err() { - crate::error::bail!("Java not found in PATH, default Android Studio Java installation not found at {default_java_home} and JAVA_HOME environment variable not set. Please install Java before proceeding"); - } - } - +pub fn check_java_gradle_versions() -> Option<()> { + let java_major_version = java_major_version()?; + let project_gradle_version = project_gradle_version()?; + let gradle_max_supported_java = gradle_max_supported_java(&project_gradle_version)?; // Read the Gradle version this project actually uses (the user may have upgraded it away from // the template default) and only warn when the detected Java is too new for that Gradle. - if let (Some(java), Some(gradle_version)) = (java_major_version(), project_gradle_version()) { - if let Some(max_java) = gradle_max_supported_java(&gradle_version) { - if java > max_java { - log::warn!( - "Detected Java {java}, but Gradle {gradle_version} used by this project can only run on Java up to {max_java}. \ - Android builds will likely fail with a cryptic error. Install a JDK that Gradle {gradle_version} supports \ - (Java {max_java} or older) and point JAVA_HOME at it. \ + if java_major_version > gradle_max_supported_java { + log::warn!( + "Detected Java {java_major_version}, but Gradle {project_gradle_version} used by this project can only run on Java up to {gradle_max_supported_java}. \ + Android builds will likely fail with a cryptic error. Install a JDK that Gradle {project_gradle_version} supports \ + (Java {gradle_max_supported_java} or older) and point JAVA_HOME at it. \ See https://docs.gradle.org/current/userguide/compatibility.html" ); - } - } } - - Ok(()) + Some(()) } /// Reads the Gradle version this project is pinned to from its diff --git a/crates/tauri-cli/src/mobile/android/mod.rs b/crates/tauri-cli/src/mobile/android/mod.rs index 6073767aa2e6..34b899dddf5b 100644 --- a/crates/tauri-cli/src/mobile/android/mod.rs +++ b/crates/tauri-cli/src/mobile/android/mod.rs @@ -39,13 +39,14 @@ use super::{ use crate::{ error::Context, helpers::config::{BundleResources, Config as TauriConfig}, + mobile::android::check_java_gradle_versions::check_java_gradle_versions, ConfigValue, Error, ErrorExt, Result, }; mod android_studio_script; mod build; +mod check_java_gradle_versions; mod dev; -mod java; pub(crate) mod project; mod run; @@ -187,6 +188,8 @@ pub fn get_config( src_main_dir.join("generated"), ); + check_java_gradle_versions(); + (config, metadata) } From 2f7fa7dbfb7c13b8b1bee659096a989b4404986b Mon Sep 17 00:00:00 2001 From: Tony Date: Thu, 13 Aug 2026 15:57:39 +0800 Subject: [PATCH 7/7] check 9.1 and 9.4 --- .../android/check_java_gradle_versions.rs | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/crates/tauri-cli/src/mobile/android/check_java_gradle_versions.rs b/crates/tauri-cli/src/mobile/android/check_java_gradle_versions.rs index 40874d28347e..79047ca4445e 100644 --- a/crates/tauri-cli/src/mobile/android/check_java_gradle_versions.rs +++ b/crates/tauri-cli/src/mobile/android/check_java_gradle_versions.rs @@ -42,19 +42,21 @@ fn project_gradle_version() -> Option { } /// Highest Java feature version a given Gradle release can *run* on. -/// Only 8.x releases are mapped; for Gradle 9+ (which tracks new Java quickly) -/// we return `None` to avoid false warnings. See +/// Known Gradle 7.x through 9.x releases are mapped. Unknown future major +/// versions return `None` to avoid false warnings. See /// . fn gradle_max_supported_java(version: &str) -> Option { let mut parts = version.split('.'); let major: u32 = parts.next()?.parse().ok()?; let minor: u32 = parts.next().and_then(|m| m.parse().ok()).unwrap_or(0); - if major >= 9 { + if major >= 10 { return None; } Some(match (major, minor) { + v if v >= (9, 4) => 26, + v if v >= (9, 1) => 25, v if v >= (8, 14) => 24, v if v >= (8, 10) => 23, v if v >= (8, 8) => 22, @@ -136,13 +138,18 @@ mod tests { #[test] fn maps_gradle_to_max_java() { + assert_eq!(gradle_max_supported_java("9.6.1"), Some(26)); + assert_eq!(gradle_max_supported_java("9.4.0"), Some(26)); + assert_eq!(gradle_max_supported_java("9.3"), Some(25)); + assert_eq!(gradle_max_supported_java("9.1.0"), Some(25)); + assert_eq!(gradle_max_supported_java("9.0"), Some(24)); assert_eq!(gradle_max_supported_java("8.14"), Some(24)); assert_eq!(gradle_max_supported_java("8.5"), Some(21)); assert_eq!(gradle_max_supported_java("7.3"), Some(17)); // pre-7.3 is unmapped assert_eq!(gradle_max_supported_java("7.2"), None); - // Gradle 9+ tracks new Java quickly, so we don't warn - assert_eq!(gradle_max_supported_java("9.0"), None); + // Unknown future Gradle majors are left unmapped to avoid false warnings. + assert_eq!(gradle_max_supported_java("10.0"), None); // unparsable assert_eq!(gradle_max_supported_java("not-a-version"), None); }