Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
SeleniumHQ
GitHub Repository: SeleniumHQ/Selenium
Path: blob/trunk/rust/src/jre.rs
10192 views
1
// Licensed to the Software Freedom Conservancy (SFC) under one
2
// or more contributor license agreements. See the NOTICE file
3
// distributed with this work for additional information
4
// regarding copyright ownership. The SFC licenses this file
5
// to you under the Apache License, Version 2.0 (the
6
// "License"); you may not use this file except in compliance
7
// with the License. You may obtain a copy of the License at
8
//
9
// http://www.apache.org/licenses/LICENSE-2.0
10
//
11
// Unless required by applicable law or agreed to in writing,
12
// software distributed under the License is distributed on an
13
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14
// KIND, either express or implied. See the License for the
15
// specific language governing permissions and limitations
16
// under the License.
17
18
use crate::downloads::{download_to_tmp_folder, parse_json_from_url};
19
use crate::files::{create_path_if_not_exists, default_cache_folder, uncompress};
20
use crate::lock::Lock;
21
use crate::{Logger, create_http_client};
22
use anyhow::Error;
23
use anyhow::anyhow;
24
use regex::Regex;
25
use serde::{Deserialize, Serialize};
26
use std::env::consts::{ARCH, OS};
27
use std::fs;
28
use std::path::{Path, PathBuf};
29
use std::process::Command;
30
use walkdir::WalkDir;
31
use which::which;
32
33
const JRE_MAJOR_VERSION: &str = "21";
34
const MIN_SUPPORTED_JAVA_MAJOR: i32 = 11;
35
36
#[derive(Debug)]
37
pub struct JavaRuntime {
38
pub java_path: PathBuf,
39
pub version: String,
40
pub source: String,
41
}
42
43
#[derive(Debug, Deserialize, Serialize)]
44
struct AdoptiumAsset {
45
binary: AdoptiumBinary,
46
version_data: Option<AdoptiumVersionData>,
47
}
48
49
#[derive(Debug, Deserialize, Serialize)]
50
struct AdoptiumBinary {
51
package: AdoptiumPackage,
52
}
53
54
#[derive(Debug, Deserialize, Serialize)]
55
struct AdoptiumPackage {
56
link: String,
57
}
58
59
#[derive(Debug, Deserialize, Serialize)]
60
struct AdoptiumVersionData {
61
openjdk_version: Option<String>,
62
semver: Option<String>,
63
}
64
65
pub fn ensure_jre(
66
cache_path: Option<&str>,
67
timeout: u64,
68
proxy: Option<&str>,
69
offline: bool,
70
log: &Logger,
71
) -> Result<JavaRuntime, Error> {
72
if let Some(runtime) = detect_system_java(log)? {
73
return Ok(runtime);
74
}
75
76
let install_root = resolve_managed_jre_root(cache_path);
77
let install_parent = install_root
78
.parent()
79
.ok_or_else(|| anyhow!("Failed to get parent directory of JRE install root"))?
80
.to_path_buf();
81
if let Some(runtime) = detect_managed_jre_candidate(&install_root)? {
82
return Ok(runtime);
83
}
84
85
// Hold the lock in the stable parent directory because installation removes and
86
// recreates install_root while extracting archives into the parent cache folder.
87
let _lock = Lock::acquire(log, &install_parent, None)?;
88
if let Some(runtime) = detect_managed_jre_candidate(&install_root)? {
89
return Ok(runtime);
90
}
91
92
if offline {
93
return Err(Error::msg(
94
"Java not found and cannot be downloaded in offline mode",
95
));
96
}
97
98
let jre_asset = request_latest_jre_asset(timeout, proxy, log)?;
99
100
// Remove old installation if it exists
101
if install_root.exists() {
102
fs::remove_dir_all(&install_root)?;
103
}
104
105
create_path_if_not_exists(&install_parent)?;
106
107
let entries_before_uncompress = fs::read_dir(&install_parent)?
108
.filter_map(|entry| entry.ok().map(|entry| entry.path()))
109
.collect::<Vec<PathBuf>>();
110
111
let http_client = create_http_client(timeout, proxy.unwrap_or_default())?;
112
let (_tmp, archive) = download_to_tmp_folder(&http_client, jre_asset.binary.package.link, log)?;
113
114
// Extract to a temporary directory first
115
let temp_extract = install_parent.join(format!(
116
"jre_extract_tmp_{}",
117
std::time::SystemTime::now()
118
.duration_since(std::time::UNIX_EPOCH)
119
.unwrap_or_default()
120
.as_nanos()
121
));
122
123
uncompress(&archive, &temp_extract, log, OS, None, None)?;
124
125
// Move the extracted JRE root directory to install_root
126
// Adoptium tarballs can extract into the parent directory, while some archives
127
// can extract into the provided target path.
128
let mut extracted_root = None;
129
if temp_extract.exists() {
130
if find_java_binary(&temp_extract).is_some() {
131
extracted_root = Some(temp_extract.clone());
132
} else {
133
for entry in fs::read_dir(&temp_extract)? {
134
let entry = entry?;
135
let path = entry.path();
136
if path.is_dir() && find_java_binary(&path).is_some() {
137
extracted_root = Some(path);
138
break;
139
}
140
}
141
}
142
}
143
144
if extracted_root.is_none() {
145
for entry in fs::read_dir(&install_parent)? {
146
let entry = entry?;
147
let path = entry.path();
148
if path.is_dir()
149
&& !entries_before_uncompress.contains(&path)
150
&& find_java_binary(&path).is_some()
151
{
152
extracted_root = Some(path);
153
break;
154
}
155
}
156
}
157
158
let extracted_root = extracted_root.ok_or_else(|| {
159
anyhow!(
160
"Downloaded archive did not contain expected Java runtime structure in {} or {}",
161
temp_extract.display(),
162
install_parent.display()
163
)
164
})?;
165
166
if extracted_root != install_root {
167
fs::rename(&extracted_root, &install_root)?;
168
}
169
170
// Clean up temporary extraction directory
171
if temp_extract.exists() && temp_extract != install_root {
172
fs::remove_dir_all(&temp_extract)?;
173
}
174
175
let runtime = detect_managed_jre_candidate(&install_root)?.ok_or_else(|| {
176
anyhow!(format!(
177
"Downloaded Java runtime but failed to resolve java binary in {}",
178
install_root.display()
179
))
180
})?;
181
182
Ok(runtime)
183
}
184
185
fn detect_managed_jre_candidate(install_root: &Path) -> Result<Option<JavaRuntime>, Error> {
186
if install_root.exists()
187
&& let Some(runtime) = detect_managed_jre(install_root)?
188
{
189
return Ok(Some(runtime));
190
}
191
192
if let Some(parent) = install_root.parent()
193
&& parent.exists()
194
&& let Some(runtime) = detect_managed_jre(parent)?
195
{
196
return Ok(Some(runtime));
197
}
198
199
Ok(None)
200
}
201
202
fn detect_system_java(log: &Logger) -> Result<Option<JavaRuntime>, Error> {
203
let java_path = match which("java") {
204
Ok(path) => path,
205
Err(_) => return Ok(None),
206
};
207
208
let version = match read_java_version(&java_path)? {
209
Some(version) => version,
210
None => return Ok(None),
211
};
212
213
if !is_supported_java_version(&version) {
214
log.debug(format!(
215
"System Java found at {} but version {} is below minimum {}",
216
java_path.display(),
217
version,
218
MIN_SUPPORTED_JAVA_MAJOR
219
));
220
return Ok(None);
221
}
222
223
Ok(Some(JavaRuntime {
224
java_path,
225
version,
226
source: "system-jre".to_string(),
227
}))
228
}
229
230
fn detect_managed_jre(install_root: &Path) -> Result<Option<JavaRuntime>, Error> {
231
let java_path = find_java_binary(install_root);
232
if java_path.is_none() {
233
return Ok(None);
234
}
235
let java_path = java_path.unwrap();
236
237
let version = match read_java_version(&java_path)? {
238
Some(version) => version,
239
None => return Ok(None),
240
};
241
242
if !is_supported_java_version(&version) {
243
return Ok(None);
244
}
245
246
Ok(Some(JavaRuntime {
247
java_path,
248
version,
249
source: "managed-jre".to_string(),
250
}))
251
}
252
253
fn request_latest_jre_asset(
254
timeout: u64,
255
proxy: Option<&str>,
256
log: &Logger,
257
) -> Result<AdoptiumAsset, Error> {
258
let client = create_http_client(timeout, proxy.unwrap_or_default())?;
259
let os = map_os_to_adoptium(OS)?;
260
let arch = map_arch_to_adoptium(ARCH)?;
261
let url = format!(
262
"https://api.adoptium.net/v3/assets/latest/{}/hotspot?architecture={}&heap_size=normal&image_type=jre&jvm_impl=hotspot&os={}&project=jdk&vendor=eclipse",
263
JRE_MAJOR_VERSION, arch, os
264
);
265
let assets = parse_json_from_url::<Vec<AdoptiumAsset>>(&client, &url)?;
266
if assets.is_empty() {
267
return Err(anyhow!(format!("No JRE assets available in {}", url)));
268
}
269
let asset = assets.into_iter().next().unwrap();
270
if let Some(version_data) = &asset.version_data {
271
if let Some(version) = &version_data.openjdk_version {
272
log.debug(format!("Selected managed JRE version {}", version));
273
} else if let Some(semver) = &version_data.semver {
274
log.debug(format!("Selected managed JRE semver {}", semver));
275
}
276
}
277
Ok(asset)
278
}
279
280
fn resolve_managed_jre_root(cache_path: Option<&str>) -> PathBuf {
281
let root = cache_path
282
.map(PathBuf::from)
283
.unwrap_or_else(default_cache_folder);
284
root.join("jre").join(JRE_MAJOR_VERSION)
285
}
286
287
fn map_os_to_adoptium(os: &str) -> Result<&'static str, Error> {
288
match os {
289
"macos" => Ok("mac"),
290
"linux" => Ok("linux"),
291
"windows" => Ok("windows"),
292
_ => Err(anyhow!(format!("Unsupported OS for JRE download: {}", os))),
293
}
294
}
295
296
fn map_arch_to_adoptium(arch: &str) -> Result<&'static str, Error> {
297
match arch {
298
"x86_64" => Ok("x64"),
299
"aarch64" => Ok("aarch64"),
300
"x86" => Ok("x32"),
301
_ => Err(anyhow!(format!(
302
"Unsupported architecture for JRE download: {}",
303
arch
304
))),
305
}
306
}
307
308
fn find_java_binary(root: &Path) -> Option<PathBuf> {
309
let java_binary = if OS == "windows" { "java.exe" } else { "java" };
310
for entry in WalkDir::new(root).into_iter().flatten() {
311
let path = entry.path();
312
if path.is_file()
313
&& path
314
.file_name()
315
.map(|name| name.eq_ignore_ascii_case(java_binary))
316
.unwrap_or(false)
317
&& path
318
.parent()
319
.and_then(|parent| parent.file_name())
320
.map(|name| name.eq_ignore_ascii_case("bin"))
321
.unwrap_or(false)
322
{
323
return Some(path.to_path_buf());
324
}
325
}
326
None
327
}
328
329
fn read_java_version(java_path: &Path) -> Result<Option<String>, Error> {
330
let output = Command::new(java_path).arg("-version").output()?;
331
let combined_output = format!(
332
"{}\n{}",
333
String::from_utf8_lossy(&output.stdout),
334
String::from_utf8_lossy(&output.stderr)
335
);
336
parse_java_version(&combined_output)
337
}
338
339
fn parse_java_version(output: &str) -> Result<Option<String>, Error> {
340
let re = Regex::new(r#"version\s+\"([^\"]+)\""#)?;
341
Ok(re
342
.captures(output)
343
.and_then(|captures| captures.get(1).map(|m| m.as_str().to_string())))
344
}
345
346
fn is_supported_java_version(version: &str) -> bool {
347
parse_java_major(version)
348
.map(|major| major >= MIN_SUPPORTED_JAVA_MAJOR)
349
.unwrap_or(false)
350
}
351
352
fn parse_java_major(version: &str) -> Option<i32> {
353
let mut parts = version.split('.');
354
let first = parts.next()?.parse::<i32>().ok()?;
355
if first == 1 {
356
return parts.next()?.parse::<i32>().ok();
357
}
358
Some(first)
359
}
360
361
#[cfg(test)]
362
mod tests {
363
use super::{
364
OS, find_java_binary, is_supported_java_version, map_arch_to_adoptium, map_os_to_adoptium,
365
parse_java_major, parse_java_version,
366
};
367
use std::fs::{self, File};
368
use std::time::{SystemTime, UNIX_EPOCH};
369
370
fn create_test_dir(prefix: &str) -> std::path::PathBuf {
371
let unique = SystemTime::now()
372
.duration_since(UNIX_EPOCH)
373
.unwrap()
374
.as_nanos();
375
let dir = std::env::temp_dir().join(format!("{}_{}", prefix, unique));
376
fs::create_dir_all(&dir).unwrap();
377
dir
378
}
379
380
#[test]
381
fn parses_java_major_versions() {
382
assert_eq!(Some(8), parse_java_major("1.8.0_422"));
383
assert_eq!(Some(11), parse_java_major("11.0.25"));
384
assert_eq!(Some(21), parse_java_major("21.0.3"));
385
}
386
387
#[test]
388
fn validates_supported_versions() {
389
assert!(!is_supported_java_version("1.8.0_422"));
390
assert!(is_supported_java_version("11.0.25"));
391
assert!(is_supported_java_version("21.0.3"));
392
}
393
394
#[test]
395
fn extracts_version_from_java_output() {
396
let output = "openjdk version \"21.0.3\" 2026-04-15";
397
assert_eq!(
398
Some("21.0.3".to_string()),
399
parse_java_version(output).unwrap()
400
);
401
}
402
403
#[test]
404
fn map_os_to_adoptium_returns_expected_values() {
405
assert_eq!("mac", map_os_to_adoptium("macos").unwrap());
406
assert_eq!("linux", map_os_to_adoptium("linux").unwrap());
407
assert_eq!("windows", map_os_to_adoptium("windows").unwrap());
408
}
409
410
#[test]
411
fn map_os_to_adoptium_rejects_unknown_values() {
412
assert!(map_os_to_adoptium("freebsd").is_err());
413
assert!(map_os_to_adoptium("unknown").is_err());
414
}
415
416
#[test]
417
fn map_arch_to_adoptium_returns_expected_values() {
418
assert_eq!("x64", map_arch_to_adoptium("x86_64").unwrap());
419
assert_eq!("aarch64", map_arch_to_adoptium("aarch64").unwrap());
420
assert_eq!("x32", map_arch_to_adoptium("x86").unwrap());
421
}
422
423
#[test]
424
fn map_arch_to_adoptium_rejects_unknown_values() {
425
assert!(map_arch_to_adoptium("armv7").is_err());
426
assert!(map_arch_to_adoptium("unknown").is_err());
427
}
428
429
#[test]
430
fn find_java_binary_detects_managed_runtime_layout() {
431
let root = create_test_dir("jre_find_java_binary");
432
let java_name = if OS == "windows" { "java.exe" } else { "java" };
433
let java_path = root.join("jdk-21").join("bin").join(java_name);
434
435
fs::create_dir_all(java_path.parent().unwrap()).unwrap();
436
File::create(&java_path).unwrap();
437
438
let detected = find_java_binary(&root);
439
440
assert_eq!(detected, Some(java_path));
441
442
fs::remove_dir_all(&root).unwrap();
443
}
444
445
#[test]
446
fn find_java_binary_ignores_non_bin_locations() {
447
let root = create_test_dir("jre_find_java_outside_bindir");
448
let java_name = if OS == "windows" { "java.exe" } else { "java" };
449
let java_path = root.join("jdk-21").join(java_name);
450
451
fs::create_dir_all(java_path.parent().unwrap()).unwrap();
452
File::create(&java_path).unwrap();
453
454
let detected = find_java_binary(&root);
455
456
assert!(detected.is_none());
457
458
fs::remove_dir_all(&root).unwrap();
459
}
460
461
#[test]
462
fn find_java_binary_picks_first_in_bin_directory() {
463
let root = create_test_dir("jre_find_java_binsearch");
464
let java_name = if OS == "windows" { "java.exe" } else { "java" };
465
466
// Create two java binaries in different directories
467
let java_path1 = root.join("jdk-20").join("bin").join(java_name);
468
let java_path2 = root.join("jdk-21").join("bin").join(java_name);
469
470
fs::create_dir_all(java_path1.parent().unwrap()).unwrap();
471
fs::create_dir_all(java_path2.parent().unwrap()).unwrap();
472
File::create(&java_path1).unwrap();
473
File::create(&java_path2).unwrap();
474
475
let detected = find_java_binary(&root);
476
477
// Should find at least one java binary
478
assert!(detected.is_some());
479
let detected_path = detected.unwrap();
480
assert!(detected_path.to_string_lossy().contains("bin"));
481
assert!(detected_path.file_name().unwrap() == java_name);
482
483
fs::remove_dir_all(&root).unwrap();
484
}
485
}
486
487