#![warn(
clippy::all,
clippy::pedantic,
rust_2018_idioms,
missing_docs,
clippy::missing_docs_in_private_items
)]
#![allow(
clippy::option_if_let_else,
clippy::module_name_repetitions,
clippy::shadow_unrelated,
clippy::must_use_candidate,
clippy::implicit_hasher
)]
use std::{
ffi::OsStr,
fs::File,
io::{BufWriter, Write},
os::unix::ffi::OsStrExt,
path::{Path, PathBuf},
};
use clap::{Parser, Subcommand};
use color_eyre::eyre::{eyre, Context, Result};
use nix_cache_watcher::nix::{
sign_store_paths, upload_paths_to_cache, NixConfiguration, StoreState,
};
use tracing::{debug, info, instrument, metadata::LevelFilter};
use tracing_subscriber::{fmt, prelude::*, EnvFilter};
#[derive(Parser)]
#[clap(name = "nix-cache-watcher")]
#[clap(version, author, about, long_about = None)]
#[clap(propagate_version = true)]
struct Cli {
#[clap(short, long, action, global(true))]
verbose: bool,
#[clap(short = 'u', long, action, global(true))]
respect_unicode: bool,
#[clap(
long,
value_parser,
value_name = "FILE",
default_value = ".nix_store_state",
global(true)
)]
state_location: PathBuf,
#[clap(long, value_parser, default_value_t = 50)]
fanout_factor: usize,
#[clap(short, long, action, global(true))]
filter_against_cache: bool,
#[clap(
short,
long,
value_parser,
value_name = "URI",
default_value = "https://cache.nixos.org",
global(true)
)]
cache_uri: String,
#[clap(short, long, value_parser, value_name = "URI", global(true))]
local_cache_uri: Option<String>,
#[clap(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
SaveStore,
DiffStore,
SignStore {
#[clap(short, long, value_parser, value_name = "FILE")]
key_file: PathBuf,
},
UploadDiff {
#[clap(short, long, value_parser, value_name = "STORE_PATH")]
remote_store: String,
},
}
#[async_std::main]
async fn main() -> Result<()> {
let cli = Cli::parse();
color_eyre::install()?;
if cli.verbose {
tracing_subscriber::registry()
.with(fmt::layer().with_writer(std::io::stderr).pretty())
.with(
EnvFilter::builder()
.with_default_directive(LevelFilter::DEBUG.into())
.from_env_lossy(),
)
.init();
} else {
tracing_subscriber::registry()
.with(fmt::layer().with_writer(std::io::stderr).pretty())
.with(
EnvFilter::builder()
.with_default_directive(LevelFilter::WARN.into())
.from_env_lossy(),
)
.init();
}
let nix_config = NixConfiguration::default();
let cache_uri: Option<&str> = if cli.filter_against_cache {
Some(&cli.cache_uri)
} else {
None
};
match cli.command {
Commands::SaveStore => {
let store_state = StoreState::from_store(&nix_config)
.context("Error reading state of the nix store")?;
println!("Captured {} store paths", store_state.path_count());
let location = cli.state_location;
info!(?location, "Creating store state file");
let file = File::create(&location).context("Failed to create store state file")?;
let mut encoder = xz2::write::XzEncoder::new(file, 3);
bincode::encode_into_std_write(&store_state, &mut encoder, bincode::config::standard())
.context("Failed to serialize the store state to file")?;
let mut file = encoder.finish().context("Failed to flush xz encoder")?;
file.flush().context("Failed to close out file")?;
}
Commands::DiffStore => {
let location = cli.state_location;
let diff = diff_state(
&location,
&nix_config,
cache_uri,
cli.local_cache_uri.as_deref(),
)
.await?;
info!("{} new store paths detected", diff.len());
print_paths(diff, cli.respect_unicode).context("Error printing paths")?;
}
Commands::SignStore { key_file } => {
let location = cli.state_location;
let diff = diff_state(
&location,
&nix_config,
cache_uri,
cli.local_cache_uri.as_deref(),
)
.await?;
info!("Signing {} new store paths", diff.len());
let results = sign_store_paths(diff, key_file, cli.fanout_factor)
.context("Failed to sign some or all store paths")?;
print!("Signed {} paths in {:?}", results.count, results.duration);
}
Commands::UploadDiff { remote_store } => {
let location = cli.state_location;
let diff = diff_state(
&location,
&nix_config,
cache_uri,
cli.local_cache_uri.as_deref(),
)
.await?;
info!("Uploading {} new store paths", diff.len());
upload_paths_to_cache(diff, &remote_store, cli.fanout_factor)
.context("Failed uploading paths to cache")?;
}
}
Ok(())
}
#[instrument]
async fn diff_state(
location: &Path,
nix_config: &NixConfiguration,
filter: Option<&str>,
local_cache: Option<&str>,
) -> Result<Vec<PathBuf>> {
let old_state = open_state_file(location)?;
let store_state =
StoreState::from_store(nix_config).context("Error reading state of the nix store")?;
info!("Captured {} store paths", store_state.path_count());
let state = old_state
.diff(&store_state)
.context("Failed to diff store")?;
let path_count = state.path_count();
info!("{} new paths", path_count);
let state = match filter {
Some(cache_uri) => {
let filtered = state
.filter_against_cache(cache_uri)
.await
.context("Failed to filter diff")?;
info!(
"{} paths filtered out against remote cache, {} paths remaining",
path_count - filtered.path_count(),
filtered.path_count()
);
filtered
}
None => state,
};
let path_count = state.path_count();
match local_cache {
Some(cache_uri) => {
let filtered = state
.filter_against_cache(cache_uri)
.await
.context("Failed to filter diff")?;
info!(
"{} paths filtered out against local cache, {} paths remaining.",
path_count - filtered.path_count(),
filtered.path_count()
);
Ok(filtered.paths().collect())
}
None => Ok(state.paths().collect()),
}
}
#[instrument]
fn open_state_file(location: &Path) -> Result<StoreState> {
info!(?location, "Opening store state file");
let file = File::open(&location).context("Failed to create store state file")?;
let mut decoder = xz2::read::XzDecoder::new(file);
let state: StoreState =
bincode::decode_from_std_read(&mut decoder, bincode::config::standard())
.context("Failed to decode store state from snapshot file")?;
info!(
"Decoded previous store state with {} captured paths",
state.path_count()
);
Ok(state)
}
fn print_paths(
paths: impl IntoIterator<Item = impl AsRef<Path>>,
respect_unicode: bool,
) -> Result<()> {
let stdout = std::io::stdout().lock();
let mut stdout = BufWriter::new(stdout);
if respect_unicode || atty::is(atty::Stream::Stdout) {
debug!(
?respect_unicode,
"Respecting unicode, this may have been forcibly enabled by printing to a tty."
);
for path in paths {
writeln!(stdout, "{}", path.as_ref().display())
.context("Failed writing path to stdout")?;
}
} else {
debug!(?respect_unicode, "Not respecting unicode");
for path in paths {
let path: &OsStr = path.as_ref().as_ref();
let path = path.as_bytes();
stdout
.write_all(path)
.context("Failed writing path to stdout")?;
writeln!(stdout).context("Failed writing path to stdout")?;
}
}
let mut stdout = match stdout.into_inner() {
Ok(x) => x,
Err(_) => return Err(eyre!("Failed flushing stdout")),
};
stdout.flush().context("Failed flushing stdout")?;
Ok(())
}