rust-usd 0.0.3

Rust bindings to OpenUSD (pxr C++): stage open, prim/mesh attrs, variants, sublayer authoring, UsdShade read+write, ArResolver hook.
use std::env;
use std::path::PathBuf;
use std::process;

fn main() {
    let (include_dir, lib_dir) = resolve_usd_paths();
    let lib_prefix = env::var("USD_LIB_PREFIX").unwrap_or_default();
    let monolithic = env::var("USD_MONOLITHIC")
        .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
        .unwrap_or(false);

    let link_python = env::var("USD_LINK_PYTHON").unwrap_or_else(|_| "framework".to_string());

    let mut usd_libs: Vec<&str> = if monolithic {
        vec!["usd_ms"]
    } else {
        // Subset needed for UsdStage::Open + prim traversal. Order doesn't
        // matter for dynamic linking; transitive deps resolve at load.
        vec![
            "usd", "usdGeom", "usdShade", "sdf", "ar", "pcp", "plug", "kind",
            "vt", "gf", "tf", "work", "trace", "js", "arch", "ts",
        ]
    };

    // USD built with Python support inlines pxr_boost::python wrappers into
    // template instantiations that show up in our objects (e.g. SdfSpecifier
    // converter registrations). Linking pxr_python + pxr_boost satisfies them.
    if link_python != "none" && !monolithic {
        usd_libs.push("python");
        usd_libs.push("boost");
    }

    for lib in &usd_libs {
        println!("cargo:rustc-link-lib=dylib={}{}", lib_prefix, lib);
    }

    println!("cargo:rustc-link-search=native={}", lib_dir.display());
    // Embed rpath so binaries can find the USD dylibs at runtime without
    // requiring DYLD_LIBRARY_PATH. Cargo strips link args from rlib outputs,
    // so this only affects executables (examples, tests, downstream bins).
    println!("cargo:rustc-link-arg=-Wl,-rpath,{}", lib_dir.display());

    // Houdini's USD links @rpath/Python — give the loader a place to find
    // Python.framework (one level up from Libraries/ in a Houdini install).
    if let Some(parent) = lib_dir.parent() {
        println!("cargo:rustc-link-arg=-Wl,-rpath,{}", parent.display());
    }

    // CPython runtime symbols (__Py_Dealloc, __Py_NoneStruct, ...) come from
    // either a Python framework (macOS Houdini-style) or a libpython.
    match link_python.as_str() {
        "framework" => {
            let fw_name = env::var("USD_PYTHON_FRAMEWORK").unwrap_or_else(|_| "Python".into());
            if let Ok(fw_dir) = env::var("USD_PYTHON_FRAMEWORK_DIR") {
                println!("cargo:rustc-link-search=framework={}", fw_dir);
                // USD's libpxr_python install_name is @rpath/Python (bare,
                // not @rpath/Python.framework/...) so the rpath must point
                // INSIDE the framework so <rpath>/Python finds the symlink.
                let inside_fw = PathBuf::from(&fw_dir).join(format!("{}.framework", fw_name));
                println!("cargo:rustc-link-arg=-Wl,-rpath,{}", inside_fw.display());
            }
            println!("cargo:rustc-link-lib=framework={}", fw_name);
        }
        "lib" => {
            if let Ok(py_lib_dir) = env::var("USD_PYTHON_LIB_DIR") {
                println!("cargo:rustc-link-search=native={}", py_lib_dir);
                println!("cargo:rustc-link-arg=-Wl,-rpath,{}", py_lib_dir);
            }
            let py_lib = env::var("USD_PYTHON_LIB_NAME").unwrap_or_else(|_| "python3.11".into());
            println!("cargo:rustc-link-lib=dylib={}", py_lib);
        }
        "none" => {}
        other => {
            eprintln!(
                "rust-usd build.rs: USD_LINK_PYTHON={:?} not understood; expected framework|lib|none",
                other
            );
            process::exit(1);
        }
    }

    let mut build = cxx_build::bridge("src/lib.rs");
    build
        .file("cpp/usd_bridge.cpp")
        .include(&include_dir)
        .include("cpp")
        .flag_if_supported("-std=c++17")
        .flag_if_supported("-Wno-deprecated-declarations");

    // USD built with Python support leaks Python.h through headers
    // (e.g. ar/resolverContext.h → tf/pyLock.h → wrap_python.hpp). Consumers
    // must point at a CPython include dir even if they don't use Python.
    if let Ok(py_inc) = env::var("USD_PYTHON_INCLUDE_DIR") {
        build.include(py_inc);
    }

    build.compile("rust_usd_bridge");

    println!("cargo:rerun-if-changed=cpp/usd_bridge.h");
    println!("cargo:rerun-if-changed=cpp/usd_bridge.cpp");
    println!("cargo:rerun-if-changed=src/lib.rs");
    println!("cargo:rerun-if-changed=build.rs");
    for var in [
        "USD_INSTALL_DIR",
        "USD_INCLUDE_DIR",
        "USD_LIB_DIR",
        "USD_LIB_PREFIX",
        "USD_MONOLITHIC",
        "USD_PYTHON_INCLUDE_DIR",
        "USD_LINK_PYTHON",
        "USD_PYTHON_FRAMEWORK_DIR",
        "USD_PYTHON_FRAMEWORK",
        "USD_PYTHON_LIB_DIR",
        "USD_PYTHON_LIB_NAME",
        "PXR_CMAKE_PREFIX",
    ] {
        println!("cargo:rerun-if-env-changed={}", var);
    }
}

