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}