use std::{collections::BTreeMap, env, path::PathBuf};
use serde::{Deserialize, Serialize};
mod async_master;
mod error;
mod eval;
pub mod json;
mod remote_proto;
mod remote_worker;
mod run;
mod serde_config;
mod session;
mod state;
mod watch;
mod worker;
mod worker_config;
mod worker_process;
#[allow(clippy::all, warnings)]
mod worker_capnp {
include!(concat!(env!("OUT_DIR"), "/worker_capnp.rs"));
}
pub use error::{Error, Result};
pub use session::Session;
pub const WORKER_ENV: &str = "EVIX_WORKER";
#[derive(Debug, Clone)]
pub enum Input {
Flake(String),
Expr(String),
File(PathBuf),
}
#[derive(Debug, Clone)]
pub enum AutoArg {
Expr(String),
Str(String),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Config {
#[serde(with = "serde_config::input")]
pub input: Input,
#[serde(with = "serde_config::auto_args")]
pub auto_args: Vec<(String, AutoArg)>,
#[serde(default)]
pub force_recurse: bool,
pub gc_roots_dir: Option<PathBuf>,
pub workers: usize,
pub max_memory_size: usize,
#[serde(default)]
pub meta: bool,
#[serde(default)]
pub show_input_drvs: bool,
#[serde(default)]
pub override_inputs: Vec<(String, String)>,
#[serde(default)]
pub nix_options: Vec<(String, String)>,
#[serde(default)]
pub remotes: Vec<Remote>,
#[serde(skip)]
pub worker_exe: Option<PathBuf>,
}
impl Default for Config {
fn default() -> Self {
Self {
input: Input::Expr("{}".into()),
auto_args: Vec::new(),
force_recurse: false,
gc_roots_dir: None,
workers: 1,
max_memory_size: 4096,
meta: false,
show_input_drvs: false,
override_inputs: Vec::new(),
nix_options: Vec::new(),
remotes: Vec::new(),
worker_exe: None,
}
}
}
impl Config {
pub fn expr(expr: impl Into<String>) -> Self {
Self {
input: Input::Expr(expr.into()),
..Self::default()
}
}
pub fn file(path: impl Into<PathBuf>) -> Self {
Self {
input: Input::File(path.into()),
..Self::default()
}
}
pub fn flake(reference: impl Into<String>) -> Self {
Self {
input: Input::Flake(reference.into()),
..Self::default()
}
}
pub fn builder(self) -> ConfigBuilder {
ConfigBuilder { config: self }
}
}
#[derive(Debug, Clone)]
pub struct ConfigBuilder {
config: Config,
}
impl ConfigBuilder {
pub fn expr(expr: impl Into<String>) -> Self {
Config::expr(expr).builder()
}
pub fn file(path: impl Into<PathBuf>) -> Self {
Config::file(path).builder()
}
pub fn flake(reference: impl Into<String>) -> Self {
Config::flake(reference).builder()
}
pub fn force_recurse(mut self, enabled: bool) -> Self {
self.config.force_recurse = enabled;
self
}
pub fn gc_roots_dir(mut self, path: impl Into<PathBuf>) -> Self {
self.config.gc_roots_dir = Some(path.into());
self
}
pub fn workers(mut self, workers: usize) -> Self {
self.config.workers = workers;
self
}
pub fn max_memory_size(mut self, size: usize) -> Self {
self.config.max_memory_size = size;
self
}
pub fn meta(mut self, enabled: bool) -> Self {
self.config.meta = enabled;
self
}
pub fn show_input_drvs(mut self, enabled: bool) -> Self {
self.config.show_input_drvs = enabled;
self
}
pub fn auto_arg_expr(
mut self,
name: impl Into<String>,
value: impl Into<String>,
) -> Self {
self
.config
.auto_args
.push((name.into(), AutoArg::Expr(value.into())));
self
}
pub fn auto_arg_str(
mut self,
name: impl Into<String>,
value: impl Into<String>,
) -> Self {
self
.config
.auto_args
.push((name.into(), AutoArg::Str(value.into())));
self
}
pub fn override_input(
mut self,
name: impl Into<String>,
reference: impl Into<String>,
) -> Self {
self
.config
.override_inputs
.push((name.into(), reference.into()));
self
}
pub fn nix_option(
mut self,
key: impl Into<String>,
value: impl Into<String>,
) -> Self {
self.config.nix_options.push((key.into(), value.into()));
self
}
pub fn remote(mut self, remote: Remote) -> Self {
self.config.remotes.push(remote);
self
}
pub fn worker_exe(mut self, path: impl Into<PathBuf>) -> Self {
self.config.worker_exe = Some(path.into());
self
}
pub fn build(self) -> Config {
self.config
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Remote {
#[serde(alias = "host")]
pub endpoint: String,
pub systems: Vec<String>,
pub workers: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Derivation {
pub attr: String,
pub attr_path: Vec<String>,
pub name: String,
pub system: String,
pub drv_path: String,
pub outputs: BTreeMap<String, Option<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub meta: Option<serde_json::Value>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub input_drvs: BTreeMap<String, Vec<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub constituents: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub gc_root_error: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EvalError {
pub attr: String,
pub attr_path: Vec<String>,
pub error: String,
pub fatal: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Diff {
pub added: Vec<Derivation>,
pub removed: Vec<Derivation>,
pub errors: Vec<EvalError>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Filter {
pub systems: Option<Vec<String>>,
pub attr_prefix: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub attr_prefixes: Option<Vec<Vec<String>>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub attrs: Option<Vec<Vec<String>>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub names: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub drv_paths: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub include_patterns: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub exclude_patterns: Option<Vec<String>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Event {
Derivation(Derivation),
AttrSet {
attr: String,
attr_path: Vec<String>,
attrs: Vec<String>,
},
Error(EvalError),
}
impl Event {
pub fn attr(&self) -> &str {
match self {
Event::Derivation(d) => &d.attr,
Event::AttrSet { attr, .. } => attr,
Event::Error(e) => &e.attr,
}
}
pub fn attr_path(&self) -> &[String] {
match self {
Event::Derivation(d) => &d.attr_path,
Event::AttrSet { attr_path, .. } => attr_path,
Event::Error(e) => &e.attr_path,
}
}
}
pub fn run_worker() -> Result<()> {
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_io()
.build()
.map_err(|err| {
Error::Internal {
message: err.to_string(),
}
})?;
runtime.block_on(worker::run()).map_err(Error::from)
}
pub fn run_worker_if_requested() -> Result<bool> {
if env::var_os(WORKER_ENV).is_none() {
return Ok(false);
}
run_worker()?;
Ok(true)
}
pub async fn serve_remote_worker(addr: &str) -> Result<()> {
remote_worker::serve(addr).await.map_err(Error::from)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn config_constructors_set_input_and_defaults() {
let expr = Config::expr("{}");
let Input::Expr(value) = expr.input else {
panic!("expected expr input");
};
assert_eq!(value, "{}");
assert_eq!(expr.workers, Config::default().workers);
let file = Config::file("default.nix");
let Input::File(path) = file.input else {
panic!("expected file input");
};
assert_eq!(path, PathBuf::from("default.nix"));
let flake = Config::flake(".#checks");
let Input::Flake(reference) = flake.input else {
panic!("expected flake input");
};
assert_eq!(reference, ".#checks");
}
#[test]
fn config_builder_sets_library_options() {
let config = ConfigBuilder::flake(".#hydraJobs")
.workers(4)
.max_memory_size(1024)
.meta(true)
.show_input_drvs(true)
.force_recurse(true)
.gc_roots_dir("gcroots")
.auto_arg_expr("pkgs", "import <nixpkgs> {}")
.auto_arg_str("system", "x86_64-linux")
.override_input("nixpkgs", "github:NixOS/nixpkgs/nixos-unstable")
.nix_option("allow-import-from-derivation", "false")
.remote(Remote {
endpoint: "127.0.0.1:9000".into(),
systems: vec!["x86_64-linux".into()],
workers: 2,
})
.worker_exe("evix-worker")
.build();
assert_eq!(config.workers, 4);
assert_eq!(config.max_memory_size, 1024);
assert!(config.meta);
assert!(config.show_input_drvs);
assert!(config.force_recurse);
assert_eq!(config.gc_roots_dir, Some(PathBuf::from("gcroots")));
assert_eq!(config.auto_args.len(), 2);
assert_eq!(config.override_inputs.len(), 1);
assert_eq!(config.nix_options.len(), 1);
assert_eq!(config.remotes.len(), 1);
assert_eq!(config.worker_exe, Some(PathBuf::from("evix-worker")));
}
}