Skip to main content

ephemeris_release/proposal/
assets.rs

1//! Dependency assets built from the worktree of the dependency release to
2//! pin their digests.
3
4use std::path::{Path, PathBuf};
5use std::process::Command;
6use std::{fs, io};
7
8use crate::state::DigestPin;
9
10/// An asset package cannot be built or read.
11#[derive(Debug, thiserror::Error)]
12pub(crate) enum AssetError {
13    /// The Nix command did not start.
14    #[error("cannot run nix to build {package:?}")]
15    Spawn {
16        /// The package attribute.
17        package: String,
18        /// The failure.
19        #[source]
20        source: io::Error,
21    },
22    /// The Nix build failed.
23    #[error("nix could not build {package:?}: {stderr}")]
24    Build {
25        /// The package attribute.
26        package: String,
27        /// What Nix wrote to its error stream.
28        stderr: String,
29    },
30    /// The Nix build printed no output path.
31    #[error("nix built {package:?} and printed no output path")]
32    NoOutputPath {
33        /// The package attribute.
34        package: String,
35    },
36    /// The asset file is missing or unreadable.
37    #[error("cannot read the built asset {}", path.display())]
38    Output {
39        /// The expected asset file.
40        path: PathBuf,
41        /// The failure.
42        #[source]
43        source: io::Error,
44    },
45}
46
47/// Builds a package of the proposal tree.
48pub(crate) trait AssetBuilder {
49    /// Build `package` for the current system and return its output directory.
50    fn build(&self, worktree: &Path, package: &str) -> Result<PathBuf, AssetError>;
51}
52
53/// Builds packages with `nix build` on a Git flake reference of the tree.
54#[derive(Clone, Copy, Debug)]
55pub(crate) struct NixBuilder;
56
57impl AssetBuilder for NixBuilder {
58    fn build(&self, worktree: &Path, package: &str) -> Result<PathBuf, AssetError> {
59        let reference = format!("git+file://{}#{package}", worktree.display());
60        let output = Command::new("nix")
61            .args(["build", "--no-link", "--print-out-paths", &reference])
62            .output()
63            .map_err(|source| AssetError::Spawn {
64                package: package.to_owned(),
65                source,
66            })?;
67        if !output.status.success() {
68            return Err(AssetError::Build {
69                package: package.to_owned(),
70                stderr: String::from_utf8_lossy(&output.stderr).into_owned(),
71            });
72        }
73        String::from_utf8_lossy(&output.stdout)
74            .lines()
75            .next()
76            .map(PathBuf::from)
77            .ok_or_else(|| AssetError::NoOutputPath {
78                package: package.to_owned(),
79            })
80    }
81}
82
83impl DigestPin {
84    /// Build the package of this pin and hash the asset.
85    pub(crate) fn digest<B: AssetBuilder>(
86        &self,
87        builder: &B,
88        worktree: &Path,
89    ) -> Result<String, AssetError> {
90        let file = builder.build(worktree, &self.package)?.join(&self.output);
91        let bytes = fs::read(&file).map_err(|source| AssetError::Output { path: file, source })?;
92        Ok(self.algorithm.hex(&bytes))
93    }
94}