Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
306 changes: 306 additions & 0 deletions rust/src/jre.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,306 @@
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.

use crate::downloads::{download_to_tmp_folder, parse_json_from_url};
use crate::files::{create_path_if_not_exists, default_cache_folder, path_to_string, uncompress};
use crate::lock::Lock;
use crate::{create_http_client, Logger};
use anyhow::anyhow;
use anyhow::Error;
use regex::Regex;
use serde::{Deserialize, Serialize};
use std::env::consts::{ARCH, OS};
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
use walkdir::WalkDir;
use which::which;

const JRE_MAJOR_VERSION: &str = "21";
const MIN_SUPPORTED_JAVA_MAJOR: i32 = 11;

#[derive(Debug)]
pub struct JavaRuntime {
pub java_path: PathBuf,
pub version: String,
pub source: String,
}

#[derive(Debug, Deserialize, Serialize)]
struct AdoptiumAsset {
binary: AdoptiumBinary,
version_data: Option<AdoptiumVersionData>,
}

#[derive(Debug, Deserialize, Serialize)]
struct AdoptiumBinary {
package: AdoptiumPackage,
}

#[derive(Debug, Deserialize, Serialize)]
struct AdoptiumPackage {
link: String,
}

#[derive(Debug, Deserialize, Serialize)]
struct AdoptiumVersionData {
openjdk_version: Option<String>,
semver: Option<String>,
}

pub fn ensure_jre(
cache_path: Option<&str>,
timeout: u64,
proxy: Option<&str>,
log: &Logger,
) -> Result<JavaRuntime, Error> {
if let Some(runtime) = detect_system_java(log)? {
return Ok(runtime);
}

let install_root = resolve_managed_jre_root(cache_path);
if let Some(runtime) = detect_managed_jre_candidate(&install_root)? {
return Ok(runtime);
}

let mut lock = Lock::acquire(log, &install_root, None)?;
if let Some(runtime) = detect_managed_jre_candidate(&install_root)? {
lock.release();
return Ok(runtime);
}

let jre_asset = request_latest_jre_asset(timeout, proxy, log)?;
if install_root.exists() {
fs::remove_dir_all(&install_root)?;
}
Comment thread
AutomatedTester marked this conversation as resolved.

if let Some(parent) = install_root.parent() {
create_path_if_not_exists(parent)?;
}

let http_client = create_http_client(timeout, proxy.unwrap_or_default())?;
let (_tmp, archive) = download_to_tmp_folder(&http_client, jre_asset.binary.package.link, log)?;
uncompress(&archive, &install_root, log, OS, None, None)?;

Comment thread
AutomatedTester marked this conversation as resolved.
let runtime = detect_managed_jre_candidate(&install_root)?.ok_or_else(|| {
anyhow!(format!(
"Downloaded Java runtime but failed to resolve java binary in {}",
install_root.display()
))
})?;
lock.release();

Ok(runtime)
Comment thread
AutomatedTester marked this conversation as resolved.
Outdated
}

fn detect_managed_jre_candidate(install_root: &Path) -> Result<Option<JavaRuntime>, Error> {
if install_root.exists() && let Some(runtime) = detect_managed_jre(install_root)? {
return Ok(Some(runtime));
}

if let Some(parent) = install_root.parent()
&& parent.exists()
&& let Some(runtime) = detect_managed_jre(parent)?
{
return Ok(Some(runtime));
}

Ok(None)
}

fn detect_system_java(log: &Logger) -> Result<Option<JavaRuntime>, Error> {
let java_path = match which("java") {
Ok(path) => path,
Err(_) => return Ok(None),
};

let version = match read_java_version(&java_path)? {
Some(version) => version,
None => return Ok(None),
};

if !is_supported_java_version(&version) {
log.debug(format!(
"System Java found at {} but version {} is below minimum {}",
java_path.display(),
version,
MIN_SUPPORTED_JAVA_MAJOR
));
return Ok(None);
}

Ok(Some(JavaRuntime {
java_path,
version,
source: "system-jre".to_string(),
}))
}

