hexomc_lib/java/
installer.rs1use std::path::{Path, PathBuf};
2use tokio::fs;
3
4use crate::{
5 download::{download_file_with_progress, DownloadTask},
6 error::{HexoError, Result},
7 install::loader::{no_progress, ProgressFn},
8};
9
10const ADOPTIUM_API: &str = "https://api.adoptium.net/v3/assets/latest";
11
12pub async fn download_java(version: u32, base_dir: &Path) -> Result<PathBuf> {
15 download_java_with_progress(version, base_dir, no_progress()).await
16}
17
18pub async fn download_java_with_progress(
23 version: u32,
24 base_dir: &Path,
25 progress: ProgressFn,
26) -> Result<PathBuf> {
27 let java_dir = base_dir.join("java").join(version.to_string());
28 let extract_dir = java_dir.join("jre");
29 if let Some(java_bin) = find_java_bin_in(&extract_dir) {
30 progress(0, 0, "Using installed Java");
31 return Ok(java_bin);
32 }
33 fs::create_dir_all(&java_dir).await?;
34
35 progress(0, 0, "Fetching Java release");
36 let (os, arch, archive_type) = platform_info();
37 let url = format!(
38 "{}/{}/hotspot?image_type=jre&os={}&architecture={}&vendor=eclipse",
39 ADOPTIUM_API, version, os, arch
40 );
41
42 let client = reqwest::Client::new();
43 let releases: Vec<AdoptiumRelease> = client
44 .get(&url)
45 .send()
46 .await?
47 .error_for_status()?
48 .json()
49 .await?;
50
51 let release = releases
52 .into_iter()
53 .next()
54 .ok_or_else(|| HexoError::JavaNotFound { required: version })?;
55
56 let binary = &release.binary;
57 let pkg = &binary.package;
58
59 let archive_path = java_dir.join(&pkg.name);
60 download_file_with_progress(&pkg.download_task(&archive_path), |downloaded, total| {
61 let total = if total == 0 { pkg.size as usize } else { total };
62 progress(downloaded, total, "Downloading Java");
63 })
64 .await?;
65
66 progress(0, 0, "Extracting Java");
67 fs::create_dir_all(&extract_dir).await?;
68
69 if archive_type == "zip" {
70 extract_zip(&archive_path, &extract_dir).await?;
71 } else {
72 extract_tar_gz(&archive_path, &extract_dir).await?;
73 }
74
75 let java_bin =
76 find_java_bin_in(&extract_dir).ok_or(HexoError::JavaNotFound { required: version })?;
77 progress(0, 0, "Java ready");
78 Ok(java_bin)
79}
80
81fn platform_info() -> (&'static str, &'static str, &'static str) {
82 let os = if cfg!(target_os = "windows") {
83 "windows"
84 } else if cfg!(target_os = "macos") {
85 "mac"
86 } else {
87 "linux"
88 };
89
90 let arch = if cfg!(target_arch = "x86_64") {
91 "x64"
92 } else if cfg!(target_arch = "aarch64") {
93 "aarch64"
94 } else {
95 "x32"
96 };
97
98 let archive = if cfg!(target_os = "windows") {
99 "zip"
100 } else {
101 "tar.gz"
102 };
103
104 (os, arch, archive)
105}
106
107#[derive(serde::Deserialize)]
108struct AdoptiumRelease {
109 binary: AdoptiumBinary,
110}
111
112#[derive(serde::Deserialize)]
113struct AdoptiumBinary {
114 package: AdoptiumPackage,
115}
116
117#[derive(serde::Deserialize)]
118struct AdoptiumPackage {
119 name: String,
120 link: String,
121 checksum: String,
122 #[serde(default)]
123 size: u64,
124}
125
126impl AdoptiumPackage {
127 fn download_task(&self, path: &Path) -> DownloadTask {
129 DownloadTask::new(&self.link, path).with_sha256(&self.checksum)
130 }
131}
132
133async fn extract_zip(archive: &Path, dest: &Path) -> Result<()> {
134 let archive = archive.to_owned();
135 let dest = dest.to_owned();
136 tokio::task::spawn_blocking(move || {
137 let file = std::fs::File::open(&archive)?;
138 let mut zip = zip::ZipArchive::new(file)?;
139 zip.extract(&dest)?;
140 Ok(())
141 })
142 .await
143 .map_err(|e| HexoError::Other(e.to_string()))?
144}
145
146async fn extract_tar_gz(archive: &Path, dest: &Path) -> Result<()> {
147 let archive = archive.to_owned();
148 let dest = dest.to_owned();
149 tokio::task::spawn_blocking(move || {
150 let file = std::fs::File::open(&archive)?;
151 let gz = flate2::read::GzDecoder::new(file);
152 let mut tar = tar::Archive::new(gz);
153 tar.unpack(&dest)?;
154 Ok(())
155 })
156 .await
157 .map_err(|e| HexoError::Other(e.to_string()))?
158}
159
160fn find_java_bin_in(dir: &Path) -> Option<PathBuf> {
161 let java_name = if cfg!(target_os = "windows") {
162 "java.exe"
163 } else {
164 "java"
165 };
166
167 let find_in = |root: &Path| {
168 [
169 root.join("bin").join(java_name),
170 root.join("Contents/Home/bin").join(java_name),
171 ]
172 .into_iter()
173 .find(|candidate| candidate.is_file())
174 };
175 find_in(dir).or_else(|| {
176 std::fs::read_dir(dir)
177 .ok()?
178 .flatten()
179 .find_map(|entry| find_in(&entry.path()))
180 })
181}
182
183#[cfg(test)]
184mod tests {
185 use super::*;
186
187 #[tokio::test]
188 async fn extracted_java_is_reused_without_network_or_extraction() {
189 for layout in ["bin", "jdk-21/bin", "jdk-21/Contents/Home/bin"] {
190 let temp = tempfile::tempdir().unwrap();
191 let java_dir = temp.path().join("java/21");
192 let bin_dir = java_dir.join("jre").join(layout);
193 fs::create_dir_all(&bin_dir).await.unwrap();
194 let java_bin = bin_dir.join(if cfg!(windows) { "java.exe" } else { "java" });
195 fs::write(&java_bin, b"existing runtime").await.unwrap();
196 let events = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
197 let collected = events.clone();
198 let progress: ProgressFn = std::sync::Arc::new(move |done, total, stage| {
199 collected
200 .lock()
201 .unwrap()
202 .push((done, total, stage.to_string()));
203 });
204 let actual = download_java_with_progress(21, temp.path(), progress)
205 .await
206 .unwrap();
207 assert_eq!(actual, java_bin);
208 assert_eq!(download_java(21, temp.path()).await.unwrap(), java_bin);
209 assert_eq!(fs::read(&java_bin).await.unwrap(), b"existing runtime");
210 assert_eq!(
211 *events.lock().unwrap(),
212 vec![(0, 0, "Using installed Java".into())]
213 );
214 assert_eq!(std::fs::read_dir(&java_dir).unwrap().count(), 1);
215 }
216 }
217
218 #[test]
219 fn java_directory_is_not_mistaken_for_executable() {
220 let temp = tempfile::tempdir().unwrap();
221 let name = if cfg!(windows) { "java.exe" } else { "java" };
222 std::fs::create_dir_all(temp.path().join("bin").join(name)).unwrap();
223 assert!(find_java_bin_in(temp.path()).is_none());
224 }
225
226 #[tokio::test]
227 async fn adoptium_package_validates_cached_archive_with_sha256() {
228 let package: AdoptiumPackage = serde_json::from_str(
229 r#"{
230 "name": "jre.zip", "link": "http://127.0.0.1:0/jre.zip",
231 "checksum": "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
232 }"#,
233 )
234 .unwrap();
235 let temp = tempfile::tempdir().unwrap();
236 let path = temp.path().join(&package.name);
237 fs::write(&path, b"abc").await.unwrap();
238 let task = package.download_task(&path);
239 assert!(task.sha1.is_none());
240 assert_eq!(task.sha256.as_deref(), Some(package.checksum.as_str()));
241 crate::download::download_file(&task).await.unwrap();
242 }
243}