use super::{Platform, default_cache_dir};
use crate::lockfile::Lockfile;
use crate::{Error, Result};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::collections::{BTreeMap, HashSet};
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct ToolActivationStep {
pub var: String,
pub op: ToolActivationOperation,
#[serde(default = "default_separator")]
pub separator: String,
pub from: ToolActivationSource,
}
fn default_separator() -> String {
":".to_string()
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum ToolActivationOperation {
Set,
Prepend,
Append,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum ToolActivationSource {
AllBinDirs,
AllLibDirs,
ToolBinDir { tool: String },
ToolLibDir { tool: String },
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ResolvedToolActivationStep {
pub var: String,
pub op: ToolActivationOperation,
pub separator: String,
pub value: String,
}
#[derive(Debug, Clone)]
pub struct ToolActivationResolveOptions<'a> {
pub lockfile: &'a Lockfile,
pub lockfile_path: &'a Path,
pub platform: Platform,
pub cache_dir: PathBuf,
}
impl<'a> ToolActivationResolveOptions<'a> {
#[must_use]
pub fn new(lockfile: &'a Lockfile, lockfile_path: &'a Path) -> Self {
Self {
lockfile,
lockfile_path,
platform: Platform::current(),
cache_dir: default_cache_dir(),
}
}
#[must_use]
pub fn with_platform(mut self, platform: Platform) -> Self {
self.platform = platform;
self
}
#[must_use]
pub fn with_cache_dir(mut self, cache_dir: PathBuf) -> Self {
self.cache_dir = cache_dir;
self
}
}
pub fn validate_tool_activation(options: &ToolActivationResolveOptions<'_>) -> Result<()> {
if options.lockfile.tools.is_empty() {
return Ok(());
}
if options.lockfile.tools_activation.is_empty() {
return Ok(());
}
let platform_key = options.platform.to_string();
for step in &options.lockfile.tools_activation {
if step.var.trim().is_empty() {
return Err(Error::configuration(
"Tool activation entry has an empty `var` value.",
));
}
match &step.from {
ToolActivationSource::ToolBinDir { tool }
| ToolActivationSource::ToolLibDir { tool } => {
let Some(locked_tool) = options.lockfile.tools.get(tool) else {
return Err(Error::configuration(format!(
"Tool activation references unknown tool '{}'.",
tool
)));
};
if let Some(platform_data) = locked_tool.platforms.get(&platform_key)
&& platform_data.provider == "nix"
{
return Err(Error::configuration(format!(
"Tool activation per-tool references do not support Nix tools yet ('{}'). \
Use allBinDirs/allLibDirs instead.",
tool
)));
}
}
ToolActivationSource::AllBinDirs | ToolActivationSource::AllLibDirs => {}
}
}
Ok(())
}
pub fn resolve_tool_activation(
options: &ToolActivationResolveOptions<'_>,
) -> Result<Vec<ResolvedToolActivationStep>> {
let path_index = ToolPathIndex::collect(options)?;
if options.lockfile.tools_activation.is_empty() {
let mut resolved = vec![
ResolvedToolActivationStep {
var: "PATH".to_string(),
op: ToolActivationOperation::Prepend,
separator: ":".to_string(),
value: join_paths(&path_index.all_bin_dirs, ":"),
},
ResolvedToolActivationStep {
var: "DYLD_LIBRARY_PATH".to_string(),
op: ToolActivationOperation::Prepend,
separator: ":".to_string(),
value: join_paths(&path_index.all_lib_dirs, ":"),
},
ResolvedToolActivationStep {
var: "LD_LIBRARY_PATH".to_string(),
op: ToolActivationOperation::Prepend,
separator: ":".to_string(),
value: join_paths(&path_index.all_lib_dirs, ":"),
},
ResolvedToolActivationStep {
var: "CPATH".to_string(),
op: ToolActivationOperation::Prepend,
separator: ":".to_string(),
value: join_paths(&path_index.all_include_dirs, ":"),
},
ResolvedToolActivationStep {
var: "PKG_CONFIG_PATH".to_string(),
op: ToolActivationOperation::Prepend,
separator: ":".to_string(),
value: join_paths(&path_index.all_pkgconfig_dirs, ":"),
},
];
for (var, value) in &path_index.file_env_exports {
resolved.push(ResolvedToolActivationStep {
var: var.clone(),
op: ToolActivationOperation::Set,
separator: ":".to_string(),
value: value.to_string_lossy().to_string(),
});
}
return Ok(resolved);
}
validate_tool_activation(options)?;
let mut resolved = Vec::with_capacity(options.lockfile.tools_activation.len());
for step in &options.lockfile.tools_activation {
let paths = match &step.from {
ToolActivationSource::AllBinDirs => path_index.all_bin_dirs.clone(),
ToolActivationSource::AllLibDirs => path_index.all_lib_dirs.clone(),
ToolActivationSource::ToolBinDir { tool } => path_index
.tool_bin_dirs
.get(tool)
.cloned()
.unwrap_or_default(),
ToolActivationSource::ToolLibDir { tool } => path_index
.tool_lib_dirs
.get(tool)
.cloned()
.unwrap_or_default(),
};
resolved.push(ResolvedToolActivationStep {
var: step.var.clone(),
op: step.op.clone(),
separator: step.separator.clone(),
value: join_paths(&paths, &step.separator),
});
}
Ok(resolved)
}
#[must_use]
pub fn apply_resolved_tool_activation(
current: Option<&str>,
step: &ResolvedToolActivationStep,
) -> Option<String> {
match step.op {
ToolActivationOperation::Set => Some(step.value.clone()),
ToolActivationOperation::Prepend => {
if step.value.is_empty() {
return None;
}
match current {
Some(existing) if !existing.is_empty() => {
Some(format!("{}{}{}", step.value, step.separator, existing))
}
_ => Some(step.value.clone()),
}
}
ToolActivationOperation::Append => {
if step.value.is_empty() {
return None;
}
match current {
Some(existing) if !existing.is_empty() => {
Some(format!("{}{}{}", existing, step.separator, step.value))
}
_ => Some(step.value.clone()),
}
}
}
}
#[derive(Debug, Default)]
struct ToolPathIndex {
all_bin_dirs: Vec<PathBuf>,
all_lib_dirs: Vec<PathBuf>,
all_include_dirs: Vec<PathBuf>,
all_pkgconfig_dirs: Vec<PathBuf>,
tool_bin_dirs: BTreeMap<String, Vec<PathBuf>>,
tool_lib_dirs: BTreeMap<String, Vec<PathBuf>>,
file_env_exports: BTreeMap<String, PathBuf>,
}
impl ToolPathIndex {
fn collect(options: &ToolActivationResolveOptions<'_>) -> Result<Self> {
Self::collect_with(options, nix_profile_path_for_project)
}
fn collect_with<F>(
options: &ToolActivationResolveOptions<'_>,
nix_profile_path_for_project: F,
) -> Result<Self>
where
F: Fn(&Path) -> Result<PathBuf>,
{
let mut index = Self::default();
let mut all_bin_seen = HashSet::new();
let mut all_lib_seen = HashSet::new();
let mut all_include_seen = HashSet::new();
let mut all_pkgconfig_seen = HashSet::new();
let platform_key = options.platform.to_string();
let lockfile_dir = options.lockfile_path.parent().unwrap_or(Path::new("."));
let mut nix_profile_path: Option<Option<PathBuf>> = None;
for (name, tool) in &options.lockfile.tools {
let Some(platform_data) = tool.platforms.get(&platform_key) else {
continue;
};
match platform_data.provider.as_str() {
"nix" => {
let profile_path = nix_profile_path
.get_or_insert_with(|| nix_profile_path_for_project(lockfile_dir).ok());
let Some(profile_path) = profile_path.as_ref() else {
continue;
};
add_existing_dir(
&mut index.all_bin_dirs,
&mut all_bin_seen,
profile_path.join("bin"),
);
add_existing_dir(
&mut index.all_lib_dirs,
&mut all_lib_seen,
profile_path.join("lib"),
);
add_existing_dir(
&mut index.all_include_dirs,
&mut all_include_seen,
profile_path.join("include"),
);
add_existing_dir(
&mut index.all_pkgconfig_dirs,
&mut all_pkgconfig_seen,
profile_path.join("lib").join("pkgconfig"),
);
add_tool_existing_dir(&mut index.tool_bin_dirs, name, profile_path.join("bin"));
add_tool_existing_dir(&mut index.tool_lib_dirs, name, profile_path.join("lib"));
}
"rustup" => {
let toolchain = platform_data
.source
.get("toolchain")
.and_then(|v| v.as_str())
.unwrap_or("stable");
let rustup_dir = rustup_toolchain_dir(toolchain, &options.platform);
add_existing_dir(
&mut index.all_bin_dirs,
&mut all_bin_seen,
rustup_dir.join("bin"),
);
add_existing_dir(
&mut index.all_lib_dirs,
&mut all_lib_seen,
rustup_dir.join("lib"),
);
add_existing_dir(
&mut index.all_include_dirs,
&mut all_include_seen,
rustup_dir.join("include"),
);
add_existing_dir(
&mut index.all_pkgconfig_dirs,
&mut all_pkgconfig_seen,
rustup_dir.join("lib").join("pkgconfig"),
);
add_tool_existing_dir(&mut index.tool_bin_dirs, name, rustup_dir.join("bin"));
add_tool_existing_dir(&mut index.tool_lib_dirs, name, rustup_dir.join("lib"));
}
"github" => {
let tool_dir = options
.cache_dir
.join("github")
.join(name)
.join(&tool.version);
let extract: Vec<super::ToolExtract> = platform_data
.source
.get("extract")
.cloned()
.and_then(|value| serde_json::from_value(value).ok())
.unwrap_or_default();
if extract.is_empty() {
let path_hint_is_lib = platform_data
.source
.get("path")
.and_then(|v| v.as_str())
.is_some_and(path_looks_like_library);
if path_hint_is_lib {
let lib_dir = tool_dir.join("lib");
add_existing_dir(
&mut index.all_lib_dirs,
&mut all_lib_seen,
lib_dir.clone(),
);
add_tool_existing_dir(&mut index.tool_lib_dirs, name, lib_dir);
} else {
let bin_dir = tool_dir.join("bin");
add_existing_dir(
&mut index.all_bin_dirs,
&mut all_bin_seen,
bin_dir.clone(),
);
add_tool_existing_dir(&mut index.tool_bin_dirs, name, bin_dir);
}
continue;
}
for item in extract {
match item {
super::ToolExtract::Bin { .. } => {
let bin_dir = tool_dir.join("bin");
add_existing_dir(
&mut index.all_bin_dirs,
&mut all_bin_seen,
bin_dir.clone(),
);
add_tool_existing_dir(&mut index.tool_bin_dirs, name, bin_dir);
}
super::ToolExtract::Lib { path, env } => {
let lib_dir = tool_dir.join("lib");
add_existing_dir(
&mut index.all_lib_dirs,
&mut all_lib_seen,
lib_dir.clone(),
);
add_tool_existing_dir(
&mut index.tool_lib_dirs,
name,
lib_dir.clone(),
);
if let Some(var) = env {
let file_name = Path::new(&path)
.file_name()
.and_then(|n| n.to_str())
.unwrap_or(path.as_str());
let file_path = lib_dir.join(file_name);
upsert_file_env_export(
&mut index.file_env_exports,
&var,
file_path,
)?;
}
}
super::ToolExtract::Include { .. } => {
add_existing_dir(
&mut index.all_include_dirs,
&mut all_include_seen,
tool_dir.join("include"),
);
}
super::ToolExtract::PkgConfig { .. } => {
add_existing_dir(
&mut index.all_pkgconfig_dirs,
&mut all_pkgconfig_seen,
tool_dir.join("lib").join("pkgconfig"),
);
}
super::ToolExtract::File { path, env } => {
if let Some(var) = env {
let file_name = Path::new(&path)
.file_name()
.and_then(|n| n.to_str())
.unwrap_or(path.as_str());
let file_path = tool_dir.join("files").join(file_name);
upsert_file_env_export(
&mut index.file_env_exports,
&var,
file_path,
)?;
}
}
}
}
}
provider_name => {
let tool_dir = options
.cache_dir
.join(provider_name)
.join(name)
.join(&tool.version);
let bin_dir = tool_dir.join("bin");
let lib_dir = tool_dir.join("lib");
let include_dir = tool_dir.join("include");
let pkgconfig_dir = tool_dir.join("lib").join("pkgconfig");
add_existing_dir(&mut index.all_bin_dirs, &mut all_bin_seen, bin_dir.clone());
add_existing_dir(&mut index.all_lib_dirs, &mut all_lib_seen, lib_dir.clone());
add_existing_dir(
&mut index.all_include_dirs,
&mut all_include_seen,
include_dir.clone(),
);
add_existing_dir(
&mut index.all_pkgconfig_dirs,
&mut all_pkgconfig_seen,
pkgconfig_dir,
);
add_tool_existing_dir(&mut index.tool_bin_dirs, name, bin_dir);
add_tool_existing_dir(&mut index.tool_lib_dirs, name, lib_dir);
if tool_dir.join(name).exists() || tool_dir.join(format!("{name}.exe")).exists()
{
add_existing_dir(
&mut index.all_bin_dirs,
&mut all_bin_seen,
tool_dir.clone(),
);
add_tool_existing_dir(&mut index.tool_bin_dirs, name, tool_dir);
}
}
}
}
Ok(index)
}
}
fn add_existing_dir(paths: &mut Vec<PathBuf>, seen: &mut HashSet<PathBuf>, dir: PathBuf) {
if !dir.exists() {
return;
}
if seen.insert(dir.clone()) {
paths.push(dir);
}
}
fn add_tool_existing_dir(map: &mut BTreeMap<String, Vec<PathBuf>>, tool: &str, dir: PathBuf) {
if !dir.exists() {
return;
}
let dirs = map.entry(tool.to_string()).or_default();
if !dirs.contains(&dir) {
dirs.push(dir);
}
}
fn path_looks_like_library(path: &str) -> bool {
let ext_is = |target: &str| {
Path::new(path)
.extension()
.and_then(|ext| ext.to_str())
.is_some_and(|ext| ext.eq_ignore_ascii_case(target))
};
ext_is("dylib") || ext_is("so") || path.to_ascii_lowercase().contains(".so.") || ext_is("dll")
}
fn upsert_file_env_export(
exports: &mut BTreeMap<String, PathBuf>,
var: &str,
path: PathBuf,
) -> Result<()> {
match exports.get(var) {
Some(existing) if existing != &path => Err(Error::configuration(format!(
"Conflicting file env export for '{}': '{}' vs '{}'",
var,
existing.display(),
path.display()
))),
Some(_) => Ok(()),
None => {
exports.insert(var.to_string(), path);
Ok(())
}
}
}
fn join_paths(paths: &[PathBuf], separator: &str) -> String {
paths
.iter()
.map(|p| p.display().to_string())
.collect::<Vec<_>>()
.join(separator)
}
fn rustup_toolchain_dir(toolchain: &str, platform: &Platform) -> PathBuf {
let rustup_home = std::env::var("RUSTUP_HOME").map_or_else(
|_| {
dirs::home_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join(".rustup")
},
PathBuf::from,
);
rustup_home
.join("toolchains")
.join(format!("{toolchain}-{}", rustup_host_triple(platform)))
}
fn rustup_host_triple(platform: &Platform) -> String {
let arch = match platform.arch {
super::Arch::Arm64 => "aarch64",
super::Arch::X86_64 => "x86_64",
};
let os = match platform.os {
super::Os::Darwin => "apple-darwin",
super::Os::Linux => "unknown-linux-gnu",
};
format!("{arch}-{os}")
}
fn nix_profile_path_for_project(project_root: &Path) -> Result<PathBuf> {
let cache = crate::paths::cache_dir()?;
let project_id = project_profile_id(project_root);
Ok(cache.join("nix-profiles").join(project_id))
}
fn project_profile_id(project_root: &Path) -> String {
let canonical = project_root
.canonicalize()
.unwrap_or_else(|_| project_root.to_path_buf());
let mut hasher = Sha256::new();
hasher.update(canonical.to_string_lossy().as_bytes());
format!("{:x}", hasher.finalize())[..16].to_string()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::lockfile::{LockedTool, LockedToolPlatform, Lockfile};
use std::collections::BTreeMap;
use std::fs;
fn current_platform_key() -> String {
Platform::current().to_string()
}
#[test]
fn test_validate_missing_activation_is_allowed() {
let platform_key = current_platform_key();
let mut lockfile = Lockfile::new();
lockfile.tools.insert(
"jq".to_string(),
LockedTool {
version: "1.7.1".to_string(),
platforms: BTreeMap::from([(
platform_key,
LockedToolPlatform {
provider: "github".to_string(),
digest: "sha256:abc".to_string(),
source: serde_json::json!({
"type": "github",
"repo": "jqlang/jq",
"tag": "jq-1.7.1",
"asset": "jq-macos-arm64",
}),
size: None,
dependencies: vec![],
},
)]),
},
);
let temp = tempfile::tempdir().unwrap();
let lockfile_path = temp.path().join("cuenv.lock");
let options = ToolActivationResolveOptions::new(&lockfile, &lockfile_path);
let result = validate_tool_activation(&options);
assert!(result.is_ok());
}
#[test]
fn test_validate_rejects_per_tool_nix_reference() {
let platform_key = current_platform_key();
let mut lockfile = Lockfile::new();
lockfile.tools.insert(
"rust".to_string(),
LockedTool {
version: "1.0.0".to_string(),
platforms: BTreeMap::from([(
platform_key,
LockedToolPlatform {
provider: "nix".to_string(),
digest: "sha256:def".to_string(),
source: serde_json::json!({
"type": "nix",
"flake": "nixpkgs",
"package": "rustc",
}),
size: None,
dependencies: vec![],
},
)]),
},
);
lockfile.tools_activation = vec![ToolActivationStep {
var: "PATH".to_string(),
op: ToolActivationOperation::Prepend,
separator: ":".to_string(),
from: ToolActivationSource::ToolBinDir {
tool: "rust".to_string(),
},
}];
let temp = tempfile::tempdir().unwrap();
let lockfile_path = temp.path().join("cuenv.lock");
let options = ToolActivationResolveOptions::new(&lockfile, &lockfile_path);
let result = validate_tool_activation(&options);
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("do not support Nix tools")
);
}
#[test]
fn test_collect_with_failing_nix_profile_lookup_still_collects_non_nix_tools() {
let platform_key = current_platform_key();
let mut lockfile = Lockfile::new();
lockfile.tools.insert(
"jq".to_string(),
LockedTool {
version: "1.7.1".to_string(),
platforms: BTreeMap::from([(
platform_key,
LockedToolPlatform {
provider: "github".to_string(),
digest: "sha256:abc".to_string(),
source: serde_json::json!({
"type": "github",
"repo": "jqlang/jq",
"tag": "jq-1.7.1",
"asset": "jq",
}),
size: None,
dependencies: vec![],
},
)]),
},
);
let temp = tempfile::tempdir().unwrap();
let lockfile_path = temp.path().join("cuenv.lock");
let cache_dir = temp.path().join("cache");
let bin_dir = cache_dir
.join("github")
.join("jq")
.join("1.7.1")
.join("bin");
fs::create_dir_all(&bin_dir).unwrap();
let options =
ToolActivationResolveOptions::new(&lockfile, &lockfile_path).with_cache_dir(cache_dir);
let index = ToolPathIndex::collect_with(&options, |_| {
Err(Error::configuration("Could not determine cache directory"))
})
.unwrap();
assert_eq!(index.all_bin_dirs, vec![bin_dir.clone()]);
assert_eq!(index.tool_bin_dirs.get("jq"), Some(&vec![bin_dir]));
}
#[test]
fn test_collect_with_failing_nix_profile_lookup_skips_nix_tools() {
let platform_key = current_platform_key();
let mut lockfile = Lockfile::new();
lockfile.tools.insert(
"jq".to_string(),
LockedTool {
version: "1.7.1".to_string(),
platforms: BTreeMap::from([(
platform_key.clone(),
LockedToolPlatform {
provider: "github".to_string(),
digest: "sha256:abc".to_string(),
source: serde_json::json!({
"type": "github",
"repo": "jqlang/jq",
"tag": "jq-1.7.1",
"asset": "jq",
}),
size: None,
dependencies: vec![],
},
)]),
},
);
lockfile.tools.insert(
"rust".to_string(),
LockedTool {
version: "1.85.0".to_string(),
platforms: BTreeMap::from([(
platform_key,
LockedToolPlatform {
provider: "nix".to_string(),
digest: "sha256:def".to_string(),
source: serde_json::json!({
"type": "nix",
"flake": "nixpkgs",
"package": "rustc",
}),
size: None,
dependencies: vec![],
},
)]),
},
);
let temp = tempfile::tempdir().unwrap();
let lockfile_path = temp.path().join("cuenv.lock");
let cache_dir = temp.path().join("cache");
let bin_dir = cache_dir
.join("github")
.join("jq")
.join("1.7.1")
.join("bin");
fs::create_dir_all(&bin_dir).unwrap();
let options =
ToolActivationResolveOptions::new(&lockfile, &lockfile_path).with_cache_dir(cache_dir);
let index = ToolPathIndex::collect_with(&options, |_| {
Err(Error::configuration("Could not determine cache directory"))
})
.unwrap();
assert_eq!(index.all_bin_dirs, vec![bin_dir.clone()]);
assert_eq!(index.tool_bin_dirs.get("jq"), Some(&vec![bin_dir]));
assert!(!index.tool_bin_dirs.contains_key("rust"));
assert!(index.all_lib_dirs.is_empty());
}
#[test]
fn test_resolve_tool_activation_includes_file_env_exports() {
let platform = Platform::current();
let platform_key = platform.to_string();
let mut lockfile = Lockfile::new();
lockfile.tools.insert(
"foundationdb".to_string(),
LockedTool {
version: "7.3.63".to_string(),
platforms: BTreeMap::from([(
platform_key,
LockedToolPlatform {
provider: "github".to_string(),
digest: "sha256:abc".to_string(),
source: serde_json::json!({
"type": "github",
"repo": "apple/foundationdb",
"tag": "7.3.63",
"asset": "FoundationDB.pkg",
"extract": [
{"kind": "bin", "path": "bin/fdbcli"},
{"kind": "lib", "path": "lib/libfdb_c.dylib", "env": "FDB_CLIENT_LIB"},
{"kind": "file", "path": "etc/fdb.cluster", "env": "FDB_CLUSTER_FILE"},
{"kind": "include", "path": "include/foundationdb/fdb_c.h"},
{"kind": "pkgconfig", "path": "lib/pkgconfig/foundationdb.pc"}
],
}),
size: None,
dependencies: vec![],
},
)]),
},
);
let temp = tempfile::tempdir().unwrap();
let lockfile_path = temp.path().join("cuenv.lock");
let cache_dir = temp.path().join("cache");
let tool_dir = cache_dir.join("github").join("foundationdb").join("7.3.63");
let bin_dir = tool_dir.join("bin");
let lib_dir = tool_dir.join("lib");
let files_dir = tool_dir.join("files");
let include_dir = tool_dir.join("include");
let pkgconfig_dir = lib_dir.join("pkgconfig");
fs::create_dir_all(&bin_dir).unwrap();
fs::create_dir_all(&lib_dir).unwrap();
fs::create_dir_all(&files_dir).unwrap();
fs::create_dir_all(&include_dir).unwrap();
fs::create_dir_all(&pkgconfig_dir).unwrap();
fs::write(bin_dir.join("fdbcli"), "").unwrap();
fs::write(lib_dir.join("libfdb_c.dylib"), "").unwrap();
fs::write(files_dir.join("fdb.cluster"), "").unwrap();
fs::write(include_dir.join("fdb_c.h"), "").unwrap();
fs::write(pkgconfig_dir.join("foundationdb.pc"), "").unwrap();
let options = ToolActivationResolveOptions::new(&lockfile, &lockfile_path)
.with_platform(platform)
.with_cache_dir(cache_dir);
let steps = resolve_tool_activation(&options).unwrap();
assert!(
steps
.iter()
.any(|step| step.var == "PATH" && step.value == bin_dir.to_string_lossy())
);
assert!(steps.iter().any(|step| {
step.var == "FDB_CLIENT_LIB"
&& step.op == ToolActivationOperation::Set
&& step.value == lib_dir.join("libfdb_c.dylib").to_string_lossy()
}));
assert!(steps.iter().any(|step| {
step.var == "FDB_CLUSTER_FILE"
&& step.op == ToolActivationOperation::Set
&& step.value == files_dir.join("fdb.cluster").to_string_lossy()
}));
assert!(
steps
.iter()
.any(|step| step.var == "CPATH" && step.value == include_dir.to_string_lossy())
);
assert!(steps.iter().any(|step| {
step.var == "PKG_CONFIG_PATH" && step.value == pkgconfig_dir.to_string_lossy()
}));
}
#[test]
fn test_apply_activation_operations() {
let set_step = ResolvedToolActivationStep {
var: "PATH".to_string(),
op: ToolActivationOperation::Set,
separator: ":".to_string(),
value: "/a:/b".to_string(),
};
let prepend_step = ResolvedToolActivationStep {
var: "PATH".to_string(),
op: ToolActivationOperation::Prepend,
separator: ":".to_string(),
value: "/tools".to_string(),
};
let append_step = ResolvedToolActivationStep {
var: "PATH".to_string(),
op: ToolActivationOperation::Append,
separator: ":".to_string(),
value: "/tail".to_string(),
};
let set_value = apply_resolved_tool_activation(None, &set_step).unwrap();
assert_eq!(set_value, "/a:/b");
let prepend_value =
apply_resolved_tool_activation(Some("/usr/bin"), &prepend_step).unwrap();
assert_eq!(prepend_value, "/tools:/usr/bin");
let append_value = apply_resolved_tool_activation(Some("/usr/bin"), &append_step).unwrap();
assert_eq!(append_value, "/usr/bin:/tail");
}
}