refactor(java): simpler java check (#698)

This commit is contained in:
LynithDev 2026-08-06 12:14:53 +02:00 • committed by GitHub
parent fdb3b9a0a3
commit b60ce5af12
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 158 additions and 254 deletions

4
.gitattributes vendored
View file

@ -1,6 +1,2 @@
* text=auto eol=lf * text=auto eol=lf
Cargo.lock -diff linguist-generated Cargo.lock -diff linguist-generated
pnpm-lock.yaml -diff linguist-generated
packages/client/src/bindings.ts -diff linguist-generated=true
.github/actions/publish-artifacts/dist/index.js -diff linguist-generated=true
.github/actions/clippy/dist/index.js -diff linguist-generated=true

View file

@ -1,6 +1,6 @@
{ {
"db_name": "SQLite", "db_name": "SQLite",
"query": "\n\t\tINSERT INTO java_versions (absolute_path, major, version, vendor, os_arch)\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\tRETURNING absolute_path, major, version, vendor, os_arch\n\t\t", "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",
"describe": { "describe": {
"columns": [ "columns": [
{ {
@ -57,18 +57,30 @@
"name": "os_arch" "name": "os_arch"
} }
} }
},
{
"name": "is_jdk: bool",
"ordinal": 5,
"type_info": "Integer",
"origin": {
"Table": {
"table": "java_versions",
"name": "is_jdk"
}
}
} }
], ],
"parameters": { "parameters": {
"Right": 5 "Right": 6
}, },
"nullable": [ "nullable": [
false, false,
false, false,
false, false,
false, false,
false,
false false
] ]
}, },
"hash": "a3bd6cf3048a6a5ed9d29e6bbb7f517676fe715c8af45eb5c58d69073e2d0304" "hash": "010f5dcda59ec32eaad1b9c3e2e9807f547a1376372c8b201653a36d6dd14cbc"
} }

View file

@ -1,6 +1,6 @@
{ {
"db_name": "SQLite", "db_name": "SQLite",
"query": "\n SELECT absolute_path, major, version, vendor, os_arch 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\"\n FROM java_versions\n ORDER BY major DESC, version DESC\n ",
"describe": { "describe": {
"columns": [ "columns": [
{ {
@ -57,6 +57,17 @@
"name": "os_arch" "name": "os_arch"
} }
} }
},
{
"name": "is_jdk: bool",
"ordinal": 5,
"type_info": "Integer",
"origin": {
"Table": {
"table": "java_versions",
"name": "is_jdk"
}
}
} }
], ],
"parameters": { "parameters": {
@ -67,8 +78,9 @@
false, false,
false, false,
false, false,
false,
false false
] ]
}, },
"hash": "c1d3cf930d5f38e8f1046da1a8f895ee1de2d3fe9a40ecb47731618962d2a4ad" "hash": "0d9fb5d2fa8a788770e946241ea2f62f4b794c0fdda3ccbde49384689911aeba"
} }

View file

@ -1,6 +1,6 @@
{ {
"db_name": "SQLite", "db_name": "SQLite",
"query": "\n\t\tSELECT absolute_path, major, version, vendor, os_arch\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\"\n\t\tFROM java_versions\n\t\tWHERE major = ?\n\t\tORDER BY version DESC\n\t\tLIMIT 1\n\t\t",
"describe": { "describe": {
"columns": [ "columns": [
{ {
@ -57,6 +57,17 @@
"name": "os_arch" "name": "os_arch"
} }
} }
},
{
"name": "is_jdk: bool",
"ordinal": 5,
"type_info": "Integer",
"origin": {
"Table": {
"table": "java_versions",
"name": "is_jdk"
}
}
} }
], ],
"parameters": { "parameters": {
@ -67,8 +78,9 @@
false, false,
false, false,
false, false,
false,
false false
] ]
}, },
"hash": "b153f762849649456dd97b6a5cd35fb7dafc3d5539441328e03442cea195483a" "hash": "33771b7003e64c8f19d15d663de773afa568d626c60a37bbc32cc932746ad49b"
} }

