Skip to main content

lighty_java/
jre_downloader.rs

1// Copyright (c) 2025 Hamadi
2// Licensed under the MIT License
3
4//! JRE download and extraction.
5
6use std::io::Cursor;
7use std::path::{Path, PathBuf};
8use crate::errors::{JreError, JreResult};
9use lighty_core::errors::DownloadError;
10use path_absolutize::Absolutize;
11use tokio::fs;
12
13use lighty_core::system::{OperatingSystem, OS};
14use lighty_core::download::download_file;
15use lighty_core::extract::{tar_gz_extract, zip_extract};
16
17use super::JavaDistribution;
18
19#[cfg(feature = "events")]
20use lighty_event::{EventBus, Event, JavaEvent};
21
22/// Locates an existing Java binary in the runtime directory.
23///
24/// Automatically uses a fallback distribution for unsupported
25/// version/platform combinations.
26pub async fn find_java_binary(
27    runtimes_folder: &Path,
28    distribution: &JavaDistribution,
29    version: &u8,
30) -> JreResult<PathBuf> {
31    let effective_distribution = distribution
32        .get_fallback(*version)
33        .unwrap_or_else(|| distribution.clone());
34
35    let runtime_dir = build_runtime_path(runtimes_folder, &effective_distribution, version);
36
37    let binary_path = locate_binary_in_directory(&runtime_dir).await?;
38
39    #[cfg(unix)]
40    ensure_executable_permissions(&binary_path).await?;
41
42    Ok(binary_path.absolutize()?.to_path_buf())
43}
44
45/// Downloads and installs a JRE to `runtimes_folder` (events feature).
46#[cfg(feature = "events")]
47pub async fn jre_download<F>(
48    runtimes_folder: &Path,
49    distribution: &JavaDistribution,
50    version: &u8,
51    on_progress: F,
52    event_bus: Option<&EventBus>,
53) -> JreResult<PathBuf>
54where
55    F: Fn(u64, u64),
56{
57    let effective_distribution = distribution
58        .get_fallback(*version)
59        .unwrap_or_else(|| distribution.clone());
60
61    let runtime_dir = build_runtime_path(runtimes_folder, &effective_distribution, version);
62
63    prepare_installation_directory(&runtime_dir).await?;
64
65    let download_url = effective_distribution.get_download_url(version).await?;
66
67    if let Some(bus) = event_bus {
68        let response = lighty_core::hosts::HTTP_CLIENT
69            .get(&download_url)
70            .send()
71            .await
72            .map_err(DownloadError::from)?;
73
74        let total_bytes = response.content_length().unwrap_or(0);
75
76        bus.emit(Event::Java(JavaEvent::JavaDownloadStarted {
77            distribution: effective_distribution.get_name().to_string(),
78            version: *version,
79            total_bytes,
80        }));
81    }
82
83    let archive_bytes = {
84        let event_bus_ref = event_bus;
85        download_file(&download_url, |current, _total| {
86            on_progress(current, _total);
87            if let Some(bus) = event_bus_ref {
88                // Skip the initial 0 chunk
89                if current > 0 {
90                    bus.emit(Event::Java(JavaEvent::JavaDownloadProgress {
91                        bytes: current,
92                    }));
93                }
94            }
95        })
96        .await
97?
98    };
99
100    if let Some(bus) = event_bus {
101        bus.emit(Event::Java(JavaEvent::JavaDownloadCompleted {
102            distribution: effective_distribution.get_name().to_string(),
103            version: *version,
104        }));
105    }
106
107    if let Some(bus) = event_bus {
108        bus.emit(Event::Java(JavaEvent::JavaExtractionStarted {
109            distribution: effective_distribution.get_name().to_string(),
110            version: *version,
111        }));
112    }
113
114    extract_archive(
115        &archive_bytes,
116        &runtime_dir,
117        event_bus,
118    ).await?;
119
120    let binary_path = find_java_binary(runtimes_folder, &effective_distribution, version).await?;
121
122    if let Some(bus) = event_bus {
123        bus.emit(Event::Java(JavaEvent::JavaExtractionCompleted {
124            distribution: effective_distribution.get_name().to_string(),
125            version: *version,
126            binary_path: binary_path.to_string_lossy().to_string(),
127        }));
128    }
129
130    Ok(binary_path)
131}
132
133/// Downloads and installs a JRE to `runtimes_folder`.
134#[cfg(not(feature = "events"))]
135pub async fn jre_download<F>(
136    runtimes_folder: &Path,
137    distribution: &JavaDistribution,
138    version: &u8,
139    on_progress: F,
140) -> JreResult<PathBuf>
141where
142    F: Fn(u64, u64),
143{
144    let effective_distribution = distribution
145        .get_fallback(*version)
146        .unwrap_or_else(|| distribution.clone());
147
148    let runtime_dir = build_runtime_path(runtimes_folder, &effective_distribution, version);
149
150    prepare_installation_directory(&runtime_dir).await?;
151
152    let download_url = effective_distribution.get_download_url(version).await?;
153
154    let archive_bytes = download_file(&download_url, on_progress)
155        .await
156?;
157
158    extract_archive(&archive_bytes, &runtime_dir).await?;
159
160    find_java_binary(runtimes_folder, &effective_distribution, version).await
161}
162
163/// Constructs the runtime installation path for a given distribution and version
164fn build_runtime_path(
165    runtimes_folder: &Path,
166    distribution: &JavaDistribution,
167    version: &u8,
168) -> PathBuf {
169    let mut path = runtimes_folder.to_path_buf();
170    path.push(format!("{}_{}", distribution.get_name(), version));
171    path
172}
173
174/// Prepares the installation directory by removing existing files
175async fn prepare_installation_directory(runtime_dir: &Path) -> JreResult<()> {
176    if runtime_dir.exists() {
177        fs::remove_dir_all(runtime_dir).await?;
178    }
179    fs::create_dir_all(runtime_dir).await?;
180    Ok(())
181}
182
183/// Extracts the JRE archive based on the operating system (events feature).
184#[cfg(feature = "events")]
185async fn extract_archive(
186    archive_bytes: &[u8],
187    destination: &Path,
188    event_bus: Option<&EventBus>,
189) -> JreResult<()> {
190    let cursor = Cursor::new(archive_bytes);
191
192    match OS {
193        OperatingSystem::WINDOWS => {
194            zip_extract(cursor, destination, event_bus)
195                .await
196?;
197        }
198        OperatingSystem::LINUX | OperatingSystem::OSX => {
199            tar_gz_extract(cursor, destination, event_bus)
200                .await
201?;
202        }
203        OperatingSystem::UNKNOWN => {
204            return Err(JreError::UnsupportedOS);
205        }
206    }
207
208    Ok(())
209}
210
211/// Extracts the JRE archive based on the operating system.
212#[cfg(not(feature = "events"))]
213async fn extract_archive(archive_bytes: &[u8], destination: &Path) -> JreResult<()> {
214    let cursor = Cursor::new(archive_bytes);
215
216    match OS {
217        OperatingSystem::WINDOWS => {
218            zip_extract(cursor, destination)
219                .await
220?;
221        }
222        OperatingSystem::LINUX | OperatingSystem::OSX => {
223            tar_gz_extract(cursor, destination)
224                .await
225?;
226        }
227        OperatingSystem::UNKNOWN => {
228            return Err(JreError::UnsupportedOS);
229        }
230    }
231
232    Ok(())
233}
234
235/// Locates the java binary within the extracted JRE directory.
236///
237/// Structure varies by OS and distribution:
238/// - Windows: jre_root/bin/java.exe
239/// - macOS (bundle): jre_root/Contents/Home/bin/java (Temurin)
240/// - macOS (nested bundle): jre_root/*.jre/Contents/Home/bin/java (Zulu Java 8)
241/// - macOS (flat): jre_root/bin/java (Liberica tar.gz)
242/// - Linux: jre_root/bin/java
243async fn locate_binary_in_directory(runtime_dir: &Path) -> JreResult<PathBuf> {
244    let mut entries = fs::read_dir(runtime_dir).await?;
245
246    let jre_root = entries
247        .next_entry()
248        .await?
249        .ok_or_else(|| JreError::NotFound {
250            path: runtime_dir.to_path_buf(),
251        })?
252        .path();
253
254    let java_binary = match OS {
255        OperatingSystem::WINDOWS => jre_root.join("bin").join("java.exe"),
256        OperatingSystem::OSX => {
257            // Try direct bundle, then nested .jre bundle (Zulu Java 8), then flat (Liberica tar.gz).
258            let bundle_path = jre_root.join("Contents").join("Home").join("bin").join("java");
259            if bundle_path.exists() {
260                bundle_path
261            }
262            else if let Some(nested) = find_nested_jre_bundle(&jre_root).await {
263                nested
264            }
265            else {
266                jre_root.join("bin").join("java")
267            }
268        }
269        _ => jre_root.join("bin").join("java"),
270    };
271
272    if !java_binary.exists() {
273        return Err(JreError::NotFound {
274            path: java_binary.clone(),
275        });
276    }
277
278    Ok(java_binary)
279}
280
281/// Finds a nested .jre bundle inside the JRE root (Zulu Java 8 on macOS)
282#[cfg(target_os = "macos")]
283async fn find_nested_jre_bundle(jre_root: &Path) -> Option<PathBuf> {
284    let mut entries = fs::read_dir(jre_root).await.ok()?;
285
286    while let Ok(Some(entry)) = entries.next_entry().await {
287        let path = entry.path();
288        if path.is_dir() {
289            let name = path.file_name()?.to_str()?;
290            if name.ends_with(".jre") {
291                let java_path = path.join("Contents").join("Home").join("bin").join("java");
292                if java_path.exists() {
293                    return Some(java_path);
294                }
295            }
296        }
297    }
298    None
299}
300
301#[cfg(not(target_os = "macos"))]
302async fn find_nested_jre_bundle(_jre_root: &Path) -> Option<PathBuf> {
303    None
304}
305
306/// Ensures the java binary has execution permissions on Unix systems
307#[cfg(unix)]
308async fn ensure_executable_permissions(binary_path: &Path) -> JreResult<()> {
309    use std::os::unix::fs::PermissionsExt;
310
311    let metadata = fs::metadata(binary_path).await?;
312    let current_permissions = metadata.permissions();
313
314    if current_permissions.mode() & 0o111 == 0 {
315        let mut new_permissions = current_permissions;
316        new_permissions.set_mode(0o755);
317        fs::set_permissions(binary_path, new_permissions).await?;
318    }
319
320    Ok(())
321}