actually properly check for AWT

This commit is contained in:
Julian Chang 2026-08-09 16:18:31 +07:00
parent 26c5aa8c49
commit 2a711c1ef3
No known key found for this signature in database
14 changed files with 215 additions and 22 deletions

View file

@ -1,6 +1,6 @@
{
"db_name": "SQLite",
"query": "\n SELECT absolute_path, major, version, vendor, os_arch, is_jdk as \"is_jdk: bool\"\n FROM java_versions\n ORDER BY major DESC, version DESC\n ",
"query": "\n SELECT absolute_path, major, version, vendor, os_arch, is_jdk as \"is_jdk: bool\", probe_version\n FROM java_versions\n ORDER BY major DESC, version DESC\n ",
"describe": {
"columns": [
{
@ -68,6 +68,17 @@
"name": "is_jdk"
}
}
},
{
"name": "probe_version",
"ordinal": 6,
"type_info": "Integer",
"origin": {
"Table": {
"table": "java_versions",
"name": "probe_version"
}
}
}
],
"parameters": {
@ -79,8 +90,9 @@
false,
false,
false,
false,
false
]
},
"hash": "0d9fb5d2fa8a788770e946241ea2f62f4b794c0fdda3ccbde49384689911aeba"
"hash": "0c3651cf175cb900c2466f0e97beca338b95637d43d9d067b974327f7720637b"
}

View file

@ -1,6 +1,6 @@
{
"db_name": "SQLite",
"query": "\n\t\tSELECT absolute_path, major, version, vendor, os_arch, is_jdk as \"is_jdk: bool\"\n\t\tFROM java_versions\n\t\tWHERE major = ?\n\t\tORDER BY version DESC\n\t\tLIMIT 1\n\t\t",
"query": "\n\t\tSELECT absolute_path, major, version, vendor, os_arch, is_jdk as \"is_jdk: bool\", probe_version\n\t\tFROM java_versions\n\t\tWHERE major = ?\n\t\tORDER BY version DESC\n\t\tLIMIT 1\n\t\t",
"describe": {
"columns": [
{
@ -68,6 +68,17 @@
"name": "is_jdk"
}
}
},
{
"name": "probe_version",
"ordinal": 6,
"type_info": "Integer",
"origin": {
"Table": {
"table": "java_versions",
"name": "probe_version"
}
}
}
],
"parameters": {
@ -79,8 +90,9 @@
false,
false,
false,
false,
false
]
},
"hash": "33771b7003e64c8f19d15d663de773afa568d626c60a37bbc32cc932746ad49b"
"hash": "14ab205c5b8d4c1c435814e4b7a155acbae41a67925b6b179b82dea1758d621a"
}

View file

@ -1,6 +1,6 @@
{
"db_name": "SQLite",
"query": "\n SELECT absolute_path, major, version, vendor, os_arch, is_jdk as \"is_jdk: bool\"\n FROM java_versions\n WHERE absolute_path = ?\n ",
"query": "\n SELECT absolute_path, major, version, vendor, os_arch, is_jdk as \"is_jdk: bool\", probe_version\n FROM java_versions\n WHERE absolute_path = ?\n ",
"describe": {
"columns": [
{
@ -68,6 +68,17 @@
"name": "is_jdk"
}
}
},
{
"name": "probe_version",
"ordinal": 6,
"type_info": "Integer",
"origin": {
"Table": {
"table": "java_versions",
"name": "probe_version"
}
}
}
],
"parameters": {
@ -79,8 +90,9 @@
false,
false,
false,
false,
false
]
},
"hash": "f5992c9cec6a6d7015cda4c04b463bd5fd9c42ac38b434fa8a0c0fe4f7517cf2"
"hash": "331bee3da454f39a8a2cc8ed4e5338b743a7cd033fdef023c4b3b93827248be1"
}

View file

