use std::collections::HashMap; use std::path::{self, PathBuf}; use async_zip::base::read::mem::ZipFileReader; use interpulse::api::minecraft::VersionInfo; use onelauncher_entity::{java_versions, setting_profiles}; use serde::{Deserialize, Serialize}; use crate::api::ingress::{init_ingress, init_ingress_opt, send_ingress}; use crate::error::LauncherResult; use crate::store::ingress::{IngressType, SubIngress}; use crate::store::{Dirs, State}; use crate::utils::http::{fetch_advanced, fetch_json}; use crate::utils::io::{self, IOError}; pub mod dao; #[onelauncher_macro::error] #[derive(Debug, thiserror::Error)] pub enum JavaError { #[error("failed to parse version '{0}'")] ParseVersion( String, #[source] #[skip] std::num::ParseIntError, ), #[error("failed to execute java command")] Execute( #[from] #[skip] std::io::Error, ), #[error("no java installations found")] MissingJava, } /// Returns the relative path to the Java executable #[must_use] pub fn get_java_bin() -> PathBuf { #[cfg(target_os = "macos")] { PathBuf::new() .join("Contents") .join("Home") .join("bin") .join("java") } #[cfg(target_os = "windows")] { PathBuf::new().join("bin").join("javaw.exe") } #[cfg(not(any(target_os = "macos", target_os = "windows")))] { PathBuf::new().join("bin").join("java") } } #[onelauncher_macro::specta] #[derive(Debug, Clone, Serialize)] pub struct JavaInfo { pub os_arch: String, pub java_version: String, pub java_vendor: String, } #[onelauncher_macro::specta] #[derive(Serialize, Deserialize)] pub struct JavaPackage { pub download_url: String, pub name: PathBuf, pub java_version: Vec, } const JAVA_INFO_CLASS: &[u8] = include_bytes!("../../../assets/java/JavaInfo.class"); /// Accepts a path to a java executable and returns the [`JavaInfo`] pub async fn check_java_runtime( absolute_path: &PathBuf, with_ingress: bool, ) -> LauncherResult { let id = init_ingress_opt( with_ingress, IngressType::JavaCheck, "checking JRE information", 100.0, ) .await?; let id = id.as_ref(); let dir = io::tempdir()?; let file = dir.path().join("JavaInfo.class"); id.map(async |id| send_ingress(id, 25.0).await); io::write(&file, JAVA_INFO_CLASS).await?; id.map(async |id| send_ingress(id, 25.0).await); let java_info = tokio::process::Command::new(absolute_path) .arg("-cp") .arg(dir.path()) .arg("JavaInfo") .env_remove("_JAVA_OPTIONS") .output() .await .map_err(JavaError::from)?; let java_info = String::from_utf8_lossy(&java_info.stdout); id.map(async |id| send_ingress(id, 50.0).await); let info = java_info .lines() .map(|line| { let mut parts = line.splitn(2, '='); let key = parts.next().unwrap_or("unknown"); let value = parts.next().unwrap_or("unknown"); (key.to_string(), value.to_string()) }) .collect::>(); Ok(JavaInfo { os_arch: info .get("os.arch") .cloned() .unwrap_or_else(|| String::from("unknown")), java_version: info .get("java.version") .cloned() .unwrap_or_else(|| String::from("unknown")), java_vendor: info .get("java.vendor") .cloned() .unwrap_or_else(|| String::from("unknown")), }) } // MARK: Recommended Java Version /// Gets the recommended java version for the given [`VersionInfo`] and optionally profile (excl. "Global"). pub async fn get_recommended_java( info: &VersionInfo, profile: Option<&setting_profiles::Model>, ) -> LauncherResult> { // Settings profile is an override (it has highest priority) if let Some(profile) = profile && !profile.is_global() && let Some(java_id) = profile.java_id && let Ok(Some(java)) = dao::get_java_by_id(java_id).await { return Ok(Some(java)); } // Check if the version info has a suggested version let Some(supported_ver) = &info.java_version else { return Ok(None); }; dao::get_latest_java_by_major(supported_ver.major_version).await } pub async fn prepare_java(major: u32) -> LauncherResult { let id = init_ingress(IngressType::JavaPrepare, "preparing java", 100.0).await?; let java = dao::get_latest_java_by_major(major).await?; if let Some(java) = &java { send_ingress(&id, 100.0).await?; return Ok(java.clone()); } let java = locate_java().await?; if !java.is_empty() { for (path, info) in java { dao::insert_java(path, info).await?; } if let Some(java) = dao::get_latest_java_by_major(major).await? { send_ingress(&id, 100.0).await?; return Ok(java); } } tracing::warn!("no java installations found on the system, attempting to download java {}", major); let java_path = install_java_from_major(major).await?; let java_info = check_java_runtime(&java_path, false).await?; dao::insert_java(java_path, java_info).await?; if let Some(java) = dao::get_latest_java_by_major(major).await? { send_ingress(&id, 100.0).await?; return Ok(java); } Err(JavaError::MissingJava.into()) } /// Attempts to scan common paths for Java installations. /// /// **Can be heavy on I/O, don't run often!** pub async fn locate_java() -> LauncherResult> { let id = init_ingress( IngressType::JavaLocate, "locating java installations", 110.0, ) .await?; let paths = internal_locate_java(); let mut valid = HashMap::new(); send_ingress(&id, 10.0).await?; let total = paths.len(); let step_amount = 100.0 / total as f64; for path in paths { send_ingress(&id, step_amount).await?; let Ok(info) = check_java_runtime(&path, false).await else { tracing::warn!("java installation at '{}' is not valid", path.display()); continue; }; valid.insert(path, info); } Ok(valid) } #[cfg(target_os = "windows")] fn internal_locate_java() -> Vec { let mut java_homes = Vec::new(); // epic common paths let common_paths = [ r"C:\Program Files\Java", r"C:\Program Files (x86)\Java", r"C:\Program Files\OpenJDK", r"C:\Program Files\Eclipse Adoptium", r"C:\Program Files\Zulu", r"C:\Program Files\Amazon Corretto", ]; for base_path in common_paths { if let Ok(entries) = std::fs::read_dir(base_path) { for entry in entries.flatten() { if entry.file_type().map_or(false, |ft| ft.is_dir()) { let java_exe = entry.path().join("bin").join("java.exe"); if java_exe.exists() { java_homes.push(java_exe); } } } } } // env vars if let Ok(java_home) = std::env::var("JAVA_HOME") { let path_buf = PathBuf::from(java_home); if path_buf.join("bin").join("java.exe").exists() { java_homes.push(path_buf); } } if let Ok(path) = std::env::var("PATH") { for path_entry in path.split(';') { let java_exe = PathBuf::from(path_entry).join("java.exe"); if java_exe.exists() { if let Some(bin_dir) = java_exe.parent() { if let Some(java_home) = bin_dir.parent() { let java_home_path = java_home.to_path_buf(); if !java_homes.contains(&java_home_path) { java_homes.push(java_home_path); } } } } } } java_homes } #[cfg(target_os = "macos")] fn internal_locate_java() -> Vec { let mut found = Vec::new(); // TODO(macos): More paths for Java installations let paths = vec![ r"/System/Library/Frameworks/JavaVM.framework/Versions/Current/Commands", r"/Applications/Xcode.app/Contents/Applications/Application Loader.app/Contents/MacOS/itms/java", r"/Library/Internet Plug-Ins/JavaAppletPlugin.plugin/Contents/Home", ]; for path in paths { let path = PathBuf::from(path).join(get_java_bin()); if path.exists() { found.push(path); } } found } #[cfg(target_os = "linux")] fn internal_locate_java() -> Vec { let mut found = Vec::new(); let paths = vec![ r"/usr/java", r"/usr/lib/jvm", r"/usr/lib64/jvm", r"/usr/lib32/jvm", r"/opt/jdk", r"/opt/jdks", r"/app/jdk", r"~/.jdks", // Intellij downloaded jdks r"~/.gradle/jdks", // Gradle downloaded jdks r"~/.sdkman/candidates/java", // SDKMAN downloaded jdks ]; for path in paths { let path = PathBuf::from(path.replace('~', std::env::var("HOME").unwrap_or_default().as_str())); if let Ok(children) = std::fs::read_dir(path) { for child in children.flatten() { let path = child.path().join(get_java_bin()); if path.exists() { found.push(path); } } } } found } pub async fn get_zulu_packages() -> LauncherResult> { fetch_json::>( reqwest::Method::GET, format!( "https://api.azul.com/metadata/v1/zulu/packages/?os={}&arch={}&archive_type=zip&java_package_type=jre&javafx_bundled=false&latest=true&release_status=ga&availability_types=CA&certifications=tck&page=1&page_size=100", std::env::consts::OS, std::env::consts::ARCH ).as_str(), None, None, ) .await } pub async fn install_java_from_major(version: u32) -> LauncherResult { let packages = get_zulu_packages().await?; let package = packages.into_iter() .find(|p| p.java_version.contains(&version)) .ok_or_else(|| anyhow::anyhow!( "Could not find a java package for version {}", version ))?; install_java_package(package).await } pub async fn install_java_package(package: JavaPackage) -> LauncherResult { const INGRESS_TOTAL: f64 = 100.0; const INGRESS_TASKS: f64 = 4.0; const INGRESS_STEP: f64 = INGRESS_TOTAL / INGRESS_TASKS; let version = package.download_url; let ingress_id = init_ingress(IngressType::JavaPrepare, "preparing java", INGRESS_TOTAL).await?; let file = fetch_advanced( reqwest::Method::GET, &version, None, None, None, Some(&SubIngress::new(&ingress_id, INGRESS_STEP)), ) .await?; let java_dir = Dirs::get_java_dir().await?; let archive = ZipFileReader::new(file.to_vec()) .await .map_err(IOError::AsyncZipError)?; if let Some(entry) = archive.file().entries().first() { if let Some(dir) = entry .filename() .clone() .into_string() .unwrap() .split('/') .next() { let path = java_dir.join(dir); if path.exists() { io::remove_dir_all(&path).await?; } } } io::unzip_bytes(archive.data().to_vec(), &java_dir).await?; let mut base_path = java_dir.join( package .name .file_stem() .unwrap_or_default() .to_string_lossy() .to_string(), ); #[cfg(target_os = "macos")] { let java_version = package.java_version.iter().map(|v| v.to_string()).collect::>().join("."); base_path = base_path .join(format!("zulu-{java_version}.jre")) .join("Contents") .join("Home") .join("bin") .join("java"); } #[cfg(not(target_os = "macos"))] { base_path = base_path.join("bin").join(crate::constants::JAVA_BIN); } Ok(base_path) }