├── .gitignore ├── .vscode └── settings.json ├── .rustfmt.toml ├── src ├── tests.rs ├── os │ ├── mod.rs │ ├── linux.rs │ ├── macos.rs │ ├── macos │ │ └── cf_exts.rs │ └── windows.rs ├── config.rs ├── bin │ └── wolfram-app-discovery │ │ ├── output.rs │ │ └── main.rs ├── build_scripts.rs └── lib.rs ├── docs ├── Maintenance.md ├── Development.md ├── CommandLineHelp.md └── CHANGELOG.md ├── LICENSE-MIT ├── CONTRIBUTING.md ├── Cargo.toml ├── tests └── main.rs ├── .github └── workflows │ └── build-executables.yml ├── README.md └── LICENSE-APACHE /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true 3 | "editor.formatOnSaveMode": "file" 4 | } 5 | -------------------------------------------------------------------------------- /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | max_width = 90 2 | match_block_trailing_comma = true 3 | blank_lines_upper_bound = 2 4 | merge_derives = false 5 | -------------------------------------------------------------------------------- /src/tests.rs: -------------------------------------------------------------------------------- 1 | use crate::WolframVersion; 2 | 3 | #[test] 4 | fn test_wolfram_version_ordering() { 5 | let v13_2_0 = WolframVersion::new(13, 2, 0); 6 | let v13_2_1 = WolframVersion::new(13, 2, 1); 7 | let v13_3_0 = WolframVersion::new(13, 3, 0); 8 | 9 | assert!(v13_2_0 == v13_2_0); 10 | assert!(v13_2_0 <= v13_2_0); 11 | 12 | assert!(v13_2_0 != v13_2_1); 13 | assert!(v13_2_0 <= v13_2_1); 14 | 15 | assert!(v13_3_0 > v13_2_0); 16 | assert!(v13_3_0 > v13_2_1); 17 | } 18 | -------------------------------------------------------------------------------- /docs/Maintenance.md: -------------------------------------------------------------------------------- 1 | # Maintenance 2 | 3 | This document describes steps required to maintain the `wolfram-app-discovery` project. 4 | 5 | ### `wolfram-app-discovery` command-line executable help text 6 | 7 | This maintenance task should be run every time the `wolfram-app-discovery` command-line 8 | interface changes. 9 | 10 | The [`CommandLineHelp.md`](./CommandLineHelp.md) file contains the `--help` text for the 11 | `wolfram-app-discovery` command-line tool. Storing this overview of the help text in a 12 | markdown file makes the functionality of `wolfram-app-discovery` more discoverable, and 13 | serves as an informal "cheet sheet" / reference material. Creation of the contents of 14 | `CommandLineHelp.md` is partially automated by the undocumented `print-all-help` 15 | subcommand. 16 | 17 | To update [`CommandLineHelp.md`](./CommandLineHelp.md), execute the following 18 | command: 19 | 20 | ``` 21 | $ cargo run --features=cli -- print-all-help --markdown > docs/CommandLineHelp.md 22 | ``` 23 | 24 | If the content has changed, commit it with a commit message like: 25 | `chore: Regenerate CommandLineHelp.md`. 26 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Wolfram Research Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Wolfram® 2 | 3 | Thank you for taking the time to contribute to the 4 | [Wolfram Research](https://github.com/wolframresearch) repositories on GitHub. 5 | 6 | ## Licensing of Contributions 7 | 8 | By contributing to Wolfram, you agree and affirm that: 9 | 10 | > Wolfram may release your contribution under the terms of the [MIT license](https://opensource.org/licenses/MIT) 11 | > AND the [Apache 2.0 license](https://opensource.org/licenses/Apache-2.0); and 12 | 13 | > You have read and agreed to the [Developer Certificate of Origin](http://developercertificate.org/), version 1.1 or later. 14 | 15 | Please see [LICENSE](LICENSE) for licensing conditions pertaining 16 | to individual repositories. 17 | 18 | 19 | ## Bug reports 20 | 21 | ### Security Bugs 22 | 23 | Please **DO NOT** file a public issue regarding a security issue. 24 | Rather, send your report privately to security@wolfram.com. Security 25 | reports are appreciated and we will credit you for it. We do not offer 26 | a security bounty, but the forecast in your neighborhood will be cloudy 27 | with a chance of Wolfram schwag! 28 | 29 | ### General Bugs 30 | 31 | Please use the repository issues page to submit general bug issues. 32 | 33 | Please do not duplicate issues. 34 | 35 | Please do send a complete and well-written report to us. Note: **the 36 | thoroughness of your report will positively correlate with our ability to address it**. 37 | 38 | When reporting issues, always include: 39 | 40 | * Your version of *Mathematica*® or the Wolfram Language. 41 | * Your operating system. 42 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "wolfram-app-discovery" 3 | version = "0.4.9" 4 | license = "MIT OR Apache-2.0" 5 | readme = "README.md" 6 | repository = "https://github.com/WolframResearch/wolfram-app-discovery-rs" 7 | description = "Find local installations of the Wolfram Language" 8 | keywords = ["wolfram", "wolfram-language", "discovery", "mathematica", "wolfram-engine"] 9 | categories = ["command-line-utilities", "development-tools", "development-tools::build-utils"] 10 | edition = "2021" 11 | 12 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 13 | 14 | exclude = [ 15 | ".vscode/*", 16 | ] 17 | 18 | #=================== 19 | # Features 20 | #=================== 21 | 22 | [features] 23 | default = [] 24 | cli = ["clap", "clap-markdown"] 25 | 26 | #=================== 27 | # Targets 28 | #=================== 29 | 30 | [[bin]] 31 | name = "wolfram-app-discovery" 32 | required-features = ["cli"] 33 | 34 | #=================== 35 | # Dependencies 36 | #=================== 37 | 38 | [dependencies] 39 | log = "0.4.17" 40 | 41 | clap = { version = "4.0.29", features = ["derive"], optional = true } 42 | clap-markdown = { version = "0.1.3", optional = true } 43 | 44 | [target.'cfg(target_os = "macos")'.dependencies] 45 | core-foundation = "0.9.2" 46 | 47 | [target.'cfg(target_os = "windows")'.dependencies] 48 | once_cell = "1.9.0" 49 | regex = "1.5.4" 50 | 51 | [target.'cfg(target_os = "windows")'.dependencies.windows] 52 | version = "0.32.0" 53 | features = [ 54 | "alloc", 55 | "Win32_Foundation", 56 | "Win32_System_Registry", 57 | "Win32_System_Threading", 58 | "Win32_System_SystemInformation", 59 | "Win32_System_SystemServices", 60 | "Win32_System_Diagnostics_Debug", 61 | "Win32_Storage_FileSystem", 62 | "Win32_Storage_Packaging_Appx", 63 | ] -------------------------------------------------------------------------------- /tests/main.rs: -------------------------------------------------------------------------------- 1 | use wolfram_app_discovery::{discover, WolframApp, WolframAppType}; 2 | 3 | #[test] 4 | fn test_try_default() { 5 | let _: WolframApp = WolframApp::try_default() 6 | .expect("WolframApp::try_default() could not locate any apps"); 7 | } 8 | 9 | #[test] 10 | fn macos_default_wolframscript_path() { 11 | if cfg!(not(target_os = "macos")) { 12 | return; 13 | } 14 | 15 | let app = WolframApp::try_default().expect("failed to locate Wolfram app"); 16 | 17 | let wolframscript_path = app 18 | .wolframscript_executable_path() 19 | .expect("failed to locate wolframscript"); 20 | 21 | assert!(wolframscript_path.ends_with("MacOS/wolframscript")); 22 | } 23 | 24 | /// Test that the WolframApp representing a Wolfram Engine application correctly resolves 25 | /// paths to the Wolfram Player.app that is used to support Wolfram Engine. 26 | #[test] 27 | fn macos_wolfram_engine_contains_wolfram_player() { 28 | if cfg!(not(target_os = "macos")) { 29 | return; 30 | } 31 | 32 | let engine: WolframApp = discover() 33 | .into_iter() 34 | .filter(|app: &WolframApp| app.app_type() == WolframAppType::Engine) 35 | .next() 36 | .expect("unable to locate a Wolfram Engine installation"); 37 | 38 | let install_dir = engine.installation_directory().to_str().unwrap().to_owned(); 39 | 40 | assert!(install_dir.contains("Wolfram Player.app")); 41 | } 42 | 43 | #[test] 44 | fn macos_wolfram_engine_properties() { 45 | if cfg!(not(target_os = "macos")) { 46 | return; 47 | } 48 | 49 | let engine: WolframApp = discover() 50 | .into_iter() 51 | .filter(|app: &WolframApp| app.app_type() == WolframAppType::Engine) 52 | .next() 53 | .expect("unable to locate a Wolfram Engine installation"); 54 | 55 | engine.wolfram_version().unwrap(); 56 | engine.wolframscript_executable_path().unwrap(); 57 | engine.kernel_executable_path().unwrap(); 58 | engine.target_wstp_sdk().unwrap(); 59 | engine.library_link_c_includes_directory().unwrap(); 60 | } 61 | -------------------------------------------------------------------------------- /src/os/mod.rs: -------------------------------------------------------------------------------- 1 | #[cfg(target_os = "macos")] 2 | pub mod macos; 3 | 4 | #[cfg(target_os = "windows")] 5 | pub mod windows; 6 | 7 | #[cfg(target_os = "linux")] 8 | pub mod linux; 9 | 10 | 11 | use std::path::PathBuf; 12 | 13 | use crate::{Error, WolframApp}; 14 | 15 | pub fn discover_all() -> Vec { 16 | #[cfg(target_os = "macos")] 17 | return macos::discover_all(); 18 | 19 | #[cfg(target_os = "windows")] 20 | return windows::discover_all(); 21 | 22 | #[cfg(target_os = "linux")] 23 | return linux::discover_all(); 24 | 25 | #[allow(unreachable_code)] 26 | { 27 | crate::print_platform_unimplemented_warning( 28 | "discover all installed Wolfram applications", 29 | ); 30 | 31 | Vec::new() 32 | } 33 | } 34 | 35 | pub fn from_app_directory(dir: &PathBuf) -> Result { 36 | #[cfg(target_os = "macos")] 37 | return macos::from_app_directory(dir); 38 | 39 | #[cfg(target_os = "windows")] 40 | return windows::from_app_directory(dir); 41 | 42 | #[cfg(target_os = "linux")] 43 | return linux::from_app_directory(dir); 44 | 45 | #[allow(unreachable_code)] 46 | Err(Error::platform_unsupported( 47 | "WolframApp::from_app_directory()", 48 | )) 49 | } 50 | 51 | //====================================== 52 | // Utilities 53 | //====================================== 54 | 55 | /// Operating systems supported by supported by `wolfram-app-discovery`. 56 | /// 57 | /// This enum and [`OperatingSystem::target_os()`] exist to be a less fragile 58 | /// alternative to code like: 59 | /// 60 | /// ```ignore 61 | /// if cfg!(target_os = "macos") { 62 | /// // ... 63 | /// } else if cfg!(target_os = "windows") { 64 | /// // ... 65 | /// } else if cfg!(target_os = "linux") { 66 | /// // ... 67 | /// } else { 68 | /// // Error 69 | /// } 70 | /// ``` 71 | /// 72 | /// Using an enum ensures that all variants are handled in any place where 73 | /// platform-specific logic is required. 74 | #[derive(Debug, Clone, PartialEq)] 75 | pub(crate) enum OperatingSystem { 76 | MacOS, 77 | Windows, 78 | Linux, 79 | Other, 80 | } 81 | 82 | impl OperatingSystem { 83 | /// Get the [`OperatingSystem`] value for the platform being targeted by the build 84 | /// of this Rust code. 85 | pub fn target_os() -> Self { 86 | if cfg!(target_os = "macos") { 87 | OperatingSystem::MacOS 88 | } else if cfg!(target_os = "windows") { 89 | OperatingSystem::Windows 90 | } else if cfg!(target_os = "linux") { 91 | OperatingSystem::Linux 92 | } else { 93 | OperatingSystem::Other 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /docs/Development.md: -------------------------------------------------------------------------------- 1 | 2 | # Development 3 | 4 | ## Build the `wolfram-app-discovery` executable 5 | 6 | The `wolfram-app-discovery` executable target requires the `"cli"` crate feature to be 7 | enabled: 8 | 9 | ```shell 10 | $ cargo build --features cli 11 | $ ./target/debug/wolfram-app-discovery 12 | ``` 13 | 14 | ### Check building on other platforms 15 | 16 | Doing a full test of `wolfram-app-discovery` requires actually running it 17 | on each platform. However, it is often useful to test that type checking and 18 | building complete successfully when targeting each of the three operating 19 | systems (macOS, Windows, and Linux) that `wolfram-app-discovery` supports. 20 | 21 | Note that when doing these quick "does it build?" tests, testing both x86_64 and 22 | ARM variants of an operating system doesn't provide much additional coverage 23 | beyond checking only one or the other. 24 | 25 | **Build for macOS:** 26 | 27 | ```shell 28 | $ cargo build --target x86_64-apple-darwin 29 | $ cargo build --target aarch64-apple-darwin 30 | ``` 31 | 32 | **Build for Windows:** 33 | 34 | ```shell 35 | $ cargo build --target x86_64-pc-windows-msvc 36 | ``` 37 | 38 | **Build for Linux:** 39 | 40 | x86-64: 41 | 42 | ```shell 43 | $ cargo build --target x86_64-unknown-linux-gnu 44 | $ cargo build --target aarch64-unknown-linux-gnu 45 | ``` 46 | 47 | ## Manual Testing 48 | 49 | There is currently no automated method for testing the `wolfram-app-discovery` 50 | CLI. The listings below attempt to enumerate common and uncommon ways to invoke 51 | the CLI so that they can be tested manually by the developer when changes are 52 | made. 53 | 54 | ### `wolfram-app-discovery` CLI 55 | 56 | #### `wolfram-app-discovery default` 57 | 58 | **Typical usage:** 59 | 60 | ```shell 61 | wolfram-app-discovery default 62 | wolfram-app-discovery default --format csv 63 | wolfram-app-discovery default --all-properties 64 | wolfram-app-discovery default --properties app-type,wolfram-version 65 | ``` 66 | 67 | **Combining format and property options:** 68 | 69 | ```shell 70 | wolfram-app-discovery default --all-properties --format csv 71 | ``` 72 | 73 | **`--raw-value`:** 74 | 75 | ```shell 76 | wolfram-app-discovery default --raw-value library-link-c-includes-directory 77 | ``` 78 | 79 | **Malformed argument errors:** 80 | 81 | ```shell 82 | # ERROR 83 | wolfram-app-discovery default --properties app-type --all-properties 84 | 85 | # ERROR 86 | wolfram-app-discovery default --raw-value library-link-c-includes-directory --all-properties 87 | ``` 88 | 89 | #### `wolfram-app-discovery list` 90 | 91 | ```shell 92 | wolfram-app-discovery list 93 | wolfram-app-discovery list --app-type mathematica 94 | wolfram-app-discovery list --app-type engine,desktop 95 | wolfram-app-discovery list --format csv 96 | wolfram-app-discovery list --all-properties 97 | wolfram-app-discovery list --all-properties --format csv 98 | ``` -------------------------------------------------------------------------------- /.github/workflows/build-executables.yml: -------------------------------------------------------------------------------- 1 | name: Build wolfram-app-discovery executable 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | ref: 7 | description: "The commit SHA or tag to build" 8 | required: true 9 | 10 | env: 11 | CARGO_TERM_COLOR: always 12 | 13 | jobs: 14 | 15 | build-release-artifacts: 16 | 17 | runs-on: ${{ matrix.os }} 18 | strategy: 19 | matrix: 20 | build: [linux-gnu, linux-musl, linux-arm-gnu, macos-x86-64, macos-arm, win-msvc] 21 | include: 22 | - build: linux-gnu 23 | os: ubuntu-20.04 24 | target: x86_64-unknown-linux-gnu 25 | 26 | - build: linux-musl 27 | os: ubuntu-20.04 28 | target: x86_64-unknown-linux-musl 29 | 30 | - build: linux-arm-gnu 31 | os: ubuntu-20.04 32 | target: aarch64-unknown-linux-gnu 33 | 34 | - build: macos-x86-64 35 | os: macos-12 36 | target: x86_64-apple-darwin 37 | 38 | - build: macos-arm 39 | os: macos-12 40 | target: aarch64-apple-darwin 41 | 42 | - build: win-msvc 43 | os: windows-2019 44 | target: x86_64-pc-windows-msvc 45 | 46 | steps: 47 | - uses: actions/checkout@v3 48 | with: 49 | ref: ${{ github.event.inputs.ref }} 50 | 51 | - name: Install Rust target 52 | run: rustup target add ${{ matrix.target }} 53 | 54 | - name: Install ARM64 linker, if applicable 55 | shell: bash 56 | run: | 57 | # If building for ARM64 Linux, install an ARM64 linker, 58 | # and set RUSTFLAGS so that cargo will use that linker. 59 | if [ "${{ matrix.target }}" = "aarch64-unknown-linux-gnu" ]; then 60 | sudo apt install gcc-aarch64-linux-gnu 61 | export RUSTFLAGS="-C linker=aarch64-linux-gnu-gcc" 62 | fi 63 | 64 | cargo build --release --features=cli --target ${{ matrix.target }} --verbose 65 | 66 | - name: Construct platform release archive 67 | shell: bash 68 | run: | 69 | staging="wolfram-app-discovery--${{ github.event.inputs.ref }}--${{ matrix.target}}" 70 | 71 | mkdir -p "$staging" 72 | 73 | cp {README.md,"docs/CommandLineHelp.md"} "$staging/" 74 | 75 | # Copy the built wolfram-app-discovery program to $staging. 76 | # 77 | # On macOS and Linux cargo will generate an executable with the name 78 | # `wolfram-app-discovery`. On Windows, it has the name `wolfram-app-discovery.exe`. 79 | if [ "${{ matrix.os }}" = "windows-2019"]; then 80 | cp "target/${{ matrix.target }}/release/wolfram-app-discovery.exe" "$staging/" 81 | else 82 | cp "target/${{ matrix.target }}/release/wolfram-app-discovery" "$staging/" 83 | fi 84 | 85 | # Compress the output archive ourselves, instead of letting 86 | # action/upload-artifact do it for us, due to this issue: 87 | # https://github.com/actions/upload-artifact/issues/38 88 | # Causing `wolfram-app-discovery` to lose its `+x` executable 89 | # file mode flag. 90 | if [ "${{ matrix.os }}" = "windows-2019"]; then 91 | 7z a "$staging.zip" "$staging" 92 | echo "ARTIFACT=$staging.zip" >> $GITHUB_ENV 93 | else 94 | tar czf "$staging.tar.gz" "$staging" 95 | echo "ARTIFACT=$staging.tar.gz" >> $GITHUB_ENV 96 | fi 97 | 98 | - name: Upload artifact 99 | uses: actions/upload-artifact@v3 100 | with: 101 | name: wolfram-app-discovery--${{ github.event.inputs.ref }}--${{ matrix.target}} 102 | path: ${{ env.ARTIFACT }} 103 | 104 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | //! Configuration of `wolfram-app-discovery` behavior. 2 | 3 | use std::sync::atomic::{AtomicBool, Ordering}; 4 | 5 | //====================================== 6 | // Environment variable names 7 | //====================================== 8 | 9 | // ==== Warning! ==== 10 | // 11 | // The names of these environment variables are ***part of the public API of the 12 | // wolfram-app-discovery library and executable***. Changing which environment 13 | // variables get checked is a backwards incompatible change! 14 | // // ==== Warning! ==== 15 | 16 | /// Environment variables. 17 | pub mod env_vars { 18 | // TODO: Rename to WOLFRAM_INSTALLATION_DIRECTORY, check for this as a 19 | // deprecated environment variable as practice. 20 | /// *Deprecated:* Use [`WOLFRAM_APP_DIRECTORY`] instead. 21 | /// Name of the environment variable that specifies the default Wolfram installation 22 | /// directory. 23 | #[deprecated(note = "use WOLFRAM_APP_DIRECTORY instead")] 24 | pub(crate) const RUST_WOLFRAM_LOCATION: &str = "RUST_WOLFRAM_LOCATION"; 25 | 26 | /// Name of the environment variable that specifies the default Wolfram application 27 | /// directory. 28 | pub const WOLFRAM_APP_DIRECTORY: &str = "WOLFRAM_APP_DIRECTORY"; 29 | 30 | /// WSTP `CompilerAdditions` directory 31 | #[deprecated(note = "use WSTP_COMPILER_ADDITIONS_DIRECTORY instead")] 32 | pub const WSTP_COMPILER_ADDITIONS: &str = "WSTP_COMPILER_ADDITIONS"; 33 | 34 | /// WSTP `CompilerAdditions` directory 35 | /// 36 | /// In a typical Wolfram Language installation, this is the 37 | /// `$InstallationDirectory/SystemFiles/Links/WSTP/DeveloperKit/$SystemID/CompilerAdditions/` 38 | /// directory. 39 | pub const WSTP_COMPILER_ADDITIONS_DIRECTORY: &str = 40 | "WSTP_COMPILER_ADDITIONS_DIRECTORY"; 41 | 42 | // /// *Deprecated:* Use [`WOLFRAM_LIBRARY_LINK_C_INCLUDES_DIRECTORY`] instead. 43 | // #[deprecated(note = "use WOLFRAM_LIBRARY_LINK_C_INCLUDES_DIRECTORY instead.")] 44 | 45 | 46 | /// Wolfram `$InstallationDirectory/SystemFiles/IncludeFiles/C` directory. 47 | pub const WOLFRAM_C_INCLUDES: &str = "WOLFRAM_C_INCLUDES"; 48 | 49 | /// Directory containing the Wolfram *LibraryLink* C header files. 50 | /// 51 | /// In a typical Wolfram Language installation, this is the 52 | /// `$InstallationDirectory/SystemFiles/IncludeFiles/C/` directory. 53 | pub const WOLFRAM_LIBRARY_LINK_C_INCLUDES_DIRECTORY: &str = 54 | "WOLFRAM_LIBRARY_LINK_C_INCLUDES_DIRECTORY"; 55 | } 56 | 57 | static PRINT_CARGO_INSTRUCTIONS: AtomicBool = AtomicBool::new(false); 58 | 59 | /// Set whether or not `wolfram-app-discovery` will print 60 | /// `cargo:rerun-if-env-changed=` directives. 61 | /// 62 | /// Defaults to `false`. The previous value for this configuration is returned. 63 | /// 64 | /// If `true`, `wolfram-app-discovery` functions will print: 65 | /// 66 | /// ```text 67 | /// cargo:rerun-if-env-changed= 68 | /// ``` 69 | /// 70 | /// each time an environment variable is checked by this library (where `` is the 71 | /// name of the environment variable). 72 | /// 73 | /// Cargo build scripts are intended to set this variable to `true` to ensure that 74 | /// changes in the build's environment configuration will trigger a rebuild. See the 75 | /// [Build Scripts] section of the Cargo Book for more information. 76 | /// 77 | /// 78 | /// [Build Scripts]: https://doc.rust-lang.org/cargo/reference/build-scripts.html#outputs-of-the-build-script 79 | pub fn set_print_cargo_build_script_directives(should_print: bool) -> bool { 80 | PRINT_CARGO_INSTRUCTIONS.swap(should_print, Ordering::SeqCst) 81 | } 82 | 83 | fn should_print_cargo_build_script_directives() -> bool { 84 | PRINT_CARGO_INSTRUCTIONS.load(Ordering::SeqCst) 85 | } 86 | 87 | //====================================== 88 | // Helpers 89 | //====================================== 90 | 91 | pub(crate) fn print_deprecated_env_var_warning(var: &str, value: &str) { 92 | let message = format!( 93 | "wolfram-app-discovery: warning: use of deprecated environment variable '{var}' (value={value:?})", 94 | ); 95 | 96 | // Print to stderr. 97 | eprintln!("{message}"); 98 | 99 | // If this is a cargo build script, print a directive that Cargo will 100 | // highlight to the user. 101 | if should_print_cargo_build_script_directives() { 102 | println!("cargo:warning={message}"); 103 | } 104 | } 105 | 106 | pub(crate) fn get_env_var(var: &'static str) -> Option { 107 | if should_print_cargo_build_script_directives() { 108 | println!("cargo:rerun-if-env-changed={}", var); 109 | } 110 | 111 | match std::env::var(var) { 112 | Ok(string) => Some(string), 113 | Err(std::env::VarError::NotPresent) => None, 114 | Err(std::env::VarError::NotUnicode(err)) => { 115 | panic!("value of env var '{}' is not valid unicode: {:?}", var, err) 116 | }, 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # wolfram-app-discovery 2 | 3 | [![Crates.io](https://img.shields.io/crates/v/wolfram-app-discovery.svg)](https://crates.io/crates/wolfram-app-discovery) 4 | ![License](https://img.shields.io/crates/l/wolfram-app-discovery.svg) 5 | [![Documentation](https://docs.rs/wolfram-app-discovery/badge.svg)](https://docs.rs/wolfram-app-discovery) 6 | 7 | #### [API Documentation](https://docs.rs/wolfram-app-discovery) | [CLI Documentation](./docs/CommandLineHelp.md) | [Changelog](./docs/CHANGELOG.md) | [Contributing](./CONTRIBUTING.md) 8 | 9 | ## About 10 | 11 | Find local installations of the Wolfram Language and Wolfram applications. 12 | 13 | This crate provides: 14 | 15 | * The `wolfram-app-discovery` Rust crate *([API docs](https://docs.rs/wolfram-app-discovery))* 16 | * The `wolfram-app-discovery` command-line tool *([CLI docs](./docs/CommandLineHelp.md), [Installation](#installing-wolfram-app-discovery))* 17 | 18 | ## Examples 19 | 20 | ### Using the API 21 | 22 | Locate the default Wolfram Language installation on this computer: 23 | ```rust 24 | use wolfram_app_discovery::WolframApp; 25 | 26 | let app = WolframApp::try_default() 27 | .expect("unable to locate any Wolfram applications"); 28 | 29 | // Prints a path like: 30 | // $InstallationDirectory: /Applications/Mathematica.app/Contents/ 31 | println!("$InstallationDirectory: {}", app.installation_directory().display()); 32 | ``` 33 | 34 | See also: [`WolframApp::try_default()`][WolframApp::try_default] 35 | 36 | ### Using the command-line tool 37 | 38 | Locate the default Wolfram Language installation on this computer: 39 | 40 | ```shell 41 | $ wolfram-app-discovery default 42 | App type: Mathematica 43 | Wolfram Language version: 13.1.0 44 | Application directory: /Applications/Wolfram/Mathematica.app 45 | ``` 46 | 47 | See [CommandLineHelp.md](./docs/CommandLineHelp.md) for more information on the 48 | `wolfram-app-discovery` command-line interface. 49 | 50 | ### Scenario: Building a LibraryLink library 51 | 52 | Suppose you have the following C program that provides a function via the 53 | Wolfram *LibraryLink* interface, which you would like to compile and call from 54 | Wolfram Language: 55 | 56 | ```c 57 | #include "WolframLibrary.h" 58 | 59 | /* Adds one to the input, returning the result */ 60 | DLLEXPORT int increment( 61 | WolframLibraryData libData, 62 | mint argc, 63 | MArgument *args, 64 | MArgument result 65 | ) { 66 | mint arg = MArgument_getInteger(args[0]); 67 | MArgument_setInteger(result, arg + 1); 68 | return LIBRARY_NO_ERROR; 69 | } 70 | ``` 71 | 72 | To successfully compile this program, a C compiler will need to be able to find 73 | the included `"WolframLibrary.h"` header file. We can use `wolfram-app-discovery` 74 | to get the path to the appropriate directory: 75 | 76 | ```shell 77 | # Get the LibraryLink includes directory 78 | $ export WOLFRAM_C_INCLUDES=`wolfram-app-discovery default --raw-value library-link-c-includes-directory` 79 | ``` 80 | 81 | And then pass that value to a C compiler: 82 | 83 | ```shell 84 | # Invoke the C compiler 85 | $ clang increment.c -I$WOLFRAM_C_INCLUDES -shared -o libincrement 86 | ``` 87 | 88 | The resulting compiled library can be loaded into Wolfram Language using 89 | [`LibraryFunctionLoad`](https://reference.wolfram.com/language/ref/LibraryFunctionLoad) 90 | and then called: 91 | 92 | ```wolfram 93 | func = LibraryFunctionLoad["~/libincrement", "increment", {Integer}, Integer]; 94 | 95 | func[5] (* Returns 6 *) 96 | ``` 97 | 98 | ## Installing `wolfram-app-discovery` 99 | 100 | [**Download `wolfram-app-discovery` releases.**](https://github.com/WolframResearch/wolfram-app-discovery-rs/releases) 101 | 102 | Precompiled binaries for the `wolfram-app-discovery` command-line tool are 103 | available for all major platforms from the GitHub Releases page. 104 | 105 | ### Using cargo 106 | 107 | `wolfram-app-discovery` can be installed using `cargo` 108 | (the [Rust package manager](https://doc.rust-lang.org/cargo/)) by executing: 109 | 110 | ```shell 111 | $ cargo install --features=cli wolfram-app-discovery 112 | ``` 113 | 114 | This will install the latest version of 115 | [`wolfram-app-discovery` from crates.io](https://crates.io/crates/wolfram-app-discovery). 116 | 117 | ## Configuration 118 | 119 | The default method used to locate a Wolfram Language installation 120 | ([`WolframApp::try_default()`][WolframApp::try_default]) will use the following 121 | steps to attempt to locate any local installations, returning the first one found: 122 | 123 | 1. The location specified by the `WOLFRAM_APP_DIRECTORY` environment variable, if set. 124 | 2. If `wolframscript` is on `PATH`, use it to locate the system installation. 125 | 3. Check in the operating system applications directory. 126 | 127 | #### Configuration example 128 | 129 | Specify a particular Wolfram Language installation to use (on macOS): 130 | 131 | ```shell 132 | $ export WOLFRAM_APP_DIRECTORY="/Applications/Mathematica.app" 133 | ``` 134 | 135 | This environment variable is checked by both the `wolfram-app-discovery` library and 136 | command-line executable. 137 | 138 | ## License 139 | 140 | Licensed under either of 141 | 142 | * Apache License, Version 2.0 143 | ([LICENSE-APACHE](LICENSE-APACHE) or ) 144 | * MIT license 145 | ([LICENSE-MIT](LICENSE-MIT) or ) 146 | 147 | at your option. 148 | 149 | ### Wolfram application licenses 150 | 151 | Wolfram applications are covered by different licensing terms than `wolfram-app-discovery`. 152 | 153 | [Wolfram Engine Community Edition](https://wolfram.com/engine) is a free 154 | distribution of the Wolfram Language, licensed for personal and non-production use cases. 155 | 156 | ## Contribution 157 | 158 | Unless you explicitly state otherwise, any contribution intentionally submitted 159 | for inclusion in the work by you, as defined in the Apache-2.0 license, shall be 160 | dual licensed as above, without any additional terms or conditions. 161 | 162 | See [CONTRIBUTING.md](./CONTRIBUTING.md) for more information. 163 | 164 | See [**Development.md**](./docs/Development.md) for instructions on how to 165 | perform common development tasks when contributing to this project. 166 | 167 | See [*Maintenance.md*](./docs/Maintenance.md) for instructions on how to 168 | maintain this project. 169 | 170 | 171 | [WolframApp::try_default]: https://docs.rs/wolfram-app-discovery/latest/wolfram_app_discovery/struct.WolframApp.html#method.try_default 172 | -------------------------------------------------------------------------------- /src/os/linux.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fs, 3 | path::{Path, PathBuf}, 4 | }; 5 | 6 | use crate::{AppVersion, Error, WolframApp, WolframAppType}; 7 | 8 | pub fn discover_all() -> Vec { 9 | match do_discover_all() { 10 | Ok(apps) => apps, 11 | Err(err) => { 12 | crate::warning(&format!("IO error discovering apps: {err}")); 13 | Vec::new() 14 | }, 15 | } 16 | } 17 | 18 | fn do_discover_all() -> Result, std::io::Error> { 19 | // Wolfram apps on Linux are by default installed to a location with the 20 | // following structure: 21 | // 22 | // /usr/local/Wolfram/// 23 | 24 | // TODO(polish): Are there any other root locations that Wolfram products 25 | // are or used to be installed to by default on Linux? 26 | #[rustfmt::skip] 27 | let roots = [ 28 | Path::new("/usr/local/Wolfram"), 29 | Path::new("/opt/Wolfram"), 30 | ]; 31 | 32 | let mut apps = Vec::new(); 33 | 34 | for apps_dir in roots { 35 | match get_apps_in_wolfram_apps_dir(apps_dir, &mut apps) { 36 | Ok(()) => (), 37 | Err(io_err) => { 38 | // Log this error as a warning, and continue looking in 39 | // other directories for potentially valid Wolfram apps. 40 | crate::warning(&format!( 41 | "error looking for Wolfram apps in '{}': {io_err}", 42 | apps_dir.display() 43 | )) 44 | }, 45 | } 46 | } 47 | 48 | Ok(apps) 49 | } 50 | 51 | /// Find Wolfram apps installed into a shared Wolfram "apps directory". 52 | /// 53 | /// Wolfram apps on Linux are by default installed to a location with the 54 | /// following structure: 55 | /// 56 | /// ```text 57 | /// /usr/local/Wolfram/// 58 | /// ``` 59 | /// 60 | /// where `/usr/local/Wolfram` is an "apps directory" that itself contains 61 | /// other Wolfram applications, where the application type and version number 62 | /// is encoded in their location inside the apps directory. 63 | /// 64 | /// Some concrete examples: 65 | /// 66 | /// * `/usr/local/Wolfram/Mathematica/13.1/` — the `$InstallationDirectory` for a Mathematica v13.1 app 67 | /// * `/usr/local/Wolfram/WolframEngine/13.2/` — the `$InstallationDirectory` for a Wolfram Engine v13.2 app 68 | fn get_apps_in_wolfram_apps_dir( 69 | apps_dir: &Path, 70 | apps: &mut Vec, 71 | ) -> Result<(), std::io::Error> { 72 | for app_type_dir in fs::read_dir(&apps_dir)? { 73 | let app_type_dir = app_type_dir?.path(); 74 | 75 | if !app_type_dir.is_dir() { 76 | continue; 77 | } 78 | 79 | for app_version_dir in fs::read_dir(&app_type_dir)? { 80 | let app_version_dir = app_version_dir?.path(); 81 | 82 | if !app_version_dir.is_dir() { 83 | continue; 84 | } 85 | 86 | match from_app_directory(&app_version_dir) { 87 | Ok(app) => apps.push(app), 88 | Err(err) => { 89 | // Log this error as a warning, but continue looking in 90 | // other directories for potentially valid Wolfram apps. 91 | crate::warning(&format!( 92 | "unable to interpret directory '{}' as Wolfram app: {err}", 93 | app_version_dir.display() 94 | )) 95 | }, 96 | } 97 | } 98 | } 99 | 100 | Ok(()) 101 | } 102 | 103 | //====================================== 104 | // WolframApp from app directory 105 | //====================================== 106 | 107 | pub fn from_app_directory(path: &PathBuf) -> Result { 108 | let (app_type, app_version) = parse_app_info_from_files(path)?; 109 | 110 | Ok(WolframApp { 111 | app_name: app_type.app_name().to_owned(), 112 | app_type, 113 | app_version, 114 | 115 | app_directory: path.clone(), 116 | 117 | app_executable: None, 118 | 119 | embedded_player: None, 120 | }) 121 | } 122 | 123 | // TODO(cleanup): 124 | // This entire function is a very hacky way of getting information about an 125 | // app on Linux, a platform where there is no OS-required standard for 126 | // application metadata. 127 | fn parse_app_info_from_files( 128 | app_directory: &PathBuf, 129 | ) -> Result<(WolframAppType, AppVersion), Error> { 130 | // 131 | // Parse the app type from the first line of LICENSE.txt 132 | // 133 | 134 | let license_txt = app_directory.join("LICENSE.txt"); 135 | 136 | if !license_txt.is_file() { 137 | return Err(Error::unexpected_app_layout_2( 138 | "LICENSE.txt file", 139 | app_directory.clone(), 140 | license_txt, 141 | )); 142 | } 143 | 144 | let contents: String = std::fs::read_to_string(&license_txt) 145 | .map_err(|err| Error::other(format!("Error reading LICENSE.txt: {err}")))?; 146 | 147 | // TODO(cleanup): Find a better way of determining the WolframAppType than 148 | // parsing LICENSE.txt. 149 | let app_type = match contents.lines().next() { 150 | Some("Wolfram Mathematica License Agreement") => WolframAppType::Mathematica, 151 | Some("Wolfram Mathematica® License Agreement") => WolframAppType::Mathematica, 152 | Some("Free Wolfram Engine(TM) for Developers: Terms and Conditions of Use") => WolframAppType::Engine, 153 | Some("Free Wolfram Engine™ for Developers: Terms and Conditions of Use") => WolframAppType::Engine, 154 | Some(other) => return Err(Error::other(format!( 155 | "Unable to determine Wolfram app type from LICENSE.txt: first line was: {other:?}" 156 | ))), 157 | None => return Err(Error::other("Unable to determine Wolfram app type from LICENSE.txt: file is empty.".to_owned())), 158 | }; 159 | 160 | // 161 | // Parse the Wolfram version from the WolframKernel launch script 162 | // 163 | 164 | let wolfram_kernel = app_directory.join("Executables").join("WolframKernel"); 165 | 166 | if !wolfram_kernel.is_file() { 167 | return Err(Error::unexpected_app_layout_2( 168 | "WolframKernel executable", 169 | app_directory.clone(), 170 | wolfram_kernel, 171 | )); 172 | } 173 | 174 | let contents: String = std::fs::read_to_string(&wolfram_kernel).map_err(|err| { 175 | Error::other(format!("Error reading WolframKernel executable: {err}")) 176 | })?; 177 | 178 | let app_version = match parse_wolfram_kernel_script_contents(&contents)? { 179 | Some(app_version) => app_version, 180 | None => { 181 | return Err(Error::other(format!( 182 | "Unable to parse app version from WolframKernel: unexpected file contents" 183 | ))) 184 | }, 185 | }; 186 | 187 | Ok((app_type, app_version)) 188 | } 189 | 190 | fn parse_wolfram_kernel_script_contents( 191 | contents: &str, 192 | ) -> Result, Error> { 193 | let mut lines = contents.lines(); 194 | 195 | if lines.next() != Some("#!/bin/sh") { 196 | return Ok(None); 197 | } 198 | 199 | if lines.next() != Some("#") { 200 | return Ok(None); 201 | } 202 | 203 | let info_line = match lines.next() { 204 | Some(line) => line, 205 | None => return Ok(None), 206 | }; 207 | 208 | let components: Vec<&str> = info_line.split(' ').collect(); 209 | 210 | let version_string = match components.as_slice() { 211 | &["#", "", "Mathematica", version_string, "Kernel", "command", "file"] => { 212 | version_string 213 | }, 214 | _other => return Ok(None), 215 | }; 216 | 217 | let app_version = AppVersion::parse(version_string)?; 218 | 219 | Ok(Some(app_version)) 220 | } 221 | -------------------------------------------------------------------------------- /docs/CommandLineHelp.md: -------------------------------------------------------------------------------- 1 | # Command-Line Help for `wolfram-app-discovery` 2 | 3 | This document contains the help content for the `wolfram-app-discovery` command-line program. 4 | 5 | **Command Overview:** 6 | 7 | * [`wolfram-app-discovery`↴](#wolfram-app-discovery) 8 | * [`wolfram-app-discovery default`↴](#wolfram-app-discovery-default) 9 | * [`wolfram-app-discovery list`↴](#wolfram-app-discovery-list) 10 | * [`wolfram-app-discovery inspect`↴](#wolfram-app-discovery-inspect) 11 | 12 | ## `wolfram-app-discovery` 13 | 14 | Find local installations of the Wolfram Language and Wolfram apps 15 | 16 | **Usage:** `wolfram-app-discovery ` 17 | 18 | ###### **Subcommands:** 19 | 20 | * `default` — Print the default Wolfram app 21 | * `list` — List all locatable Wolfram apps 22 | * `inspect` — Print information about a specified Wolfram application 23 | 24 | 25 | 26 | ## `wolfram-app-discovery default` 27 | 28 | Print the default Wolfram app. 29 | 30 | This method uses [`WolframApp::try_default()`] to locate the default app. 31 | 32 | **Usage:** `wolfram-app-discovery default [OPTIONS]` 33 | 34 | ###### **Options:** 35 | 36 | * `--app-type ` — Wolfram application types to include 37 | 38 | Possible values: 39 | - `wolfram-app`: 40 | Unified Wolfram App 41 | - `mathematica`: 42 | [Wolfram Mathematica](https://www.wolfram.com/mathematica/) 43 | - `engine`: 44 | [Wolfram Engine](https://wolfram.com/engine) 45 | - `desktop`: 46 | [Wolfram Desktop](https://www.wolfram.com/desktop/) 47 | - `player`: 48 | [Wolfram Player](https://www.wolfram.com/player/) 49 | - `player-pro`: 50 | [Wolfram Player Pro](https://www.wolfram.com/player-pro/) 51 | - `finance-platform`: 52 | [Wolfram Finance Platform](https://www.wolfram.com/finance-platform/) 53 | - `programming-lab`: 54 | [Wolfram Programming Lab](https://www.wolfram.com/programming-lab/) 55 | - `wolfram-alpha-notebook-edition`: 56 | [Wolfram|Alpha Notebook Edition](https://www.wolfram.com/wolfram-alpha-notebook-edition/) 57 | 58 | * `--debug` — Whether to print application information in the verbose Debug format 59 | * `--raw-value ` — If specified, the value of this property will be written without any trailing newline 60 | 61 | Possible values: 62 | - `app-type`: 63 | [`WolframAppType`] value describing the installation 64 | - `app-directory` 65 | - `wolfram-version`: 66 | [`WolframVersion`] value of the installation 67 | - `installation-directory`: 68 | [`$InstallationDirectory`] value of the installation 69 | - `library-link-c-includes-directory`: 70 | Wolfram *LibraryLink* C includes directory 71 | - `kernel-executable-path`: 72 | Location of the [`WolframKernel`] executable 73 | - `wolfram-script-executable-path`: 74 | Location of the [`wolframscript`] executable 75 | - `wstp-compiler-additions-directory`: 76 | Location of the WSTP SDK 'CompilerAdditions' directory 77 | 78 | * `--property ` — Properties to output 79 | 80 | Default values: `app-type`, `wolfram-version`, `app-directory` 81 | 82 | Possible values: 83 | - `app-type`: 84 | [`WolframAppType`] value describing the installation 85 | - `app-directory` 86 | - `wolfram-version`: 87 | [`WolframVersion`] value of the installation 88 | - `installation-directory`: 89 | [`$InstallationDirectory`] value of the installation 90 | - `library-link-c-includes-directory`: 91 | Wolfram *LibraryLink* C includes directory 92 | - `kernel-executable-path`: 93 | Location of the [`WolframKernel`] executable 94 | - `wolfram-script-executable-path`: 95 | Location of the [`wolframscript`] executable 96 | - `wstp-compiler-additions-directory`: 97 | Location of the WSTP SDK 'CompilerAdditions' directory 98 | 99 | * `--all-properties` — If set, all available properties will be printed 100 | * `--format ` 101 | 102 | Default value: `text` 103 | 104 | Possible values: `text`, `csv` 105 | 106 | 107 | 108 | 109 | ## `wolfram-app-discovery list` 110 | 111 | List all locatable Wolfram apps 112 | 113 | **Usage:** `wolfram-app-discovery list [OPTIONS]` 114 | 115 | ###### **Options:** 116 | 117 | * `--app-type ` — Wolfram application types to include 118 | 119 | Possible values: 120 | - `wolfram-app`: 121 | Unified Wolfram App 122 | - `mathematica`: 123 | [Wolfram Mathematica](https://www.wolfram.com/mathematica/) 124 | - `engine`: 125 | [Wolfram Engine](https://wolfram.com/engine) 126 | - `desktop`: 127 | [Wolfram Desktop](https://www.wolfram.com/desktop/) 128 | - `player`: 129 | [Wolfram Player](https://www.wolfram.com/player/) 130 | - `player-pro`: 131 | [Wolfram Player Pro](https://www.wolfram.com/player-pro/) 132 | - `finance-platform`: 133 | [Wolfram Finance Platform](https://www.wolfram.com/finance-platform/) 134 | - `programming-lab`: 135 | [Wolfram Programming Lab](https://www.wolfram.com/programming-lab/) 136 | - `wolfram-alpha-notebook-edition`: 137 | [Wolfram|Alpha Notebook Edition](https://www.wolfram.com/wolfram-alpha-notebook-edition/) 138 | 139 | * `--debug` — Whether to print application information in the verbose Debug format 140 | * `--property ` — Properties to output 141 | 142 | Default values: `app-type`, `wolfram-version`, `app-directory` 143 | 144 | Possible values: 145 | - `app-type`: 146 | [`WolframAppType`] value describing the installation 147 | - `app-directory` 148 | - `wolfram-version`: 149 | [`WolframVersion`] value of the installation 150 | - `installation-directory`: 151 | [`$InstallationDirectory`] value of the installation 152 | - `library-link-c-includes-directory`: 153 | Wolfram *LibraryLink* C includes directory 154 | - `kernel-executable-path`: 155 | Location of the [`WolframKernel`] executable 156 | - `wolfram-script-executable-path`: 157 | Location of the [`wolframscript`] executable 158 | - `wstp-compiler-additions-directory`: 159 | Location of the WSTP SDK 'CompilerAdditions' directory 160 | 161 | * `--all-properties` — If set, all available properties will be printed 162 | * `--format ` 163 | 164 | Default value: `text` 165 | 166 | Possible values: `text`, `csv` 167 | 168 | 169 | 170 | 171 | ## `wolfram-app-discovery inspect` 172 | 173 | Print information about a specified Wolfram application 174 | 175 | **Usage:** `wolfram-app-discovery inspect [OPTIONS] ` 176 | 177 | ###### **Arguments:** 178 | 179 | * `` 180 | 181 | ###### **Options:** 182 | 183 | * `--raw-value ` — If specified, the value of this property will be written without any trailing newline 184 | 185 | Possible values: 186 | - `app-type`: 187 | [`WolframAppType`] value describing the installation 188 | - `app-directory` 189 | - `wolfram-version`: 190 | [`WolframVersion`] value of the installation 191 | - `installation-directory`: 192 | [`$InstallationDirectory`] value of the installation 193 | - `library-link-c-includes-directory`: 194 | Wolfram *LibraryLink* C includes directory 195 | - `kernel-executable-path`: 196 | Location of the [`WolframKernel`] executable 197 | - `wolfram-script-executable-path`: 198 | Location of the [`wolframscript`] executable 199 | - `wstp-compiler-additions-directory`: 200 | Location of the WSTP SDK 'CompilerAdditions' directory 201 | 202 | * `--property ` — Properties to output 203 | 204 | Default values: `app-type`, `wolfram-version`, `app-directory` 205 | 206 | Possible values: 207 | - `app-type`: 208 | [`WolframAppType`] value describing the installation 209 | - `app-directory` 210 | - `wolfram-version`: 211 | [`WolframVersion`] value of the installation 212 | - `installation-directory`: 213 | [`$InstallationDirectory`] value of the installation 214 | - `library-link-c-includes-directory`: 215 | Wolfram *LibraryLink* C includes directory 216 | - `kernel-executable-path`: 217 | Location of the [`WolframKernel`] executable 218 | - `wolfram-script-executable-path`: 219 | Location of the [`wolframscript`] executable 220 | - `wstp-compiler-additions-directory`: 221 | Location of the WSTP SDK 'CompilerAdditions' directory 222 | 223 | * `--all-properties` — If set, all available properties will be printed 224 | * `--format ` 225 | 226 | Default value: `text` 227 | 228 | Possible values: `text`, `csv` 229 | 230 | * `--debug` — Whether to print application information in the verbose Debug format 231 | 232 | 233 | 234 |
235 | 236 | 237 | This document was generated automatically by 238 | clap-markdown. 239 | 240 | 241 | -------------------------------------------------------------------------------- /src/bin/wolfram-app-discovery/output.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fmt::{self, Display}, 3 | io, 4 | }; 5 | 6 | use wolfram_app_discovery::WolframApp; 7 | 8 | /// A property of a Wolfram installation that can be discovered. 9 | #[derive(Debug, Clone, PartialEq)] 10 | #[derive(clap::ValueEnum)] 11 | pub enum Property { 12 | /// [`WolframAppType`] value describing the installation. 13 | /// 14 | /// [`WolframAppType`]: https://docs.rs/wolfram-app-discovery/latest/wolfram_app_discovery/enum.WolframAppType.html 15 | AppType, 16 | 17 | AppDirectory, 18 | 19 | /// [`WolframVersion`] value of the installation. 20 | /// 21 | /// [`WolframVersion`]: https://docs.rs/wolfram-app-discovery/latest/wolfram_app_discovery/struct.WolframVersion.html 22 | WolframVersion, 23 | 24 | /// [`$InstallationDirectory`] value of the installation. 25 | /// 26 | /// [`$InstallationDirectory`]: https://reference.wolfram.com/language/ref/$InstallationDirectory 27 | InstallationDirectory, 28 | 29 | /// Wolfram *LibraryLink* C includes directory 30 | LibraryLinkCIncludesDirectory, 31 | 32 | /// Location of the [`WolframKernel`] executable. 33 | /// 34 | /// [`WolframKernel`]: https://reference.wolfram.com/language/ref/program/WolframKernel.html 35 | KernelExecutablePath, 36 | 37 | /// Location of the [`wolframscript`] executable. 38 | /// 39 | /// [`wolframscript`]: https://reference.wolfram.com/language/ref/program/wolframscript.html 40 | WolframScriptExecutablePath, 41 | 42 | /// Location of the WSTP SDK 'CompilerAdditions' directory. 43 | WstpCompilerAdditionsDirectory, 44 | } 45 | 46 | /// Represents the value of the specified property on the given app for the 47 | /// purposes of formatting. 48 | /// 49 | /// The purpose of this type is to implement [`Display`]. 50 | /// 51 | pub struct PropertyValue<'app>(pub &'app WolframApp, pub Property); 52 | 53 | //========================================================== 54 | // Impls 55 | //========================================================== 56 | 57 | impl Property { 58 | pub const fn variants() -> &'static [Property] { 59 | // NOTE: Whenever the match statement below causes a compile time failure 60 | // because a variant has been added, update the returned slice to 61 | // include the new variant. 62 | if false { 63 | #[allow(unused_variables)] 64 | let property: Property = unreachable!(); 65 | 66 | #[allow(unreachable_code)] 67 | match property { 68 | Property::AppType 69 | | Property::WolframVersion 70 | | Property::AppDirectory 71 | | Property::InstallationDirectory 72 | | Property::KernelExecutablePath 73 | | Property::WolframScriptExecutablePath 74 | | Property::WstpCompilerAdditionsDirectory 75 | | Property::LibraryLinkCIncludesDirectory => unreachable!(), 76 | } 77 | } 78 | 79 | &[ 80 | Property::AppType, 81 | Property::WolframVersion, 82 | Property::AppDirectory, 83 | Property::InstallationDirectory, 84 | Property::KernelExecutablePath, 85 | Property::WolframScriptExecutablePath, 86 | Property::WstpCompilerAdditionsDirectory, 87 | Property::LibraryLinkCIncludesDirectory, 88 | ] 89 | } 90 | } 91 | 92 | //========================================================== 93 | // CSV 94 | //========================================================== 95 | 96 | pub fn write_csv_header( 97 | fmt: &mut dyn io::Write, 98 | properties: &[Property], 99 | ) -> io::Result<()> { 100 | let header: String = properties 101 | .iter() 102 | .map(ToString::to_string) 103 | .collect::>() 104 | .join(","); 105 | 106 | writeln!(fmt, "{header}") 107 | } 108 | 109 | pub fn write_csv_row( 110 | fmt: &mut dyn io::Write, 111 | app: &WolframApp, 112 | properties: &[Property], 113 | ) -> io::Result<()> { 114 | for (index, prop) in properties.iter().cloned().enumerate() { 115 | let value = format!("{}", PropertyValue(app, prop)); 116 | 117 | // Write the value as an escaped string. 118 | // TODO: Find a better method for CSV-escaping values. 119 | write!(fmt, "{value:?}")?; 120 | 121 | // If this isn't the last column, write a comma separator. 122 | if index != properties.len() - 1 { 123 | write!(fmt, ",")?; 124 | } 125 | } 126 | 127 | write!(fmt, "\n") 128 | } 129 | 130 | //====================================== 131 | // Display and formatting 132 | //====================================== 133 | 134 | impl Display for Property { 135 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 136 | let name = match self { 137 | Property::AppType => "App type", 138 | Property::WolframVersion => "Wolfram Language version", 139 | Property::AppDirectory => "Application directory", 140 | Property::InstallationDirectory => "$InstallationDirectory", 141 | Property::KernelExecutablePath => "WolframKernel executable", 142 | Property::WolframScriptExecutablePath => "wolframscript executable", 143 | Property::WstpCompilerAdditionsDirectory => { 144 | "WSTP CompilerAdditions directory" 145 | }, 146 | Property::LibraryLinkCIncludesDirectory => "LibraryLink C includes directory", 147 | }; 148 | 149 | write!(f, "{name}") 150 | } 151 | } 152 | 153 | impl<'app> Display for PropertyValue<'app> { 154 | fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { 155 | let PropertyValue(app, property) = self; 156 | 157 | match property { 158 | Property::AppType => { 159 | write!(fmt, "{:?}", app.app_type()) 160 | }, 161 | Property::WolframVersion => match app.wolfram_version() { 162 | Ok(version) => write!(fmt, "{version}"), 163 | Err(error) => { 164 | // Print an error to stderr. 165 | eprintln!("Error getting WolframVersion value: {error}"); 166 | 167 | write!(fmt, "Error") 168 | }, 169 | }, 170 | Property::AppDirectory => { 171 | write!(fmt, "{}", app.app_directory().display()) 172 | }, 173 | Property::InstallationDirectory => { 174 | write!(fmt, "{}", app.installation_directory().display()) 175 | }, 176 | Property::KernelExecutablePath => match app.kernel_executable_path() { 177 | Ok(path) => write!(fmt, "{}", path.display()), 178 | Err(error) => { 179 | // Print an error to stderr. 180 | eprintln!("Error getting WolframKernel location: {error}"); 181 | 182 | write!(fmt, "Error") 183 | }, 184 | }, 185 | Property::WolframScriptExecutablePath => { 186 | match app.wolframscript_executable_path() { 187 | Ok(path) => write!(fmt, "{}", path.display()), 188 | Err(error) => { 189 | // Print an error to stderr. 190 | eprintln!("Error getting wolframscript location: {error}"); 191 | 192 | write!(fmt, "Error") 193 | }, 194 | } 195 | }, 196 | Property::WstpCompilerAdditionsDirectory => { 197 | match app.target_wstp_sdk() { 198 | Ok(wstp_sdk) => write!( 199 | fmt, 200 | "{}", 201 | wstp_sdk.wstp_compiler_additions_directory().display() 202 | ), 203 | Err(error) => { 204 | // Print an error to stderr. 205 | eprintln!("Error getting target WSTP SDK location: {error}"); 206 | 207 | write!(fmt, "Error") 208 | }, 209 | } 210 | }, 211 | Property::LibraryLinkCIncludesDirectory => match app 212 | .library_link_c_includes_directory() 213 | { 214 | Ok(value) => write!(fmt, "{}", value.display()), 215 | Err(error) => { 216 | // Print an error to stderr. 217 | eprintln!("Error getting LibraryLink C includes directory: {error}"); 218 | 219 | write!(fmt, "Error") 220 | }, 221 | }, 222 | } 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /src/os/macos.rs: -------------------------------------------------------------------------------- 1 | mod cf_exts; 2 | 3 | use std::path::PathBuf; 4 | 5 | use core_foundation::{ 6 | array::{CFArrayGetCount, CFArrayGetValueAtIndex, CFArrayRef}, 7 | base::CFRelease, 8 | base::TCFType, 9 | bundle::{CFBundleCopyExecutableURL, CFBundleCreate, CFBundleRef}, 10 | error::{CFError, CFErrorRef}, 11 | string::CFStringRef, 12 | url::CFURLRef, 13 | }; 14 | 15 | use crate::{AppVersion, Error, WolframApp, WolframAppType}; 16 | 17 | pub fn discover_all() -> Vec { 18 | load_installed_products_from_launch_services() 19 | } 20 | 21 | pub fn from_app_directory(path: &PathBuf) -> Result { 22 | let url: CFURLRef = match cf_exts::url_create_with_file_system_path(path) { 23 | Some(url) => url, 24 | None => { 25 | return Err(Error::other(format!( 26 | "unable to create CFURL from path: {:?}", 27 | path 28 | ))) 29 | }, 30 | }; 31 | 32 | unsafe { get_app_from_url(url, None) } 33 | } 34 | 35 | impl WolframAppType { 36 | #[rustfmt::skip] 37 | fn bundle_id(&self) -> &'static str { 38 | use WolframAppType::*; 39 | 40 | match self { 41 | WolframApp => "com.wolfram.WolframApp", 42 | Mathematica => "com.wolfram.Mathematica", 43 | PlayerPro => "com.wolfram.Mathematica.PlayerPro", 44 | Player => "com.wolfram.Mathematica.Player", 45 | Desktop => "com.wolfram.Desktop", 46 | Engine => "com.wolfram.WolframEngine", 47 | FinancePlatform => "com.wolfram.FinancePlatform", 48 | ProgrammingLab => "com.wolfram.ProgrammingLab", 49 | WolframAlphaNotebookEdition => "com.wolfram.WolframAlpha.Notebook", 50 | } 51 | } 52 | } 53 | 54 | unsafe fn get_app_from_url( 55 | app_url: CFURLRef, 56 | mut app_type: Option, 57 | ) -> Result { 58 | let bundle: CFBundleRef = CFBundleCreate(std::ptr::null(), app_url); 59 | 60 | if bundle.is_null() { 61 | return Err(Error::other("invalid CFBundleRef pointer".to_owned())); 62 | } 63 | 64 | // 65 | // Get the application bundle identifier 66 | // 67 | 68 | let bundle_id = match cf_exts::bundle_identifier(bundle) { 69 | Some(id) => id, 70 | None => { 71 | return Err(Error::other(format!( 72 | "unable to read application bundle identifier" 73 | ))) 74 | }, 75 | }; 76 | 77 | // Sanity check that the app type declared by the caller matches the apps actual 78 | // bundle identifier. 79 | if let Some(ref app_type) = app_type { 80 | assert_eq!(bundle_id, app_type.bundle_id()); 81 | } 82 | 83 | // 84 | // Get the application type (if not declared already by the caller) 85 | // 86 | 87 | let app_type: WolframAppType = match app_type { 88 | Some(type_) => type_, 89 | None => { 90 | app_type = WolframAppType::variants().into_iter().find(|app| { 91 | // Perform a case-insensitive comparison. 92 | app.bundle_id().to_ascii_lowercase() == bundle_id.to_ascii_lowercase() 93 | }); 94 | 95 | match app_type { 96 | Some(type_) => type_, 97 | None => { 98 | return Err(Error::other(format!( 99 | "application bundle identifier is not a known Wolfram app: {}", 100 | bundle_id 101 | ))) 102 | }, 103 | } 104 | }, 105 | }; 106 | 107 | // 108 | // Get the application directory 109 | // 110 | 111 | let app_directory: PathBuf = 112 | match cf_exts::url_get_file_system_representation(app_url) { 113 | Some(path) => path, 114 | None => { 115 | return Err(Error::other(format!( 116 | "unable to convert application CFURL to file system representation" 117 | ))) 118 | }, 119 | }; 120 | 121 | assert!(app_directory.is_absolute()); 122 | 123 | // 124 | // Get the application main executable 125 | // 126 | 127 | let exec_url: CFURLRef = CFBundleCopyExecutableURL(bundle); 128 | 129 | let app_executable: Option = if !exec_url.is_null() { 130 | let path: PathBuf = match cf_exts::url_get_file_system_representation(exec_url) { 131 | Some(path) => path, 132 | None => { 133 | return Err(Error::other(format!( 134 | "unable to convert application executable CFURL to file system \ 135 | representation" 136 | ))) 137 | }, 138 | }; 139 | 140 | assert!(path.is_absolute()); 141 | 142 | CFRelease(exec_url as *const _); 143 | Some(path) 144 | } else { 145 | None 146 | }; 147 | 148 | // 149 | // Get the application version number 150 | // 151 | 152 | let app_version = match cf_exts::bundle_get_value_for_info_dictionary_key( 153 | bundle, 154 | "CFBundleShortVersionString", 155 | ) { 156 | Some(version) => AppVersion::parse(&version).map_err(|err| { 157 | Error::other(format!( 158 | "unable to parse application short version string: '{}': {}", 159 | version, err 160 | )) 161 | })?, 162 | None => { 163 | return Err(Error::other(format!( 164 | "unable to read application short version string" 165 | ))) 166 | }, 167 | }; 168 | 169 | let app_name = 170 | cf_exts::bundle_get_value_for_info_dictionary_key(bundle, "CFBundleName") 171 | .ok_or_else(|| { 172 | Error::other("app is missing CFBundleName property".to_owned()) 173 | })?; 174 | 175 | // 176 | // Release `bundle` and return the final WolframApp description. 177 | // 178 | 179 | CFRelease(bundle as *const _); 180 | 181 | WolframApp { 182 | app_type, 183 | app_name, 184 | app_directory, 185 | app_executable, 186 | app_version, 187 | embedded_player: None, 188 | } 189 | .set_engine_embedded_player() 190 | } 191 | 192 | fn load_installed_products_from_launch_services() -> Vec { 193 | let mut app_bundles = Vec::new(); 194 | 195 | for app_type in WolframAppType::variants() { 196 | let bundle_id: CFStringRef = cf_exts::cf_string_from_str(app_type.bundle_id()); 197 | 198 | unsafe { 199 | let mut err: CFErrorRef = std::ptr::null_mut(); 200 | let app_urls: CFArrayRef = 201 | cf_exts::LSCopyApplicationURLsForBundleIdentifier(bundle_id, &mut err); 202 | 203 | // Assume that if an error occurs, it is kLSApplicationNotFoundErr. 204 | // TODO: core_foundation doesn't currently expose 205 | // kLSApplicationNotFoundErr as a constant; if that is added, 206 | // check for it here. 207 | if !err.is_null() { 208 | // Deallocate the error object. 209 | let _err = CFError::wrap_under_create_rule(err); 210 | 211 | /* 212 | crate::warning(&format!( 213 | "warning: error searching for '{:?}' application instances", 214 | app_type 215 | )); 216 | */ 217 | 218 | continue; 219 | } 220 | 221 | let count: isize = CFArrayGetCount(app_urls); 222 | 223 | for index in 0..count { 224 | let url: CFURLRef = CFArrayGetValueAtIndex(app_urls, index) as CFURLRef; 225 | if url.is_null() { 226 | // This shouldn't happen, so ignore it. 227 | crate::warning("CFURLRef was unexpectedly NULL"); 228 | continue; 229 | } 230 | 231 | match get_app_from_url(url, Some(app_type.clone())) { 232 | Ok(app) => app_bundles.push(app), 233 | Err(err) => { 234 | // TODO: Do something else here? 235 | // We don't want this to be a catastrophic error, 236 | // because one "corrupted" app installation shouldn't 237 | // prevent us from returning a list of other valid 238 | // installations. But we should inform the user of this 239 | // somehow. 240 | crate::warning(&format!( 241 | "warning: wolfram app had unexpected or invalid\ 242 | structure: {}", 243 | err 244 | )) 245 | }, 246 | } 247 | } 248 | 249 | CFRelease(app_urls as *const _); 250 | CFRelease(bundle_id as *const _); 251 | } 252 | } 253 | 254 | app_bundles 255 | } 256 | -------------------------------------------------------------------------------- /src/bin/wolfram-app-discovery/main.rs: -------------------------------------------------------------------------------- 1 | mod output; 2 | 3 | 4 | use std::path::PathBuf; 5 | 6 | use clap::Parser; 7 | 8 | use wolfram_app_discovery::{self as wad, Filter, WolframApp, WolframAppType}; 9 | 10 | use self::output::{Property, PropertyValue}; 11 | 12 | /// Find local installations of the Wolfram Language and Wolfram apps. 13 | #[derive(Parser, Debug)] 14 | struct Args { 15 | #[clap(subcommand)] 16 | command: Command, 17 | } 18 | 19 | #[derive(Parser, Debug)] 20 | enum Command { 21 | /// Print the default Wolfram app. 22 | /// 23 | /// This method uses [`WolframApp::try_default()`] to locate the default app. 24 | #[clap(display_order(1))] 25 | Default { 26 | #[clap(flatten)] 27 | discovery: DiscoveryOpts, 28 | 29 | #[clap(flatten)] 30 | output: SingleOutputOpts, 31 | }, 32 | /// List all locatable Wolfram apps. 33 | #[clap(display_order(2))] 34 | List { 35 | #[clap(flatten)] 36 | discovery: DiscoveryOpts, 37 | 38 | #[clap(flatten)] 39 | output: OutputOpts, 40 | }, 41 | /// Print information about a specified Wolfram application. 42 | #[clap(display_order(3))] 43 | Inspect { 44 | app_dir: PathBuf, 45 | 46 | #[clap(flatten)] 47 | opts: SingleOutputOpts, 48 | 49 | #[clap(flatten)] 50 | debug: Debug, 51 | }, 52 | // For generating `docs/CommandLineHelp.md`. 53 | #[clap(hide = true)] 54 | PrintAllHelp { 55 | #[arg(long, required = true)] 56 | markdown: bool, 57 | }, 58 | } 59 | 60 | //====================================== 61 | // Arguments and options parsing 62 | //====================================== 63 | 64 | /// CLI arguments that affect which apps get discovered. 65 | #[derive(Debug, Clone)] 66 | #[derive(Parser)] 67 | struct DiscoveryOpts { 68 | /// Wolfram application types to include. 69 | #[arg( 70 | long = "app-type", 71 | // Allow `--properties=prop1,prop2,etc` 72 | value_delimiter = ',', 73 | value_enum 74 | )] 75 | app_types: Vec, 76 | 77 | #[clap(flatten)] 78 | debug: Debug, 79 | } 80 | 81 | /// CLI arguments used by commands that work on a single app instance (i.e. `default` 82 | /// and `inspect`, but not `list`). 83 | #[derive(Debug, Clone)] 84 | #[derive(Parser)] 85 | struct SingleOutputOpts { 86 | /// If specified, the value of this property will be written without any 87 | /// trailing newline. 88 | /// 89 | /// This is useful when using `wolfram-app-discovery` to initialize the 90 | /// value of variables in shell scripts or build scripts (e.g. CMake). 91 | #[arg(long, value_name = "PROPERTY", conflicts_with_all = ["format", "properties", "all_properties"])] 92 | raw_value: Option, 93 | 94 | #[clap(flatten)] 95 | output_opts: OutputOpts, 96 | } 97 | 98 | /// CLI arguments that affect the content and format of the output. 99 | #[derive(Debug, Clone)] 100 | #[derive(Parser)] 101 | struct OutputOpts { 102 | /// Properties to output. 103 | #[arg( 104 | long = "property", 105 | alias = "properties", 106 | value_enum, 107 | // Allow `--properties=prop1,prop2,etc` 108 | value_delimiter = ',', 109 | default_values = ["app-type", "wolfram-version", "app-directory"] 110 | )] 111 | properties: Vec, 112 | 113 | /// If set, all available properties will be printed. 114 | #[arg(long, conflicts_with = "properties")] 115 | all_properties: bool, 116 | 117 | #[arg(long, value_enum, default_value = "text")] 118 | format: OutputFormat, 119 | } 120 | 121 | /// The format to use when writing output. 122 | #[derive(Debug, Clone)] 123 | #[derive(clap::ValueEnum)] 124 | enum OutputFormat { 125 | Text, 126 | CSV, 127 | } 128 | 129 | #[derive(Debug, Clone)] 130 | #[derive(Parser)] 131 | struct Debug { 132 | /// Whether to print application information in the verbose Debug format. 133 | #[arg(long)] 134 | debug: bool, 135 | } 136 | 137 | //====================================== 138 | // main() 139 | //====================================== 140 | 141 | fn main() -> Result<(), wad::Error> { 142 | let Args { command } = Args::parse(); 143 | 144 | match command { 145 | Command::Default { discovery, output } => default(discovery, output), 146 | Command::List { discovery, output } => list(discovery, output), 147 | Command::Inspect { 148 | app_dir, 149 | opts, 150 | debug, 151 | } => inspect(app_dir, &opts, debug), 152 | Command::PrintAllHelp { markdown } => { 153 | // This is a required argument for the time being. 154 | assert!(markdown); 155 | 156 | let () = clap_markdown::print_help_markdown::(); 157 | 158 | Ok(()) 159 | }, 160 | } 161 | } 162 | 163 | //====================================== 164 | // Subcommand entrypoints 165 | //====================================== 166 | 167 | fn default( 168 | discovery: DiscoveryOpts, 169 | single_output: SingleOutputOpts, 170 | ) -> Result<(), wad::Error> { 171 | let DiscoveryOpts { app_types, debug } = discovery; 172 | 173 | let filter = make_filter(app_types); 174 | 175 | let app = WolframApp::try_default_with_filter(&filter)?; 176 | 177 | print_single_app(&app, &single_output, debug)?; 178 | 179 | Ok(()) 180 | } 181 | 182 | fn list(discovery: DiscoveryOpts, output: OutputOpts) -> Result<(), wad::Error> { 183 | let DiscoveryOpts { app_types, debug } = discovery; 184 | 185 | let filter = make_filter(app_types); 186 | 187 | let OutputOpts { 188 | format, 189 | properties, 190 | all_properties, 191 | } = &output; 192 | 193 | let apps: Vec = wad::discover_with_filter(&filter); 194 | 195 | let properties: &[Property] = match all_properties { 196 | true => Property::variants(), 197 | false => properties, 198 | }; 199 | 200 | match format { 201 | OutputFormat::Text => { 202 | for (index, app) in apps.iter().enumerate() { 203 | println!("\nWolfram App #{}:\n", index); 204 | print_app_info(app, &output, debug.debug)?; 205 | } 206 | }, 207 | OutputFormat::CSV => { 208 | let mut stdout = std::io::stdout(); 209 | 210 | output::write_csv_header(&mut stdout, properties) 211 | .expect("error formatting CSV header"); 212 | 213 | for app in &apps { 214 | output::write_csv_row(&mut stdout, app, properties) 215 | .expect("error formatting CSV row"); 216 | } 217 | }, 218 | } 219 | 220 | 221 | Ok(()) 222 | } 223 | 224 | fn inspect( 225 | location: PathBuf, 226 | opts: &SingleOutputOpts, 227 | debug: Debug, 228 | ) -> Result<(), wad::Error> { 229 | let app = WolframApp::from_app_directory(location)?; 230 | 231 | print_single_app(&app, opts, debug) 232 | } 233 | 234 | //====================================== 235 | // Utility functions 236 | //====================================== 237 | 238 | fn print_single_app( 239 | app: &WolframApp, 240 | opts: &SingleOutputOpts, 241 | debug: Debug, 242 | ) -> Result<(), wad::Error> { 243 | let SingleOutputOpts { 244 | raw_value, 245 | output_opts, 246 | } = opts; 247 | 248 | if let Some(prop) = raw_value { 249 | // NOTE: Use print! instead of println! to avoid printing a newline, 250 | // which would require the user to remove the newline in some 251 | // use-cases. 252 | print!("{}", PropertyValue(&app, prop.clone())); 253 | 254 | return Ok(()); 255 | } 256 | 257 | print_app_info(app, output_opts, debug.debug) 258 | } 259 | 260 | fn print_app_info( 261 | app: &WolframApp, 262 | opts: &OutputOpts, 263 | debug: bool, 264 | ) -> Result<(), wad::Error> { 265 | let OutputOpts { 266 | format, 267 | properties, 268 | all_properties, 269 | } = opts; 270 | 271 | if debug { 272 | println!("{:#?}", app); 273 | return Ok(()); 274 | } 275 | 276 | let properties: &[Property] = match all_properties { 277 | true => Property::variants(), 278 | false => properties, 279 | }; 280 | 281 | match format { 282 | OutputFormat::Text => { 283 | for prop in properties { 284 | let value = PropertyValue(app, prop.clone()); 285 | 286 | let name = format!("{prop}:"); 287 | 288 | println!("{name: { 292 | let mut stdout = std::io::stdout(); 293 | 294 | output::write_csv_header(&mut stdout, properties) 295 | .expect("error formatting CSV header"); 296 | output::write_csv_row(&mut stdout, app, properties) 297 | .expect("error formatting CSV row"); 298 | }, 299 | } 300 | 301 | Ok(()) 302 | } 303 | 304 | fn make_filter(app_types: Vec) -> Filter { 305 | let app_types = if app_types.is_empty() { 306 | None 307 | } else { 308 | Some(app_types) 309 | }; 310 | 311 | Filter { app_types } 312 | } 313 | -------------------------------------------------------------------------------- /src/os/macos/cf_exts.rs: -------------------------------------------------------------------------------- 1 | //! Extensions to the [`core_foundation`] crate. 2 | //! 3 | //! At attempt should be made to upstream an improved version of these back into 4 | //! `core_foundation` at some point. 5 | 6 | // TODO: Remove this if upstreaming this code into core-foundation. 7 | #![allow(dead_code)] 8 | 9 | use std::{ 10 | ffi::{CStr, CString}, 11 | path::Path, 12 | }; 13 | 14 | use cf::url::kCFURLPOSIXPathStyle; 15 | use core_foundation::{ 16 | self as cf, 17 | array::CFArrayRef, 18 | base::{CFIndex, CFTypeRef}, 19 | bundle::CFBundleRef, 20 | error::CFErrorRef, 21 | string::{ 22 | kCFStringEncodingUTF8, CFStringCreateWithCString, CFStringEncoding, 23 | CFStringGetBytes, CFStringGetCStringPtr, CFStringGetLength, CFStringRef, 24 | }, 25 | url::{ 26 | CFURLCopyAbsoluteURL, CFURLCopyFileSystemPath, CFURLCopyPath, 27 | CFURLCreateWithFileSystemPath, CFURLGetFileSystemRepresentation, CFURLGetString, 28 | CFURLRef, 29 | }, 30 | }; 31 | 32 | //====================================== 33 | // Begin CoreFoundation bindings interlude 34 | //====================================== 35 | 36 | extern "C" { 37 | pub fn CFBundleGetIdentifier(bundle: CFBundleRef) -> CFStringRef; 38 | 39 | fn CFBundleGetValueForInfoDictionaryKey( 40 | bundle: CFBundleRef, 41 | key: CFStringRef, 42 | ) -> CFTypeRef; 43 | 44 | pub fn LSCopyApplicationURLsForBundleIdentifier( 45 | inBundleIdentifier: CFStringRef, 46 | outError: *mut CFErrorRef, 47 | ) -> CFArrayRef; 48 | 49 | pub fn CFStringGetMaximumSizeForEncoding( 50 | length: CFIndex, 51 | encoding: CFStringEncoding, 52 | ) -> CFIndex; 53 | } 54 | 55 | pub unsafe fn get_cf_string(cf_str: CFStringRef) -> Option { 56 | // First try to efficiently get a pointer to the C string data. 57 | // If the `cf_str` is not stored as a C string, this will fail. 58 | let c_str = CFStringGetCStringPtr(cf_str, kCFStringEncodingUTF8); 59 | if !c_str.is_null() { 60 | let id = CStr::from_ptr(c_str); 61 | // TODO: Instead of returning None here if this isn't valid UTF, continue to 62 | // doing the conversion below. 63 | let id_str = id.to_str().ok()?; 64 | 65 | return Some(id_str.to_owned()); 66 | } 67 | 68 | //---------------------------------------- 69 | // Fall back to copying the C string data. 70 | // First determine the maximum buffer size we could need. 71 | //---------------------------------------- 72 | 73 | // Number (in terms of UTF-16 code pairs) of Unicode characters in the string. 74 | let string_char_length: CFIndex = CFStringGetLength(cf_str); 75 | 76 | // Maximum number of bytes necessary to store a unicode string with the specified 77 | // number of characters in encoded UTF-8 format. 78 | let buffer_max_cf_size: CFIndex = 79 | CFStringGetMaximumSizeForEncoding(string_char_length, kCFStringEncodingUTF8); 80 | let buffer_max_size = usize::try_from(buffer_max_cf_size) 81 | .expect("string maximum buffer length overflows usize"); 82 | 83 | let mut buffer = Vec::with_capacity(buffer_max_size); 84 | 85 | //------------------------------------------- 86 | // Copy the string contents, encoded as UTF-8 87 | //------------------------------------------- 88 | 89 | // TODO: Use CFStringGetBytes() here instead, to avoid the extra allocation to convert 90 | // from CString -> String. Instead we could use String::from_vec(). 91 | let mut used_buffer_len: CFIndex = 0; 92 | let converted_char_length = CFStringGetBytes( 93 | cf_str, 94 | cf::base::CFRange::init(0, string_char_length), 95 | kCFStringEncodingUTF8, 96 | 0, // Don't lossily encode bytes, fail. 97 | false as u8, // Don't use an "external representation" (containing byte order marks). 98 | buffer.as_mut_ptr(), 99 | buffer_max_cf_size, 100 | &mut used_buffer_len, 101 | ); 102 | 103 | if converted_char_length != string_char_length { 104 | return None; 105 | } 106 | 107 | let used_buffer_len: usize = usize::try_from(used_buffer_len) 108 | .expect("CFStringGetBytes() used buffer length overflows usize"); 109 | 110 | // Only this many bytes will have been initialized, so this is the length. 111 | buffer.set_len(used_buffer_len); 112 | 113 | // TODO: `buffer.shrink_to_fit()`? Perhaps mention this as something the caller can 114 | // optionally do if they want to conserve memory. 115 | 116 | // TODO: Panic if this fails? 117 | String::from_utf8(buffer).ok() 118 | } 119 | 120 | /// # Panics 121 | /// 122 | /// This function will panic if the underlying call to `CFStringCreateWithCString()` 123 | /// fails. 124 | pub fn cf_string_from_cstr(cstr: &CStr) -> CFStringRef { 125 | let cf_string: CFStringRef = unsafe { 126 | // Use the default allocator. 127 | let allocator = std::ptr::null(); 128 | 129 | CFStringCreateWithCString(allocator, cstr.as_ptr(), kCFStringEncodingUTF8) 130 | }; 131 | 132 | if cf_string.is_null() { 133 | panic!("unable to create CFStringRef from &CStr") 134 | } 135 | 136 | cf_string 137 | } 138 | 139 | pub fn cf_string_from_str(str: &str) -> CFStringRef { 140 | let cstring = CString::new(str).expect("unable to create CString from &str"); 141 | cf_string_from_cstr(&cstring) 142 | } 143 | 144 | //-------------------------------------- 145 | // CFBundle 146 | //-------------------------------------- 147 | 148 | pub unsafe fn bundle_identifier(bundle: CFBundleRef) -> Option { 149 | let id_cf_str: CFStringRef = CFBundleGetIdentifier(bundle); 150 | 151 | if id_cf_str.is_null() { 152 | return None; 153 | } 154 | 155 | get_cf_string(id_cf_str) 156 | } 157 | 158 | /// *CoreFoundation API Documentation*: 159 | /// [`CFBundleGetValueForInfoDictionaryKey`](https://developer.apple.com/documentation/corefoundation/1537102-cfbundlegetvalueforinfodictionar?language=objc) 160 | pub unsafe fn bundle_get_value_for_info_dictionary_key( 161 | bundle: CFBundleRef, 162 | key: &str, 163 | ) -> Option { 164 | let key_cstring = CString::new(key).expect(""); 165 | 166 | let key_cfstring = cf_string_from_cstr(&key_cstring); 167 | 168 | let value: CFTypeRef = CFBundleGetValueForInfoDictionaryKey(bundle, key_cfstring); 169 | 170 | // println!("value type id: {}", cf::base::CFGetTypeID(value)); 171 | // println!( 172 | // "string type id: {}", 173 | // cf::base::CFGetTypeID(cf_string_from_str("hello") as *const _) 174 | // ); 175 | 176 | // FIXME: Assert that `value`'s dynamic type is actually CFStringRef. 177 | let value: CFStringRef = value as CFStringRef; 178 | 179 | if !value.is_null() { 180 | let name: String = match get_cf_string(value) { 181 | Some(name) => name, 182 | None => panic!( 183 | "CFBundleRef info dictionary value for key '{}' was invalid", 184 | key 185 | ), 186 | }; 187 | return Some(name); 188 | } else { 189 | None 190 | } 191 | } 192 | 193 | //-------------------------------------- 194 | // CFURL 195 | //-------------------------------------- 196 | 197 | pub unsafe fn url_absolute_url(url: CFURLRef) -> CFURLRef { 198 | CFURLCopyAbsoluteURL(url) 199 | } 200 | 201 | /// *CoreFoundation API Documentation*: 202 | /// [`CFURLCopyPath`](https://developer.apple.com/documentation/corefoundation/1541982-cfurlcopypath?language=objc) 203 | pub unsafe fn url_path(url: CFURLRef) -> String { 204 | get_cf_string(CFURLCopyPath(url)).expect("CFURLRef path does not exist or is invalid") 205 | } 206 | 207 | pub unsafe fn url_file_system_path(url: CFURLRef) -> String { 208 | get_cf_string(CFURLCopyFileSystemPath(url, kCFURLPOSIXPathStyle)) 209 | .expect("CFURLRef file system path does not exist or is invalid") 210 | } 211 | 212 | /// *CoreFoundation API Documentation*: 213 | /// [`CFURLGetFileSystemRepresentation`](https://developer.apple.com/documentation/corefoundation/1541515-cfurlgetfilesystemrepresentation?changes=_4&language=objc) 214 | pub unsafe fn url_get_file_system_representation From<&'s str>>( 215 | url: CFURLRef, 216 | ) -> Option { 217 | const SIZE: usize = 1024; 218 | 219 | let mut buffer: [u8; SIZE] = [0; SIZE]; 220 | 221 | let was_successful: bool = CFURLGetFileSystemRepresentation( 222 | url, 223 | true as u8, 224 | buffer.as_mut_ptr(), 225 | (SIZE - 1) as isize, 226 | ) > 0; 227 | 228 | if !was_successful { 229 | return None; 230 | } 231 | 232 | let cstr = CStr::from_ptr(buffer.as_ptr() as *const i8); 233 | 234 | // TODO: CFURLGetFileSystemRepresentation doesn't state exactly what encoding the 235 | // output buffer will have, so it's unclear if this can be expected to work in 236 | // all cases. 237 | let str = cstr 238 | .to_str() 239 | .expect("CFURLRef file system representation was not valid UTF-8"); 240 | 241 | Some(T::from(str)) 242 | } 243 | 244 | pub unsafe fn url_get_string(url: CFURLRef) -> CFStringRef { 245 | CFURLGetString(url) 246 | } 247 | 248 | pub fn url_create_with_file_system_path(path: &Path) -> Option { 249 | let path_str: &str = path.to_str()?; 250 | 251 | let path_cfstring: CFStringRef = cf_string_from_str(path_str); 252 | 253 | let url: CFURLRef = unsafe { 254 | // Use the default allocator. 255 | let allocator = std::ptr::null(); 256 | 257 | // TODO: If `path` is not absolute, this function will resolve it relative to 258 | // the current working directory. Is that the behavior we want? Should we 259 | // panic if `!path.is_absolute()`? 260 | CFURLCreateWithFileSystemPath( 261 | allocator, 262 | path_cfstring, 263 | cf::url::kCFURLPOSIXPathStyle, 264 | path.is_dir() as u8, 265 | ) 266 | }; 267 | 268 | if url.is_null() { 269 | None 270 | } else { 271 | Some(url) 272 | } 273 | } 274 | 275 | //====================================== 276 | // End CoreFoundation bindings interlude 277 | //====================================== 278 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS -------------------------------------------------------------------------------- /src/build_scripts.rs: -------------------------------------------------------------------------------- 1 | //! Functions for querying the locations of Wolfram development SDK resources, 2 | //! for use in build scripts. 3 | //! 4 | //! The functions in this module are designed to be used from Cargo build scripts 5 | //! via the Rust API. 6 | // TODO: or from the command-line via the `wolfram-app-discovery config` subcommand. 7 | //! 8 | //! Each function will first check a corresponding environment 9 | //! variable before falling back to look up the path in the optionally specified 10 | //! [`WolframApp`]. 11 | //! 12 | //! See Also: 13 | //! 14 | //! * [`crate::config::set_print_cargo_build_script_directives()`] 15 | 16 | use std::path::PathBuf; 17 | 18 | use log::{info, trace}; 19 | 20 | #[allow(deprecated)] 21 | use crate::{ 22 | config::{ 23 | self, 24 | env_vars::{ 25 | WOLFRAM_C_INCLUDES, WOLFRAM_LIBRARY_LINK_C_INCLUDES_DIRECTORY, 26 | WSTP_COMPILER_ADDITIONS, WSTP_COMPILER_ADDITIONS_DIRECTORY, 27 | }, 28 | }, 29 | os::OperatingSystem, 30 | Error, WolframApp, 31 | }; 32 | 33 | //====================================== 34 | // API 35 | //====================================== 36 | 37 | /// Discovered resource that can come from either a configuration environment 38 | /// variable or from a [`WolframApp`] installation. 39 | /// 40 | /// Use [`Discovery::into_path_buf()`] to get the underlying file system path. 41 | #[derive(Clone, Debug)] 42 | #[cfg_attr(test, derive(PartialEq))] 43 | pub enum Discovery { 44 | /// Location came from the [`WolframApp`] passed to the lookup function. 45 | App(PathBuf), 46 | 47 | /// Location derived from an environment variable. 48 | Env { 49 | /// The environment variable that was read from. 50 | /// 51 | /// This will be a value from [`crate::config::env_vars`]. 52 | variable: &'static str, 53 | 54 | /// The path that was derived from `variable`. 55 | /// 56 | /// This value is not always equal to the value of the environment 57 | /// variable, path components may have been added or removed. 58 | path: PathBuf, 59 | }, 60 | } 61 | 62 | impl Discovery { 63 | /// Converts `self` into a [`PathBuf`]. 64 | pub fn into_path_buf(self) -> PathBuf { 65 | match self { 66 | Discovery::App(path) => path, 67 | Discovery::Env { variable: _, path } => path, 68 | } 69 | } 70 | } 71 | 72 | /// Discover the directory containing the 73 | /// [Wolfram *LibraryLink*](https://reference.wolfram.com/language/guide/LibraryLink.html) 74 | /// C header files. 75 | /// 76 | /// The following locations are searched in order: 77 | /// 78 | /// 1. The [`WOLFRAM_LIBRARY_LINK_C_INCLUDES_DIRECTORY`] environment variable 79 | /// 2. *Deprecated:* The [`WOLFRAM_C_INCLUDES`] environment variable 80 | /// 3. If `app` contains a value, [`WolframApp::library_link_c_includes_directory()`]. 81 | /// 82 | /// The standard set of *LibraryLink* C header files includes: 83 | /// 84 | /// * WolframLibrary.h 85 | /// * WolframSparseLibrary.h 86 | /// * WolframImageLibrary.h 87 | /// * WolframNumericArrayLibrary.h 88 | /// 89 | /// *Note: The [wolfram-library-link](https://crates.io/crates/wolfram-library-link) crate 90 | /// provides safe Rust bindings to the Wolfram *LibraryLink* interface.* 91 | pub fn library_link_c_includes_directory( 92 | app: Option<&WolframApp>, 93 | ) -> Result { 94 | trace!("start library_link_c_includes_directory(app={app:?})"); 95 | 96 | if let Some(resource) = 97 | get_env_resource(WOLFRAM_LIBRARY_LINK_C_INCLUDES_DIRECTORY, false) 98 | { 99 | info!("discovered in env: {resource:?}"); 100 | return Ok(resource); 101 | } 102 | 103 | if let Some(resource) = get_env_resource(WOLFRAM_C_INCLUDES, true) { 104 | info!("discovered in env: {resource:?}"); 105 | return Ok(resource); 106 | } 107 | 108 | if let Some(app) = app { 109 | let path = app.library_link_c_includes_directory()?; 110 | 111 | #[rustfmt::skip] 112 | info!("discovered in app ({:?}): {}", app.installation_directory().display(), path.display()); 113 | 114 | return Ok(Discovery::App(path)); 115 | } 116 | 117 | let err = Error::undiscoverable( 118 | "LibraryLink C includes directory".to_owned(), 119 | Some(WOLFRAM_LIBRARY_LINK_C_INCLUDES_DIRECTORY), 120 | ); 121 | 122 | info!("discovery failed: {err}"); 123 | 124 | Err(err) 125 | } 126 | 127 | //====================================== 128 | // WSTP 129 | //====================================== 130 | 131 | /// Discover the CompilerAdditions subdirectory of the WSTP SDK. 132 | /// 133 | /// The following locations are searched in order: 134 | /// 135 | /// 1. The [`WSTP_COMPILER_ADDITIONS_DIRECTORY`] environment variable. 136 | /// 2. *Deprecated:* The [`WSTP_COMPILER_ADDITIONS`] environment variable. 137 | /// 3. If `app` contains a value, [`WolframApp::wstp_compiler_additions_directory()`]. 138 | /// 139 | /// *Note: The [wstp](https://crates.io/crates/wstp) crate provides safe Rust bindings 140 | /// to WSTP.* 141 | /// 142 | /// # Alternatives 143 | /// 144 | /// When trying to get the path to the 145 | /// [`wstp.h`](https://reference.wolfram.com/language/ref/file/wstp.h.html) 146 | /// header file, or the WSTP static or dynamic library file, prefer to use 147 | /// the following dedicated functions: 148 | /// 149 | /// * [`wstp_c_header_path()`] 150 | pub fn wstp_compiler_additions_directory( 151 | app: Option<&WolframApp>, 152 | ) -> Result { 153 | trace!("start wstp_compiler_additions_directory(app={app:?})"); 154 | 155 | if let Some(resource) = get_env_resource(WSTP_COMPILER_ADDITIONS_DIRECTORY, false) { 156 | info!("discovered in env: {resource:?}"); 157 | return Ok(resource); 158 | } 159 | 160 | #[allow(deprecated)] 161 | if let Some(resource) = get_env_resource(WSTP_COMPILER_ADDITIONS, true) { 162 | info!("discovered in env: {resource:?}"); 163 | return Ok(resource); 164 | } 165 | 166 | if let Some(app) = app { 167 | let path = app.target_wstp_sdk()?.wstp_compiler_additions_directory(); 168 | 169 | #[rustfmt::skip] 170 | info!("discovered in app ({:?}): {}", app.installation_directory().display(), path.display()); 171 | 172 | return Ok(Discovery::App(path)); 173 | } 174 | 175 | let err = Error::undiscoverable( 176 | "WSTP CompilerAdditions directory".to_owned(), 177 | Some(WSTP_COMPILER_ADDITIONS_DIRECTORY), 178 | ); 179 | 180 | info!("discovery failed: {err}"); 181 | 182 | Err(err) 183 | } 184 | 185 | /// Discover the 186 | /// [`wstp.h`](https://reference.wolfram.com/language/ref/file/wstp.h.html) 187 | /// header file. 188 | /// 189 | /// The following locations are searched in order: 190 | /// 191 | /// 1. Location derived from [`wstp_compiler_additions_directory()`]. 192 | /// 2. If `app` contains a value, [`WolframApp::wstp_c_header_path()`]. 193 | /// 194 | /// *Note: The [wstp](https://crates.io/crates/wstp) crate provides safe Rust bindings 195 | /// to WSTP.* 196 | pub fn wstp_c_header_path(app: Option<&WolframApp>) -> Result { 197 | trace!("start wstp_c_header_path(app={app:?})"); 198 | 199 | match wstp_compiler_additions_directory(app)? { 200 | // If this location came from `app`, unwrap the app and return 201 | // app.wstp_c_header_path() directly. 202 | Discovery::App(_) => { 203 | let app = app.unwrap(); 204 | let path = app.target_wstp_sdk()?.wstp_c_header_path(); 205 | #[rustfmt::skip] 206 | info!("discovered in app ({:?}): {}", app.installation_directory().display(), path.display()); 207 | return Ok(Discovery::App(path)); 208 | }, 209 | Discovery::Env { variable, path } => { 210 | let wstp_h = path.join("wstp.h"); 211 | 212 | if !wstp_h.is_file() { 213 | let err = Error::unexpected_env_layout( 214 | "wstp.h C header file", 215 | variable, 216 | path, 217 | wstp_h, 218 | ); 219 | info!("discovery failed: {err}"); 220 | return Err(err); 221 | } 222 | 223 | let discovery = Discovery::Env { 224 | variable, 225 | path: wstp_h, 226 | }; 227 | info!("discovered in env: {discovery:?}"); 228 | return Ok(discovery); 229 | }, 230 | } 231 | } 232 | 233 | /// Discover the 234 | /// [WSTP](https://reference.wolfram.com/language/guide/WSTPAPI.html) 235 | /// static library. 236 | /// 237 | /// 1. Location derived from [`wstp_compiler_additions_directory()`]. 238 | /// 2. If `app` contains a value, [`WolframApp::wstp_static_library_path()`]. 239 | /// 240 | /// *Note: The [wstp](https://crates.io/crates/wstp) crate provides safe Rust bindings 241 | /// to WSTP.* 242 | pub fn wstp_static_library_path(app: Option<&WolframApp>) -> Result { 243 | trace!("start wstp_static_library_path(app={app:?})"); 244 | 245 | let static_archive_name = 246 | wstp_static_library_file_name(OperatingSystem::target_os())?; 247 | 248 | match wstp_compiler_additions_directory(app)? { 249 | // If this location came from `app`, unwrap the app and return 250 | // app.wstp_c_header_path() directly. 251 | Discovery::App(_) => { 252 | let app = app.unwrap(); 253 | let path = app.target_wstp_sdk()?.wstp_static_library_path(); 254 | #[rustfmt::skip] 255 | info!("discovered in app ({:?}): {}", app.installation_directory().display(), path.display()); 256 | return Ok(Discovery::App(path)); 257 | }, 258 | Discovery::Env { variable, path } => { 259 | let static_lib_path = path.join(static_archive_name); 260 | 261 | if !static_lib_path.is_file() { 262 | let err = Error::unexpected_env_layout( 263 | "WSTP static library file", 264 | variable, 265 | path, 266 | static_lib_path, 267 | ) 268 | .into(); 269 | info!("discovery failed: {err}"); 270 | return Err(err); 271 | } 272 | 273 | let discovery = Discovery::Env { 274 | variable, 275 | path: static_lib_path, 276 | }; 277 | info!("discovered in env: {discovery:?}"); 278 | return Ok(discovery); 279 | }, 280 | } 281 | } 282 | 283 | //====================================== 284 | // Helpers 285 | //====================================== 286 | 287 | fn get_env_resource(var: &'static str, deprecated: bool) -> Option { 288 | if let Some(path) = config::get_env_var(var) { 289 | if deprecated { 290 | config::print_deprecated_env_var_warning(var, &path); 291 | } 292 | 293 | return Some(Discovery::Env { 294 | variable: var, 295 | path: PathBuf::from(path), 296 | }); 297 | } 298 | 299 | None 300 | } 301 | 302 | // Note: In theory, this can also vary based on the WSTP library 'interface' version 303 | // (currently v4). But that has not changed in a long time. If the interface 304 | // version does change, this logic should be updated to also check the WL 305 | // version. 306 | pub(crate) fn wstp_static_library_file_name( 307 | os: OperatingSystem, 308 | ) -> Result<&'static str, Error> { 309 | let static_archive_name = match os { 310 | OperatingSystem::MacOS => "libWSTPi4.a", 311 | OperatingSystem::Windows => "wstp64i4s.lib", 312 | OperatingSystem::Linux => "libWSTP64i4.a", 313 | OperatingSystem::Other => { 314 | return Err(Error::platform_unsupported( 315 | "wstp_static_library_file_name()", 316 | )); 317 | }, 318 | }; 319 | 320 | Ok(static_archive_name) 321 | } 322 | 323 | //====================================== 324 | // Tests 325 | //====================================== 326 | 327 | #[test] 328 | fn test_wstp_c_header_path() { 329 | use crate::ErrorKind; 330 | 331 | //======================== 332 | 333 | std::env::remove_var("WSTP_COMPILER_ADDITIONS_DIRECTORY"); 334 | 335 | assert_eq!( 336 | wstp_c_header_path(None), 337 | Err(Error(ErrorKind::Undiscoverable { 338 | resource: "WSTP CompilerAdditions directory".into(), 339 | environment_variable: Some("WSTP_COMPILER_ADDITIONS_DIRECTORY".into()) 340 | })) 341 | ); 342 | 343 | //======================== 344 | 345 | std::env::set_var("WSTP_COMPILER_ADDITIONS_DIRECTORY", std::env::temp_dir()); 346 | 347 | assert_eq!( 348 | wstp_c_header_path(None), 349 | Err(Error(ErrorKind::UnexpectedEnvironmentValueLayout { 350 | resource_name: "wstp.h C header file".into(), 351 | env_var: "WSTP_COMPILER_ADDITIONS_DIRECTORY".into(), 352 | env_value: PathBuf::from(std::env::temp_dir().to_str().unwrap()), 353 | derived_path: std::env::temp_dir().join("wstp.h") 354 | })) 355 | ); 356 | 357 | //======================== 358 | 359 | // Set WSTP_COMPILER_ADDITIONS_DIRECTORY to a valid value by assuming we 360 | // can discovery an installed WolframApp that has one. 361 | let compiler_additions_dir = WolframApp::try_default() 362 | .unwrap() 363 | .target_wstp_sdk() 364 | .unwrap() 365 | .wstp_compiler_additions_directory(); 366 | std::env::set_var("WSTP_COMPILER_ADDITIONS_DIRECTORY", &compiler_additions_dir); 367 | 368 | assert_eq!( 369 | wstp_c_header_path(None), 370 | Ok(Discovery::Env { 371 | variable: "WSTP_COMPILER_ADDITIONS_DIRECTORY", 372 | path: compiler_additions_dir.join("wstp.h") 373 | }) 374 | ); 375 | } 376 | -------------------------------------------------------------------------------- /docs/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | 10 | ## [0.4.9] — 2025-02-23 11 | 12 | ### Added 13 | 14 | * Added new `WolframAppType::WolframApp` variant to cover the new unified 15 | Wolfram app used in versions 14.1+. Also support recognizing the new 16 | 'com.wolfram.WolframApp' bundle identifier for this app. ([#70]) 17 | 18 | ## [0.4.8] — 2023-06-14 19 | 20 | ### Fixed 21 | 22 | * Fix issue with WSTP static library name being determined based on compile time 23 | Rust target OS instead of System ID specified by the caller at runtime. ([#66]) 24 | 25 | This caused `WstpSdk::try_from_directory()` and 26 | `WstpSdk::try_from_directory_with_system_id()` to look for the WSTP static 27 | library in the wrong place when doing cross-compilation operations. 28 | 29 | 30 | 31 | ## [0.4.7] — 2023-06-12 32 | 33 | ### Added 34 | 35 | * Added new 36 | [`SystemID`](https://docs.rs/wolfram-app-discovery/0.4.7/wolfram_app_discovery/enum.SystemID.html) 37 | enum. ([#63]) 38 | 39 | This enum has a variant for each value of Wolfram 40 | [`$SystemID`](https://reference.wolfram.com/language/ref/$System) 41 | that is in common use. 42 | 43 | - `SystemID::current_rust_target()` can be used to get the Wolfram system ID 44 | of the current Rust target. 45 | - `SystemID::try_from_rust_target()` takes a 46 | [Rust target triple](https://doc.rust-lang.org/nightly/rustc/platform-support.html) 47 | and returns the corresponding Wolfram system ID. 48 | 49 | * Added new 50 | [`WstpSdk`](https://docs.rs/wolfram-app-discovery/0.4.7/wolfram_app_discovery/struct.WstpSdk.html) 51 | struct. ([#63]) 52 | 53 | * Added new 54 | [`WolframApp::wstp_sdks()`](https://docs.rs/wolfram-app-discovery/0.4.7/wolfram_app_discovery/struct.WolframApp.html#method.wstp_sdks) 55 | method to enumerate each per-SystemID copy of the WSTP SDK that is bundled within a particular Wolfram app installation. ([#63]) 56 | 57 | * Added new 58 | [`WolframApp::target_wstp_sdk()`](https://docs.rs/wolfram-app-discovery/0.4.7/wolfram_app_discovery/struct.WolframApp.html#method.target_wstp_sdk) 59 | method to get the WSTP SDK appropriate for the current Rust target. ([#63]) 60 | 61 | ### Deprecated 62 | 63 | The following items have been deprecated and will be removed in a future version 64 | of `wolfram-app-discovery`: 65 | 66 | * `wolfram_app_discovery::target_system_id()` 67 | * `wolfram_app_discovery::system_id_from_target()` 68 | * `WolframApp::wstp_c_header_path()` 69 | * `WolframApp::wstp_static_library_path()` 70 | * `WolframApp::wstp_compiler_additions_directory()` 71 | 72 | 73 | 74 | ## [0.4.6] — 2023-06-11 75 | 76 | ### Added 77 | 78 | * Added new `WolframVersion::new()` method. ([#60]) 79 | 80 | 81 | 82 | ## [0.4.5] — 2023-05-19 83 | 84 | ### Fixed 85 | 86 | * Fixed `wstp_c_header_path()` and `wstp_static_library_path()` in the 87 | `wolfram_app_discovery::build_scripts` module returning the path to the 88 | CompilerAdditions directory instead of, respectively, the path to wstp.h and 89 | the WSTP static library. ([#58]) 90 | 91 | 92 | 93 | ## [0.4.4] — 2023-03-27 94 | 95 | ### Fixed 96 | 97 | * Support discovery of older Mathematica versions on Linux. ([#52]) 98 | 99 | This fixes one problem described in [#51]. 100 | 101 | 102 | 103 | ## [0.4.3] — 2023-02-03 104 | 105 | ### Added 106 | 107 | * Added logging output along discovery success and error code paths using the 108 | [`log`](https://crates.io/crates/log) crate logging facade. ([#48]) 109 | 110 | Programs making use of wolfram-app-discovery can enable logging in their 111 | application by initializing a logging implemenation compatible with the `log` 112 | crate. [`env_logger`](https://crates.io/crates/env_logger) is a common choice 113 | for logging that can be customized via the `RUST_LOG` environment variable. 114 | 115 | **Logging in Rust build scripts** 116 | 117 | Rust crate `build.rs` scripts using wolfram-app-discovery are strongly 118 | encouraged to use `env_logger` to make debugging build script behavior easier. 119 | 120 | Adding logging to a `build.rs` script can be done by adding a dependency on 121 | `env_logger` to Cargo.toml: 122 | 123 | ```toml 124 | [build-dependencies] 125 | env_logger = "0.10.0" 126 | ``` 127 | 128 | and initializing `env_logger` at the beginning of `build.rs/main()`: 129 | 130 | ```rust 131 | fn main() { 132 | env_logger::init(); 133 | 134 | // ... 135 | } 136 | ``` 137 | 138 | Logging output can be enabled in subsequent crate builds by executing: 139 | 140 | ```shell 141 | $ RUST_LOG=trace cargo build 142 | ``` 143 | 144 | *Note that `cargo` will suppress output printed by build scripts by default 145 | unless the build script fails with an error (which matches the expected usage 146 | of logging output: it is most useful when something goes wrong). Verbose 147 | `cargo` output (including logging) can be enabled using `cargo -vv`.* 148 | 149 | 150 | 151 | ## [0.4.2] — 2023-02-02 152 | 153 | ### Fixed 154 | 155 | * Workaround issue with Wolfram pre-release builds with app version numbers that 156 | overflow `u32` version fields. ([#46]) 157 | 158 | 159 | 160 | ## [0.4.1] — 2023-01-06 161 | 162 | ### Added 163 | 164 | * Add new 165 | [`.github/workflows/build-executables.yml`](https://github.com/WolframResearch/wolfram-app-discovery-rs/blob/v0.4.1/.github/workflows/build-executables.yml) 166 | file, which was used to retroactively build precompiled binaries for the 167 | [v0.4.0 release](https://github.com/WolframResearch/wolfram-app-discovery-rs/releases/tag/v0.4.0) 168 | of the `wolfram-app-discovery` command-line tool. ([#31], [#32], [#33]) 169 | 170 | * Improve README.md with new 'CLI Documentation' quick link and 171 | 'Installing wolfram-app-discovery' sections, and other minor link and wording 172 | changes. ([#34], [#35]) 173 | 174 | * Make major improvements to the `wolfram-app-discovery` command-line tool. ([#36], [#39]) 175 | 176 | - The following options are now supported on the `default`, `list`, and `inspect` 177 | subcommands: 178 | 179 | * `--property ` (alias: `--properties`) 180 | * `--all-properties` 181 | * `--format ` 182 | 183 | If `--format csv` is specified, the output will be written in the CSV format. 184 | 185 | If `--property` is specified, only the properties listed as an argument will be 186 | included in the output. 187 | 188 | If `--all-properties` is specified, all available properties will be included in 189 | the output. 190 | 191 | - The `default` and `inspect` subcommands now support a `--raw-value ` 192 | option, which will cause only the value of the specified property to be 193 | printed. 194 | 195 | This is useful when using `wolfram-app-discovery` as part of a 196 | compilation workflow or build script. For example: 197 | 198 | ```shell 199 | # Get the LibraryLink includes directory 200 | $ export WOLFRAM_C_INCLUDES=`wolfram-app-discovery default --raw-value library-link-c-includes-directory` 201 | 202 | # Invoke a C compiler and provide the LibraryLink headers location 203 | $ clang increment.c -I$WOLFRAM_C_INCLUDES -shared -o libincrement 204 | ``` 205 | 206 | See [`docs/CommandLineHelp.md`][CommandLineHelp.md@v0.4.1] for complete 207 | documentation on the `wolfram-app-discovery` command-line interface. 208 | 209 | * Add `/opt/Wolfram/` to list of app search locations used on Linux. ([#41]) 210 | 211 | ### Changed 212 | 213 | * Replaced custom logic with a dependency on 214 | [`clap-markdown`](https://crates.io/crates/clap-markdown), 215 | and used it to regenerate an improved 216 | [`docs/CommandLineHelp.md`][CommandLineHelp.md@v0.4.1]. ([#30], [#38]) 217 | 218 | 219 | ### Fixed 220 | 221 | * Fix spurious warnings generated on macOS when no Wolfram applications of 222 | a particular `WolframAppType` variant could be discovered. ([#37]) 223 | 224 | * Fix missing support for Linux in 225 | `wolfram_app_discovery::build_scripts::wstp_static_library_path()` ([#40]) 226 | 227 | This ought to have been fixed in [#28], but copy-pasted code meant the same 228 | fix needed to be applied in two places, and only one was fixed in #28. 229 | 230 | This was preventing the [`wstp-sys`](https://crates.io/crates/wstp-sys) crate 231 | from compiling on Linux. 232 | 233 | 234 | 235 | ## [0.4.0] — 2022-12-14 236 | 237 | ### Added 238 | 239 | * Added support for app discovery on Linux ([#28]) 240 | 241 | This address issue [#27]. 242 | 243 | [`discover()`](https://docs.rs/wolfram-app-discovery/0.4.0/wolfram_app_discovery/fn.discover.html) 244 | will now return all Wolfram apps found in the default installation location 245 | on Linux (currently just `/usr/local/Wolfram/`). 246 | 247 | [`WolframApp::from_app_directory()`](https://docs.rs/wolfram-app-discovery/0.4.0/wolfram_app_discovery/struct.WolframApp.html#method.from_app_directory) 248 | can now be used to get information on a Wolfram app installed in a non-standard 249 | location. 250 | 251 | The following `WolframApp` methods are now supported on Linux: 252 | 253 | - [`WolframApp::installation_directory()`](https://docs.rs/wolfram-app-discovery/0.4.0/wolfram_app_discovery/struct.WolframApp.html#method.installation_directory) 254 | - [`WolframApp::kernel_executable_path()`](https://docs.rs/wolfram-app-discovery/0.4.0/wolfram_app_discovery/struct.WolframApp.html#method.kernel_executable_path) 255 | - [`WolframApp::wolframscript_executable_path()`](https://docs.rs/wolfram-app-discovery/0.4.0/wolfram_app_discovery/struct.WolframApp.html#method.wolframscript_executable_path) 256 | - [`WolframApp::wstp_static_library_path()`](https://docs.rs/wolfram-app-discovery/0.4.0/wolfram_app_discovery/struct.WolframApp.html#method.wstp_static_library_path) 257 | 258 | * Added custom logic for determining app metadata, in the absence of an 259 | available standard OS-provided format or API. At the moment, this consists 260 | of parsing LICENSE.txt and the WolframKernel script for the application type 261 | and version number, respectively. 262 | 263 | This is likely more fragile than the implementation methods used on macOS and 264 | Windows, but necessary and sufficient for the time being to get discovery 265 | working for the most common use-cases. Future improvements are expected. 266 | 267 | ### Changed 268 | 269 | #### Backwards Incompatible 270 | 271 | - Changed the [`AppVersion::build_code()`] method to return `Option` 272 | (was `u32`). ([#28]) 273 | 274 | ### Fixed 275 | 276 | - Fixed an issue with platform unsupported error generated by 277 | `WolframApp::installation_directory()` incorrectly reporting that the error 278 | was in `WolframApp::from_app_directory()`. ([#28]) 279 | 280 | - Filled in an erroneously incomplete `todo!()` in the `Display` impl for 281 | `Error`. ([#28]) 282 | 283 | 284 | 285 | 286 | ## [0.3.0] – 2022-09-19 287 | 288 | ### Added 289 | 290 | * Add a new 291 | [`wolfram_library_link::build_scripts`](https://docs.rs/wolfram-app-discovery/0.3.0/wolfram_app_discovery/build_scripts/index.html) 292 | submodule. ([#25]) 293 | 294 | Functions from this module will be used by the `build.rs` scripts of the 295 | [`wstp`](https://crates.io/crates/wstp) and 296 | [`wolfram-library-link`](https://crates.io/crates/wolfram-library-link) 297 | crates. The current implementation of those scripts relies on 298 | calling methods on a `WolframApp` instance, which means that they don't work 299 | when no Wolfram applications are available, even if configuration environment 300 | variables are manually set to point at the necessary headers and libraries. 301 | 302 | - Add new [`Discovery`](https://docs.rs/wolfram-app-discovery/0.3.0/wolfram_app_discovery/build_scripts/enum.Discovery.html) type. ([#25]) 303 | 304 | ### Changed 305 | 306 | * Remove unnecessary warning about embedded Wolfram Player. ([#24]) 307 | 308 | #### Backwards Incompatible 309 | 310 | * Change `WolframApp` methods that previously would check an environment 311 | variable to check only within the app installation directory. ([#25]) 312 | 313 | The original usecase for these functions was to get the file paths of the 314 | LibraryLink and WSTP header files and compiled libraries, for use in the 315 | build.rs scripts of the `wstp` and `wolfram-library-link` crates. Because 316 | build scripts often need to be configurable to use files from non-default 317 | locations, it seemed to make sense to make the `WolframApp` methods themselves 318 | also have behavior configurable by environment variables. 319 | 320 | However, that behavior was both a bit unintuitive to explain and document (If 321 | `WolframApp` represents a specific WL installation, why would its methods 322 | ever return paths *outside* of that app?), and lacked flexibility for the 323 | build script usecase. 324 | 325 | * Move the environment variable declarations into their own 326 | [`wolfram_library_link::config::env_vars`](https://docs.rs/wolfram-app-discovery/0.3.0/wolfram_app_discovery/config/env_vars/index.html) 327 | submodule. ([#25]) 328 | 329 | * Rename `set_print_cargo_build_script_instructions()` to `set_print_cargo_build_script_directives()`. ([#25]) 330 | 331 | 332 | 333 | ## [0.2.2] – 2022-03-07 334 | 335 | ### Added 336 | 337 | * Improve crate documentation. ([#22]) 338 | 339 | - Add examples to crate root comment 340 | - Update and expand on `WolframApp::try_default()` doc comment. 341 | 342 | 343 | 344 | ## [0.2.1] – 2022-03-02 345 | 346 | ### Added 347 | 348 | * Added Windows support for `WolframApp::from_installation_directory()`. ([#20]) 349 | 350 | 351 | 352 | ## [0.2.0] – 2022-02-16 353 | 354 | ### Added 355 | 356 | * Added support for app discovery on Windows ([#17]) 357 | - Fixed the `wolfram-app-discovery` build on Windows 358 | - Add app discovery logic based on product identifier look-ups in the Windows registry. 359 | - Improve maintainability of code that branches based on the operating system. 360 | 361 | ### Changed 362 | 363 | * Improve `discover()` to return apps sorted by version number and feature set 364 | (e.g. apps that provide a notebook front end are sorted ahead of those that don't, if 365 | the version numbers are otherwise the same). ([#18]) 366 | 367 | ### Fixed 368 | 369 | * Fixed slow execution of `WolframApp::wolfram_version()` (1-3 seconds) due to 370 | launching a full Wolfram Language kernel process. ([#17]) 371 | 372 | 373 | 374 | ## [0.1.2] – 2022-02-08 375 | 376 | ### Fixed 377 | 378 | * Fix compilation failure on non-macOS platforms. ([#14]) 379 | 380 | 381 | 382 | ## [0.1.1] – 2022-02-08 383 | 384 | ### Added 385 | 386 | * Added badges for the crates.io version/link, license, and docs.rs link. ([#10]) 387 | 388 | ### Changed 389 | 390 | * Changes the README.md summary line to be consistent with the Cargo.toml `description` 391 | field. ([#10]) 392 | 393 | ### Fixed 394 | 395 | * Fix broken `target_system_id()` compilation on Linux and Windows that was preventing 396 | docs.rs from building the crate. ([#10]). 397 | 398 | 399 | 400 | ## [0.1.0] – 2022-02-08 401 | 402 | Initial release of `wolfram-app-discovery`. 403 | 404 | ### Added 405 | 406 | * `WolframApp`, which can be used to query information about installed Wolfram 407 | applications: 408 | 409 | ```rust 410 | use wolfram_app_discovery::WolframApp; 411 | 412 | let app = WolframApp::try_default() 413 | .expect("unable to locate any Wolfram applications"); 414 | 415 | // Print the $InstallationDirectory of this Wolfram Language installation: 416 | println!("$InstallationDirectory: {}", app.installation_directory().display()); 417 | ``` 418 | 419 | * `$ wolfram-app-discovery` command-line tool: 420 | 421 | ```shell 422 | $ ./wolfram-app-discovery 423 | Default Wolfram Language installation: 424 | 425 | Product: Mathematica 426 | Wolfram Language version: 13.0.0 427 | $InstallationDirectory: /Applications/Mathematica.app/Contents 428 | ``` 429 | 430 | * Semi-automatically generated [docs/CommandLineHelp.md](https://github.com/WolframResearch/wolfram-app-discovery-rs/blob/v0.1.0/docs/CommandLineHelp.md) documentation. 431 | 432 | 433 | 434 | 435 | 436 | [CommandLineHelp.md@v0.4.1]: https://github.com/WolframResearch/wolfram-app-discovery-rs/blob/v0.4.1/docs/CommandLineHelp.md 437 | 438 | 439 | 440 | [#10]: https://github.com/WolframResearch/wolfram-app-discovery-rs/pull/10 441 | [#14]: https://github.com/WolframResearch/wolfram-app-discovery-rs/pull/14 442 | [#17]: https://github.com/WolframResearch/wolfram-app-discovery-rs/pull/17 443 | [#18]: https://github.com/WolframResearch/wolfram-app-discovery-rs/pull/18 444 | [#20]: https://github.com/WolframResearch/wolfram-app-discovery-rs/pull/20 445 | 446 | 447 | [#22]: https://github.com/WolframResearch/wolfram-app-discovery-rs/pull/20 448 | 449 | 450 | [#24]: https://github.com/WolframResearch/wolfram-app-discovery-rs/pull/24 451 | [#25]: https://github.com/WolframResearch/wolfram-app-discovery-rs/pull/25 452 | 453 | 454 | [#27]: https://github.com/WolframResearch/wolfram-app-discovery-rs/issues/27 455 | [#28]: https://github.com/WolframResearch/wolfram-app-discovery-rs/pull/28 456 | 457 | 458 | [#30]: https://github.com/WolframResearch/wolfram-app-discovery-rs/pull/30 459 | [#31]: https://github.com/WolframResearch/wolfram-app-discovery-rs/pull/31 460 | [#32]: https://github.com/WolframResearch/wolfram-app-discovery-rs/pull/32 461 | [#33]: https://github.com/WolframResearch/wolfram-app-discovery-rs/pull/33 462 | [#34]: https://github.com/WolframResearch/wolfram-app-discovery-rs/pull/34 463 | [#35]: https://github.com/WolframResearch/wolfram-app-discovery-rs/pull/35 464 | [#36]: https://github.com/WolframResearch/wolfram-app-discovery-rs/pull/36 465 | [#37]: https://github.com/WolframResearch/wolfram-app-discovery-rs/pull/37 466 | [#38]: https://github.com/WolframResearch/wolfram-app-discovery-rs/pull/38 467 | [#39]: https://github.com/WolframResearch/wolfram-app-discovery-rs/pull/39 468 | [#40]: https://github.com/WolframResearch/wolfram-app-discovery-rs/pull/40 469 | [#41]: https://github.com/WolframResearch/wolfram-app-discovery-rs/pull/41 470 | 471 | 472 | [#46]: https://github.com/WolframResearch/wolfram-app-discovery-rs/pull/46 473 | 474 | 475 | [#48]: https://github.com/WolframResearch/wolfram-app-discovery-rs/pull/48 476 | 477 | 478 | [#51]: https://github.com/WolframResearch/wolfram-app-discovery-rs/issues/51 479 | [#52]: https://github.com/WolframResearch/wolfram-app-discovery-rs/pull/52 480 | 481 | 482 | [#58]: https://github.com/WolframResearch/wolfram-app-discovery-rs/pull/58 483 | 484 | 485 | [#60]: https://github.com/WolframResearch/wolfram-app-discovery-rs/pull/60 486 | 487 | 488 | [#63]: https://github.com/WolframResearch/wolfram-app-discovery-rs/pull/63 489 | 490 | 491 | [#66]: https://github.com/WolframResearch/wolfram-app-discovery-rs/pull/66 492 | 493 | 494 | [#70]: https://github.com/WolframResearch/wolfram-app-discovery-rs/pull/70 495 | 496 | 497 | 498 | [Unreleased]: https://github.com/WolframResearch/wolfram-app-discovery-rs/compare/v0.4.9...HEAD 499 | 500 | [0.4.9]: https://github.com/WolframResearch/wolfram-app-discovery-rs/compare/v0.4.8...v0.4.9 501 | [0.4.8]: https://github.com/WolframResearch/wolfram-app-discovery-rs/compare/v0.4.7...v0.4.8 502 | [0.4.7]: https://github.com/WolframResearch/wolfram-app-discovery-rs/compare/v0.4.6...v0.4.7 503 | [0.4.6]: https://github.com/WolframResearch/wolfram-app-discovery-rs/compare/v0.4.5...v0.4.6 504 | [0.4.5]: https://github.com/WolframResearch/wolfram-app-discovery-rs/compare/v0.4.4...v0.4.5 505 | [0.4.4]: https://github.com/WolframResearch/wolfram-app-discovery-rs/compare/v0.4.3...v0.4.4 506 | [0.4.3]: https://github.com/WolframResearch/wolfram-app-discovery-rs/compare/v0.4.2...v0.4.3 507 | [0.4.2]: https://github.com/WolframResearch/wolfram-app-discovery-rs/compare/v0.4.1...v0.4.2 508 | [0.4.1]: https://github.com/WolframResearch/wolfram-app-discovery-rs/compare/v0.4.0...v0.4.1 509 | [0.4.0]: https://github.com/WolframResearch/wolfram-app-discovery-rs/compare/v0.3.0...v0.4.0 510 | [0.3.0]: https://github.com/WolframResearch/wolfram-app-discovery-rs/compare/v0.2.2...v0.3.0 511 | [0.2.2]: https://github.com/WolframResearch/wolfram-app-discovery-rs/compare/v0.2.1...v0.2.2 512 | [0.2.1]: https://github.com/WolframResearch/wolfram-app-discovery-rs/compare/v0.2.0...v0.2.1 513 | [0.2.0]: https://github.com/WolframResearch/wolfram-app-discovery-rs/compare/v0.1.2...v0.2.0 514 | [0.1.2]: https://github.com/WolframResearch/wolfram-app-discovery-rs/compare/v0.1.1...v0.1.2 515 | [0.1.1]: https://github.com/WolframResearch/wolfram-app-discovery-rs/compare/v0.1.0...v0.1.1 516 | [0.1.0]: https://github.com/WolframResearch/wolfram-app-discovery-rs/releases/tag/v0.1.0 517 | -------------------------------------------------------------------------------- /src/os/windows.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | collections::HashMap, ffi::c_void, path::PathBuf, ptr::null_mut as nullptr, 3 | str::FromStr, 4 | }; 5 | 6 | use windows::Win32::{ 7 | Foundation::{ 8 | BOOL, ERROR_INSUFFICIENT_BUFFER, ERROR_NO_MORE_ITEMS, ERROR_SUCCESS, MAX_PATH, 9 | PWSTR, 10 | }, 11 | Storage::{ 12 | FileSystem::{Wow64DisableWow64FsRedirection, Wow64RevertWow64FsRedirection}, 13 | Packaging::Appx::{ 14 | ClosePackageInfo, GetPackageInfo, GetPackagesByPackageFamily, 15 | GetStagedPackageOrigin, OpenPackageInfoByFullName, PackageOrigin, 16 | PackageOrigin_DeveloperSigned, PackageOrigin_DeveloperUnsigned, 17 | PackageOrigin_Inbox, PackageOrigin_LineOfBusiness, PackageOrigin_Store, 18 | PackageOrigin_Unknown, PackageOrigin_Unsigned, APPX_PACKAGE_ARCHITECTURE, 19 | APPX_PACKAGE_ARCHITECTURE_ARM, APPX_PACKAGE_ARCHITECTURE_ARM64, 20 | APPX_PACKAGE_ARCHITECTURE_X64, APPX_PACKAGE_ARCHITECTURE_X86, PACKAGE_INFO, 21 | PACKAGE_INFORMATION_FULL, _PACKAGE_INFO_REFERENCE, 22 | }, 23 | }, 24 | System::{ 25 | Diagnostics::Debug::{ 26 | PROCESSOR_ARCHITECTURE, PROCESSOR_ARCHITECTURE_AMD64, 27 | PROCESSOR_ARCHITECTURE_ARM, PROCESSOR_ARCHITECTURE_INTEL, 28 | }, 29 | Registry::{ 30 | RegCloseKey, RegEnumKeyW, RegGetValueW, RegOpenKeyExA, RegOpenKeyExW, HKEY, 31 | HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE, KEY_READ, KEY_WOW64_32KEY, 32 | KEY_WOW64_64KEY, REG_SAM_FLAGS, RRF_RT_REG_DWORD, RRF_RT_REG_SZ, 33 | }, 34 | SystemInformation::{GetNativeSystemInfo, SYSTEM_INFO}, 35 | SystemServices::PROCESSOR_ARCHITECTURE_ARM64, 36 | Threading::{GetCurrentProcess, IsWow64Process}, 37 | }, 38 | }; 39 | 40 | use once_cell::sync::Lazy; 41 | use regex::Regex; 42 | 43 | use crate::{AppVersion, Error, WolframApp, WolframAppType}; 44 | 45 | //====================================== 46 | // Public Interface 47 | //====================================== 48 | 49 | pub fn discover_all() -> Vec { 50 | unsafe { load_apps_from_registry() } 51 | } 52 | 53 | pub fn from_app_directory(dir: &PathBuf) -> Result { 54 | if let Some(app) = discover_all() 55 | .into_iter() 56 | .find(|app| &app.app_directory() == dir) 57 | { 58 | return Ok(app); 59 | } else { 60 | // NOTE: 61 | // On macOS we can use CFBundleCreate to use a path to get information about 62 | // the application that resides at that path, but I'm not currently aware of 63 | // a way to do a similar lookup on Windows. 64 | // 65 | // For now, fall back to hoping that WOLFRAM_APP_DIRECTORY is only being used 66 | // to point to an app that we can otherwise discover in the registry in the 67 | // normal way. 68 | // 69 | // TODO: Investigate this more thoroughly. 70 | return Err(Error::other(format!( 71 | "unable to construct WolframApp from specified app directory '{}': \ 72 | app could not be found in the discover() list", 73 | dir.display() 74 | ))); 75 | } 76 | } 77 | 78 | //====================================== 79 | // Implementation 80 | //====================================== 81 | 82 | 83 | #[derive(Debug, Default)] 84 | struct WolframAppBuilder { 85 | app_name: Option, 86 | app_version: Option, 87 | 88 | app_type: Option, 89 | 90 | system_id: Option, 91 | 92 | id: Option, 93 | 94 | installation_directory: Option, 95 | 96 | language_tag: Option, 97 | 98 | executable_path: Option, 99 | 100 | digitally_signed: Option, 101 | 102 | origin: Option, 103 | } 104 | 105 | #[non_exhaustive] 106 | #[derive(Debug)] 107 | enum Origin { 108 | Sideloaded, 109 | Store, 110 | Unknown, 111 | } 112 | 113 | impl WolframAppBuilder { 114 | fn finish(self) -> Result { 115 | let WolframAppBuilder { 116 | app_name, 117 | app_version, 118 | app_type, 119 | installation_directory, 120 | executable_path, 121 | // TODO: Expose these fields? 122 | system_id: _, 123 | id: _, 124 | language_tag: _, 125 | digitally_signed: _, 126 | origin: _, 127 | } = self; 128 | 129 | Ok(WolframApp { 130 | app_name: app_name.ok_or(())?, 131 | app_version: app_version.ok_or(())?, 132 | app_type: app_type.ok_or(())?, 133 | 134 | // TODO: Is this always correct on Windows? 135 | app_directory: installation_directory.ok_or(())?, 136 | app_executable: executable_path, 137 | 138 | embedded_player: None, 139 | } 140 | .set_engine_embedded_player() 141 | .map_err(|_| ())?) 142 | } 143 | } 144 | 145 | impl AppVersion { 146 | fn parse_windows(version: &str, build_number: u32) -> Result { 147 | fn parse(s: &str) -> Result { 148 | u32::from_str(s).map_err(|err| { 149 | Error::other(format!( 150 | "invalid application version number component: '{}': {}", 151 | s, err 152 | )) 153 | }) 154 | } 155 | 156 | let components: Vec<&str> = version.split(".").collect(); 157 | 158 | let app_version = match components.as_slice() { 159 | // 4 components: major.minor.revision.minor_revision 160 | [major, minor, revision, minor_revision] => AppVersion { 161 | major: parse(major)?, 162 | minor: parse(minor)?, 163 | revision: parse(revision)?, 164 | 165 | minor_revision: Some(parse(minor_revision)?), 166 | build_code: Some(build_number), 167 | }, 168 | // 3 components: major.minor.revision 169 | [major, minor, revision] => AppVersion { 170 | major: parse(major)?, 171 | minor: parse(minor)?, 172 | revision: parse(revision)?, 173 | 174 | minor_revision: None, 175 | build_code: Some(build_number), 176 | }, 177 | _ => { 178 | return Err(Error::other(format!( 179 | "unexpected application version number format: {}", 180 | version 181 | ))) 182 | }, 183 | }; 184 | 185 | Ok(app_version) 186 | } 187 | } 188 | 189 | type DWORD = u32; 190 | type WCHAR = u16; 191 | 192 | const PRODUCTS: &[&str] = &[ 193 | "Wolfram.Mathematica_ztr62y9da0nfr", 194 | "Wolfram.Desktop_ztr62y9da0nfr", 195 | "Wolfram.Player_ztr62y9da0nfr", 196 | "Wolfram.FinancePlatform_ztr62y9da0nfr", 197 | "Wolfram.ProgrammingLab_ztr62y9da0nfr", 198 | "Wolfram.AlphaNotebook_ztr62y9da0nfr", 199 | "Wolfram.Engine_ztr62y9da0nfr", 200 | ]; 201 | 202 | #[rustfmt::skip] 203 | static PACKAGE_FAMILY_TO_PRODUCT_NAMES: Lazy> = Lazy::new(|| { 204 | HashMap::from_iter([ 205 | ("Wolfram.Mathematica", "Wolfram Mathematica"), 206 | ("Wolfram.Mathematica.Documentation", "Wolfram Mathematica Documentation"), 207 | ("Wolfram.Desktop", "Wolfram Desktop"), 208 | ("Wolfram.Desktop.Documentation", "Wolfram Desktop Documentation"), 209 | ("Wolfram.Player", "Wolfram Player"), 210 | ("Wolfram.FinancePlatform", "Wolfram Finance Platform"), 211 | ("Wolfram.FinancePlatform.Documentation", "Wolfram Finance Platform Documentation"), 212 | ("Wolfram.ProgrammingLab", "Wolfram Programming Lab"), 213 | ("Wolfram.ProgrammingLab.Documentation", "Wolfram Programming Lab Documentation"), 214 | ("Wolfram.AlphaNotebook", "Wolfram|Alpha Notebook Edition"), 215 | ("Wolfram.AlphaNotebook.Documentation", "Wolfram|Alpha Notebook Edition Documentation"), 216 | ("Wolfram.Engine", "Wolfram Engine"), 217 | ]) 218 | }); 219 | 220 | #[rustfmt::skip] 221 | static PACKAGE_FAMILY_TO_APP_TYPE: Lazy> = Lazy::new(|| { 222 | // FIXME: How should documentation installations be handled? Modeling them as 223 | // independent `WolframApp` instances doesn't seem quite optimal, since 224 | // nominally most Wolfram apps provide a copy of the Wolfram Language 225 | // runtime, which documentation does not. 226 | HashMap::from_iter([ 227 | ("Wolfram.Mathematica", WolframAppType::Mathematica), 228 | // ("Wolfram.Mathematica.Documentation", PRODUCT_MATHEMATICA), 229 | ("Wolfram.Desktop", WolframAppType::Desktop), 230 | // ("Wolfram.Desktop.Documentation", PRODUCT_WOLFRAMDESKTOP), 231 | ("Wolfram.Player", WolframAppType::Player), 232 | ("Wolfram.FinancePlatform", WolframAppType::FinancePlatform), 233 | // ("Wolfram.FinancePlatform.Documentation", PRODUCT_WOLFRAMFINANCE), 234 | ("Wolfram.ProgrammingLab", WolframAppType::ProgrammingLab), 235 | // ("Wolfram.ProgrammingLab.Documentation", PRODUCT_WOLFRAMPROGLAB), 236 | ("Wolfram.AlphaNotebook", WolframAppType::WolframAlphaNotebookEdition), 237 | // ("Wolfram.AlphaNotebook.Documentation", PRODUCT_WOLFRAMALPHANB), 238 | ("Wolfram.Engine", WolframAppType::Engine) 239 | ]) 240 | }); 241 | 242 | fn parse_build_number(build_number: &str) -> Option { 243 | let regex = Regex::new( 244 | "^[a-zA-Z]-[a-zA-Z0-9]+-[a-zA-Z]+(?:\\.[-a-zA-Z]+)?\\.[0-9]+\\.[0-9]+\\.[0-9]+\\.([0-9]+)$" 245 | // build number ^^^^^^^^ 246 | ).unwrap(); 247 | 248 | if let Some(captures) = regex.captures(&build_number) { 249 | return DWORD::from_str(&captures[1]).ok(); 250 | } else if let Ok(number) = DWORD::from_str(&build_number) { 251 | return Some(number); 252 | } else { 253 | None 254 | } 255 | } 256 | 257 | fn win_is_wow_process() -> bool { 258 | // #if _M_X64 || _M_ARM64 259 | if cfg!(any(target_arch = "x86_64", target_arch = "aarch64")) { 260 | return false; 261 | } else { 262 | let mut is_wow: BOOL = BOOL::from(false); 263 | 264 | unsafe { 265 | IsWow64Process(GetCurrentProcess(), &mut is_wow); 266 | } 267 | 268 | return is_wow.as_bool(); 269 | } 270 | } 271 | 272 | fn win_host_system_id() -> String { 273 | let PROCESSOR_ARCHITECTURE(arch) = unsafe { 274 | let mut info: SYSTEM_INFO = SYSTEM_INFO::default(); 275 | GetNativeSystemInfo(&mut info); 276 | 277 | info.Anonymous.Anonymous.wProcessorArchitecture 278 | }; 279 | 280 | let arch = u32::from(arch); 281 | 282 | let system_id = match arch { 283 | _ if arch == u32::from(PROCESSOR_ARCHITECTURE_ARM.0) => "Windows-ARM", 284 | PROCESSOR_ARCHITECTURE_ARM64 => "Windows-ARM64", 285 | _ if arch == u32::from(PROCESSOR_ARCHITECTURE_AMD64.0) => "Windows-x86-64", 286 | _ if arch == u32::from(PROCESSOR_ARCHITECTURE_INTEL.0) => "Windows", 287 | _ => "Windows", 288 | }; 289 | 290 | String::from(system_id) 291 | } 292 | 293 | unsafe fn load_app_from_registry( 294 | build_key: HKEY, 295 | system_id: &str, 296 | build_number: *const WCHAR, 297 | ) -> Result { 298 | let mut app_builder: WolframAppBuilder = Default::default(); 299 | 300 | app_builder.system_id = Some(String::from(system_id)); 301 | 302 | let is_wow_proc = win_is_wow_process(); 303 | 304 | let mut enabled: DWORD = 0; 305 | let mut product: DWORD = 0; 306 | let mut caps: DWORD = 0; 307 | let mut size: DWORD; 308 | 309 | let build_number = utf16_ptr_to_string(build_number); 310 | 311 | let build_number: DWORD = match parse_build_number(&build_number) { 312 | Some(0) => return Err(()), 313 | Some(build_number) => build_number, 314 | None => return Err(()), 315 | }; 316 | 317 | size = std::mem::size_of::() as u32; 318 | if RegGetValueW( 319 | build_key, 320 | PWSTR(nullptr()), 321 | "Caps", 322 | RRF_RT_REG_DWORD, 323 | nullptr(), 324 | &mut caps as *mut DWORD as *mut c_void, 325 | &mut size, 326 | ) != ERROR_SUCCESS 327 | { 328 | return Err(()); 329 | } 330 | 331 | // TODO: This appears to be some kind of bit field. Parse out the fields and 332 | // store them. 333 | // app_builder.setCaps(caps); 334 | 335 | size = std::mem::size_of::() as u32; 336 | if RegGetValueW( 337 | build_key, 338 | PWSTR(nullptr()), 339 | "ProductType", 340 | RRF_RT_REG_DWORD, 341 | nullptr(), 342 | (&mut product) as *mut DWORD as *mut c_void, 343 | &mut size, 344 | ) != ERROR_SUCCESS 345 | { 346 | return Err(()); 347 | } 348 | 349 | app_builder.app_type = WolframAppType::from_windows_product_type(product); 350 | 351 | if let Some(id) = reg_get_value_string(build_key, "CLSID") { 352 | app_builder.id = Some(id); 353 | } 354 | 355 | if let Some(dir) = reg_get_value_string(build_key, "InstallationDirectory") { 356 | app_builder.installation_directory = Some(PathBuf::from(dir)); 357 | } 358 | 359 | if let Some(exec_path) = reg_get_value_string(build_key, "ExecutablePath") { 360 | let exec_path = PathBuf::from(exec_path); 361 | 362 | app_builder.executable_path = Some(exec_path.clone()); 363 | 364 | // If `installation_directory` is not set but `executable_path` is, derive 365 | // the installation directory from the executable path. 366 | if app_builder.installation_directory.is_none() && exec_path.exists() { 367 | let install_dir = exec_path.parent().unwrap().to_path_buf(); 368 | app_builder.installation_directory = Some(install_dir); 369 | } 370 | } 371 | 372 | { 373 | let has_exec_path = match app_builder.executable_path { 374 | None => false, 375 | Some(ref path) => path.exists(), 376 | }; 377 | 378 | let has_install_dir = match app_builder.installation_directory { 379 | None => false, 380 | Some(ref path) => path.exists(), 381 | }; 382 | 383 | if !has_exec_path && !has_install_dir { 384 | return Err(()); 385 | } 386 | } 387 | 388 | app_builder.language_tag = Some( 389 | reg_get_value_string(build_key, "Language").unwrap_or_else(|| String::from("en")), 390 | ); 391 | 392 | app_builder.app_name = match reg_get_value_string(build_key, "ProductName") { 393 | name @ Some(_) => name, 394 | None => return Err(()), 395 | }; 396 | 397 | if let Some(version_string) = reg_get_value_string(build_key, "ProductVersion") { 398 | match AppVersion::parse_windows(&version_string, build_number) { 399 | Ok(version) => { 400 | app_builder.app_version = Some(version); 401 | }, 402 | Err(_) => { 403 | // TODO: Generate an error here? 404 | }, 405 | } 406 | } 407 | 408 | if RegGetValueW( 409 | build_key, 410 | PWSTR(nullptr()), 411 | "Version", 412 | RRF_RT_REG_DWORD, 413 | nullptr(), 414 | &mut enabled as *mut DWORD as *mut c_void, 415 | &mut size, 416 | ) == ERROR_SUCCESS 417 | { 418 | let [major, minor, revision, minor_revision] = enabled.to_be_bytes(); 419 | 420 | if (major, minor, revision, minor_revision) == (0, 0, 0, 0) { 421 | // TODO: Does this zero version number appear only in Prototype builds? 422 | 423 | // Don't set the version number based on this registry value. 424 | crate::warning(&format!( 425 | "application registry key \"Version\" value is 0.0.0.0 (at: {:?})", 426 | app_builder.installation_directory 427 | )); 428 | } else { 429 | app_builder.app_version = Some(AppVersion { 430 | major: u32::from(major), 431 | minor: u32::from(minor), 432 | revision: u32::from(revision), 433 | minor_revision: Some(u32::from(minor_revision)), 434 | 435 | build_code: Some(build_number), 436 | }); 437 | } 438 | } 439 | 440 | if !app_builder.app_version.is_some() { 441 | let version_file: PathBuf = app_builder 442 | .installation_directory 443 | .clone() 444 | .unwrap() 445 | .join(".VersionID"); 446 | 447 | let mut orginal_value: *mut c_void = nullptr(); 448 | 449 | if is_wow_proc { 450 | Wow64DisableWow64FsRedirection(&mut orginal_value); 451 | } 452 | let result = std::fs::read_to_string(&version_file); 453 | if is_wow_proc { 454 | Wow64RevertWow64FsRedirection(orginal_value); 455 | } 456 | 457 | if let Ok(version_string) = result { 458 | if let Ok(app_version) = 459 | AppVersion::parse_windows(&version_string, build_number) 460 | { 461 | app_builder.app_version = Some(app_version); 462 | } 463 | } 464 | } 465 | 466 | if app_builder.app_version.is_none() { 467 | return Err(()); 468 | } 469 | 470 | return app_builder.finish(); 471 | } 472 | 473 | unsafe fn load_app_from_package_info( 474 | package_info: &PACKAGE_INFO, 475 | app_builder: &mut WolframAppBuilder, 476 | ) -> Result<(), String> { 477 | app_builder.id = Some(utf16_ptr_to_string(package_info.packageFullName.0)); 478 | 479 | // FIXME: 480 | // app_builder.setFullVersion(package_info.packageId.version.Anonymous.Version); 481 | 482 | let package_id_name = utf16_ptr_to_string(package_info.packageId.name.0); 483 | 484 | { 485 | // because we cannot get our hands on the display name... 486 | let mut product_title = String::from("Unknown"); 487 | 488 | if let Some(iter) = PACKAGE_FAMILY_TO_PRODUCT_NAMES.get(package_id_name.as_str()) 489 | { 490 | let app_version = app_builder.app_version.clone().unwrap(); 491 | 492 | let iter: &str = iter; 493 | product_title = iter.to_owned() + " " + &app_version.major().to_string(); 494 | 495 | if app_version.minor() != 0 { 496 | product_title += &format!(".{}", &app_version.minor()); 497 | } 498 | } 499 | 500 | app_builder.app_name = Some(product_title); 501 | } 502 | 503 | if let Some(app_type) = PACKAGE_FAMILY_TO_APP_TYPE.get(package_id_name.as_str()) { 504 | app_builder.app_type = Some(app_type.clone()); 505 | } else { 506 | return Err(format!("unrecognized package id name: {}", package_id_name)); 507 | } 508 | 509 | let system_id = match APPX_PACKAGE_ARCHITECTURE( 510 | package_info 511 | .packageId 512 | .processorArchitecture 513 | .try_into() 514 | .unwrap(), 515 | ) { 516 | APPX_PACKAGE_ARCHITECTURE_ARM => "Windows-ARM", 517 | APPX_PACKAGE_ARCHITECTURE_ARM64 => "Windows-ARM64", 518 | APPX_PACKAGE_ARCHITECTURE_X86 => "Windows", 519 | APPX_PACKAGE_ARCHITECTURE_X64 => "Windows-x86-64", 520 | _ => "Unknown", 521 | }; 522 | 523 | app_builder.system_id = Some(String::from(system_id)); 524 | 525 | let mut raw_origin = PackageOrigin::default(); 526 | 527 | #[allow(non_upper_case_globals)] 528 | if GetStagedPackageOrigin(package_info.packageFullName, &mut raw_origin) 529 | == ERROR_SUCCESS.0 as i32 530 | { 531 | let origin = match raw_origin { 532 | PackageOrigin_DeveloperUnsigned 533 | | PackageOrigin_DeveloperSigned 534 | | PackageOrigin_Inbox 535 | | PackageOrigin_LineOfBusiness 536 | | PackageOrigin_Unsigned => Origin::Sideloaded, 537 | PackageOrigin_Store => Origin::Store, 538 | PackageOrigin_Unknown | _ => Origin::Unknown, 539 | }; 540 | 541 | app_builder.origin = Some(origin); 542 | 543 | match raw_origin { 544 | PackageOrigin_Inbox 545 | | PackageOrigin_DeveloperSigned 546 | | PackageOrigin_LineOfBusiness 547 | | PackageOrigin_Store => { 548 | app_builder.digitally_signed = Some(true); 549 | }, 550 | 551 | PackageOrigin_DeveloperUnsigned 552 | | PackageOrigin_Unknown 553 | | PackageOrigin_Unsigned 554 | | _ => { 555 | app_builder.digitally_signed = Some(false); 556 | }, 557 | } 558 | } 559 | 560 | // TODO: Set language tag to None in this case? 561 | app_builder.language_tag = Some(String::from("Neutral")); 562 | app_builder.installation_directory = 563 | Some(PathBuf::from(utf16_ptr_to_string(package_info.path.0))); 564 | 565 | // FIXME: 566 | // app_builder.setBuildNumber(ReadCreationIDFileFromLayout(package_info.path)); 567 | 568 | Ok(()) 569 | } 570 | 571 | fn merge_user_installed_packages(apps: &mut Vec) { 572 | for product in PRODUCTS { 573 | let product_apps = unsafe { get_user_packages(product) }; 574 | apps.extend(product_apps); 575 | } 576 | } 577 | 578 | unsafe fn get_user_packages(product: &str) -> Vec { 579 | let mut count: u32 = 0; 580 | let mut buffer_length: u32 = 0; 581 | 582 | let error: i32 = GetPackagesByPackageFamily( 583 | product, 584 | &mut count, 585 | nullptr(), 586 | &mut buffer_length, 587 | PWSTR(nullptr()), 588 | ); 589 | 590 | if count == 0 || error != ERROR_INSUFFICIENT_BUFFER.0 as i32 { 591 | return vec![]; 592 | } 593 | 594 | // let buffer: PWSTR = malloc(size_of::() * buffer_length) as *mut WCHAR; 595 | let mut buffer_vec: Vec = 596 | Vec::with_capacity(usize::try_from(buffer_length).unwrap()); 597 | let buffer: *mut u16 = buffer_vec.as_mut_ptr(); 598 | 599 | // let packageFullNames: *mut PWSTR = malloc(size_of::() * count) as *mut PWSTR; 600 | let mut package_full_names: Vec = 601 | Vec::with_capacity(usize::try_from(count).unwrap()); 602 | 603 | if GetPackagesByPackageFamily( 604 | product, 605 | &mut count, 606 | package_full_names.as_mut_ptr(), 607 | &mut buffer_length, 608 | PWSTR(buffer), 609 | ) != ERROR_SUCCESS.0 as i32 610 | { 611 | return vec![]; 612 | } 613 | 614 | package_full_names.set_len(usize::try_from(count).unwrap()); 615 | 616 | let mut apps = Vec::new(); 617 | 618 | for package_full_name in package_full_names { 619 | let mut piref: *mut _PACKAGE_INFO_REFERENCE = nullptr(); 620 | 621 | if OpenPackageInfoByFullName(package_full_name, 0, &mut piref) 622 | != ERROR_SUCCESS.0 as i32 623 | { 624 | continue; 625 | } 626 | 627 | let mut app_builder = WolframAppBuilder::default(); 628 | 629 | let mut pack_length: u32 = 0; 630 | let mut pack_count: u32 = 0; 631 | 632 | if GetPackageInfo( 633 | piref, 634 | PACKAGE_INFORMATION_FULL, 635 | &mut pack_length, 636 | nullptr(), 637 | &mut pack_count, 638 | ) == ERROR_INSUFFICIENT_BUFFER.0 as i32 639 | { 640 | let mut pack_info_buffer: Vec = 641 | Vec::with_capacity(usize::try_from(pack_length).unwrap()); 642 | 643 | if GetPackageInfo( 644 | piref, 645 | PACKAGE_INFORMATION_FULL, 646 | &mut pack_length, 647 | pack_info_buffer.as_mut_ptr(), 648 | &mut pack_count, 649 | ) == ERROR_SUCCESS.0 as i32 650 | { 651 | // FIXME: Is this safe? We're casting a Vec's buffer to a struct instance. Is this 652 | // well-aligned? 653 | let package_info: *const PACKAGE_INFO = 654 | pack_info_buffer.as_ptr() as *const PACKAGE_INFO; 655 | 656 | match load_app_from_package_info(&*package_info, &mut app_builder) { 657 | Ok(()) => (), 658 | Err(err) => { 659 | crate::warning(&format!( 660 | "unable to process Wolfram application package '{}': {}", 661 | utf16_ptr_to_string(package_full_name.0), 662 | err 663 | )); 664 | 665 | ClosePackageInfo(piref); 666 | continue; 667 | }, 668 | } 669 | 670 | // TODO: 671 | // UpdateCapsFromApplicationIds(piref, package_info, app_builder); 672 | } 673 | } 674 | 675 | // UINT32 optPackLength = 0, optPackCount = 0; 676 | // if (GetPackageInfo(piref, PACKAGE_FILTER_OPTIONAL, &optPackLength, nullptr, &optPackCount) 677 | // == ERROR_INSUFFICIENT_BUFFER) 678 | // { 679 | // LPBYTE optPackInfoBuffer = (LPBYTE)malloc(optPackLength); 680 | // if (GetPackageInfo(piref, PACKAGE_FILTER_OPTIONAL, &optPackLength, optPackInfoBuffer, &optPackCount) 681 | // == ERROR_SUCCESS) 682 | // { 683 | // std::vector theOptionalProducts; 684 | // for (UINT32 i = 0; i < optPackCount; i++) 685 | // { 686 | // PACKAGE_INFO_REFERENCE optpiref = nullptr; 687 | // PACKAGE_INFO* package_info = (PACKAGE_INFO*)optPackInfoBuffer; 688 | // Wolfram::Apps::InstalledProduct theOptionalProduct; 689 | 690 | // if (OpenPackageInfoByFullName(package_info->packageFullName, 0, &optpiref) == ERROR_SUCCESS) 691 | // { 692 | // LoadInstalledProductInfoFromPackageInfo(package_info, theOptionalProduct); 693 | // UpdateCapsFromApplicationIds(optpiref, package_info, theOptionalProduct); 694 | // cpi(optpiref); 695 | // } 696 | 697 | // theOptionalProducts.push(theOptionalProduct); 698 | // } 699 | 700 | // app_builder.setOptionalPackages(theOptionalProducts); 701 | // } 702 | 703 | // free(optPackInfoBuffer); 704 | // } 705 | 706 | match app_builder.finish() { 707 | Ok(app) => apps.push(app), 708 | Err(()) => crate::warning("WolframAppBuilder had incomplete information"), 709 | }; 710 | 711 | ClosePackageInfo(piref); 712 | } 713 | 714 | apps 715 | } 716 | 717 | 718 | unsafe fn load_apps_from_registry() -> Vec { 719 | let mut installations: Vec = Vec::new(); 720 | 721 | let mut the_root_key: HKEY = HKEY(0); 722 | let mut the_alt_root_key: HKEY = HKEY(0); 723 | let mut the_user_key: HKEY = HKEY(0); 724 | 725 | let is_wow: bool = win_is_wow_process(); 726 | let mut needs_alt: bool = true; 727 | 728 | let mut access_type: REG_SAM_FLAGS = KEY_READ | KEY_WOW64_64KEY; 729 | let mut alt_access_type: REG_SAM_FLAGS = KEY_READ | KEY_WOW64_32KEY; 730 | 731 | let host_system_id: String = win_host_system_id(); 732 | 733 | // #if _M_X64 || _M_ARM64 734 | if cfg!(any(target_arch = "x86_64", target_arch = "aarch64")) { 735 | if !is_wow { 736 | access_type = KEY_READ; 737 | alt_access_type = KEY_READ; 738 | needs_alt = false; 739 | } 740 | } 741 | 742 | // 64-bit key on WIN64 || is_wow, 32-bit key on WIN32 && !is_wow 743 | RegOpenKeyExA( 744 | HKEY_LOCAL_MACHINE, 745 | "Software\\Wolfram Research\\Installations", 746 | 0, 747 | access_type, 748 | &mut the_root_key, 749 | ); 750 | RegOpenKeyExA( 751 | HKEY_CURRENT_USER, 752 | "Software\\Wolfram Research\\Installations", 753 | 0, 754 | access_type, 755 | &mut the_user_key, 756 | ); 757 | 758 | if needs_alt { 759 | // 32-bit key on WIN64 || is_wow 760 | RegOpenKeyExA( 761 | HKEY_LOCAL_MACHINE, 762 | "Software\\Wolfram Research\\Installations", 763 | 0, 764 | alt_access_type, 765 | &mut the_alt_root_key, 766 | ); 767 | } 768 | 769 | let mut load_products_from_registry_key = 770 | |the_key: HKEY, access_type: REG_SAM_FLAGS, system_id: &str| { 771 | let mut build_number: [u16; MAX_PATH as usize] = [0; MAX_PATH as usize]; 772 | let mut index: DWORD = 0; 773 | 774 | while RegEnumKeyW(the_key, index, PWSTR(build_number.as_mut_ptr()), MAX_PATH) 775 | != ERROR_NO_MORE_ITEMS 776 | { 777 | let mut build_key: HKEY = HKEY(0); 778 | if RegOpenKeyExW( 779 | the_key, 780 | PWSTR(build_number.as_ptr()), 781 | 0, 782 | access_type, 783 | &mut build_key, 784 | ) == ERROR_SUCCESS 785 | { 786 | if let Ok(app) = load_app_from_registry( 787 | build_key, 788 | system_id, 789 | build_number.as_ptr(), 790 | ) { 791 | installations.push(app); 792 | } 793 | 794 | RegCloseKey(build_key); 795 | } 796 | 797 | index += 1; 798 | } 799 | }; 800 | 801 | if the_root_key != HKEY(0) { 802 | load_products_from_registry_key( 803 | the_root_key, 804 | access_type, 805 | if needs_alt { 806 | &host_system_id 807 | } else { 808 | "Windows" 809 | }, 810 | ); 811 | RegCloseKey(the_root_key); 812 | } 813 | 814 | if needs_alt && the_alt_root_key != HKEY(0) { 815 | load_products_from_registry_key(the_alt_root_key, alt_access_type, "Windows"); 816 | RegCloseKey(the_alt_root_key); 817 | } 818 | 819 | if the_user_key != HKEY(0) { 820 | load_products_from_registry_key( 821 | the_user_key, 822 | access_type, 823 | if needs_alt { 824 | &host_system_id 825 | } else { 826 | "Windows" 827 | }, 828 | ); 829 | RegCloseKey(the_user_key); 830 | } 831 | 832 | merge_user_installed_packages(&mut installations); 833 | 834 | return installations; 835 | } 836 | 837 | impl WolframAppType { 838 | /// Construct a [`WolframAppType`] from the Windows registry `"ProductType"` field 839 | /// associated with an application. 840 | #[rustfmt::skip] 841 | fn from_windows_product_type(id: u32) -> Option { 842 | use WolframAppType::*; 843 | 844 | // const UNIVERSAL: u32 = 0xFFFFFFFF; 845 | const MATHEMATICA: u32 = 1 << 28; //(0x10000000) 846 | const DESKTOP: u32 = 1 << 27; //(0x08000000) 847 | const PROGRAMMING_LAB: u32 = 1 << 26; //(0x04000000) 848 | const FINANCE_PLATFORM: u32 = 1 << 25; //(0x02000000) 849 | const ALPHA_NB_EDITION: u32 = 1 << 24; //(0x01000000) 850 | const ENGINE: u32 = 1 << 15; //(0x00008000) 851 | const PLAYER_PRO: u32 = 1 << 14; //(0x00004000) 852 | const PLAYER: u32 = 1 << 1; //(0x00000002) 853 | // const READER: u32 = 1; 854 | // const NONE: u32 = 0; 855 | 856 | let app_type = match id { 857 | MATHEMATICA => Mathematica, 858 | DESKTOP => Desktop, 859 | PROGRAMMING_LAB => ProgrammingLab, 860 | FINANCE_PLATFORM => FinancePlatform, 861 | ALPHA_NB_EDITION => WolframAlphaNotebookEdition, 862 | ENGINE => Engine, 863 | PLAYER_PRO => PlayerPro, 864 | PLAYER => Player, 865 | _ => return None, 866 | }; 867 | 868 | Some(app_type) 869 | } 870 | } 871 | 872 | //====================================== 873 | // Utilities 874 | //====================================== 875 | 876 | unsafe fn utf16_ptr_to_string(str: *const u16) -> String { 877 | if str.is_null() { 878 | return String::new(); 879 | } 880 | 881 | // Find the offset of the NULL byte. 882 | let len: usize = { 883 | let mut end = str; 884 | while *end != 0 { 885 | end = end.add(1); 886 | } 887 | 888 | usize::try_from(end.offset_from(str)).unwrap() 889 | }; 890 | 891 | let slice: &[u16] = std::slice::from_raw_parts(str, len); 892 | 893 | String::from_utf16(slice).expect("unable to convert string to UTF-16") 894 | } 895 | 896 | unsafe fn reg_get_value_string(key: HKEY, name: &str) -> Option { 897 | let mut size_in_bytes: DWORD = 0; 898 | 899 | if RegGetValueW( 900 | key, 901 | PWSTR(nullptr()), 902 | name, 903 | RRF_RT_REG_SZ, 904 | nullptr(), 905 | nullptr(), 906 | &mut size_in_bytes, 907 | ) != ERROR_SUCCESS 908 | { 909 | return None; 910 | } 911 | 912 | let size_in_elements = size_in_bytes / (std::mem::size_of::() as DWORD); 913 | 914 | let mut buffer: Vec = vec![0; usize::try_from(size_in_elements).unwrap()]; 915 | 916 | if RegGetValueW( 917 | key, 918 | PWSTR(nullptr()), 919 | name, 920 | RRF_RT_REG_SZ, 921 | nullptr(), 922 | buffer.as_mut_ptr() as *mut c_void, 923 | &mut size_in_bytes, 924 | ) != ERROR_SUCCESS 925 | { 926 | return None; 927 | } 928 | 929 | Some(utf16_ptr_to_string(buffer.as_ptr())) 930 | } 931 | -------------------------------------------------------------------------------- /src/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 | 53 | pub mod build_scripts; 54 | pub mod config; 55 | 56 | mod os; 57 | 58 | #[cfg(test)] 59 | mod tests; 60 | 61 | #[doc(hidden)] 62 | mod test_readme { 63 | // Ensure that doc tests in the README.md file get run. 64 | #![doc = include_str!("../README.md")] 65 | } 66 | 67 | 68 | use std::{ 69 | cmp::Ordering, 70 | fmt::{self, Display}, 71 | path::PathBuf, 72 | process, 73 | str::FromStr, 74 | }; 75 | 76 | use log::info; 77 | 78 | #[allow(deprecated)] 79 | use config::env_vars::{RUST_WOLFRAM_LOCATION, WOLFRAM_APP_DIRECTORY}; 80 | 81 | use 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)] 92 | pub 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, 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>, 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))] 114 | pub 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] 143 | pub 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)] 178 | pub struct AppVersion { 179 | major: u32, 180 | minor: u32, 181 | revision: u32, 182 | minor_revision: Option, 183 | 184 | build_code: Option, 185 | } 186 | 187 | /// Wolfram Language version number. 188 | #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] 189 | #[non_exhaustive] 190 | pub 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)] 198 | pub 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)] 209 | pub struct Filter { 210 | pub app_types: Option>, 211 | } 212 | 213 | /// Wolfram app discovery error. 214 | #[derive(Debug, Clone)] 215 | #[cfg_attr(test, derive(PartialEq))] 216 | pub struct Error(ErrorKind); 217 | 218 | #[derive(Debug, Clone)] 219 | #[cfg_attr(test, derive(PartialEq))] 220 | pub(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))] 268 | pub(crate) enum FilterError { 269 | FilterDoesNotMatchAppType { 270 | app_type: WolframAppType, 271 | allowed: Vec, 272 | }, 273 | } 274 | 275 | impl 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 | 377 | impl 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. 394 | pub fn discover() -> Vec { 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. 414 | pub fn discover_with_filter(filter: &Filter) -> Vec { 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")] 430 | pub 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")] 438 | pub 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 | 446 | impl WolframAppType { 447 | /// Enumerate all `WolframAppType` variants. 448 | pub fn variants() -> Vec { 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 | 521 | impl FromStr for SystemID { 522 | type Err = (); 523 | 524 | fn from_str(string: &str) -> Result { 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 | 543 | impl 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 { 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 { 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 | 671 | impl 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 | 714 | impl 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 { 732 | self.minor_revision 733 | } 734 | 735 | #[allow(missing_docs)] 736 | pub const fn build_code(&self) -> Option { 737 | self.build_code 738 | } 739 | 740 | fn parse(version: &str) -> Result { 741 | fn parse(s: &str) -> Result { 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)] 815 | impl 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 { 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 { 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 | 941 | impl 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 | 963 | impl 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 { 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 { 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 = 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 { 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 { 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 { 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 { 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::>(); 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 { 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 { 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>, 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 = 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 { 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 { 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 { 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 { 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 { 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 { 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 | 1564 | pub(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))] 1572 | fn warning(message: &str) { 1573 | eprintln!("warning: {}", message) 1574 | } 1575 | 1576 | fn wolframscript_output( 1577 | wolframscript_command: &PathBuf, 1578 | args: &[String], 1579 | ) -> Result { 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)`. 1619 | fn try_wolframscript_installation_directory() -> Result, 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 | 1649 | impl 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 { 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 | 1703 | impl From 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 | 1713 | impl 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 | 1721 | impl 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 | 1784 | impl 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 | 1798 | impl 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 | 1810 | impl Display for SystemID { 1811 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 1812 | write!(f, "{}", self.as_str()) 1813 | } 1814 | } 1815 | --------------------------------------------------------------------------------