From d89d8fa62302bd3553b66fd47ecfa046b2fc84ef Mon Sep 17 00:00:00 2001 From: TenderDeve Date: Thu, 13 Aug 2026 14:04:29 +0530 Subject: [PATCH 1/2] feat(cli): warn when Java is too new for the bundled Gradle (#15780) * 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 * 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. * 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). * 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. * revert ensure java * move the check to get_config the project path is only available after it * check 9.1 and 9.4 --------- Co-authored-by: Tony <68118705+Legend-Master@users.noreply.github.com> Co-authored-by: Tony --- .changes/android-java-gradle-compat-hint.md | 6 + .../android/check_java_gradle_versions.rs | 156 ++++++++++++++++++ crates/tauri-cli/src/mobile/android/mod.rs | 4 + 3 files changed, 166 insertions(+) create mode 100644 .changes/android-java-gradle-compat-hint.md create mode 100644 crates/tauri-cli/src/mobile/android/check_java_gradle_versions.rs 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) } From e3bdc0345bd0acb94cf31d29cf44b30ec4ed0808 Mon Sep 17 00:00:00 2001 From: Tony <68118705+Legend-Master@users.noreply.github.com> Date: Thu, 13 Aug 2026 17:02:32 +0800 Subject: [PATCH 2/2] fix(ci): restore ndk symlinks on macos (#15870) --- .github/workflows/test-android.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test-android.yml b/.github/workflows/test-android.yml index b8c60cf88168..58901b6fab87 100644 --- a/.github/workflows/test-android.yml +++ b/.github/workflows/test-android.yml @@ -59,8 +59,10 @@ jobs: # TODO check after https://github.com/nttld/setup-ndk/issues/518 is fixed - name: Restore Android Symlinks if: matrix.platform == 'ubuntu-latest' || matrix.platform == 'macos-latest' + env: + NDK_HOST: ${{ matrix.platform == 'ubuntu-latest' && 'linux-x86_64' || 'darwin-x86_64' }} run: | - directory="${{ steps.setup-ndk.outputs.ndk-path }}/toolchains/llvm/prebuilt/linux-x86_64/bin" + directory="${{ steps.setup-ndk.outputs.ndk-path }}/toolchains/llvm/prebuilt/$NDK_HOST/bin" find "$directory" -type l | while read link; do current_target=$(readlink "$link") new_target="$directory/$(basename "$current_target")"