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/check_java_gradle_versions.rs b/crates/tauri-cli/src/mobile/android/check_java_gradle_versions.rs new file mode 100644 index 000000000000..79047ca4445e --- /dev/null +++ b/crates/tauri-cli/src/mobile/android/check_java_gradle_versions.rs @@ -0,0 +1,156 @@ +// 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}; + +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 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" + ); + } + Some(()) +} + +/// 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. +/// 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 >= 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, + 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("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); + // 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); + } +} diff --git a/crates/tauri-cli/src/mobile/android/mod.rs b/crates/tauri-cli/src/mobile/android/mod.rs index c0add29e61bc..34b899dddf5b 100644 --- a/crates/tauri-cli/src/mobile/android/mod.rs +++ b/crates/tauri-cli/src/mobile/android/mod.rs @@ -39,11 +39,13 @@ 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; pub(crate) mod project; mod run; @@ -186,6 +188,8 @@ pub fn get_config( src_main_dir.join("generated"), ); + check_java_gradle_versions(); + (config, metadata) }