fn resolve_usd_paths() -> (PathBuf, PathBuf) {
    // Two ways to point at USD:
    //   1. USD_INCLUDE_DIR + USD_LIB_DIR (precise, needed for non-conventional
    //      installs like Houdini where lib lives at .../Libraries/)
    //   2. USD_INSTALL_DIR (or PXR_CMAKE_PREFIX) — assumes include/ + lib/
    let explicit_include = env::var("USD_INCLUDE_DIR").ok().map(PathBuf::from);
    let explicit_lib = env::var("USD_LIB_DIR").ok().map(PathBuf::from);

    if let (Some(inc), Some(lib)) = (explicit_include.clone(), explicit_lib.clone()) {
        return validate(inc, lib);
    }

    let install = env::var("USD_INSTALL_DIR")
        .ok()
        .or_else(|| env::var("PXR_CMAKE_PREFIX").ok());

    if let Some(install) = install {
        let install = PathBuf::from(install);
        let inc = explicit_include.unwrap_or_else(|| install.join("include"));
        let lib = explicit_lib.unwrap_or_else(|| install.join("lib"));
        return validate(inc, lib);
    }

    eprintln!(
        "rust-usd build.rs: no USD install configured.\n\
         Set USD_INSTALL_DIR (with include/ + lib/ subdirs), or set USD_INCLUDE_DIR and USD_LIB_DIR explicitly.\n\
         Optional: USD_LIB_PREFIX (e.g. \"pxr_\" for Houdini), USD_MONOLITHIC=1 (single libusd_ms)."
    );
    process::exit(1);
}

fn validate(include_dir: PathBuf, lib_dir: PathBuf) -> (PathBuf, PathBuf) {
    if !include_dir.join("pxr/usd/usd/stage.h").exists() {
        eprintln!(
            "rust-usd build.rs: USD headers not found.\n\
             Expected pxr/usd/usd/stage.h under: {}",
            include_dir.display()
        );
        process::exit(1);
    }
    if !lib_dir.is_dir() {
        eprintln!(
            "rust-usd build.rs: USD lib dir does not exist: {}",
            lib_dir.display()
        );
        process::exit(1);
    }
    (include_dir, lib_dir)
}