pub mod python {
use super_process::{
base::{self, CommandBase},
exe, fs,
sync::SyncInvocable,
};
use async_trait::async_trait;
use displaydoc::Display;
use once_cell::sync::Lazy;
use regex::Regex;
use thiserror::Error;
use std::{env, ffi::OsString, io, path::Path, str};
#[derive(Debug, Display, Error)]
pub enum PythonError {
UnknownError(String),
Command(#[from] exe::CommandErrorWrapper),
Setup(#[from] base::SetupErrorWrapper),
Io(#[from] io::Error),
}
#[derive(Debug, Clone)]
pub struct PythonInvocation {
exe: exe::Exe,
inner: exe::Command,
}
#[async_trait]
impl CommandBase for PythonInvocation {
async fn setup_command(self) -> Result<exe::Command, base::SetupError> {
let Self { exe, mut inner } = self;
inner.unshift_new_exe(exe);
Ok(inner)
}
}
#[derive(Debug, Clone)]
pub struct FoundPython {
pub exe: exe::Exe,
pub version: String,
}
pub static PYTHON_VERSION_REGEX: Lazy<Regex> =
Lazy::new(|| Regex::new(r"^Python (3\.[0-9]+\.[0-9]+).*\n$").unwrap());
impl FoundPython {
fn determine_python_exename() -> exe::Exe {
let exe_name: OsString = env::var_os("SPACK_PYTHON").unwrap_or_else(|| "python3".into());
let exe_path = Path::new(&exe_name).to_path_buf();
exe::Exe(fs::File(exe_path))
}
pub async fn detect() -> Result<Self, PythonError> {
let py = Self::determine_python_exename();
let command = PythonInvocation {
exe: py.clone(),
inner: exe::Command {
argv: ["--version"].as_ref().into(),
..Default::default()
},
}
.setup_command()
.await
.map_err(|e| e.with_context(format!("in FoundPython::detect(py = {:?})", &py)))?;
let output = command.invoke().await?;
let stdout = str::from_utf8(&output.stdout).map_err(|e| {
PythonError::UnknownError(format!(
"could not parse utf8 from '{} --version' stdout ({}); received:\n{:?}",
&py, &e, &output.stdout
))
})?;
match PYTHON_VERSION_REGEX.captures(stdout) {
Some(m) => Ok(Self {
exe: py,
version: m.get(1).unwrap().as_str().to_string(),
}),
None => Err(PythonError::UnknownError(format!(
"could not parse '{} --version'; received:\n(stdout):\n{}",
py, &stdout,
))),
}
}
pub(crate) fn with_python_exe(self, inner: exe::Command) -> PythonInvocation {
let Self { exe, .. } = self;
PythonInvocation { exe, inner }
}
}
#[cfg(test)]
mod test {
use super::*;
use tokio;
#[tokio::test]
async fn test_detect_python() -> Result<(), crate::Error> {
let _python = FoundPython::detect().await.unwrap();
Ok(())
}
}
}
pub mod spack {
use super::python;
use crate::{commands, summoning};
use super_process::{
base::{self, CommandBase},
exe, fs,
sync::SyncInvocable,
};
use async_trait::async_trait;
use displaydoc::Display;
use thiserror::Error;
use std::{
io,
path::{Path, PathBuf},
str,
};
#[derive(Debug, Display, Error)]
pub enum InvocationSummoningError {
Validation(#[from] base::SetupErrorWrapper),
Command(#[from] exe::CommandErrorWrapper),
Summon(#[from] summoning::SummoningError),
CompilerFind(#[from] commands::compiler_find::CompilerFindError),
Bootstrap(#[from] commands::install::InstallError),
Python(#[from] python::PythonError),
Io(#[from] io::Error),
}
#[derive(Debug, Clone)]
pub struct SpackInvocation {
python: python::FoundPython,
repo: summoning::SpackRepo,
#[allow(dead_code)]
pub version: String,
}
pub(crate) static SUMMON_CUR_PROCESS_LOCK: once_cell::sync::Lazy<tokio::sync::Mutex<()>> =
once_cell::sync::Lazy::new(|| tokio::sync::Mutex::new(()));
impl SpackInvocation {
pub(crate) fn cache_location(&self) -> &Path { self.repo.cache_location() }
pub async fn create(
python: python::FoundPython,
repo: summoning::SpackRepo,
) -> Result<Self, InvocationSummoningError> {
let script_path = format!("{}", repo.script_path.display());
let command = python
.clone()
.with_python_exe(exe::Command {
argv: [&script_path, "--version"].as_ref().into(),
..Default::default()
})
.setup_command()
.await
.map_err(|e| e.with_context(format!("with py {:?} and repo {:?}", &python, &repo)))?;
let output = command.clone().invoke().await?;
let version = str::from_utf8(&output.stdout)
.map_err(|e| format!("utf8 decoding error {}: from {:?}", e, &output.stdout))
.and_then(|s| {
s.strip_suffix('\n')
.ok_or_else(|| format!("failed to strip final newline from output: '{}'", s))
})
.map_err(|e: String| {
python::PythonError::UnknownError(format!(
"error parsing '{} {} --version' output: {}",
&python.exe, &script_path, e
))
})?
.to_string();
Ok(Self {
python,
repo,
version,
})
}
async fn ensure_compilers_found(&self) -> Result<(), InvocationSummoningError> {
let find_site_compilers = commands::compiler_find::CompilerFind {
spack: self.clone(),
paths: vec![PathBuf::from("/usr/bin")],
scope: Some("site".to_string()),
};
find_site_compilers.compiler_find().await?;
Ok(())
}
async fn bootstrap(
&self,
cache_dir: summoning::CacheDir,
) -> Result<(), InvocationSummoningError> {
let bootstrap_proof_name: PathBuf = format!("{}.bootstrap_proof", cache_dir.dirname()).into();
let bootstrap_proof_path = cache_dir.location().join(bootstrap_proof_name);
match tokio::fs::File::open(&bootstrap_proof_path).await {
Ok(_) => return Ok(()),
Err(e) if e.kind() == io::ErrorKind::NotFound => (),
Err(e) => return Err(e.into()),
}
let bootstrap_lock_name: PathBuf = format!("{}.bootstrap_lock", cache_dir.dirname()).into();
let bootstrap_lock_path = cache_dir.location().join(bootstrap_lock_name);
let mut lockfile =
tokio::task::spawn_blocking(move || fslock::LockFile::open(&bootstrap_lock_path))
.await
.unwrap()?;
let _lockfile = tokio::task::spawn_blocking(move || {
lockfile.lock_with_pid()?;
Ok::<_, io::Error>(lockfile)
})
.await
.unwrap()?;
if tokio::fs::File::open(&bootstrap_proof_path).await.is_ok() {
return Ok(());
}
eprintln!(
"bootstrapping spack {}",
crate::versions::patches::PATCHES_TOPLEVEL_COMPONENT,
);
self.ensure_compilers_found().await?;
let bootstrap_install = commands::install::Install {
spack: self.clone(),
spec: commands::CLISpec::new("m4"),
verbosity: Default::default(),
env: None,
repos: None,
};
let installed_spec = bootstrap_install.install_find().await?;
use tokio::io::AsyncWriteExt;
let mut proof = tokio::fs::File::create(bootstrap_proof_path).await?;
proof
.write_all(format!("{}", installed_spec.hashed_spec()).as_bytes())
.await?;
Ok(())
}
pub async fn summon() -> Result<Self, InvocationSummoningError> {
let _lock = SUMMON_CUR_PROCESS_LOCK.lock().await;
let python = python::FoundPython::detect().await?;
let cache_dir = summoning::CacheDir::get_or_create().await?;
let spack_repo = summoning::SpackRepo::summon(cache_dir.clone()).await?;
let spack = Self::create(python, spack_repo).await?;
spack.bootstrap(cache_dir).await?;
Ok(spack)
}
pub(crate) fn with_spack_exe(self, inner: exe::Command) -> ReadiedSpackInvocation {
let Self { python, repo, .. } = self;
ReadiedSpackInvocation {
python,
repo,
inner,
}
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::{subprocess::python::*, summoning::*};
use tokio;
#[tokio::test]
async fn test_summon() -> Result<(), crate::Error> {
let spack = SpackInvocation::summon().await?;
assert_eq!(spack.version, "0.22.0.dev0");
Ok(())
}
#[tokio::test]
async fn test_create_invocation() -> Result<(), crate::Error> {
let _lock = SUMMON_CUR_PROCESS_LOCK.lock().await;
let python = FoundPython::detect().await.unwrap();
let cache_dir = CacheDir::get_or_create().await.unwrap();
let spack_exe = SpackRepo::summon(cache_dir).await.unwrap();
let spack = SpackInvocation::create(python, spack_exe).await?;
assert_eq!(spack.version, "0.22.0.dev0");
Ok(())
}
}
pub(crate) struct ReadiedSpackInvocation {
pub python: python::FoundPython,
pub repo: summoning::SpackRepo,
pub inner: exe::Command,
}
#[async_trait]
impl base::CommandBase for ReadiedSpackInvocation {
async fn setup_command(self) -> Result<exe::Command, base::SetupError> {
let Self {
python,
repo:
summoning::SpackRepo {
script_path,
repo_path,
..
},
mut inner,
} = self;
assert!(inner.wd.is_none(), "assuming working dir was not yet set");
inner.wd = Some(fs::Directory(repo_path));
assert!(inner.exe.is_empty());
inner.unshift_new_exe(exe::Exe(fs::File(script_path)));
let py = python.with_python_exe(inner);
Ok(py.setup_command().await?)
}
}
}