use crate::download::ProgressCallback; use crate::runtime::{self, RuntimeError}; use reqwest::blocking::Client; use serde::Serialize; use std::{env, fmt, path::{Path, PathBuf}, process::Command}; #[derive(Clone, Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct JavaInstallation { pub executable: String, pub major: u8, pub version: String, } #[derive(Debug)] pub enum EnsureJavaError { Provisioning(RuntimeError), ProvisionedButUnrecognised(PathBuf), } impl fmt::Display for EnsureJavaError { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Provisioning(error) => write!(formatter, "Cannot install a Java runtime: {error}"), Self::ProvisionedButUnrecognised(path) => { write!(formatter, "Installed a Java runtime at {path:?}, but it did not report a usable version") } } } } /// 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 { candidates().into_iter().find_map(check_candidate) } /// Returns a Java runtime with at least `required_major`, preferring /// whatever the user already has installed. Only downloads and extracts a /// ShaCraft-managed Eclipse Temurin JRE under `runtime_root` (never touches /// the user's own Java) when nothing suitable is already on the machine. /// `on_progress` reports real download bytes when a JRE actually needs /// fetching; it fires once with `(1, 1)` when an existing Java is reused. pub fn ensure_java(client: &Client, runtime_root: &Path, required_major: u8, on_progress: &ProgressCallback) -> Result { if let Some(installation) = detect() { if installation.major >= required_major { on_progress(1, 1); return Ok(installation); } } let executable = runtime::ensure_runtime(client, runtime_root, required_major, on_progress).map_err(EnsureJavaError::Provisioning)?; check_candidate(executable.clone()).ok_or(EnsureJavaError::ProvisionedButUnrecognised(executable)) } fn candidates() -> Vec { 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 { 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 { 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 { let mut segments = version.split('.'); let first = segments.next()?.parse::().ok()?; if first == 1 { segments.next()?.parse::().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)); } }