1pub mod python {
7 use super_process::{
8 base::{self, CommandBase},
9 exe, fs,
10 sync::SyncInvocable,
11 };
12
13 use async_trait::async_trait;
14 use displaydoc::Display;
15 use once_cell::sync::Lazy;
16 use regex::Regex;
17 use thiserror::Error;
18
19 use std::{env, ffi::OsString, io, path::Path, str};
20
21 #[derive(Debug, Display, Error)]
23 pub enum PythonError {
24 UnknownError(String),
26 Command(#[from] exe::CommandErrorWrapper),
28 Setup(#[from] base::SetupErrorWrapper),
30 Io(#[from] io::Error),
32 }
33
34 #[derive(Debug, Clone)]
35 pub struct PythonInvocation {
36 exe: exe::Exe,
37 inner: exe::Command,
38 }
39
40 #[async_trait]
41 impl CommandBase for PythonInvocation {
42 async fn setup_command(self) -> Result<exe::Command, base::SetupError> {
43 let Self { exe, mut inner } = self;
44 inner.unshift_new_exe(exe);
45 Ok(inner)
46 }
47 }
48
49 #[derive(Debug, Clone)]
52 pub struct FoundPython {
53 pub exe: exe::Exe,
54 pub version: String,
56 }
57
58 pub static PYTHON_VERSION_REGEX: Lazy<Regex> =
60 Lazy::new(|| Regex::new(r"^Python (3\.[0-9]+\.[0-9]+).*\n$").unwrap());
61
62 impl FoundPython {
63 fn determine_python_exename() -> exe::Exe {
64 let exe_name: OsString = env::var_os("SPACK_PYTHON").unwrap_or_else(|| "python3".into());
65 let exe_path = Path::new(&exe_name).to_path_buf();
66 exe::Exe(fs::File(exe_path))
67 }
68
69 pub async fn detect() -> Result<Self, PythonError> {
72 let py = Self::determine_python_exename();
73 let command = PythonInvocation {
74 exe: py.clone(),
75 inner: exe::Command {
76 argv: ["--version"].as_ref().into(),
77 ..Default::default()
78 },
79 }
80 .setup_command()
81 .await
82 .map_err(|e| e.with_context(format!("in FoundPython::detect(py = {:?})", &py)))?;
83 let output = command.invoke().await?;
84 let stdout = str::from_utf8(&output.stdout).map_err(|e| {
85 PythonError::UnknownError(format!(
86 "could not parse utf8 from '{} --version' stdout ({}); received:\n{:?}",
87 &py, &e, &output.stdout
88 ))
89 })?;
90 match PYTHON_VERSION_REGEX.captures(stdout) {
91 Some(m) => Ok(Self {
92 exe: py,
93 version: m.get(1).unwrap().as_str().to_string(),
94 }),
95 None => Err(PythonError::UnknownError(format!(
96 "could not parse '{} --version'; received:\n(stdout):\n{}",
97 py, &stdout,
98 ))),
99 }
100 }
101
102 pub(crate) fn with_python_exe(self, inner: exe::Command) -> PythonInvocation {
103 let Self { exe, .. } = self;
104 PythonInvocation { exe, inner }
105 }
106 }
107
108 #[cfg(test)]
109 mod test {
110 use super::*;
111
112 use tokio;
113
114 #[tokio::test]
115 async fn test_detect_python() -> Result<(), crate::Error> {
116 let _python = FoundPython::detect().await.unwrap();
117 Ok(())
118 }
119 }
120}
121
122pub mod spack {
123 use super::python;
124 use crate::{commands, summoning};
125 use super_process::{
126 base::{self, CommandBase},
127 exe, fs,
128 sync::SyncInvocable,
129 };
130
131 use async_trait::async_trait;
132 use displaydoc::Display;
133 use thiserror::Error;
134
135 use std::{
136 io,
137 path::{Path, PathBuf},
138 str,
139 };
140
141 #[derive(Debug, Display, Error)]
142 pub enum InvocationSummoningError {
143 Validation(#[from] base::SetupErrorWrapper),
145 Command(#[from] exe::CommandErrorWrapper),
147 Summon(#[from] summoning::SummoningError),
149 CompilerFind(#[from] commands::compiler_find::CompilerFindError),
151 Bootstrap(#[from] commands::install::InstallError),
153 Python(#[from] python::PythonError),
155 Io(#[from] io::Error),
157 }
158
159 #[derive(Debug, Clone)]
161 pub struct SpackInvocation {
162 python: python::FoundPython,
164 repo: summoning::SpackRepo,
166 #[allow(dead_code)]
168 pub version: String,
169 }
170
171 pub(crate) static SUMMON_CUR_PROCESS_LOCK: once_cell::sync::Lazy<tokio::sync::Mutex<()>> =
172 once_cell::sync::Lazy::new(|| tokio::sync::Mutex::new(()));
173
174 impl SpackInvocation {
175 pub(crate) fn cache_location(&self) -> &Path { self.repo.cache_location() }
176
177 pub async fn create(
183 python: python::FoundPython,
184 repo: summoning::SpackRepo,
185 ) -> Result<Self, InvocationSummoningError> {
186 let script_path = format!("{}", repo.script_path.display());
187 let command = python
188 .clone()
189 .with_python_exe(exe::Command {
190 argv: [&script_path, "--version"].as_ref().into(),
191 ..Default::default()
192 })
193 .setup_command()
194 .await
195 .map_err(|e| e.with_context(format!("with py {:?} and repo {:?}", &python, &repo)))?;
196 let output = command.clone().invoke().await?;
197 let version = str::from_utf8(&output.stdout)
198 .map_err(|e| format!("utf8 decoding error {}: from {:?}", e, &output.stdout))
199 .and_then(|s| {
200 s.strip_suffix('\n')
201 .ok_or_else(|| format!("failed to strip final newline from output: '{}'", s))
202 })
203 .map_err(|e: String| {
204 python::PythonError::UnknownError(format!(
205 "error parsing '{} {} --version' output: {}",
206 &python.exe, &script_path, e
207 ))
208 })?
209 .to_string();
210 Ok(Self {
211 python,
212 repo,
213 version,
214 })
215 }
216
217 async fn ensure_compilers_found(&self) -> Result<(), InvocationSummoningError> {
218 let find_site_compilers = commands::compiler_find::CompilerFind {
219 spack: self.clone(),
220 paths: vec![PathBuf::from("/usr/bin")],
221 scope: Some("site".to_string()),
222 };
223 find_site_compilers.compiler_find().await?;
224 Ok(())
225 }
226
227 async fn bootstrap(
228 &self,
229 cache_dir: summoning::CacheDir,
230 ) -> Result<(), InvocationSummoningError> {
231 let bootstrap_proof_name: PathBuf = format!("{}.bootstrap_proof", cache_dir.dirname()).into();
232 let bootstrap_proof_path = cache_dir.location().join(bootstrap_proof_name);
233
234 match tokio::fs::File::open(&bootstrap_proof_path).await {
235 Ok(_) => return Ok(()),
236 Err(e) if e.kind() == io::ErrorKind::NotFound => (),
238 Err(e) => return Err(e.into()),
239 }
240
241 let bootstrap_lock_name: PathBuf = format!("{}.bootstrap_lock", cache_dir.dirname()).into();
242 let bootstrap_lock_path = cache_dir.location().join(bootstrap_lock_name);
243 let mut lockfile =
244 tokio::task::spawn_blocking(move || fslock::LockFile::open(&bootstrap_lock_path))
245 .await
246 .unwrap()?;
247 let _lockfile = tokio::task::spawn_blocking(move || {
249 lockfile.lock_with_pid()?;
250 Ok::<_, io::Error>(lockfile)
251 })
252 .await
253 .unwrap()?;
254
255 if tokio::fs::File::open(&bootstrap_proof_path).await.is_ok() {
257 return Ok(());
259 }
260
261 eprintln!(
262 "bootstrapping spack {}",
263 crate::versions::patches::PATCHES_TOPLEVEL_COMPONENT,
264 );
265
266 self.ensure_compilers_found().await?;
267
268 let bootstrap_install = commands::install::Install {
269 spack: self.clone(),
270 spec: commands::CLISpec::new("zlib"),
271 verbosity: Default::default(),
272 env: None,
273 repos: None,
274 };
275 let installed_spec = bootstrap_install.install_find().await?;
276
277 use tokio::io::AsyncWriteExt;
278 let mut proof = tokio::fs::File::create(bootstrap_proof_path).await?;
279 proof
280 .write_all(format!("{}", installed_spec.hashed_spec()).as_bytes())
281 .await?;
282
283 Ok(())
284 }
285
286 pub async fn summon() -> Result<Self, InvocationSummoningError> {
288 let _lock = SUMMON_CUR_PROCESS_LOCK.lock().await;
292
293 let python = python::FoundPython::detect().await?;
294 let cache_dir = summoning::CacheDir::get_or_create().await?;
295 let spack_repo = summoning::SpackRepo::summon(cache_dir.clone()).await?;
296 let spack = Self::create(python, spack_repo).await?;
297 spack.bootstrap(cache_dir).await?;
298 Ok(spack)
299 }
300
301 pub(crate) fn with_spack_exe(self, inner: exe::Command) -> ReadiedSpackInvocation {
303 let Self { python, repo, .. } = self;
304 ReadiedSpackInvocation {
305 python,
306 repo,
307 inner,
308 }
309 }
310 }
311
312 #[cfg(test)]
313 mod test {
314 use super::*;
315
316 use crate::{subprocess::python::*, summoning::*};
317
318 use tokio;
319
320 #[tokio::test]
321 async fn test_summon() -> Result<(), crate::Error> {
322 let spack = SpackInvocation::summon().await?;
323 assert_eq!(spack.version, "1.0.0.dev0");
325 Ok(())
326 }
327
328 #[tokio::test]
329 async fn test_create_invocation() -> Result<(), crate::Error> {
330 let _lock = SUMMON_CUR_PROCESS_LOCK.lock().await;
331
332 let python = FoundPython::detect().await.unwrap();
334 let cache_dir = CacheDir::get_or_create().await.unwrap();
335 let spack_exe = SpackRepo::summon(cache_dir).await.unwrap();
336 let spack = SpackInvocation::create(python, spack_exe).await?;
337
338 assert_eq!(spack.version, "1.0.0.dev0");
340 Ok(())
341 }
342 }
343
344 pub(crate) struct ReadiedSpackInvocation {
345 pub python: python::FoundPython,
346 pub repo: summoning::SpackRepo,
347 pub inner: exe::Command,
348 }
349
350 #[async_trait]
351 impl base::CommandBase for ReadiedSpackInvocation {
352 async fn setup_command(self) -> Result<exe::Command, base::SetupError> {
353 let Self {
354 python,
355 repo:
356 summoning::SpackRepo {
357 script_path,
358 repo_path,
359 ..
360 },
361 mut inner,
362 } = self;
363
364 assert!(inner.wd.is_none(), "assuming working dir was not yet set");
365 inner.wd = Some(fs::Directory(repo_path));
366
367 assert!(inner.exe.is_empty());
368 inner.unshift_new_exe(exe::Exe(fs::File(script_path)));
369 let py = python.with_python_exe(inner);
370
371 Ok(py.setup_command().await?)
372 }
373 }
374}