From 1947e2d534f22389d62f4f6678d3c576493240fd Mon Sep 17 00:00:00 2001 From: LynithDev <61880709+LynithDev@users.noreply.github.com> Date: Sat, 1 Aug 2026 11:54:23 +0200 Subject: [PATCH] feat(java): prefer JDKs, require java.awt, install kits only --- .../oneclient_java/assets/java/JavaInfo.java | 27 +- packages/oneclient_java/assets/java/README.md | 7 + .../examples/install_provider.rs | 2 +- .../oneclient_java/examples/list_providers.rs | 6 +- packages/oneclient_java/src/checker.rs | 267 +++++++++++++++++- packages/oneclient_java/src/error.rs | 3 + packages/oneclient_java/src/lib.rs | 2 +- packages/oneclient_java/src/locate.rs | 129 ++++++++- packages/oneclient_java/src/service.rs | 39 ++- .../oneclient_java/src/vendors/adoptium.rs | 17 +- .../oneclient_java/src/vendors/liberica.rs | 16 +- packages/oneclient_java/src/vendors/zulu.rs | 24 +- 12 files changed, 510 insertions(+), 29 deletions(-) diff --git a/packages/oneclient_java/assets/java/JavaInfo.java b/packages/oneclient_java/assets/java/JavaInfo.java index 0d8571cf..1e89916b 100644 --- a/packages/oneclient_java/assets/java/JavaInfo.java +++ b/packages/oneclient_java/assets/java/JavaInfo.java @@ -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; + } + } } diff --git a/packages/oneclient_java/assets/java/README.md b/packages/oneclient_java/assets/java/README.md index 2c311285..eb9180c4 100644 --- a/packages/oneclient_java/assets/java/README.md +++ b/packages/oneclient_java/assets/java/README.md @@ -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. diff --git a/packages/oneclient_java/examples/install_provider.rs b/packages/oneclient_java/examples/install_provider.rs index 85611236..ac82c420 100644 --- a/packages/oneclient_java/examples/install_provider.rs +++ b/packages/oneclient_java/examples/install_provider.rs @@ -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. diff --git a/packages/oneclient_java/examples/list_providers.rs b/packages/oneclient_java/examples/list_providers.rs index bbab2b63..2f91d866 100644 --- a/packages/oneclient_java/examples/list_providers.rs +++ b/packages/oneclient_java/examples/list_providers.rs @@ -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() { diff --git a/packages/oneclient_java/src/checker.rs b/packages/oneclient_java/src/checker.rs index bb3765ff..f6bc6615 100644 --- a/packages/oneclient_java/src/checker.rs +++ b/packages/oneclient_java/src/checker.rs @@ -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 JavaResult 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, 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 { + 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, key: &str) -> Option { + info.get(key).map(|value| value.eq_ignore_ascii_case("true")) +} + static TEMP_JAVA_INFO: OnceCell = 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 { + 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")); + } +} diff --git a/packages/oneclient_java/src/error.rs b/packages/oneclient_java/src/error.rs index 919b65a0..633cc4c7 100644 --- a/packages/oneclient_java/src/error.rs +++ b/packages/oneclient_java/src/error.rs @@ -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 }, diff --git a/packages/oneclient_java/src/lib.rs b/packages/oneclient_java/src/lib.rs index e00e022c..7dfaa3b1 100644 --- a/packages/oneclient_java/src/lib.rs +++ b/packages/oneclient_java/src/lib.rs @@ -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, diff --git a/packages/oneclient_java/src/locate.rs b/packages/oneclient_java/src/locate.rs index d8a52917..1a50e750 100644 --- a/packages/oneclient_java/src/locate.rs +++ b/packages/oneclient_java/src/locate.rs @@ -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> { +pub async fn locate_java() -> JavaResult> { let paths = internal_locate_java(); - let mut valid = HashMap::new(); + let mut valid: Vec = 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) { + 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::().ok()) + .collect(); + + (major, parts) +} + #[cfg(target_os = "windows")] #[tracing::instrument(level = "debug")] fn internal_locate_java() -> Vec { @@ -169,3 +215,78 @@ fn find_java_in_path(mut found: Vec) -> Vec { } 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) -> Vec { + 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()); + } +} diff --git a/packages/oneclient_java/src/service.rs b/packages/oneclient_java/src/service.rs index 09ad2d8f..8fda005f 100644 --- a/packages/oneclient_java/src/service.rs +++ b/packages/oneclient_java/src/service.rs @@ -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 { - 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, - ) -> 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 Result { +pub(crate) fn parse_major_version(version: &str) -> Result { if let Some(rest) = version.strip_prefix("1.") { let digit = rest.chars().next().ok_or_else(|| JavaError::ParseVersion { version: version.to_string(), diff --git a/packages/oneclient_java/src/vendors/adoptium.rs b/packages/oneclient_java/src/vendors/adoptium.rs index 0fcf6132..0e8ff275 100644 --- a/packages/oneclient_java/src/vendors/adoptium.rs +++ b/packages/oneclient_java/src/vendors/adoptium.rs @@ -115,7 +115,9 @@ fn adoptium_url(low: u32, high: u32) -> JavaResult { 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"))); + } +} diff --git a/packages/oneclient_java/src/vendors/liberica.rs b/packages/oneclient_java/src/vendors/liberica.rs index 261253e1..29fff985 100644 --- a/packages/oneclient_java/src/vendors/liberica.rs +++ b/packages/oneclient_java/src/vendors/liberica.rs @@ -81,7 +81,8 @@ fn liberica_url(major: Option) -> JavaResult { 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"))); + } +} diff --git a/packages/oneclient_java/src/vendors/zulu.rs b/packages/oneclient_java/src/vendors/zulu.rs index ab2eb828..83d72574 100644 --- a/packages/oneclient_java/src/vendors/zulu.rs +++ b/packages/oneclient_java/src/vendors/zulu.rs @@ -53,7 +53,8 @@ fn zulu_url(major: Option) -> JavaResult { 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", -}; \ No newline at end of file +}; + +#[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")) + ); + } +} \ No newline at end of file