java_runtimes/
lib.rs

1//! `java-runtimes` is a rust library for detecting java runtimes in current system.
2//!
3//! * To detect java runtimes, see [`detector`]
4//!
5//! # Examples
6//!
7
8//! Detect Java runtime from environment variables
9//!
10//! ```rust
11//! use java_runtimes::detector;
12//!
13//! let runtimes = detector::detect_java_in_environments();
14//! println!("Detected Java runtimes: {:?}", runtimes);
15//! ```
16//!
17//! Detect Java runtimes recursively within multiple paths
18//!
19//! ```rust
20//! use java_runtimes::detector;
21//!
22//! let runtimes = detector::detect_java_in_paths(&[
23//!     "/usr".as_ref(),
24//!     "/opt".as_ref(),
25//! ], 2);
26//! println!("Detected Java runtimes in multiple paths: {:?}", runtimes);
27//! ```
28
29pub mod detector;
30pub mod error;
31
32use crate::error::{Error, ErrorKind};
33use regex::Regex;
34use serde::{Deserialize, Serialize};
35use std::env;
36use std::ffi::OsString;
37use std::path::{Path, PathBuf};
38use std::process::Command;
39
40/// Struct [`JavaRuntime`] Represents a java runtime in specific path.
41///
42/// To detect java runtimes from specific path, see [`detector`]
43#[derive(Serialize, Deserialize, Debug)]
44pub struct JavaRuntime {
45    os: String,
46    path: PathBuf,
47    version_string: String,
48}
49
50impl JavaRuntime {
51    /// Used to match the version string in the command output
52    ///
53    const VERSION_PATTERN: &'static str = r#".*"((\d+)\.(\d+)([\d._]+)?)".*"#;
54    /// Create a [`JavaRuntime`] object from the path of java executable file
55    ///
56    /// It executes command `java -version` to get the version information
57    ///
58    /// # Parameters
59    ///
60    /// * `path` Path to java executable file.
61    ///
62    /// # Examples
63    ///
64    /// ```rust
65    /// use java_runtimes::JavaRuntime;
66    ///
67    /// let _ = JavaRuntime::from_executable(r"D:\java\jdk-17.0.4.1\bin\java.exe".as_ref());
68    /// let _ = JavaRuntime::from_executable(r"../../runtimes/jdk-1.8.0_291/bin/java".as_ref());
69    /// ```
70    pub fn from_executable(path: &Path) -> Result<Self, Error> {
71        let mut java = Self {
72            os: env::consts::OS.to_string(),
73            path: path.to_path_buf(),
74            version_string: String::new(),
75        };
76        java.update()?;
77        Ok(java)
78    }
79
80    /// Mannually create a [`JavaRuntime`] instance, without checking if it's available
81    ///
82    /// # Parameters
83    ///
84    /// * `os` Got from [`env::consts::OS`]
85    /// * `path` The path of java executable file, can be either relative or absolute
86    /// * `version_string` can be like `"17.0.4.1"` or the output of command `java -version`
87    ///
88    /// # Examples
89    ///
90    /// ```rust
91    /// use java_runtimes::JavaRuntime;
92    /// use std::env;
93    /// use std::path::Path;
94    ///
95    /// let java_exe_path = Path::new("../java/jdk-17.0.4.1/bin/java");
96    /// let version_outputs = r#"java version "17.0.4.1" 2022-08-18 LTS
97    /// Java(TM) SE Runtime Environment (build 17.0.4.1+1-LTS-2)
98    /// Java HotSpot(TM) 64-Bit Server VM (build 17.0.4.1+1-LTS-2, mixed mode, sharing)
99    /// "#;
100    /// let runtime = JavaRuntime::new(env::consts::OS, java_exe_path, version_outputs).unwrap();
101    /// assert_eq!(runtime.get_version_string(), "17.0.4.1");
102    /// assert!(runtime.is_same_os());
103    /// ```
104    pub fn new(os: &str, path: &Path, version_string: &str) -> Result<Self, Error> {
105        let version_string = Self::extract_version(version_string)?;
106        Ok(Self {
107            os: os.to_string(),
108            path: path.to_path_buf(),
109            version_string: version_string.to_string(),
110        })
111    }
112
113    /// Get the operating system of the java runtime
114    ///
115    /// The os string comes from [`env::consts::OS`] when this object was created.
116    pub fn get_os(&self) -> &str {
117        &self.os
118    }
119    pub fn is_windows(&self) -> bool {
120        self.os == "windows"
121    }
122    /// Get the path of java executable file
123    ///
124    /// It can be absolute or relative, depends on how you created it.
125    ///
126    /// # Examples
127    ///
128    /// * `D:\Java\jdk-17.0.4.1\bin\java.exe` (Windows, absolute)
129    /// * `../../runtimes/jdk-1.8.0_291/bin/java` (Linux, relative)
130    pub fn get_executable(&self) -> &Path {
131        &self.path
132    }
133
134    /// Returns `true` if the `Path` has a root.
135    ///
136    /// Refer to [`Path::has_root`]
137    ///
138    /// # Examples
139    ///
140    /// ```rust
141    /// use java_runtimes::JavaRuntime;
142    ///
143    /// let runtime = JavaRuntime::new("linux", "/jdk/bin/java".as_ref(), "21.0.3").unwrap();
144    /// assert!(runtime.has_root());
145    ///
146    /// let runtime = JavaRuntime::new("windows", r"D:\jdk\bin\java.exe".as_ref(), "21.0.3").unwrap();
147    /// assert!(runtime.has_root());
148    ///
149    /// let runtime = JavaRuntime::new("linux", "../jdk/bin/java".as_ref(), "21.0.3").unwrap();
150    /// assert!(!runtime.has_root());
151    ///
152    /// let runtime = JavaRuntime::new("windows", r"..\jdk\bin\java.exe".as_ref(), "21.0.3").unwrap();
153    /// assert!(!runtime.has_root());
154    /// ```
155    pub fn has_root(&self) -> bool {
156        self.path.has_root()
157    }
158
159    /// Get the version string
160    ///
161    /// # Examples
162    ///
163    /// ```rust
164    /// use java_runtimes::JavaRuntime;
165    ///
166    /// let runtime = JavaRuntime::new("linux", "/jdk/bin/java".as_ref(), "21.0.3").unwrap();
167    /// assert_eq!(runtime.get_version_string(), "21.0.3");
168    /// ```
169    pub fn get_version_string(&self) -> &str {
170        &self.version_string
171    }
172
173    /// Check if this is the same os as current
174    pub fn is_same_os(&self) -> bool {
175        self.os == env::consts::OS
176    }
177
178    /// Create a new [`JavaRuntime`] with absolute path.
179    ///
180    /// # Errors
181    ///
182    /// Returns an [`Err`] if the current working directory value is invalid. Refer to [`env::current_dir`]
183    ///
184    /// Possible cases:
185    ///
186    /// * Current directory does not exist.
187    /// * There are insufficient permissions to access the current directory.
188    pub fn to_absolute(&self) -> Result<Self, Error> {
189        let cwd = env::current_dir().or(Err(Error::new(ErrorKind::InvalidWorkDir)))?;
190        let path_absolute = self.path.join(cwd);
191        let new_runtime = Self::new(&self.os, &path_absolute, &self.version_string)?;
192        Ok(new_runtime)
193    }
194
195    /// Try executing `java -version` and parse the output to get the version.
196    ///
197    /// If success, it will update the version value in this [`JavaRuntime`] instance.
198    pub fn update(&mut self) -> Result<(), Error> {
199        if !Self::looks_like_java_executable_file(&self.path) {
200            return Err(Error::new(ErrorKind::LooksNotLikeJavaExecutableFile(
201                self.path.clone(),
202            )));
203        }
204
205        let output = Command::new(&self.path)
206            .arg("-version")
207            .output()
208            .map_err(|err| Error::new(ErrorKind::JavaOutputFailed(err)))?;
209
210        if output.status.success() {
211            let version_output = String::from_utf8_lossy(&output.stderr).to_string();
212            self.version_string = Self::extract_version(&version_output)?;
213            Ok(())
214        } else {
215            Err(Error::new(ErrorKind::GettingJavaVersionFailed(
216                self.path.clone(),
217            )))
218        }
219    }
220
221    /// Test if this runtime is available currently
222    ///
223    /// It executes command `java -version` to see if it works
224    pub fn is_available(&self) -> bool {
225        self.is_same_os() && Self::from_executable(&self.path).is_ok()
226    }
227
228    /// Parse version string
229    ///
230    /// # Return
231    ///
232    /// `(version_string, version_major)`
233    ///
234    /// # Examples
235    ///
236    /// ```rust
237    /// use java_runtimes::JavaRuntime;
238    ///
239    /// assert_eq!(JavaRuntime::extract_version("1.8.0_333").unwrap(), "1.8.0_333");
240    /// assert_eq!(JavaRuntime::extract_version("17.0.4.1").unwrap(), "17.0.4.1");
241    /// assert_eq!(JavaRuntime::extract_version("\"17.0.4.1").unwrap(), "17.0.4.1");
242    /// assert_eq!(JavaRuntime::extract_version("java version \"17.0.4.1\"").unwrap(), "17.0.4.1");
243    /// ```
244    pub fn extract_version(version_string: &str) -> Result<String, Error> {
245        Ok(Regex::new(Self::VERSION_PATTERN)
246            .unwrap()
247            .captures(&format!("\"{}\"", &version_string))
248            .ok_or(Error::new(ErrorKind::NoJavaVersionStringFound))?
249            .get(1)
250            .ok_or(Error::new(ErrorKind::NoJavaVersionStringFound))?
251            .as_str()
252            .to_string())
253    }
254
255    /// Check if the given path looks like a java executable file
256    ///
257    /// The file must exists.
258    ///
259    /// The given path must be `**/bin/java.exe` in windows, or `**/bin/java` in unix
260    fn looks_like_java_executable_file(path: &Path) -> bool {
261        if !path.is_file() {
262            return false;
263        }
264        // to absolute
265        let path_absolute = match path.canonicalize() {
266            Ok(path) => path,
267            _ => return false,
268        };
269        // check file name
270        if let Some(file_name) = path_absolute.file_name() {
271            if file_name == Self::get_java_executable_name() {
272                // check parent name
273                if let Some(parent) = path_absolute.parent() {
274                    if let Some(dir_name) = parent.file_name() {
275                        if dir_name == "bin" {
276                            return true;
277                        }
278                    }
279                }
280            }
281        }
282        false
283    }
284
285    /// # Examples
286    /// * `java.exe` (windows)
287    /// * `java` (linux)
288    fn get_java_executable_name() -> OsString {
289        let mut java_exe = OsString::from("java");
290        java_exe.push(env::consts::EXE_SUFFIX);
291        java_exe
292    }
293}
294impl Clone for JavaRuntime {
295    /// # Examples
296    ///
297    /// ```rust
298    /// use java_runtimes::JavaRuntime;
299    ///
300    /// let r1 = JavaRuntime::new("linux", "/jdk/bin/java".as_ref(), "21.0.3").unwrap();
301    /// let r2 = r1.clone();
302    ///
303    /// assert_eq!(r1, r2);
304    /// ```
305    fn clone(&self) -> Self {
306        Self {
307            os: self.os.clone(),
308            path: self.path.clone(),
309            version_string: self.version_string.clone(),
310        }
311    }
312    /// # Examples
313    ///
314    /// ```rust
315    /// use java_runtimes::JavaRuntime;
316    ///
317    /// let mut r1 = JavaRuntime::new("windows", "/jdk/bin/java".as_ref(), "21.0.3").unwrap();
318    /// let r2 = JavaRuntime::new("windows", r"D:\jdk\bin\java.exe".as_ref(), "21.0.3").unwrap();
319    ///
320    /// r1.clone_from(&r2);
321    /// assert_eq!(r1, r2);
322    /// ```
323    fn clone_from(&mut self, source: &Self) {
324        self.os = source.os.clone();
325        self.path = source.path.clone();
326        self.version_string = source.version_string.clone();
327    }
328}
329
330impl PartialEq for JavaRuntime {
331    /// # Examples
332    ///
333    /// ```rust
334    /// use java_runtimes::JavaRuntime;
335    ///
336    /// let r1 = JavaRuntime::new("linux", "/jdk/bin/java".as_ref(), "21.0.3").unwrap();
337    /// let r2 = JavaRuntime::new("linux", "/jdk/bin/java".as_ref(), "21.0.3").unwrap();
338    /// let r3 = JavaRuntime::new("windows", r"D:\jdk\bin\java.exe".as_ref(), "21.0.3").unwrap();
339    /// let r4 = JavaRuntime::new("windows", r"D:\jdk-17\bin\java.exe".as_ref(), "21.0.3").unwrap();
340    ///
341    /// assert_eq!(r1, r2);
342    /// assert_ne!(r1, r3);
343    /// assert_ne!(r2, r3);
344    /// assert_ne!(r2, r4);
345    /// assert_ne!(r3, r4);
346    /// ```
347    fn eq(&self, other: &Self) -> bool {
348        self.os == other.os && self.path == other.path
349    }
350
351    fn ne(&self, other: &Self) -> bool {
352        !self.eq(other)
353    }
354}