cargo-bin-file 0.1.1

Read binary paths from Cargo's CARGO_BIN_FILE_* environment variables
Documentation
use std::{env, path::PathBuf};

/// Returns the path stored in Cargo's `CARGO_BIN_FILE_*` environment
/// variables for the given binary crate name.
///
/// Cargo sets these variables for integration tests and benchmarks so one
/// package can locate another package's compiled binary by name.
///
/// This helper first checks the crate-scoped variable and then falls back to
/// the bin-scoped form Cargo may expose for artifact dependencies:
///
/// - `CARGO_BIN_FILE_<CRATE_NAME>`
/// - `CARGO_BIN_FILE_<CRATE_NAME>_<BIN_NAME>`
///
/// The input crate or bin name is normalized the same way Cargo does for the
/// crate portion of these variables: hyphens become underscores and ASCII
/// letters are uppercased.
///
/// Returns `None` when the expected environment variable is not set.
///
/// # Examples
///
/// ```
/// use cargo_bin_file::bin_path;
///
/// let key = "CARGO_BIN_FILE_EXAMPLE_CRATE";
///
/// unsafe {
///     std::env::set_var(key, "/tmp/example-crate");
/// }
///
/// assert_eq!(
///     bin_path("example-crate"),
///     Some(std::path::PathBuf::from("/tmp/example-crate"))
/// );
///
/// unsafe {
///     std::env::remove_var(key);
/// }
/// ```
pub fn bin_path(name: &str) -> Option<PathBuf> {
    bin_file_env_vars(name)
        .into_iter()
        .find_map(|key| env::var(key).ok().map(Into::into))
}

fn bin_file_env_var(name: &str) -> String {
    format!("CARGO_BIN_FILE_{}", crate_env_name(name))
}

fn bin_file_env_var_with_bin_name(name: &str) -> String {
    format!("{}_{}", bin_file_env_var(name), name)
}

fn bin_file_env_vars(name: &str) -> [String; 2] {
    [bin_file_env_var(name), bin_file_env_var_with_bin_name(name)]
}

fn crate_env_name(name: &str) -> String {
    name.replace("-", "_").to_ascii_uppercase()
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::sync::{Mutex, OnceLock};

    fn env_lock() -> &'static Mutex<()> {
        static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
        LOCK.get_or_init(|| Mutex::new(()))
    }

    #[test]
    fn crate_name_is_normalized_for_env_var_suffix() {
        assert_eq!(crate_env_name("my-crate"), "MY_CRATE");
        assert_eq!(crate_env_name("already_UPPER"), "ALREADY_UPPER");
    }

    #[test]
    fn crate_name_maps_to_cargo_bin_file_env_var() {
        assert_eq!(
            bin_file_env_var("my-crate"),
            "CARGO_BIN_FILE_MY_CRATE".to_string()
        );
    }

    #[test]
    fn crate_name_maps_to_bin_scoped_cargo_bin_file_env_var() {
        assert_eq!(
            bin_file_env_var_with_bin_name("my-crate"),
            "CARGO_BIN_FILE_MY_CRATE_my-crate".to_string()
        );
    }

    #[test]
    fn returns_none_when_env_var_is_missing() {
        let _guard = env_lock().lock().unwrap();
        let [crate_key, bin_key] = bin_file_env_vars("missing-crate");

        unsafe {
            env::remove_var(&crate_key);
            env::remove_var(&bin_key);
        }

        assert_eq!(bin_path("missing-crate"), None);
    }

    #[test]
    fn returns_path_from_matching_env_var() {
        let _guard = env_lock().lock().unwrap();
        let key = bin_file_env_var("example-crate");
        let value = "/tmp/example-artifact";

        unsafe {
            env::set_var(&key, value);
        }

        assert_eq!(bin_path("example-crate"), Some(PathBuf::from(value)));

        unsafe {
            env::remove_var(&key);
        }
    }

    #[test]
    fn falls_back_to_bin_scoped_env_var() {
        let _guard = env_lock().lock().unwrap();
        let [crate_key, bin_key] = bin_file_env_vars("example-crate");
        let value = "/tmp/example-bin-artifact";

        unsafe {
            env::remove_var(&crate_key);
            env::set_var(&bin_key, value);
        }

        assert_eq!(bin_path("example-crate"), Some(PathBuf::from(value)));

        unsafe {
            env::remove_var(&bin_key);
        }
    }

    #[test]
    fn prefers_crate_scoped_env_var_when_both_exist() {
        let _guard = env_lock().lock().unwrap();
        let [crate_key, bin_key] = bin_file_env_vars("example-crate");
        let crate_value = "/tmp/example-crate-artifact";
        let bin_value = "/tmp/example-bin-artifact";

        unsafe {
            env::set_var(&crate_key, crate_value);
            env::set_var(&bin_key, bin_value);
        }

        assert_eq!(bin_path("example-crate"), Some(PathBuf::from(crate_value)));

        unsafe {
            env::remove_var(&crate_key);
            env::remove_var(&bin_key);
        }
    }
}