spack/
summoning.rs

1/* Copyright 2022-2023 Danny McClanahan */
2/* SPDX-License-Identifier: (Apache-2.0 OR MIT) */
3
4//! Get a copy of spack.
5
6use crate::{utils, versions::patches::*};
7
8use displaydoc::Display;
9use flate2::read::GzDecoder;
10use fslock;
11use hex::ToHex;
12use reqwest;
13use sha2::{Digest, Sha256};
14use tar;
15use thiserror::Error;
16use tokio::{
17  fs,
18  io::{self, AsyncReadExt, AsyncWriteExt},
19  task,
20};
21
22use std::{
23  env,
24  path::{Path, PathBuf},
25};
26
27/// Errors that can occur while summoning.
28#[derive(Debug, Display, Error)]
29pub enum SummoningError {
30  /// reqwest error: {0}
31  Http(#[from] reqwest::Error),
32  /// i/o error: {0}
33  Io(#[from] io::Error),
34  /// checksum error from URL {0}; expected {1}, got {2}
35  Checksum(String, String, String),
36  /// unknown error: {0}
37  UnknownError(String),
38}
39
40/// Base directory for cached spack installs.
41#[derive(Clone, Debug)]
42pub struct CacheDir {
43  location: PathBuf,
44}
45
46impl CacheDir {
47  /// Goes to `~/.spack/summonings`.
48  ///
49  /// Name intentionally chosen to be overridden later after upstreaming to
50  /// spack (?).
51  pub async fn get_or_create() -> Result<Self, SummoningError> {
52    let path = PathBuf::from(env::var("HOME").expect("$HOME should always be defined!"))
53      .join(".spack")
54      .join("summonings");
55    let p = path.clone();
56    task::spawn_blocking(move || utils::safe_create_dir_all_ioerror(&p))
57      .await
58      .unwrap()?;
59    Ok(Self { location: path })
60  }
61
62  pub fn location(&self) -> &Path { &self.location }
63
64  /// We use the hex-encoded checksum value as the ultimate directory name.
65  pub fn dirname(&self) -> String { PATCHES_SHA256SUM.encode_hex() }
66
67  /// The path to unpack the tar archive into.
68  pub fn unpacking_path(&self) -> PathBuf { self.location.join(PATCHES_TOPLEVEL_COMPONENT) }
69
70  /// The path to download the release tarball to.
71  pub fn tarball_path(&self) -> PathBuf { self.location.join(format!("{}.tar.gz", self.dirname())) }
72
73  /// The path to the root of the spack repo, through a symlink.
74  ///
75  /// FIXME: Note that this repeats the
76  /// [`PATCHES_TOPLEVEL_COMPONENT`] component
77  /// used in [`Self::unpacking_path`].
78  pub fn repo_root(&self) -> PathBuf { self.unpacking_path().join(PATCHES_TOPLEVEL_COMPONENT) }
79
80  /// The path to the spack script in the spack repo, through a symlink.
81  pub fn spack_script(&self) -> PathBuf { self.repo_root().join("bin").join("spack") }
82}
83
84struct SpackTarball {
85  downloaded_location: PathBuf,
86}
87
88impl SpackTarball {
89  pub fn downloaded_path(&self) -> &Path { self.downloaded_location.as_ref() }
90
91  async fn check_tarball_digest(
92    tgz_path: &Path,
93    tgz: &mut fs::File,
94  ) -> Result<Self, SummoningError> {
95    /* If we have a file already, we just need to check the digest. */
96    let mut tarball_bytes: Vec<u8> = vec![];
97    tgz.read_to_end(&mut tarball_bytes).await?;
98    let mut hasher = Sha256::new();
99    hasher.update(&tarball_bytes);
100    let checksum: [u8; 32] = hasher.finalize().into();
101    if checksum == PATCHES_SHA256SUM {
102      Ok(Self {
103        downloaded_location: tgz_path.to_path_buf(),
104      })
105    } else {
106      Err(SummoningError::Checksum(
107        format!("file://{}", tgz_path.display()),
108        PATCHES_SHA256SUM.encode_hex(),
109        checksum.encode_hex(),
110      ))
111    }
112  }
113
114  /* FIXME: test the checksum checking!!! */
115  pub async fn fetch_spack_tarball(cache_dir: CacheDir) -> Result<Self, SummoningError> {
116    let tgz_path = cache_dir.tarball_path();
117
118    match fs::File::open(&tgz_path).await {
119      Ok(mut tgz) => Self::check_tarball_digest(&tgz_path, &mut tgz).await,
120      Err(e) if e.kind() == io::ErrorKind::NotFound => {
121        /* If we don't already have a file, we download it! */
122        let lockfile_name: PathBuf = format!("{}.tgz.lock", cache_dir.dirname()).into();
123        let lockfile_path = cache_dir.location().join(lockfile_name);
124        let mut lockfile = task::spawn_blocking(move || fslock::LockFile::open(&lockfile_path))
125          .await
126          .unwrap()?;
127        /* This unlocks the lockfile upon drop! */
128        let _lockfile = task::spawn_blocking(move || {
129          lockfile.lock_with_pid()?;
130          Ok::<_, io::Error>(lockfile)
131        })
132        .await
133        .unwrap()?;
134        /* FIXME: delete the lockfile after the proof is written! */
135
136        /* See if the target file was created since we locked the lockfile. */
137        if let Ok(mut tgz) = fs::File::open(&tgz_path).await {
138          /* If so, check the digest! */
139          return Self::check_tarball_digest(&tgz_path, &mut tgz).await;
140        }
141
142        eprintln!(
143          "downloading spack {} from {}...",
144          PATCHES_TOPLEVEL_COMPONENT, PATCHES_SPACK_URL,
145        );
146        let resp = reqwest::get(PATCHES_SPACK_URL).await?;
147        let tarball_bytes = resp.bytes().await?;
148        let mut hasher = Sha256::new();
149        hasher.update(&tarball_bytes);
150        let checksum: [u8; 32] = hasher.finalize().into();
151        if checksum == PATCHES_SHA256SUM {
152          let mut tgz = fs::File::create(&tgz_path).await?;
153          tgz.write_all(&tarball_bytes).await?;
154          tgz.sync_all().await?;
155          Ok(Self {
156            downloaded_location: tgz_path.to_path_buf(),
157          })
158        } else {
159          Err(SummoningError::Checksum(
160            PATCHES_SPACK_URL.to_string(),
161            PATCHES_SHA256SUM.encode_hex(),
162            checksum.encode_hex(),
163          ))
164        }
165      },
166      Err(e) => Err(e.into()),
167    }
168  }
169}
170
171/// Location of a spack executable script.
172#[derive(Debug, Clone)]
173pub struct SpackRepo {
174  /// NB: This script was not checked to be executable!
175  pub script_path: PathBuf,
176  /// This directory *must* exist when returned by [Self::summon].
177  pub repo_path: PathBuf,
178  cache_dir: CacheDir,
179}
180
181impl SpackRepo {
182  pub(crate) fn cache_location(&self) -> &Path { self.cache_dir.location() }
183
184  pub(crate) fn unzip_archive(from: &Path, into: &Path) -> Result<Option<()>, SummoningError> {
185    match std::fs::File::open(from) {
186      Ok(tgz) => {
187        let gz_decoded = GzDecoder::new(tgz);
188        let mut archive = tar::Archive::new(gz_decoded);
189        Ok(Some(archive.unpack(into)?))
190      },
191      Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(None),
192      Err(e) => Err(e.into()),
193    }
194  }
195
196  async fn unzip_spack_archive(cache_dir: CacheDir) -> Result<Option<()>, SummoningError> {
197    let from = cache_dir.tarball_path();
198    let into = cache_dir.unpacking_path();
199    task::spawn_blocking(move || Self::unzip_archive(&from, &into))
200      .await
201      .unwrap()
202  }
203
204  pub(crate) async fn get_spack_script(cache_dir: CacheDir) -> Result<Self, SummoningError> {
205    let path = cache_dir.spack_script();
206    let _ = fs::File::open(&path).await?;
207    Ok(Self {
208      script_path: path,
209      repo_path: cache_dir.repo_root(),
210      cache_dir,
211    })
212  }
213
214  async fn ensure_unpacked(
215    current_link_path: PathBuf,
216    cache_dir: &CacheDir,
217  ) -> Result<(), SummoningError> {
218    match fs::read_dir(&current_link_path).await {
219      Ok(_) => Ok(()),
220      Err(e) if e.kind() == io::ErrorKind::NotFound => {
221        /* (2) If the spack repo wasn't found on disk, try finding an adjacent
222         * tarball. */
223
224        let lockfile_name: PathBuf = format!("{}.lock", cache_dir.dirname()).into();
225        let lockfile_path = cache_dir.location().join(lockfile_name);
226        let mut lockfile = task::spawn_blocking(move || fslock::LockFile::open(&lockfile_path))
227          .await
228          .unwrap()?;
229        /* This unlocks the lockfile upon drop! */
230        let _lockfile = task::spawn_blocking(move || {
231          lockfile.lock_with_pid()?;
232          Ok::<_, io::Error>(lockfile)
233        })
234        .await
235        .unwrap()?;
236
237        /* See if the target dir was created since we locked the lockfile. */
238        match fs::read_dir(&current_link_path).await {
239          /* If so, return early! */
240          Ok(_) => Ok::<_, SummoningError>(()),
241          /* Otherwise, extract it! */
242          Err(e) if e.kind() == io::ErrorKind::NotFound => {
243            eprintln!("extracting spack {}...", PATCHES_TOPLEVEL_COMPONENT,);
244            assert!(Self::unzip_spack_archive(cache_dir.clone())
245              .await?
246              .is_some());
247            Ok(())
248          },
249          Err(e) => Err(e.into()),
250        }
251      },
252      Err(e) => Err(e.into()),
253    }
254  }
255
256  /// Get the most up-to-date version of spack with appropriate changes.
257  ///
258  /// If necessary, download the release tarball, validate its checksum, then
259  /// expand the tarball. Return the path to the spack root directory.
260  pub async fn summon(cache_dir: CacheDir) -> Result<Self, SummoningError> {
261    let spack_tarball = SpackTarball::fetch_spack_tarball(cache_dir.clone()).await?;
262    dbg!(spack_tarball.downloaded_path());
263
264    let current_link_path = cache_dir.unpacking_path();
265    Self::ensure_unpacked(current_link_path, &cache_dir).await?;
266
267    Self::get_spack_script(cache_dir).await
268  }
269}
270
271/* FIXME: this test will break all the other ones if it modifies the $HOME
272 * variable! */
273/* #[cfg(test)] */
274/* mod test { */
275/* use tokio; */
276
277/* #[tokio::test] */
278/* async fn test_summon() -> Result<(), super::SummoningError> { */
279/* use crate::summoning::*; */
280/* use std::fs::File; */
281
282/* let td = tempdir::TempDir::new("spack-summon-test").unwrap(); */
283/* std::env::set_var("HOME", td.path()); */
284/* let cache_dir = CacheDir::get_or_create()?; */
285/* let spack_exe = SpackRepo::summon(cache_dir).await?; */
286/* let _ = File::open(&spack_exe.script_path).expect("spack script should
287 * exist"); */
288/* Ok(()) */
289/* } */
290/* } */