fix: derivation semantic
This commit is contained in:
@@ -365,65 +365,52 @@ const specialAttrs = new Set([
|
||||
|
||||
export const derivation = (args: NixValue): NixAttrs => {
|
||||
const attrs = forceAttrs(args);
|
||||
const strict = derivationStrict(args);
|
||||
|
||||
const outputs: string[] = extractOutputs(attrs);
|
||||
const drvName = validateName(attrs);
|
||||
const collectedContext: NixStringContext = new Set();
|
||||
const builder = validateBuilder(attrs, collectedContext);
|
||||
const platform = validateSystem(attrs);
|
||||
const structuredAttrs = "__structuredAttrs" in attrs ? force(attrs.__structuredAttrs) === true : false;
|
||||
const ignoreNulls = "__ignoreNulls" in attrs ? force(attrs.__ignoreNulls) === true : false;
|
||||
const drvArgs = extractArgs(attrs, collectedContext);
|
||||
|
||||
const baseAttrs: NixAttrs = {
|
||||
const strictThunk = createThunk(() => derivationStrict(args), "derivationStrict");
|
||||
|
||||
const commonAttrs: NixAttrs = { ...attrs };
|
||||
|
||||
const outputToAttrListElement = (outputName: string): { name: string; value: NixAttrs } => {
|
||||
const value: NixAttrs = {
|
||||
...commonAttrs,
|
||||
outPath: createThunk(() => (force(strictThunk) as NixAttrs)[outputName], `outPath_${outputName}`),
|
||||
drvPath: createThunk(() => (force(strictThunk) as NixAttrs).drvPath, "drvPath"),
|
||||
type: "derivation",
|
||||
drvPath: strict.drvPath,
|
||||
name: drvName,
|
||||
builder,
|
||||
system: platform,
|
||||
};
|
||||
|
||||
if (drvArgs.length > 0) {
|
||||
baseAttrs.args = drvArgs;
|
||||
}
|
||||
|
||||
if (!structuredAttrs) {
|
||||
for (const [key, value] of Object.entries(attrs)) {
|
||||
if (!specialAttrs.has(key) && !outputs.includes(key)) {
|
||||
const forcedValue = force(value);
|
||||
if (!(ignoreNulls && forcedValue === null)) {
|
||||
baseAttrs[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const outputsList: NixAttrs[] = [];
|
||||
|
||||
for (const outputName of outputs) {
|
||||
const outputObj: NixAttrs = {
|
||||
...baseAttrs,
|
||||
outPath: strict[outputName],
|
||||
outputName,
|
||||
};
|
||||
outputsList.push(outputObj);
|
||||
}
|
||||
return { name: outputName, value };
|
||||
};
|
||||
|
||||
baseAttrs.drvAttrs = attrs;
|
||||
for (const [i, outputName] of outputs.entries()) {
|
||||
baseAttrs[outputName] = createThunk(() => outputsList[i], `output_${outputName}`);
|
||||
}
|
||||
baseAttrs.all = createThunk(() => outputsList, "all_outputs");
|
||||
const outputsList = outputs.map(outputToAttrListElement);
|
||||
|
||||
for (const outputObj of outputsList) {
|
||||
for (const { name: outputName, value } of outputsList) {
|
||||
commonAttrs[outputName] = createThunk(
|
||||
() => outputsList.find((o) => o.name === outputName)!.value,
|
||||
`output_${outputName}`,
|
||||
);
|
||||
}
|
||||
commonAttrs.all = createThunk(
|
||||
() => outputsList.map((o) => o.value),
|
||||
"all_outputs",
|
||||
);
|
||||
commonAttrs.drvAttrs = attrs;
|
||||
|
||||
for (const { value: outputObj } of outputsList) {
|
||||
for (const { name: outputName } of outputsList) {
|
||||
outputObj[outputName] = createThunk(
|
||||
() => outputsList.find((o) => o.name === outputName)!.value,
|
||||
`output_${outputName}`,
|
||||
);
|
||||
}
|
||||
outputObj.all = createThunk(
|
||||
() => outputsList.map((o) => o.value),
|
||||
"all_outputs",
|
||||
);
|
||||
outputObj.drvAttrs = attrs;
|
||||
for (const [i, outputName] of outputs.entries()) {
|
||||
outputObj[outputName] = createThunk(() => outputsList[i], `output_${outputName}`);
|
||||
}
|
||||
outputObj.all = createThunk(() => outputsList, "all_outputs");
|
||||
}
|
||||
|
||||
return outputsList[0];
|
||||
return outputsList[0].value;
|
||||
};
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* All functionality is exported via the global `Nix` object
|
||||
*/
|
||||
|
||||
import { createThunk, force, isThunk, IS_THUNK, DEBUG_THUNKS } from "./thunk";
|
||||
import { createThunk, force, isThunk, IS_THUNK, DEBUG_THUNKS, forceDeepSafe, IS_CYCLE } from "./thunk";
|
||||
import {
|
||||
select,
|
||||
selectWithDefault,
|
||||
@@ -34,9 +34,11 @@ export type NixRuntime = typeof Nix;
|
||||
export const Nix = {
|
||||
createThunk,
|
||||
force,
|
||||
forceDeepSafe,
|
||||
forceBool,
|
||||
isThunk,
|
||||
IS_THUNK,
|
||||
IS_CYCLE,
|
||||
HAS_CONTEXT,
|
||||
IS_PATH,
|
||||
DEBUG_THUNKS,
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
*/
|
||||
|
||||
import type { NixValue, NixThunkInterface, NixStrictValue } from "./types";
|
||||
import { HAS_CONTEXT } from "./string-context";
|
||||
import { IS_PATH } from "./types";
|
||||
|
||||
/**
|
||||
* Symbol used to mark objects as thunks
|
||||
@@ -132,3 +134,50 @@ export const force = (value: NixValue): NixStrictValue => {
|
||||
export const createThunk = (func: () => NixValue, label?: string): NixThunkInterface => {
|
||||
return new NixThunk(func, label);
|
||||
};
|
||||
|
||||
/**
|
||||
* Symbol to mark cyclic references detected during deep forcing
|
||||
*/
|
||||
export const IS_CYCLE = Symbol("is_cycle");
|
||||
|
||||
/**
|
||||
* Marker object for cyclic references
|
||||
*/
|
||||
export const CYCLE_MARKER = { [IS_CYCLE]: true };
|
||||
|
||||
/**
|
||||
* Deeply force a value, handling cycles by returning a special marker.
|
||||
* Uses WeakSet to track seen objects and avoid infinite recursion.
|
||||
* Returns a fully forced value where thunks are replaced with their results.
|
||||
* Cyclic references are replaced with CYCLE_MARKER.
|
||||
*/
|
||||
export const forceDeepSafe = (value: NixValue, seen: WeakSet<object> = new WeakSet()): NixStrictValue => {
|
||||
const forced = force(value);
|
||||
|
||||
if (forced === null || typeof forced !== "object") {
|
||||
return forced;
|
||||
}
|
||||
|
||||
if (seen.has(forced)) {
|
||||
return CYCLE_MARKER;
|
||||
}
|
||||
seen.add(forced);
|
||||
|
||||
if (HAS_CONTEXT in forced || IS_PATH in forced) {
|
||||
return forced;
|
||||
}
|
||||
|
||||
if (Array.isArray(forced)) {
|
||||
return forced.map((item) => forceDeepSafe(item, seen));
|
||||
}
|
||||
|
||||
if (typeof forced === "object") {
|
||||
const result: Record<string, NixValue> = {};
|
||||
for (const [key, val] of Object.entries(forced)) {
|
||||
result[key] = forceDeepSafe(val, seen);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
return forced;
|
||||
};
|
||||
|
||||
@@ -37,7 +37,7 @@ impl Context {
|
||||
|
||||
tracing::debug!("Executing JavaScript");
|
||||
self.runtime
|
||||
.eval(format!("Nix.force({code})"), &mut self.ctx)
|
||||
.eval(format!("Nix.forceDeepSafe({code})"), &mut self.ctx)
|
||||
}
|
||||
|
||||
pub fn compile_code(&mut self, source: Source) -> Result<String> {
|
||||
|
||||
@@ -552,6 +552,7 @@ pub(crate) struct Runtime<Ctx: RuntimeContext> {
|
||||
primop_metadata_symbol: v8::Global<v8::Symbol>,
|
||||
has_context_symbol: v8::Global<v8::Symbol>,
|
||||
is_path_symbol: v8::Global<v8::Symbol>,
|
||||
is_cycle_symbol: v8::Global<v8::Symbol>,
|
||||
_marker: PhantomData<Ctx>,
|
||||
}
|
||||
|
||||
@@ -571,7 +572,7 @@ impl<Ctx: RuntimeContext> Runtime<Ctx> {
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
let (is_thunk_symbol, primop_metadata_symbol, has_context_symbol, is_path_symbol) = {
|
||||
let (is_thunk_symbol, primop_metadata_symbol, has_context_symbol, is_path_symbol, is_cycle_symbol) = {
|
||||
deno_core::scope!(scope, &mut js_runtime);
|
||||
Self::get_symbols(scope)?
|
||||
};
|
||||
@@ -582,6 +583,7 @@ impl<Ctx: RuntimeContext> Runtime<Ctx> {
|
||||
primop_metadata_symbol,
|
||||
has_context_symbol,
|
||||
is_path_symbol,
|
||||
is_cycle_symbol,
|
||||
_marker: PhantomData,
|
||||
})
|
||||
}
|
||||
@@ -609,6 +611,7 @@ impl<Ctx: RuntimeContext> Runtime<Ctx> {
|
||||
let primop_metadata_symbol = v8::Local::new(scope, &self.primop_metadata_symbol);
|
||||
let has_context_symbol = v8::Local::new(scope, &self.has_context_symbol);
|
||||
let is_path_symbol = v8::Local::new(scope, &self.is_path_symbol);
|
||||
let is_cycle_symbol = v8::Local::new(scope, &self.is_cycle_symbol);
|
||||
|
||||
Ok(to_value(
|
||||
local_value,
|
||||
@@ -617,10 +620,11 @@ impl<Ctx: RuntimeContext> Runtime<Ctx> {
|
||||
primop_metadata_symbol,
|
||||
has_context_symbol,
|
||||
is_path_symbol,
|
||||
is_cycle_symbol,
|
||||
))
|
||||
}
|
||||
|
||||
/// get (IS_THUNK, PRIMOP_METADATA, HAS_CONTEXT, IS_PATH)
|
||||
/// get (IS_THUNK, PRIMOP_METADATA, HAS_CONTEXT, IS_PATH, IS_CYCLE)
|
||||
#[allow(clippy::type_complexity)]
|
||||
fn get_symbols(
|
||||
scope: &ScopeRef,
|
||||
@@ -629,6 +633,7 @@ impl<Ctx: RuntimeContext> Runtime<Ctx> {
|
||||
v8::Global<v8::Symbol>,
|
||||
v8::Global<v8::Symbol>,
|
||||
v8::Global<v8::Symbol>,
|
||||
v8::Global<v8::Symbol>,
|
||||
)> {
|
||||
let global = scope.get_current_context().global(scope);
|
||||
let nix_key = v8::String::new(scope, "Nix")
|
||||
@@ -659,8 +664,9 @@ impl<Ctx: RuntimeContext> Runtime<Ctx> {
|
||||
let primop_metadata = get_symbol("PRIMOP_METADATA")?;
|
||||
let has_context = get_symbol("HAS_CONTEXT")?;
|
||||
let is_path = get_symbol("IS_PATH")?;
|
||||
let is_cycle = get_symbol("IS_CYCLE")?;
|
||||
|
||||
Ok((is_thunk, primop_metadata, has_context, is_path))
|
||||
Ok((is_thunk, primop_metadata, has_context, is_path, is_cycle))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -671,6 +677,7 @@ fn to_value<'a>(
|
||||
primop_metadata_symbol: LocalSymbol<'a>,
|
||||
has_context_symbol: LocalSymbol<'a>,
|
||||
is_path_symbol: LocalSymbol<'a>,
|
||||
is_cycle_symbol: LocalSymbol<'a>,
|
||||
) -> Value {
|
||||
match () {
|
||||
_ if val.is_big_int() => {
|
||||
@@ -707,6 +714,7 @@ fn to_value<'a>(
|
||||
primop_metadata_symbol,
|
||||
has_context_symbol,
|
||||
is_path_symbol,
|
||||
is_cycle_symbol,
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
@@ -724,6 +732,10 @@ fn to_value<'a>(
|
||||
return Value::Thunk;
|
||||
}
|
||||
|
||||
if is_cycle(val, scope, is_cycle_symbol) {
|
||||
return Value::Thunk;
|
||||
}
|
||||
|
||||
if let Some(path_val) = extract_path(val, scope, is_path_symbol) {
|
||||
return Value::Path(path_val);
|
||||
}
|
||||
@@ -753,6 +765,7 @@ fn to_value<'a>(
|
||||
primop_metadata_symbol,
|
||||
has_context_symbol,
|
||||
is_path_symbol,
|
||||
is_cycle_symbol,
|
||||
),
|
||||
)
|
||||
})
|
||||
@@ -772,6 +785,15 @@ fn is_thunk<'a>(val: LocalValue<'a>, scope: &ScopeRef<'a, '_>, symbol: LocalSymb
|
||||
matches!(obj.get(scope, symbol.into()), Some(v) if v.is_true())
|
||||
}
|
||||
|
||||
fn is_cycle<'a>(val: LocalValue<'a>, scope: &ScopeRef<'a, '_>, symbol: LocalSymbol<'a>) -> bool {
|
||||
if !val.is_object() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let obj = val.to_object(scope).expect("infallible conversion");
|
||||
matches!(obj.get(scope, symbol.into()), Some(v) if v.is_true())
|
||||
}
|
||||
|
||||
fn extract_string_with_context<'a>(
|
||||
val: LocalValue<'a>,
|
||||
scope: &ScopeRef<'a, '_>,
|
||||
|
||||
@@ -470,8 +470,8 @@ fn structured_attrs_basic() {
|
||||
Value::AttrSet(attrs) => {
|
||||
assert!(attrs.contains_key("drvPath"));
|
||||
assert!(attrs.contains_key("outPath"));
|
||||
assert!(!attrs.contains_key("foo"));
|
||||
assert!(!attrs.contains_key("count"));
|
||||
assert!(attrs.contains_key("foo"));
|
||||
assert!(attrs.contains_key("count"));
|
||||
}
|
||||
_ => panic!("Expected AttrSet"),
|
||||
}
|
||||
@@ -492,7 +492,7 @@ fn structured_attrs_nested() {
|
||||
match result {
|
||||
Value::AttrSet(attrs) => {
|
||||
assert!(attrs.contains_key("drvPath"));
|
||||
assert!(!attrs.contains_key("data"));
|
||||
assert!(attrs.contains_key("data"));
|
||||
}
|
||||
_ => panic!("Expected AttrSet"),
|
||||
}
|
||||
@@ -554,7 +554,7 @@ fn ignore_nulls_true() {
|
||||
match result {
|
||||
Value::AttrSet(attrs) => {
|
||||
assert!(attrs.contains_key("foo"));
|
||||
assert!(!attrs.contains_key("nullValue"));
|
||||
assert!(attrs.contains_key("nullValue"));
|
||||
}
|
||||
_ => panic!("Expected AttrSet"),
|
||||
}
|
||||
@@ -600,8 +600,8 @@ fn ignore_nulls_with_structured_attrs() {
|
||||
match result {
|
||||
Value::AttrSet(attrs) => {
|
||||
assert!(attrs.contains_key("drvPath"));
|
||||
assert!(!attrs.contains_key("foo"));
|
||||
assert!(!attrs.contains_key("nullValue"));
|
||||
assert!(attrs.contains_key("foo"));
|
||||
assert!(attrs.contains_key("nullValue"));
|
||||
}
|
||||
_ => panic!("Expected AttrSet"),
|
||||
}
|
||||
@@ -627,8 +627,8 @@ fn all_features_combined() {
|
||||
assert!(attrs.contains_key("out"));
|
||||
assert!(attrs.contains_key("dev"));
|
||||
assert!(attrs.contains_key("outPath"));
|
||||
assert!(!attrs.contains_key("data"));
|
||||
assert!(!attrs.contains_key("nullValue"));
|
||||
assert!(attrs.contains_key("data"));
|
||||
assert!(attrs.contains_key("nullValue"));
|
||||
}
|
||||
_ => panic!("Expected AttrSet"),
|
||||
}
|
||||
@@ -651,7 +651,7 @@ fn fixed_output_with_structured_attrs() {
|
||||
Value::AttrSet(attrs) => {
|
||||
assert!(attrs.contains_key("outPath"));
|
||||
assert!(attrs.contains_key("drvPath"));
|
||||
assert!(!attrs.contains_key("data"));
|
||||
assert!(attrs.contains_key("data"));
|
||||
}
|
||||
_ => panic!("Expected AttrSet"),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user