@ -1,6 +1,6 @@
{
"db_name": "SQLite",
"query": "\n\t\tINSERT INTO java_versions (absolute_path, major, version, vendor, os_arch, is_jdk)\n\t\tVALUES (?, ?, ?, ?, ?, ?)\n\t\tON CONFLICT(absolute_path) DO UPDATE SET\n\t\t\tmajor = excluded.major,\n\t\t\tversion = excluded.version,\n\t\t\tvendor = excluded.vendor,\n\t\t\tos_arch = excluded.os_arch,\n\t\t\tis_jdk = excluded.is_jdk\n\t\tRETURNING absolute_path, major, version, vendor, os_arch, is_jdk as \"is_jdk: bool\"\n\t\t",
"query": "\n\t\tINSERT INTO java_versions (absolute_path, major, version, vendor, os_arch, is_jdk, probe_version)\n\t\tVALUES (?, ?, ?, ?, ?, ?, ?)\n\t\tON CONFLICT(absolute_path) DO UPDATE SET\n\t\t\tmajor = excluded.major,\n\t\t\tversion = excluded.version,\n\t\t\tvendor = excluded.vendor,\n\t\t\tos_arch = excluded.os_arch,\n\t\t\tis_jdk = excluded.is_jdk,\n\t\t\tprobe_version = excluded.probe_version\n\t\tRETURNING absolute_path, major, version, vendor, os_arch, is_jdk as \"is_jdk: bool\", probe_version\n\t\t",
"describe": {
"columns": [
{
@ -68,10 +68,21 @@
"name": "is_jdk"
}
}
},
{
"name": "probe_version",
"ordinal": 6,
"type_info": "Integer",
"origin": {
"Table": {
"table": "java_versions",
"name": "probe_version"
}
}
}
],
"parameters": {
"Right": 6
"Right": 7
},
"nullable": [
false,
@ -79,8 +90,9 @@
false,
false,
false,
false,
false
]
},
"hash": "010f5dcda59ec32eaad1b9c3e2e9807f547a1376372c8b201653a36d6dd14cbc"
"hash": "f5dbcf7798afb495efe228a4ba7d1ddd9904cc25f34bef3c16f29cfb141158f0"
}

View file

@ -25,6 +25,7 @@ fn row_to_runtime(row: JavaVersionRow) -> JavaRuntime {
vendor: JavaVendor::from_str(&row.vendor).unwrap_or(JavaVendor::Other(row.vendor)),
os_arch: row.os_arch,
is_jdk: row.is_jdk,
probe_version: row.probe_version.try_into().unwrap_or(0),
}
}
@ -39,6 +40,7 @@ impl JavaStore for SqlJavaStore {
&runtime.vendor.to_string(),
&runtime.os_arch,
runtime.is_jdk,
runtime.probe_version,
)
.await
.map_err(StoreError::new)?;

View file

@ -0,0 +1 @@
ALTER TABLE `java_versions` ADD COLUMN `probe_version` INTEGER NOT NULL DEFAULT 0;

View file

