use std::collections::BTreeMap;
use std::rc::Rc;
use crate::eval_ir::{coerce_to_string_ctx, IrAttrs, IrContextElem, IrEvalError, IrValue};
use sui_compat::derivation::Derivation;
pub(crate) fn build_derivation(arg: &IrValue) -> Result<IrValue, IrEvalError> {
let forced = arg.force()?;
let input = match &forced {
IrValue::Attrs(a) => a.clone(),
other => {
return Err(IrEvalError::TypeMismatch {
expected: "set",
got: other.type_name(),
})
}
};
let name = force_attr_string(&input, "name")?;
let (drv_path, out_paths) = compute_full_drv(&input, &name)?;
build_derivation_result(&input, &name, &drv_path, &out_paths)
}
fn force_attr_string(input: &IrAttrs, key: &str) -> Result<String, IrEvalError> {
let v = input
.get(key)
.ok_or_else(|| IrEvalError::AttrNotFound(key.to_string()))?;
let forced = v.force()?;
let (s, _ctx) = coerce_to_string_ctx(&forced, true)?;
Ok(s)
}
fn optional_attr_string(input: &IrAttrs, key: &str) -> Result<Option<String>, IrEvalError> {
match input.get(key) {
None => Ok(None),
Some(v) => {
let forced = v.force()?;
let (s, _ctx) = coerce_to_string_ctx(&forced, true)?;
Ok(Some(s))
}
}
}
fn compute_full_drv(
input: &IrAttrs,
name: &str,
) -> Result<(String, BTreeMap<String, String>), IrEvalError> {
let drv = construct_derivation(input, name)?;
compute_derivation_outputs(input, name, drv)
}
fn construct_derivation(input: &IrAttrs, name: &str) -> Result<Derivation, IrEvalError> {
use crate::eval_ir::IrStringContext;
let system = force_attr_string(input, "system")?;
let mut collected_ctx = IrStringContext::new();
let builder = {
let v = input.get("builder").ok_or_else(|| {
IrEvalError::TypeError("derivation: missing required attribute 'builder'".into())
})?;
let forced = v.force()?;
let (s, ctx) = coerce_to_string_ctx(&forced, true)?;
collected_ctx.merge(&ctx);
s
};
let args_list: Vec<String> = if let Some(a) = input.get("args") {
let forced_args = a.force()?;
let list = as_list(&forced_args)?;
let mut out = Vec::with_capacity(list.len());
for item in list.iter() {
let forced = item.force()?;
let (s, ctx) = coerce_to_string_ctx(&forced, true)?;
collected_ctx.merge(&ctx);
out.push(s);
}
out
} else {
Vec::new()
};
let ignore_nulls = input
.get("__ignoreNulls")
.and_then(|v| v.force().ok())
.is_some_and(|v| matches!(v, IrValue::Bool(true)));
let structured_attrs = input
.get("__structuredAttrs")
.and_then(|v| v.force().ok())
.is_some_and(|v| matches!(v, IrValue::Bool(true)));
let mut env_vars: BTreeMap<String, String> = BTreeMap::new();
if structured_attrs {
let mut obj = serde_json::Map::new();
for (k, v) in input.iter() {
if matches!(
k.as_str(),
"args" | "__structuredAttrs" | "__ignoreNulls" | "__impure"
| "__contentAddressed"
) {
continue;
}
let forced_v = match v.force() {
Ok(fv) => fv,
Err(_) => continue,
};
if ignore_nulls && matches!(forced_v, IrValue::Null) {
continue;
}
match ir_to_json_ctx(&forced_v, &mut collected_ctx) {
Ok(jv) => {
obj.insert(k.clone(), jv);
}
Err(
e @ (IrEvalError::Throw(_)
| IrEvalError::Abort(_)
| IrEvalError::AssertionFailed
| IrEvalError::Unsupported(_)
| IrEvalError::MissingBuiltin(_)),
) => return Err(e),
Err(_) => {}
}
}
let json =
serde_json::to_string(&serde_json::Value::Object(obj)).unwrap_or_default();
env_vars.insert("__json".to_string(), json);
} else {
for (k, v) in input.iter() {
if matches!(
k.as_str(),
"name" | "system" | "builder" | "args" | "__ignoreNulls" | "__impure"
| "__contentAddressed"
) {
continue;
}
let forced_v = match v.force() {
Ok(fv) => fv,
Err(_) => continue,
};
if ignore_nulls && matches!(forced_v, IrValue::Null) {
continue;
}
match coerce_to_string_ctx(&forced_v, true) {
Ok((s, ctx)) => {
collected_ctx.merge(&ctx);
env_vars.insert(k.clone(), s);
}
Err(
e @ (IrEvalError::Throw(_)
| IrEvalError::Abort(_)
| IrEvalError::AssertionFailed
| IrEvalError::Unsupported(_)
| IrEvalError::MissingBuiltin(_)),
) => return Err(e),
Err(_) => {}
}
}
env_vars.insert("name".to_string(), name.to_string());
env_vars.insert("system".to_string(), system.clone());
env_vars.insert("builder".to_string(), builder.clone());
}
let mut input_derivations: BTreeMap<String, Vec<String>> = BTreeMap::new();
let mut input_sources: Vec<String> = Vec::new();
for elem in collected_ctx.iter() {
match elem {
IrContextElem::Output { drv, output } => {
input_derivations
.entry(drv.clone())
.or_default()
.push(output.clone());
}
IrContextElem::Plain(p) => {
if p.starts_with("/nix/store/") && !input_sources.contains(p) {
input_sources.push(p.clone());
}
}
IrContextElem::DrvDeep(_) => {} }
}
for outs in input_derivations.values_mut() {
outs.sort();
outs.dedup();
}
Ok(Derivation {
outputs: BTreeMap::new(),
input_derivations,
input_sources,
system,
builder,
args: args_list,
env: env_vars,
})
}
fn compute_derivation_outputs(
input: &IrAttrs,
name: &str,
mut drv: Derivation,
) -> Result<(String, BTreeMap<String, String>), IrEvalError> {
if input.contains_key("outputHash") {
use sui_compat::derivation::DerivationOutput;
let raw_output_hash = force_attr_string(input, "outputHash")?;
let raw_algo = optional_attr_string(input, "outputHashAlgo")?.unwrap_or_default();
let output_hash_mode =
optional_attr_string(input, "outputHashMode")?.unwrap_or_else(|| "flat".to_string());
let is_recursive = output_hash_mode == "recursive" || output_hash_mode == "nar";
let output_hash_algo = if raw_algo.is_empty() {
infer_algo_from_hash(&raw_output_hash).unwrap_or_else(|| "sha256".to_string())
} else {
raw_algo
};
let algo = sui_compat::hash::HashAlgorithm::from_nix_str(&output_hash_algo).map_err(|e| {
IrEvalError::TypeError(format!(
"derivation: invalid outputHashAlgo {output_hash_algo:?}: {e}"
))
})?;
let parsed =
sui_compat::hash::NixHash::parse_any(algo, &raw_output_hash).map_err(|e| {
IrEvalError::TypeError(format!(
"derivation: invalid outputHash {raw_output_hash:?}: {e}"
))
})?;
let output_hash_hex = parsed.to_hex();
let out_path = sui_compat::store_path::compute_fixed_output_hash(
&output_hash_algo,
&output_hash_hex,
is_recursive,
name,
);
drv.outputs.insert(
"out".to_string(),
DerivationOutput {
path: out_path.clone(),
hash_algo: if is_recursive {
format!("r:{output_hash_algo}")
} else {
output_hash_algo.clone()
},
hash: output_hash_hex.clone(),
},
);
drv.env.insert("out".to_string(), out_path.clone());
let drv_content = drv.serialize();
let drv_refs: Vec<String> = drv
.input_derivations
.keys()
.cloned()
.chain(drv.input_sources.iter().cloned())
.collect();
let drv_path = sui_compat::store_path::compute_drv_path_with_refs(
drv_content.as_bytes(),
name,
&drv_refs,
);
let method_algo = if is_recursive {
format!("r:{output_hash_algo}")
} else {
output_hash_algo.clone()
};
let modulo_preimage = format!("fixed:out:{method_algo}:{output_hash_hex}:{out_path}");
let modulo_hex: String = {
use sha2::{Digest, Sha256};
Sha256::digest(modulo_preimage.as_bytes())
.iter()
.map(|b| format!("{b:02x}"))
.collect()
};
sui_spec::derivation::remember_modulo_hash(&drv_path, &modulo_hex);
dump_drv_if_requested(name, &drv_path, &drv);
let mut out_paths = BTreeMap::new();
out_paths.insert("out".to_string(), out_path);
return Ok((drv_path, out_paths));
}
let outputs = parse_outputs_list(input)?;
let algo = sui_spec::derivation::load_canonical().map_err(|e| {
IrEvalError::TypeError(format!("derivation algorithm spec failed to load: {e}"))
})?;
let (drv_path, out_paths, drv_final) = sui_spec::derivation::apply(&algo, drv, outputs, name)
.map_err(|e| {
IrEvalError::TypeError(format!("derivation algorithm interpreter failed: {e}"))
})?;
dump_drv_if_requested(name, &drv_path, &drv_final);
Ok((drv_path, out_paths))
}
fn dump_drv_if_requested(name: &str, drv_path: &str, drv: &Derivation) {
use std::sync::OnceLock;
static FILTER: OnceLock<Option<String>> = OnceLock::new();
static ATERM: OnceLock<bool> = OnceLock::new();
let Some(filter) = FILTER.get_or_init(|| std::env::var("SUI_IR_DUMP_DRV").ok()) else {
return;
};
let matches = filter == "ALL" || filter.split(',').any(|f| !f.is_empty() && name.contains(f));
if !matches {
return;
}
eprintln!("[[DRV]] name={name} path={drv_path}");
if *ATERM.get_or_init(|| std::env::var_os("SUI_IR_DUMP_ATERM").is_some()) {
eprintln!("[[ATERM]] {}", drv.serialize());
}
}
fn ir_to_json_ctx(
v: &IrValue,
ctx: &mut crate::eval_ir::IrStringContext,
) -> Result<serde_json::Value, IrEvalError> {
let forced = v.force()?;
Ok(match &forced {
IrValue::Null => serde_json::Value::Null,
IrValue::Bool(b) => serde_json::Value::Bool(*b),
IrValue::Int(n) => serde_json::json!(*n),
IrValue::Float(f) => serde_json::json!(*f),
IrValue::Str(s, c) => {
if let Some(c) = c {
ctx.merge(c);
}
serde_json::Value::String((**s).clone())
}
IrValue::Path(_) => {
let (s, c) = coerce_to_string_ctx(&forced, true)?;
ctx.merge(&c);
serde_json::Value::String(s)
}
IrValue::List(items) => {
let mut arr = Vec::with_capacity(items.len());
for item in items.iter() {
arr.push(ir_to_json_ctx(item, ctx)?);
}
serde_json::Value::Array(arr)
}
IrValue::Attrs(attrs) => {
if attrs.contains_key("__toString") || attrs.contains_key("outPath") {
let (s, c) = coerce_to_string_ctx(&forced, true)?;
ctx.merge(&c);
return Ok(serde_json::Value::String(s));
}
let mut map = serde_json::Map::new();
for (k, val) in attrs.iter() {
map.insert(k.clone(), ir_to_json_ctx(val, ctx)?);
}
serde_json::Value::Object(map)
}
IrValue::Lambda(_) | IrValue::Builtin(..) => {
return Err(IrEvalError::TypeError(format!(
"cannot serialize {} to JSON (__structuredAttrs)",
forced.type_name()
)));
}
IrValue::Thunk(_) => unreachable!("force() returned a thunk"),
})
}
fn infer_algo_from_hash(hash: &str) -> Option<String> {
for algo in ["sha256", "sha512", "sha1", "md5"] {
if hash.starts_with(&format!("{algo}-")) {
return Some(algo.to_string());
}
}
None
}
fn parse_outputs_list(input: &IrAttrs) -> Result<Vec<String>, IrEvalError> {
if let Some(o) = input.get("outputs") {
let forced_o = o.force()?;
let list = as_list(&forced_o)?;
let mut out = Vec::with_capacity(list.len());
for item in list.iter() {
let forced = item.force()?;
let s = match &forced {
IrValue::Str(s, _) => (**s).clone(),
other => {
return Err(IrEvalError::TypeMismatch {
expected: "string",
got: other.type_name(),
})
}
};
out.push(s);
}
if out.is_empty() {
return Err(IrEvalError::TypeError(
"derivation: outputs list must not be empty".into(),
));
}
Ok(out)
} else {
Ok(vec!["out".to_string()])
}
}
fn build_derivation_result(
input: &IrAttrs,
name: &str,
drv_path: &str,
out_paths: &BTreeMap<String, String>,
) -> Result<IrValue, IrEvalError> {
use crate::eval_ir::IrStringContext;
let output_names = parse_outputs_list(input)?;
let primary = output_names
.first()
.cloned()
.unwrap_or_else(|| "out".to_string());
let drv_path_val = {
let mut ctx = IrStringContext::new();
ctx.add_drv_deep(drv_path.to_string());
IrValue::string_with_context(drv_path.to_string(), ctx)
};
let out_path_val = {
let p = out_paths.get(&primary).cloned().unwrap_or_default();
let mut ctx = IrStringContext::new();
ctx.add_output(drv_path.to_string(), primary.clone());
IrValue::string_with_context(p, ctx)
};
let mut result = (*input).clone();
result.insert("type".to_string(), IrValue::string("derivation"));
result.insert("drvPath".to_string(), drv_path_val.clone());
result.insert("drvAttrs".to_string(), IrValue::Attrs(Rc::new((*input).clone())));
result.insert("outPath".to_string(), out_path_val);
result.insert("outputName".to_string(), IrValue::string(primary.clone()));
let mut all_outputs: Vec<IrValue> = Vec::with_capacity(output_names.len());
for output_name in &output_names {
let mut out_attrs = IrAttrs::new();
let p = out_paths.get(output_name).cloned().unwrap_or_default();
let mut ctx = IrStringContext::new();
ctx.add_output(drv_path.to_string(), output_name.clone());
out_attrs.insert(
"outPath".to_string(),
IrValue::string_with_context(p, ctx),
);
out_attrs.insert("drvPath".to_string(), drv_path_val.clone());
out_attrs.insert("type".to_string(), IrValue::string("derivation"));
out_attrs.insert("outputName".to_string(), IrValue::string(output_name.clone()));
out_attrs.insert("name".to_string(), IrValue::string(name));
let out_val = IrValue::Attrs(Rc::new(out_attrs));
all_outputs.push(out_val.clone());
result.insert(output_name.clone(), out_val);
}
result.insert("all".to_string(), IrValue::List(Rc::new(all_outputs)));
Ok(IrValue::Attrs(Rc::new(result)))
}
fn as_list(v: &IrValue) -> Result<Rc<Vec<IrValue>>, IrEvalError> {
match v {
IrValue::List(items) => Ok(items.clone()),
other => Err(IrEvalError::TypeMismatch {
expected: "list",
got: other.type_name(),
}),
}
}