nix-cache-watcher 0.0.7

Upload new nix artifacts to an s3-compatible binary cache
Documentation
//! Types and methods for interacting with `Nix`
use bincode::{Decode, Encode};
use snafu::{ResultExt, Snafu};
use std::{
    path::{Path, PathBuf},
    process::Command,
    time::{Duration, Instant},
};
use tracing::{debug, debug_span, error, info, info_span, instrument};

/// Types for interacting with the store
mod store;

pub use store::{StoreError, StoreState};

/// Configuration needed to interact with `Nix` properly
///
/// The [`Default`] provided value for this type is as follows:
/// ``` rust
/// use nix_cache_watcher::nix::NixConfiguration;
///
/// let configuration = NixConfiguration {
///     store_path: "/nix/store".into(),
/// };
/// assert_eq!(configuration, NixConfiguration::default())
/// ```
#[derive(Clone, Debug, PartialEq, Eq, Encode, Decode)]
pub struct NixConfiguration {
    /// The path to the nix store
    pub store_path: PathBuf,
}

impl Default for NixConfiguration {
    fn default() -> Self {
        Self {
            store_path: "/nix/store/".into(),
        }
    }
}

/// Errors that can happen when shelling out to nix
#[derive(Debug, Snafu)]
#[non_exhaustive]
pub enum NixError {
    /// Error occured spawning process
    ProcessSpawn {
        /// Underlying error
        source: std::io::Error,
    },
    /// Error calling `nix store sign`
    #[snafu(display(
        "Error calling `nix store sign`:\nexit_code:{:?}\nstdout:{}\n\nstderr:{}\npaths:{:?}`",
        exit_code,
        stdout,
        stderr,
        paths
    ))]
    SignatureError {
        /// The exit code, if there was any
        exit_code: Option<i32>,
        /// The paths being signed
        paths: Vec<PathBuf>,
        /// The contents of stdout
        stdout: String,
        /// The contents of stderror
        stderr: String,
    },
    /// Error calling `nix copy`
    #[snafu(display(
        "Error calling `nix copy`:\nexit_code:{:?}\nstdout:{}\n\nstderr:{}`",
        exit_code,
        stdout,
        stderr
    ))]
    CopyError {
        /// The exit code, if there was any
        exit_code: Option<i32>,
        /// The paths being signed
        paths: Vec<PathBuf>,
        /// The contents of stdout
        stdout: String,
        /// The contents of stderror
        stderr: String,
    },
}

/// Results struct for [`sign_store_paths`]
pub struct SignatureResults {
    /// Total number of new signatures
    pub count: u64,
    /// Duration that the sign_store_paths took to complete
    pub duration: Duration,
}

/// Sign a list of top-level store paths recursively
///
/// Returns the number of paths signed
///
/// # Errors
///
/// Will propagate an error if the shelling out to the `nix` command fails
#[instrument(skip(paths, key_path))]
pub fn sign_store_paths(
    paths: impl IntoIterator<Item = impl AsRef<Path>>,
    key_path: impl AsRef<Path>,
    fanout_factor: usize,
) -> Result<SignatureResults, NixError> {
    // Start the timer
    let start = Instant::now();
    let key_path = key_path.as_ref();
    debug!(?key_path);
    // Get the paths owned first
    let paths: Vec<PathBuf> = paths.into_iter().map(|x| x.as_ref().to_owned()).collect();
    let count = paths
        .chunks(fanout_factor)
        .map(|paths| {
            debug_span!("nix store sign inner loop");
            // Start a timer
            let start = Instant::now();
            // TODO Call out to nix for the signing
            debug!("Attempting to sign {} paths", paths.len());
            let output = Command::new("nix")
                // Pass in our main arguments, we want to sign things recursively
                .args(["store", "sign", "-r", "-v", "--key-file"])
                // Add in the key file
                .arg(key_path)
                // Now add in the paths
                .args(paths)
                // Let it run and capture the output
                .output()
                .context(ProcessSpawnSnafu)?;
            // Process the output, first handle any errors
            if !output.status.success() {
                error!(?output);
                return SignatureSnafu {
                    exit_code: output.status.code(),
                    stdout: String::from(String::from_utf8_lossy(&output.stdout)),
                    stderr: String::from(String::from_utf8_lossy(&output.stderr)),
                    paths,
                }
                .fail();
            }
            // `nix store sign -v` outputs a format that looks like 'added 3 signatures' to stderr
            let stderr: String = String::from_utf8_lossy(&output.stderr).into();
            // The second one should be the number
            let count: u64 = match stderr.split(' ').nth(1) {
                Some(x) => match x.parse::<u64>() {
                    Ok(x) => x,
                    Err(_) => {
                        return SignatureSnafu {
                            exit_code: output.status.code(),
                            stdout: String::from(String::from_utf8_lossy(&output.stdout)),
                            stderr: String::from(String::from_utf8_lossy(&output.stderr)),
                            paths,
                        }
                        .fail();
                    }
                },
                None => {
                    return SignatureSnafu {
                        exit_code: output.status.code(),
                        stdout: String::from(String::from_utf8_lossy(&output.stdout)),
                        stderr: String::from(String::from_utf8_lossy(&output.stderr)),
                        paths,
                    }
                    .fail();
                }
            };

            // Stop the timer
            let end = Instant::now();
            let duration = end - start;
            debug!(?count, ?duration, "nix sign store completed");
            Ok::<u64, NixError>(count)
        })
        .collect::<Result<Vec<_>, _>>()?
        .into_iter()
        .sum();
    // Stop the timer
    let end = Instant::now();
    let duration = end - start;
    info!(?duration, ?count, "Completed signatures");
    Ok(SignatureResults { count, duration })
}

/// Upload store paths to cache using the nix tooling
#[instrument(skip(paths))]
pub fn upload_paths_to_cache(
    paths: impl IntoIterator<Item = impl AsRef<Path>>,
    cache: &str,
    fanout_factor: usize,
) -> Result<(), NixError> {
    // Get the paths owned first
    let paths: Vec<PathBuf> = paths.into_iter().map(|x| x.as_ref().to_owned()).collect();
    for paths in paths.chunks(fanout_factor) {
        info_span!("nix store sign inner loop");
        info!("Attempting to upload {} paths", paths.len());
        let output = Command::new("nix")
            // Pass in our main arguments, we want to copy to a store
            .args(["copy", "--no-recursive", "--to"])
            // Add in the store location
            .arg(cache)
            // Now add in the paths
            .args(paths)
            // Let it run and capture the output
            .output()
            .context(ProcessSpawnSnafu)?;
        // Process the output, first handle any errors
        if !output.status.success() {
            error!(?output);
            return CopySnafu {
                exit_code: output.status.code(),
                stdout: String::from(String::from_utf8_lossy(&output.stdout)),
                stderr: String::from(String::from_utf8_lossy(&output.stderr)),
                paths,
            }
            .fail();
        }
    }
    Ok(())
}