use super::*;
pub(crate) fn register(builtins: &mut NixAttrs) {
register_builtin(builtins, "derivation", |args| {
build_derivation(&args[0])
});
register_builtin(builtins, "derivationStrict", |args| {
build_derivation(&args[0])
});
}
pub mod parity_strict {
use std::cell::RefCell;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum DropSite {
FlatEnv,
StructuredAttrs,
}
#[derive(Clone, Debug)]
pub struct DroppedDep {
pub drv: String,
pub attr: String,
pub site: DropSite,
pub force_err: String,
}
thread_local! {
static LEDGER: RefCell<Vec<DroppedDep>> = const { RefCell::new(Vec::new()) };
}
#[inline]
pub fn enabled() -> bool {
std::env::var_os("SUI_PARITY_STRICT").is_some()
}
pub fn record(drv: &str, attr: &str, site: DropSite, force_err: &str) {
if !enabled() {
return;
}
LEDGER.with(|l| {
l.borrow_mut().push(DroppedDep {
drv: drv.to_string(),
attr: attr.to_string(),
site,
force_err: force_err.to_string(),
})
});
}
pub fn drain() -> Vec<DroppedDep> {
LEDGER.with(|l| std::mem::take(&mut *l.borrow_mut()))
}
pub fn len() -> usize {
LEDGER.with(|l| l.borrow().len())
}
}
struct ComputedDrv {
drv_path: String,
out_paths: std::collections::BTreeMap<String, String>,
}
fn compute_full_drv(input: &NixAttrs, name: &str) -> Result<ComputedDrv, EvalError> {
let (_name, drv) = construct_derivation(input)?;
let (drv_path, out_paths, mut drv) = compute_derivation_outputs(input, name, drv)?;
write_derivation_to_store(&drv_path, &out_paths, &mut drv)?;
if let Ok(dir) = std::env::var("SUI_EMIT_DRV") {
if !dir.is_empty() {
let base = drv_path.rsplit('/').next().unwrap_or(&drv_path);
let _ = std::fs::create_dir_all(&dir);
let _ = std::fs::write(std::path::Path::new(&dir).join(base), drv.serialize().as_bytes());
}
}
if let Some(want) = std::env::var_os("SUI_DUMP_DRV") {
let want = want.to_string_lossy();
if want == "all" || name.contains(want.as_ref()) {
eprintln!("[SUI_DUMP_DRV] === {name} ===");
eprintln!("[SUI_DUMP_DRV] drvPath = {drv_path}");
eprintln!("[SUI_DUMP_DRV] outputs = {out_paths:#?}");
eprintln!("[SUI_DUMP_DRV] inputDrvs = {:#?}", drv.input_derivations);
eprintln!("[SUI_DUMP_DRV] inputSrcs = {:#?}", drv.input_sources);
eprintln!("[SUI_DUMP_DRV] builder = {}", drv.builder);
eprintln!("[SUI_DUMP_DRV] args = {:#?}", drv.args);
eprintln!("[SUI_DUMP_DRV] env = {:#?}", drv.env);
eprintln!("[SUI_DUMP_DRV] ATerm = {}", drv.serialize());
}
}
Ok(ComputedDrv { drv_path, out_paths })
}
pub fn build_derivation(arg: &Value) -> Result<Value, EvalError> {
let forced = crate::eval::force_value(arg)?;
let input_owned = forced.to_attrs()?;
let input = &input_owned;
let name = force_attr_string(input, "name")?;
build_derivation_result(input, &name)
}
fn construct_derivation(
input: &NixAttrs,
) -> Result<(String, sui_compat::derivation::Derivation), EvalError> {
use std::collections::BTreeMap;
use crate::value::{ContextElement, StringContext};
let name = force_attr_string(input, "name")?;
let system = force_attr_string(input, "system")?;
let mut collected_ctx = StringContext::new();
let builder = {
let v = input.get("builder").ok_or_else(|| {
EvalError::TypeError("derivation: missing required attribute 'builder'".into())
})?;
let (s, ctx) = coerce_drv_value_to_string_with_context(v)?;
collected_ctx.merge(&ctx);
s
};
let args_list: Vec<String> = if let Some(a) = input.get("args") {
let forced_args = crate::eval::force_value(a)?;
let list = forced_args.as_list()?;
let mut out = Vec::with_capacity(list.len());
for item in list {
let (s, ctx) = coerce_drv_value_to_string_with_context(item)?;
collected_ctx.merge(&ctx);
out.push(s);
}
out
} else {
Vec::new()
};
let ignore_nulls = input
.get("__ignoreNulls")
.and_then(|v| crate::eval::force_value(v).ok())
.is_some_and(|v| matches!(v, Value::Bool(true)));
let structured_attrs = input
.get("__structuredAttrs")
.and_then(|v| crate::eval::force_value(v).ok())
.is_some_and(|v| matches!(v, Value::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 crate::eval::force_value(v) {
Ok(v) => v,
Err(_e) => {
parity_strict::record(
&name,
&k,
parity_strict::DropSite::StructuredAttrs,
&format!("{_e:?}"),
);
continue;
}
};
if ignore_nulls && matches!(forced_v, Value::Null) {
continue;
}
if let Ok(jv) = forced_v.to_json_with_context(&mut collected_ctx) {
obj.insert(k.clone(), jv);
}
}
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_unsorted() {
if matches!(
k.as_str(),
"name" | "system" | "builder" | "args"
| "__ignoreNulls" | "__impure" | "__contentAddressed"
) {
continue;
}
let forced_v = match crate::eval::force_value(v) {
Ok(v) => v,
Err(_e) => {
if std::env::var_os("SUI_DEBUG_DRV").is_some() {
eprintln!("[SUI_DEBUG_DRV] drv={name} attr={k} FORCE-ERR: {_e:?}");
}
parity_strict::record(
&name,
&k,
parity_strict::DropSite::FlatEnv,
&format!("{_e:?}"),
);
continue;
}
};
if ignore_nulls && matches!(forced_v, Value::Null) {
continue;
}
match coerce_drv_value_to_string_opt_with_context(&forced_v)? {
Some((s, ctx)) => {
collected_ctx.merge(&ctx);
env_vars.insert(k.clone(), s);
}
None => {
if std::env::var_os("SUI_DEBUG_DRV").is_some() {
eprintln!("[SUI_DEBUG_DRV] drv={name} attr={k} COERCE-NONE type={}", forced_v.type_name());
}
parity_strict::record(
&name,
&k,
parity_strict::DropSite::FlatEnv,
&format!("coerce-none type={}", forced_v.type_name()),
);
}
}
}
env_vars.insert("name".to_string(), name.clone());
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 {
ContextElement::Output { drv, output } => {
input_derivations
.entry(drv.to_string())
.or_default()
.push(output.to_string());
}
ContextElement::Plain(p) => {
let s = p.to_string();
if s.starts_with("/nix/store/") && !input_sources.contains(&s) {
input_sources.push(s);
}
}
_ => {} }
}
for outs in input_derivations.values_mut() {
outs.sort();
outs.dedup();
}
let drv = sui_compat::derivation::Derivation {
outputs: BTreeMap::new(),
input_derivations,
input_sources,
system,
builder,
args: args_list,
env: env_vars,
};
Ok((name, drv))
}
fn compute_derivation_outputs(
input: &NixAttrs,
name: &str,
mut drv: sui_compat::derivation::Derivation,
) -> Result<(String, std::collections::BTreeMap<String, String>, sui_compat::derivation::Derivation), EvalError> {
use std::collections::BTreeMap;
use sui_compat::derivation::DerivationOutput;
let is_fod = input.contains_key("outputHash");
if is_fod {
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| EvalError::TypeError(
format!("derivation: invalid outputHashAlgo {output_hash_algo:?}: {e}"),
))?;
let parsed = sui_compat::hash::NixHash::parse_any(algo, &raw_output_hash)
.map_err(|e| EvalError::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()
};
if std::env::var_os("SUI_DUMP_MODULO").is_some() {
eprintln!("[SUI_DUMP_MODULO] (FOD) {drv_path} = {modulo_hex}");
}
sui_spec::derivation::remember_modulo_hash(&drv_path, &modulo_hex);
let mut out_paths = BTreeMap::new();
out_paths.insert("out".to_string(), out_path);
Ok((drv_path, out_paths, drv))
} else {
let outputs = parse_outputs_list(input)?;
let algo = sui_spec::derivation::load_canonical()
.map_err(|e| EvalError::TypeError(
format!("derivation algorithm spec failed to load: {e}")
))?;
sui_spec::derivation::apply(&algo, drv, outputs, name)
.map_err(|e| EvalError::TypeError(
format!("derivation algorithm interpreter failed: {e}")
))
}
}
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: &NixAttrs) -> Result<Vec<String>, EvalError> {
if let Some(o) = input.get("outputs") {
let forced_o = crate::eval::force_value(o)?;
let list = forced_o.as_list()?;
let mut out = Vec::with_capacity(list.len());
for item in list {
let s = crate::eval::force_value(item)?
.as_string()
.map_err(|_| EvalError::TypeError("derivation: outputs entries must be strings".into()))?
.to_string();
out.push(s);
}
if out.is_empty() {
return Err(EvalError::TypeError("derivation: outputs list must not be empty".into()));
}
Ok(out)
} else {
Ok(vec!["out".to_string()])
}
}
fn write_derivation_to_store(
drv_path: &str,
out_paths: &std::collections::BTreeMap<String, String>,
drv: &mut sui_compat::derivation::Derivation,
) -> Result<(), EvalError> {
for (output_name, output_path) in out_paths {
if let Some(output) = drv.outputs.get_mut(output_name)
&& output.path.is_empty() {
output.path.clone_from(output_path);
}
drv.env.insert(output_name.clone(), output_path.clone());
}
let drv_content_final = drv.serialize();
let store_dir = std::env::var("SUI_STORE_DIR")
.unwrap_or_else(|_| "/nix/store".to_string());
let disk_path = if store_dir != "/nix/store" {
drv_path.replacen("/nix/store", &store_dir, 1)
} else {
drv_path.to_string()
};
let drv_file = std::path::Path::new(&disk_path);
if !drv_file.exists() {
if let Some(parent) = drv_file.parent() {
std::fs::create_dir_all(parent).ok();
}
match std::fs::write(drv_file, drv_content_final.as_bytes()) {
Ok(()) => {}
Err(e) if e.kind() == std::io::ErrorKind::PermissionDenied => {
let fallback_dir = std::env::temp_dir().join("sui-drv-cache");
std::fs::create_dir_all(&fallback_dir).ok();
let fallback_path = fallback_dir.join(drv_file.file_name().unwrap_or_default());
if let Err(e2) = std::fs::write(&fallback_path, drv_content_final.as_bytes()) {
tracing::warn!("failed to write .drv to both {} and {}: {e}, {e2}", drv_path, fallback_path.display());
} else {
tracing::debug!("wrote .drv to fallback: {}", fallback_path.display());
}
}
Err(e) => {
return Err(EvalError::IoError {
context: format!("writing derivation {drv_path}"),
message: e.to_string(),
});
}
}
}
Ok(())
}
fn build_derivation_result(
input: &NixAttrs,
name: &str,
) -> Result<Value, EvalError> {
use crate::value::{NixString, StringContext};
let computed: Rc<std::cell::OnceCell<Rc<ComputedDrv>>> =
Rc::new(std::cell::OnceCell::new());
let input_shared = Rc::new(input.clone());
let name_shared: Rc<str> = Rc::from(name);
let get_computed = {
let computed = computed.clone();
let input_shared = input_shared.clone();
let name_shared = name_shared.clone();
move || -> Result<Rc<ComputedDrv>, EvalError> {
if let Some(c) = computed.get() {
return Ok(c.clone());
}
let c = Rc::new(compute_full_drv(&input_shared, &name_shared)?);
let _ = computed.set(c.clone());
Ok(computed.get().cloned().unwrap_or(c))
}
};
let drv_path_thunk = {
let get_computed = get_computed.clone();
Value::Thunk(crate::value::Thunk::new_native(move || {
let c = get_computed()?;
let mut ctx = StringContext::new();
ctx.add_drv_deep(c.drv_path.clone());
Ok(Value::String(Rc::new(NixString::with_context(&c.drv_path, ctx))))
}))
};
let primary_output_name = parse_outputs_list(input)?
.into_iter()
.next()
.unwrap_or_else(|| "out".to_string());
let out_path_thunk = {
let get_computed = get_computed.clone();
let primary = primary_output_name.clone();
Value::Thunk(crate::value::Thunk::new_native(move || {
let c = get_computed()?;
let p = c.out_paths.get(&primary).cloned().unwrap_or_default();
let mut ctx = StringContext::new();
ctx.add_output(c.drv_path.clone(), primary.clone());
Ok(Value::String(Rc::new(NixString::with_context(&p, ctx))))
}))
};
let mut result = (*input_shared).clone();
result.insert("type".to_string(), Value::string("derivation"));
result.insert("drvPath".to_string(), drv_path_thunk.clone());
result.insert("drvAttrs".to_string(), Value::Attrs(input_shared.clone()));
result.insert("outPath".to_string(), out_path_thunk);
result.insert("outputName".to_string(), Value::string(primary_output_name.clone()));
let output_names = parse_outputs_list(input)?;
let output_names = if output_names.is_empty() {
vec!["out".to_string()]
} else {
output_names
};
let mut all_outputs: Vec<Value> = Vec::new();
for output_name in &output_names {
let mut out_attrs = NixAttrs::new();
let out_path_field = {
let get_computed = get_computed.clone();
let output_name = output_name.clone();
Value::Thunk(crate::value::Thunk::new_native(move || {
let c = get_computed()?;
let p = c.out_paths.get(&output_name).cloned().unwrap_or_default();
let mut ctx = StringContext::new();
ctx.add_output(c.drv_path.clone(), output_name.clone());
Ok(Value::String(Rc::new(NixString::with_context(&p, ctx))))
}))
};
out_attrs.insert("outPath".to_string(), out_path_field);
out_attrs.insert("drvPath".to_string(), drv_path_thunk.clone());
out_attrs.insert("type".to_string(), Value::string("derivation"));
out_attrs.insert("outputName".to_string(), Value::string(output_name.clone()));
out_attrs.insert("name".to_string(), Value::string(&*name_shared));
let out_val = Value::Attrs(Rc::new(out_attrs));
all_outputs.push(out_val.clone());
result.insert(output_name.clone(), out_val);
}
result.insert("all".to_string(), Value::List(Rc::new(NixList::new(all_outputs))));
Ok(Value::Attrs(Rc::new(result)))
}