1use std::collections::{BTreeMap, HashMap};
42
43use serde::{Deserialize, Serialize};
44use sha2::{Digest, Sha256};
45use sui_compat::derivation::{Derivation, DerivationOutput};
46use tatara_lisp::DeriveTataraDomain;
47
48use crate::SpecError;
49
50#[derive(DeriveTataraDomain, Serialize, Deserialize, Debug, Clone)]
60#[tatara(keyword = "defderivation-algorithm")]
61pub struct DerivationAlgorithm {
62 pub name: String,
63 pub phases: Vec<Phase>,
64}
65
66#[derive(Serialize, Deserialize, Debug, Clone)]
71pub struct Phase {
72 pub kind: PhaseKind,
73 #[serde(default)]
74 pub bind: Option<String>,
75 #[serde(default)]
76 pub from: Option<String>,
77 #[serde(default, rename = "fromHash")]
78 pub from_hash: Option<String>,
79}
80
81#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
85pub enum PhaseKind {
86 MaskOutputsAndEnv,
91
92 Serialize,
95
96 Sha256,
99
100 ComputeOutputPaths,
104
105 FillPlaceholders,
109
110 ComputeDrvPath,
114
115 SerializeModulo,
125
126 CacheSelfModulo,
131
132 SeedFixedOutputHash,
145
146 MarkContentAddressed,
152
153 EmitCaPlaceholders,
157}
158
159pub struct DerivationState {
168 pub drv: Derivation,
169 pub outputs_list: Vec<String>,
170 pub drv_name: String,
171 pub binds: HashMap<String, Vec<u8>>,
172 pub out_paths: BTreeMap<String, String>,
173 pub drv_path: Option<String>,
174}
175
176impl DerivationState {
177 #[must_use]
178 pub fn new(drv: Derivation, outputs_list: Vec<String>, drv_name: String) -> Self {
179 Self {
180 drv,
181 outputs_list,
182 drv_name,
183 binds: HashMap::new(),
184 out_paths: BTreeMap::new(),
185 drv_path: None,
186 }
187 }
188
189 fn get_bytes(&self, key: &str) -> Result<&[u8], SpecError> {
190 self.binds
191 .get(key)
192 .map(std::vec::Vec::as_slice)
193 .ok_or_else(|| SpecError::UnboundSlot(key.to_string()))
194 }
195}
196
197pub fn apply(
213 algo: &DerivationAlgorithm,
214 drv: Derivation,
215 outputs_list: Vec<String>,
216 name: &str,
217) -> Result<(String, BTreeMap<String, String>, Derivation), SpecError> {
218 let mut state = DerivationState::new(drv, outputs_list, name.to_string());
219 for phase in &algo.phases {
220 run_phase(phase, &mut state)?;
221 }
222 let drv_path = state.drv_path.ok_or_else(|| SpecError::Interp {
223 phase: "finalize".into(),
224 message: "algorithm completed without binding a .drv path \
225 (missing ComputeDrvPath phase?)".into(),
226 })?;
227 Ok((drv_path, state.out_paths, state.drv))
228}
229
230fn run_phase(phase: &Phase, s: &mut DerivationState) -> Result<(), SpecError> {
231 match phase.kind {
232 PhaseKind::MaskOutputsAndEnv => mask_outputs_and_env(s),
233 PhaseKind::Serialize => serialize(phase, s),
234 PhaseKind::Sha256 => sha256(phase, s),
235 PhaseKind::ComputeOutputPaths => compute_output_paths(phase, s),
236 PhaseKind::FillPlaceholders => fill_placeholders(s),
237 PhaseKind::ComputeDrvPath => compute_drv_path(phase, s),
238 PhaseKind::SerializeModulo => serialize_modulo(phase, s),
239 PhaseKind::CacheSelfModulo => cache_self_modulo(phase, s),
240 PhaseKind::SeedFixedOutputHash => Err(SpecError::Interp {
241 phase: "SeedFixedOutputHash".into(),
242 message: "fixed-output derivation phase not yet implemented — \
243 M3 will wire to sui_compat::store_path::compute_fixed_output_path"
244 .into(),
245 }),
246 PhaseKind::MarkContentAddressed => Err(SpecError::Interp {
247 phase: "MarkContentAddressed".into(),
248 message: "content-addressed derivation phase not yet implemented — \
249 M4 work hangs off this border"
250 .into(),
251 }),
252 PhaseKind::EmitCaPlaceholders => Err(SpecError::Interp {
253 phase: "EmitCaPlaceholders".into(),
254 message: "CA placeholder emission not yet implemented (M4)".into(),
255 }),
256 }
257}
258
259use std::cell::RefCell;
274
275thread_local! {
276 static MODULO_CACHE: RefCell<HashMap<String, String>> = RefCell::new(HashMap::new());
277}
278
279pub fn remember_modulo_hash(drv_path: &str, modulo_hex: &str) {
282 MODULO_CACHE.with(|c| {
283 c.borrow_mut().insert(drv_path.to_string(), modulo_hex.to_string());
284 });
285}
286
287#[must_use]
292pub fn modulo_of(drv_path: &str) -> String {
293 MODULO_CACHE.with(|c| {
294 c.borrow().get(drv_path).cloned().unwrap_or_else(|| drv_path.to_string())
295 })
296}
297
298pub fn reset_modulo_cache() {
300 MODULO_CACHE.with(|c| c.borrow_mut().clear());
301}
302
303fn mask_outputs_and_env(s: &mut DerivationState) -> Result<(), SpecError> {
304 for o in &s.outputs_list {
305 s.drv.outputs.insert(o.clone(), DerivationOutput {
306 path: String::new(),
307 hash_algo: String::new(),
308 hash: String::new(),
309 });
310 s.drv.env.insert(o.clone(), String::new());
311 }
312 Ok(())
313}
314
315fn serialize(phase: &Phase, s: &mut DerivationState) -> Result<(), SpecError> {
316 let slot = phase.bind.clone().ok_or_else(|| SpecError::Interp {
317 phase: "Serialize".into(),
318 message: ":bind is required".into(),
319 })?;
320 let bytes = s.drv.serialize().into_bytes();
321 s.binds.insert(slot, bytes);
322 Ok(())
323}
324
325fn sha256(phase: &Phase, s: &mut DerivationState) -> Result<(), SpecError> {
326 let from = phase.from.clone().ok_or_else(|| SpecError::Interp {
327 phase: "Sha256".into(),
328 message: ":from is required".into(),
329 })?;
330 let bind = phase.bind.clone().ok_or_else(|| SpecError::Interp {
331 phase: "Sha256".into(),
332 message: ":bind is required".into(),
333 })?;
334 let input = s.get_bytes(&from)?;
335 let digest = Sha256::digest(input);
336 let hex: String = digest.iter().map(|b| format!("{b:02x}")).collect();
337 s.binds.insert(bind, hex.into_bytes());
338 Ok(())
339}
340
341fn compute_output_paths(phase: &Phase, s: &mut DerivationState) -> Result<(), SpecError> {
342 let from = phase.from_hash.clone().ok_or_else(|| SpecError::Interp {
343 phase: "ComputeOutputPaths".into(),
344 message: ":from-hash is required".into(),
345 })?;
346 let hex = {
347 let bytes = s.get_bytes(&from)?;
348 std::str::from_utf8(bytes).map_err(|e| SpecError::Interp {
349 phase: "ComputeOutputPaths".into(),
350 message: format!("slot {from} is not valid utf-8: {e}"),
351 })?.to_string()
352 };
353 let outputs_snapshot: Vec<String> = s.outputs_list.clone();
354 let drv_name = s.drv_name.clone();
355 for o in &outputs_snapshot {
356 let p = sui_compat::store_path::compute_output_path(&hex, o, &drv_name);
357 s.out_paths.insert(o.clone(), p);
358 }
359 Ok(())
360}
361
362fn fill_placeholders(s: &mut DerivationState) -> Result<(), SpecError> {
363 for o in &s.outputs_list {
364 let placeholder = s.out_paths.get(o).cloned().ok_or_else(|| SpecError::Interp {
365 phase: "FillPlaceholders".into(),
366 message: format!("no path computed for output {o} \
367 (did ComputeOutputPaths run first?)"),
368 })?;
369 if let Some(entry) = s.drv.outputs.get_mut(o) {
370 entry.path = placeholder.clone();
371 }
372 s.drv.env.insert(o.clone(), placeholder);
373 }
374 Ok(())
375}
376
377fn compute_drv_path(phase: &Phase, s: &mut DerivationState) -> Result<(), SpecError> {
378 let from = phase.from_hash.clone().ok_or_else(|| SpecError::Interp {
379 phase: "ComputeDrvPath".into(),
380 message: ":from-hash is required".into(),
381 })?;
382 {
386 let bytes = s.get_bytes(&from)?;
387 let _ = std::str::from_utf8(bytes).map_err(|e| SpecError::Interp {
388 phase: "ComputeDrvPath".into(),
389 message: format!("slot {from} is not valid utf-8: {e}"),
390 })?;
391 }
392 let bytes_slot = from.trim_end_matches("-hex").to_string();
401 let drv_name = s.drv_name.clone();
402 let refs: Vec<String> = s.drv.input_derivations
403 .keys()
404 .cloned()
405 .chain(s.drv.input_sources.iter().cloned())
406 .collect();
407 let drv_path = {
408 let bytes = s.get_bytes(&bytes_slot)?;
409 sui_compat::store_path::compute_drv_path_with_refs(bytes, &drv_name, &refs)
410 };
411 s.drv_path = Some(drv_path);
412 Ok(())
413}
414
415fn serialize_modulo(phase: &Phase, s: &mut DerivationState) -> Result<(), SpecError> {
416 let slot = phase.bind.clone().ok_or_else(|| SpecError::Interp {
417 phase: "SerializeModulo".into(),
418 message: ":bind is required".into(),
419 })?;
420 let bytes = s.drv.serialize_modulo(|drv_path| modulo_of(drv_path)).into_bytes();
421 s.binds.insert(slot, bytes);
422 Ok(())
423}
424
425fn cache_self_modulo(phase: &Phase, s: &mut DerivationState) -> Result<(), SpecError> {
426 let from = phase.from_hash.clone().ok_or_else(|| SpecError::Interp {
427 phase: "CacheSelfModulo".into(),
428 message: ":from-hash is required".into(),
429 })?;
430 let drv_path = s.drv_path.clone().ok_or_else(|| SpecError::Interp {
431 phase: "CacheSelfModulo".into(),
432 message: "no drv path bound yet (run ComputeDrvPath first)".into(),
433 })?;
434 let hex = {
435 let bytes = s.get_bytes(&from)?;
436 std::str::from_utf8(bytes).map_err(|e| SpecError::Interp {
437 phase: "CacheSelfModulo".into(),
438 message: format!("slot {from} is not valid utf-8: {e}"),
439 })?.to_string()
440 };
441 remember_modulo_hash(&drv_path, &hex);
442 Ok(())
443}
444
445pub const CPPNIX_INPUT_ADDRESSED_LISP: &str = include_str!("../specs/derivation.lisp");
453
454pub fn load_canonical() -> Result<DerivationAlgorithm, SpecError> {
463 load_named("cppnix-input-addressed")
464}
465
466pub fn load_all_canonical() -> Result<Vec<DerivationAlgorithm>, SpecError> {
472 Ok(tatara_lisp::compile_typed::<DerivationAlgorithm>(
473 CPPNIX_INPUT_ADDRESSED_LISP,
474 )?)
475}
476
477pub fn load_named(name: &str) -> Result<DerivationAlgorithm, SpecError> {
488 let all = load_all_canonical()?;
489 all.into_iter()
490 .find(|a| a.name == name)
491 .ok_or_else(|| SpecError::Load(
492 format!("no (defderivation-algorithm) with :name {name:?}"),
493 ))
494}
495
496#[cfg(test)]
497mod tests {
498 use super::*;
499
500 #[test]
501 fn canonical_spec_parses() {
502 let algo = load_canonical().expect("canonical spec must compile");
503 assert_eq!(algo.name, "cppnix-input-addressed");
504 assert!(!algo.phases.is_empty(), "algorithm must have phases");
507 }
508
509 #[test]
510 fn fixed_output_algorithm_parses() {
511 let algo = load_named("cppnix-fixed-output")
512 .expect("FOD algorithm must compile");
513 let kinds: Vec<PhaseKind> = algo.phases.iter().map(|p| p.kind).collect();
514 assert!(kinds.contains(&PhaseKind::SeedFixedOutputHash));
515 assert!(kinds.contains(&PhaseKind::ComputeDrvPath));
516 }
517
518 #[test]
519 fn content_addressed_algorithm_parses() {
520 let algo = load_named("cppnix-content-addressed")
521 .expect("CA-drv algorithm must compile");
522 let kinds: Vec<PhaseKind> = algo.phases.iter().map(|p| p.kind).collect();
523 assert!(kinds.contains(&PhaseKind::MarkContentAddressed));
524 assert!(kinds.contains(&PhaseKind::EmitCaPlaceholders));
525 }
526
527 #[test]
528 fn all_canonical_algorithms_load() {
529 let all = load_all_canonical().expect("all algos must compile");
530 let names: std::collections::HashSet<&str> =
531 all.iter().map(|a| a.name.as_str()).collect();
532 for required in [
533 "cppnix-input-addressed",
534 "cppnix-fixed-output",
535 "cppnix-content-addressed",
536 ] {
537 assert!(
538 names.contains(required),
539 "canonical corpus missing algorithm `{required}`",
540 );
541 }
542 }
543
544 #[test]
545 fn fod_apply_returns_typed_not_yet() {
546 let algo = load_named("cppnix-fixed-output").unwrap();
547 let mut env = std::collections::BTreeMap::new();
548 env.insert("outputHash".into(), "sha256-abc123".into());
549 let drv = Derivation {
550 outputs: std::collections::BTreeMap::new(),
551 input_derivations: std::collections::BTreeMap::new(),
552 input_sources: Vec::new(),
553 system: "aarch64-darwin".into(),
554 builder: "/bin/sh".into(),
555 args: vec![],
556 env,
557 };
558 let err = apply(&algo, drv, vec!["out".into()], "fixed-output-test")
559 .expect_err("FOD apply must surface typed not-yet");
560 match err {
561 SpecError::Interp { phase, message } => {
562 assert_eq!(phase, "SeedFixedOutputHash");
563 assert!(message.contains("M3"));
564 }
565 _ => panic!("expected SpecError::Interp, got {err:?}"),
566 }
567 }
568
569 #[test]
570 fn canonical_spec_matches_cppnix_on_hello_derivation() {
571 let algo = load_canonical().unwrap();
572 let mut env = std::collections::BTreeMap::new();
573 env.insert("builder".into(), "/bin/sh".into());
574 env.insert("name".into(), "hello".into());
575 env.insert("system".into(), "aarch64-darwin".into());
576 let drv = Derivation {
577 outputs: std::collections::BTreeMap::new(),
578 input_derivations: std::collections::BTreeMap::new(),
579 input_sources: Vec::new(),
580 system: "aarch64-darwin".into(),
581 builder: "/bin/sh".into(),
582 args: vec!["-c".into(), "echo hi > $out".into()],
583 env,
584 };
585 let (drv_path, out_paths, _final_drv) =
586 apply(&algo, drv, vec!["out".to_string()], "hello").unwrap();
587 assert_eq!(
590 drv_path,
591 "/nix/store/mypmkciickjnhjjimhzjn6w7qj7g8n2k-hello.drv"
592 );
593 assert_eq!(
594 out_paths.get("out").map(String::as_str),
595 Some("/nix/store/k6lq59b6dilrfy0blhkr10m27ga7ncwr-hello"),
596 );
597 }
598}