├── .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 | [](https://crates.io/crates/wolfram-app-discovery)
4 | 
5 | [](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