use std::io::Write;
use std::path::PathBuf;
use std::{env, process};
use anyhow::Result;
use serde_json::{Map, Value as Json, json};
use tracing::{info, warn};
use evix::{AutoArg, Config, Event, Input, WORKER_ENV};
use pound::Parse;
#[derive(Parse, Clone)]
#[pound(name = "evix", required_group = "input")]
struct Args {
#[pound(long, group = "input")]
flake: Option<String>,
#[pound(long, group = "input")]
expr: Option<String>,
#[pound(long, group = "input")]
file: Option<PathBuf>,
#[pound(long)]
arg: Vec<String>,
#[pound(long)]
argstr: Vec<String>,
#[pound(long)]
override_input: Vec<String>,
#[pound(long)]
option: Vec<String>,
#[pound(long)]
meta: bool,
#[pound(long)]
show_input_drvs: bool,
#[pound(long, default = "1")]
workers: usize,
#[pound(long, default = "4096")]
max_memory_size: usize,
#[pound(long)]
force_recurse: bool,
#[pound(long)]
gc_roots_dir: Option<PathBuf>,
#[pound(short, long, count)]
verbose: u8,
}
impl Args {
fn to_config(&self) -> Config {
let input = if let Some(flake) = &self.flake {
Input::Flake(flake.clone())
} else if let Some(expr) = &self.expr {
Input::Expr(expr.clone())
} else if let Some(file) = &self.file {
Input::File(file.clone())
} else {
unreachable!("pound requires one of --flake, --expr, --file")
};
let mut auto_args = Vec::new();
for chunk in self.arg.chunks(2) {
let [name, expr] = chunk else {
eprintln!("--arg requires paired NAME EXPR values: use --arg NAME --arg EXPR");
process::exit(2);
};
auto_args.push((name.clone(), AutoArg::Expr(expr.clone())));
}
for chunk in self.argstr.chunks(2) {
let [name, value] = chunk else {
eprintln!(
"--argstr requires paired NAME VALUE values: use --argstr NAME --argstr VALUE"
);
process::exit(2);
};
auto_args.push((name.clone(), AutoArg::Str(value.clone())));
}
let mut override_inputs = Vec::new();
for chunk in self.override_input.chunks(2) {
let [name, value] = chunk else {
eprintln!(
"--override-input requires paired NAME REF values: use --override-input NAME --override-input REF"
);
process::exit(2);
};
override_inputs.push((name.clone(), value.clone()));
}
let mut nix_options = Vec::new();
for chunk in self.option.chunks(2) {
let [key, value] = chunk else {
eprintln!(
"--option requires paired KEY VALUE values: use --option KEY --option VALUE"
);
process::exit(2);
};
nix_options.push((key.clone(), value.clone()));
}
Config {
input,
auto_args,
force_recurse: self.force_recurse,
gc_roots_dir: self.gc_roots_dir.clone(),
workers: self.workers,
max_memory_size: self.max_memory_size,
meta: self.meta,
show_input_drvs: self.show_input_drvs,
override_inputs,
nix_options,
}
}
fn init_tracing(&self) {
init_tracing_subscriber(self.verbose);
}
}
fn init_tracing_subscriber(verbose: u8) {
let level = match verbose {
0 => "info",
1 => "debug",
_ => "trace",
};
tracing_subscriber::fmt()
.with_writer(std::io::stderr)
.with_target(false)
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new(level)),
)
.init();
}
fn main() -> Result<()> {
if env::var(WORKER_ENV).is_ok() {
init_tracing_subscriber(0);
return evix::run_worker();
}
let args = Args::parse();
args.init_tracing();
let config = args.to_config();
info!(workers = config.workers, "starting evix evaluation");
evix::evaluate(&config, |event| {
let line = format_event(event);
writeln!(std::io::stdout().lock(), "{line}")?;
if let Event::Derivation(d) = event
&& let Some(ref err) = d.gc_root_error
{
warn!(drv_path = %d.drv_path, error = %err, "failed to register gc root");
}
Ok(())
})
}
fn format_event(event: &Event) -> String {
let json = match event {
Event::Derivation(d) => {
let mut outputs = Map::new();
for (k, v) in &d.outputs {
outputs.insert(
k.clone(),
v.as_ref().map_or(Json::Null, |p| Json::String(p.clone())),
);
}
let mut obj = Map::new();
obj.insert("attr".into(), json!(d.attr));
obj.insert("attrPath".into(), json!(d.attr_path));
obj.insert("name".into(), json!(d.name));
obj.insert("system".into(), json!(d.system));
obj.insert("drvPath".into(), json!(d.drv_path));
obj.insert("outputs".into(), Json::Object(outputs));
if let Some(meta) = &d.meta {
obj.insert("meta".into(), meta.clone());
}
if !d.input_drvs.is_empty() {
let drvs: Map<String, Json> = d.input_drvs.clone().into_iter().collect();
obj.insert("inputDrvs".into(), Json::Object(drvs));
}
if let Some(constituents) = &d.constituents {
obj.insert("constituents".into(), json!(constituents));
}
Json::Object(obj)
}
Event::AttrSet {
attr,
attr_path,
attrs,
} => json!({
"attr": attr,
"attrPath": attr_path,
"attrs": attrs,
}),
Event::Error(e) => json!({
"attr": e.attr,
"attrPath": e.attr_path,
"error": e.error,
"fatal": e.fatal,
}),
};
json.to_string()
}