wolfram_app_discovery/lib.rs
1//! Find local installations of the [Wolfram Language](https://www.wolfram.com/language/)
2//! and Wolfram applications.
3//!
4//! This crate provides functionality to find and query information about Wolfram Language
5//! applications installed on the current computer.
6//!
7//! # Use cases
8//!
9//! * Programs that depend on the Wolfram Language, and want to automatically use the
10//! newest version available locally.
11//!
12//! * Build scripts that need to locate the Wolfram LibraryLink or WSTP header files and
13//! static/dynamic library assets.
14//!
15//! - The [wstp] and [wolfram-library-link] crate build scripts are examples of Rust
16//! libraries that do this.
17//!
18//! * A program used on different computers that will automatically locate the Wolfram Language,
19//! even if it resides in a different location on each computer.
20//!
21//! [wstp]: https://crates.io/crates/wstp
22//! [wolfram-library-link]: https://crates.io/crates/wolfram-library-link
23//!
24//! # Examples
25//!
26//! ###### Find the default Wolfram Language installation on this computer
27//!
28//! ```
29//! use wolfram_app_discovery::WolframApp;
30//!
31//! let app = WolframApp::try_default()
32//! .expect("unable to locate any Wolfram apps");
33//!
34//! println!("App location: {:?}", app.app_directory());
35//! println!("Wolfram Language version: {}", app.wolfram_version().unwrap());
36//! ```
37//!
38//! ###### Find a local Wolfram Engine installation
39//!
40//! ```
41//! use wolfram_app_discovery::{discover, WolframApp, WolframAppType};
42//!
43//! let engine: WolframApp = discover()
44//! .into_iter()
45//! .filter(|app: &WolframApp| app.app_type() == WolframAppType::Engine)
46//! .next()
47//! .unwrap();
48//! ```
49
50#![warn(missing_docs)]
51
52
53pub mod build_scripts;
54pub mod config;
55
56mod os;
57
58#[cfg(test)]
59mod tests;
60
61#[doc(hidden)]
62mod test_readme {
63 // Ensure that doc tests in the README.md file get run.
64 #![doc = include_str!("../README.md")]
65}
66
67
68use std::{
69 cmp::Ordering,
70 fmt::{self, Display},
71 path::PathBuf,
72 process,
73 str::FromStr,
74};
75
76use log::info;
77
78#[allow(deprecated)]
79use config::env_vars::{RUST_WOLFRAM_LOCATION, WOLFRAM_APP_DIRECTORY};
80
81use crate::os::OperatingSystem;
82
83//======================================
84// Types
85//======================================
86
87/// A local installation of the Wolfram System.
88///
89/// See the [wolfram-app-discovery](crate) crate documentation for usage examples.
90#[rustfmt::skip]
91#[derive(Debug, Clone)]
92pub struct WolframApp {
93 //-----------------------
94 // Application properties
95 //-----------------------
96 #[allow(dead_code)]
97 app_name: String,
98 app_type: WolframAppType,
99 app_version: AppVersion,
100
101 app_directory: PathBuf,
102
103 app_executable: Option<PathBuf>,
104
105 // If this is a Wolfram Engine application, then it contains an embedded Wolfram
106 // Player application that actually contains the WL system content.
107 embedded_player: Option<Box<WolframApp>>,
108}
109
110/// Standalone application type distributed by Wolfram Research.
111#[derive(Debug, Clone, PartialEq, Hash)]
112#[non_exhaustive]
113#[cfg_attr(feature = "cli", derive(clap::ValueEnum))]
114pub enum WolframAppType {
115 /// Unified Wolfram App
116 WolframApp,
117 /// [Wolfram Mathematica](https://www.wolfram.com/mathematica/)
118 Mathematica,
119 /// [Wolfram Engine](https://wolfram.com/engine)
120 Engine,
121 /// [Wolfram Desktop](https://www.wolfram.com/desktop/)
122 Desktop,
123 /// [Wolfram Player](https://www.wolfram.com/player/)
124 Player,
125 /// [Wolfram Player Pro](https://www.wolfram.com/player-pro/)
126 #[doc(hidden)]
127 PlayerPro,
128 /// [Wolfram Finance Platform](https://www.wolfram.com/finance-platform/)
129 FinancePlatform,
130 /// [Wolfram Programming Lab](https://www.wolfram.com/programming-lab/)
131 ProgrammingLab,
132 /// [Wolfram|Alpha Notebook Edition](https://www.wolfram.com/wolfram-alpha-notebook-edition/)
133 WolframAlphaNotebookEdition,
134 // NOTE: When adding a new variant here, be sure to update WolframAppType::variants().
135}
136
137/// Possible values of [`$SystemID`][$SystemID].
138///
139/// [$SystemID]: https://reference.wolfram.com/language/ref/$SystemID
140#[allow(non_camel_case_types, missing_docs)]
141#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
142#[non_exhaustive]
143pub enum SystemID {
144 /// `"MacOSX-x86-64"`
145 MacOSX_x86_64,
146 /// `"MacOSX-ARM64"`
147 MacOSX_ARM64,
148 /// `"Windows-x86-64"`
149 Windows_x86_64,
150 /// `"Linux-x86-64"`
151 Linux_x86_64,
152 /// `"Linux-ARM64"`
153 Linux_ARM64,
154 /// `"Linux-ARM"`
155 ///
156 /// E.g. Raspberry Pi
157 Linux_ARM,
158 /// `"iOS-ARM64"`
159 iOS_ARM64,
160 /// `"Android"`
161 Android,
162
163 /// `"Windows"`
164 ///
165 /// Legacy Windows 32-bit x86
166 Windows,
167 /// `"Linux"`
168 ///
169 /// Legacy Linux 32-bit x86
170 Linux,
171}
172
173/// Wolfram application version number.
174///
175/// The major, minor, and revision components of most Wolfram applications will
176/// be the same as version of the Wolfram Language they provide.
177#[derive(Debug, Clone)]
178pub struct AppVersion {
179 major: u32,
180 minor: u32,
181 revision: u32,
182 minor_revision: Option<u32>,
183
184 build_code: Option<u32>,
185}
186
187/// Wolfram Language version number.
188#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
189#[non_exhaustive]
190pub struct WolframVersion {
191 major: u32,
192 minor: u32,
193 patch: u32,
194}
195
196/// A local copy of the WSTP developer kit for a particular [`SystemID`].
197#[derive(Debug, Clone)]
198pub struct WstpSdk {
199 system_id: SystemID,
200 /// E.g. `$InstallationDirectory/SystemFiles/Links/WSTP/DeveloperKit/MacOSX-x86-64/`
201 sdk_dir: PathBuf,
202 compiler_additions: PathBuf,
203
204 wstp_h: PathBuf,
205 wstp_static_library: PathBuf,
206}
207
208#[doc(hidden)]
209pub struct Filter {
210 pub app_types: Option<Vec<WolframAppType>>,
211}
212
213/// Wolfram app discovery error.
214#[derive(Debug, Clone)]
215#[cfg_attr(test, derive(PartialEq))]
216pub struct Error(ErrorKind);
217
218#[derive(Debug, Clone)]
219#[cfg_attr(test, derive(PartialEq))]
220pub(crate) enum ErrorKind {
221 Undiscoverable {
222 /// The thing that could not be located.
223 resource: String,
224 /// Environment variable that could be set to make this property
225 /// discoverable.
226 environment_variable: Option<&'static str>,
227 },
228 /// The file system layout of the Wolfram installation did not have the
229 /// expected structure, and a file or directory did not appear at the
230 /// expected location.
231 UnexpectedAppLayout {
232 resource_name: &'static str,
233 app_installation_dir: PathBuf,
234 /// Path within `app_installation_dir` that was expected to exist, but
235 /// does not.
236 path: PathBuf,
237 },
238 UnexpectedLayout {
239 resource_name: &'static str,
240 dir: PathBuf,
241 path: PathBuf,
242 },
243 /// The non-app directory specified by the configuration environment
244 /// variable `env_var` does not contain a file at the expected location.
245 UnexpectedEnvironmentValueLayout {
246 resource_name: &'static str,
247 env_var: &'static str,
248 env_value: PathBuf,
249 /// Path within `env_value` that was expected to exist, but does not.
250 derived_path: PathBuf,
251 },
252 /// The app manually specified by an environment variable does not match the
253 /// filter the app is expected to satisfy.
254 SpecifiedAppDoesNotMatchFilter {
255 environment_variable: &'static str,
256 filter_err: FilterError,
257 },
258 UnsupportedPlatform {
259 operation: String,
260 target_os: OperatingSystem,
261 },
262 IO(String),
263 Other(String),
264}
265
266#[derive(Debug, Clone)]
267#[cfg_attr(test, derive(PartialEq))]
268pub(crate) enum FilterError {
269 FilterDoesNotMatchAppType {
270 app_type: WolframAppType,
271 allowed: Vec<WolframAppType>,
272 },
273}
274
275impl Error {
276 pub(crate) fn other(message: String) -> Self {
277 let err = Error(ErrorKind::Other(message));
278 info!("discovery error: {err}");
279 err
280 }
281
282 pub(crate) fn undiscoverable(
283 resource: String,
284 environment_variable: Option<&'static str>,
285 ) -> Self {
286 let err = Error(ErrorKind::Undiscoverable {
287 resource,
288 environment_variable,
289 });
290 info!("discovery error: {err}");
291 err
292 }
293
294 pub(crate) fn unexpected_app_layout(
295 resource_name: &'static str,
296 app: &WolframApp,
297 path: PathBuf,
298 ) -> Self {
299 let err = Error(ErrorKind::UnexpectedAppLayout {
300 resource_name,
301 app_installation_dir: app.installation_directory(),
302 path,
303 });
304 info!("discovery error: {err}");
305 err
306 }
307
308 pub(crate) fn unexpected_layout(
309 resource_name: &'static str,
310 dir: PathBuf,
311 path: PathBuf,
312 ) -> Self {
313 let err = Error(ErrorKind::UnexpectedLayout {
314 resource_name,
315 dir,
316 path,
317 });
318 info!("discovery error: {err}");
319 err
320 }
321
322 /// Alternative to [`Error::unexpected_app_layout()`], used when a valid
323 /// [`WolframApp`] hasn't even been constructed yet.
324 #[allow(dead_code)]
325 pub(crate) fn unexpected_app_layout_2(
326 resource_name: &'static str,
327 app_installation_dir: PathBuf,
328 path: PathBuf,
329 ) -> Self {
330 let err = Error(ErrorKind::UnexpectedAppLayout {
331 resource_name,
332 app_installation_dir,
333 path,
334 });
335 info!("discovery error: {err}");
336 err
337 }
338
339 pub(crate) fn unexpected_env_layout(
340 resource_name: &'static str,
341 env_var: &'static str,
342 env_value: PathBuf,
343 derived_path: PathBuf,
344 ) -> Self {
345 let err = Error(ErrorKind::UnexpectedEnvironmentValueLayout {
346 resource_name,
347 env_var,
348 env_value,
349 derived_path,
350 });
351 info!("discovery error: {err}");
352 err
353 }
354
355 pub(crate) fn platform_unsupported(name: &str) -> Self {
356 let err = Error(ErrorKind::UnsupportedPlatform {
357 operation: name.to_owned(),
358 target_os: OperatingSystem::target_os(),
359 });
360 info!("discovery error: {err}");
361 err
362 }
363
364 pub(crate) fn app_does_not_match_filter(
365 environment_variable: &'static str,
366 filter_err: FilterError,
367 ) -> Self {
368 let err = Error(ErrorKind::SpecifiedAppDoesNotMatchFilter {
369 environment_variable,
370 filter_err,
371 });
372 info!("discovery error: {err}");
373 err
374 }
375}
376
377impl std::error::Error for Error {}
378
379//======================================
380// Functions
381//======================================
382
383/// Discover all installed Wolfram applications.
384///
385/// The [`WolframApp`] elements in the returned vector will be sorted by Wolfram
386/// Language version and application feature set. The newest and most general app
387/// will be at the start of the list.
388///
389/// # Caveats
390///
391/// This function will use operating-system specific logic to discover installations of
392/// Wolfram applications. If a Wolfram application is installed to a non-standard
393/// location, it may not be discoverable by this function.
394pub fn discover() -> Vec<WolframApp> {
395 let mut apps = os::discover_all();
396
397 // Sort `apps` so that the "best" app is the last element in the vector.
398 apps.sort_by(WolframApp::best_order);
399
400 // Reverse `apps`, so that the best come first.
401 apps.reverse();
402
403 apps
404}
405
406/// Discover all installed Wolfram applications that match the specified filtering
407/// parameters.
408///
409/// # Caveats
410///
411/// This function will use operating-system specific logic to discover installations of
412/// Wolfram applications. If a Wolfram application is installed to a non-standard
413/// location, it may not be discoverable by this function.
414pub fn discover_with_filter(filter: &Filter) -> Vec<WolframApp> {
415 let mut apps = discover();
416
417 apps.retain(|app| filter.check_app(&app).is_ok());
418
419 apps
420}
421
422/// Returns the [`$SystemID`][ref/$SystemID] value of the system this code was built for.
423///
424/// This does require access to a Wolfram Language evaluator.
425///
426/// [ref/$SystemID]: https://reference.wolfram.com/language/ref/$SystemID.html
427// TODO: What exactly does this function mean if the user tries to cross-compile a
428// library?
429#[deprecated(note = "use `SystemID::current_rust_target()` instead")]
430pub fn target_system_id() -> &'static str {
431 SystemID::current_rust_target().as_str()
432}
433
434/// Returns the System ID value that corresponds to the specified Rust
435/// [target triple](https://doc.rust-lang.org/nightly/rustc/platform-support.html), if
436/// any.
437#[deprecated(note = "use `SystemID::try_from_rust_target()` instead")]
438pub fn system_id_from_target(rust_target: &str) -> Result<&'static str, Error> {
439 SystemID::try_from_rust_target(rust_target).map(|id| id.as_str())
440}
441
442//======================================
443// Struct Impls
444//======================================
445
446impl WolframAppType {
447 /// Enumerate all `WolframAppType` variants.
448 pub fn variants() -> Vec<WolframAppType> {
449 use WolframAppType::*;
450
451 vec![
452 WolframApp,
453 Mathematica,
454 Desktop,
455 Engine,
456 Player,
457 PlayerPro,
458 FinancePlatform,
459 ProgrammingLab,
460 WolframAlphaNotebookEdition,
461 ]
462 }
463
464 /// The 'usefulness' value of a Wolfram application type, all else being equal.
465 ///
466 /// This is a rough, arbitrary indicator of how general and flexible the Wolfram
467 /// Language capabilites offered by a particular application type are.
468 ///
469 /// This relative ordering is not necessarily best for all use cases. For example,
470 /// it will rank a Wolfram Engine installation above Wolfram Player, but e.g. an
471 /// application that needs a notebook front end may actually prefer Player over
472 /// Wolfram Engine.
473 //
474 // TODO: Break this up into separately orderable properties, e.g. `has_front_end()`,
475 // `is_restricted()`.
476 #[rustfmt::skip]
477 fn ordering_value(&self) -> u32 {
478 use WolframAppType::*;
479
480 match self {
481 // Unrestricted | with a front end
482 WolframApp => 110,
483 Desktop => 100,
484 Mathematica => 99,
485 FinancePlatform => 98,
486 ProgrammingLab => 97,
487
488 // Unrestricted | without a front end
489 Engine => 96,
490
491 // Restricted | with a front end
492 PlayerPro => 95,
493 Player => 94,
494 WolframAlphaNotebookEdition => 93,
495
496 // Restricted | without a front end
497 // TODO?
498 }
499 }
500
501 // TODO(cleanup): Make this method unnecessary. This is a synthesized thing,
502 // not necessarily meaningful. Remove WolframApp.app_name?
503 #[allow(dead_code)]
504 fn app_name(&self) -> &'static str {
505 match self {
506 WolframAppType::WolframApp => "Wolfram",
507 WolframAppType::Mathematica => "Mathematica",
508 WolframAppType::Engine => "Wolfram Engine",
509 WolframAppType::Desktop => "Wolfram Desktop",
510 WolframAppType::Player => "Wolfram Player",
511 WolframAppType::PlayerPro => "Wolfram Player Pro",
512 WolframAppType::FinancePlatform => "Wolfram Finance Platform",
513 WolframAppType::ProgrammingLab => "Wolfram Programming Lab",
514 WolframAppType::WolframAlphaNotebookEdition => {
515 "Wolfram|Alpha Notebook Edition"
516 },
517 }
518 }
519}
520
521impl FromStr for SystemID {
522 type Err = ();
523
524 fn from_str(string: &str) -> Result<Self, Self::Err> {
525 let value = match string {
526 "MacOSX-x86-64" => SystemID::MacOSX_x86_64,
527 "MacOSX-ARM64" => SystemID::MacOSX_ARM64,
528 "Windows-x86-64" => SystemID::Windows_x86_64,
529 "Linux-x86-64" => SystemID::Linux_x86_64,
530 "Linux-ARM64" => SystemID::Linux_ARM64,
531 "Linux-ARM" => SystemID::Linux_ARM,
532 "iOS-ARM64" => SystemID::iOS_ARM64,
533 "Android" => SystemID::Android,
534 "Windows" => SystemID::Windows,
535 "Linux" => SystemID::Linux,
536 _ => return Err(()),
537 };
538
539 Ok(value)
540 }
541}
542
543impl SystemID {
544 /// [`$SystemID`][$SystemID] string value of this [`SystemID`].
545 ///
546 /// [$SystemID]: https://reference.wolfram.com/language/ref/$SystemID
547 pub const fn as_str(self) -> &'static str {
548 match self {
549 SystemID::MacOSX_x86_64 => "MacOSX-x86-64",
550 SystemID::MacOSX_ARM64 => "MacOSX-ARM64",
551 SystemID::Windows_x86_64 => "Windows-x86-64",
552 SystemID::Linux_x86_64 => "Linux-x86-64",
553 SystemID::Linux_ARM64 => "Linux-ARM64",
554 SystemID::Linux_ARM => "Linux-ARM",
555 SystemID::iOS_ARM64 => "iOS-ARM64",
556 SystemID::Android => "Android",
557 SystemID::Windows => "Windows",
558 SystemID::Linux => "Linux",
559 }
560 }
561
562 /// Returns the [`$SystemID`][$SystemID] value associated with the Rust
563 /// target this code is being compiled for.
564 ///
565 /// [$SystemID]: https://reference.wolfram.com/language/ref/$SystemID
566 ///
567 /// # Host vs. Target in `build.rs`
568 ///
569 /// **Within a build.rs script**, if the current build is a
570 /// cross-compilation, this function will return the system ID of the
571 /// _host_ that the build script was compiled for, and not the _target_
572 /// system ID that the current Rust project is being compiled for.
573 ///
574 /// To get the target system ID of the main build, use:
575 ///
576 /// ```
577 /// use wolfram_app_discovery::SystemID;
578 ///
579 /// // Read the target from the _runtime_ environment of the build.rs script.
580 /// let target = std::env::var("TARGET").unwrap();
581 ///
582 /// let system_id = SystemID::try_from_rust_target(&target).unwrap();
583 /// ```
584 ///
585 /// # Panics
586 ///
587 /// This function will panic if the underlying call to
588 /// [`SystemID::try_current_rust_target()`] fails.
589 pub fn current_rust_target() -> SystemID {
590 match SystemID::try_current_rust_target() {
591 Ok(system_id) => system_id,
592 Err(err) => panic!(
593 "target_system_id() has not been implemented for the current target: {err}"
594 ),
595 }
596 }
597
598 /// Variant of [`SystemID::current_rust_target()`] that returns an error
599 /// instead of panicking.
600 pub fn try_current_rust_target() -> Result<SystemID, Error> {
601 SystemID::try_from_rust_target(env!("TARGET"))
602 }
603
604 /// Get the [`SystemID`] value corresponding to the specified
605 /// [Rust target triple][targets].
606 ///
607 /// ```
608 /// use wolfram_app_discovery::SystemID;
609 ///
610 /// assert_eq!(
611 /// SystemID::try_from_rust_target("x86_64-apple-darwin").unwrap(),
612 /// SystemID::MacOSX_x86_64
613 /// );
614 /// ```
615 ///
616 /// [targets]: https://doc.rust-lang.org/nightly/rustc/platform-support.html
617 pub fn try_from_rust_target(rust_target: &str) -> Result<SystemID, Error> {
618 #[rustfmt::skip]
619 let id = match rust_target {
620 //
621 // Rust Tier 1 Targets (all at time of writing)
622 //
623 "aarch64-unknown-linux-gnu" => SystemID::Linux_ARM64,
624 "i686-pc-windows-gnu" |
625 "i686-pc-windows-msvc" => SystemID::Windows,
626 "i686-unknown-linux-gnu" => SystemID::Linux,
627 "x86_64-apple-darwin" => SystemID::MacOSX_x86_64,
628 "x86_64-pc-windows-gnu" |
629 "x86_64-pc-windows-msvc" => {
630 SystemID::Windows_x86_64
631 },
632 "x86_64-unknown-linux-gnu" => SystemID::Linux_x86_64,
633
634 //
635 // Rust Tier 2 Targets (subset)
636 //
637
638 // 64-bit ARM
639 "aarch64-apple-darwin" => SystemID::MacOSX_ARM64,
640 "aarch64-apple-ios" |
641 "aarch64-apple-ios-sim" => SystemID::iOS_ARM64,
642 "aarch64-linux-android" => SystemID::Android,
643 // 32-bit ARM (e.g. Raspberry Pi)
644 "armv7-unknown-linux-gnueabihf" => SystemID::Linux_ARM,
645
646 _ => {
647 return Err(Error::other(format!(
648 "no known Wolfram System ID value associated with Rust target triple: {}",
649 rust_target
650 )))
651 },
652 };
653
654 Ok(id)
655 }
656
657 pub(crate) fn operating_system(&self) -> OperatingSystem {
658 match self {
659 SystemID::MacOSX_x86_64 | SystemID::MacOSX_ARM64 => OperatingSystem::MacOS,
660 SystemID::Windows_x86_64 | SystemID::Windows => OperatingSystem::Windows,
661 SystemID::Linux_x86_64
662 | SystemID::Linux_ARM64
663 | SystemID::Linux_ARM
664 | SystemID::Linux => OperatingSystem::Linux,
665 SystemID::iOS_ARM64 => OperatingSystem::Other,
666 SystemID::Android => OperatingSystem::Other,
667 }
668 }
669}
670
671impl WolframVersion {
672 /// Construct a new [`WolframVersion`].
673 ///
674 /// `WolframVersion` instances can be compared:
675 ///
676 /// ```
677 /// use wolfram_app_discovery::WolframVersion;
678 ///
679 /// let v13_2 = WolframVersion::new(13, 2, 0);
680 /// let v13_3 = WolframVersion::new(13, 3, 0);
681 ///
682 /// assert!(v13_2 < v13_3);
683 /// ```
684 pub const fn new(major: u32, minor: u32, patch: u32) -> Self {
685 WolframVersion {
686 major,
687 minor,
688 patch,
689 }
690 }
691
692 /// First component of [`$VersionNumber`][ref/$VersionNumber].
693 ///
694 /// [ref/$VersionNumber]: https://reference.wolfram.com/language/ref/$VersionNumber.html
695 pub const fn major(&self) -> u32 {
696 self.major
697 }
698
699 /// Second component of [`$VersionNumber`][ref/$VersionNumber].
700 ///
701 /// [ref/$VersionNumber]: https://reference.wolfram.com/language/ref/$VersionNumber.html
702 pub const fn minor(&self) -> u32 {
703 self.minor
704 }
705
706 /// [`$ReleaseNumber`][ref/$ReleaseNumber]
707 ///
708 /// [ref/$ReleaseNumber]: https://reference.wolfram.com/language/ref/$ReleaseNumber.html
709 pub const fn patch(&self) -> u32 {
710 self.patch
711 }
712}
713
714impl AppVersion {
715 #[allow(missing_docs)]
716 pub const fn major(&self) -> u32 {
717 self.major
718 }
719
720 #[allow(missing_docs)]
721 pub const fn minor(&self) -> u32 {
722 self.minor
723 }
724
725 #[allow(missing_docs)]
726 pub const fn revision(&self) -> u32 {
727 self.revision
728 }
729
730 #[allow(missing_docs)]
731 pub const fn minor_revision(&self) -> Option<u32> {
732 self.minor_revision
733 }
734
735 #[allow(missing_docs)]
736 pub const fn build_code(&self) -> Option<u32> {
737 self.build_code
738 }
739
740 fn parse(version: &str) -> Result<Self, Error> {
741 fn parse(s: &str) -> Result<u32, Error> {
742 u32::from_str(s).map_err(|err| make_error(s, err))
743 }
744
745 fn make_error(s: &str, err: std::num::ParseIntError) -> Error {
746 Error::other(format!(
747 "invalid application version number component: '{}': {}",
748 s, err
749 ))
750 }
751
752 let components: Vec<&str> = version.split(".").collect();
753
754 let app_version = match components.as_slice() {
755 // 5 components: major.minor.revision.minor_revision.build_code
756 [major, minor, revision, minor_revision, build_code] => AppVersion {
757 major: parse(major)?,
758 minor: parse(minor)?,
759 revision: parse(revision)?,
760
761 minor_revision: Some(parse(minor_revision)?),
762 build_code: Some(parse(build_code)?),
763 },
764 // 4 components: major.minor.revision.build_code
765 [major, minor, revision, build_code] => AppVersion {
766 major: parse(major)?,
767 minor: parse(minor)?,
768 revision: parse(revision)?,
769
770 minor_revision: None,
771 // build_code: Some(parse(build_code)?),
772 build_code: match u32::from_str(build_code) {
773 Ok(code) => Some(code),
774 // FIXME(breaking):
775 // Change build_code to be able to represent internal
776 // build codes like '202302011100' (which are technically
777 // numeric, but overflow u32's).
778 //
779 // The code below is a workaround bugfix to avoid hard
780 // erroring on WolframApp's with these build codes, with
781 // the contraint that this fix doesn't break semantic
782 // versioning compatibility by changing the build_code()
783 // return type.
784 //
785 // This fix should be changed when then next major version
786 // release of wolfram-app-discovery is made.
787 Err(err) if *err.kind() == std::num::IntErrorKind::PosOverflow => {
788 None
789 },
790 Err(other) => return Err(make_error(build_code, other)),
791 },
792 },
793 // 3 components: [major.minor.revision]
794 [major, minor, revision] => AppVersion {
795 major: parse(major)?,
796 minor: parse(minor)?,
797 revision: parse(revision)?,
798
799 minor_revision: None,
800 build_code: None,
801 },
802 _ => {
803 return Err(Error::other(format!(
804 "unexpected application version number format: {}",
805 version
806 )))
807 },
808 };
809
810 Ok(app_version)
811 }
812}
813
814#[allow(missing_docs)]
815impl WstpSdk {
816 /// Construct a new [`WstpSdk`] from a directory.
817 ///
818 /// # Examples
819 ///
820 /// ```
821 /// use std::path::PathBuf;
822 /// use wolfram_app_discovery::WstpSdk;
823 ///
824 /// let sdk = WstpSdk::try_from_directory(PathBuf::from(
825 /// "/Applications/Wolfram/Mathematica-Latest.app/Contents/SystemFiles/Links/WSTP/DeveloperKit/MacOSX-x86-64"
826 /// )).unwrap();
827 ///
828 /// assert_eq!(
829 /// sdk.wstp_c_header_path().file_name().unwrap(),
830 /// "wstp.h"
831 /// );
832 /// ```
833 pub fn try_from_directory(dir: PathBuf) -> Result<Self, Error> {
834 let Some(system_id) = dir.file_name() else {
835 return Err(Error::other(format!(
836 "WSTP SDK dir path file name is empty: {}",
837 dir.display()
838 )));
839 };
840
841 let system_id = system_id.to_str().ok_or_else(|| {
842 Error::other(format!(
843 "WSTP SDK dir path is not valid UTF-8: {}",
844 dir.display()
845 ))
846 })?;
847
848 let system_id = SystemID::from_str(system_id).map_err(|()| {
849 Error::other(format!(
850 "WSTP SDK dir path is does not end in a recognized SystemID: {}",
851 dir.display()
852 ))
853 })?;
854
855 Self::try_from_directory_with_system_id(dir, system_id)
856 }
857
858 pub fn try_from_directory_with_system_id(
859 dir: PathBuf,
860 system_id: SystemID,
861 ) -> Result<Self, Error> {
862 if !dir.is_dir() {
863 return Err(Error::other(format!(
864 "WSTP SDK dir path is not a directory: {}",
865 dir.display()
866 )));
867 };
868
869
870 let compiler_additions = dir.join("CompilerAdditions");
871
872 let wstp_h = compiler_additions.join("wstp.h");
873
874 if !wstp_h.is_file() {
875 return Err(Error::unexpected_layout(
876 "wstp.h C header file",
877 dir,
878 wstp_h,
879 ));
880 }
881
882 // NOTE: Determine the file name based on the specified `system_id`,
883 // NOT based on the current target OS.
884 let wstp_static_library = compiler_additions.join(
885 build_scripts::wstp_static_library_file_name(system_id.operating_system())?,
886 );
887
888 if !wstp_static_library.is_file() {
889 return Err(Error::unexpected_layout(
890 "WSTP static library file",
891 dir,
892 wstp_static_library,
893 ));
894 }
895
896 Ok(WstpSdk {
897 system_id,
898 sdk_dir: dir,
899 compiler_additions,
900
901 wstp_h,
902 wstp_static_library,
903 })
904 }
905
906 pub fn system_id(&self) -> SystemID {
907 self.system_id
908 }
909
910 pub fn sdk_dir(&self) -> PathBuf {
911 self.sdk_dir.clone()
912 }
913
914 /// Returns the location of the CompilerAdditions subdirectory of the WSTP
915 /// SDK.
916 pub fn wstp_compiler_additions_directory(&self) -> PathBuf {
917 self.compiler_additions.clone()
918 }
919
920 /// Returns the location of the
921 /// [`wstp.h`](https://reference.wolfram.com/language/ref/file/wstp.h.html)
922 /// header file.
923 ///
924 /// *Note: The [wstp](https://crates.io/crates/wstp) crate provides safe Rust bindings
925 /// to WSTP.*
926 pub fn wstp_c_header_path(&self) -> PathBuf {
927 self.wstp_h.clone()
928 }
929
930 /// Returns the location of the
931 /// [WSTP](https://reference.wolfram.com/language/guide/WSTPAPI.html)
932 /// static library.
933 ///
934 /// *Note: The [wstp](https://crates.io/crates/wstp) crate provides safe Rust bindings
935 /// to WSTP.*
936 pub fn wstp_static_library_path(&self) -> PathBuf {
937 self.wstp_static_library.clone()
938 }
939}
940
941impl Filter {
942 fn allow_all() -> Self {
943 Filter { app_types: None }
944 }
945
946 fn check_app(&self, app: &WolframApp) -> Result<(), FilterError> {
947 let Filter { app_types } = self;
948
949 // Filter by application type: Mathematica, Engine, Desktop, etc.
950 if let Some(app_types) = app_types {
951 if !app_types.contains(&app.app_type()) {
952 return Err(FilterError::FilterDoesNotMatchAppType {
953 app_type: app.app_type(),
954 allowed: app_types.clone(),
955 });
956 }
957 }
958
959 Ok(())
960 }
961}
962
963impl WolframApp {
964 /// Find the default Wolfram Language installation on this computer.
965 ///
966 /// # Discovery procedure
967 ///
968 /// 1. If the [`WOLFRAM_APP_DIRECTORY`][crate::config::env_vars::WOLFRAM_APP_DIRECTORY]
969 /// environment variable is set, return that.
970 ///
971 /// - Setting this environment variable may be necessary if a Wolfram application
972 /// was installed to a location not supported by the automatic discovery
973 /// mechanisms.
974 ///
975 /// - This enables advanced users of programs based on `wolfram-app-discovery` to
976 /// specify the Wolfram installation they would prefer to use.
977 ///
978 /// 2. If `wolframscript` is available on `PATH`, use it to evaluate
979 /// [`$InstallationDirectory`][$InstallationDirectory], and return the app at
980 /// that location.
981 ///
982 /// 3. Use operating system APIs to discover installed Wolfram applications.
983 /// - This will discover apps installed in standard locations, like `/Applications`
984 /// on macOS or `C:\Program Files` on Windows.
985 ///
986 /// [$InstallationDirectory]: https://reference.wolfram.com/language/ref/$InstallationDirectory.html
987 pub fn try_default() -> Result<Self, Error> {
988 let result = WolframApp::try_default_with_filter(&Filter::allow_all());
989
990 match &result {
991 Ok(app) => {
992 info!("App discovery succeeded: {}", app.app_directory().display())
993 },
994 Err(err) => info!("App discovery failed: {}", err),
995 }
996
997 result
998 }
999
1000 #[doc(hidden)]
1001 pub fn try_default_with_filter(filter: &Filter) -> Result<Self, Error> {
1002 //------------------------------------------------------------------------
1003 // If set, use RUST_WOLFRAM_LOCATION (deprecated) or WOLFRAM_APP_DIRECTORY
1004 //------------------------------------------------------------------------
1005
1006 #[allow(deprecated)]
1007 if let Some(dir) = config::get_env_var(RUST_WOLFRAM_LOCATION) {
1008 // This environment variable has been deprecated and will not be checked in
1009 // a future version of wolfram-app-discovery. Use the
1010 // WOLFRAM_APP_DIRECTORY environment variable instead.
1011 config::print_deprecated_env_var_warning(RUST_WOLFRAM_LOCATION, &dir);
1012
1013 let dir = PathBuf::from(dir);
1014
1015 // TODO: If an error occurs in from_path(), attach the fact that we're using
1016 // the environment variable to the error message.
1017 let app = WolframApp::from_installation_directory(dir)?;
1018
1019 // If the app doesn't satisfy the filter, return an error. We return an error
1020 // instead of silently proceeding to try the next discovery step because
1021 // setting an environment variable constitutes (typically) an explicit choice
1022 // by the user to use a specific installation. We can't fulfill that choice
1023 // because it doesn't satisfy the filter, but we can respect it by informing
1024 // them via an error instead of silently ignoring their choice.
1025 if let Err(filter_err) = filter.check_app(&app) {
1026 return Err(Error::app_does_not_match_filter(
1027 RUST_WOLFRAM_LOCATION,
1028 filter_err,
1029 ));
1030 }
1031
1032 return Ok(app);
1033 }
1034
1035 // TODO: WOLFRAM_(APP_)?INSTALLATION_DIRECTORY? Is this useful in any
1036 // situation where WOLFRAM_APP_DIRECTORY wouldn't be easy to set
1037 // (e.g. set based on $InstallationDirectory)?
1038
1039 if let Some(dir) = config::get_env_var(WOLFRAM_APP_DIRECTORY) {
1040 let dir = PathBuf::from(dir);
1041
1042 let app = WolframApp::from_app_directory(dir)?;
1043
1044 if let Err(filter_err) = filter.check_app(&app) {
1045 return Err(Error::app_does_not_match_filter(
1046 WOLFRAM_APP_DIRECTORY,
1047 filter_err,
1048 ));
1049 }
1050
1051 return Ok(app);
1052 }
1053
1054 //-----------------------------------------------------------------------
1055 // If wolframscript is on PATH, use it to evaluate $InstallationDirectory
1056 //-----------------------------------------------------------------------
1057
1058 if let Some(dir) = try_wolframscript_installation_directory()? {
1059 let app = WolframApp::from_installation_directory(dir)?;
1060 // If the app doesn't pass the filter, silently ignore it.
1061 if !filter.check_app(&app).is_err() {
1062 return Ok(app);
1063 }
1064 }
1065
1066 //--------------------------------------------------
1067 // Look in the operating system applications folder.
1068 //--------------------------------------------------
1069
1070 let apps: Vec<WolframApp> = discover_with_filter(filter);
1071
1072 if let Some(first) = apps.into_iter().next() {
1073 return Ok(first);
1074 }
1075
1076 //------------------------------------------------------------
1077 // No Wolfram applications could be found, so return an error.
1078 //------------------------------------------------------------
1079
1080 Err(Error::undiscoverable(
1081 "default Wolfram Language installation".to_owned(),
1082 Some(WOLFRAM_APP_DIRECTORY),
1083 ))
1084 }
1085
1086 /// Construct a `WolframApp` from an application directory path.
1087 ///
1088 /// # Example paths:
1089 ///
1090 /// Operating system | Example path
1091 /// -----------------|-------------
1092 /// macOS | /Applications/Mathematica.app
1093 pub fn from_app_directory(app_dir: PathBuf) -> Result<WolframApp, Error> {
1094 if !app_dir.is_dir() {
1095 return Err(Error::other(format!(
1096 "specified application location is not a directory: {}",
1097 app_dir.display()
1098 )));
1099 }
1100
1101 os::from_app_directory(&app_dir)?.set_engine_embedded_player()
1102 }
1103
1104 /// Construct a `WolframApp` from the
1105 /// [`$InstallationDirectory`][ref/$InstallationDirectory]
1106 /// of a Wolfram System installation.
1107 ///
1108 /// [ref/$InstallationDirectory]: https://reference.wolfram.com/language/ref/$InstallationDirectory.html
1109 ///
1110 /// # Example paths:
1111 ///
1112 /// Operating system | Example path
1113 /// -----------------|-------------
1114 /// macOS | /Applications/Mathematica.app/Contents/
1115 pub fn from_installation_directory(location: PathBuf) -> Result<WolframApp, Error> {
1116 if !location.is_dir() {
1117 return Err(Error::other(format!(
1118 "invalid Wolfram app location: not a directory: {}",
1119 location.display()
1120 )));
1121 }
1122
1123 // Canonicalize the $InstallationDirectory to the application directory, then
1124 // delegate to from_app_directory().
1125 let app_dir: PathBuf = match OperatingSystem::target_os() {
1126 OperatingSystem::MacOS => {
1127 if location.iter().last().unwrap() != "Contents" {
1128 return Err(Error::other(format!(
1129 "expected last component of installation directory to be \
1130 'Contents': {}",
1131 location.display()
1132 )));
1133 }
1134
1135 location.parent().unwrap().to_owned()
1136 },
1137 OperatingSystem::Windows => {
1138 // TODO: $InstallationDirectory appears to be the same as the app
1139 // directory in Mathematica v13. Is that true for all versions
1140 // released in the last few years, and for all Wolfram app types?
1141 location
1142 },
1143 OperatingSystem::Linux | OperatingSystem::Other => {
1144 return Err(Error::platform_unsupported(
1145 "WolframApp::from_installation_directory()",
1146 ));
1147 },
1148 };
1149
1150 WolframApp::from_app_directory(app_dir)
1151
1152 // if cfg!(target_os = "macos") {
1153 // ... check for .app, application plist metadata, etc.
1154 // canonicalize between ".../Mathematica.app" and ".../Mathematica.app/Contents/"
1155 // }
1156 }
1157
1158 // Properties
1159
1160 /// Get the product type of this application.
1161 pub fn app_type(&self) -> WolframAppType {
1162 self.app_type.clone()
1163 }
1164
1165 /// Get the application version.
1166 ///
1167 /// See also [`WolframApp::wolfram_version()`], which returns the version of the
1168 /// Wolfram Language bundled with app.
1169 pub fn app_version(&self) -> &AppVersion {
1170 &self.app_version
1171 }
1172
1173 /// Application directory location.
1174 pub fn app_directory(&self) -> PathBuf {
1175 self.app_directory.clone()
1176 }
1177
1178 /// Location of the application's main executable.
1179 ///
1180 /// * **macOS:** `CFBundleCopyExecutableURL()` location.
1181 /// * **Windows:** `RegGetValue(_, _, "ExecutablePath", ...)` location.
1182 /// * **Linux:** *TODO*
1183 pub fn app_executable(&self) -> Option<PathBuf> {
1184 self.app_executable.clone()
1185 }
1186
1187 /// Returns the version of the [Wolfram Language][WL] bundled with this application.
1188 ///
1189 /// [WL]: https://wolfram.com/language
1190 pub fn wolfram_version(&self) -> Result<WolframVersion, Error> {
1191 if self.app_version.major == 0 {
1192 return Err(Error::other(format!(
1193 "wolfram app has invalid application version: {:?} (at: {})",
1194 self.app_version,
1195 self.app_directory.display()
1196 )));
1197 }
1198
1199 // TODO: Are there any Wolfram products where the application version number is
1200 // not the same as the Wolfram Language version it contains?
1201 //
1202 // What about any Wolfram apps that do not contain a Wolfram Languae instance?
1203 Ok(WolframVersion {
1204 major: self.app_version.major,
1205 minor: self.app_version.minor,
1206 patch: self.app_version.revision,
1207 })
1208
1209 /* TODO:
1210 Look into fixing or working around the `wolframscript` hang on Windows, and generally
1211 improving this approach. E.g. use WSTP instead of parsing the stdout of wolframscript.
1212
1213 // MAJOR.MINOR
1214 let major_minor = self
1215 .wolframscript_output("$VersionNumber")?
1216 .split(".")
1217 .map(ToString::to_string)
1218 .collect::<Vec<String>>();
1219
1220 let [major, mut minor]: [String; 2] = match <[String; 2]>::try_from(major_minor) {
1221 Ok(pair @ [_, _]) => pair,
1222 Err(major_minor) => {
1223 return Err(Error(format!(
1224 "$VersionNumber has unexpected number of components: {:?}",
1225 major_minor
1226 )))
1227 },
1228 };
1229 // This can happen in major versions, when $VersionNumber formats as e.g. "13."
1230 if minor == "" {
1231 minor = String::from("0");
1232 }
1233
1234 // PATCH
1235 let patch = self.wolframscript_output("$ReleaseNumber")?;
1236
1237 let major = u32::from_str(&major).expect("unexpected $VersionNumber format");
1238 let minor = u32::from_str(&minor).expect("unexpected $VersionNumber format");
1239 let patch = u32::from_str(&patch).expect("unexpected $ReleaseNumber format");
1240
1241 Ok(WolframVersion {
1242 major,
1243 minor,
1244 patch,
1245 })
1246 */
1247 }
1248
1249 /// The [`$InstallationDirectory`][ref/$InstallationDirectory] of this Wolfram System
1250 /// installation.
1251 ///
1252 /// [ref/$InstallationDirectory]: https://reference.wolfram.com/language/ref/$InstallationDirectory.html
1253 pub fn installation_directory(&self) -> PathBuf {
1254 if let Some(ref player) = self.embedded_player {
1255 return player.installation_directory();
1256 }
1257
1258 match OperatingSystem::target_os() {
1259 OperatingSystem::MacOS => self.app_directory.join("Contents"),
1260 OperatingSystem::Windows => self.app_directory.clone(),
1261 // FIXME: Fill this in for Linux
1262 OperatingSystem::Linux => self.app_directory().clone(),
1263 OperatingSystem::Other => {
1264 panic!(
1265 "{}",
1266 Error::platform_unsupported("WolframApp::installation_directory()",)
1267 )
1268 },
1269 }
1270 }
1271
1272 //----------------------------------
1273 // Files
1274 //----------------------------------
1275
1276 /// Returns the location of the
1277 /// [`WolframKernel`](https://reference.wolfram.com/language/ref/program/WolframKernel.html)
1278 /// executable.
1279 pub fn kernel_executable_path(&self) -> Result<PathBuf, Error> {
1280 let path = match OperatingSystem::target_os() {
1281 OperatingSystem::MacOS => {
1282 // TODO: In older versions of the product, MacOSX was used instead of MacOS.
1283 // Look for either, depending on the version number.
1284 self.installation_directory()
1285 .join("MacOS")
1286 .join("WolframKernel")
1287 },
1288 OperatingSystem::Windows => {
1289 self.installation_directory().join("WolframKernel.exe")
1290 },
1291 OperatingSystem::Linux => {
1292 // NOTE: This empirically is valid for:
1293 // - Mathematica (tested: 13.1)
1294 // - Wolfram Engine (tested: 13.0, 13.3 prerelease)
1295 // TODO: Is this correct for Wolfram Desktop?
1296 self.installation_directory()
1297 .join("Executables")
1298 .join("WolframKernel")
1299 },
1300 OperatingSystem::Other => {
1301 return Err(Error::platform_unsupported("kernel_executable_path()"));
1302 },
1303 };
1304
1305 if !path.is_file() {
1306 return Err(Error::unexpected_app_layout(
1307 "WolframKernel executable",
1308 self,
1309 path,
1310 ));
1311 }
1312
1313 Ok(path)
1314 }
1315
1316 /// Returns the location of the
1317 /// [`wolframscript`](https://reference.wolfram.com/language/ref/program/wolframscript.html)
1318 /// executable.
1319 pub fn wolframscript_executable_path(&self) -> Result<PathBuf, Error> {
1320 if let Some(ref player) = self.embedded_player {
1321 return player.wolframscript_executable_path();
1322 }
1323
1324 let path = match OperatingSystem::target_os() {
1325 OperatingSystem::MacOS => PathBuf::from("MacOS").join("wolframscript"),
1326 OperatingSystem::Windows => PathBuf::from("wolframscript.exe"),
1327 OperatingSystem::Linux => {
1328 // NOTE: This empirically is valid for:
1329 // - Mathematica (tested: 13.1)
1330 // - Wolfram Engine (tested: 13.0, 13.3 prerelease)
1331 PathBuf::from("SystemFiles")
1332 .join("Kernel")
1333 .join("Binaries")
1334 .join(SystemID::current_rust_target().as_str())
1335 .join("wolframscript")
1336 },
1337 OperatingSystem::Other => {
1338 return Err(Error::platform_unsupported(
1339 "wolframscript_executable_path()",
1340 ));
1341 },
1342 };
1343
1344 let path = self.installation_directory().join(&path);
1345
1346 if !path.is_file() {
1347 return Err(Error::unexpected_app_layout(
1348 "wolframscript executable",
1349 self,
1350 path,
1351 ));
1352 }
1353
1354 Ok(path)
1355 }
1356
1357 /// Get a list of all [`WstpSdk`]s provided by this app.
1358 pub fn wstp_sdks(&self) -> Result<Vec<Result<WstpSdk, Error>>, Error> {
1359 let root = self
1360 .installation_directory()
1361 .join("SystemFiles")
1362 .join("Links")
1363 .join("WSTP")
1364 .join("DeveloperKit");
1365
1366 let mut sdks = Vec::new();
1367
1368 if !root.is_dir() {
1369 return Err(Error::unexpected_app_layout(
1370 "WSTP DeveloperKit directory",
1371 self,
1372 root,
1373 ));
1374 }
1375
1376 for entry in std::fs::read_dir(root)? {
1377 let value: Result<WstpSdk, Error> = match entry {
1378 Ok(entry) => WstpSdk::try_from_directory(entry.path()),
1379 Err(io_err) => Err(Error::from(io_err)),
1380 };
1381
1382 sdks.push(value);
1383 }
1384
1385 Ok(sdks)
1386 }
1387
1388 /// Get the [`WstpSdk`] for the current target platform.
1389 ///
1390 /// This function uses [`SystemID::current_rust_target()`] to determine
1391 /// the appropriate entry from [`WolframApp::wstp_sdks()`] to return.
1392 pub fn target_wstp_sdk(&self) -> Result<WstpSdk, Error> {
1393 self.wstp_sdks()?
1394 .into_iter()
1395 .flat_map(|sdk| sdk.ok())
1396 .find(|sdk| sdk.system_id() == SystemID::current_rust_target())
1397 .ok_or_else(|| {
1398 Error::other(format!("unable to locate WSTP SDK for current target"))
1399 })
1400 }
1401
1402 /// Returns the location of the
1403 /// [`wstp.h`](https://reference.wolfram.com/language/ref/file/wstp.h.html)
1404 /// header file.
1405 ///
1406 /// *Note: The [wstp](https://crates.io/crates/wstp) crate provides safe Rust bindings
1407 /// to WSTP.*
1408 #[deprecated(
1409 note = "use `WolframApp::target_wstp_sdk()?.wstp_c_header_path()` instead"
1410 )]
1411 pub fn wstp_c_header_path(&self) -> Result<PathBuf, Error> {
1412 Ok(self.target_wstp_sdk()?.wstp_c_header_path().to_path_buf())
1413 }
1414
1415 /// Returns the location of the
1416 /// [WSTP](https://reference.wolfram.com/language/guide/WSTPAPI.html)
1417 /// static library.
1418 ///
1419 /// *Note: The [wstp](https://crates.io/crates/wstp) crate provides safe Rust bindings
1420 /// to WSTP.*
1421 #[deprecated(
1422 note = "use `WolframApp::target_wstp_sdk()?.wstp_static_library_path()` instead"
1423 )]
1424 pub fn wstp_static_library_path(&self) -> Result<PathBuf, Error> {
1425 Ok(self
1426 .target_wstp_sdk()?
1427 .wstp_static_library_path()
1428 .to_path_buf())
1429 }
1430
1431 /// Returns the location of the directory containing the
1432 /// [Wolfram *LibraryLink*](https://reference.wolfram.com/language/guide/LibraryLink.html)
1433 /// C header files.
1434 ///
1435 /// The standard set of *LibraryLink* C header files includes:
1436 ///
1437 /// * WolframLibrary.h
1438 /// * WolframSparseLibrary.h
1439 /// * WolframImageLibrary.h
1440 /// * WolframNumericArrayLibrary.h
1441 ///
1442 /// *Note: The [wolfram-library-link](https://crates.io/crates/wolfram-library-link) crate
1443 /// provides safe Rust bindings to the Wolfram *LibraryLink* interface.*
1444 pub fn library_link_c_includes_directory(&self) -> Result<PathBuf, Error> {
1445 if let Some(ref player) = self.embedded_player {
1446 return player.library_link_c_includes_directory();
1447 }
1448
1449 let path = self
1450 .installation_directory()
1451 .join("SystemFiles")
1452 .join("IncludeFiles")
1453 .join("C");
1454
1455 if !path.is_dir() {
1456 return Err(Error::unexpected_app_layout(
1457 "LibraryLink C header includes directory",
1458 self,
1459 path,
1460 ));
1461 }
1462
1463 Ok(path)
1464 }
1465
1466 //----------------------------------
1467 // Sorting `WolframApp`s
1468 //----------------------------------
1469
1470 /// Order two `WolframApp`s by which is "best".
1471 ///
1472 /// This comparison will sort apps using the following factors in the given order:
1473 ///
1474 /// * Wolfram Language version number.
1475 /// * Application feature set (has a front end, is unrestricted)
1476 ///
1477 /// For example, [Mathematica][WolframAppType::Mathematica] is a more complete
1478 /// installation of the Wolfram System than [Wolfram Engine][WolframAppType::Engine],
1479 /// because it provides a notebook front end.
1480 ///
1481 /// See also [WolframAppType::ordering_value()].
1482 fn best_order(a: &WolframApp, b: &WolframApp) -> Ordering {
1483 //
1484 // First, sort by Wolfram Language version.
1485 //
1486
1487 let version_order = match (a.wolfram_version().ok(), b.wolfram_version().ok()) {
1488 (Some(a), Some(b)) => a.cmp(&b),
1489 (Some(_), None) => Ordering::Greater,
1490 (None, Some(_)) => Ordering::Less,
1491 (None, None) => Ordering::Equal,
1492 };
1493
1494 if version_order != Ordering::Equal {
1495 return version_order;
1496 }
1497
1498 //
1499 // Then, sort by application type.
1500 //
1501
1502 // Sort based roughly on the 'usefulness' of a particular application type.
1503 // E.g. Wolfram Desktop > Mathematica > Wolfram Engine > etc.
1504 let app_type_order = {
1505 let a = a.app_type().ordering_value();
1506 let b = b.app_type().ordering_value();
1507 a.cmp(&b)
1508 };
1509
1510 if app_type_order != Ordering::Equal {
1511 return app_type_order;
1512 }
1513
1514 debug_assert_eq!(a.wolfram_version().ok(), b.wolfram_version().ok());
1515 debug_assert_eq!(a.app_type().ordering_value(), b.app_type().ordering_value());
1516
1517 // TODO: Are there any other metrics by which we could sort this apps?
1518 // Installation location? Released build vs Prototype/nightly?
1519 Ordering::Equal
1520 }
1521
1522 //----------------------------------
1523 // Utilities
1524 //----------------------------------
1525
1526 /// Returns the location of the CompilerAdditions subdirectory of the WSTP
1527 /// SDK.
1528 #[deprecated(
1529 note = "use `WolframApp::target_wstp_sdk().sdk_dir().join(\"CompilerAdditions\")` instead"
1530 )]
1531 pub fn wstp_compiler_additions_directory(&self) -> Result<PathBuf, Error> {
1532 if let Some(ref player) = self.embedded_player {
1533 return player.wstp_compiler_additions_directory();
1534 }
1535
1536 let path = self.target_wstp_sdk()?.wstp_compiler_additions_directory();
1537
1538 if !path.is_dir() {
1539 return Err(Error::unexpected_app_layout(
1540 "WSTP CompilerAdditions directory",
1541 self,
1542 path,
1543 ));
1544 }
1545
1546 Ok(path)
1547 }
1548
1549 #[allow(dead_code)]
1550 fn wolframscript_output(&self, input: &str) -> Result<String, Error> {
1551 let mut args = vec!["-code".to_owned(), input.to_owned()];
1552
1553 args.push("-local".to_owned());
1554 args.push(self.kernel_executable_path().unwrap().display().to_string());
1555
1556 wolframscript_output(&self.wolframscript_executable_path()?, &args)
1557 }
1558}
1559
1560//----------------------------------
1561// Utilities
1562//----------------------------------
1563
1564pub(crate) fn print_platform_unimplemented_warning(op: &str) {
1565 eprintln!(
1566 "warning: operation '{}' is not yet implemented on this platform",
1567 op
1568 )
1569}
1570
1571#[cfg_attr(target_os = "windows", allow(dead_code))]
1572fn warning(message: &str) {
1573 eprintln!("warning: {}", message)
1574}
1575
1576fn wolframscript_output(
1577 wolframscript_command: &PathBuf,
1578 args: &[String],
1579) -> Result<String, Error> {
1580 let output: process::Output = process::Command::new(wolframscript_command)
1581 .args(args)
1582 .output()
1583 .expect("unable to execute wolframscript command");
1584
1585 // NOTE: The purpose of the 2nd clause here checking for exit code 3 is to work around
1586 // a mis-feature of wolframscript to return the same exit code as the Kernel.
1587 // TODO: Fix the bug in wolframscript which makes this necessary and remove the check
1588 // for `3`.
1589 if !output.status.success() && output.status.code() != Some(3) {
1590 panic!(
1591 "wolframscript exited with non-success status code: {}",
1592 output.status
1593 );
1594 }
1595
1596 let stdout = match String::from_utf8(output.stdout.clone()) {
1597 Ok(s) => s,
1598 Err(err) => {
1599 panic!(
1600 "wolframscript output is not valid UTF-8: {}: {}",
1601 err,
1602 String::from_utf8_lossy(&output.stdout)
1603 );
1604 },
1605 };
1606
1607 let first_line = stdout
1608 .lines()
1609 .next()
1610 .expect("wolframscript output was empty");
1611
1612 Ok(first_line.to_owned())
1613}
1614
1615/// If `wolframscript` is available on the users PATH, use it to evaluate
1616/// `$InstallationDirectory` to locate the default Wolfram Language installation.
1617///
1618/// If `wolframscript` is not on PATH, return `Ok(None)`.
1619fn try_wolframscript_installation_directory() -> Result<Option<PathBuf>, Error> {
1620 use std::process::Command;
1621
1622 // Use `wolframscript` if it's on PATH.
1623 let wolframscript = PathBuf::from("wolframscript");
1624
1625 // Run `wolframscript -h` to test whether `wolframscript` exists. `-h` because it
1626 // should never fail, never block, and only ever print to stdout.
1627 if let Err(err) = Command::new(&wolframscript).args(&["-h"]).output() {
1628 if err.kind() == std::io::ErrorKind::NotFound {
1629 // wolframscript executable is not available on PATH
1630 return Ok(None);
1631 } else {
1632 return Err(Error::other(format!(
1633 "unable to launch wolframscript: {}",
1634 err
1635 )));
1636 }
1637 };
1638
1639 // FIXME: Check if `wolframscript` is on the PATH first. If it isn't, we should
1640 // give a nicer error message.
1641 let location = wolframscript_output(
1642 &wolframscript,
1643 &["-code".to_owned(), "$InstallationDirectory".to_owned()],
1644 )?;
1645
1646 Ok(Some(PathBuf::from(location)))
1647}
1648
1649impl WolframApp {
1650 /// If `app` represents a Wolfram Engine app, set the `embedded_player` field to be
1651 /// the WolframApp representation of the embedded Wolfram Player.app that backs WE.
1652 fn set_engine_embedded_player(mut self) -> Result<Self, Error> {
1653 if self.app_type() != WolframAppType::Engine {
1654 return Ok(self);
1655 }
1656
1657 let embedded_player_path = match OperatingSystem::target_os() {
1658 OperatingSystem::MacOS => self
1659 .app_directory
1660 .join("Contents")
1661 .join("Resources")
1662 .join("Wolfram Player.app"),
1663 // Wolfram Engine does not contain an embedded Wolfram Player
1664 // on Windows.
1665 OperatingSystem::Windows | OperatingSystem::Linux => {
1666 return Ok(self);
1667 },
1668 OperatingSystem::Other => {
1669 // TODO: Does Wolfram Engine on Linux/Windows contain an embedded Wolfram Player,
1670 // or is that only done on macOS?
1671 print_platform_unimplemented_warning(
1672 "determine Wolfram Engine path to embedded Wolfram Player",
1673 );
1674
1675 // On the hope that returning `app` is more helpful than returning an error here,
1676 // do that.
1677 return Ok(self);
1678 },
1679 };
1680
1681 // TODO: If this `?` propagates an error
1682 let embedded_player = match WolframApp::from_app_directory(embedded_player_path) {
1683 Ok(player) => player,
1684 Err(err) => {
1685 return Err(Error::other(format!(
1686 "Wolfram Engine application does not contain Wolfram Player.app in the \
1687 expected location: {}",
1688 err
1689 )))
1690 },
1691 };
1692
1693 self.embedded_player = Some(Box::new(embedded_player));
1694
1695 Ok(self)
1696 }
1697}
1698
1699//======================================
1700// Conversion Impls
1701//======================================
1702
1703impl From<std::io::Error> for Error {
1704 fn from(err: std::io::Error) -> Error {
1705 Error(ErrorKind::IO(err.to_string()))
1706 }
1707}
1708
1709//======================================
1710// Formatting Impls
1711//======================================
1712
1713impl Display for Error {
1714 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1715 let Error(kind) = self;
1716
1717 write!(f, "Wolfram app error: {}", kind)
1718 }
1719}
1720
1721impl Display for ErrorKind {
1722 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1723 match self {
1724 ErrorKind::Undiscoverable {
1725 resource,
1726 environment_variable,
1727 } => match environment_variable {
1728 Some(var) => write!(f, "unable to locate {resource}. Hint: try setting {var}"),
1729 None => write!(f, "unable to locate {resource}"),
1730 },
1731 ErrorKind::UnexpectedAppLayout {
1732 resource_name,
1733 app_installation_dir,
1734 path,
1735 } => {
1736 write!(
1737 f,
1738 "in app at '{}', {resource_name} does not exist at the expected location: {}",
1739 app_installation_dir.display(),
1740 path.display()
1741 )
1742 },
1743 ErrorKind::UnexpectedLayout {
1744 resource_name,
1745 dir,
1746 path,
1747 } => {
1748 write!(
1749 f,
1750 "in component at '{}', {resource_name} does not exist at the expected location: {}",
1751 dir.display(),
1752 path.display()
1753 )
1754 },
1755 ErrorKind::UnexpectedEnvironmentValueLayout {
1756 resource_name,
1757 env_var,
1758 env_value,
1759 derived_path
1760 } => write!(
1761 f,
1762 "{resource_name} does not exist at expected location (derived from env config: {}={}): {}",
1763 env_var,
1764 env_value.display(),
1765 derived_path.display()
1766 ),
1767 ErrorKind::SpecifiedAppDoesNotMatchFilter {
1768 environment_variable: env_var,
1769 filter_err,
1770 } => write!(
1771 f,
1772 "app specified by environment variable '{env_var}' does not match filter: {filter_err}",
1773 ),
1774 ErrorKind::UnsupportedPlatform { operation, target_os } => write!(
1775 f,
1776 "operation '{operation}' is not yet implemented for this platform: {target_os:?}",
1777 ),
1778 ErrorKind::IO(io_err) => write!(f, "IO error during discovery: {}", io_err),
1779 ErrorKind::Other(message) => write!(f, "{message}"),
1780 }
1781 }
1782}
1783
1784impl Display for FilterError {
1785 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1786 match self {
1787 FilterError::FilterDoesNotMatchAppType { app_type, allowed } => {
1788 write!(f,
1789 "application type '{:?}' is not present in list of filtered app types: {:?}",
1790 app_type, allowed
1791 )
1792 },
1793 }
1794 }
1795}
1796
1797
1798impl Display for WolframVersion {
1799 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1800 let WolframVersion {
1801 major,
1802 minor,
1803 patch,
1804 } = *self;
1805
1806 write!(f, "{}.{}.{}", major, minor, patch)
1807 }
1808}
1809
1810impl Display for SystemID {
1811 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1812 write!(f, "{}", self.as_str())
1813 }
1814}