View file

@ -0,0 +1,62 @@
{
"db_name": "SQLite",
"query": "\n\t\tSELECT DISTINCT ca.cluster_id, ca.hash, ca.cluster_file_name, ca.enabled\n\t\tFROM cluster_artifacts ca\n\t\tJOIN provider_releases pr ON pr.hash = ca.hash\n\t\tWHERE ca.cluster_id = ?\n\t\t\tAND pr.provider = ?\n\t\t\tAND pr.project_id = ?\n\t\t\tAND ca.hash <> ?\n\t\t",
"describe": {
"columns": [
{
"name": "cluster_id",
"ordinal": 0,
"type_info": "Integer",
"origin": {
"Table": {
"table": "cluster_artifacts",
"name": "cluster_id"
}
}
},
{
"name": "hash",
"ordinal": 1,
"type_info": "Text",
"origin": {
"Table": {
"table": "cluster_artifacts",
"name": "hash"
}
}
},
{
"name": "cluster_file_name",
"ordinal": 2,
"type_info": "Text",
"origin": {
"Table": {
"table": "cluster_artifacts",
"name": "cluster_file_name"
}
}
},
{
"name": "enabled",
"ordinal": 3,
"type_info": "Integer",
"origin": {
"Table": {
"table": "cluster_artifacts",
"name": "enabled"
}
}
}
],
"parameters": {
"Right": 4
},
"nullable": [
false,
false,
false,
false
]
},
"hash": "bbbc24e8d4f13a39ebce8e8598f4adbc864909984313ea1c065ba226465b23ca"
}

View file

@ -1,6 +1,6 @@
{ {
"db_name": "SQLite", "db_name": "SQLite",
"query": "\n SELECT absolute_path, major, version, vendor, os_arch FROM java_versions \n WHERE absolute_path = ?\n ", "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 ",
"describe": { "describe": {
"columns": [ "columns": [
{ {
@ -57,6 +57,17 @@
"name": "os_arch" "name": "os_arch"
} }
} }
},
{
"name": "is_jdk: bool",
"ordinal": 5,
"type_info": "Integer",
"origin": {
"Table": {
"table": "java_versions",
"name": "is_jdk"
}
}
} }
], ],
"parameters": { "parameters": {
@ -67,8 +78,9 @@
false, false,
false, false,
false, false,
false,
false false
] ]
}, },
"hash": "c570e309195b6063cf0fbee866d68972de05107769dab983524fd2e74a7ae31a" "hash": "f5992c9cec6a6d7015cda4c04b463bd5fd9c42ac38b434fa8a0c0fe4f7517cf2"
} }

View file

@ -30,6 +30,7 @@ fn row_to_runtime(row: JavaVersionRow) -> JavaRuntime {
version: row.version, version: row.version,
vendor: JavaVendor::from_str(&row.vendor).unwrap_or(JavaVendor::Other(row.vendor)), vendor: JavaVendor::from_str(&row.vendor).unwrap_or(JavaVendor::Other(row.vendor)),
os_arch: row.os_arch, os_arch: row.os_arch,
is_jdk: row.is_jdk,
} }
} }
@ -43,6 +44,7 @@ impl JavaStore for SqlJavaStore {
&runtime.version, &runtime.version,
&runtime.vendor.to_string(), &runtime.vendor.to_string(),
&runtime.os_arch, &runtime.os_arch,
runtime.is_jdk,
) )
.await .await
.map_err(StoreError::new)?; .map_err(StoreError::new)?;

View file

@ -0,0 +1,5 @@
-- Whether a recorded runtime is a development kit rather than a bare runtime
-- image. Probing for it means spawning the java process, so it is remembered
-- alongside the version instead. Rows written before this migration default to
-- 0 and are corrected the next time they are probed.
ALTER TABLE `java_versions` ADD COLUMN `is_jdk` INTEGER NOT NULL DEFAULT 0;

View file

