Skip to content
Merged
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
6 changes: 6 additions & 0 deletions .changes/android-java-gradle-compat-hint.md
Original file line number Diff line number Diff line change
@@ -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.
4 changes: 3 additions & 1 deletion .github/workflows/test-android.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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")"
Expand Down
156 changes: 156 additions & 0 deletions crates/tauri-cli/src/mobile/android/check_java_gradle_versions.rs
Original file line number Diff line number Diff line change
@@ -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<String> {
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
/// <https://docs.gradle.org/current/userguide/compatibility.html>.
fn gradle_max_supported_java(version: &str) -> Option<u32> {
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<u32> {
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` (`<home>/bin/java` -> `<home>`).
fn java_home_dir() -> Option<PathBuf> {
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<u32> {
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);
}
}
4 changes: 4 additions & 0 deletions crates/tauri-cli/src/mobile/android/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -186,6 +188,8 @@ pub fn get_config(
src_main_dir.join("generated"),
);

check_java_gradle_versions();

(config, metadata)
}

Expand Down
Loading