@ -3,6 +3,7 @@ use sqlx::SqlitePool;
use crate::DbError;
use crate::models::JavaVersionRow;
#[allow(clippy::too_many_arguments)]
pub async fn insert(
pool: &SqlitePool,
absolute_path: &str,
@ -11,19 +12,21 @@ pub async fn insert(
vendor: &str,
os_arch: &str,
is_jdk: bool,
probe_version: u32,
) -> Result<JavaVersionRow, DbError> {
sqlx::query_as!(
JavaVersionRow,
r#"
INSERT INTO java_versions (absolute_path, major, version, vendor, os_arch, is_jdk)
VALUES (?, ?, ?, ?, ?, ?)
INSERT INTO java_versions (absolute_path, major, version, vendor, os_arch, is_jdk, probe_version)
VALUES (?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(absolute_path) DO UPDATE SET
major = excluded.major,
version = excluded.version,
vendor = excluded.vendor,
os_arch = excluded.os_arch,
is_jdk = excluded.is_jdk
RETURNING absolute_path, major, version, vendor, os_arch, is_jdk as "is_jdk: bool"
is_jdk = excluded.is_jdk,
probe_version = excluded.probe_version
RETURNING absolute_path, major, version, vendor, os_arch, is_jdk as "is_jdk: bool", probe_version
"#,
absolute_path,
i64::from(major),
@ -31,6 +34,7 @@ pub async fn insert(
vendor,
os_arch,
is_jdk,
i64::from(probe_version),
)
.fetch_one(pool)
.await
@ -44,7 +48,7 @@ pub async fn get_by_path(
let row = sqlx::query_as!(
JavaVersionRow,
r#"
SELECT absolute_path, major, version, vendor, os_arch, is_jdk as "is_jdk: bool"
SELECT absolute_path, major, version, vendor, os_arch, is_jdk as "is_jdk: bool", probe_version
FROM java_versions
WHERE absolute_path = ?
"#,
@ -63,7 +67,7 @@ pub async fn get_latest_by_major(
let row = sqlx::query_as!(
JavaVersionRow,
r#"
SELECT absolute_path, major, version, vendor, os_arch, is_jdk as "is_jdk: bool"
SELECT absolute_path, major, version, vendor, os_arch, is_jdk as "is_jdk: bool", probe_version
FROM java_versions
WHERE major = ?
ORDER BY version DESC
@ -94,7 +98,7 @@ pub async fn list_all(pool: &SqlitePool) -> Result<Vec<JavaVersionRow>, DbError>
let rows = sqlx::query_as!(
JavaVersionRow,
r#"
SELECT absolute_path, major, version, vendor, os_arch, is_jdk as "is_jdk: bool"
SELECT absolute_path, major, version, vendor, os_arch, is_jdk as "is_jdk: bool", probe_version
FROM java_versions
ORDER BY major DESC, version DESC
"#,

View file

@ -8,4 +8,5 @@ pub struct JavaVersionRow {
pub vendor: String,
pub os_arch: String,
pub is_jdk: bool,
pub probe_version: i64,
}

View file

@ -1,3 +1,6 @@
import java.io.File;
import java.util.Locale;
public final class JavaInfo {
private static final String[] CHECKED_PROPERTIES = new String[] {"os.arch", "java.version", "java.vendor", "java.home"};
@ -15,6 +18,7 @@ public final class JavaInfo {
}
System.out.println("java.awt=" + hasClass("java.awt.Toolkit"));
System.out.println("java.awt.natives=" + hasAwtNatives());
System.out.println("java.jdk=" + hasCompiler());
System.exit(returnCode);
@ -30,6 +34,48 @@ public final class JavaInfo {
}
}
private static boolean hasAwtNatives() {
try {
String os = System.getProperty("os.name", "").toLowerCase(Locale.ROOT);
String[] required;
if (os.contains("win")) {
required = new String[] {"awt.dll", "fontmanager.dll"};
} else if (os.contains("mac") || os.contains("darwin")) {
required = new String[] {"libawt_lwawt.dylib", "libfontmanager.dylib"};
} else {
required = new String[] {"libawt_xawt.so", "libfontmanager.so"};
}
for (String name : required) {
if (!findNative(name)) {
return false;
}
}
return true;
} catch (Throwable ignored) {
return false;
}
}
private static boolean findNative(String name) {
File home = new File(System.getProperty("java.home", "."));
File[] roots = new File[] {
new File(home, "lib"),
new File(home, "bin"),
new File(new File(home, "lib"), System.getProperty("os.arch", "")),
};
for (File root : roots) {
if (new File(root, name).isFile()) {
return true;
}
}
return false;
}
private static boolean hasCompiler() {
try {
return javax.tools.ToolProvider.getSystemJavaCompiler() != null;

View file

@ -26,7 +26,9 @@ pub async fn check_java_runtime(absolute_path: String) -> JavaResult<JavaCheckIn
.arg("-cp")
.arg(temp_dir)
.arg("JavaInfo")
.env_remove("_JAVA_OPTIONS");
.env_remove("_JAVA_OPTIONS")
.env_remove("JAVA_TOOL_OPTIONS")
.env_remove("JDK_JAVA_OPTIONS");
let program = command.as_std().get_program().to_string_lossy();
let args: Vec<String> = command
@ -66,10 +68,12 @@ pub async fn check_java_runtime(absolute_path: String) -> JavaResult<JavaCheckIn
};
// Minecraft needs AWT a headless image launches fine and then dies mid-game
if !probe_flag(&info, "java.awt") {
if !has_usable_awt(&info) {
tracing::warn!(
path = %absolute_path,
"java installation has no java.awt support"
classes = probe_flag(&info, "java.awt"),
natives = probe_flag(&info, "java.awt.natives"),
"java installation has no usable java.awt support"
);
return Err(JavaError::MissingAwtSupport { path: absolute_path });
}
@ -88,6 +92,10 @@ pub async fn check_java_runtime(absolute_path: String) -> JavaResult<JavaCheckIn
})
}
fn has_usable_awt(info: &HashMap<String, String>) -> bool {
probe_flag(info, "java.awt") && probe_flag(info, "java.awt.natives")
}
fn probe_flag(info: &HashMap<String, String>, key: &str) -> bool {
info.get(key)
.is_some_and(|value| value.eq_ignore_ascii_case("true"))
@ -132,4 +140,22 @@ mod tests {
assert!(probe_flag(&info(&[("java.awt", "TRUE")]), "java.awt"));
assert!(!probe_flag(&info(&[("java.awt", "false")]), "java.awt"));
}
#[test]
fn awt_classes_without_their_natives_are_not_usable() {
assert!(!has_usable_awt(&info(&[
("java.awt", "true"),
("java.awt.natives", "false"),
])));
}
#[test]
fn awt_needs_both_halves() {
assert!(has_usable_awt(&info(&[
("java.awt", "true"),
("java.awt.natives", "true"),
])));
assert!(!has_usable_awt(&info(&[("java.awt", "true")])));
assert!(!has_usable_awt(&info(&[("java.awt.natives", "true")])));
}
}

View file

@ -13,6 +13,7 @@ pub struct JavaRuntime {
pub vendor: JavaVendor,
pub os_arch: String,
pub is_jdk: bool,
pub probe_version: u32,
}

View file

@ -41,7 +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)")]
#[error(
"java installation at '{path}' has no usable java.awt (the java.desktop module or its native libraries are missing)"
)]
MissingAwtSupport { path: String },
#[error("selected java is version {found}, but version {expected} is required")]
@ -61,6 +63,16 @@ pub enum JavaError {
}
impl JavaError {
#[must_use]
pub fn is_invalid_installation(&self) -> bool {
matches!(
self,
JavaError::InvalidJavaPath { .. }
| JavaError::MissingAwtSupport { .. }
| JavaError::ParseVersion { .. }
)
}
/// Whether this is an expected outcome or the user's environment rather
/// than a launcher bug
/// Callers decide what to do with that (e.g. suppress error reporting)

View file

@ -22,6 +22,10 @@ pub const JAVA_VENDOR_HINT: &str = "java-vendor";
pub const INSTALLABLE_MAJORS: &[u32] = &[8, 11, 16, 17, 18, 19, 20, 21, 22, 23];
pub const PROBE_VERSION: u32 = 1;
const _: () = assert!(PROBE_VERSION > 0);
enum JavaPromptAnswer {
Download,
PickFolder,
@ -68,7 +72,47 @@ impl JavaService {
let Some(path) = java_path else {
return Ok(None);
};
Ok(self.store.get_by_path(path).await?)
let Some(runtime) = self.store.get_by_path(path).await? else {
return Ok(None);
};
self.revalidate(runtime).await
}
#[tracing::instrument(level = "debug", skip(self))]
async fn revalidate(&self, runtime: JavaRuntime) -> JavaResult<Option<JavaRuntime>> {
if runtime.probe_version == PROBE_VERSION {
if Path::new(&runtime.absolute_path).is_file() {
return Ok(Some(runtime));
}
tracing::warn!(
path = %runtime.absolute_path,
"forgetting recorded Java runtime: the executable is gone"
);
self.store.delete_by_path(&runtime.absolute_path).await?;
return Ok(None);
}
match checker::check_java_runtime(runtime.absolute_path.clone()).await {
Ok(info) => Ok(Some(
self.persist(Path::new(&runtime.absolute_path), &info).await?,
)),
Err(err) if err.is_invalid_installation() => {
tracing::warn!(
path = %runtime.absolute_path,
"forgetting recorded Java runtime: {err}"
);
self.store.delete_by_path(&runtime.absolute_path).await?;
Ok(None)
}
Err(err) => {
tracing::warn!(
path = %runtime.absolute_path,
"could not re-probe recorded Java runtime, trusting the record: {err}"
);
Ok(Some(runtime))
}
}
}
#[tracing::instrument(level = "debug", skip(self))]
@ -125,7 +169,14 @@ impl JavaService {
auto_install: bool,
progress: Option<&GroupedProgressSession>,
) -> JavaResult<JavaRuntime> {
let recorded = self.store.latest_by_major(major).await?;
let recorded = loop {
let Some(runtime) = self.store.latest_by_major(major).await? else {
break None;
};
if let Some(valid) = self.revalidate(runtime).await? {
break Some(valid);
}
};
if let Some(runtime) = &recorded
&& runtime.is_jdk
@ -314,6 +365,7 @@ impl JavaService {
.unwrap_or_else(|_| JavaVendor::Other(info.vendor.clone())),
os_arch: info.os_arch.clone(),
is_jdk: info.is_jdk,
probe_version: PROBE_VERSION,
};
Ok(self.store.upsert(&runtime).await?)