feat(java): prefer JDKs, require java.awt, install kits only
This commit is contained in:
parent
038c3542ce
commit
1947e2d534
12 changed files with 510 additions and 29 deletions
|
|
@ -1,5 +1,5 @@
|
|||
public final class JavaInfo {
|
||||
private static final String[] CHECKED_PROPERTIES = new String[] {"os.arch", "java.version", "java.vendor"};
|
||||
private static final String[] CHECKED_PROPERTIES = new String[] {"os.arch", "java.version", "java.vendor", "java.home"};
|
||||
|
||||
public static void main(String[] args) {
|
||||
int returnCode = 0;
|
||||
|
|
@ -14,6 +14,31 @@ public final class JavaInfo {
|
|||
}
|
||||
}
|
||||
|
||||
// Minecraft needs AWT. Stripped, headless and hand-jlinked images drop
|
||||
// java.desktop and only blow up once the game reaches for it.
|
||||
System.out.println("java.awt=" + hasClass("java.awt.Toolkit"));
|
||||
// A kit ships a compiler, a runtime image does not. On 8 this finds the
|
||||
// one in tools.jar, on 9+ the jdk.compiler module.
|
||||
System.out.println("java.jdk=" + hasCompiler());
|
||||
|
||||
System.exit(returnCode);
|
||||
}
|
||||
|
||||
/** Loads the class without initialising it, so no display is ever needed. */
|
||||
private static boolean hasClass(String name) {
|
||||
try {
|
||||
Class.forName(name, false, JavaInfo.class.getClassLoader());
|
||||
return true;
|
||||
} catch (Throwable ignored) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean hasCompiler() {
|
||||
try {
|
||||
return javax.tools.ToolProvider.getSystemJavaCompiler() != null;
|
||||
} catch (Throwable ignored) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,3 +4,10 @@ os.arch=amd64
|
|||
java.version=1.8.0_382
|
||||
java.vendor=Azul Systems, Inc.
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> The checked-in `JavaInfo.class` predates the `java.home`, `java.awt` and `java.jdk`
|
||||
> lines and does not print them yet. Recompiling it against a Java 8 target
|
||||
> (`javac --release 8 JavaInfo.java`) is what turns the probe-side answers on; until
|
||||
> then [`checker.rs`](../../src/checker.rs) falls back to inspecting the installation
|
||||
> on disk, which reaches the same verdict for every image seen in the wild.
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
//! Downloads and probes one vendor's JRE, then records it in an in-memory store.
|
||||
//! Downloads and probes one vendor's JDK, then records it in an in-memory store.
|
||||
//!
|
||||
//! Runs against `MemoryJavaStore`, so it needs no database and no ephemeral
|
||||
//! `LauncherState` just to list packages.
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
//! Lists the JRE packages each vendor currently publishes.
|
||||
//! Lists the JDK packages each vendor currently publishes.
|
||||
//!
|
||||
//! Needs only a network client, no database or launcher state. That is the
|
||||
//! point of `JavaStore` being a port: the vendor providers are exercisable on
|
||||
|
|
@ -22,8 +22,8 @@ async fn main() -> JavaResult<()> {
|
|||
let net = RequestClient::new(NetConfig::default())?;
|
||||
|
||||
match major {
|
||||
Some(major) => println!("Listing JRE packages for Java {major} (metadata only):\n"),
|
||||
None => println!("Listing JRE packages for all majors (metadata only):\n"),
|
||||
Some(major) => println!("Listing JDK packages for Java {major} (metadata only):\n"),
|
||||
None => println!("Listing JDK packages for all majors (metadata only):\n"),
|
||||
}
|
||||
|
||||
for provider in runtime_providers() {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,7 @@
|
|||
use std::{collections::HashMap, path::PathBuf};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use async_tempfile::TempDir;
|
||||
use tokio::sync::OnceCell;
|
||||
|
|
@ -7,11 +10,30 @@ use crate::error::{JavaError, JavaResult};
|
|||
|
||||
const JAVA_INFO_CLASS: &[u8] = include_bytes!("../assets/java/JavaInfo.class");
|
||||
|
||||
#[derive(Debug)]
|
||||
/// Tools that only ever ship with a development kit, never with a runtime image.
|
||||
const JAVAC_BIN: &str = if cfg!(windows) { "javac.exe" } else { "javac" };
|
||||
const JMOD_BIN: &str = if cfg!(windows) { "jmod.exe" } else { "jmod" };
|
||||
|
||||
/// The native library behind `java.awt`. An image linked without `java.desktop`
|
||||
/// simply does not ship it.
|
||||
#[cfg(target_os = "windows")]
|
||||
const AWT_LIBRARY: &str = "awt.dll";
|
||||
#[cfg(target_os = "macos")]
|
||||
const AWT_LIBRARY: &str = "libawt.dylib";
|
||||
#[cfg(all(unix, not(target_os = "macos")))]
|
||||
const AWT_LIBRARY: &str = "libawt.so";
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct JavaCheckInfo {
|
||||
pub version: String,
|
||||
pub vendor: String,
|
||||
pub os_arch: String,
|
||||
/// Whether this is a development kit rather than a bare runtime image.
|
||||
///
|
||||
/// Minecraft starts on either, but mod loaders and the game's own tooling
|
||||
/// expect the kit, so the locator prefers one when both satisfy the
|
||||
/// requested major (see [`crate::locate::best_for_major`]).
|
||||
pub is_jdk: bool,
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "debug", skip(absolute_path))]
|
||||
|
|
@ -62,6 +84,20 @@ pub async fn check_java_runtime(absolute_path: String) -> JavaResult<JavaCheckIn
|
|||
return Err(JavaError::InvalidJavaPath { path: absolute_path });
|
||||
};
|
||||
|
||||
let home = java_home(&absolute_path, info.get("java.home"));
|
||||
|
||||
// Minecraft draws its window through AWT. A headless or hand-jlinked image
|
||||
// without `java.desktop` launches happily and then dies mid-game, so it is
|
||||
// rejected here rather than recorded as usable.
|
||||
if !has_awt_support(&info, &home) {
|
||||
tracing::warn!(
|
||||
path = %absolute_path,
|
||||
home = %home.display(),
|
||||
"java installation has no java.awt support"
|
||||
);
|
||||
return Err(JavaError::MissingAwtSupport { path: absolute_path });
|
||||
}
|
||||
|
||||
Ok(JavaCheckInfo {
|
||||
os_arch: info
|
||||
.get("os.arch")
|
||||
|
|
@ -72,9 +108,101 @@ pub async fn check_java_runtime(absolute_path: String) -> JavaResult<JavaCheckIn
|
|||
.get("java.vendor")
|
||||
.cloned()
|
||||
.unwrap_or_else(|| String::from("unknown")),
|
||||
is_jdk: probe_flag(&info, "java.jdk").unwrap_or(false) || is_jdk_home(&home),
|
||||
})
|
||||
}
|
||||
|
||||
/// Whether the installation behind a `java` executable is a development kit.
|
||||
///
|
||||
/// Nothing is recorded about kit-ness in the store, so callers holding only a
|
||||
/// path ask the filesystem again instead of the schema growing a column for a
|
||||
/// hint this cheap to recompute.
|
||||
pub(crate) fn is_jdk_executable(absolute_path: &str) -> bool {
|
||||
is_jdk_home(&java_home(absolute_path, None))
|
||||
}
|
||||
|
||||
/// Whether `home` is a development kit rather than a runtime image.
|
||||
///
|
||||
/// Directory names lie in both directions (`.../jdk-21/jre`, runtimes unpacked
|
||||
/// into a folder someone called `jdk`), so this only looks for artefacts a
|
||||
/// runtime image never carries: the compiler, `jmod`, the JDK 8 `tools.jar`,
|
||||
/// and the JNI headers.
|
||||
pub(crate) fn is_jdk_home(home: &Path) -> bool {
|
||||
let bin = home.join("bin");
|
||||
|
||||
bin.join(JAVAC_BIN).is_file()
|
||||
|| bin.join(JMOD_BIN).is_file()
|
||||
|| home.join("jmods").is_dir()
|
||||
|| home.join("lib").join("tools.jar").is_file()
|
||||
|| home.join("include").join("jni.h").is_file()
|
||||
}
|
||||
|
||||
/// The installation root.
|
||||
///
|
||||
/// `java.home` as the probe reports it is authoritative; when the compiled
|
||||
/// probe class predates that property we fall back to the executable's own
|
||||
/// location, resolving symlinks first so a `/usr/bin/java` shim lands in the
|
||||
/// real home rather than in `/usr`.
|
||||
fn java_home(absolute_path: &str, reported: Option<&String>) -> PathBuf {
|
||||
if let Some(home) = reported.filter(|home| !home.is_empty()) {
|
||||
return PathBuf::from(home);
|
||||
}
|
||||
|
||||
let executable = std::fs::canonicalize(absolute_path)
|
||||
.unwrap_or_else(|_| PathBuf::from(absolute_path));
|
||||
|
||||
executable
|
||||
.parent()
|
||||
.and_then(Path::parent)
|
||||
.map(Path::to_path_buf)
|
||||
.unwrap_or(executable)
|
||||
}
|
||||
|
||||
/// Whether `java.awt` is there: the probe's own answer when it gave one, and
|
||||
/// otherwise what the installation ships on disk.
|
||||
fn has_awt_support(info: &HashMap<String, String>, home: &Path) -> bool {
|
||||
if let Some(reported) = probe_flag(info, "java.awt") {
|
||||
return reported;
|
||||
}
|
||||
|
||||
// Java 9+ images record the modules they were linked with.
|
||||
if let Some(modules) = release_modules(home) {
|
||||
return modules.split_whitespace().any(|module| module == "java.desktop");
|
||||
}
|
||||
|
||||
// Java 8, and anything else without a `release` file: AWT is a native
|
||||
// library sitting next to the runtime.
|
||||
awt_library_exists(home)
|
||||
}
|
||||
|
||||
/// The `MODULES` line of an image's `release` file, if it has one.
|
||||
fn release_modules(home: &Path) -> Option<String> {
|
||||
let release = std::fs::read_to_string(home.join("release")).ok()?;
|
||||
|
||||
release.lines().find_map(|line| {
|
||||
let modules = line.strip_prefix("MODULES=")?;
|
||||
Some(modules.trim_matches('"').to_string())
|
||||
})
|
||||
}
|
||||
|
||||
fn awt_library_exists(home: &Path) -> bool {
|
||||
// Windows keeps native libraries in `bin` and everyone else in `lib`; a
|
||||
// JDK 8 also nests a whole runtime under `jre`.
|
||||
[
|
||||
home.join("lib"),
|
||||
home.join("bin"),
|
||||
home.join("jre").join("lib"),
|
||||
home.join("jre").join("bin"),
|
||||
]
|
||||
.iter()
|
||||
.any(|dir| dir.join(AWT_LIBRARY).exists())
|
||||
}
|
||||
|
||||
/// Reads a boolean the probe printed, `None` when it printed no such line.
|
||||
fn probe_flag(info: &HashMap<String, String>, key: &str) -> Option<bool> {
|
||||
info.get(key).map(|value| value.eq_ignore_ascii_case("true"))
|
||||
}
|
||||
|
||||
static TEMP_JAVA_INFO: OnceCell<TempDir> = OnceCell::const_new();
|
||||
|
||||
#[tracing::instrument(level = "debug")]
|
||||
|
|
@ -92,3 +220,138 @@ async fn get_java_info_dir() -> Result<&'static PathBuf, polyio::IOError> {
|
|||
|
||||
Ok(dir?.dir_path())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn info(pairs: &[(&str, &str)]) -> HashMap<String, String> {
|
||||
pairs
|
||||
.iter()
|
||||
.map(|(key, value)| ((*key).to_string(), (*value).to_string()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn touch(path: &Path) {
|
||||
if let Some(parent) = path.parent() {
|
||||
std::fs::create_dir_all(parent).expect("create parent");
|
||||
}
|
||||
std::fs::write(path, b"").expect("write file");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn a_runtime_image_is_not_mistaken_for_a_kit() {
|
||||
let dir = polyio::tempdir().await.expect("tempdir");
|
||||
let home = dir.dir_path();
|
||||
|
||||
touch(&home.join("bin").join("java"));
|
||||
touch(&home.join("lib").join("modules"));
|
||||
|
||||
assert!(!is_jdk_home(home));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn a_kit_is_recognised_by_its_compiler() {
|
||||
let dir = polyio::tempdir().await.expect("tempdir");
|
||||
let home = dir.dir_path();
|
||||
|
||||
touch(&home.join("bin").join("java"));
|
||||
touch(&home.join("bin").join(JAVAC_BIN));
|
||||
|
||||
assert!(is_jdk_home(home));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn a_kit_is_recognised_by_its_headers_alone() {
|
||||
let dir = polyio::tempdir().await.expect("tempdir");
|
||||
let home = dir.dir_path();
|
||||
|
||||
touch(&home.join("bin").join("java"));
|
||||
touch(&home.join("include").join("jni.h"));
|
||||
|
||||
assert!(is_jdk_home(home));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn a_legacy_kit_is_recognised_by_tools_jar() {
|
||||
let dir = polyio::tempdir().await.expect("tempdir");
|
||||
let home = dir.dir_path();
|
||||
|
||||
touch(&home.join("lib").join("tools.jar"));
|
||||
|
||||
assert!(is_jdk_home(home));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn the_directory_name_alone_proves_nothing() {
|
||||
let dir = polyio::tempdir().await.expect("tempdir");
|
||||
let home = dir.dir_path().join("jdk-21");
|
||||
|
||||
touch(&home.join("bin").join("java"));
|
||||
|
||||
assert!(!is_jdk_home(&home));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_probe_has_the_last_word_on_awt() {
|
||||
let missing = Path::new("/nonexistent-java-home");
|
||||
|
||||
assert!(has_awt_support(&info(&[("java.awt", "true")]), missing));
|
||||
assert!(!has_awt_support(&info(&[("java.awt", "false")]), missing));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn a_module_list_without_java_desktop_fails_the_check() {
|
||||
let dir = polyio::tempdir().await.expect("tempdir");
|
||||
let home = dir.dir_path();
|
||||
|
||||
std::fs::write(
|
||||
home.join("release"),
|
||||
"JAVA_VERSION=\"21.0.3\"\nMODULES=\"java.base java.logging\"\n",
|
||||
)
|
||||
.expect("write release");
|
||||
|
||||
assert!(!has_awt_support(&info(&[]), home));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn a_module_list_with_java_desktop_passes() {
|
||||
let dir = polyio::tempdir().await.expect("tempdir");
|
||||
let home = dir.dir_path();
|
||||
|
||||
std::fs::write(
|
||||
home.join("release"),
|
||||
"JAVA_VERSION=\"21.0.3\"\nMODULES=\"java.base java.desktop java.logging\"\n",
|
||||
)
|
||||
.expect("write release");
|
||||
|
||||
assert!(has_awt_support(&info(&[]), home));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn a_release_less_image_falls_back_to_the_native_library() {
|
||||
let dir = polyio::tempdir().await.expect("tempdir");
|
||||
let home = dir.dir_path();
|
||||
|
||||
assert!(!has_awt_support(&info(&[]), home));
|
||||
|
||||
touch(&home.join("lib").join(AWT_LIBRARY));
|
||||
|
||||
assert!(has_awt_support(&info(&[]), home));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_missing_home_falls_back_to_the_executable_location() {
|
||||
let home = java_home("/opt/jdk-21/bin/java", None);
|
||||
|
||||
assert_eq!(home, PathBuf::from("/opt/jdk-21"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_reported_home_wins_over_the_executable_location() {
|
||||
let reported = String::from("/opt/real-home");
|
||||
let home = java_home("/usr/bin/java", Some(&reported));
|
||||
|
||||
assert_eq!(home, PathBuf::from("/opt/real-home"));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,6 +41,9 @@ pub enum JavaError {
|
|||
#[error("selected path is not a valid java installation: {path}")]
|
||||
InvalidJavaPath { path: String },
|
||||
|
||||
#[error("java installation at '{path}' is missing java.awt (the java.desktop module)")]
|
||||
MissingAwtSupport { path: String },
|
||||
|
||||
#[error("selected java is version {found}, but version {expected} is required")]
|
||||
VersionMismatch { expected: u32, found: u32 },
|
||||
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ pub use checker::{JavaCheckInfo, check_java_runtime};
|
|||
pub use data::{JavaPackage, JavaRuntime, PackageArchive, java_executable_relative_path};
|
||||
pub use error::{JavaError, JavaResult};
|
||||
pub use install::install_package;
|
||||
pub use locate::locate_java;
|
||||
pub use locate::{LocatedJava, best_for_major, locate_java};
|
||||
pub use platform::{HostArch, HostOs, HostTarget};
|
||||
pub use service::{
|
||||
AvailableJava, INSTALLABLE_MAJORS, JAVA_CHOICE_DOWNLOAD, JAVA_CHOICE_FOLDER, JAVA_VENDOR_HINT,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
use std::collections::HashMap;
|
||||
use std::cmp::Ordering;
|
||||
use std::path::PathBuf;
|
||||
|
||||
|
||||
|
|
@ -6,25 +6,71 @@ use oneclient_common::constants::JAVA_BIN;
|
|||
use crate::checker::{JavaCheckInfo, check_java_runtime};
|
||||
use crate::data::java_executable_relative_path;
|
||||
use crate::error::JavaResult;
|
||||
use crate::service::parse_major_version;
|
||||
|
||||
/// A `java` executable found on this machine and what probing it reported.
|
||||
pub type LocatedJava = (PathBuf, JavaCheckInfo);
|
||||
|
||||
/// Every usable installation on this machine, best first.
|
||||
#[tracing::instrument(level = "debug")]
|
||||
pub async fn locate_java() -> JavaResult<HashMap<PathBuf, JavaCheckInfo>> {
|
||||
pub async fn locate_java() -> JavaResult<Vec<LocatedJava>> {
|
||||
let paths = internal_locate_java();
|
||||
let mut valid = HashMap::new();
|
||||
let mut valid: Vec<LocatedJava> = Vec::new();
|
||||
|
||||
for path in paths {
|
||||
if valid.iter().any(|(existing, _)| existing == &path) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let Ok(info) = check_java_runtime(path.display().to_string()).await else {
|
||||
tracing::warn!("java installation at '{}' is not valid", path.display());
|
||||
continue;
|
||||
};
|
||||
valid.insert(path, info);
|
||||
valid.push((path, info));
|
||||
}
|
||||
|
||||
valid.sort_by(|(_, a), (_, b)| compare_candidates(a, b));
|
||||
|
||||
tracing::debug!(count = valid.len(), "located Java runtimes");
|
||||
|
||||
Ok(valid)
|
||||
}
|
||||
|
||||
/// The best installation satisfying `major`, if this machine has one.
|
||||
///
|
||||
/// The version requirement is a filter rather than part of the ranking, so a
|
||||
/// newer JDK can never stand in for the major that was actually asked for.
|
||||
#[must_use]
|
||||
pub fn best_for_major(candidates: &[LocatedJava], major: u32) -> Option<&LocatedJava> {
|
||||
candidates
|
||||
.iter()
|
||||
.filter(|(_, info)| parse_major_version(&info.version).is_ok_and(|found| found == major))
|
||||
.min_by(|(_, a), (_, b)| compare_candidates(a, b))
|
||||
}
|
||||
|
||||
/// Best first: a JDK beats a JRE, and between two of a kind the newer wins.
|
||||
///
|
||||
/// A kit is preferred outright rather than only as a tie-break on version,
|
||||
/// since a JRE is missing tooling that mod loaders reach for, whereas an older
|
||||
/// patch level of the same major is merely older.
|
||||
fn compare_candidates(a: &JavaCheckInfo, b: &JavaCheckInfo) -> Ordering {
|
||||
b.is_jdk
|
||||
.cmp(&a.is_jdk)
|
||||
.then_with(|| version_key(b).cmp(&version_key(a)))
|
||||
}
|
||||
|
||||
/// `java.version` as numbers, so `21.0.10` sorts above `21.0.9`.
|
||||
fn version_key(info: &JavaCheckInfo) -> (u32, Vec<u32>) {
|
||||
let major = parse_major_version(&info.version).unwrap_or(0);
|
||||
let parts = info
|
||||
.version
|
||||
.split(|c: char| !c.is_ascii_digit())
|
||||
.filter_map(|part| part.parse::<u32>().ok())
|
||||
.collect();
|
||||
|
||||
(major, parts)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
#[tracing::instrument(level = "debug")]
|
||||
fn internal_locate_java() -> Vec<PathBuf> {
|
||||
|
|
@ -169,3 +215,78 @@ fn find_java_in_path(mut found: Vec<PathBuf>) -> Vec<PathBuf> {
|
|||
}
|
||||
found
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn candidate(path: &str, version: &str, is_jdk: bool) -> LocatedJava {
|
||||
(
|
||||
PathBuf::from(path),
|
||||
JavaCheckInfo {
|
||||
version: version.to_string(),
|
||||
vendor: String::from("Test"),
|
||||
os_arch: String::from("x64"),
|
||||
is_jdk,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn ranked(mut candidates: Vec<LocatedJava>) -> Vec<String> {
|
||||
candidates.sort_by(|(_, a), (_, b)| compare_candidates(a, b));
|
||||
candidates
|
||||
.into_iter()
|
||||
.map(|(path, _)| path.display().to_string())
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_kit_outranks_a_runtime_even_when_it_is_older() {
|
||||
let ranking = ranked(vec![
|
||||
candidate("/jre-21", "21.0.9", false),
|
||||
candidate("/jdk-21", "21.0.1", true),
|
||||
]);
|
||||
|
||||
assert_eq!(ranking, vec!["/jdk-21", "/jre-21"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn patch_levels_are_compared_as_numbers() {
|
||||
let ranking = ranked(vec![
|
||||
candidate("/jdk-21.0.9", "21.0.9", true),
|
||||
candidate("/jdk-21.0.10", "21.0.10", true),
|
||||
]);
|
||||
|
||||
assert_eq!(ranking, vec!["/jdk-21.0.10", "/jdk-21.0.9"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_requested_major_still_wins_over_a_newer_kit() {
|
||||
let candidates = vec![
|
||||
candidate("/jdk-21", "21.0.3", true),
|
||||
candidate("/jre-8", "1.8.0_412", false),
|
||||
];
|
||||
|
||||
let (path, _) = best_for_major(&candidates, 8).expect("a java 8 candidate");
|
||||
assert_eq!(path, &PathBuf::from("/jre-8"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_kit_wins_among_candidates_of_the_requested_major() {
|
||||
let candidates = vec![
|
||||
candidate("/jre-17-newer", "17.0.11", false),
|
||||
candidate("/jdk-17", "17.0.2", true),
|
||||
candidate("/jdk-21", "21.0.3", true),
|
||||
];
|
||||
|
||||
let (path, _) = best_for_major(&candidates, 17).expect("a java 17 candidate");
|
||||
assert_eq!(path, &PathBuf::from("/jdk-17"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_major_nobody_satisfies_has_no_best() {
|
||||
let candidates = vec![candidate("/jdk-21", "21.0.3", true)];
|
||||
|
||||
assert!(best_for_major(&candidates, 8).is_none());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -114,7 +114,7 @@ impl JavaService {
|
|||
/// Re-scans the well-known install locations and records what it finds.
|
||||
#[tracing::instrument(level = "debug", skip(self))]
|
||||
pub async fn rescan(&self) -> JavaResult<()> {
|
||||
self.register_located(crate::locate::locate_java().await?)
|
||||
self.register_located(&crate::locate::locate_java().await?)
|
||||
.await
|
||||
}
|
||||
|
||||
|
|
@ -134,6 +134,10 @@ impl JavaService {
|
|||
// --- acquisition -------------------------------------------------------
|
||||
|
||||
/// Finds a usable Java `major`, downloading or asking the user if needed.
|
||||
///
|
||||
/// A JDK is preferred over a JRE of the same major throughout: a recorded
|
||||
/// runtime that turns out to be a bare JRE is kept only as a fallback, so a
|
||||
/// system scan gets its chance to turn up a kit first.
|
||||
#[tracing::instrument(level = "debug", skip(self, progress))]
|
||||
pub async fn prepare(
|
||||
&self,
|
||||
|
|
@ -142,19 +146,31 @@ impl JavaService {
|
|||
auto_install: bool,
|
||||
progress: Option<&GroupedProgressSession>,
|
||||
) -> JavaResult<JavaRuntime> {
|
||||
if let Some(runtime) = self.store.latest_by_major(major).await? {
|
||||
return Ok(runtime);
|
||||
let recorded = self.store.latest_by_major(major).await?;
|
||||
|
||||
if let Some(runtime) = &recorded
|
||||
&& checker::is_jdk_executable(&runtime.absolute_path)
|
||||
{
|
||||
return Ok(runtime.clone());
|
||||
}
|
||||
|
||||
if search_system {
|
||||
self.register_located(crate::locate::locate_java().await?)
|
||||
.await?;
|
||||
let located = crate::locate::locate_java().await?;
|
||||
self.register_located(&located).await?;
|
||||
|
||||
if let Some(runtime) = self.store.latest_by_major(major).await? {
|
||||
return Ok(runtime);
|
||||
// Anything located only displaces what is already recorded when it
|
||||
// is the kit that recording is not.
|
||||
if let Some((path, info)) = crate::locate::best_for_major(&located, major)
|
||||
&& (info.is_jdk || recorded.is_none())
|
||||
{
|
||||
return self.persist(path, info).await;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(runtime) = recorded {
|
||||
return Ok(runtime);
|
||||
}
|
||||
|
||||
if auto_install {
|
||||
return self.download_and_register(major, progress).await;
|
||||
}
|
||||
|
|
@ -310,12 +326,9 @@ impl JavaService {
|
|||
/// Records everything a filesystem scan turned up, skipping anything that
|
||||
/// fails to probe. A broken JDK on the system should not abort the scan.
|
||||
#[tracing::instrument(level = "debug", skip_all)]
|
||||
async fn register_located(
|
||||
&self,
|
||||
located: impl IntoIterator<Item = (PathBuf, JavaCheckInfo)>,
|
||||
) -> JavaResult<()> {
|
||||
async fn register_located(&self, located: &[(PathBuf, JavaCheckInfo)]) -> JavaResult<()> {
|
||||
for (path, info) in located {
|
||||
if let Err(err) = self.persist(&path, &info).await {
|
||||
if let Err(err) = self.persist(path, info).await {
|
||||
tracing::warn!(
|
||||
path = %path.display(),
|
||||
version = %info.version,
|
||||
|
|
@ -350,7 +363,7 @@ fn provider_for_vendor(vendor: &JavaVendor) -> Option<Box<dyn vendors::JavaRunti
|
|||
///
|
||||
/// Java 8 and earlier report `1.8.0_412`, where the major is the *second*
|
||||
/// component; 9 and later report `21.0.3`, where it is the first.
|
||||
fn parse_major_version(version: &str) -> Result<u32, JavaError> {
|
||||
pub(crate) fn parse_major_version(version: &str) -> Result<u32, JavaError> {
|
||||
if let Some(rest) = version.strip_prefix("1.") {
|
||||
let digit = rest.chars().next().ok_or_else(|| JavaError::ParseVersion {
|
||||
version: version.to_string(),
|
||||
|
|
|
|||
17
packages/oneclient_java/src/vendors/adoptium.rs
vendored
17
packages/oneclient_java/src/vendors/adoptium.rs
vendored
|
|
@ -115,7 +115,9 @@ fn adoptium_url(low: u32, high: u32) -> JavaResult<Url> {
|
|||
url.query_pairs_mut()
|
||||
.append_pair("os", ADOPTIUM_OS)
|
||||
.append_pair("architecture", ADOPTIUM_ARCH)
|
||||
.append_pair("image_type", "jre")
|
||||
// Kits only: a JRE image is missing tooling the game and its mod
|
||||
// loaders expect, so the launcher never installs one.
|
||||
.append_pair("image_type", "jdk")
|
||||
.append_pair("jvm_impl", "hotspot")
|
||||
.append_pair("project", "jdk")
|
||||
.append_pair("heap_size", "normal")
|
||||
|
|
@ -144,3 +146,16 @@ const ADOPTIUM_OS: &str = match HostTarget::CURRENT.os {
|
|||
HostOs::Linux { musl: true } => "alpine-linux",
|
||||
HostOs::Linux { musl: false } => "linux",
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn only_kits_are_ever_requested() {
|
||||
let url = adoptium_url(21, 21).expect("a valid url");
|
||||
|
||||
assert!(url.query().is_some_and(|q| q.contains("image_type=jdk")));
|
||||
assert!(!url.query().is_some_and(|q| q.contains("image_type=jre")));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
16
packages/oneclient_java/src/vendors/liberica.rs
vendored
16
packages/oneclient_java/src/vendors/liberica.rs
vendored
|
|
@ -81,7 +81,8 @@ fn liberica_url(major: Option<u32>) -> JavaResult<Url> {
|
|||
let mut q = url.query_pairs_mut();
|
||||
q.append_pair("os", LIBERICA_OS)
|
||||
.append_pair("arch", LIBERICA_ARCH)
|
||||
.append_pair("bundle-type", "jre")
|
||||
// Kits only, never a bare runtime image.
|
||||
.append_pair("bundle-type", "jdk")
|
||||
.append_pair("bitness", LIBERICA_BITNESS)
|
||||
.append_pair("package-type", LIBERICA_PACKAGE)
|
||||
.append_pair("installation-type", "archive")
|
||||
|
|
@ -114,3 +115,16 @@ const LIBERICA_OS: &str = match HostTarget::CURRENT.os {
|
|||
};
|
||||
|
||||
const LIBERICA_PACKAGE: &str = HostTarget::CURRENT.archive_ext();
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn only_kits_are_ever_requested() {
|
||||
let url = liberica_url(Some(21)).expect("a valid url");
|
||||
|
||||
assert!(url.query().is_some_and(|q| q.contains("bundle-type=jdk")));
|
||||
assert!(!url.query().is_some_and(|q| q.contains("bundle-type=jre")));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
24
packages/oneclient_java/src/vendors/zulu.rs
vendored
24
packages/oneclient_java/src/vendors/zulu.rs
vendored
|
|
@ -53,7 +53,8 @@ fn zulu_url(major: Option<u32>) -> JavaResult<Url> {
|
|||
q.append_pair("os", ZULU_OS)
|
||||
.append_pair("arch", ZULU_ARCH)
|
||||
.append_pair("archive_type", "zip")
|
||||
.append_pair("java_package_type", "jre")
|
||||
// Kits only, never a bare runtime image.
|
||||
.append_pair("java_package_type", "jdk")
|
||||
.append_pair("javafx_bundled", "false")
|
||||
.append_pair("release_status", "ga")
|
||||
.append_pair("availability_types", "CA")
|
||||
|
|
@ -80,4 +81,23 @@ const ZULU_OS: &str = match HostTarget::CURRENT.os {
|
|||
HostOs::MacOs => "macos",
|
||||
HostOs::Linux { musl: true } => "linux-musl",
|
||||
HostOs::Linux { musl: false } => "linux",
|
||||
};
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn only_kits_are_ever_requested() {
|
||||
let url = zulu_url(Some(21)).expect("a valid url");
|
||||
|
||||
assert!(
|
||||
url.query()
|
||||
.is_some_and(|q| q.contains("java_package_type=jdk"))
|
||||
);
|
||||
assert!(
|
||||
!url.query()
|
||||
.is_some_and(|q| q.contains("java_package_type=jre"))
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue