java_locator/
lib.rs

1// Copyright 2019 astonbitecode
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15/*!
16
17# java-locator
18
19This is a small utility written in [Rust](https://www.rust-lang.org/).
20
21It locates the active Java installation in the host.
22
23## Usage
24
25The utility can be used as a library, or as an executable:
26
27### Library
28
29```rust
30extern crate java_locator;
31
32fn main() -> java_locator::errors::Result<()> {
33    let java_home = java_locator::locate_java_home()?;
34    let dyn_lib_path = java_locator::locate_jvm_dyn_library()?;
35    let libjsig  = java_locator::locate_file("libjsig.so")?;
36
37    println!("The java home is {}", java_home);
38    println!("The jvm dynamic library path is {}", dyn_lib_path);
39    println!("The file libjsig.so is located in {}", libjsig);
40
41    Ok(())
42}
43```
44
45### Executable
46
47Having rust [installed](https://www.rust-lang.org/tools/install), you may install the utility using cargo:
48
49`cargo install java-locator --features build-binary`
50
51And then, issuing
52
53`java-locator`
54
55you should have an output like:
56
57> /usr/lib/jvm/java-11-openjdk-amd64
58
59You may retrieve the location of the `jvm` shared library:
60
61`java-locator --jvmlib`
62
63should give an output like:
64
65> /usr/lib/jvm/java-11-openjdk-amd64/lib/server
66
67This may be used in cases when the `LD_LIBRARY_PATH` (or `PATH` in windows) should be populated.
68
69You may also retrieve the location of any file inside the Java installation:
70
71`java-locator --file libjsig.so`
72
73and you can even use wildcards:
74
75`java-locator --file libjsig*`
76
77The latter two commands should return something like:
78
79> /usr/lib/jvm/java-11-openjdk-amd64/lib
80
81## Available Features
82
83* `build-binary`: Generates a `java-locator` executable
84* `locate-jdk-only`: Instructs `java-locator` to locate __only JDKs__.
85
86    In a system that has only JREs installed, `java-locator` will not find any Java installation if this feature is enabled.
87
88    This feature also solves issues when using JDK 8:  In usual installations, the symlinks of the `java` executable in the `$PATH`
89    lead to the `jre` directory that lies inside the JDK 8. When `$JAVA_HOME` is not defined in the system, `java-locator` attempts to locate the
90    Java installation following the symlinks of the `java` executable. Having done that, it cannot locate development artifacts like `jni.h` headers,
91    `javac` etc. With this feature enabled though, `java-locator` will locate development artifacts normally.
92
93## License
94
95At your option, under:
96
97* Apache License, Version 2.0, (<http://www.apache.org/licenses/LICENSE-2.0>)
98* MIT license (<http://opensource.org/licenses/MIT>)
99
100 */
101
102use std::env;
103use std::path::PathBuf;
104use std::process::Command;
105
106use errors::{JavaLocatorError, Result};
107use glob::{glob, Pattern};
108
109pub mod errors;
110
111#[cfg(not(feature = "locate-jdk-only"))]
112const LOCATE_BINARY: &str = "java";
113#[cfg(feature = "locate-jdk-only")]
114const LOCATE_BINARY: &str = "javac";
115
116/// Returns the name of the jvm dynamic library:
117///
118/// * libjvm.so for Linux
119///
120/// * libjvm.dlyb for Macos
121///
122/// * jvm.dll for Windows
123pub fn get_jvm_dyn_lib_file_name() -> &'static str {
124    if cfg!(target_os = "windows") {
125        "jvm.dll"
126    } else if cfg!(target_os = "macos") {
127        "libjvm.dylib"
128    } else {
129        "libjvm.so"
130    }
131}
132
133/// Returns the Java home path.
134///
135/// If `JAVA_HOME` env var is defined, the function returns it without any checks whether the var points to a valid directory or not.
136///
137/// If `JAVA_HOME` is not defined, the function tries to locate it using the `java` executable.
138pub fn locate_java_home() -> Result<String> {
139    match &env::var("JAVA_HOME") {
140        Ok(s) if s.is_empty() => do_locate_java_home(),
141        Ok(java_home_env_var) => Ok(java_home_env_var.clone()),
142        Err(_) => do_locate_java_home(),
143    }
144}
145
146#[cfg(target_os = "windows")]
147fn do_locate_java_home() -> Result<String> {
148    let output = Command::new("where")
149        .arg(LOCATE_BINARY)
150        .output()
151        .map_err(|e| JavaLocatorError::new(format!("Failed to run command `where` ({e})")))?;
152
153    let java_exec_path_raw = std::str::from_utf8(&output.stdout)?;
154    java_exec_path_validation(java_exec_path_raw)?;
155
156    // Windows will return multiple lines if there are multiple `java` in the PATH.
157    let paths_found = java_exec_path_raw.lines().count();
158    if paths_found > 1 {
159        eprintln!("WARNING: java_locator found {paths_found} possible java locations. Using the first one. To silence this warning set JAVA_HOME env var.")
160    }
161
162    let java_exec_path = java_exec_path_raw
163        .lines()
164        // The first line is the one that would be run, so take just that line.
165        .next()
166        .expect("gauranteed to have at least one line by java_exec_path_validation")
167        .trim();
168
169    let mut home_path = follow_symlinks(java_exec_path);
170
171    home_path.pop();
172    home_path.pop();
173
174    home_path
175        .into_os_string()
176        .into_string()
177        .map_err(|path| JavaLocatorError::new(format!("Java path {path:?} is invalid utf8")))
178}
179
180#[cfg(target_os = "macos")]
181fn do_locate_java_home() -> Result<String> {
182    let output = Command::new("/usr/libexec/java_home")
183        .output()
184        .map_err(|e| {
185            JavaLocatorError::new(format!(
186                "Failed to run command `/usr/libexec/java_home` ({e})"
187            ))
188        })?;
189
190    let java_exec_path = std::str::from_utf8(&output.stdout)?.trim();
191
192    java_exec_path_validation(java_exec_path)?;
193    let home_path = follow_symlinks(java_exec_path);
194
195    home_path
196        .into_os_string()
197        .into_string()
198        .map_err(|path| JavaLocatorError::new(format!("Java path {path:?} is invalid utf8")))
199}
200
201#[cfg(not(any(target_os = "windows", target_os = "macos")))] // Unix
202fn do_locate_java_home() -> Result<String> {
203    let output = Command::new("which")
204        .arg(LOCATE_BINARY)
205        .output()
206        .map_err(|e| JavaLocatorError::new(format!("Failed to run command `which` ({e})")))?;
207    let java_exec_path = std::str::from_utf8(&output.stdout)?.trim();
208
209    java_exec_path_validation(java_exec_path)?;
210    let mut home_path = follow_symlinks(java_exec_path);
211
212    // Here we should have found ourselves in a directory like /usr/lib/jvm/java-8-oracle/jre/bin/java
213    home_path.pop();
214    home_path.pop();
215
216    home_path
217        .into_os_string()
218        .into_string()
219        .map_err(|path| JavaLocatorError::new(format!("Java path {path:?} is invalid utf8")))
220}
221
222fn java_exec_path_validation(path: &str) -> Result<()> {
223    if path.is_empty() {
224        return Err(JavaLocatorError::new(
225            "Java is not installed or not in the system PATH".into(),
226        ));
227    }
228
229    Ok(())
230}
231
232fn follow_symlinks(path: &str) -> PathBuf {
233    let mut test_path = PathBuf::from(path);
234    while let Ok(path) = test_path.read_link() {
235        test_path = if path.is_absolute() {
236            path
237        } else {
238            test_path.pop();
239            test_path.push(path);
240            test_path
241        };
242    }
243    test_path
244}
245
246/// Returns the path that contains the `libjvm.so` (or `jvm.dll` in windows).
247pub fn locate_jvm_dyn_library() -> Result<String> {
248    if cfg!(target_os = "windows") {
249        locate_file("jvm.dll")
250    } else {
251        locate_file("libjvm.*")
252    }
253}
254
255/// Returns the path that contains the file with the provided name.
256///
257/// This function argument can be a wildcard.
258pub fn locate_file(file_name: &str) -> Result<String> {
259    // Find the JAVA_HOME
260    let java_home = locate_java_home()?;
261
262    let query = format!("{}/**/{}", Pattern::escape(&java_home), file_name);
263
264    let path = glob(&query)?.filter_map(|x| x.ok()).next().ok_or_else(|| {
265        JavaLocatorError::new(format!(
266            "Could not find the {file_name} library in any subdirectory of {java_home}",
267        ))
268    })?;
269
270    let parent_path = path.parent().unwrap();
271    match parent_path.to_str() {
272        Some(parent_path) => Ok(parent_path.to_owned()),
273        None => Err(JavaLocatorError::new(format!(
274            "Java path {parent_path:?} is invalid utf8"
275        ))),
276    }
277}
278
279#[cfg(test)]
280mod unit_tests {
281    use super::*;
282
283    #[test]
284    fn locate_java_home_test() {
285        println!("locate_java_home: {}", locate_java_home().unwrap());
286        println!(
287            "locate_jvm_dyn_library: {}",
288            locate_jvm_dyn_library().unwrap()
289        );
290    }
291
292    #[test]
293    fn locate_java_from_exec_test() {
294        println!("do_locate_java_home: {}", do_locate_java_home().unwrap());
295    }
296
297    #[test]
298    fn jni_headers_test() {
299        let java_home = do_locate_java_home().unwrap();
300        assert!(PathBuf::from(java_home)
301            .join("include")
302            .join("jni.h")
303            .exists());
304    }
305}