1use super_process::exe::Argv;
7
8use displaydoc::Display;
9use thiserror::Error;
10
11use std::{
12 ffi::{OsStr, OsString},
13 io::{self, Write},
14 path::PathBuf,
15};
16
17#[derive(Debug, Display, Clone)]
22#[ignore_extra_doc_attributes]
23pub struct CLISpec(pub String);
24
25impl From<&str> for CLISpec {
26 fn from(value: &str) -> Self { Self(value.to_string()) }
27}
28
29impl CLISpec {
30 pub fn new<R: AsRef<str>>(r: R) -> Self { Self(r.as_ref().to_string()) }
32}
33
34pub trait ArgvWrapper {
35 fn modify_argv(self, args: &mut Argv);
36}
37
38#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
39pub struct EnvName(pub String);
40
41impl ArgvWrapper for EnvName {
42 fn modify_argv(self, args: &mut Argv) {
43 args.unshift(OsString::from(self.0));
44 args.unshift(OsStr::new("--env").to_os_string());
45 }
46}
47
48#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
49pub struct RepoDirs(pub Vec<PathBuf>);
50
51impl ArgvWrapper for RepoDirs {
53 fn modify_argv(mut self, args: &mut Argv) {
54 if self.0.is_empty() {
55 return;
56 }
57 assert!(self.0.iter().all(|p| p.is_absolute()));
58 self.0.push("$spack/var/spack/repos/spack_repo/builtin".into());
59 let joined = self
60 .0
61 .into_iter()
62 .chain([])
63 .map(|p| format!("{}", p.display()))
64 .collect::<Vec<_>>()
65 .join(",");
66 let config_arg = format!("repos:[{}]", joined);
67 args.unshift(OsString::from(config_arg));
68 args.unshift(OsStr::new("-c").to_os_string());
69 }
70}
71
72#[derive(Debug, Display, Error)]
74pub enum CommandError {
75 Config(config::Config, #[source] config::ConfigError),
77 Find(find::Find, #[source] find::FindError),
79 FindPrefix(find::FindPrefix, #[source] find::FindError),
81 Load(load::Load, #[source] load::LoadError),
83 Install(install::Install, #[source] install::InstallError),
85 BuildEnv(build_env::BuildEnv, #[source] build_env::BuildEnvError),
87 CompilerFind(
89 compiler_find::CompilerFind,
90 #[source] compiler_find::CompilerFindError,
91 ),
92 FindCompilerSpecs(
94 compiler_find::FindCompilerSpecs,
95 #[source] compiler_find::CompilerFindError,
96 ),
97}
98
99pub mod config {
100 use super::*;
101 use crate::SpackInvocation;
102 use super_process::{
103 base::{self, CommandBase},
104 exe,
105 sync::SyncInvocable,
106 };
107
108 use async_trait::async_trait;
109 use once_cell::sync::Lazy;
110 use serde::{Deserialize, Serialize};
111 use serde_yaml;
112
113 use std::{ffi::OsStr, fmt::Debug};
114
115 #[derive(Debug, Display, Error)]
117 pub enum ConfigError {
118 Command(#[from] exe::CommandErrorWrapper),
120 Setup(#[from] base::SetupErrorWrapper),
122 Yaml(#[from] serde_yaml::Error),
124 YamlManipulation(String, &'static str),
126 }
127
128 impl ConfigError {
129 pub fn yaml_manipulation<D: Debug>(yaml_source: D, msg: &'static str) -> Self {
130 Self::YamlManipulation(format!("{:?}", yaml_source), msg)
131 }
132 }
133
134 #[derive(Debug, Clone)]
135 pub struct Config {
136 #[allow(missing_docs)]
137 pub spack: SpackInvocation,
138 pub scope: Option<String>,
140 pub passthrough: exe::Argv,
141 }
142
143 pub trait ConfigCommand {
144 fn into_base_config(self) -> Config;
145 }
146
147 #[async_trait]
148 impl CommandBase for Config {
149 async fn setup_command(self) -> Result<exe::Command, base::SetupError> {
150 let Self {
151 spack,
152 scope,
153 passthrough,
154 } = self;
155 let scope_args = if let Some(scope) = &scope {
156 vec!["--scope", scope]
157 } else {
158 vec![]
159 };
160 let argv = exe::Argv(
161 ["config"]
162 .into_iter()
163 .chain(scope_args)
164 .map(|s| OsStr::new(s).to_os_string())
165 .chain(passthrough.0)
166 .collect(),
167 );
168 Ok(
169 spack
170 .with_spack_exe(exe::Command {
171 argv,
172 ..Default::default()
173 })
174 .setup_command()
175 .await?,
176 )
177 }
178 }
179
180 #[derive(Debug, Clone)]
183 struct Get {
184 #[allow(missing_docs)]
185 pub spack: SpackInvocation,
186 pub scope: Option<String>,
188 pub section: String,
190 }
191
192 impl ConfigCommand for Get {
193 fn into_base_config(self) -> Config {
194 let Self {
195 spack,
196 scope,
197 section,
198 } = self;
199 Config {
200 spack,
201 scope,
202 passthrough: ["get", §ion].into(),
203 }
204 }
205 }
206
207 #[derive(Debug, Display, Serialize, Deserialize, Clone)]
209 pub struct CompilerPaths {
210 pub cc: Option<PathBuf>,
212 pub cxx: Option<PathBuf>,
214 pub f77: Option<PathBuf>,
216 pub fc: Option<PathBuf>,
218 }
219
220 #[derive(Debug, Display, Serialize, Deserialize, Clone)]
222 pub struct CompilerSpec {
223 pub spec: String,
226 pub paths: CompilerPaths,
228 flags: serde_yaml::Value,
229 operating_system: String,
230 target: String,
231 modules: serde_yaml::Value,
232 environment: serde_yaml::Value,
233 extra_rpaths: serde_yaml::Value,
234 }
235
236 #[derive(Debug, Clone)]
238 pub struct GetCompilers {
239 #[allow(missing_docs)]
240 pub spack: SpackInvocation,
241 pub scope: Option<String>,
243 }
244
245 impl ConfigCommand for GetCompilers {
246 fn into_base_config(self) -> Config {
247 let Self { spack, scope } = self;
248 let get = Get {
249 spack,
250 scope,
251 section: "compilers".to_string(),
252 };
253 get.into_base_config()
254 }
255 }
256
257 impl GetCompilers {
258 pub async fn get_compilers(self) -> Result<Vec<CompilerSpec>, ConfigError> {
260 let config_request = self.into_base_config();
261 let command = config_request
262 .setup_command()
263 .await
264 .map_err(|e| e.with_context("in GetCompilers::get_compilers()".to_string()))?;
265 let output = command.invoke().await?;
266
267 let top_level: serde_yaml::Value = serde_yaml::from_slice(&output.stdout)?;
268
269 static TOP_LEVEL_KEY: Lazy<serde_yaml::Value> =
270 Lazy::new(|| serde_yaml::Value::String("compilers".to_string()));
271 static SECOND_KEY: Lazy<serde_yaml::Value> =
272 Lazy::new(|| serde_yaml::Value::String("compiler".to_string()));
273
274 let compiler_objects: Vec<&serde_yaml::Value> = top_level
275 .as_mapping()
276 .and_then(|m| m.get(&TOP_LEVEL_KEY))
277 .and_then(|c| c.as_sequence())
278 .ok_or_else(|| {
279 ConfigError::yaml_manipulation(
280 &top_level,
281 "expected top-level YAML to be a mapping with key 'compilers'",
282 )
283 })?
284 .iter()
285 .map(|o| {
286 o.as_mapping()
287 .and_then(|c| c.get(&SECOND_KEY))
288 .ok_or_else(|| {
289 ConfigError::yaml_manipulation(
290 o,
291 "expected 'compilers' entries to be mappings with key 'compiler'",
292 )
293 })
294 })
295 .collect::<Result<Vec<_>, _>>()?;
296 let compiler_specs: Vec<CompilerSpec> = compiler_objects
297 .into_iter()
298 .map(|v| serde_yaml::from_value(v.clone()))
299 .collect::<Result<Vec<CompilerSpec>, _>>()?;
300
301 Ok(compiler_specs)
302 }
303 }
304
305 #[cfg(test)]
306 mod test {
307 use tokio;
308
309 #[tokio::test]
310 async fn test_get_compilers() -> Result<(), crate::Error> {
311 use crate::{
312 commands::{config::*, CommandError},
313 SpackInvocation,
314 };
315 use super_process::{exe, sync::SyncInvocable};
316
317 let spack = SpackInvocation::summon().await?;
319
320 let get_compilers = GetCompilers { spack, scope: None };
322 let found_compilers = get_compilers
323 .clone()
324 .get_compilers()
325 .await
326 .map_err(|e| CommandError::Config(get_compilers.into_base_config(), e))?;
327 assert!(!found_compilers.is_empty());
328
329 let first_cc: exe::Exe = found_compilers[0]
331 .paths
332 .cc
333 .as_ref()
334 .expect("cc should have been defined")
335 .into();
336 let command = exe::Command {
337 exe: first_cc,
338 argv: ["--version"].into(),
339 ..Default::default()
340 };
341 let output = command
342 .invoke()
343 .await
344 .expect("cc --version should have succeeded");
345 assert!(!output.stdout.is_empty());
346 Ok(())
347 }
348 }
349}
350
351pub mod find {
353 use super::*;
354 use crate::{utils::prefix, SpackInvocation};
355 use super_process::{
356 base::{self, CommandBase},
357 exe,
358 sync::SyncInvocable,
359 };
360
361 use async_trait::async_trait;
362 use once_cell::sync::Lazy;
363 use regex::Regex;
364 use serde::{Deserialize, Serialize};
365 use serde_json;
366
367 use std::{ffi::OsStr, str};
368
369 #[derive(Debug, Display, Serialize, Deserialize, Clone)]
371 pub struct FoundSpec {
372 pub name: String,
374 pub version: ConcreteVersion,
376 pub arch: serde_json::Value,
377 pub namespace: String,
378 pub parameters: serde_json::Value,
379 pub package_hash: String,
380 pub dependencies: Option<serde_json::Value>,
381 pub annotations: serde_json::Value,
382 pub hash: String,
384 }
385
386 impl FoundSpec {
387 pub fn hashed_spec(&self) -> CLISpec { CLISpec(format!("{}/{}", &self.name, &self.hash)) }
389 }
390
391 #[derive(Debug, Display, Serialize, Deserialize, Clone)]
393 pub struct ConcreteVersion(pub String);
394
395 #[derive(Debug, Display, Error)]
397 pub enum FindError {
398 Command(#[from] exe::CommandErrorWrapper),
400 Setup(#[from] base::SetupErrorWrapper),
402 Install(#[from] install::InstallError),
404 Parse(String),
406 Json(#[from] serde_json::Error),
408 }
409
410 #[derive(Debug, Clone)]
412 pub struct Find {
413 pub spack: SpackInvocation,
414 pub spec: CLISpec,
415 pub env: Option<EnvName>,
416 pub repos: Option<RepoDirs>,
417 }
418
419 #[async_trait]
420 impl CommandBase for Find {
421 async fn setup_command(self) -> Result<exe::Command, base::SetupError> {
422 let Self {
423 spack,
424 spec,
425 env,
426 repos,
427 } = self;
428 let mut args = exe::Argv(
429 ["find", "--json", &spec.0]
430 .into_iter()
431 .map(|s| OsStr::new(s).to_os_string())
432 .collect(),
433 );
434 if let Some(env) = env {
435 env.modify_argv(&mut args);
436 }
437 if let Some(repos) = repos {
438 repos.modify_argv(&mut args);
439 }
440 Ok(
441 spack
442 .with_spack_exe(exe::Command {
443 argv: args,
444 ..Default::default()
445 })
446 .setup_command()
447 .await?,
448 )
449 }
450 }
451
452 impl Find {
453 pub async fn find(self) -> Result<Vec<FoundSpec>, FindError> {
455 let command = self
456 .setup_command()
457 .await
458 .map_err(|e| e.with_context("in Find::find()".to_string()))?;
459 let output = command.invoke().await?;
460
461 match serde_json::from_slice::<'_, serde_json::Value>(&output.stdout)? {
462 serde_json::Value::Array(values) => {
463 let found_specs: Vec<FoundSpec> = values
464 .into_iter()
465 .map(serde_json::from_value)
466 .collect::<Result<Vec<FoundSpec>, _>>()?;
467 Ok(found_specs)
468 },
469 value => Err(FindError::Parse(format!(
470 "unable to parse find output: {:?}",
471 value
472 ))),
473 }
474 }
475 }
476
477 #[derive(Debug, Clone)]
479 pub struct FindPrefix {
480 pub spack: SpackInvocation,
481 pub spec: CLISpec,
482 pub env: Option<EnvName>,
483 pub repos: Option<RepoDirs>,
484 }
485
486 #[async_trait]
487 impl CommandBase for FindPrefix {
488 async fn setup_command(self) -> Result<exe::Command, base::SetupError> {
489 let Self {
490 spack,
491 spec,
492 env,
493 repos,
494 } = self;
495 let mut args = exe::Argv(
496 ["find", "--no-groups", "-p", spec.0.as_ref()]
497 .map(|s| OsStr::new(s).to_os_string())
498 .into_iter()
499 .collect(),
500 );
501
502 if let Some(env) = env {
503 env.modify_argv(&mut args);
504 }
505 if let Some(repos) = repos {
506 repos.modify_argv(&mut args);
507 }
508
509 Ok(
510 spack
511 .with_spack_exe(exe::Command {
512 argv: args,
513 ..Default::default()
514 })
515 .setup_command()
516 .await?,
517 )
518 }
519 }
520
521 impl FindPrefix {
522 pub async fn find_prefix(self) -> Result<Option<prefix::Prefix>, FindError> {
524 let spec = self.spec.clone();
525 let command = self
526 .setup_command()
527 .await
528 .map_err(|e| e.with_context("in FindPrefix::find_prefix()".to_string()))?;
529
530 match command.clone().invoke().await {
531 Ok(output) => {
532 static FIND_PREFIX_REGEX: Lazy<Regex> =
533 Lazy::new(|| Regex::new(r"^([^@]+)@([^ ]+) +([^ ].*)$").unwrap());
534 let stdout = str::from_utf8(&output.stdout).map_err(|e| {
535 FindError::Parse(format!("failed to parse utf8 ({}): got {:?}", e, &output))
536 })?;
537 dbg!(&stdout);
538 let lines: Vec<&str> = stdout.split('\n').collect();
539 let last_line = match lines.iter().filter(|l| !l.is_empty()).last() {
540 None => {
541 return Err(FindError::Parse(format!(
542 "stdout was empty (stderr was {})",
543 str::from_utf8(&output.stderr).unwrap_or("<could not parse utf8>"),
544 )))
545 },
546 Some(line) => line,
547 };
548 dbg!(&last_line);
549 let m = FIND_PREFIX_REGEX.captures(last_line).unwrap();
550 let name = m.get(1).unwrap().as_str();
551 assert!(spec.0.starts_with(name));
553 let prefix: PathBuf = m.get(3).unwrap().as_str().into();
554 Ok(Some(prefix::Prefix { path: prefix }))
555 },
556 Err(exe::CommandErrorWrapper {
557 context,
558 error: exe::CommandError::NonZeroExit(1),
559 ..
560 }) if context.contains("==> No package matches") => Ok(None),
561 Err(e) => Err(e.into()),
562 }
563 }
564 }
565
566 #[cfg(test)]
567 mod test {
568 use tokio;
569
570 #[tokio::test]
571 async fn test_find() -> Result<(), crate::Error> {
572 use crate::{
573 commands::{find::*, install::*},
574 SpackInvocation,
575 };
576
577 let spack = SpackInvocation::summon().await?;
579
580 let install = Install {
582 spack: spack.clone(),
583 spec: CLISpec::new("zlib@1.3.1"),
584 verbosity: Default::default(),
585 env: None,
586 repos: None,
587 };
588 let found_spec = install.clone().install_find().await.unwrap();
589
590 let find = Find {
592 spack,
593 spec: found_spec.hashed_spec(),
594 env: None,
595 repos: None,
596 };
597
598 let found_specs = find
600 .clone()
601 .find()
602 .await
603 .map_err(|e| crate::commands::CommandError::Find(find, e))?;
604
605 assert!(&found_specs[0].name == "zlib");
607 assert!(&found_specs[0].hash == &found_spec.hash);
609 assert!(&found_specs[0].version.0[..5] == "1.3.1");
612 Ok(())
613 }
614
615 #[tokio::test]
616 async fn test_find_prefix() -> Result<(), crate::Error> {
617 use crate::{
618 commands::{find::*, install::*},
619 SpackInvocation,
620 };
621 use std::fs;
622
623 let spack = SpackInvocation::summon().await?;
625
626 let install = Install {
628 spack: spack.clone(),
629 spec: CLISpec::new("zip"),
630 verbosity: Default::default(),
631 env: None,
632 repos: None,
633 };
634 let found_spec = install
635 .clone()
636 .install_find()
637 .await
638 .map_err(|e| crate::commands::CommandError::Install(install, e))?;
639
640 let find_prefix = FindPrefix {
642 spack,
643 spec: found_spec.hashed_spec(),
644 env: None,
645 repos: None,
646 };
647
648 let zip_prefix = find_prefix
650 .clone()
651 .find_prefix()
652 .await
653 .map_err(|e| crate::commands::CommandError::FindPrefix(find_prefix, e))?
654 .unwrap();
655
656 let zip_exe = zip_prefix.path.join("bin").join("zip");
658 assert!(fs::File::open(zip_exe).is_ok());
659 Ok(())
660 }
661 }
662}
663
664pub mod load {
666 use super::*;
667 use crate::SpackInvocation;
668 use super_process::{
669 base::{self, CommandBase},
670 exe, sh,
671 sync::SyncInvocable,
672 };
673
674 use async_trait::async_trait;
675
676 use std::{ffi::OsStr, str};
677
678 #[derive(Debug, Display, Error)]
680 pub enum LoadError {
681 Command(#[from] exe::CommandErrorWrapper),
683 Setup(#[from] base::SetupErrorWrapper),
685 Utf8(#[from] str::Utf8Error),
687 Shell(#[from] sh::ShellErrorWrapper),
689 }
690
691 #[derive(Debug, Clone)]
693 pub struct Load {
694 #[allow(missing_docs)]
695 pub spack: SpackInvocation,
696 pub specs: Vec<CLISpec>,
698 }
699
700 #[async_trait]
701 impl CommandBase for Load {
702 async fn setup_command(self) -> Result<exe::Command, base::SetupError> {
703 let Self { spack, specs } = self;
704 let args = exe::Argv(
705 ["load", "--sh"]
706 .into_iter()
707 .map(|s| s.to_string())
708 .chain(specs.into_iter().map(|s| s.0))
709 .map(|s| OsStr::new(&s).to_os_string())
710 .collect(),
711 );
712 Ok(
713 spack
714 .with_spack_exe(exe::Command {
715 argv: args,
716 ..Default::default()
717 })
718 .setup_command()
719 .await?,
720 )
721 }
722 }
723
724 impl Load {
725 pub async fn load(self) -> Result<exe::EnvModifications, LoadError> {
727 let command = self
728 .setup_command()
729 .await
730 .map_err(|e| e.with_context("in Load::load()".to_string()))?;
731 let load_output = command.invoke().await?;
732 let env = sh::EnvAfterScript {
735 source: sh::ShellSource {
736 contents: load_output.stdout,
737 },
738 }
739 .extract_env_bindings()
740 .await?;
741
742 Ok(env)
743 }
744 }
745
746 #[cfg(test)]
747 mod test {
748 use tokio;
749
750 #[tokio::test]
751 async fn test_load() -> Result<(), crate::Error> {
752 use crate::{
753 commands::{install::*, load::*},
754 SpackInvocation,
755 };
756 use std::ffi::OsStr;
757 use super_process::exe;
758
759 let spack = SpackInvocation::summon().await?;
761
762 let install = Install {
764 spack: spack.clone(),
765 spec: CLISpec::new("zlib"),
766 verbosity: Default::default(),
767 env: None,
768 repos: None,
769 };
770 let found_spec = install.clone().install_find().await.unwrap();
771
772 let load = Load {
774 spack,
775 specs: vec![found_spec.hashed_spec()],
776 };
777 let exe::EnvModifications(zlib_env) = load
779 .clone()
780 .load()
781 .await
782 .map_err(|e| crate::commands::CommandError::Load(load, e))?;
783 let hashes: Vec<&str> = zlib_env
784 .get(OsStr::new("SPACK_LOADED_HASHES"))
785 .unwrap()
786 .to_str()
787 .unwrap()
788 .split(":")
789 .collect();
790 assert_eq!(&hashes[0], &found_spec.hash);
791 Ok(())
792 }
793 }
794}
795
796pub mod install {
798 use super::{find::*, *};
799 use crate::SpackInvocation;
800 use super_process::{
801 base::{self, CommandBase},
802 exe,
803 stream::Streamable,
804 };
805
806 use async_trait::async_trait;
807 use num_cpus;
808
809 use std::ffi::OsStr;
810
811 #[derive(Debug, Display, Error)]
813 pub enum InstallError {
814 Inner(#[source] Box<CommandError>),
816 Command(#[from] exe::CommandErrorWrapper),
818 Setup(#[from] base::SetupErrorWrapper),
820 }
821
822 #[derive(Debug, Display, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
823 pub enum InstallVerbosity {
824 Standard,
826 Verbose,
828 }
829
830 impl Default for InstallVerbosity {
831 fn default() -> Self { Self::Verbose }
832 }
833
834 impl InstallVerbosity {
835 pub(crate) fn verbosity_args(self) -> Vec<String> {
836 match self {
837 Self::Standard => vec![],
838 Self::Verbose => vec!["--verbose".to_string()],
839 }
840 }
841 }
842
843 #[derive(Debug, Clone)]
845 pub struct Install {
846 pub spack: SpackInvocation,
847 pub spec: CLISpec,
848 pub verbosity: InstallVerbosity,
849 pub env: Option<EnvName>,
850 pub repos: Option<RepoDirs>,
851 }
852
853 #[async_trait]
854 impl CommandBase for Install {
855 async fn setup_command(self) -> Result<exe::Command, base::SetupError> {
856 let Self {
857 spack,
858 spec,
859 verbosity,
860 env,
861 repos,
862 } = self;
863
864 let jobs_arg = format!("-j{}", num_cpus::get());
867 let mut args = vec!["install", "--fail-fast", &jobs_arg];
868 if env.is_some() {
871 args.push("--add");
872 }
873 let mut argv = exe::Argv(
874 args
875 .into_iter()
876 .map(|s| s.to_string())
877 .chain(verbosity.verbosity_args())
878 .chain([spec.0.clone()])
879 .map(|s| OsStr::new(&s).to_os_string())
880 .collect(),
881 );
882
883 if let Some(env) = env {
884 env.modify_argv(&mut argv);
885 }
886 if let Some(repos) = repos {
887 repos.modify_argv(&mut argv);
888 }
889
890 Ok(
891 spack
892 .with_spack_exe(exe::Command {
893 argv,
894 ..Default::default()
895 })
896 .setup_command()
897 .await?,
898 )
899 }
900 }
901
902 impl Install {
903 pub async fn install(self) -> Result<(), InstallError> {
906 let command = self
907 .setup_command()
908 .await
909 .map_err(|e| e.with_context("in Install::install()".to_string()))?;
910
911 let streaming = command.invoke_streaming()?;
913 streaming.wait().await?;
914
915 Ok(())
916 }
917
918 pub async fn install_find(self) -> Result<FoundSpec, InstallError> {
920 let Self {
921 spack,
922 spec,
923 env,
924 repos,
925 ..
926 } = self.clone();
927
928 let cached_find = Find {
930 spack,
931 spec,
932 env,
933 repos,
934 };
935 if let Ok(found_specs) = cached_find.clone().find().await {
937 return Ok(found_specs[0].clone());
938 }
939
940 self.install().await?;
941
942 let found_specs = cached_find
946 .clone()
947 .find()
948 .await
949 .map_err(|e| CommandError::Find(cached_find, e))
950 .map_err(|e| InstallError::Inner(Box::new(e)))?;
951 Ok(found_specs[0].clone())
952 }
953
954 pub async fn install_with_env(
958 self,
959 load_env: exe::EnvModifications,
960 ) -> Result<(), InstallError> {
961 let mut command = self
962 .setup_command()
963 .await
964 .map_err(|e| e.with_context("in Install::install_with_env()".to_string()))?;
965 command.env = load_env;
966
967 let streaming = command.invoke_streaming()?;
968 streaming.wait().await?;
969
970 Ok(())
971 }
972 }
973
974 #[cfg(test)]
975 mod test {
976 use tokio;
977
978 #[tokio::test]
979 async fn test_install() -> Result<(), crate::Error> {
980 use crate::{commands::install::*, SpackInvocation};
981
982 let spack = SpackInvocation::summon().await?;
984
985 let install = Install {
987 spack: spack.clone(),
988 spec: CLISpec::new("libiberty@2.37"),
989 verbosity: Default::default(),
990 env: None,
991 repos: None,
992 };
993 let found_spec = install
994 .clone()
995 .install_find()
996 .await
997 .map_err(|e| crate::commands::CommandError::Install(install, e))?;
998
999 assert!(&found_spec.name == "libiberty");
1001 assert!(&found_spec.version.0 == "2.37");
1002 Ok(())
1003 }
1004
1005 #[tokio::test]
1006 async fn test_install_find() -> Result<(), crate::Error> {
1007 use crate::{commands::install::*, SpackInvocation};
1008
1009 let spack = SpackInvocation::summon().await?;
1011
1012 let install = Install {
1014 spack: spack.clone(),
1015 spec: CLISpec::new("libiberty@2.37"),
1016 verbosity: Default::default(),
1017 env: None,
1018 repos: None,
1019 };
1020 let found_spec = install
1021 .clone()
1022 .install_find()
1023 .await
1024 .map_err(|e| crate::commands::CommandError::Install(install, e))?;
1025
1026 assert!(&found_spec.name == "libiberty");
1028 assert!(&found_spec.version.0 == "2.37");
1029 Ok(())
1030 }
1031 }
1032}
1033
1034pub mod build_env {
1036 use super::*;
1037 use crate::SpackInvocation;
1038 use super_process::{
1039 base::{self, CommandBase},
1040 exe,
1041 sync::{self, SyncInvocable},
1042 };
1043
1044 use async_trait::async_trait;
1045
1046 use std::{ffi::OsStr, path::PathBuf};
1047
1048 #[derive(Debug, Display, Error)]
1050 pub enum BuildEnvError {
1051 Command(#[from] exe::CommandErrorWrapper),
1053 Setup(#[from] base::SetupErrorWrapper),
1055 Install(#[from] install::InstallError),
1057 Io(#[from] io::Error),
1059 }
1060
1061 #[derive(Debug, Clone)]
1063 pub struct BuildEnv {
1064 #[allow(missing_docs)]
1065 pub spack: SpackInvocation,
1066 pub spec: CLISpec,
1068 pub dump: Option<PathBuf>,
1070 pub env: Option<EnvName>,
1071 pub repos: Option<RepoDirs>,
1072 pub argv: exe::Argv,
1077 }
1078
1079 #[async_trait]
1080 impl CommandBase for BuildEnv {
1081 async fn setup_command(self) -> Result<exe::Command, base::SetupError> {
1082 eprintln!("BuildEnv");
1083 dbg!(&self);
1084 let Self {
1085 spack,
1086 spec,
1087 env,
1088 argv,
1089 dump,
1090 repos,
1091 } = self;
1092
1093 let dump_args = if let Some(d) = dump {
1094 vec!["--dump".to_string(), format!("{}", d.display())]
1095 } else {
1096 vec![]
1097 };
1098
1099 let mut argv = exe::Argv(
1100 ["build-env".to_string()]
1101 .into_iter()
1102 .chain(dump_args)
1103 .chain([spec.0])
1104 .map(|s| OsStr::new(&s).to_os_string())
1105 .chain(argv.trailing_args().0)
1106 .collect(),
1107 );
1108
1109 if let Some(env) = env {
1110 env.modify_argv(&mut argv);
1111 }
1112 if let Some(repos) = repos {
1113 repos.modify_argv(&mut argv);
1114 }
1115
1116 let command = spack
1117 .with_spack_exe(exe::Command {
1118 argv,
1119 ..Default::default()
1120 })
1121 .setup_command()
1122 .await?;
1123
1124 Ok(command)
1125 }
1126 }
1127
1128 impl BuildEnv {
1129 pub async fn build_env(self) -> Result<sync::RawOutput, BuildEnvError> {
1131 let command = self
1132 .setup_command()
1133 .await
1134 .map_err(|e| e.with_context("in BuildEnv::build_env()".to_string()))?;
1135 let output = command.invoke().await?;
1136 Ok(output)
1137 }
1138 }
1139
1140 #[cfg(test)]
1141 mod test {
1142 use tokio;
1143
1144 #[tokio::test]
1145 async fn test_build_env() -> Result<(), crate::Error> {
1146 let td = tempdir::TempDir::new("spack-summon-test").unwrap();
1147 use crate::{
1148 commands::{build_env::*, install::*},
1149 SpackInvocation,
1150 };
1151 use std::{fs, io::BufRead};
1152
1153 let spack = SpackInvocation::summon().await?;
1155
1156 let install = Install {
1158 spack: spack.clone(),
1159 spec: CLISpec::new("m4"),
1160 verbosity: Default::default(),
1161 env: None,
1162 repos: None,
1163 };
1164 let found_spec = install
1165 .clone()
1166 .install_find()
1167 .await
1168 .map_err(|e| crate::commands::CommandError::Install(install, e))?;
1169
1170 let build_env = BuildEnv {
1172 spack: spack.clone(),
1173 spec: found_spec.hashed_spec(),
1175 env: None,
1176 repos: None,
1177 dump: None,
1178 argv: Default::default(),
1179 };
1180 let output = build_env
1182 .clone()
1183 .build_env()
1184 .await
1185 .map_err(|e| crate::commands::CommandError::BuildEnv(build_env, e))?;
1186
1187 let mut spec_was_found: bool = false;
1189 for line in output.stdout.lines() {
1190 let line = line.unwrap();
1191 if line.starts_with("SPACK_SHORT_SPEC") {
1192 spec_was_found = true;
1193 assert!("m4" == &line[17..19]);
1194 }
1195 }
1196 assert!(spec_was_found);
1197
1198 let dump = td.path().join(".env-dump");
1200 let build_env = BuildEnv {
1201 spack: spack.clone(),
1202 spec: found_spec.hashed_spec(),
1203 env: None,
1204 repos: None,
1205 dump: Some(dump.clone()),
1206 argv: Default::default(),
1207 };
1208 let _ = build_env
1210 .clone()
1211 .build_env()
1212 .await
1213 .map_err(|e| crate::commands::CommandError::BuildEnv(build_env, e))?;
1214 spec_was_found = false;
1215 for line in fs::read_to_string(&dump)
1216 .expect(".env-dump was created")
1217 .lines()
1218 {
1219 if line.starts_with("SPACK_SHORT_SPEC") {
1220 spec_was_found = true;
1221 assert!("m4" == &line[18..20]);
1222 }
1223 }
1224 assert!(spec_was_found);
1225
1226 let build_env = BuildEnv {
1228 spack,
1229 spec: found_spec.hashed_spec(),
1230 env: None,
1231 repos: None,
1232 dump: None,
1233 argv: ["sh", "-c", "echo $SPACK_SHORT_SPEC"].as_ref().into(),
1234 };
1235 let output = build_env
1236 .clone()
1237 .build_env()
1238 .await
1239 .map_err(|e| crate::commands::CommandError::BuildEnv(build_env, e))?;
1240 assert!(&output.stdout[..2] == b"m4");
1241 Ok(())
1242 }
1243 }
1244}
1245
1246pub mod python {
1248 use super::*;
1249 use crate::subprocess::spack;
1250 use super_process::{
1251 base::{self, CommandBase},
1252 exe,
1253 };
1254
1255 use async_trait::async_trait;
1256 use tempfile::{NamedTempFile, TempPath};
1257
1258 use std::ffi::OsStr;
1259
1260 #[derive(Debug, Clone)]
1262 pub struct SpackPython {
1263 #[allow(missing_docs)]
1264 pub spack: spack::SpackInvocation,
1265 pub script: String,
1267 pub passthrough: exe::Argv,
1268 }
1269
1270 impl SpackPython {
1271 fn write_python_script(script: String) -> io::Result<TempPath> {
1272 let (mut script_file, script_path) = NamedTempFile::new()?.into_parts();
1274 script_file.write_all(script.as_bytes())?;
1275 script_file.sync_all()?;
1276 Ok(script_path)
1278 }
1279 }
1280
1281 #[async_trait]
1282 impl CommandBase for SpackPython {
1283 async fn setup_command(self) -> Result<exe::Command, base::SetupError> {
1284 eprintln!("SpackPython");
1285 dbg!(&self);
1286 let Self {
1287 spack,
1288 script,
1289 passthrough,
1290 } = self;
1291
1292 let script_path = Self::write_python_script(script)?;
1294 let script_path = script_path
1296 .keep()
1297 .expect("no error avoiding drop of temp file");
1298
1299 let argv = exe::Argv(
1301 [OsStr::new("python"), OsStr::new(&script_path)]
1302 .into_iter()
1303 .map(|s| s.to_os_string())
1304 .chain(passthrough.0)
1305 .collect(),
1306 );
1307 let command = spack
1308 .with_spack_exe(exe::Command {
1309 argv,
1310 ..Default::default()
1311 })
1312 .setup_command()
1313 .await?;
1314
1315 Ok(command)
1316 }
1317 }
1318
1319 #[cfg(test)]
1320 mod test {
1321 use tokio;
1322
1323 #[tokio::test]
1324 async fn test_python() -> Result<(), crate::Error> {
1325 use crate::{commands::python, SpackInvocation};
1326 use super_process::{base::CommandBase, sync::SyncInvocable};
1327
1328 let spack = SpackInvocation::summon().await.unwrap();
1330
1331 let spack_python = python::SpackPython {
1333 spack: spack.clone(),
1334 script: "import spack; print(spack.__version__)".to_string(),
1335 passthrough: Default::default(),
1336 };
1337 let command = spack_python
1338 .setup_command()
1339 .await
1340 .expect("hydration failed");
1341
1342 let output = command.clone().invoke().await.expect("sync command failed");
1344 let decoded = output.decode(command.clone()).expect("decoding failed");
1346 let version = decoded.stdout.strip_suffix("\n").unwrap();
1348 assert!(version == &spack.version);
1349 Ok(())
1350 }
1351 }
1352}
1353
1354pub mod compiler_find {
1356 use super::*;
1357 use crate::SpackInvocation;
1358 use super_process::{
1359 base::{self, CommandBase},
1360 exe,
1361 sync::SyncInvocable,
1362 };
1363
1364 use async_trait::async_trait;
1365 use serde::{Deserialize, Serialize};
1366 use serde_json;
1367
1368 use std::{ffi::OsStr, io, path::PathBuf};
1369
1370 #[derive(Debug, Display, Error)]
1372 pub enum CompilerFindError {
1373 Command(#[from] exe::CommandErrorWrapper),
1375 Setup(#[from] base::SetupErrorWrapper),
1377 Io(#[from] io::Error),
1379 Json(#[from] serde_json::Error),
1381 Unknown(String),
1383 }
1384
1385 #[derive(Debug, Clone)]
1387 pub struct CompilerFind {
1388 #[allow(missing_docs)]
1389 pub spack: SpackInvocation,
1390 pub paths: Vec<PathBuf>,
1392 pub scope: Option<String>,
1394 }
1395
1396 #[async_trait]
1397 impl CommandBase for CompilerFind {
1398 async fn setup_command(self) -> Result<exe::Command, base::SetupError> {
1399 let Self {
1400 spack,
1401 paths,
1402 scope,
1403 } = self;
1404 let args = exe::Argv(
1405 ["compiler", "find"]
1406 .map(|s| s.to_string())
1407 .into_iter()
1408 .chain(
1409 scope
1410 .map(|s| vec!["--scope".to_string(), s])
1411 .unwrap_or_else(Vec::new),
1412 )
1413 .chain(paths.into_iter().map(|p| format!("{}", p.display())))
1414 .map(|s| OsStr::new(&s).to_os_string())
1415 .collect(),
1416 );
1417 Ok(
1418 spack
1419 .with_spack_exe(exe::Command {
1420 argv: args,
1421 ..Default::default()
1422 })
1423 .setup_command()
1424 .await?,
1425 )
1426 }
1427 }
1428
1429 impl CompilerFind {
1430 pub async fn compiler_find(self) -> Result<(), CompilerFindError> {
1435 let command = self
1436 .setup_command()
1437 .await
1438 .map_err(|e| e.with_context("in compiler_find()!".to_string()))?;
1439 let _ = command.invoke().await?;
1440 Ok(())
1441 }
1442 }
1443
1444 #[derive(Debug, Clone)]
1445 pub struct FindCompilerSpecs {
1446 #[allow(missing_docs)]
1447 pub spack: SpackInvocation,
1448 pub paths: Vec<PathBuf>,
1450 }
1451
1452 #[derive(Debug, Display, Serialize, Deserialize, Clone)]
1454 pub struct FoundCompiler {
1455 pub spec: OuterSpec,
1456 }
1457
1458 #[derive(Debug, Display, Serialize, Deserialize, Clone)]
1459 pub struct OuterSpec {
1460 pub nodes: Vec<CompilerSpec>,
1461 }
1462
1463 impl FoundCompiler {
1464 pub fn into_compiler_spec_string(self) -> String {
1465 let CompilerSpec { name, version } = &self.spec.nodes[0];
1466 format!("{}@{}", name, version)
1467 }
1468 }
1469
1470 #[derive(Debug, Display, Serialize, Deserialize, Clone)]
1471 pub struct CompilerSpec {
1472 pub name: String,
1473 pub version: String,
1474 }
1475
1476 const COMPILER_SPEC_SOURCE: &str = include_str!("compiler-find.py");
1477
1478 impl FindCompilerSpecs {
1479 pub async fn find_compiler_specs(self) -> Result<Vec<FoundCompiler>, CompilerFindError> {
1485 let command = self
1486 .setup_command()
1487 .await
1488 .map_err(|e| e.with_context("in find_compiler_specs()!".to_string()))?;
1489 let output = command.invoke().await?;
1490
1491 match serde_json::from_slice::<'_, serde_json::Value>(&output.stdout)? {
1492 serde_json::Value::Array(values) => {
1493 let compiler_specs: Vec<FoundCompiler> = values
1494 .into_iter()
1495 .map(serde_json::from_value)
1496 .collect::<Result<Vec<FoundCompiler>, _>>()?;
1497 Ok(compiler_specs)
1498 },
1499 value => Err(CompilerFindError::Unknown(format!(
1500 "unable to parse compiler-find.py output: {:?}",
1501 value
1502 ))),
1503 }
1504 }
1505 }
1506
1507 #[async_trait]
1508 impl CommandBase for FindCompilerSpecs {
1509 async fn setup_command(self) -> Result<exe::Command, base::SetupError> {
1510 let Self { spack, paths } = self.clone();
1511
1512 let argv = exe::Argv(
1513 paths
1514 .into_iter()
1515 .map(|p| format!("{}", p.display()))
1516 .map(|s| OsStr::new(&s).to_os_string())
1517 .collect(),
1518 );
1519 let python = python::SpackPython {
1520 spack: spack.clone(),
1521 script: COMPILER_SPEC_SOURCE.to_string(),
1522 passthrough: argv,
1523 };
1524 Ok(python.setup_command().await?)
1525 }
1526 }
1527
1528 #[cfg(test)]
1529 mod test {
1530 use tokio;
1531
1532 #[tokio::test]
1533 async fn test_compiler_find() -> Result<(), crate::Error> {
1534 use crate::{commands::compiler_find::*, SpackInvocation};
1535
1536 let spack = SpackInvocation::summon().await.unwrap();
1538
1539 let find_compiler_specs = FindCompilerSpecs {
1541 spack: spack.clone(),
1542 paths: vec![],
1543 };
1544 let found_compilers = find_compiler_specs
1545 .clone()
1546 .find_compiler_specs()
1547 .await
1548 .map_err(|e| CommandError::FindCompilerSpecs(find_compiler_specs, e))?;
1549 let first_name = &found_compilers[0].spec.nodes[0].name;
1551 assert!(first_name == "gcc" || first_name == "llvm");
1552 Ok(())
1553 }
1554 }
1555}
1556
1557pub mod checksum {
1559 use super::*;
1560 use crate::SpackInvocation;
1561 use super_process::{
1562 base::{self, CommandBase},
1563 exe,
1564 sync::SyncInvocable,
1565 };
1566
1567 use async_trait::async_trait;
1568 use indexmap::IndexSet;
1569 use tokio::task;
1570
1571 use std::{ffi::OsStr, io, path::PathBuf, str};
1572
1573
1574 #[derive(Debug, Display, Error)]
1575 pub enum ChecksumError {
1576 Command(#[from] exe::CommandErrorWrapper),
1578 Setup(#[from] base::SetupErrorWrapper),
1580 Utf8(#[from] str::Utf8Error),
1582 Io(#[from] io::Error),
1584 }
1585
1586 #[derive(Debug, Clone)]
1587 pub struct VersionsRequest {
1588 #[allow(missing_docs)]
1589 pub spack: SpackInvocation,
1590 pub package_name: String,
1591 }
1592
1593 #[async_trait]
1594 impl CommandBase for VersionsRequest {
1595 async fn setup_command(self) -> Result<exe::Command, base::SetupError> {
1596 eprintln!("VersionsRequest");
1597 dbg!(&self);
1598 let Self {
1599 spack,
1600 package_name,
1601 } = self;
1602
1603 let argv = exe::Argv(
1604 ["versions", "--safe", &package_name]
1605 .into_iter()
1606 .map(|s| OsStr::new(&s).to_os_string())
1607 .collect(),
1608 );
1609
1610 Ok(
1611 spack
1612 .with_spack_exe(exe::Command {
1613 argv,
1614 ..Default::default()
1615 })
1616 .setup_command()
1617 .await?,
1618 )
1619 }
1620 }
1621
1622 impl VersionsRequest {
1623 pub async fn safe_versions(self) -> Result<Vec<String>, ChecksumError> {
1624 let command = self
1625 .setup_command()
1626 .await
1627 .map_err(|e| e.with_context("in VersionsRequest::safe_versions()".to_string()))?;
1628 let output = command.invoke().await?;
1629
1630 let versions: Vec<String> = str::from_utf8(&output.stdout)?
1631 .split('\n')
1632 .filter(|s| !s.is_empty())
1633 .map(|s| s.strip_prefix(" ").unwrap().to_string())
1634 .collect();
1635
1636 Ok(versions)
1637 }
1638 }
1639
1640 #[derive(Debug, Clone)]
1642 pub struct AddToPackage {
1643 #[allow(missing_docs)]
1644 pub spack: SpackInvocation,
1645 pub package_name: String,
1646 pub new_version: String,
1647 }
1648
1649 #[async_trait]
1650 impl CommandBase for AddToPackage {
1651 async fn setup_command(self) -> Result<exe::Command, base::SetupError> {
1652 eprintln!("AddToPackage");
1653 dbg!(&self);
1654 let Self {
1655 spack,
1656 package_name,
1657 new_version,
1658 } = self;
1659
1660 let argv = exe::Argv(
1661 ["checksum", "--add-to-package", &package_name, &new_version]
1662 .into_iter()
1663 .map(|s| OsStr::new(&s).to_os_string())
1664 .collect(),
1665 );
1666
1667 let env: exe::EnvModifications = [("SPACK_EDITOR", "echo")].into();
1669
1670 Ok(
1671 spack
1672 .with_spack_exe(exe::Command {
1673 argv,
1674 env,
1675 ..Default::default()
1676 })
1677 .setup_command()
1678 .await?,
1679 )
1680 }
1681 }
1682
1683 pub(crate) static ENSURE_PACKAGE_VERSION_LOCK: once_cell::sync::Lazy<tokio::sync::Mutex<()>> =
1684 once_cell::sync::Lazy::new(|| tokio::sync::Mutex::new(()));
1685
1686 impl AddToPackage {
1687 async fn version_is_known(
1688 req: VersionsRequest,
1689 new_version: &str,
1690 ) -> Result<bool, ChecksumError> {
1691 let versions: IndexSet<String> = req.safe_versions().await?.into_iter().collect();
1692
1693 Ok(versions.contains(new_version))
1694 }
1695
1696 async fn force_new_version(
1697 self,
1698 req: VersionsRequest,
1699 new_version: &str,
1700 ) -> Result<(), ChecksumError> {
1701 let command = self
1702 .setup_command()
1703 .await
1704 .map_err(|e| e.with_context("in add_to_package()!".to_string()))?;
1705
1706 let _ = command.invoke().await?;
1708
1709 assert!(Self::version_is_known(req, new_version).await?);
1711
1712 Ok(())
1713 }
1714
1715 pub async fn idempotent_ensure_version_for_package(self) -> Result<(), ChecksumError> {
1716 let _lock = ENSURE_PACKAGE_VERSION_LOCK.lock().await;
1720
1721 let req = VersionsRequest {
1722 spack: self.spack.clone(),
1723 package_name: self.package_name.clone(),
1724 };
1725
1726 if Self::version_is_known(req.clone(), &self.new_version).await? {
1728 eprintln!(
1729 "we already have the version {} for package {}!",
1730 &self.new_version, &self.package_name
1731 );
1732 return Ok(());
1733 }
1734
1735 let lockfile_name: PathBuf =
1737 format!("{}@{}.lock", &self.package_name, &self.new_version).into();
1738 let lockfile_path = self.spack.cache_location().join(lockfile_name);
1739 let mut lockfile = task::spawn_blocking(move || fslock::LockFile::open(&lockfile_path))
1740 .await
1741 .unwrap()?;
1742 let _lockfile = task::spawn_blocking(move || {
1744 lockfile.lock_with_pid()?;
1745 Ok::<_, io::Error>(lockfile)
1746 })
1747 .await
1748 .unwrap()?;
1749
1750 if Self::version_is_known(req.clone(), &self.new_version).await? {
1752 eprintln!(
1753 "the version {} for package {} was created while we locked the file handle!",
1754 &self.new_version, &self.package_name
1755 );
1756 return Ok(());
1757 }
1758
1759 let new_version = self.new_version.clone();
1761 self.force_new_version(req, &new_version).await?;
1762
1763 Ok(())
1764 }
1765 }
1766
1767 #[cfg(test)]
1768 mod test {
1769 use super::*;
1770
1771 #[tokio::test]
1772 async fn test_ensure_re2_2022_12_01() -> eyre::Result<()> {
1773 let spack = SpackInvocation::summon().await?;
1775
1776 let req = AddToPackage {
1777 spack,
1778 package_name: "re2".to_string(),
1779 new_version: "2022-12-01".to_string(),
1780 };
1781 req.idempotent_ensure_version_for_package().await?;
1782
1783 Ok(())
1784 }
1785 }
1786}
1787
1788pub mod env {
1789 use super::*;
1790 use crate::{metadata_spec::spec, SpackInvocation};
1791 use super_process::{
1792 base::{self, CommandBase},
1793 exe,
1794 sync::SyncInvocable,
1795 };
1796
1797 use async_trait::async_trait;
1798 use indexmap::IndexSet;
1799 use tokio::task;
1800
1801 use std::{borrow::Cow, ffi::OsStr, io, path::PathBuf};
1802
1803 #[derive(Debug, Display, Error)]
1804 pub enum EnvError {
1805 Install(#[from] install::InstallError),
1807 Command(#[from] exe::CommandErrorWrapper),
1809 Setup(#[from] base::SetupErrorWrapper),
1811 Io(#[from] io::Error),
1813 }
1814
1815 #[derive(Debug, Clone)]
1816 pub struct EnvList {
1817 #[allow(missing_docs)]
1818 pub spack: SpackInvocation,
1819 }
1820
1821 #[async_trait]
1822 impl CommandBase for EnvList {
1823 async fn setup_command(self) -> Result<exe::Command, base::SetupError> {
1824 eprintln!("EnvList");
1825 dbg!(&self);
1826 let Self { spack } = self;
1827
1828 let argv = exe::Argv(
1829 ["env", "list"]
1830 .into_iter()
1831 .map(|s| OsStr::new(&s).to_os_string())
1832 .collect(),
1833 );
1834
1835 Ok(
1836 spack
1837 .with_spack_exe(exe::Command {
1838 argv,
1839 ..Default::default()
1840 })
1841 .setup_command()
1842 .await?,
1843 )
1844 }
1845 }
1846
1847 impl EnvList {
1848 pub async fn env_list(self) -> Result<IndexSet<EnvName>, EnvError> {
1849 let command = self
1850 .setup_command()
1851 .await
1852 .map_err(|e| e.with_context("in env_list()!".to_string()))?;
1853 let output = command.clone().invoke().await?;
1854 let output = output.decode(command)?;
1855 Ok(
1856 output
1857 .stdout
1858 .split('\n')
1859 .map(|s| EnvName(s.trim().to_string()))
1860 .collect(),
1861 )
1862 }
1863 }
1864
1865 #[derive(Debug, Clone)]
1866 pub struct EnvCreate {
1867 #[allow(missing_docs)]
1868 pub spack: SpackInvocation,
1869 pub env: EnvName,
1870 }
1871
1872 #[async_trait]
1873 impl CommandBase for EnvCreate {
1874 async fn setup_command(self) -> Result<exe::Command, base::SetupError> {
1875 eprintln!("EnvCreate");
1876 dbg!(&self);
1877 let Self {
1878 spack,
1879 env: EnvName(env_name),
1880 } = self;
1881
1882 let argv = exe::Argv(
1883 ["env", "create", env_name.as_str()]
1884 .into_iter()
1885 .map(|s| OsStr::new(&s).to_os_string())
1886 .collect(),
1887 );
1888
1889 Ok(
1890 spack
1891 .with_spack_exe(exe::Command {
1892 argv,
1893 ..Default::default()
1894 })
1895 .setup_command()
1896 .await?,
1897 )
1898 }
1899 }
1900
1901 pub(crate) static ENSURE_ENV_CREATE_LOCK: once_cell::sync::Lazy<tokio::sync::Mutex<()>> =
1902 once_cell::sync::Lazy::new(|| tokio::sync::Mutex::new(()));
1903
1904 impl EnvCreate {
1905 async fn env_exists(env_list: EnvList, env_name: &EnvName) -> Result<bool, EnvError> {
1906 let existing_envs = env_list.env_list().await?;
1907 Ok(existing_envs.contains(env_name))
1908 }
1909
1910 pub async fn idempotent_env_create(
1911 self,
1912 instructions: Cow<'_, spec::EnvInstructions>,
1913 ) -> Result<EnvName, EnvError> {
1914 let _lock = ENSURE_ENV_CREATE_LOCK.lock().await;
1918
1919 let lockfile_name: PathBuf = format!("env@{}.lock", &self.env.0).into();
1921 let lockfile_path = self.spack.cache_location().join(lockfile_name);
1922 dbg!(&lockfile_path);
1923 let mut lockfile = task::spawn_blocking(move || fslock::LockFile::open(&lockfile_path))
1924 .await
1925 .unwrap()?;
1926
1927 let _lockfile = task::spawn_blocking(move || {
1929 lockfile.lock_with_pid()?;
1930 Ok::<_, io::Error>(lockfile)
1931 })
1932 .await
1933 .unwrap()?;
1934
1935 let req = EnvList {
1936 spack: self.spack.clone(),
1937 };
1938
1939 let completed_sentinel_filename: PathBuf = format!("SENTINEL@env@{}", &self.env.0).into();
1940 let sentinel_file = self
1941 .spack
1942 .cache_location()
1943 .join(completed_sentinel_filename);
1944 dbg!(&sentinel_file);
1945 if sentinel_file.is_file() {
1946 if Self::env_exists(req.clone(), &self.env).await? {
1947 eprintln!(
1948 "env {:?} already exists and sentinel file {} does too!",
1949 &self.env,
1950 sentinel_file.display()
1951 );
1952 return Ok(self.env);
1953 }
1954 eprintln!(
1955 "env {:?} does not exist, but the sentinel file {:?} does; removing...",
1956 &self.env,
1957 sentinel_file.display()
1958 );
1959 let outfile = sentinel_file.clone();
1960 task::spawn_blocking(move || std::fs::remove_file(outfile))
1961 .await
1962 .unwrap()?;
1963 assert!(!sentinel_file.is_file());
1964 }
1965
1966 let spack = self.spack.clone();
1967 let env = self.env.clone();
1968
1969 if Self::env_exists(req.clone(), &env).await? {
1970 eprintln!(
1971 "the env {:?} was created while waiting for file lock!",
1972 &env
1973 );
1974 } else {
1975 let command = self
1976 .setup_command()
1977 .await
1978 .map_err(|e| e.with_context("in idempotent_env_create()!".to_string()))?;
1979 let _ = command.invoke().await?;
1980 assert!(Self::env_exists(req, &env).await?);
1981 }
1982
1983 dbg!(&instructions);
1985 let spec::EnvInstructions { specs, repo } = instructions.into_owned();
1986 let repo_dirs = match repo {
1987 Some(spec::Repo { path }) => Some(RepoDirs(vec![std::env::current_dir()?.join(path)])),
1988 None => None,
1989 };
1990
1991 for spec in specs.into_iter() {
1992 let spec = CLISpec::new(spec.0);
1993 let install = install::Install {
1994 spack: spack.clone(),
1995 spec,
1996 verbosity: install::InstallVerbosity::Verbose,
1997 env: Some(env.clone()),
1998 repos: repo_dirs.clone(),
1999 };
2000 install.install().await?;
2001 }
2002
2003 let outfile = sentinel_file.clone();
2004 task::spawn_blocking(move || std::fs::write(outfile, b""))
2005 .await
2006 .unwrap()?;
2007 assert!(sentinel_file.is_file());
2008
2009 Ok(env)
2010 }
2011 }
2012}