jvm_find/
lib.rs

1
2#![doc(html_root_url = "https://docs.rs/jvm-find/0.1.0")]
3
4use std::io::ErrorKind;
5use std::path::{Path, PathBuf};
6use std::process::Command;
7use std::ops::Deref;
8
9// TODO: apply #[doc(cfg(feature = "glob"))] to applicable features when stabilized (#43781)
10
11#[derive(Debug, thiserror::Error)]
12pub enum Error {
13	#[error("Error running Java executable on system path")]
14	JavaExecution(#[source] std::io::Error),
15
16	#[error("Error accessing JavaHome path")]
17	IoError(#[source] std::io::Error),
18
19	#[error("The JavaHome path (possibly obtained from JAVA_HOME environment variable) contained bad data. It could have been outdated, or pointing to something other than a directory. (bad path: {})", .0.display())]
20	BadJavaHomePath(PathBuf),
21
22	// Is this possible? (I guess technically yes, but do any JVM impls not?)
23	#[error("The installed java executable did not report a `java.home` property")]
24	NoJavaHomeProperty,
25
26	#[cfg(feature = "glob")]
27	#[error("Attempted to perform an operation with a non-utf8 path that does not support non-utf8 paths")]
28	PathNotUTF8(PathBuf),
29
30	#[cfg(feature = "glob")]
31	#[error("Unspecified error while globbing JAVA_HOME")]
32	GlobError(#[from] glob::GlobError),
33
34	#[cfg(feature = "glob")]
35	#[error("Unable to find native library file within JAVA_HOME")]
36	NoNativeLibrary,
37}
38
39type Result<T> = std::result::Result<T, Error>;
40
41/// The Windows native library that applications can link to - `jvm.dll`
42pub const NATIVE_LIBRARY_FILENAME_WIN: &str = "jvm.dll";
43/// The Linux shared library object that applications can link to - `libjvm.so`
44pub const NATIVE_LIBRARY_FILENAME_LIN: &str = "libjvm.so";
45/// The MacOS dynamic library that applications can link to - `libjli.dylib`
46///
47/// Note that MacOS consumers should link to `libjli.dylib` instead of `libjvm.dylib` due to a bug with the distribution.
48pub const NATIVE_LIBRARY_FILENAME_MAC: &str = "libjli.dylib";
49// (does the MacOS native library need to depend on the version of Java?)
50
51/// The specific native library filename, chosen for the built platform.
52#[cfg(target_os = "windows")]
53pub const NATIVE_LIBRARY_FILENAME: &str = NATIVE_LIBRARY_FILENAME_WIN;
54/// The specific native library filename, chosen for the built platform.
55#[cfg(any(
56	target_os = "freebsd",
57	target_os = "linux",
58	target_os = "netbsd",
59	target_os = "openbsd"
60))]
61pub const NATIVE_LIBRARY_FILENAME: &str = NATIVE_LIBRARY_FILENAME_LIN;
62/// The specific native library filename, chosen for the built platform.
63#[cfg(target_os = "macos")]
64pub const NATIVE_LIBRARY_FILENAME: &str = NATIVE_LIBRARY_FILENAME_MAC;
65
66/// A located Java home directory. Note that this type does not necessarily contain a valid one (in the case of a bad JAVA_HOME variable, custom-created one, etc) - as such, all usages of the contained path must be validated.
67#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
68pub struct JavaHome {
69	pub path: PathBuf,
70}
71impl JavaHome {
72	/// The `JAVA_HOME` environment variable name.
73	pub const ENV_VAR: &'static str = "JAVA_HOME";
74
75	/// Returns the existing JAVA_HOME environment variable if it is non-empty, or queries the active Java installation.
76	///
77	/// # Errors
78	/// This function will error in any situation that `JavaHome::find_active_home()` will - refer to that function for detailed error cases.
79	pub fn find_home() -> Result<Self> {
80		std::env::var_os(JavaHome::ENV_VAR)
81			.filter(|var| !var.is_empty())
82			.map(PathBuf::from)
83			.map(|path| Ok(JavaHome { path }))
84			.unwrap_or_else(JavaHome::find_active_home)
85	}
86
87	/// Checks that any existing JAVA_HOME environment variable points to a valid directory, and if not, it falls back to the currently active Java installation's home directory.
88	///
89	/// # Errors
90	/// This will error if the directory specified by JAVA_HOME is unreachable (due to permissions/broken links/etc errors). Also contains error conditions as specified by `JavaHome::find_active_home()`
91	pub fn find_valid_home() -> Result<Self> {
92		std::env::var_os(JavaHome::ENV_VAR)
93			.filter(|var| !var.is_empty())
94			.map(PathBuf::from)
95			.map(|path| {
96				match path.metadata() {
97					// bubble up IO (permission/etc) errors
98					Err(e) if e.kind() != ErrorKind::NotFound => Some(Err(Error::IoError(e))),
99
100					// return Some(pb) iff this is an existing directory
101					Ok(meta) if meta.is_dir() => Some(Ok(JavaHome { path })),
102
103					// It's not a directory? outdated env var? Query java executable
104					_ => None
105				}
106			})
107			.flatten().transpose()?.map(Ok)
108
109			// fallback to JavaHome::find_active_home
110			.unwrap_or_else(JavaHome::find_active_home)
111	}
112
113	/// Queries the first found `java` executable on the system path for its home directory.
114	///
115	/// # Errors
116	/// This function will error if there is an issue finding/running a `java` executable from the path, or if `java -XshowSettings:properties -version` does not return a `java.home` property.
117	pub fn find_active_home() -> Result<Self> {
118		// TODO: Query registry on windows?
119		// https://docs.oracle.com/javase/9/install/installation-jdk-and-jre-microsoft-windows-platforms.htm#JSJIG-GUID-C11500A9-252C-46FE-BB17-FC5A9528EAEB
120
121		let output = Command::new("java")
122			.arg("-XshowSettings:properties")
123			.arg("-version")
124			.output()
125			.map_err(|e| Error::JavaExecution(e))?;
126
127		let stdout = String::from_utf8_lossy(&output.stdout);
128		let stderr = String::from_utf8_lossy(&output.stderr);
129		let java_home = stdout.lines()
130			.chain(stderr.lines())
131			.filter(|line| line.contains("java.home"))
132			.find_map(|line| line.find('=').map(|i| line[i..].trim()));
133
134		match java_home {
135			Some(path) => Ok(JavaHome { path: PathBuf::from(path) }),
136			None => Err(Error::NoJavaHomeProperty),
137		}
138	}
139
140	// (will happily accept PRs for these)
141	
142	// All installations found on path (walk path and get the home dir for all java executables on it)
143	// (query registry if on Windows?)
144	//   HKLM/SOFTWARE/JavaSoft/Java Development Kit/(<= JDK 1.8)/JavaHome
145	//   HKLM/SOFTWARE/JavaSoft/JDK/(>= JDK 1.9)/JavaHome
146	//   HKLM/SOFTWARE/JavaSoft/Java Runtime Environment/(<= JDK 1.8)/JavaHome
147	//   HKLM/SOFTWARE/JavaSoft/JRE/(>= JDK 1.9)/JavaHome
148	// TODO: pub fn installations() -> Result<Vec<PathBuf>>
149
150	// jre home
151	// TODO: pub fn jre(&self) -> Result<PathBuf>
152	
153	// jdk home
154	// TODO: pub fn jdk(&self) -> Result<Option<PathBuf>>
155
156	// binary folder
157	// TODO: pub fn bin(&self) -> Result<PathBuf>
158
159	/// If the JDK is installed, returns a list of all the include folders necesssary to load JNI/etc headers.
160	#[cfg(feature = "glob")]
161	pub fn include(&self) -> Result<Option<Vec<PathBuf>>> {
162		let base = self.join("include");
163
164		match base.metadata() {
165			Err(e) if e.kind() == ErrorKind::NotFound => Ok(None),
166			Err(e) => Err(Error::IoError(e)),
167			Ok(meta) if meta.is_dir() => Ok(Some({
168				let escaped = glob::Pattern::escape(base.to_str().ok_or_else(|| Error::PathNotUTF8(base.clone()))?);
169				let pattern = escaped + "/**/*/";
170
171				std::iter::once(Ok(base.clone())) // include base dir
172					.chain(glob::glob(&pattern).unwrap()) // platform dependent dirs
173					.collect::<std::result::Result<Vec<PathBuf>, glob::GlobError>>()? // collect + bubble errors
174			})),
175			Ok(_) => Err(Error::BadJavaHomePath(self.path.clone())),
176		}
177	}
178
179	/// The path to the JVM's platform-specific native library, suitable for linking with. (`jvm.dll`/`libjvm.so`/`libjli.dylib`)
180	#[cfg(feature = "glob")]
181	pub fn native_library(&self) -> Result<PathBuf> {
182		let base = &self.path;
183		let escaped = glob::Pattern::escape(base.to_str().ok_or_else(|| Error::PathNotUTF8(base.clone()))?);
184		let pattern = escaped + "/**/" + NATIVE_LIBRARY_FILENAME;
185		Ok(glob::glob(&pattern)
186			.unwrap() // pattern should always be valid
187			.next().ok_or(Error::NoNativeLibrary)??)
188	}
189
190	/// A convience function to search for a specific file within the home directory. Matches the filename literally.
191	#[cfg(feature = "glob")]
192	pub fn find_file(&self, file: &str) -> Result<Option<PathBuf>> {
193		let base = &self.path;
194		let base_escaped = glob::Pattern::escape(base.to_str().ok_or_else(|| Error::PathNotUTF8(base.clone()))?);
195		let file_escaped = glob::Pattern::escape(file);
196		let pattern = base_escaped + "/**/" + &file_escaped;
197		glob::glob(&pattern)
198			.unwrap() // pattern should always be valid
199			.next()
200			.transpose()
201			.map_err(Error::GlobError)
202	}
203
204	/// A convience function to search for a specific folder within the home directory. Matches the folder name literally.
205	#[cfg(feature = "glob")]
206	pub fn find_folder(&self, folder: &str) -> Result<Option<PathBuf>> {
207		let base = &self.path;
208		let base_escaped = glob::Pattern::escape(base.to_str().ok_or_else(|| Error::PathNotUTF8(base.clone()))?);
209		let fold_escaped = glob::Pattern::escape(folder);
210		let pattern = base_escaped + "/**/" + &fold_escaped + "/";
211		glob::glob(&pattern)
212			.unwrap() // pattern should always be valid
213			.next()
214			.transpose()
215			.map_err(Error::GlobError)
216	}
217}
218impl Deref for JavaHome {
219	type Target = Path;
220	fn deref(&self) -> &Self::Target {
221		&self.path
222	}
223}