use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use tatara_lisp_derive::TataraDomain as DeriveTataraDomain;
use crate::store::{StoreHash, StorePath};
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct InputRef {
pub name: String,
#[serde(default)]
pub version: Option<String>,
#[serde(default)]
pub pinned: Option<StorePath>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind")]
pub enum Source {
Inline { content: String },
Path { path: String },
Git {
url: String,
rev: String,
#[serde(default)]
submodules: bool,
},
Tarball { url: String, hash: String },
Derivation { input: InputRef },
}
impl Default for Source {
fn default() -> Self {
Self::Inline {
content: String::new(),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum BuilderPhase {
Unpack,
Patch,
Configure,
Build,
Check,
Install,
Fixup,
InstallCheck,
Dist,
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BuilderPhases {
#[serde(default)]
pub phases: Vec<BuilderPhase>,
#[serde(default)]
pub commands: BTreeMap<String, Vec<String>>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Outputs {
#[serde(default = "default_primary")]
pub primary: String,
#[serde(default)]
pub extra: Vec<String>,
}
impl Default for Outputs {
fn default() -> Self {
Self {
primary: default_primary(),
extra: Vec::new(),
}
}
}
fn default_primary() -> String {
"out".to_string()
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EnvVar {
pub name: String,
pub value: String,
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BridgeTarget {
pub attr_path: String,
#[serde(default)]
pub pkg_set: Option<String>,
}
impl BridgeTarget {
pub fn nixpkgs(attr_path: impl Into<String>) -> Self {
Self {
attr_path: attr_path.into(),
pkg_set: None,
}
}
pub fn resolved_pkg_set(&self) -> &str {
self.pkg_set.as_deref().unwrap_or("import <nixpkgs> {}")
}
}
#[derive(DeriveTataraDomain, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[tatara(keyword = "defderivation")]
pub struct Derivation {
pub name: String,
#[serde(default)]
pub version: Option<String>,
#[serde(default)]
pub inputs: Vec<InputRef>,
#[serde(default)]
pub source: Source,
#[serde(default)]
pub builder: BuilderPhases,
#[serde(default)]
pub outputs: Outputs,
#[serde(default)]
pub env: Vec<EnvVar>,
#[serde(default)]
pub sandbox: Sandbox,
#[serde(default)]
pub bridge: Option<BridgeTarget>,
#[serde(default)]
pub nix_expr: Option<String>,
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Sandbox {
#[serde(default)]
pub allow_network: bool,
#[serde(default)]
pub extra_paths: Vec<String>,
#[serde(default)]
pub impure_env: Vec<String>,
}
impl Derivation {
pub fn store_path(&self) -> StorePath {
StorePath::new(StoreHash::of(self), self.name.clone(), self.version.clone())
}
}
#[cfg(test)]
mod tests {
use super::*;
use tatara_lisp::{domain::TataraDomain, read};
#[test]
fn derivation_store_path_is_deterministic() {
let d1 = Derivation {
name: "hello".into(),
version: Some("2.12.1".into()),
..default_derivation()
};
let d2 = Derivation {
name: "hello".into(),
version: Some("2.12.1".into()),
..default_derivation()
};
assert_eq!(d1.store_path(), d2.store_path());
}
#[test]
fn derivation_store_path_varies_with_version() {
let d1 = Derivation {
name: "hello".into(),
version: Some("2.12.1".into()),
..default_derivation()
};
let d2 = Derivation {
name: "hello".into(),
version: Some("2.12.2".into()),
..default_derivation()
};
assert_ne!(d1.store_path(), d2.store_path());
}
#[test]
fn minimal_derivation_compiles_from_lisp() {
let forms = read(
r#"(defderivation
:name "hello"
:version "2.12.1")"#,
)
.unwrap();
let d = Derivation::compile_from_sexp(&forms[0]).unwrap();
assert_eq!(d.name, "hello");
assert_eq!(d.version.as_deref(), Some("2.12.1"));
assert_eq!(d.outputs.primary, "out");
}
#[test]
fn full_derivation_compiles_from_lisp() {
let forms = read(
r#"(defderivation
:name "hello"
:version "2.12.1"
:inputs ((:name "gcc" :version "^13")
(:name "glibc"))
:source (:kind Git
:url "https://github.com/gnu/hello.git"
:rev "v2.12.1")
:builder (:phases (Unpack Configure Build Install))
:outputs (:primary "out" :extra ("doc"))
:env ((:name "CFLAGS" :value "-O2")))"#,
)
.unwrap();
let d = Derivation::compile_from_sexp(&forms[0]).unwrap();
assert_eq!(d.inputs.len(), 2);
assert!(matches!(&d.source, Source::Git { .. }));
assert_eq!(d.builder.phases.len(), 4);
assert_eq!(d.outputs.extra, vec!["doc".to_string()]);
assert_eq!(d.env[0].name, "CFLAGS");
}
fn default_derivation() -> Derivation {
Derivation {
name: String::new(),
version: None,
inputs: vec![],
source: Source::default(),
builder: BuilderPhases::default(),
outputs: Outputs::default(),
env: vec![],
sandbox: Sandbox::default(),
bridge: None,
nix_expr: None,
}
}
#[test]
fn bridge_defaults_to_nixpkgs() {
let b = BridgeTarget::nixpkgs("hello");
assert_eq!(b.attr_path, "hello");
assert_eq!(b.resolved_pkg_set(), "import <nixpkgs> {}");
}
#[test]
fn bridge_respects_custom_pkg_set() {
let b = BridgeTarget {
attr_path: "myPkg".into(),
pkg_set: Some("import ./flake/release.nix { system = \"x86_64-linux\"; }".into()),
};
assert_eq!(
b.resolved_pkg_set(),
"import ./flake/release.nix { system = \"x86_64-linux\"; }"
);
}
}