fn detect_managed_jre(install_root: &Path) -> Result<Option<JavaRuntime>, Error> {
let java_path = find_java_binary(install_root);
if java_path.is_none() {
return Ok(None);
}
let java_path = java_path.unwrap();

let version = match read_java_version(&java_path)? {
Some(version) => version,
None => return Ok(None),
};

if !is_supported_java_version(&version) {
return Ok(None);
}

Ok(Some(JavaRuntime {
java_path,
version,
source: "managed-jre".to_string(),
}))
}

fn request_latest_jre_asset(
timeout: u64,
proxy: Option<&str>,
log: &Logger,
) -> Result<AdoptiumAsset, Error> {
let client = create_http_client(timeout, proxy.unwrap_or_default())?;
let os = map_os_to_adoptium(OS)?;
let arch = map_arch_to_adoptium(ARCH)?;
let url = format!(
"https://api.adoptium.net/v3/assets/latest/{}/hotspot?architecture={}&heap_size=normal&image_type=jre&jvm_impl=hotspot&os={}&project=jdk&vendor=eclipse",
JRE_MAJOR_VERSION, arch, os
);
let assets = parse_json_from_url::<Vec<AdoptiumAsset>>(&client, &url)?;
if assets.is_empty() {
return Err(anyhow!(format!("No JRE assets available in {}", url)));
}
let asset = assets.into_iter().next().unwrap();
if let Some(version_data) = &asset.version_data {
if let Some(version) = &version_data.openjdk_version {
log.debug(format!("Selected managed JRE version {}", version));
} else if let Some(semver) = &version_data.semver {
log.debug(format!("Selected managed JRE semver {}", semver));
}
}
Ok(asset)
}

fn resolve_managed_jre_root(cache_path: Option<&str>) -> PathBuf {
let root = cache_path
.map(PathBuf::from)
.unwrap_or_else(default_cache_folder);
root.join("jre").join(JRE_MAJOR_VERSION)
}

fn map_os_to_adoptium(os: &str) -> Result<&'static str, Error> {
match os {
"macos" => Ok("mac"),
"linux" => Ok("linux"),
"windows" => Ok("windows"),
_ => Err(anyhow!(format!("Unsupported OS for JRE download: {}", os))),
}
}

fn map_arch_to_adoptium(arch: &str) -> Result<&'static str, Error> {
match arch {
"x86_64" => Ok("x64"),
"aarch64" => Ok("aarch64"),
"x86" => Ok("x32"),
_ => Err(anyhow!(format!(
"Unsupported architecture for JRE download: {}",
arch
))),
}
}

Comment thread
AutomatedTester marked this conversation as resolved.
fn find_java_binary(root: &Path) -> Option<PathBuf> {
let java_binary = if OS == "windows" { "java.exe" } else { "java" };
for entry in WalkDir::new(root).into_iter().flatten() {
let path = entry.path();
if path.is_file()
&& path
.file_name()
.map(|name| name.eq_ignore_ascii_case(java_binary))
.unwrap_or(false)
&& path_to_string(path).contains("bin")
{
return Some(path.to_path_buf());
}
}
None
}

