nix_cache_watcher/
nix.rs

1//! Types and methods for interacting with `Nix`
2use bincode::{Decode, Encode};
3use snafu::{ResultExt, Snafu};
4use std::{
5    path::{Path, PathBuf},
6    process::Command,
7    time::{Duration, Instant},
8};
9use tracing::{debug, debug_span, error, info, info_span, instrument};
10
11/// Types for interacting with the store
12mod store;
13
14pub use store::{StoreError, StoreState};
15
16/// Configuration needed to interact with `Nix` properly
17///
18/// The [`Default`] provided value for this type is as follows:
19/// ``` rust
20/// use nix_cache_watcher::nix::NixConfiguration;
21///
22/// let configuration = NixConfiguration {
23///     store_path: "/nix/store".into(),
24/// };
25/// assert_eq!(configuration, NixConfiguration::default())
26/// ```
27#[derive(Clone, Debug, PartialEq, Eq, Encode, Decode)]
28pub struct NixConfiguration {
29    /// The path to the nix store
30    pub store_path: PathBuf,
31}
32
33impl Default for NixConfiguration {
34    fn default() -> Self {
35        Self {
36            store_path: "/nix/store/".into(),
37        }
38    }
39}
40
41/// Errors that can happen when shelling out to nix
42#[derive(Debug, Snafu)]
43#[non_exhaustive]
44pub enum NixError {
45    /// Error occured spawning process
46    ProcessSpawn {
47        /// Underlying error
48        source: std::io::Error,
49    },
50    /// Error calling `nix store sign`
51    #[snafu(display(
52        "Error calling `nix store sign`:\nexit_code:{:?}\nstdout:{}\n\nstderr:{}\npaths:{:?}`",
53        exit_code,
54        stdout,
55        stderr,
56        paths
57    ))]
58    SignatureError {
59        /// The exit code, if there was any
60        exit_code: Option<i32>,
61        /// The paths being signed
62        paths: Vec<PathBuf>,
63        /// The contents of stdout
64        stdout: String,
65        /// The contents of stderror
66        stderr: String,
67    },
68    /// Error calling `nix copy`
69    #[snafu(display(
70        "Error calling `nix copy`:\nexit_code:{:?}\nstdout:{}\n\nstderr:{}`",
71        exit_code,
72        stdout,
73        stderr
74    ))]
75    CopyError {
76        /// The exit code, if there was any
77        exit_code: Option<i32>,
78        /// The paths being signed
79        paths: Vec<PathBuf>,
80        /// The contents of stdout
81        stdout: String,
82        /// The contents of stderror
83        stderr: String,
84    },
85}
86
87/// Results struct for [`sign_store_paths`]
88pub struct SignatureResults {
89    /// Total number of new signatures
90    pub count: u64,
91    /// Duration that the sign_store_paths took to complete
92    pub duration: Duration,
93}
94
95/// Sign a list of top-level store paths recursively
96///
97/// Returns the number of paths signed
98///
99/// # Errors
100///
101/// Will propagate an error if the shelling out to the `nix` command fails
102#[instrument(skip(paths, key_path))]
103pub fn sign_store_paths(
104    paths: impl IntoIterator<Item = impl AsRef<Path>>,
105    key_path: impl AsRef<Path>,
106    fanout_factor: usize,
107) -> Result<SignatureResults, NixError> {
108    // Start the timer
109    let start = Instant::now();
110    let key_path = key_path.as_ref();
111    debug!(?key_path);
112    // Get the paths owned first
113    let paths: Vec<PathBuf> = paths.into_iter().map(|x| x.as_ref().to_owned()).collect();
114    let count = paths
115        .chunks(fanout_factor)
116        .map(|paths| {
117            debug_span!("nix store sign inner loop");
118            // Start a timer
119            let start = Instant::now();
120            // TODO Call out to nix for the signing
121            debug!("Attempting to sign {} paths", paths.len());
122            let output = Command::new("nix")
123                // Pass in our main arguments, we want to sign things recursively
124                .args(["store", "sign", "-r", "-v", "--key-file"])
125                // Add in the key file
126                .arg(key_path)
127                // Now add in the paths
128                .args(paths)
129                // Let it run and capture the output
130                .output()
131                .context(ProcessSpawnSnafu)?;
132            // Process the output, first handle any errors
133            if !output.status.success() {
134                error!(?output);
135                return SignatureSnafu {
136                    exit_code: output.status.code(),
137                    stdout: String::from(String::from_utf8_lossy(&output.stdout)),
138                    stderr: String::from(String::from_utf8_lossy(&output.stderr)),
139                    paths,
140                }
141                .fail();
142            }
143            // `nix store sign -v` outputs a format that looks like 'added 3 signatures' to stderr
144            let stderr: String = String::from_utf8_lossy(&output.stderr).into();
145            // The second one should be the number
146            let count: u64 = match stderr.split(' ').nth(1) {
147                Some(x) => match x.parse::<u64>() {
148                    Ok(x) => x,
149                    Err(_) => {
150                        return SignatureSnafu {
151                            exit_code: output.status.code(),
152                            stdout: String::from(String::from_utf8_lossy(&output.stdout)),
153                            stderr: String::from(String::from_utf8_lossy(&output.stderr)),
154                            paths,
155                        }
156                        .fail();
157                    }
158                },
159                None => {
160                    return SignatureSnafu {
161                        exit_code: output.status.code(),
162                        stdout: String::from(String::from_utf8_lossy(&output.stdout)),
163                        stderr: String::from(String::from_utf8_lossy(&output.stderr)),
164                        paths,
165                    }
166                    .fail();
167                }
168            };
169
170            // Stop the timer
171            let end = Instant::now();
172            let duration = end - start;
173            debug!(?count, ?duration, "nix sign store completed");
174            Ok::<u64, NixError>(count)
175        })
176        .collect::<Result<Vec<_>, _>>()?
177        .into_iter()
178        .sum();
179    // Stop the timer
180    let end = Instant::now();
181    let duration = end - start;
182    info!(?duration, ?count, "Completed signatures");
183    Ok(SignatureResults { count, duration })
184}
185
186/// Upload store paths to cache using the nix tooling
187#[instrument(skip(paths))]
188pub fn upload_paths_to_cache(
189    paths: impl IntoIterator<Item = impl AsRef<Path>>,
190    cache: &str,
191    fanout_factor: usize,
192) -> Result<(), NixError> {
193    // Get the paths owned first
194    let paths: Vec<PathBuf> = paths.into_iter().map(|x| x.as_ref().to_owned()).collect();
195    for paths in paths.chunks(fanout_factor) {
196        info_span!("nix store sign inner loop");
197        info!("Attempting to upload {} paths", paths.len());
198        let output = Command::new("nix")
199            // Pass in our main arguments, we want to copy to a store
200            .args(["copy", "--no-recursive", "--to"])
201            // Add in the store location
202            .arg(cache)
203            // Now add in the paths
204            .args(paths)
205            // Let it run and capture the output
206            .output()
207            .context(ProcessSpawnSnafu)?;
208        // Process the output, first handle any errors
209        if !output.status.success() {
210            error!(?output);
211            return CopySnafu {
212                exit_code: output.status.code(),
213                stdout: String::from(String::from_utf8_lossy(&output.stdout)),
214                stderr: String::from(String::from_utf8_lossy(&output.stderr)),
215                paths,
216            }
217            .fail();
218        }
219    }
220    Ok(())
221}