diff options
| author | 2026-01-14 05:12:31 +0100 | |
|---|---|---|
| committer | 2026-01-14 05:12:31 +0100 | |
| commit | f093d2a310627aa3ee5a2820339f8a18bd251e81 (patch) | |
| tree | c3d1cdda9f1b8fed6adb5f0dfd17bfa5c81ecb36 /src-tauri/src/core/java.rs | |
| parent | f1babdf9a625ddbb661f4e0678e6258511347656 (diff) | |
| download | DropOut-f093d2a310627aa3ee5a2820339f8a18bd251e81.tar.gz DropOut-f093d2a310627aa3ee5a2820339f8a18bd251e81.zip | |
feat(java): integrate Adoptium API for Java runtime download
Add automatic Java (Temurin) download and installation feature:
- Add Adoptium API v3 integration to fetch latest Java releases
- Support JRE and JDK image types with version selection (8/11/17/21)
- Implement platform detection for macOS, Linux, and Windows
- Add SHA256 checksum verification for downloaded archives
- Add tar.gz extraction support with Unix permission preservation
- Handle macOS-specific Java path structure (Contents/Home/bin)
- Add frontend UI with version selector and download progress
- Register Tauri commands: fetch_adoptium_java, download_adoptium_java,
fetch_available_java_versions
Dependencies added: sha2, flate2, tar, dirs
Diffstat (limited to 'src-tauri/src/core/java.rs')
| -rw-r--r-- | src-tauri/src/core/java.rs | 419 |
1 files changed, 419 insertions, 0 deletions
diff --git a/src-tauri/src/core/java.rs b/src-tauri/src/core/java.rs index e0962fa..7ef6998 100644 --- a/src-tauri/src/core/java.rs +++ b/src-tauri/src/core/java.rs @@ -2,6 +2,9 @@ use serde::{Deserialize, Serialize}; use std::path::PathBuf; use std::process::Command; +use crate::core::downloader; +use crate::utils::zip; + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct JavaInstallation { pub path: String, @@ -9,6 +12,342 @@ pub struct JavaInstallation { pub is_64bit: bool, } +/// Java image type: JRE or JDK +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum ImageType { + Jre, + Jdk, +} + +impl Default for ImageType { + fn default() -> Self { + Self::Jre + } +} + +impl std::fmt::Display for ImageType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Jre => write!(f, "jre"), + Self::Jdk => write!(f, "jdk"), + } + } +} + +/// Adoptium `/v3/assets/latest/{version}/hotspot` API response structures +#[derive(Debug, Clone, Deserialize)] +pub struct AdoptiumAsset { + pub binary: AdoptiumBinary, + pub release_name: String, + pub version: AdoptiumVersionData, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct AdoptiumBinary { + pub os: String, + pub architecture: String, + pub image_type: String, + pub package: AdoptiumPackage, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct AdoptiumPackage { + pub name: String, + pub link: String, + pub size: u64, + pub checksum: Option<String>, // SHA256 +} + +#[derive(Debug, Clone, Deserialize)] +pub struct AdoptiumVersionData { + pub major: u32, + pub minor: u32, + pub security: u32, + pub semver: String, + pub openjdk_version: String, +} + +/// Java download information from Adoptium +#[derive(Debug, Clone, Serialize)] +pub struct JavaDownloadInfo { + pub version: String, + pub release_name: String, + pub download_url: String, + pub file_name: String, + pub file_size: u64, + pub checksum: Option<String>, + pub image_type: String, +} + +/// Get the Adoptium OS name for the current platform +pub fn get_adoptium_os() -> &'static str { + #[cfg(target_os = "linux")] + { + // Check if Alpine Linux (musl libc) + if std::path::Path::new("/etc/alpine-release").exists() { + return "alpine-linux"; + } + "linux" + } + #[cfg(target_os = "macos")] + { + "mac" + } + #[cfg(target_os = "windows")] + { + "windows" + } + #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))] + { + "linux" // fallback + } +} + +/// Get the Adoptium Architecture name for the current architecture +pub fn get_adoptium_arch() -> &'static str { + #[cfg(target_arch = "x86_64")] + { + "x64" + } + #[cfg(target_arch = "aarch64")] + { + "aarch64" + } + #[cfg(target_arch = "x86")] + { + "x86" + } + #[cfg(target_arch = "arm")] + { + "arm" + } + #[cfg(not(any( + target_arch = "x86_64", + target_arch = "aarch64", + target_arch = "x86", + target_arch = "arm" + )))] + { + "x64" // fallback + } +} + +/// Get the default Java installation directory for DropOut +pub fn get_java_install_dir() -> PathBuf { + dirs::home_dir() + .unwrap_or_else(|| PathBuf::from(".")) + .join(".dropout") + .join("java") +} + +/// Get Adoptium API download info for a specific Java version and image type +/// +/// # Arguments +/// * `major_version` - Java major version (e.g., 8, 11, 17) +/// * `image_type` - JRE or JDK +/// +/// # Returns +/// * `Ok(JavaDownloadInfo)` - Download information +/// * `Err(String)` - Error message +pub async fn fetch_java_release( + major_version: u32, + image_type: ImageType, +) -> Result<JavaDownloadInfo, String> { + let os = get_adoptium_os(); + let arch = get_adoptium_arch(); + + let url = format!( + "https://api.adoptium.net/v3/assets/latest/{}/hotspot?os={}&architecture={}&image_type={}", + major_version, os, arch, image_type + ); + + let client = reqwest::Client::new(); + let response = client + .get(&url) + .header("Accept", "application/json") + .send() + .await + .map_err(|e| format!("Network request failed: {}", e))?; + + if !response.status().is_success() { + return Err(format!( + "Adoptium API returned error: {} - The version/platform might be unavailable", + response.status() + )); + } + + let assets: Vec<AdoptiumAsset> = response + .json() + .await + .map_err(|e| format!("Failed to parse API response: {}", e))?; + + let asset = assets + .into_iter() + .next() + .ok_or_else(|| format!("Java {} {} download not found", major_version, image_type))?; + + Ok(JavaDownloadInfo { + version: asset.version.semver.clone(), + release_name: asset.release_name, + download_url: asset.binary.package.link, + file_name: asset.binary.package.name, + file_size: asset.binary.package.size, + checksum: asset.binary.package.checksum, + image_type: asset.binary.image_type, + }) +} + +/// Fetch available Java versions from Adoptium API +pub async fn fetch_available_versions() -> Result<Vec<u32>, String> { + let url = "https://api.adoptium.net/v3/info/available_releases"; + + let response = reqwest::get(url) + .await + .map_err(|e| format!("Network request failed: {}", e))?; + + #[derive(Deserialize)] + struct AvailableReleases { + available_releases: Vec<u32>, + } + + let releases: AvailableReleases = response + .json() + .await + .map_err(|e| format!("Failed to parse response: {}", e))?; + + Ok(releases.available_releases) +} + +/// Download and install Java +/// +/// # Arguments +/// * `major_version` - Java major version (e.g., 8, 11, 17) +/// * `image_type` - JRE or JDK +/// * `custom_path` - Optional custom installation path +/// +/// # Returns +/// * `Ok(JavaInstallation)` - Information about the successfully installed Java +pub async fn download_and_install_java( + major_version: u32, + image_type: ImageType, + custom_path: Option<PathBuf>, +) -> Result<JavaInstallation, String> { + // 1. Fetch download information + let info = fetch_java_release(major_version, image_type).await?; + + // 2. Prepare installation directory + let install_base = custom_path.unwrap_or_else(get_java_install_dir); + let version_dir = install_base.join(format!("temurin-{}-{}", major_version, image_type)); + + std::fs::create_dir_all(&install_base) + .map_err(|e| format!("Failed to create installation directory: {}", e))?; + + // 3. Download the archive + let archive_path = install_base.join(&info.file_name); + + // Check if we need to download + let need_download = if archive_path.exists() { + if let Some(expected_checksum) = &info.checksum { + let data = std::fs::read(&archive_path) + .map_err(|e| format!("Failed to read downloaded file: {}", e))?; + !downloader::verify_checksum(&data, Some(expected_checksum), None) + } else { + false + } + } else { + true + }; + + if need_download { + let client = reqwest::Client::new(); + let response = client + .get(&info.download_url) + .send() + .await + .map_err(|e| format!("Download failed: {}", e))?; + + let bytes = response + .bytes() + .await + .map_err(|e| format!("Failed to read download content: {}", e))?; + + // Verify downloaded file checksum + if let Some(expected) = &info.checksum { + if !downloader::verify_checksum(&bytes, Some(expected), None) { + return Err("Downloaded file verification failed, the file may be corrupted".to_string()); + } + } + + std::fs::write(&archive_path, &bytes) + .map_err(|e| format!("Failed to save downloaded file: {}", e))?; + } + + // 4. Extract + // If the target directory exists, remove it first + if version_dir.exists() { + std::fs::remove_dir_all(&version_dir) + .map_err(|e| format!("Failed to remove old version directory: {}", e))?; + } + + std::fs::create_dir_all(&version_dir) + .map_err(|e| format!("Failed to create version directory: {}", e))?; + + let top_level_dir = if info.file_name.ends_with(".tar.gz") || info.file_name.ends_with(".tgz") { + zip::extract_tar_gz(&archive_path, &version_dir)? + } else if info.file_name.ends_with(".zip") { + zip::extract_zip(&archive_path, &version_dir)?; + // Find the top-level directory inside the extracted folder + find_top_level_dir(&version_dir)? + } else { + return Err(format!("Unsupported archive format: {}", info.file_name)); + }; + + // 5. Clean up downloaded archive + let _ = std::fs::remove_file(&archive_path); + + // 6. Locate java executable + // macOS has a different structure: jdk-xxx/Contents/Home/bin/java + // Linux/Windows: jdk-xxx/bin/java + let java_home = version_dir.join(&top_level_dir); + let java_bin = if cfg!(target_os = "macos") { + java_home.join("Contents").join("Home").join("bin").join("java") + } else if cfg!(windows) { + java_home.join("bin").join("java.exe") + } else { + java_home.join("bin").join("java") + }; + + if !java_bin.exists() { + return Err(format!( + "Installation completed but Java executable not found: {}", + java_bin.display() + )); + } + + // 7. Verify installation + let installation = check_java_installation(&java_bin) + .ok_or_else(|| "Fail to verify Java installation".to_string())?; + + Ok(installation) +} + +/// Find the top-level directory inside the extracted folder +fn find_top_level_dir(extract_dir: &PathBuf) -> Result<String, String> { + let entries: Vec<_> = std::fs::read_dir(extract_dir) + .map_err(|e| format!("Failed to read directory: {}", e))? + .filter_map(|e| e.ok()) + .filter(|e| e.path().is_dir()) + .collect(); + + if entries.len() == 1 { + Ok(entries[0].file_name().to_string_lossy().to_string()) + } else { + // No single top-level directory, return empty string + Ok(String::new()) + } +} + /// Detect Java installations on the system pub fn detect_java_installations() -> Vec<JavaInstallation> { let mut installations = Vec::new(); @@ -252,3 +591,83 @@ pub fn get_recommended_java(required_major_version: Option<u64>) -> Option<JavaI installations.into_iter().next() } } + +/// Detect all installed Java versions (including system installations and DropOut downloads) +pub fn detect_all_java_installations() -> Vec<JavaInstallation> { + let mut installations = detect_java_installations(); + + // Add DropOut downloaded Java versions + let dropout_java_dir = get_java_install_dir(); + if dropout_java_dir.exists() { + if let Ok(entries) = std::fs::read_dir(&dropout_java_dir) { + for entry in entries.flatten() { + let path = entry.path(); + if path.is_dir() { + // Find the java executable in this directory + let java_bin = find_java_executable(&path); + if let Some(java_path) = java_bin { + if let Some(java) = check_java_installation(&java_path) { + if !installations.iter().any(|j| j.path == java.path) { + installations.push(java); + } + } + } + } + } + } + } + + // Sort by version + installations.sort_by(|a, b| { + let v_a = parse_java_version(&a.version); + let v_b = parse_java_version(&b.version); + v_b.cmp(&v_a) + }); + + installations +} + +/// Recursively find the java executable in a directory +fn find_java_executable(dir: &PathBuf) -> Option<PathBuf> { + let bin_name = if cfg!(windows) { "java.exe" } else { "java" }; + + // Directly look in the bin directory + let direct_bin = dir.join("bin").join(bin_name); + if direct_bin.exists() { + return Some(direct_bin); + } + + // macOS: Contents/Home/bin/java + #[cfg(target_os = "macos")] + { + let macos_bin = dir.join("Contents").join("Home").join("bin").join(bin_name); + if macos_bin.exists() { + return Some(macos_bin); + } + } + + // Look in subdirectories (handle nested directories after Adoptium extraction) + if let Ok(entries) = std::fs::read_dir(dir) { + for entry in entries.flatten() { + let path = entry.path(); + if path.is_dir() { + // Try direct bin path + let nested_bin = path.join("bin").join(bin_name); + if nested_bin.exists() { + return Some(nested_bin); + } + + // macOS: nested/Contents/Home/bin/java + #[cfg(target_os = "macos")] + { + let macos_nested = path.join("Contents").join("Home").join("bin").join(bin_name); + if macos_nested.exists() { + return Some(macos_nested); + } + } + } + } + } + + None +} |