fn read_java_version(java_path: &Path) -> Result<Option<String>, Error> {
let output = Command::new(java_path).arg("-version").output()?;
let combined_output = format!(
"{}\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
parse_java_version(&combined_output)
}

fn parse_java_version(output: &str) -> Result<Option<String>, Error> {
let re = Regex::new(r#"version\s+\"([^\"]+)\""#)?;
Ok(re
.captures(output)
.and_then(|captures| captures.get(1).map(|m| m.as_str().to_string())))
}

fn is_supported_java_version(version: &str) -> bool {
parse_java_major(version)
.map(|major| major >= MIN_SUPPORTED_JAVA_MAJOR)
.unwrap_or(false)
}

fn parse_java_major(version: &str) -> Option<i32> {
let mut parts = version.split('.');
let first = parts.next()?.parse::<i32>().ok()?;
if first == 1 {
return parts.next()?.parse::<i32>().ok();
}
Some(first)
}

#[cfg(test)]
mod tests {
use super::{is_supported_java_version, parse_java_major, parse_java_version};

#[test]
fn parses_java_major_versions() {
assert_eq!(Some(8), parse_java_major("1.8.0_422"));
assert_eq!(Some(11), parse_java_major("11.0.25"));
assert_eq!(Some(21), parse_java_major("21.0.3"));
}

#[test]
fn validates_supported_versions() {
assert!(!is_supported_java_version("1.8.0_422"));
assert!(is_supported_java_version("11.0.25"));
assert!(is_supported_java_version("21.0.3"));
}

#[test]
fn extracts_version_from_java_output() {
let output = "openjdk version \"21.0.3\" 2026-04-15";
assert_eq!(
Some("21.0.3".to_string()),
parse_java_version(output).unwrap()
);
}
}
33 changes: 19 additions & 14 deletions rust/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,36 +15,36 @@
// specific language governing permissions and limitations
// under the License.

use crate::chrome::{CHROME_NAME, CHROMEDRIVER_NAME, ChromeManager};
use crate::chrome::{ChromeManager, CHROMEDRIVER_NAME, CHROME_NAME};
use crate::config::ARCH::{ARM64, ARMV7, X32, X64};
use crate::config::OS::{MACOS, WINDOWS};
use crate::config::{ManagerConfig, str_to_os};
use crate::config::{str_to_os, ManagerConfig};
use crate::downloads::download_to_tmp_folder;
use crate::edge::{EDGE_NAMES, EDGEDRIVER_NAME, EdgeManager, WEBVIEW2_NAME};
use crate::electron::{ELECTRON_NAME, ElectronManager};
use crate::edge::{EdgeManager, EDGEDRIVER_NAME, EDGE_NAMES, WEBVIEW2_NAME};
use crate::electron::{ElectronManager, ELECTRON_NAME};
use crate::files::get_win_file_version;
use crate::files::{BrowserPath, parse_version, uncompress};
use crate::files::{
capitalize, collect_files_from_cache, create_path_if_not_exists, default_cache_folder,
find_latest_from_cache, get_binary_extension, path_to_string,
};
use crate::firefox::{FIREFOX_NAME, FirefoxManager, GECKODRIVER_NAME};
use crate::files::{parse_version, uncompress, BrowserPath};
use crate::firefox::{FirefoxManager, FIREFOX_NAME, GECKODRIVER_NAME};
use crate::grid::GRID_NAME;
use crate::iexplorer::{IE_NAMES, IEDRIVER_NAME, IExplorerManager};
use crate::iexplorer::{IExplorerManager, IEDRIVER_NAME, IE_NAMES};
use crate::lock::Lock;
use crate::logger::Logger;
use crate::metadata::{
create_browser_metadata, create_stats_metadata, get_browser_version_from_metadata,
get_metadata, is_stats_in_metadata, write_metadata,
};
use crate::safari::{SAFARI_NAME, SAFARIDRIVER_NAME, SafariManager};
use crate::safaritp::{SAFARITP_NAMES, SafariTPManager};
use crate::safari::{SafariManager, SAFARIDRIVER_NAME, SAFARI_NAME};
use crate::safaritp::{SafariTPManager, SAFARITP_NAMES};
use crate::shell::{
Command, run_shell_command, run_shell_command_by_os, run_shell_command_with_log,
run_shell_command, run_shell_command_by_os, run_shell_command_with_log, Command,
};
use crate::stats::{Props, send_stats_to_plausible};
use anyhow::Error;
use crate::stats::{send_stats_to_plausible, Props};
use anyhow::anyhow;
use anyhow::Error;
use fs_extra::file;
use fs_extra::file::CopyOptions;
use reqwest::{Client, Proxy};
Expand All @@ -65,6 +65,7 @@ pub mod files;
pub mod firefox;
pub mod grid;
pub mod iexplorer;
pub mod jre;
pub mod lock;
pub mod logger;
pub mod metadata;
Expand Down Expand Up @@ -181,7 +182,7 @@ pub trait SeleniumManager {
fn get_browser_url_for_download(&mut self, browser_version: &str) -> Result<String, Error>;

fn get_browser_label_for_download(&self, _browser_version: &str)
-> Result<Option<&str>, Error>;
-> Result<Option<&str>, Error>;

fn is_download_browser(&self) -> bool;

Expand Down Expand Up @@ -704,7 +705,11 @@ pub trait SeleniumManager {
return None;
}
let first = vector.first().unwrap().to_string();
if first.is_empty() { None } else { Some(first) }
if first.is_empty() {
None
} else {
Some(first)
}
}

fn is_windows_admin(&self) -> bool {
Expand Down
Loading
Loading