spack/
commands.rs

1/* copyright 2022-2023 Danny McClanahan */
2/* SPDX-License-Identifier: (Apache-2.0 OR MIT) */
3
4//! Invoking specific spack commands.
5
6use 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/// {0}
18///
19/// An (abstract *or* concrete) spec string for a command-line argument. This is
20/// used in [`find::Find::find`] to resolve concrete specs from the string.
21#[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  /// Construct a cli spec from a [str].
31  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
51/* FIXME: figure out how to fix if ^C hit during env install! */
52impl 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/// Errors executing spack commands.
73#[derive(Debug, Display, Error)]
74pub enum CommandError {
75  /// config error from {0:?}: {1}
76  Config(config::Config, #[source] config::ConfigError),
77  /// find error from {0:?}: {1}
78  Find(find::Find, #[source] find::FindError),
79  /// find prefix error from {0:?}: {1}
80  FindPrefix(find::FindPrefix, #[source] find::FindError),
81  /// load error from {0:?}: {1}
82  Load(load::Load, #[source] load::LoadError),
83  /// install error from {0:?}: {1}
84  Install(install::Install, #[source] install::InstallError),
85  /// build env error from {0:?}: {1}
86  BuildEnv(build_env::BuildEnv, #[source] build_env::BuildEnvError),
87  /// compiler-find error from {0:?}: {1}
88  CompilerFind(
89    compiler_find::CompilerFind,
90    #[source] compiler_find::CompilerFindError,
91  ),
92  /// find compiler specs error from {0:?}: {1}
93  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  /// Errors manipulating config.
116  #[derive(Debug, Display, Error)]
117  pub enum ConfigError {
118    /// command error: {0}
119    Command(#[from] exe::CommandErrorWrapper),
120    /// setup error: {0}
121    Setup(#[from] base::SetupErrorWrapper),
122    /// yaml error {0}
123    Yaml(#[from] serde_yaml::Error),
124    /// error manipulating yaml object {0}: {1}
125    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    /// The scope to request the config be drawn from.
139    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  /// Request to execute `spack config get "$self.section"` and parse the YAML
181  /// output.
182  #[derive(Debug, Clone)]
183  struct Get {
184    #[allow(missing_docs)]
185    pub spack: SpackInvocation,
186    /// The scope to request the config be drawn from.
187    pub scope: Option<String>,
188    /// Section name to print.
189    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", &section].into(),
203      }
204    }
205  }
206
207  /// Paths to specific executables this compiler owns.
208  #[derive(Debug, Display, Serialize, Deserialize, Clone)]
209  pub struct CompilerPaths {
210    /// Path to the C compiler.
211    pub cc: Option<PathBuf>,
212    /// Path to the C++ compiler.
213    pub cxx: Option<PathBuf>,
214    /// Path to the FORTRAN 77 compiler.
215    pub f77: Option<PathBuf>,
216    /// Path to the fortran 90 compiler.
217    pub fc: Option<PathBuf>,
218  }
219
220  /// A single compiler's spec from running [`GetCompilers::get_compilers`].
221  #[derive(Debug, Display, Serialize, Deserialize, Clone)]
222  pub struct CompilerSpec {
223    /// Spec string that can be used to select the given compiler after a `%` in
224    /// a [`CLISpec`].
225    pub spec: String,
226    /// Paths which could be located for this compiler.
227    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  /// Request to execute `spack config get compilers` and parse the YAML output.
237  #[derive(Debug, Clone)]
238  pub struct GetCompilers {
239    #[allow(missing_docs)]
240    pub spack: SpackInvocation,
241    /// The scope to request the config be drawn from.
242    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    /// Execute `spack config get compilers` and parse the YAML output.
259    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      // Locate all the executables.
318      let spack = SpackInvocation::summon().await?;
319
320      // .get_compilers() will return an array of compiler specs.
321      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      // Get the path to a working C compiler and check that it can executed.
330      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
351/// Find command.
352pub 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  /// A single package's spec from running [`Find::find`].
370  #[derive(Debug, Display, Serialize, Deserialize, Clone)]
371  pub struct FoundSpec {
372    /// package name: {0}
373    pub name: String,
374    /// concrete package version: {0}
375    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    /// 32-character hash uniquely identifying this spec: {0}
383    pub hash: String,
384  }
385
386  impl FoundSpec {
387    /// Get a CLI argument matching the exact spec found previously.
388    pub fn hashed_spec(&self) -> CLISpec { CLISpec(format!("{}/{}", &self.name, &self.hash)) }
389  }
390
391  /// A concrete version string from [FoundSpec::version].
392  #[derive(Debug, Display, Serialize, Deserialize, Clone)]
393  pub struct ConcreteVersion(pub String);
394
395  /// Errors finding.
396  #[derive(Debug, Display, Error)]
397  pub enum FindError {
398    /// command line error {0}
399    Command(#[from] exe::CommandErrorWrapper),
400    /// setup error: {0}
401    Setup(#[from] base::SetupErrorWrapper),
402    /// installation error {0}
403    Install(#[from] install::InstallError),
404    /// output parse error {0}
405    Parse(String),
406    /// json error {0}
407    Json(#[from] serde_json::Error),
408  }
409
410  /// Find request.
411  #[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    /// Execute `spack find "$self.spec"`.
454    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  /// Find prefix request.
478  #[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    /// Execute `spack find -p "$self.spec"`.
523    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          /* FIXME: this method should be using a custom `spack python` script!! */
552          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      // Locate all the executables.
578      let spack = SpackInvocation::summon().await?;
579
580      // Ensure a zlib is installed.
581      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      // Look for a zlib spec with that exact hash.
591      let find = Find {
592        spack,
593        spec: found_spec.hashed_spec(),
594        env: None,
595        repos: None,
596      };
597
598      // .find() will return an array of values, which may have any length.
599      let found_specs = find
600        .clone()
601        .find()
602        .await
603        .map_err(|e| crate::commands::CommandError::Find(find, e))?;
604
605      // Here, we just check the first of the found specs.
606      assert!(&found_specs[0].name == "zlib");
607      // Verify that this is the same spec as before.
608      assert!(&found_specs[0].hash == &found_spec.hash);
609      // The fields of the '--json' output of 'find'
610      // are deserialized into FoundSpec instances.
611      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      // Locate all the executables.
624      let spack = SpackInvocation::summon().await?;
625
626      // Ensure a zip is installed.
627      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      // Look for a zip spec with that exact hash.
641      let find_prefix = FindPrefix {
642        spack,
643        spec: found_spec.hashed_spec(),
644        env: None,
645        repos: None,
646      };
647
648      // .find_prefix() will return the spec's prefix root wrapped in an Option.
649      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      // Verify that this prefix contains the zip executable.
657      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
664/// Load command.
665pub 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  /// Errors loading.
679  #[derive(Debug, Display, Error)]
680  pub enum LoadError {
681    /// command error: {0}
682    Command(#[from] exe::CommandErrorWrapper),
683    /// setup error: {0}
684    Setup(#[from] base::SetupErrorWrapper),
685    /// utf8 decoding error: {0}
686    Utf8(#[from] str::Utf8Error),
687    /// shell error {0}
688    Shell(#[from] sh::ShellErrorWrapper),
689  }
690
691  /// Load request.
692  #[derive(Debug, Clone)]
693  pub struct Load {
694    #[allow(missing_docs)]
695    pub spack: SpackInvocation,
696    /// Specs to load the environment for.
697    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    /// Execute `spack load --sh "$self.spec"`.
726    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      /* dbg!(std::str::from_utf8(&load_output.stdout).unwrap()); */
733
734      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      // Locate all the executables.
760      let spack = SpackInvocation::summon().await?;
761
762      // Ensure a zlib is installed.
763      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      // Look for a zlib spec with that exact hash.
773      let load = Load {
774        spack,
775        specs: vec![found_spec.hashed_spec()],
776      };
777      // This is the contents of a source-able environment script.
778      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
796/// Install command.
797pub 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  /// Errors installing.
812  #[derive(Debug, Display, Error)]
813  pub enum InstallError {
814    /// {0}
815    Inner(#[source] Box<CommandError>),
816    /// spack command error {0}
817    Command(#[from] exe::CommandErrorWrapper),
818    /// setup error: {0}
819    Setup(#[from] base::SetupErrorWrapper),
820  }
821
822  #[derive(Debug, Display, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
823  pub enum InstallVerbosity {
824    /// <standard verbosity>
825    Standard,
826    /// <with --verbose>
827    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  /// Install request.
844  #[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      /* Generate spack argv. */
865      /* FIXME: determine appropriate amount of build parallelism! */
866      let jobs_arg = format!("-j{}", num_cpus::get());
867      let mut args = vec!["install", "--fail-fast", &jobs_arg];
868      /* If running this inside an environment, the command will fail without this
869       * argument. */
870      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    /// Execute `spack install "$self.spec"`, piping stdout and stderr to the
904    /// terminal.
905    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      /* Kick off the child process, reading its streams asynchronously. */
912      let streaming = command.invoke_streaming()?;
913      streaming.wait().await?;
914
915      Ok(())
916    }
917
918    /// Execute [`Self::install`], then execute [`Find::find`].
919    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      /* Check if the spec already exists. */
929      let cached_find = Find {
930        spack,
931        spec,
932        env,
933        repos,
934      };
935      /* FIXME: ensure we have a test for both cached and uncached!!! */
936      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      /* Find the first match for the spec we just tried to install. */
943      /* NB: this will probably immediately break if the CLI spec covers more than
944       * one concrete spec! For now we just take the first result!! */
945      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    /// Do [`Self::install`], but after sourcing the contents of `load_env`.
955    ///
956    /// FIXME: DOCUMENT AND TEST!!
957    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      // Locate all the executables.
983      let spack = SpackInvocation::summon().await?;
984
985      // Install libiberty, if we don't have it already!
986      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      // The result matches our query!
1000      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      // Locate all the executables.
1010      let spack = SpackInvocation::summon().await?;
1011
1012      // Install libiberty, if we don't have it already!
1013      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      // The result matches our query!
1027      assert!(&found_spec.name == "libiberty");
1028      assert!(&found_spec.version.0 == "2.37");
1029      Ok(())
1030    }
1031  }
1032}
1033
1034/// Build-env command.
1035pub 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  /// Errors setting up the build environment.
1049  #[derive(Debug, Display, Error)]
1050  pub enum BuildEnvError {
1051    /// {0}
1052    Command(#[from] exe::CommandErrorWrapper),
1053    /// setup error {0}
1054    Setup(#[from] base::SetupErrorWrapper),
1055    /// install error {0}
1056    Install(#[from] install::InstallError),
1057    /// io error: {0}
1058    Io(#[from] io::Error),
1059  }
1060
1061  /// Build-env request.
1062  #[derive(Debug, Clone)]
1063  pub struct BuildEnv {
1064    #[allow(missing_docs)]
1065    pub spack: SpackInvocation,
1066    /// Which spec to get into the environment of.
1067    pub spec: CLISpec,
1068    /// Optional output file for sourcing environment modifications.
1069    pub dump: Option<PathBuf>,
1070    pub env: Option<EnvName>,
1071    pub repos: Option<RepoDirs>,
1072    /// Optional command line to evaluate within the package environment.
1073    ///
1074    /// If this argv is empty, the contents of the environment are printed to
1075    /// stdout with `env`.
1076    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    /// Execute `spack build-env "$self.spec" -- $self.argv`.
1130    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      // Locate all the executables.
1154      let spack = SpackInvocation::summon().await?;
1155
1156      // Let's get an m4 installed!
1157      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      // Now let's activate the build environment for it!
1171      let build_env = BuildEnv {
1172        spack: spack.clone(),
1173        // Use the precise spec we just ensured was installed.
1174        spec: found_spec.hashed_spec(),
1175        env: None,
1176        repos: None,
1177        dump: None,
1178        argv: Default::default(),
1179      };
1180      // Execute build-env to get an env printed to stdout.
1181      let output = build_env
1182        .clone()
1183        .build_env()
1184        .await
1185        .map_err(|e| crate::commands::CommandError::BuildEnv(build_env, e))?;
1186
1187      // Example ad-hoc parsing of environment source files.
1188      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      // Now let's write out the environment to a file using --dump!
1199      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      // We will have written to ./.env-dump!
1209      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      // Now let's try running a command line in a build-env!
1227      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
1246/// spack python command.
1247pub 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  /// spack python command request.
1261  #[derive(Debug, Clone)]
1262  pub struct SpackPython {
1263    #[allow(missing_docs)]
1264    pub spack: spack::SpackInvocation,
1265    /// The contents of the python script to execute.
1266    pub script: String,
1267    pub passthrough: exe::Argv,
1268  }
1269
1270  impl SpackPython {
1271    fn write_python_script(script: String) -> io::Result<TempPath> {
1272      /* Create the script. */
1273      let (mut script_file, script_path) = NamedTempFile::new()?.into_parts();
1274      script_file.write_all(script.as_bytes())?;
1275      script_file.sync_all()?;
1276      /* Close the file, but keep the path alive. */
1277      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      /* Create the script. */
1293      let script_path = Self::write_python_script(script)?;
1294      /* FIXME: the file is never deleted!! */
1295      let script_path = script_path
1296        .keep()
1297        .expect("no error avoiding drop of temp file");
1298
1299      /* Craft the command line. */
1300      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      // Locate all the executables.
1329      let spack = SpackInvocation::summon().await.unwrap();
1330
1331      // Create python execution request.
1332      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      // Spawn the child process and wait for it to complete.
1343      let output = command.clone().invoke().await.expect("sync command failed");
1344      // Parse output into UTF-8...
1345      let decoded = output.decode(command.clone()).expect("decoding failed");
1346      // ...and verify the version matches `spack.version`.
1347      let version = decoded.stdout.strip_suffix("\n").unwrap();
1348      assert!(version == &spack.version);
1349      Ok(())
1350    }
1351  }
1352}
1353
1354/// Compiler-find command.
1355pub 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  /// Errors locating a compiler.
1371  #[derive(Debug, Display, Error)]
1372  pub enum CompilerFindError {
1373    /// command line error {0}
1374    Command(#[from] exe::CommandErrorWrapper),
1375    /// setup error {0}
1376    Setup(#[from] base::SetupErrorWrapper),
1377    /// io error {0}
1378    Io(#[from] io::Error),
1379    /// json error {0}
1380    Json(#[from] serde_json::Error),
1381    /// unknown error: {0}
1382    Unknown(String),
1383  }
1384
1385  /// Compiler-find request.
1386  #[derive(Debug, Clone)]
1387  pub struct CompilerFind {
1388    #[allow(missing_docs)]
1389    pub spack: SpackInvocation,
1390    /// Paths to search for compilers in.
1391    pub paths: Vec<PathBuf>,
1392    /// The scope to request the config be written into.
1393    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    /// Run `spack compiler find $self.paths`, without parsing the output.
1431    ///
1432    /// Use [`FindCompilerSpecs::find_compiler_specs`] to get information about
1433    /// the compilers spack can find.
1434    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    /// Paths to search for compilers in.
1449    pub paths: Vec<PathBuf>,
1450  }
1451
1452  /// A compiler found by [`CompilerFind::compiler_find`].
1453  #[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    /// Run a custom `spack python` script to print out compiler specs located
1480    /// in the given paths.
1481    ///
1482    /// If the given set of [`Self::paths`] is empty, use the defaults from
1483    /// config.
1484    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      // Locate all the executables.
1537      let spack = SpackInvocation::summon().await.unwrap();
1538
1539      // Create compiler-find execution request.
1540      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      // The first compiler on the list is gcc or clang!
1550      let first_name = &found_compilers[0].spec.nodes[0].name;
1551      assert!(first_name == "gcc" || first_name == "llvm");
1552      Ok(())
1553    }
1554  }
1555}
1556
1557/// `checksum` command.
1558pub 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 line error {0}
1577    Command(#[from] exe::CommandErrorWrapper),
1578    /// setup error {0}
1579    Setup(#[from] base::SetupErrorWrapper),
1580    /// utf-8 decoding error {0}
1581    Utf8(#[from] str::Utf8Error),
1582    /// io error {0}
1583    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  /// Request to add a new version to a package in the summoned spack repo.
1641  #[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      /* Accept the changes without user interaction. */
1668      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      /* Execute the command. */
1707      let _ = command.invoke().await?;
1708
1709      /* Confirm that we have created the target version. */
1710      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      /* Our use of file locking within the summoning process does not
1717       * differentiate between different threads within the same process, so
1718       * we additionally lock in-process here. */
1719      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 the version is already known, we are done. */
1727      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      /* FIXME: in-process mutex too!! generalize this! */
1736      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      /* This unlocks the lockfile upon drop! */
1743      let _lockfile = task::spawn_blocking(move || {
1744        lockfile.lock_with_pid()?;
1745        Ok::<_, io::Error>(lockfile)
1746      })
1747      .await
1748      .unwrap()?;
1749
1750      /* See if the target version was created since we locked the lockfile. */
1751      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      /* Otherwise, we will execute a command that modifies the summoned checkout. */
1760      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      // Locate all the executables.
1774      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 error {0}
1806    Install(#[from] install::InstallError),
1807    /// command line error {0}
1808    Command(#[from] exe::CommandErrorWrapper),
1809    /// setup error {0}
1810    Setup(#[from] base::SetupErrorWrapper),
1811    /// io error {0}
1812    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      /* Our use of file locking within the summoning process does not
1915       * differentiate between different threads within the same process, so
1916       * we additionally lock in-process here. */
1917      let _lock = ENSURE_ENV_CREATE_LOCK.lock().await;
1918
1919      /* FIXME: in-process mutex too!! generalize this! */
1920      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      /* This unlocks the lockfile upon drop! */
1928      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      /* While holding the lock, install all the specs in the order given. */
1984      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}