Skip to content

Commit 883d0a3

Browse files
committed
feature: Add initial WolframApp type, for querying data from a Wolfram product layout
This is useful when writing build scripts that use assets from Wolfram products, like header files, command-line tools, and shared libraries.
1 parent 0d4e5be commit 883d0a3

File tree

3 files changed

+344
-6
lines changed

3 files changed

+344
-6
lines changed

.vscode/settings.json

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"editor.formatOnSave": true
3+
"editor.formatOnSaveMode": "file"
4+
}

Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ edition = "2021"
66
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
77

88
[dependencies]
9+
cfg-if = "0.1.10"

src/lib.rs

+339-6
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,341 @@
1-
#[cfg(test)]
2-
mod tests {
3-
#[test]
4-
fn it_works() {
5-
let result = 2 + 2;
6-
assert_eq!(result, 4);
1+
use std::{fmt, path::PathBuf, process, str::FromStr};
2+
3+
use cfg_if::cfg_if;
4+
5+
const ENV_WSTP_COMPILER_ADDITIONS_DIR: &str = "WSTP_COMPILER_ADDITIONS";
6+
7+
// const ENV_WOLFRAM_LOCATION: &str = "RUST_WSTP_SYS_WOLFRAM_LOCATION";
8+
9+
//======================================
10+
// Types
11+
//======================================
12+
13+
pub struct WolframApp {
14+
location: PathBuf,
15+
}
16+
17+
#[non_exhaustive]
18+
pub struct WolframVersion {
19+
major: u32,
20+
minor: u32,
21+
patch: u32,
22+
}
23+
24+
#[derive(Debug)]
25+
pub struct Error(String);
26+
27+
//======================================
28+
// Functions
29+
//======================================
30+
31+
/// Returns the `$SystemID` value of the system this code was built for.
32+
///
33+
/// This does require access to a Wolfram Language evaluator.
34+
///
35+
// TODO: What exactly does this function mean if the user tries to cross-compile a
36+
// library?
37+
// TODO: Use env!("TARGET") here and just call system_id_from_target()?
38+
pub fn target_system_id() -> &'static str {
39+
cfg_if![
40+
if #[cfg(all(target_os = "macos", target_arch = "x86_64"))] {
41+
const SYSTEM_ID: &str = "MacOSX-x86-64";
42+
} else {
43+
// FIXME: Update this to include common Linux/Windows (and ARM macOS)
44+
// platforms.
45+
compile_error!("target_system_id() has not been implemented for the current system")
46+
}
47+
];
48+
49+
SYSTEM_ID
50+
}
51+
52+
/// Returns the System ID value that corresponds to the specified Rust
53+
/// [target triple](https://doc.rust-lang.org/nightly/rustc/platform-support.html), if
54+
/// any.
55+
pub fn system_id_from_target(rust_target: &str) -> Result<&'static str, Error> {
56+
let id = match rust_target {
57+
"x86_64-apple-darwin" => "MacOSX-x86-64",
58+
_ => {
59+
return Err(Error(format!(
60+
"no System ID value associated with Rust target triple: {}",
61+
rust_target
62+
)))
63+
}
64+
};
65+
66+
Ok(id)
67+
}
68+
69+
//======================================
70+
// Struct Impls
71+
//======================================
72+
73+
impl WolframVersion {
74+
/// First component of `$VersionNumber`.
75+
pub fn major(&self) -> u32 {
76+
self.major
77+
}
78+
79+
/// Second component of `$VersionNumber`.
80+
pub fn minor(&self) -> u32 {
81+
self.minor
82+
}
83+
84+
/// `$ReleaseNumber`
85+
pub fn patch(&self) -> u32 {
86+
self.patch
87+
}
88+
}
89+
90+
impl WolframApp {
91+
/// Evaluate `$InstallationDirectory` using `wolframscript` to get location of the
92+
/// developers Mathematica installation.
93+
///
94+
// TODO: Make this value settable using an environment variable; some people don't
95+
// have wolframscript on their `PATH`, or they may have multiple Mathematica
96+
// installations and will want to be able to exactly specify which one to use.
97+
// WOLFRAM_INSTALLATION_DIRECTORY.
98+
pub fn try_default() -> Result<Self, Error> {
99+
let location = wolframscript_output(
100+
&PathBuf::from("wolframscript"),
101+
&["-code".to_owned(), "$InstallationDirectory".to_owned()],
102+
)?;
103+
104+
WolframApp::from_path(PathBuf::from(location))
105+
}
106+
107+
// pub fn try_from_env() -> Result<Self, Error> {
108+
// todo!()
109+
// // fn get_wolfram_output(input: &str) -> String {
110+
// // let mut args = vec!["-code".to_owned(), input.to_owned()];
111+
112+
// // if let Some(product_location) = get_env_var(ENV_WOLFRAM_LOCATION) {
113+
// // let app = WolframApp::from_path(PathBuf::from(product_location))
114+
// // .expect("invalid Wolfram app location");
115+
116+
// // args.push("-local".to_owned());
117+
// // args.push(app.kernel_executable_path().unwrap().display().to_string());
118+
// // }
119+
// // }
120+
// }
121+
122+
pub fn from_path(location: PathBuf) -> Result<WolframApp, Error> {
123+
if !location.is_dir() {
124+
return Err(Error(format!(
125+
"invalid Wolfram app location: not a directory: {}",
126+
location.display()
127+
)));
128+
}
129+
130+
// FIXME: Implement at least some basic validation that this points to an
131+
// actual Wolfram app.
132+
Ok(WolframApp::unchecked_from_path(location))
133+
134+
// if cfg!(target_os = "macos") {
135+
// ... check for .app, application plist metadata, etc.
136+
// canonicalize between ".../Mathematica.app" and ".../Mathematica.app/Contents/"
137+
// }
138+
}
139+
140+
fn unchecked_from_path(location: PathBuf) -> WolframApp {
141+
WolframApp { location }
142+
}
143+
144+
// Properties
145+
146+
pub fn location(&self) -> &PathBuf {
147+
&self.location
148+
}
149+
150+
/// Returns a Wolfram Language version number.
151+
pub fn wolfram_version(&self) -> Result<WolframVersion, Error> {
152+
// MAJOR.MINOR
153+
let major_minor = self
154+
.wolframscript_output("$VersionNumber")?
155+
.split(".")
156+
.map(ToString::to_string)
157+
.collect::<Vec<String>>();
158+
159+
let [major, mut minor]: [String; 2] = match <[String; 2]>::try_from(major_minor) {
160+
Ok(pair @ [_, _]) => pair,
161+
Err(_) => panic!("$VersionNumber has unexpected number of components"),
162+
};
163+
// This can happen in major versions, when $VersionNumber formats as e.g. "13."
164+
if minor == "" {
165+
minor = String::from("0");
166+
}
167+
168+
// PATCH
169+
let patch = self.wolframscript_output("$ReleaseNumber")?;
170+
171+
let major = u32::from_str(&major).expect("unexpected $VersionNumber format");
172+
let minor = u32::from_str(&minor).expect("unexpected $VersionNumber format");
173+
let patch = u32::from_str(&patch).expect("unexpected $ReleaseNumber format");
174+
175+
Ok(WolframVersion {
176+
major,
177+
minor,
178+
patch,
179+
})
180+
}
181+
182+
//----------------------------------
183+
// Files
184+
//----------------------------------
185+
186+
pub fn kernel_executable_path(&self) -> Result<PathBuf, Error> {
187+
let path = if cfg!(target_os = "macos") {
188+
// TODO: In older versions of the product, MacOSX was used instead of MacOS.
189+
// Look for either, depending on the version number.
190+
self.location.join("MacOS").join("WolframKernel")
191+
} else {
192+
return Err(platform_unsupported_error());
193+
};
194+
195+
if !path.is_file() {
196+
return Err(Error(format!(
197+
"WolframKernel executable does not exist in the expected location: {}",
198+
path.display()
199+
)));
200+
}
201+
202+
Ok(path)
203+
}
204+
205+
// TODO: Make this public?
206+
fn wolframscript_executable_path(&self) -> Result<PathBuf, Error> {
207+
// FIXME: This will use whatever `wolframscript` program is on the users
208+
// environment PATH. Look up the actual wolframscript executable in this
209+
// product.
210+
Ok(PathBuf::from("wolframscript"))
211+
}
212+
213+
// TODO: Make this private, as this isn't really a "leaf" asset.
214+
pub fn wstp_compiler_additions_path(&self) -> Result<PathBuf, Error> {
215+
if let Some(path) = get_env_var(ENV_WSTP_COMPILER_ADDITIONS_DIR) {
216+
// // Force a rebuild if the path has changed. This happens when developing WSTP.
217+
// println!("cargo:rerun-if-changed={}", path.display());
218+
return Ok(PathBuf::from(path));
219+
}
220+
221+
let path = if cfg!(target_os = "macos") {
222+
self.location()
223+
.join("SystemFiles/Links/WSTP/DeveloperKit/")
224+
.join(target_system_id())
225+
.join("CompilerAdditions")
226+
} else {
227+
return Err(platform_unsupported_error());
228+
};
229+
230+
if !path.is_dir() {
231+
return Err(Error(format!(
232+
"WSTP CompilerAdditions directory does not exist in the expected location: {}",
233+
path.display()
234+
)));
235+
}
236+
237+
Ok(path)
238+
}
239+
240+
pub fn wstp_static_library_path(&self) -> Result<PathBuf, Error> {
241+
let static_archive_name = if cfg!(target_os = "macos") {
242+
"libWSTPi4.a"
243+
} else {
244+
return Err(platform_unsupported_error());
245+
};
246+
247+
let lib = self
248+
.wstp_compiler_additions_path()?
249+
.join(static_archive_name);
250+
251+
Ok(lib)
252+
}
253+
254+
//----------------------------------
255+
// Utilities
256+
//----------------------------------
257+
258+
fn wolframscript_output(&self, input: &str) -> Result<String, Error> {
259+
let mut args = vec!["-code".to_owned(), input.to_owned()];
260+
261+
args.push("-local".to_owned());
262+
args.push(self.kernel_executable_path().unwrap().display().to_string());
263+
264+
wolframscript_output(&self.wolframscript_executable_path()?, &args)
265+
}
266+
}
267+
268+
//----------------------------------
269+
// Utilities
270+
//----------------------------------
271+
272+
fn platform_unsupported_error() -> Error {
273+
Error(format!(
274+
"operation is not yet implemented for this platform"
275+
))
276+
}
277+
278+
fn get_env_var(var: &'static str) -> Option<String> {
279+
// TODO: Add cargo feature to enable these print statements, so that
280+
// wolfram-app-discovery works better when used in build.rs scripts.
281+
// println!("cargo:rerun-if-env-changed={}", var);
282+
match std::env::var(var) {
283+
Ok(string) => Some(string),
284+
Err(std::env::VarError::NotPresent) => None,
285+
Err(std::env::VarError::NotUnicode(err)) => {
286+
panic!("value of env var '{}' is not valid unicode: {:?}", var, err)
287+
}
288+
}
289+
}
290+
291+
fn wolframscript_output(wolframscript_command: &PathBuf, args: &[String]) -> Result<String, Error> {
292+
let output: process::Output = process::Command::new(wolframscript_command)
293+
.args(args)
294+
.output()
295+
.expect("unable to execute wolframscript command");
296+
297+
// NOTE: The purpose of the 2nd clause here checking for exit code 3 is to work around
298+
// a mis-feature of wolframscript to return the same exit code as the Kernel.
299+
// TODO: Fix the bug in wolframscript which makes this necessary and remove the check
300+
// for `3`.
301+
if !output.status.success() && output.status.code() != Some(3) {
302+
panic!(
303+
"wolframscript exited with non-success status code: {}",
304+
output.status
305+
);
306+
}
307+
308+
let stdout = match String::from_utf8(output.stdout.clone()) {
309+
Ok(s) => s,
310+
Err(err) => {
311+
panic!(
312+
"wolframscript output is not valid UTF-8: {}: {}",
313+
err,
314+
String::from_utf8_lossy(&output.stdout)
315+
);
316+
}
317+
};
318+
319+
let first_line = stdout
320+
.lines()
321+
.next()
322+
.expect("wolframscript output was empty");
323+
324+
Ok(first_line.to_owned())
325+
}
326+
327+
//======================================
328+
// Formatting Impls
329+
//======================================
330+
331+
impl fmt::Display for WolframVersion {
332+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
333+
let WolframVersion {
334+
major,
335+
minor,
336+
patch,
337+
} = *self;
338+
339+
write!(f, "{}.{}.{}", major, minor, patch)
7340
}
8341
}

0 commit comments

Comments
 (0)