diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index dc21793..b2912d7 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -26,30 +26,41 @@ jobs: rustup component add rustfmt ### required for the build script to work ### [ "${{ matrix.arch }}" = 'aarch64' ] && sudo apt update && sudo apt install -y g++-aarch64-linux-gnu || : - - name: build fails without MUJOCO_DIR + - name: build fails without mujoco env: CARGO_BUILD_TARGET: ${{ matrix.arch }}-unknown-linux-gnu run: | if cargo build; then - echo 'cargo build succeeded without MUJOCO_DIR, which is unexpected.' + echo 'cargo build **unexpectedly** succeeded without mujoco.' exit 1 else echo 'cargo build failed as expected without mujoco.' fi - - name: install mujoco and set MUJOCO_DIR + - name: install mujoco run: | mkdir -p $HOME/.mujoco cd $HOME/.mujoco wget https://github.com/google-deepmind/mujoco/releases/download/3.3.2/mujoco-3.3.2-linux-${{ matrix.arch }}.tar.gz tar -xzf mujoco-3.3.2-linux-${{ matrix.arch }}.tar.gz - echo "MUJOCO_DIR=$HOME/.mujoco/mujoco-3.3.2" >> $GITHUB_ENV - echo "LD_LIBRARY_PATH=$HOME/.mujoco/mujoco-3.3.2/lib:$LD_LIBRARY_PATH" >> $GITHUB_ENV - - name: build succeeds with MUJOCO_DIR + - name: build succeeds with MUJOCO_LIB + env: + CARGO_BUILD_TARGET: ${{ matrix.arch }}-unknown-linux-gnu + run: | + export MUJOCO_LIB="$HOME/.mujoco/mujoco-3.3.2/lib" + cargo clean ### clean up the build cache to assure the build script is re-run ### + cargo build + cargo build --features bindgen + git diff --exit-code ./src/bindgen.rs || (echo "bindgen.rs changed after build with bindgen feature."; exit 1) + + - name: build succeeds using system mujoco without MUJOCO_LIB env: CARGO_BUILD_TARGET: ${{ matrix.arch }}-unknown-linux-gnu run: | + sudo cp $HOME/.mujoco/mujoco-3.3.2/lib/libmujoco.so /usr/local/lib/ + sudo ldconfig + cargo clean ### clean up the build cache to assure the build script is re-run ### cargo build cargo build --features bindgen git diff --exit-code ./src/bindgen.rs || (echo "bindgen.rs changed after build with bindgen feature."; exit 1) @@ -81,7 +92,8 @@ jobs: mkdir -p $HOME/.mujoco && cd $HOME/.mujoco wget https://github.com/google-deepmind/mujoco/releases/download/3.3.2/$MUJOCO_FILENAME tar -xzf $MUJOCO_FILENAME - echo "MUJOCO_DIR=$HOME/.mujoco/mujoco-3.3.2" >> $GITHUB_ENV + ### Set `MUJOCO_LIB` and `LD_LIBRARY_PATH` for the test jobs ### + echo "MUJOCO_LIB=$HOME/.mujoco/mujoco-3.3.2/lib" >> $GITHUB_ENV echo "LD_LIBRARY_PATH=$HOME/.mujoco/mujoco-3.3.2/lib:$LD_LIBRARY_PATH" >> $GITHUB_ENV - name: setup additional dependencies for examples diff --git a/Cargo.toml b/Cargo.toml index 9576b2c..d7319cb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,9 +14,13 @@ categories = ["api-bindings", "science::robotics", "simulation"] [build-dependencies] bindgen = { optional = true, version = "=0.72.1" } +cc = { version = "=1.2.45" } [dev-dependencies] glfw = "0.60" [features] bindgen = ["dep:bindgen"] # run bindgen at build time, instead of using pre-generated bindgen.rs + +# internal +DEBUG = [] diff --git a/README.md b/README.md index 0a182b8..157a745 100644 --- a/README.md +++ b/README.md @@ -32,9 +32,10 @@ ## Requirements -- [MuJoCo 3.3.2](https://github.com/google-deepmind/mujoco/releases/tag/3.3.2) downloaded - and expanded **as it is** (don't move or rename the files within it) -- `MUJOCO_DIR` environment variable set to the path of the MuJoCo directory (e.g. `$HOME/.mujoco/mujoco-3.3.2`) +- [MuJoCo 3.3.2](https://github.com/google-deepmind/mujoco/releases/tag/3.3.2) downloaded and installed +- Additionally, if you place mujoco library in a non-standard directory of the platform, + you need `MUJOCO_LIB` environment variable set to the path of the directory containing + `libmujoco.so` or `mujoco.lib` (e.g. `$HOME/.mujoco/mujoco-3.3.2/lib` when you placed the official release above in `~/.mujoco`) ### Note / Tips @@ -45,29 +46,31 @@ ``` to download & expand MuJoCo 3.3.2.\ On other platforms, do the same with the appropriate archive file for your system. - + - One way to setup is to install MuJoCo to _a default standard path_ like `/usr/local/lib/` (or a folder in _PATH_ on Windows), then if needed create symlink to `mujoco-3.3.2/lib/libmujoco.so` there, and insert to your shell config file: ```sh # example on Linux with /usr/local/lib/ - export MUJOCO_DIR="/usr/local/lib/mujoco-3.3.2" + export MUJOCO_LIB="/usr/local/lib/mujoco-3.3.2/lib" ``` Or if you'd like to avoid to install MuJoCo to such a system directory: ```sh # example on Linux with $HOME/.mujoco/ - export MUJOCO_DIR="$HOME/.mujoco/mujoco-3.3.2" - export LD_LIBRARY_PATH="$MUJOCO_DIR/lib:$LD_LIBRARY_PATH" + export MUJOCO_LIB="$HOME/.mujoco/mujoco-3.3.2/lib" + export LD_LIBRARY_PATH="$MUJOCO_LIB:$LD_LIBRARY_PATH" ``` - -- Depending on your setting, be sure to specify `$MUJOCO_DIR/lib` as shared library path - when executing your app (for example `LD_LIBRARY_PATH=$MUJOCO_DIR/lib cargo run` on Linux) + +- Or, you can get MuJoCo library through Python toolchain like `uv` or `pip`. + +- Depending on your setting, be sure to specify `$MUJOCO_LIB` as shared library path + when executing your app (for example `LD_LIBRARY_PATH=$MUJOCO_LIB cargo run` on Linux) ## Example ```toml [dependencies] -rusty_mujoco = "0.1" +rusty_mujoco = "0.2" glfw = "0.60" ``` diff --git a/build.rs b/build.rs index 40929f7..f3d8723 100644 --- a/build.rs +++ b/build.rs @@ -1,17 +1,107 @@ fn main() { if option_env!("DOCS_RS").is_some() { return } - let mujoco_dir = std::env::var("MUJOCO_DIR").expect("MUJOCO_DIR environment variable is not set"); - let mujoco_dir = std::path::Path::new(&mujoco_dir).canonicalize().expect("MUJOCO_DIR is not a valid path"); - let mujoco_lib = mujoco_dir.join("lib").to_str().unwrap().to_owned(); - - println!("cargo:rustc-link-search={mujoco_lib}"); - println!("cargo:rustc-link-lib=dylib=mujoco"); + if dbg!(!probe_mujoco_lib_with_libdir(None)) { + if let Some(mujoco_lib_dir) = mujoco_lib_directory_from_env("MUJOCO_LIB") { + assert!( + dbg!(probe_mujoco_lib_with_libdir(Some(&mujoco_lib_dir))), + "Failed to link with mujoco library even after setting `MUJOCO_LIB` environment variable!" + ); + println!("cargo:rustc-link-search=native={}", mujoco_lib_dir.display()); + } else { + panic!("\ + MuJoCo library not found. Make sure that the mujoco library is installed and, \ + if its location is non-standard, set the path via the `MUJOCO_LIB` environment variable.\ + "); + } + } + println!("cargo:rustc-link-lib=mujoco"); #[cfg(feature = "bindgen")] bindgen(); } +fn probe_mujoco_lib_with_libdir(libdir: Option<&std::path::Path>) -> bool { + let crate_root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")); + let vendor_dir = crate_root.join("vendor"); + + let probe_c = crate_root.join("probe.c"); + let vendor_include = vendor_dir.join("include"); + let vendor_include_mujoco = vendor_include.join("mujoco"); + + /* + * `cc` crate is designed as: + * + * > A library for Cargo build scripts to compile a set of C/C++/assembly/CUDA files + * into a static archive for Cargo to link into the crate being built. + * + * However, here we need to probe whether linking with `mujoco` library to an executable + * succeeds or not, so we manually invoke the compiler with appropriate arguments, + * using `cc` crate to get the compiler path and kind. + */ + + let cc = cc::Build::new().cargo_metadata(false).get_compiler(); + let cc_path = cc.path(); + let cc_args = if cc.is_like_gnu() || cc.is_like_clang() { + let mut args = vec![ + probe_c.to_str().unwrap().to_string(), + format!("-I{}", vendor_include.display()), + format!("-I{}", vendor_include_mujoco.display()), + "-o".to_string(), "/dev/null".to_string(), + ]; + if let Some(libdir) = libdir { + args.push(format!("-L{}", libdir.display())); + } + args.push("-lmujoco".to_string()); + args + } else if cc.is_like_msvc() || cc.is_like_clang_cl() { + let mut args = vec![ + probe_c.to_str().unwrap().to_string(), + format!("/I{}", vendor_include.display()), + format!("/I{}", vendor_include_mujoco.display()), + "/Fe:NUL".to_string(), + ]; + if let Some(libdir) = libdir { + args.push(format!("/LIBPATH:{}", libdir.display())); + } + args.push("mujoco.lib".to_string()); + args + } else { + panic!("Unsupported compiler: {}", cc_path.display()); + }; + + std::process::Command::new(dbg!(cc_path)) + .args(dbg!(cc_args)) + .stdout(std::process::Stdio::inherit()) + .stderr(std::process::Stdio::inherit()) + .status() + .unwrap_or_else(|err| panic!("Failed to invoke compiler to probe mujoco library: {err}")) + .success() +} + +/// Adds the given path to the library search path for linking, +/// with resolving the directory path from an environment variable `env` +/// that may be either the directory path or path of the library file itself. +fn mujoco_lib_directory_from_env(env: &'static str) -> Option { + let mujoco_lib = match std::env::var(env) { + Ok(value) => value, + Err(std::env::VarError::NotPresent) => return None, + Err(std::env::VarError::NotUnicode(os_str)) => panic!("{env} contains invalid unicode: `{}`", os_str.to_string_lossy()), + }; + let mujoco_lib = std::path::Path::new(&mujoco_lib); + + Some(if mujoco_lib.is_dir() { + mujoco_lib.to_owned() + } else if mujoco_lib.is_file() { + mujoco_lib + .parent() + .unwrap_or_else(|| panic!("{env} must be a valid path to mujoco library file or directory containing it")) + .to_owned() + } else { + panic!("{env} must be a valid path to mujoco library file or directory containing it") + }) +} + #[cfg(feature = "bindgen")] fn bindgen() { #[derive(Debug)] diff --git a/examples/README.md b/examples/README.md index f7f2759..34d63cd 100644 --- a/examples/README.md +++ b/examples/README.md @@ -15,19 +15,25 @@ Pass the path to a MuJoCo model XML file as an argument. For example, you can use the `humanoid.xml` model provided by MuJoCo: ```sh -cargo run --example visualize_left_object -- $MUJOCO_DIR/model/humanoid/humanoid.xml +cargo run --example visualize_left_object -- $MUJOCO_LIB/../model/humanoid/humanoid.xml ``` +*(replace the path to humanoid.xml with yours)* + Options: - `--camera `: Specify the name of camera to use for visualization (optional). If not provided, the default camera will be used. - example: `--camera side` for the humanoid model -Depending on your system, you may need to give: +Depending on your setting, you may need to specify: -- `MUJOCO_DIR` environment variable, to the MuJoCo directory path (e.g. `$HOME/.mujoco/mujoco-3.3.2`) +- `MUJOCO_LIB` environment variable, to the MuJoCo directory path (e.g. `$HOME/.mujoco/mujoco-3.3.2/lib`) - `LD_LIBRARY_PATH` (Linux), `DYLD_LIBRARY_PATH` (macOS), or `PATH` (Windows) configuration for searching the MuJoCo library path -like `LD_LIBRARY_PATH="$MUJOCO_DIR/lib" cargo run --example visualize_left_object -- $MUJOCO_DIR/model/humanoid.xml` +like: + +```sh +LD_LIBRARY_PATH="$MUJOCO_LIB" cargo run --example visualize_left_object -- $MUJOCO_LIB/../model/humanoid.xml +``` diff --git a/probe.c b/probe.c new file mode 100644 index 0000000..30cc1e0 --- /dev/null +++ b/probe.c @@ -0,0 +1,6 @@ +#include "mujoco/mujoco.h" + +int main(void) { + mj_version(); + return 0; +}