feat(java): prefer JDKs, require java.awt, install kits only

This commit is contained in:
LynithDev 2026-08-01 11:54:23 +02:00
parent 038c3542ce
commit 1947e2d534
12 changed files with 510 additions and 29 deletions

View file

@ -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;
}
}
}

View file

@ -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.

View file

@ -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.

View file

@ -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() {

View file

@ -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"));
}
}

View file

@ -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 },

View file

@ -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,

View file

@ -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());
}
}

View file

@ -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(),

View file

@ -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")));
}
}

View file

@ -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")));
}
}

View file

@ -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"))
);
}
}