@ -10,24 +10,27 @@ pub async fn insert(
version: &str, version: &str,
vendor: &str, vendor: &str,
os_arch: &str, os_arch: &str,
is_jdk: bool,
) -> Result<JavaVersionRow, DbError> { ) -> Result<JavaVersionRow, DbError> {
sqlx::query_as!( sqlx::query_as!(
JavaVersionRow, JavaVersionRow,
r#" r#"
INSERT INTO java_versions (absolute_path, major, version, vendor, os_arch) INSERT INTO java_versions (absolute_path, major, version, vendor, os_arch, is_jdk)
VALUES (?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?)
ON CONFLICT(absolute_path) DO UPDATE SET ON CONFLICT(absolute_path) DO UPDATE SET
major = excluded.major, major = excluded.major,
version = excluded.version, version = excluded.version,
vendor = excluded.vendor, vendor = excluded.vendor,
os_arch = excluded.os_arch os_arch = excluded.os_arch,
RETURNING absolute_path, major, version, vendor, os_arch is_jdk = excluded.is_jdk
RETURNING absolute_path, major, version, vendor, os_arch, is_jdk as "is_jdk: bool"
"#, "#,
absolute_path, absolute_path,
i64::from(major), i64::from(major),
version, version,
vendor, vendor,
os_arch, os_arch,
is_jdk,
) )
.fetch_one(pool) .fetch_one(pool)
.await .await
@ -41,7 +44,8 @@ pub async fn get_by_path(
let row = sqlx::query_as!( let row = sqlx::query_as!(
JavaVersionRow, JavaVersionRow,
r#" r#"
SELECT absolute_path, major, version, vendor, os_arch FROM java_versions SELECT absolute_path, major, version, vendor, os_arch, is_jdk as "is_jdk: bool"
FROM java_versions
WHERE absolute_path = ? WHERE absolute_path = ?
"#, "#,
absolute_path, absolute_path,
@ -59,7 +63,7 @@ pub async fn get_latest_by_major(
let row = sqlx::query_as!( let row = sqlx::query_as!(
JavaVersionRow, JavaVersionRow,
r#" r#"
SELECT absolute_path, major, version, vendor, os_arch SELECT absolute_path, major, version, vendor, os_arch, is_jdk as "is_jdk: bool"
FROM java_versions FROM java_versions
WHERE major = ? WHERE major = ?
ORDER BY version DESC ORDER BY version DESC
@ -90,7 +94,8 @@ pub async fn list_all(pool: &SqlitePool) -> Result<Vec<JavaVersionRow>, DbError>
let rows = sqlx::query_as!( let rows = sqlx::query_as!(
JavaVersionRow, JavaVersionRow,
r#" r#"
SELECT absolute_path, major, version, vendor, os_arch FROM java_versions SELECT absolute_path, major, version, vendor, os_arch, is_jdk as "is_jdk: bool"
FROM java_versions
ORDER BY major DESC, version DESC ORDER BY major DESC, version DESC
"#, "#,
) )

View file

@ -7,4 +7,5 @@ pub struct JavaVersionRow {
pub version: String, pub version: String,
pub vendor: String, pub vendor: String,
pub os_arch: String, pub os_arch: String,
pub is_jdk: bool,
} }

View file

@ -14,11 +14,7 @@ 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")); 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.out.println("java.jdk=" + hasCompiler());
System.exit(returnCode); System.exit(returnCode);

View file

@ -1,7 +1,4 @@
use std::{ use std::{collections::HashMap, path::PathBuf};
collections::HashMap,
path::{Path, PathBuf},
};
use async_tempfile::TempDir; use async_tempfile::TempDir;
use tokio::sync::OnceCell; use tokio::sync::OnceCell;
@ -10,19 +7,6 @@ use crate::error::{JavaError, JavaResult};
const JAVA_INFO_CLASS: &[u8] = include_bytes!("../assets/java/JavaInfo.class"); const JAVA_INFO_CLASS: &[u8] = include_bytes!("../assets/java/JavaInfo.class");
/// 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)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct JavaCheckInfo { pub struct JavaCheckInfo {
pub version: String, pub version: String,
@ -84,15 +68,12 @@ pub async fn check_java_runtime(absolute_path: String) -> JavaResult<JavaCheckIn
return Err(JavaError::InvalidJavaPath { path: absolute_path }); 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 // 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 // without `java.desktop` launches happily and then dies mid-game, so it is
// rejected here rather than recorded as usable. // rejected here rather than recorded as usable.
if !has_awt_support(&info, &home) { if !probe_flag(&info, "java.awt") {
tracing::warn!( tracing::warn!(
path = %absolute_path, path = %absolute_path,
home = %home.display(),
"java installation has no java.awt support" "java installation has no java.awt support"
); );
return Err(JavaError::MissingAwtSupport { path: absolute_path }); return Err(JavaError::MissingAwtSupport { path: absolute_path });
@ -108,99 +89,14 @@ pub async fn check_java_runtime(absolute_path: String) -> JavaResult<JavaCheckIn
.get("java.vendor") .get("java.vendor")
.cloned() .cloned()
.unwrap_or_else(|| String::from("unknown")), .unwrap_or_else(|| String::from("unknown")),
is_jdk: probe_flag(&info, "java.jdk").unwrap_or(false) || is_jdk_home(&home), is_jdk: probe_flag(&info, "java.jdk"),
}) })
} }
/// Whether the installation behind a `java` executable is a development kit. /// Reads a boolean the probe printed; anything else counts as `false`.
/// fn probe_flag(info: &HashMap<String, String>, key: &str) -> bool {
/// Nothing is recorded about kit-ness in the store, so callers holding only a info.get(key)
/// path ask the filesystem again instead of the schema growing a column for a .is_some_and(|value| value.eq_ignore_ascii_case("true"))
/// 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(); static TEMP_JAVA_INFO: OnceCell<TempDir> = OnceCell::const_new();
@ -232,126 +128,14 @@ mod tests {
.collect() .collect()
} }
fn touch(path: &Path) { #[test]
if let Some(parent) = path.parent() { fn a_flag_the_probe_never_printed_is_false() {
std::fs::create_dir_all(parent).expect("create parent"); assert!(!probe_flag(&info(&[]), "java.awt"));
}
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] #[test]
fn the_probe_has_the_last_word_on_awt() { fn a_flag_is_read_regardless_of_case() {
let missing = Path::new("/nonexistent-java-home"); assert!(probe_flag(&info(&[("java.awt", "TRUE")]), "java.awt"));
assert!(!probe_flag(&info(&[("java.awt", "false")]), "java.awt"));
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

@ -12,6 +12,10 @@ pub struct JavaRuntime {
pub version: String, pub version: String,
pub vendor: JavaVendor, pub vendor: JavaVendor,
pub os_arch: String, pub os_arch: String,
/// Whether this is a development kit rather than a bare runtime image, as
/// the probe reported it when the runtime was recorded (see
/// [`crate::JavaCheckInfo::is_jdk`]).
pub is_jdk: bool,
} }

View file

@ -149,7 +149,7 @@ impl JavaService {
let recorded = self.store.latest_by_major(major).await?; let recorded = self.store.latest_by_major(major).await?;
if let Some(runtime) = &recorded if let Some(runtime) = &recorded
&& checker::is_jdk_executable(&runtime.absolute_path) && runtime.is_jdk
{ {
return Ok(runtime.clone()); return Ok(runtime.clone());
} }
@ -347,6 +347,7 @@ impl JavaService {
vendor: JavaVendor::from_str(&info.vendor) vendor: JavaVendor::from_str(&info.vendor)
.unwrap_or_else(|_| JavaVendor::Other(info.vendor.clone())), .unwrap_or_else(|_| JavaVendor::Other(info.vendor.clone())),
os_arch: info.os_arch.clone(), os_arch: info.os_arch.clone(),
is_jdk: info.is_jdk,
}; };
Ok(self.store.upsert(&runtime).await?) Ok(self.store.upsert(&runtime).await?)