1use super::{Platform, default_cache_dir};
8use crate::lockfile::Lockfile;
9use crate::{Error, Result};
10use serde::{Deserialize, Serialize};
11use sha2::{Digest, Sha256};
12use std::collections::{BTreeMap, HashSet};
13use std::path::{Path, PathBuf};
14
15#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
17#[serde(rename_all = "camelCase")]
18pub struct ToolActivationStep {
19 pub var: String,
21 pub op: ToolActivationOperation,
23 #[serde(default = "default_separator")]
25 pub separator: String,
26 pub from: ToolActivationSource,
28}
29
30fn default_separator() -> String {
31 ":".to_string()
32}
33
34#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
36#[serde(rename_all = "lowercase")]
37pub enum ToolActivationOperation {
38 Set,
40 Prepend,
42 Append,
44}
45
46#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
48#[serde(tag = "type", rename_all = "camelCase")]
49pub enum ToolActivationSource {
50 AllBinDirs,
52 AllLibDirs,
54 ToolBinDir { tool: String },
56 ToolLibDir { tool: String },
58}
59
60#[derive(Debug, Clone, PartialEq, Eq)]
62pub struct ResolvedToolActivationStep {
63 pub var: String,
65 pub op: ToolActivationOperation,
67 pub separator: String,
69 pub value: String,
71}
72
73#[derive(Debug, Clone)]
75pub struct ToolActivationResolveOptions<'a> {
76 pub lockfile: &'a Lockfile,
78 pub lockfile_path: &'a Path,
80 pub platform: Platform,
82 pub cache_dir: PathBuf,
84}
85
86impl<'a> ToolActivationResolveOptions<'a> {
87 #[must_use]
89 pub fn new(lockfile: &'a Lockfile, lockfile_path: &'a Path) -> Self {
90 Self {
91 lockfile,
92 lockfile_path,
93 platform: Platform::current(),
94 cache_dir: default_cache_dir(),
95 }
96 }
97
98 #[must_use]
100 pub fn with_platform(mut self, platform: Platform) -> Self {
101 self.platform = platform;
102 self
103 }
104
105 #[must_use]
107 pub fn with_cache_dir(mut self, cache_dir: PathBuf) -> Self {
108 self.cache_dir = cache_dir;
109 self
110 }
111}
112
113pub fn validate_tool_activation(options: &ToolActivationResolveOptions<'_>) -> Result<()> {
122 if options.lockfile.tools.is_empty() {
123 return Ok(());
124 }
125
126 if options.lockfile.tools_activation.is_empty() {
128 return Ok(());
129 }
130
131 let platform_key = options.platform.to_string();
132
133 for step in &options.lockfile.tools_activation {
134 if step.var.trim().is_empty() {
135 return Err(Error::configuration(
136 "Tool activation entry has an empty `var` value.",
137 ));
138 }
139
140 match &step.from {
141 ToolActivationSource::ToolBinDir { tool }
142 | ToolActivationSource::ToolLibDir { tool } => {
143 let Some(locked_tool) = options.lockfile.tools.get(tool) else {
144 return Err(Error::configuration(format!(
145 "Tool activation references unknown tool '{}'.",
146 tool
147 )));
148 };
149
150 if let Some(platform_data) = locked_tool.platforms.get(&platform_key)
151 && platform_data.provider == "nix"
152 {
153 return Err(Error::configuration(format!(
154 "Tool activation per-tool references do not support Nix tools yet ('{}'). \
155 Use allBinDirs/allLibDirs instead.",
156 tool
157 )));
158 }
159 }
160 ToolActivationSource::AllBinDirs | ToolActivationSource::AllLibDirs => {}
161 }
162 }
163
164 Ok(())
165}
166
167pub fn resolve_tool_activation(
173 options: &ToolActivationResolveOptions<'_>,
174) -> Result<Vec<ResolvedToolActivationStep>> {
175 let path_index = ToolPathIndex::collect(options)?;
176 if options.lockfile.tools_activation.is_empty() {
177 let mut resolved = vec![
178 ResolvedToolActivationStep {
179 var: "PATH".to_string(),
180 op: ToolActivationOperation::Prepend,
181 separator: ":".to_string(),
182 value: join_paths(&path_index.all_bin_dirs, ":"),
183 },
184 ResolvedToolActivationStep {
185 var: "DYLD_LIBRARY_PATH".to_string(),
186 op: ToolActivationOperation::Prepend,
187 separator: ":".to_string(),
188 value: join_paths(&path_index.all_lib_dirs, ":"),
189 },
190 ResolvedToolActivationStep {
191 var: "LD_LIBRARY_PATH".to_string(),
192 op: ToolActivationOperation::Prepend,
193 separator: ":".to_string(),
194 value: join_paths(&path_index.all_lib_dirs, ":"),
195 },
196 ResolvedToolActivationStep {
197 var: "CPATH".to_string(),
198 op: ToolActivationOperation::Prepend,
199 separator: ":".to_string(),
200 value: join_paths(&path_index.all_include_dirs, ":"),
201 },
202 ResolvedToolActivationStep {
203 var: "PKG_CONFIG_PATH".to_string(),
204 op: ToolActivationOperation::Prepend,
205 separator: ":".to_string(),
206 value: join_paths(&path_index.all_pkgconfig_dirs, ":"),
207 },
208 ];
209 for (var, value) in &path_index.file_env_exports {
210 resolved.push(ResolvedToolActivationStep {
211 var: var.clone(),
212 op: ToolActivationOperation::Set,
213 separator: ":".to_string(),
214 value: value.to_string_lossy().to_string(),
215 });
216 }
217 return Ok(resolved);
218 }
219
220 validate_tool_activation(options)?;
221 let mut resolved = Vec::with_capacity(options.lockfile.tools_activation.len());
222
223 for step in &options.lockfile.tools_activation {
224 let paths = match &step.from {
225 ToolActivationSource::AllBinDirs => path_index.all_bin_dirs.clone(),
226 ToolActivationSource::AllLibDirs => path_index.all_lib_dirs.clone(),
227 ToolActivationSource::ToolBinDir { tool } => path_index
228 .tool_bin_dirs
229 .get(tool)
230 .cloned()
231 .unwrap_or_default(),
232 ToolActivationSource::ToolLibDir { tool } => path_index
233 .tool_lib_dirs
234 .get(tool)
235 .cloned()
236 .unwrap_or_default(),
237 };
238
239 resolved.push(ResolvedToolActivationStep {
240 var: step.var.clone(),
241 op: step.op.clone(),
242 separator: step.separator.clone(),
243 value: join_paths(&paths, &step.separator),
244 });
245 }
246
247 Ok(resolved)
248}
249
250#[must_use]
254pub fn apply_resolved_tool_activation(
255 current: Option<&str>,
256 step: &ResolvedToolActivationStep,
257) -> Option<String> {
258 match step.op {
259 ToolActivationOperation::Set => Some(step.value.clone()),
260 ToolActivationOperation::Prepend => {
261 if step.value.is_empty() {
262 return None;
263 }
264 match current {
265 Some(existing) if !existing.is_empty() => {
266 Some(format!("{}{}{}", step.value, step.separator, existing))
267 }
268 _ => Some(step.value.clone()),
269 }
270 }
271 ToolActivationOperation::Append => {
272 if step.value.is_empty() {
273 return None;
274 }
275 match current {
276 Some(existing) if !existing.is_empty() => {
277 Some(format!("{}{}{}", existing, step.separator, step.value))
278 }
279 _ => Some(step.value.clone()),
280 }
281 }
282 }
283}
284
285#[derive(Debug, Default)]
286struct ToolPathIndex {
287 all_bin_dirs: Vec<PathBuf>,
288 all_lib_dirs: Vec<PathBuf>,
289 all_include_dirs: Vec<PathBuf>,
290 all_pkgconfig_dirs: Vec<PathBuf>,
291 tool_bin_dirs: BTreeMap<String, Vec<PathBuf>>,
292 tool_lib_dirs: BTreeMap<String, Vec<PathBuf>>,
293 file_env_exports: BTreeMap<String, PathBuf>,
294}
295
296impl ToolPathIndex {
297 fn collect(options: &ToolActivationResolveOptions<'_>) -> Result<Self> {
298 Self::collect_with(options, nix_profile_path_for_project)
299 }
300
301 fn collect_with<F>(
302 options: &ToolActivationResolveOptions<'_>,
303 nix_profile_path_for_project: F,
304 ) -> Result<Self>
305 where
306 F: Fn(&Path) -> Result<PathBuf>,
307 {
308 let mut index = Self::default();
309 let mut all_bin_seen = HashSet::new();
310 let mut all_lib_seen = HashSet::new();
311 let mut all_include_seen = HashSet::new();
312 let mut all_pkgconfig_seen = HashSet::new();
313 let platform_key = options.platform.to_string();
314 let lockfile_dir = options.lockfile_path.parent().unwrap_or(Path::new("."));
315 let mut nix_profile_path: Option<Option<PathBuf>> = None;
316
317 for (name, tool) in &options.lockfile.tools {
318 let Some(platform_data) = tool.platforms.get(&platform_key) else {
319 continue;
320 };
321
322 match platform_data.provider.as_str() {
323 "nix" => {
324 let profile_path = nix_profile_path
327 .get_or_insert_with(|| nix_profile_path_for_project(lockfile_dir).ok());
328 let Some(profile_path) = profile_path.as_ref() else {
329 continue;
330 };
331 add_existing_dir(
332 &mut index.all_bin_dirs,
333 &mut all_bin_seen,
334 profile_path.join("bin"),
335 );
336 add_existing_dir(
337 &mut index.all_lib_dirs,
338 &mut all_lib_seen,
339 profile_path.join("lib"),
340 );
341 add_existing_dir(
342 &mut index.all_include_dirs,
343 &mut all_include_seen,
344 profile_path.join("include"),
345 );
346 add_existing_dir(
347 &mut index.all_pkgconfig_dirs,
348 &mut all_pkgconfig_seen,
349 profile_path.join("lib").join("pkgconfig"),
350 );
351 add_tool_existing_dir(&mut index.tool_bin_dirs, name, profile_path.join("bin"));
352 add_tool_existing_dir(&mut index.tool_lib_dirs, name, profile_path.join("lib"));
353 }
354 "rustup" => {
355 let toolchain = platform_data
356 .source
357 .get("toolchain")
358 .and_then(|v| v.as_str())
359 .unwrap_or("stable");
360 let rustup_dir = rustup_toolchain_dir(toolchain, &options.platform);
361 add_existing_dir(
362 &mut index.all_bin_dirs,
363 &mut all_bin_seen,
364 rustup_dir.join("bin"),
365 );
366 add_existing_dir(
367 &mut index.all_lib_dirs,
368 &mut all_lib_seen,
369 rustup_dir.join("lib"),
370 );
371 add_existing_dir(
372 &mut index.all_include_dirs,
373 &mut all_include_seen,
374 rustup_dir.join("include"),
375 );
376 add_existing_dir(
377 &mut index.all_pkgconfig_dirs,
378 &mut all_pkgconfig_seen,
379 rustup_dir.join("lib").join("pkgconfig"),
380 );
381 add_tool_existing_dir(&mut index.tool_bin_dirs, name, rustup_dir.join("bin"));
382 add_tool_existing_dir(&mut index.tool_lib_dirs, name, rustup_dir.join("lib"));
383 }
384 "github" => {
385 let tool_dir = options
386 .cache_dir
387 .join("github")
388 .join(name)
389 .join(&tool.version);
390 let extract: Vec<super::ToolExtract> = platform_data
391 .source
392 .get("extract")
393 .cloned()
394 .and_then(|value| serde_json::from_value(value).ok())
395 .unwrap_or_default();
396
397 if extract.is_empty() {
398 let path_hint_is_lib = platform_data
400 .source
401 .get("path")
402 .and_then(|v| v.as_str())
403 .is_some_and(path_looks_like_library);
404 if path_hint_is_lib {
405 let lib_dir = tool_dir.join("lib");
406 add_existing_dir(
407 &mut index.all_lib_dirs,
408 &mut all_lib_seen,
409 lib_dir.clone(),
410 );
411 add_tool_existing_dir(&mut index.tool_lib_dirs, name, lib_dir);
412 } else {
413 let bin_dir = tool_dir.join("bin");
414 add_existing_dir(
415 &mut index.all_bin_dirs,
416 &mut all_bin_seen,
417 bin_dir.clone(),
418 );
419 add_tool_existing_dir(&mut index.tool_bin_dirs, name, bin_dir);
420 }
421 continue;
422 }
423
424 for item in extract {
425 match item {
426 super::ToolExtract::Bin { .. } => {
427 let bin_dir = tool_dir.join("bin");
428 add_existing_dir(
429 &mut index.all_bin_dirs,
430 &mut all_bin_seen,
431 bin_dir.clone(),
432 );
433 add_tool_existing_dir(&mut index.tool_bin_dirs, name, bin_dir);
434 }
435 super::ToolExtract::Lib { path, env } => {
436 let lib_dir = tool_dir.join("lib");
437 add_existing_dir(
438 &mut index.all_lib_dirs,
439 &mut all_lib_seen,
440 lib_dir.clone(),
441 );
442 add_tool_existing_dir(
443 &mut index.tool_lib_dirs,
444 name,
445 lib_dir.clone(),
446 );
447 if let Some(var) = env {
448 let file_name = Path::new(&path)
449 .file_name()
450 .and_then(|n| n.to_str())
451 .unwrap_or(path.as_str());
452 let file_path = lib_dir.join(file_name);
453 upsert_file_env_export(
454 &mut index.file_env_exports,
455 &var,
456 file_path,
457 )?;
458 }
459 }
460 super::ToolExtract::Include { .. } => {
461 add_existing_dir(
462 &mut index.all_include_dirs,
463 &mut all_include_seen,
464 tool_dir.join("include"),
465 );
466 }
467 super::ToolExtract::PkgConfig { .. } => {
468 add_existing_dir(
469 &mut index.all_pkgconfig_dirs,
470 &mut all_pkgconfig_seen,
471 tool_dir.join("lib").join("pkgconfig"),
472 );
473 }
474 super::ToolExtract::File { path, env } => {
475 if let Some(var) = env {
476 let file_name = Path::new(&path)
477 .file_name()
478 .and_then(|n| n.to_str())
479 .unwrap_or(path.as_str());
480 let file_path = tool_dir.join("files").join(file_name);
481 upsert_file_env_export(
482 &mut index.file_env_exports,
483 &var,
484 file_path,
485 )?;
486 }
487 }
488 }
489 }
490 }
491 provider_name => {
492 let tool_dir = options
493 .cache_dir
494 .join(provider_name)
495 .join(name)
496 .join(&tool.version);
497 let bin_dir = tool_dir.join("bin");
498 let lib_dir = tool_dir.join("lib");
499 let include_dir = tool_dir.join("include");
500 let pkgconfig_dir = tool_dir.join("lib").join("pkgconfig");
501 add_existing_dir(&mut index.all_bin_dirs, &mut all_bin_seen, bin_dir.clone());
502 add_existing_dir(&mut index.all_lib_dirs, &mut all_lib_seen, lib_dir.clone());
503 add_existing_dir(
504 &mut index.all_include_dirs,
505 &mut all_include_seen,
506 include_dir.clone(),
507 );
508 add_existing_dir(
509 &mut index.all_pkgconfig_dirs,
510 &mut all_pkgconfig_seen,
511 pkgconfig_dir,
512 );
513 add_tool_existing_dir(&mut index.tool_bin_dirs, name, bin_dir);
514 add_tool_existing_dir(&mut index.tool_lib_dirs, name, lib_dir);
515
516 if tool_dir.join(name).exists() || tool_dir.join(format!("{name}.exe")).exists()
518 {
519 add_existing_dir(
520 &mut index.all_bin_dirs,
521 &mut all_bin_seen,
522 tool_dir.clone(),
523 );
524 add_tool_existing_dir(&mut index.tool_bin_dirs, name, tool_dir);
525 }
526 }
527 }
528 }
529
530 Ok(index)
531 }
532}
533
534fn add_existing_dir(paths: &mut Vec<PathBuf>, seen: &mut HashSet<PathBuf>, dir: PathBuf) {
535 if !dir.exists() {
536 return;
537 }
538 if seen.insert(dir.clone()) {
539 paths.push(dir);
540 }
541}
542
543fn add_tool_existing_dir(map: &mut BTreeMap<String, Vec<PathBuf>>, tool: &str, dir: PathBuf) {
544 if !dir.exists() {
545 return;
546 }
547 let dirs = map.entry(tool.to_string()).or_default();
548 if !dirs.contains(&dir) {
549 dirs.push(dir);
550 }
551}
552
553fn path_looks_like_library(path: &str) -> bool {
554 let ext_is = |target: &str| {
555 Path::new(path)
556 .extension()
557 .and_then(|ext| ext.to_str())
558 .is_some_and(|ext| ext.eq_ignore_ascii_case(target))
559 };
560 ext_is("dylib") || ext_is("so") || path.to_ascii_lowercase().contains(".so.") || ext_is("dll")
561}
562
563fn upsert_file_env_export(
564 exports: &mut BTreeMap<String, PathBuf>,
565 var: &str,
566 path: PathBuf,
567) -> Result<()> {
568 match exports.get(var) {
569 Some(existing) if existing != &path => Err(Error::configuration(format!(
570 "Conflicting file env export for '{}': '{}' vs '{}'",
571 var,
572 existing.display(),
573 path.display()
574 ))),
575 Some(_) => Ok(()),
576 None => {
577 exports.insert(var.to_string(), path);
578 Ok(())
579 }
580 }
581}
582
583fn join_paths(paths: &[PathBuf], separator: &str) -> String {
584 paths
585 .iter()
586 .map(|p| p.display().to_string())
587 .collect::<Vec<_>>()
588 .join(separator)
589}
590
591fn rustup_toolchain_dir(toolchain: &str, platform: &Platform) -> PathBuf {
592 let rustup_home = std::env::var("RUSTUP_HOME").map_or_else(
593 |_| {
594 dirs::home_dir()
595 .unwrap_or_else(|| PathBuf::from("."))
596 .join(".rustup")
597 },
598 PathBuf::from,
599 );
600 rustup_home
601 .join("toolchains")
602 .join(format!("{toolchain}-{}", rustup_host_triple(platform)))
603}
604
605fn rustup_host_triple(platform: &Platform) -> String {
606 let arch = match platform.arch {
607 super::Arch::Arm64 => "aarch64",
608 super::Arch::X86_64 => "x86_64",
609 };
610
611 let os = match platform.os {
612 super::Os::Darwin => "apple-darwin",
613 super::Os::Linux => "unknown-linux-gnu",
614 };
615
616 format!("{arch}-{os}")
617}
618
619fn nix_profile_path_for_project(project_root: &Path) -> Result<PathBuf> {
620 let cache = crate::paths::cache_dir()?;
621 let project_id = project_profile_id(project_root);
622 Ok(cache.join("nix-profiles").join(project_id))
623}
624
625fn project_profile_id(project_root: &Path) -> String {
626 let canonical = project_root
627 .canonicalize()
628 .unwrap_or_else(|_| project_root.to_path_buf());
629 let mut hasher = Sha256::new();
630 hasher.update(canonical.to_string_lossy().as_bytes());
631 format!("{:x}", hasher.finalize())[..16].to_string()
632}
633
634#[cfg(test)]
635mod tests {
636 use super::*;
637 use crate::lockfile::{LockedTool, LockedToolPlatform, Lockfile};
638 use std::collections::BTreeMap;
639 use std::fs;
640
641 fn current_platform_key() -> String {
642 Platform::current().to_string()
643 }
644
645 #[test]
646 fn test_validate_missing_activation_is_allowed() {
647 let platform_key = current_platform_key();
648 let mut lockfile = Lockfile::new();
649 lockfile.tools.insert(
650 "jq".to_string(),
651 LockedTool {
652 version: "1.7.1".to_string(),
653 platforms: BTreeMap::from([(
654 platform_key,
655 LockedToolPlatform {
656 provider: "github".to_string(),
657 digest: "sha256:abc".to_string(),
658 source: serde_json::json!({
659 "type": "github",
660 "repo": "jqlang/jq",
661 "tag": "jq-1.7.1",
662 "asset": "jq-macos-arm64",
663 }),
664 size: None,
665 dependencies: vec![],
666 },
667 )]),
668 },
669 );
670
671 let temp = tempfile::tempdir().unwrap();
672 let lockfile_path = temp.path().join("cuenv.lock");
673 let options = ToolActivationResolveOptions::new(&lockfile, &lockfile_path);
674 let result = validate_tool_activation(&options);
675
676 assert!(result.is_ok());
677 }
678
679 #[test]
680 fn test_validate_rejects_per_tool_nix_reference() {
681 let platform_key = current_platform_key();
682 let mut lockfile = Lockfile::new();
683 lockfile.tools.insert(
684 "rust".to_string(),
685 LockedTool {
686 version: "1.0.0".to_string(),
687 platforms: BTreeMap::from([(
688 platform_key,
689 LockedToolPlatform {
690 provider: "nix".to_string(),
691 digest: "sha256:def".to_string(),
692 source: serde_json::json!({
693 "type": "nix",
694 "flake": "nixpkgs",
695 "package": "rustc",
696 }),
697 size: None,
698 dependencies: vec![],
699 },
700 )]),
701 },
702 );
703 lockfile.tools_activation = vec![ToolActivationStep {
704 var: "PATH".to_string(),
705 op: ToolActivationOperation::Prepend,
706 separator: ":".to_string(),
707 from: ToolActivationSource::ToolBinDir {
708 tool: "rust".to_string(),
709 },
710 }];
711
712 let temp = tempfile::tempdir().unwrap();
713 let lockfile_path = temp.path().join("cuenv.lock");
714 let options = ToolActivationResolveOptions::new(&lockfile, &lockfile_path);
715 let result = validate_tool_activation(&options);
716
717 assert!(result.is_err());
718 assert!(
719 result
720 .unwrap_err()
721 .to_string()
722 .contains("do not support Nix tools")
723 );
724 }
725
726 #[test]
727 fn test_collect_with_failing_nix_profile_lookup_still_collects_non_nix_tools() {
728 let platform_key = current_platform_key();
729 let mut lockfile = Lockfile::new();
730 lockfile.tools.insert(
731 "jq".to_string(),
732 LockedTool {
733 version: "1.7.1".to_string(),
734 platforms: BTreeMap::from([(
735 platform_key,
736 LockedToolPlatform {
737 provider: "github".to_string(),
738 digest: "sha256:abc".to_string(),
739 source: serde_json::json!({
740 "type": "github",
741 "repo": "jqlang/jq",
742 "tag": "jq-1.7.1",
743 "asset": "jq",
744 }),
745 size: None,
746 dependencies: vec![],
747 },
748 )]),
749 },
750 );
751
752 let temp = tempfile::tempdir().unwrap();
753 let lockfile_path = temp.path().join("cuenv.lock");
754 let cache_dir = temp.path().join("cache");
755 let bin_dir = cache_dir
756 .join("github")
757 .join("jq")
758 .join("1.7.1")
759 .join("bin");
760 fs::create_dir_all(&bin_dir).unwrap();
761
762 let options =
763 ToolActivationResolveOptions::new(&lockfile, &lockfile_path).with_cache_dir(cache_dir);
764 let index = ToolPathIndex::collect_with(&options, |_| {
765 Err(Error::configuration("Could not determine cache directory"))
766 })
767 .unwrap();
768
769 assert_eq!(index.all_bin_dirs, vec![bin_dir.clone()]);
770 assert_eq!(index.tool_bin_dirs.get("jq"), Some(&vec![bin_dir]));
771 }
772
773 #[test]
774 fn test_collect_with_failing_nix_profile_lookup_skips_nix_tools() {
775 let platform_key = current_platform_key();
776 let mut lockfile = Lockfile::new();
777 lockfile.tools.insert(
778 "jq".to_string(),
779 LockedTool {
780 version: "1.7.1".to_string(),
781 platforms: BTreeMap::from([(
782 platform_key.clone(),
783 LockedToolPlatform {
784 provider: "github".to_string(),
785 digest: "sha256:abc".to_string(),
786 source: serde_json::json!({
787 "type": "github",
788 "repo": "jqlang/jq",
789 "tag": "jq-1.7.1",
790 "asset": "jq",
791 }),
792 size: None,
793 dependencies: vec![],
794 },
795 )]),
796 },
797 );
798 lockfile.tools.insert(
799 "rust".to_string(),
800 LockedTool {
801 version: "1.85.0".to_string(),
802 platforms: BTreeMap::from([(
803 platform_key,
804 LockedToolPlatform {
805 provider: "nix".to_string(),
806 digest: "sha256:def".to_string(),
807 source: serde_json::json!({
808 "type": "nix",
809 "flake": "nixpkgs",
810 "package": "rustc",
811 }),
812 size: None,
813 dependencies: vec![],
814 },
815 )]),
816 },
817 );
818
819 let temp = tempfile::tempdir().unwrap();
820 let lockfile_path = temp.path().join("cuenv.lock");
821 let cache_dir = temp.path().join("cache");
822 let bin_dir = cache_dir
823 .join("github")
824 .join("jq")
825 .join("1.7.1")
826 .join("bin");
827 fs::create_dir_all(&bin_dir).unwrap();
828
829 let options =
830 ToolActivationResolveOptions::new(&lockfile, &lockfile_path).with_cache_dir(cache_dir);
831 let index = ToolPathIndex::collect_with(&options, |_| {
832 Err(Error::configuration("Could not determine cache directory"))
833 })
834 .unwrap();
835
836 assert_eq!(index.all_bin_dirs, vec![bin_dir.clone()]);
837 assert_eq!(index.tool_bin_dirs.get("jq"), Some(&vec![bin_dir]));
838 assert!(!index.tool_bin_dirs.contains_key("rust"));
839 assert!(index.all_lib_dirs.is_empty());
840 }
841
842 #[test]
843 fn test_resolve_tool_activation_includes_file_env_exports() {
844 let platform = Platform::current();
845 let platform_key = platform.to_string();
846 let mut lockfile = Lockfile::new();
847 lockfile.tools.insert(
848 "foundationdb".to_string(),
849 LockedTool {
850 version: "7.3.63".to_string(),
851 platforms: BTreeMap::from([(
852 platform_key,
853 LockedToolPlatform {
854 provider: "github".to_string(),
855 digest: "sha256:abc".to_string(),
856 source: serde_json::json!({
857 "type": "github",
858 "repo": "apple/foundationdb",
859 "tag": "7.3.63",
860 "asset": "FoundationDB.pkg",
861 "extract": [
862 {"kind": "bin", "path": "bin/fdbcli"},
863 {"kind": "lib", "path": "lib/libfdb_c.dylib", "env": "FDB_CLIENT_LIB"},
864 {"kind": "file", "path": "etc/fdb.cluster", "env": "FDB_CLUSTER_FILE"},
865 {"kind": "include", "path": "include/foundationdb/fdb_c.h"},
866 {"kind": "pkgconfig", "path": "lib/pkgconfig/foundationdb.pc"}
867 ],
868 }),
869 size: None,
870 dependencies: vec![],
871 },
872 )]),
873 },
874 );
875
876 let temp = tempfile::tempdir().unwrap();
877 let lockfile_path = temp.path().join("cuenv.lock");
878 let cache_dir = temp.path().join("cache");
879 let tool_dir = cache_dir.join("github").join("foundationdb").join("7.3.63");
880 let bin_dir = tool_dir.join("bin");
881 let lib_dir = tool_dir.join("lib");
882 let files_dir = tool_dir.join("files");
883 let include_dir = tool_dir.join("include");
884 let pkgconfig_dir = lib_dir.join("pkgconfig");
885 fs::create_dir_all(&bin_dir).unwrap();
886 fs::create_dir_all(&lib_dir).unwrap();
887 fs::create_dir_all(&files_dir).unwrap();
888 fs::create_dir_all(&include_dir).unwrap();
889 fs::create_dir_all(&pkgconfig_dir).unwrap();
890 fs::write(bin_dir.join("fdbcli"), "").unwrap();
891 fs::write(lib_dir.join("libfdb_c.dylib"), "").unwrap();
892 fs::write(files_dir.join("fdb.cluster"), "").unwrap();
893 fs::write(include_dir.join("fdb_c.h"), "").unwrap();
894 fs::write(pkgconfig_dir.join("foundationdb.pc"), "").unwrap();
895
896 let options = ToolActivationResolveOptions::new(&lockfile, &lockfile_path)
897 .with_platform(platform)
898 .with_cache_dir(cache_dir);
899 let steps = resolve_tool_activation(&options).unwrap();
900
901 assert!(
902 steps
903 .iter()
904 .any(|step| step.var == "PATH" && step.value == bin_dir.to_string_lossy())
905 );
906 assert!(steps.iter().any(|step| {
907 step.var == "FDB_CLIENT_LIB"
908 && step.op == ToolActivationOperation::Set
909 && step.value == lib_dir.join("libfdb_c.dylib").to_string_lossy()
910 }));
911 assert!(steps.iter().any(|step| {
912 step.var == "FDB_CLUSTER_FILE"
913 && step.op == ToolActivationOperation::Set
914 && step.value == files_dir.join("fdb.cluster").to_string_lossy()
915 }));
916 assert!(
917 steps
918 .iter()
919 .any(|step| step.var == "CPATH" && step.value == include_dir.to_string_lossy())
920 );
921 assert!(steps.iter().any(|step| {
922 step.var == "PKG_CONFIG_PATH" && step.value == pkgconfig_dir.to_string_lossy()
923 }));
924 }
925
926 #[test]
927 fn test_apply_activation_operations() {
928 let set_step = ResolvedToolActivationStep {
929 var: "PATH".to_string(),
930 op: ToolActivationOperation::Set,
931 separator: ":".to_string(),
932 value: "/a:/b".to_string(),
933 };
934 let prepend_step = ResolvedToolActivationStep {
935 var: "PATH".to_string(),
936 op: ToolActivationOperation::Prepend,
937 separator: ":".to_string(),
938 value: "/tools".to_string(),
939 };
940 let append_step = ResolvedToolActivationStep {
941 var: "PATH".to_string(),
942 op: ToolActivationOperation::Append,
943 separator: ":".to_string(),
944 value: "/tail".to_string(),
945 };
946
947 let set_value = apply_resolved_tool_activation(None, &set_step).unwrap();
948 assert_eq!(set_value, "/a:/b");
949
950 let prepend_value =
951 apply_resolved_tool_activation(Some("/usr/bin"), &prepend_step).unwrap();
952 assert_eq!(prepend_value, "/tools:/usr/bin");
953
954 let append_value = apply_resolved_tool_activation(Some("/usr/bin"), &append_step).unwrap();
955 assert_eq!(append_value, "/usr/bin:/tail");
956 }
957}