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