use std::collections::{BTreeMap, HashMap};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use sui_compat::derivation::{Derivation, DerivationOutput};
use tatara_lisp::DeriveTataraDomain;
use crate::SpecError;
#[derive(DeriveTataraDomain, Serialize, Deserialize, Debug, Clone)]
#[tatara(keyword = "defderivation-algorithm")]
pub struct DerivationAlgorithm {
pub name: String,
pub phases: Vec<Phase>,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Phase {
pub kind: PhaseKind,
#[serde(default)]
pub bind: Option<String>,
#[serde(default)]
pub from: Option<String>,
#[serde(default, rename = "fromHash")]
pub from_hash: Option<String>,
}
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
pub enum PhaseKind {
MaskOutputsAndEnv,
Serialize,
Sha256,
ComputeOutputPaths,
FillPlaceholders,
ComputeDrvPath,
SerializeModulo,
CacheSelfModulo,
SeedFixedOutputHash,
MarkContentAddressed,
EmitCaPlaceholders,
}
pub struct DerivationState {
pub drv: Derivation,
pub outputs_list: Vec<String>,
pub drv_name: String,
pub binds: HashMap<String, Vec<u8>>,
pub out_paths: BTreeMap<String, String>,
pub drv_path: Option<String>,
}
impl DerivationState {
#[must_use]
pub fn new(drv: Derivation, outputs_list: Vec<String>, drv_name: String) -> Self {
Self {
drv,
outputs_list,
drv_name,
binds: HashMap::new(),
out_paths: BTreeMap::new(),
drv_path: None,
}
}
fn get_bytes(&self, key: &str) -> Result<&[u8], SpecError> {
self.binds
.get(key)
.map(std::vec::Vec::as_slice)
.ok_or_else(|| SpecError::UnboundSlot(key.to_string()))
}
}
pub fn apply(
algo: &DerivationAlgorithm,
drv: Derivation,
outputs_list: Vec<String>,
name: &str,
) -> Result<(String, BTreeMap<String, String>, Derivation), SpecError> {
let mut state = DerivationState::new(drv, outputs_list, name.to_string());
for phase in &algo.phases {
run_phase(phase, &mut state)?;
}
let drv_path = state.drv_path.ok_or_else(|| SpecError::Interp {
phase: "finalize".into(),
message: "algorithm completed without binding a .drv path \
(missing ComputeDrvPath phase?)".into(),
})?;
Ok((drv_path, state.out_paths, state.drv))
}
fn run_phase(phase: &Phase, s: &mut DerivationState) -> Result<(), SpecError> {
match phase.kind {
PhaseKind::MaskOutputsAndEnv => mask_outputs_and_env(s),
PhaseKind::Serialize => serialize(phase, s),
PhaseKind::Sha256 => sha256(phase, s),
PhaseKind::ComputeOutputPaths => compute_output_paths(phase, s),
PhaseKind::FillPlaceholders => fill_placeholders(s),
PhaseKind::ComputeDrvPath => compute_drv_path(phase, s),
PhaseKind::SerializeModulo => serialize_modulo(phase, s),
PhaseKind::CacheSelfModulo => cache_self_modulo(phase, s),
PhaseKind::SeedFixedOutputHash => Err(SpecError::Interp {
phase: "SeedFixedOutputHash".into(),
message: "fixed-output derivation phase not yet implemented — \
M3 will wire to sui_compat::store_path::compute_fixed_output_path"
.into(),
}),
PhaseKind::MarkContentAddressed => Err(SpecError::Interp {
phase: "MarkContentAddressed".into(),
message: "content-addressed derivation phase not yet implemented — \
M4 work hangs off this border"
.into(),
}),
PhaseKind::EmitCaPlaceholders => Err(SpecError::Interp {
phase: "EmitCaPlaceholders".into(),
message: "CA placeholder emission not yet implemented (M4)".into(),
}),
}
}
use std::cell::RefCell;
thread_local! {
static MODULO_CACHE: RefCell<HashMap<String, String>> = RefCell::new(HashMap::new());
}
pub fn remember_modulo_hash(drv_path: &str, modulo_hex: &str) {
MODULO_CACHE.with(|c| {
c.borrow_mut().insert(drv_path.to_string(), modulo_hex.to_string());
});
}
#[must_use]
pub fn modulo_of(drv_path: &str) -> String {
MODULO_CACHE.with(|c| {
c.borrow().get(drv_path).cloned().unwrap_or_else(|| drv_path.to_string())
})
}
pub fn reset_modulo_cache() {
MODULO_CACHE.with(|c| c.borrow_mut().clear());
}
fn mask_outputs_and_env(s: &mut DerivationState) -> Result<(), SpecError> {
for o in &s.outputs_list {
s.drv.outputs.insert(o.clone(), DerivationOutput {
path: String::new(),
hash_algo: String::new(),
hash: String::new(),
});
s.drv.env.insert(o.clone(), String::new());
}
Ok(())
}
fn serialize(phase: &Phase, s: &mut DerivationState) -> Result<(), SpecError> {
let slot = phase.bind.clone().ok_or_else(|| SpecError::Interp {
phase: "Serialize".into(),
message: ":bind is required".into(),
})?;
let bytes = s.drv.serialize().into_bytes();
s.binds.insert(slot, bytes);
Ok(())
}
fn sha256(phase: &Phase, s: &mut DerivationState) -> Result<(), SpecError> {
let from = phase.from.clone().ok_or_else(|| SpecError::Interp {
phase: "Sha256".into(),
message: ":from is required".into(),
})?;
let bind = phase.bind.clone().ok_or_else(|| SpecError::Interp {
phase: "Sha256".into(),
message: ":bind is required".into(),
})?;
let input = s.get_bytes(&from)?;
let digest = Sha256::digest(input);
let hex: String = digest.iter().map(|b| format!("{b:02x}")).collect();
s.binds.insert(bind, hex.into_bytes());
Ok(())
}
fn compute_output_paths(phase: &Phase, s: &mut DerivationState) -> Result<(), SpecError> {
let from = phase.from_hash.clone().ok_or_else(|| SpecError::Interp {
phase: "ComputeOutputPaths".into(),
message: ":from-hash is required".into(),
})?;
let hex = {
let bytes = s.get_bytes(&from)?;
std::str::from_utf8(bytes).map_err(|e| SpecError::Interp {
phase: "ComputeOutputPaths".into(),
message: format!("slot {from} is not valid utf-8: {e}"),
})?.to_string()
};
let outputs_snapshot: Vec<String> = s.outputs_list.clone();
let drv_name = s.drv_name.clone();
for o in &outputs_snapshot {
let p = sui_compat::store_path::compute_output_path(&hex, o, &drv_name);
s.out_paths.insert(o.clone(), p);
}
Ok(())
}
fn fill_placeholders(s: &mut DerivationState) -> Result<(), SpecError> {
for o in &s.outputs_list {
let placeholder = s.out_paths.get(o).cloned().ok_or_else(|| SpecError::Interp {
phase: "FillPlaceholders".into(),
message: format!("no path computed for output {o} \
(did ComputeOutputPaths run first?)"),
})?;
if let Some(entry) = s.drv.outputs.get_mut(o) {
entry.path = placeholder.clone();
}
s.drv.env.insert(o.clone(), placeholder);
}
Ok(())
}
fn compute_drv_path(phase: &Phase, s: &mut DerivationState) -> Result<(), SpecError> {
let from = phase.from_hash.clone().ok_or_else(|| SpecError::Interp {
phase: "ComputeDrvPath".into(),
message: ":from-hash is required".into(),
})?;
{
let bytes = s.get_bytes(&from)?;
let _ = std::str::from_utf8(bytes).map_err(|e| SpecError::Interp {
phase: "ComputeDrvPath".into(),
message: format!("slot {from} is not valid utf-8: {e}"),
})?;
}
let bytes_slot = from.trim_end_matches("-hex").to_string();
let drv_name = s.drv_name.clone();
let refs: Vec<String> = s.drv.input_derivations
.keys()
.cloned()
.chain(s.drv.input_sources.iter().cloned())
.collect();
let drv_path = {
let bytes = s.get_bytes(&bytes_slot)?;
sui_compat::store_path::compute_drv_path_with_refs(bytes, &drv_name, &refs)
};
s.drv_path = Some(drv_path);
Ok(())
}
fn serialize_modulo(phase: &Phase, s: &mut DerivationState) -> Result<(), SpecError> {
let slot = phase.bind.clone().ok_or_else(|| SpecError::Interp {
phase: "SerializeModulo".into(),
message: ":bind is required".into(),
})?;
let bytes = s.drv.serialize_modulo(|drv_path| modulo_of(drv_path)).into_bytes();
s.binds.insert(slot, bytes);
Ok(())
}
fn cache_self_modulo(phase: &Phase, s: &mut DerivationState) -> Result<(), SpecError> {
let from = phase.from_hash.clone().ok_or_else(|| SpecError::Interp {
phase: "CacheSelfModulo".into(),
message: ":from-hash is required".into(),
})?;
let drv_path = s.drv_path.clone().ok_or_else(|| SpecError::Interp {
phase: "CacheSelfModulo".into(),
message: "no drv path bound yet (run ComputeDrvPath first)".into(),
})?;
let hex = {
let bytes = s.get_bytes(&from)?;
std::str::from_utf8(bytes).map_err(|e| SpecError::Interp {
phase: "CacheSelfModulo".into(),
message: format!("slot {from} is not valid utf-8: {e}"),
})?.to_string()
};
remember_modulo_hash(&drv_path, &hex);
Ok(())
}
pub const CPPNIX_INPUT_ADDRESSED_LISP: &str = include_str!("../specs/derivation.lisp");
pub fn load_canonical() -> Result<DerivationAlgorithm, SpecError> {
load_named("cppnix-input-addressed")
}
pub fn load_all_canonical() -> Result<Vec<DerivationAlgorithm>, SpecError> {
Ok(tatara_lisp::compile_typed::<DerivationAlgorithm>(
CPPNIX_INPUT_ADDRESSED_LISP,
)?)
}
pub fn load_named(name: &str) -> Result<DerivationAlgorithm, SpecError> {
let all = load_all_canonical()?;
all.into_iter()
.find(|a| a.name == name)
.ok_or_else(|| SpecError::Load(
format!("no (defderivation-algorithm) with :name {name:?}"),
))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn canonical_spec_parses() {
let algo = load_canonical().expect("canonical spec must compile");
assert_eq!(algo.name, "cppnix-input-addressed");
assert!(!algo.phases.is_empty(), "algorithm must have phases");
}
#[test]
fn fixed_output_algorithm_parses() {
let algo = load_named("cppnix-fixed-output")
.expect("FOD algorithm must compile");
let kinds: Vec<PhaseKind> = algo.phases.iter().map(|p| p.kind).collect();
assert!(kinds.contains(&PhaseKind::SeedFixedOutputHash));
assert!(kinds.contains(&PhaseKind::ComputeDrvPath));
}
#[test]
fn content_addressed_algorithm_parses() {
let algo = load_named("cppnix-content-addressed")
.expect("CA-drv algorithm must compile");
let kinds: Vec<PhaseKind> = algo.phases.iter().map(|p| p.kind).collect();
assert!(kinds.contains(&PhaseKind::MarkContentAddressed));
assert!(kinds.contains(&PhaseKind::EmitCaPlaceholders));
}
#[test]
fn all_canonical_algorithms_load() {
let all = load_all_canonical().expect("all algos must compile");
let names: std::collections::HashSet<&str> =
all.iter().map(|a| a.name.as_str()).collect();
for required in [
"cppnix-input-addressed",
"cppnix-fixed-output",
"cppnix-content-addressed",
] {
assert!(
names.contains(required),
"canonical corpus missing algorithm `{required}`",
);
}
}
#[test]
fn fod_apply_returns_typed_not_yet() {
let algo = load_named("cppnix-fixed-output").unwrap();
let mut env = std::collections::BTreeMap::new();
env.insert("outputHash".into(), "sha256-abc123".into());
let drv = Derivation {
outputs: std::collections::BTreeMap::new(),
input_derivations: std::collections::BTreeMap::new(),
input_sources: Vec::new(),
system: "aarch64-darwin".into(),
builder: "/bin/sh".into(),
args: vec![],
env,
};
let err = apply(&algo, drv, vec!["out".into()], "fixed-output-test")
.expect_err("FOD apply must surface typed not-yet");
match err {
SpecError::Interp { phase, message } => {
assert_eq!(phase, "SeedFixedOutputHash");
assert!(message.contains("M3"));
}
_ => panic!("expected SpecError::Interp, got {err:?}"),
}
}
#[test]
fn canonical_spec_matches_cppnix_on_hello_derivation() {
let algo = load_canonical().unwrap();
let mut env = std::collections::BTreeMap::new();
env.insert("builder".into(), "/bin/sh".into());
env.insert("name".into(), "hello".into());
env.insert("system".into(), "aarch64-darwin".into());
let drv = Derivation {
outputs: std::collections::BTreeMap::new(),
input_derivations: std::collections::BTreeMap::new(),
input_sources: Vec::new(),
system: "aarch64-darwin".into(),
builder: "/bin/sh".into(),
args: vec!["-c".into(), "echo hi > $out".into()],
env,
};
let (drv_path, out_paths, _final_drv) =
apply(&algo, drv, vec!["out".to_string()], "hello").unwrap();
assert_eq!(
drv_path,
"/nix/store/mypmkciickjnhjjimhzjn6w7qj7g8n2k-hello.drv"
);
assert_eq!(
out_paths.get("out").map(String::as_str),
Some("/nix/store/k6lq59b6dilrfy0blhkr10m27ga7ncwr-hello"),
);
}
}