feat: detect installed Java runtime
This commit is contained in:
@@ -7,6 +7,7 @@
|
||||
- интерактивный интерфейс профилей и настроек;
|
||||
- Tauri 2 native shell с безопасной командой `native_host`;
|
||||
- сохранение памяти профиля в локальном каталоге данных приложения;
|
||||
- безопасное обнаружение установленной Java (включая `JAVA_HOME`) перед запуском;
|
||||
- адаптивное окно для Windows, Linux и macOS;
|
||||
- базовая структура, в которую будут добавлены манифесты, проверка файлов,
|
||||
Java 21 и запуск Minecraft.
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
use serde::Serialize;
|
||||
use std::{env, path::PathBuf, process::Command};
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct JavaInstallation {
|
||||
pub executable: String,
|
||||
pub major: u8,
|
||||
pub version: String,
|
||||
}
|
||||
|
||||
/// Finds a usable Java runtime without modifying the machine.
|
||||
///
|
||||
/// The launcher will later use this result to decide whether Java 21 needs to
|
||||
/// be bundled. We deliberately check JAVA_HOME first: it is explicit and makes
|
||||
/// development installations predictable across all supported platforms.
|
||||
pub fn detect() -> Option<JavaInstallation> {
|
||||
candidates().into_iter().find_map(check_candidate)
|
||||
}
|
||||
|
||||
fn candidates() -> Vec<PathBuf> {
|
||||
let executable = if cfg!(target_os = "windows") {
|
||||
"java.exe"
|
||||
} else {
|
||||
"java"
|
||||
};
|
||||
|
||||
let mut candidates = Vec::new();
|
||||
if let Some(java_home) = env::var_os("JAVA_HOME") {
|
||||
candidates.push(PathBuf::from(java_home).join("bin").join(executable));
|
||||
}
|
||||
candidates.push(PathBuf::from(executable));
|
||||
candidates
|
||||
}
|
||||
|
||||
fn check_candidate(candidate: PathBuf) -> Option<JavaInstallation> {
|
||||
let output = Command::new(&candidate).arg("-version").output().ok()?;
|
||||
if !output.status.success() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let source = format!(
|
||||
"{}\n{}",
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
let version = parse_version(&source)?;
|
||||
let major = parse_major(&version)?;
|
||||
Some(JavaInstallation {
|
||||
executable: candidate.display().to_string(),
|
||||
major,
|
||||
version,
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_version(source: &str) -> Option<String> {
|
||||
let marker = "version \"";
|
||||
let start = source.find(marker)? + marker.len();
|
||||
let remainder = &source[start..];
|
||||
let end = remainder.find('"')?;
|
||||
Some(remainder[..end].to_owned())
|
||||
}
|
||||
|
||||
fn parse_major(version: &str) -> Option<u8> {
|
||||
let mut segments = version.split('.');
|
||||
let first = segments.next()?.parse::<u8>().ok()?;
|
||||
if first == 1 {
|
||||
segments.next()?.parse::<u8>().ok()
|
||||
} else {
|
||||
Some(first)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{parse_major, parse_version};
|
||||
|
||||
#[test]
|
||||
fn parses_modern_java_version() {
|
||||
let output = "openjdk version \"21.0.8\" 2025-07-15";
|
||||
assert_eq!(parse_version(output).as_deref(), Some("21.0.8"));
|
||||
assert_eq!(parse_major("21.0.8"), Some(21));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_legacy_java_version() {
|
||||
assert_eq!(parse_major("1.8.0_452"), Some(8));
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
mod java;
|
||||
mod manifest;
|
||||
mod profile;
|
||||
mod settings;
|
||||
@@ -29,6 +30,12 @@ fn native_host(app: AppHandle) -> Result<NativeHost, String> {
|
||||
})
|
||||
}
|
||||
|
||||
/// Detects an existing Java installation. This is read-only and never downloads Java.
|
||||
#[tauri::command]
|
||||
fn detect_java() -> Option<java::JavaInstallation> {
|
||||
java::detect()
|
||||
}
|
||||
|
||||
/// Validates an untrusted profile manifest before any file is downloaded.
|
||||
#[tauri::command]
|
||||
fn validate_manifest(manifest_json: String) -> Result<(), String> {
|
||||
@@ -97,6 +104,7 @@ pub fn run() {
|
||||
tauri::Builder::default()
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
native_host,
|
||||
detect_java,
|
||||
validate_manifest,
|
||||
inspect_profile,
|
||||
sync_profile,
|
||||
|
||||
+26
-1
@@ -45,6 +45,12 @@ type NativeSettings = {
|
||||
memoryMb: number
|
||||
}
|
||||
|
||||
type JavaInstallation = {
|
||||
executable: string
|
||||
major: number
|
||||
version: string
|
||||
}
|
||||
|
||||
const servers: Server[] = [
|
||||
{
|
||||
id: 'aoc',
|
||||
@@ -76,6 +82,7 @@ function App() {
|
||||
const [settingsOpen, setSettingsOpen] = useState(false)
|
||||
const [ram, setRam] = useState(6)
|
||||
const [nativeHost, setNativeHost] = useState<NativeHost | null>(null)
|
||||
const [java, setJava] = useState<JavaInstallation | null | undefined>(undefined)
|
||||
|
||||
useEffect(() => {
|
||||
if (progress === null) return
|
||||
@@ -96,6 +103,9 @@ function App() {
|
||||
invoke<NativeSettings>('load_settings')
|
||||
.then((settings) => setRam(settings.memoryMb / 1024))
|
||||
.catch(() => undefined)
|
||||
invoke<JavaInstallation | null>('detect_java')
|
||||
.then(setJava)
|
||||
.catch(() => setJava(null))
|
||||
}, [])
|
||||
|
||||
const updateRam = (memoryGb: number) => {
|
||||
@@ -248,7 +258,22 @@ function App() {
|
||||
<input type="range" min="3" max="12" value={ram} onChange={(e) => updateRam(Number(e.target.value))} />
|
||||
<small>Для Aeronautics рекомендуется 6 ГБ</small>
|
||||
</label>
|
||||
<button className="setting-row"><span><FolderOpen />Папка игры</span><ChevronRight /></button>
|
||||
<div className="setting-row static">
|
||||
<span><FolderOpen />Папка игры</span>
|
||||
<small>{nativeHost ? 'В каталоге лаунчера' : 'Определяется…'}</small>
|
||||
</div>
|
||||
<div className="setting-row static">
|
||||
<span><Wrench />Java</span>
|
||||
<small>
|
||||
{java === undefined
|
||||
? 'Проверяем…'
|
||||
: java && java.major >= 21
|
||||
? `Java ${java.major} найдена`
|
||||
: java
|
||||
? `Нужна Java 21 · найдена ${java.major}`
|
||||
: 'Java не найдена'}
|
||||
</small>
|
||||
</div>
|
||||
<button className="setting-row"><span><Wrench />Дополнительные параметры</span><ChevronRight /></button>
|
||||
<div className="drawer-note">
|
||||
{nativeHost ? `Данные лаунчера: ${nativeHost.dataDir}` : 'Java 21 будет управляться лаунчером автоматически.'}
|
||||
|
||||
@@ -141,6 +141,9 @@ button:focus-visible, input:focus-visible { outline: 2px solid var(--green); out
|
||||
.setting-row span { display: flex; align-items: center; gap: 11px; font-size: 12px; }
|
||||
.setting-row svg { width: 17px; color: #677068; }
|
||||
.setting-row:hover { color: white; }
|
||||
.setting-row.static { cursor: default; }
|
||||
.setting-row.static:hover { color: #abb3ac; }
|
||||
.setting-row small { color: #7f8a80; font-size: 11px; }
|
||||
.drawer-note { position: absolute; left: 28px; right: 28px; bottom: 26px; padding: 13px 15px; background: #17231a; color: #8fb193; border-radius: 10px; font-size: 10px; }
|
||||
|
||||
@media (max-width: 1160px) {
|
||||
|
||||
Reference in New Issue
Block a user