feat: initial derivation implementation
This commit is contained in:
79
Cargo.lock
generated
79
Cargo.lock
generated
@@ -125,6 +125,15 @@ dependencies = [
|
|||||||
"wyz",
|
"wyz",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "block-buffer"
|
||||||
|
version = "0.10.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
|
||||||
|
dependencies = [
|
||||||
|
"generic-array",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "boxed_error"
|
name = "boxed_error"
|
||||||
version = "0.2.3"
|
version = "0.2.3"
|
||||||
@@ -324,6 +333,15 @@ version = "3.0.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7704b5fdd17b18ae31c4c1da5a2e0305a2bf17b5249300a9ee9ed7b72114c636"
|
checksum = "7704b5fdd17b18ae31c4c1da5a2e0305a2bf17b5249300a9ee9ed7b72114c636"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cpufeatures"
|
||||||
|
version = "0.2.17"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crc32fast"
|
name = "crc32fast"
|
||||||
version = "1.5.0"
|
version = "1.5.0"
|
||||||
@@ -400,6 +418,16 @@ version = "0.2.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
|
checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crypto-common"
|
||||||
|
version = "0.1.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
|
||||||
|
dependencies = [
|
||||||
|
"generic-array",
|
||||||
|
"typenum",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "data-encoding"
|
name = "data-encoding"
|
||||||
version = "2.9.0"
|
version = "2.9.0"
|
||||||
@@ -550,6 +578,16 @@ dependencies = [
|
|||||||
"unicode-xid",
|
"unicode-xid",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "digest"
|
||||||
|
version = "0.10.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
||||||
|
dependencies = [
|
||||||
|
"block-buffer",
|
||||||
|
"crypto-common",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "diplomat"
|
name = "diplomat"
|
||||||
version = "0.14.0"
|
version = "0.14.0"
|
||||||
@@ -776,6 +814,16 @@ dependencies = [
|
|||||||
"slab",
|
"slab",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "generic-array"
|
||||||
|
version = "0.14.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
|
||||||
|
dependencies = [
|
||||||
|
"typenum",
|
||||||
|
"version_check",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "getrandom"
|
name = "getrandom"
|
||||||
version = "0.3.4"
|
version = "0.3.4"
|
||||||
@@ -852,6 +900,12 @@ version = "0.5.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
|
checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hex"
|
||||||
|
version = "0.4.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "home"
|
name = "home"
|
||||||
version = "0.5.11"
|
version = "0.5.11"
|
||||||
@@ -1231,6 +1285,7 @@ dependencies = [
|
|||||||
"deno_error",
|
"deno_error",
|
||||||
"derive_more",
|
"derive_more",
|
||||||
"hashbrown 0.16.1",
|
"hashbrown 0.16.1",
|
||||||
|
"hex",
|
||||||
"itertools 0.14.0",
|
"itertools 0.14.0",
|
||||||
"mimalloc",
|
"mimalloc",
|
||||||
"nix-js-macros",
|
"nix-js-macros",
|
||||||
@@ -1238,6 +1293,7 @@ dependencies = [
|
|||||||
"regex",
|
"regex",
|
||||||
"rnix",
|
"rnix",
|
||||||
"rustyline",
|
"rustyline",
|
||||||
|
"sha2",
|
||||||
"string-interner",
|
"string-interner",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
@@ -1737,6 +1793,17 @@ dependencies = [
|
|||||||
"v8",
|
"v8",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sha2"
|
||||||
|
version = "0.10.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"cpufeatures",
|
||||||
|
"digest",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "shlex"
|
name = "shlex"
|
||||||
version = "1.3.0"
|
version = "1.3.0"
|
||||||
@@ -2043,6 +2110,12 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typenum"
|
||||||
|
version = "1.19.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-id-start"
|
name = "unicode-id-start"
|
||||||
version = "1.4.0"
|
version = "1.4.0"
|
||||||
@@ -2124,6 +2197,12 @@ dependencies = [
|
|||||||
"which",
|
"which",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "version_check"
|
||||||
|
version = "0.9.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "vsimd"
|
name = "vsimd"
|
||||||
version = "0.8.0"
|
version = "0.8.0"
|
||||||
|
|||||||
@@ -25,6 +25,9 @@ regex = "1.11"
|
|||||||
deno_core = "0.376"
|
deno_core = "0.376"
|
||||||
deno_error = "0.7"
|
deno_error = "0.7"
|
||||||
|
|
||||||
|
sha2 = "0.10"
|
||||||
|
hex = "0.4"
|
||||||
|
|
||||||
rnix = "0.12"
|
rnix = "0.12"
|
||||||
|
|
||||||
nix-js-macros = { path = "../nix-js-macros" }
|
nix-js-macros = { path = "../nix-js-macros" }
|
||||||
|
|||||||
@@ -1,9 +1,338 @@
|
|||||||
import type { NixValue } from "../types";
|
import type { NixValue, NixAttrs } from "../types";
|
||||||
|
import { forceString, forceList } from "../type-assert";
|
||||||
|
import { force } from "../thunk";
|
||||||
|
import { type DerivationData, type OutputInfo, generateAterm } from "../derivation-helpers";
|
||||||
|
import { coerceToString, StringCoercionMode } from "./conversion";
|
||||||
|
|
||||||
export const derivation = (args: NixValue) => {
|
const forceAttrs = (value: NixValue): NixAttrs => {
|
||||||
throw new Error("Not implemented: derivation");
|
const forced = force(value);
|
||||||
|
if (typeof forced !== "object" || forced === null || Array.isArray(forced)) {
|
||||||
|
throw new TypeError(`Expected attribute set for derivation, got ${typeof forced}`);
|
||||||
|
}
|
||||||
|
return forced as NixAttrs;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const derivationStrict = (args: NixValue) => {
|
const validateName = (attrs: NixAttrs): string => {
|
||||||
throw new Error("Not implemented: derivationStrict");
|
if (!("name" in attrs)) {
|
||||||
|
throw new Error("derivation: missing required attribute 'name'");
|
||||||
|
}
|
||||||
|
const name = forceString(attrs.name);
|
||||||
|
if (!name) {
|
||||||
|
throw new Error("derivation: 'name' cannot be empty");
|
||||||
|
}
|
||||||
|
if (name.endsWith(".drv")) {
|
||||||
|
throw new Error(`derivation: invalid name '${name}' (cannot end with .drv)`);
|
||||||
|
}
|
||||||
|
return name;
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateBuilder = (attrs: NixAttrs): string => {
|
||||||
|
if (!("builder" in attrs)) {
|
||||||
|
throw new Error("derivation: missing required attribute 'builder'");
|
||||||
|
}
|
||||||
|
return forceString(attrs.builder);
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateSystem = (attrs: NixAttrs): string => {
|
||||||
|
if (!("system" in attrs)) {
|
||||||
|
throw new Error("derivation: missing required attribute 'system'");
|
||||||
|
}
|
||||||
|
return forceString(attrs.system);
|
||||||
|
};
|
||||||
|
|
||||||
|
const extractOutputs = (attrs: NixAttrs): string[] => {
|
||||||
|
if (!("outputs" in attrs)) {
|
||||||
|
return ["out"];
|
||||||
|
}
|
||||||
|
const outputsList = forceList(attrs.outputs);
|
||||||
|
const outputs = outputsList.map((o) => forceString(o));
|
||||||
|
|
||||||
|
if (outputs.length === 0) {
|
||||||
|
throw new Error("derivation: outputs list cannot be empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (outputs.includes("drv")) {
|
||||||
|
throw new Error("derivation: invalid output name 'drv'");
|
||||||
|
}
|
||||||
|
|
||||||
|
const seen = new Set<string>();
|
||||||
|
for (const output of outputs) {
|
||||||
|
if (seen.has(output)) {
|
||||||
|
throw new Error(`derivation: duplicate output '${output}'`);
|
||||||
|
}
|
||||||
|
seen.add(output);
|
||||||
|
}
|
||||||
|
|
||||||
|
return outputs;
|
||||||
|
};
|
||||||
|
|
||||||
|
const extractArgs = (attrs: NixAttrs): string[] => {
|
||||||
|
if (!("args" in attrs)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const argsList = forceList(attrs.args);
|
||||||
|
return argsList.map((a) => coerceToString(a, StringCoercionMode.ToString, false));
|
||||||
|
};
|
||||||
|
|
||||||
|
const nixValueToJson = (value: NixValue, seen = new Set<object>()): any => {
|
||||||
|
const v = force(value);
|
||||||
|
|
||||||
|
if (v === null) return null;
|
||||||
|
if (typeof v === "boolean") return v;
|
||||||
|
if (typeof v === "string") return v;
|
||||||
|
if (typeof v === "number") return v;
|
||||||
|
|
||||||
|
if (typeof v === "bigint") {
|
||||||
|
const num = Number(v);
|
||||||
|
if (v > Number.MAX_SAFE_INTEGER || v < Number.MIN_SAFE_INTEGER) {
|
||||||
|
console.warn(`derivation: integer ${v} exceeds safe range, precision may be lost in __structuredAttrs`);
|
||||||
|
}
|
||||||
|
return num;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof v === "object" && v !== null) {
|
||||||
|
if (seen.has(v)) {
|
||||||
|
throw new Error("derivation: circular reference detected in __structuredAttrs");
|
||||||
|
}
|
||||||
|
seen.add(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(v)) {
|
||||||
|
return v.map((item) => nixValueToJson(item, seen));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof v === "object") {
|
||||||
|
const result: Record<string, any> = {};
|
||||||
|
for (const [key, val] of Object.entries(v)) {
|
||||||
|
result[key] = nixValueToJson(val, seen);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof v === "function") {
|
||||||
|
throw new Error("derivation: cannot serialize function in __structuredAttrs");
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`derivation: cannot serialize ${typeof v} to JSON`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const extractEnv = (attrs: NixAttrs, structuredAttrs: boolean, ignoreNulls: boolean): Map<string, string> => {
|
||||||
|
const specialAttrs = new Set([
|
||||||
|
"name",
|
||||||
|
"builder",
|
||||||
|
"system",
|
||||||
|
"args",
|
||||||
|
"outputs",
|
||||||
|
"__structuredAttrs",
|
||||||
|
"__ignoreNulls",
|
||||||
|
"__contentAddressed",
|
||||||
|
"impure",
|
||||||
|
]);
|
||||||
|
|
||||||
|
const env = new Map<string, string>();
|
||||||
|
|
||||||
|
if (structuredAttrs) {
|
||||||
|
const jsonAttrs: Record<string, any> = {};
|
||||||
|
for (const [key, value] of Object.entries(attrs)) {
|
||||||
|
if (!specialAttrs.has(key)) {
|
||||||
|
const forcedValue = force(value);
|
||||||
|
if (ignoreNulls && forcedValue === null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
jsonAttrs[key] = nixValueToJson(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
env.set("__json", JSON.stringify(jsonAttrs));
|
||||||
|
} else {
|
||||||
|
for (const [key, value] of Object.entries(attrs)) {
|
||||||
|
if (!specialAttrs.has(key)) {
|
||||||
|
const forcedValue = force(value);
|
||||||
|
if (ignoreNulls && forcedValue === null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
env.set(key, coerceToString(value, StringCoercionMode.ToString, false));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return env;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface FixedOutputInfo {
|
||||||
|
hash: string;
|
||||||
|
hashAlgo: string;
|
||||||
|
hashMode: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const extractFixedOutputInfo = (attrs: NixAttrs): FixedOutputInfo | null => {
|
||||||
|
if (!("outputHash" in attrs)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hash = forceString(attrs.outputHash);
|
||||||
|
const hashAlgo = "outputHashAlgo" in attrs ? forceString(attrs.outputHashAlgo) : "sha256";
|
||||||
|
const hashMode = "outputHashMode" in attrs ? forceString(attrs.outputHashMode) : "flat";
|
||||||
|
|
||||||
|
if (hashMode !== "flat" && hashMode !== "recursive") {
|
||||||
|
throw new Error(`derivation: invalid outputHashMode '${hashMode}' (must be 'flat' or 'recursive')`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { hash, hashAlgo, hashMode };
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateFixedOutputConstraints = (fixedOutput: FixedOutputInfo | null, outputs: string[]) => {
|
||||||
|
if (fixedOutput && (outputs.length !== 1 || outputs[0] !== "out")) {
|
||||||
|
throw new Error("derivation: fixed-output derivations must have exactly one 'out' output");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const derivationStrict = (args: NixValue): NixAttrs => {
|
||||||
|
const attrs = forceAttrs(args);
|
||||||
|
|
||||||
|
const drvName = validateName(attrs);
|
||||||
|
const builder = validateBuilder(attrs);
|
||||||
|
const platform = validateSystem(attrs);
|
||||||
|
|
||||||
|
const outputs = extractOutputs(attrs);
|
||||||
|
const fixedOutputInfo = extractFixedOutputInfo(attrs);
|
||||||
|
validateFixedOutputConstraints(fixedOutputInfo, outputs);
|
||||||
|
|
||||||
|
const structuredAttrs = "__structuredAttrs" in attrs ? force(attrs.__structuredAttrs) === true : false;
|
||||||
|
|
||||||
|
const ignoreNulls = "__ignoreNulls" in attrs ? force(attrs.__ignoreNulls) === true : false;
|
||||||
|
|
||||||
|
const drvArgs = extractArgs(attrs);
|
||||||
|
const env = extractEnv(attrs, structuredAttrs, ignoreNulls);
|
||||||
|
|
||||||
|
env.set("name", drvName);
|
||||||
|
env.set("builder", builder);
|
||||||
|
env.set("system", platform);
|
||||||
|
if (outputs.length > 1 || outputs[0] !== "out") {
|
||||||
|
env.set("outputs", outputs.join(" "));
|
||||||
|
}
|
||||||
|
|
||||||
|
let outputInfos: Map<string, OutputInfo>;
|
||||||
|
let drvPath: string;
|
||||||
|
|
||||||
|
if (fixedOutputInfo) {
|
||||||
|
const pathName = Deno.core.ops.op_output_path_name(drvName, "out");
|
||||||
|
const outPath = Deno.core.ops.op_make_fixed_output_path(
|
||||||
|
fixedOutputInfo.hashAlgo,
|
||||||
|
fixedOutputInfo.hash,
|
||||||
|
fixedOutputInfo.hashMode,
|
||||||
|
pathName,
|
||||||
|
);
|
||||||
|
|
||||||
|
const hashAlgoPrefix = fixedOutputInfo.hashMode === "recursive" ? "r:" : "";
|
||||||
|
outputInfos = new Map([
|
||||||
|
[
|
||||||
|
"out",
|
||||||
|
{
|
||||||
|
path: outPath,
|
||||||
|
hashAlgo: hashAlgoPrefix + fixedOutputInfo.hashAlgo,
|
||||||
|
hash: fixedOutputInfo.hash,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
env.set("out", outPath);
|
||||||
|
|
||||||
|
const finalDrv: DerivationData = {
|
||||||
|
name: drvName,
|
||||||
|
outputs: outputInfos,
|
||||||
|
inputDrvs: new Map(),
|
||||||
|
inputSrcs: new Set(),
|
||||||
|
platform,
|
||||||
|
builder,
|
||||||
|
args: drvArgs,
|
||||||
|
env,
|
||||||
|
};
|
||||||
|
const finalAterm = generateAterm(finalDrv);
|
||||||
|
const finalDrvHash = Deno.core.ops.op_sha256_hex(finalAterm);
|
||||||
|
drvPath = Deno.core.ops.op_make_store_path("text", finalDrvHash, `${drvName}.drv`);
|
||||||
|
} else {
|
||||||
|
const maskedOutputs = new Map<string, OutputInfo>(
|
||||||
|
outputs.map((o) => [
|
||||||
|
o,
|
||||||
|
{
|
||||||
|
path: "",
|
||||||
|
hashAlgo: "",
|
||||||
|
hash: "",
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
const maskedEnv = new Map(env);
|
||||||
|
for (const output of outputs) {
|
||||||
|
maskedEnv.set(output, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
const maskedDrv: DerivationData = {
|
||||||
|
name: drvName,
|
||||||
|
outputs: maskedOutputs,
|
||||||
|
inputDrvs: new Map(),
|
||||||
|
inputSrcs: new Set(),
|
||||||
|
platform,
|
||||||
|
builder,
|
||||||
|
args: drvArgs,
|
||||||
|
env: maskedEnv,
|
||||||
|
};
|
||||||
|
|
||||||
|
const maskedAterm = generateAterm(maskedDrv);
|
||||||
|
const drvModuloHash = Deno.core.ops.op_sha256_hex(maskedAterm);
|
||||||
|
|
||||||
|
outputInfos = new Map<string, OutputInfo>();
|
||||||
|
for (const outputName of outputs) {
|
||||||
|
const pathName = Deno.core.ops.op_output_path_name(drvName, outputName);
|
||||||
|
const outPath = Deno.core.ops.op_make_store_path(`output:${outputName}`, drvModuloHash, pathName);
|
||||||
|
outputInfos.set(outputName, {
|
||||||
|
path: outPath,
|
||||||
|
hashAlgo: "",
|
||||||
|
hash: "",
|
||||||
|
});
|
||||||
|
env.set(outputName, outPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
const finalDrv: DerivationData = {
|
||||||
|
...maskedDrv,
|
||||||
|
outputs: outputInfos,
|
||||||
|
env,
|
||||||
|
};
|
||||||
|
const finalAterm = generateAterm(finalDrv);
|
||||||
|
const finalDrvHash = Deno.core.ops.op_sha256_hex(finalAterm);
|
||||||
|
|
||||||
|
drvPath = Deno.core.ops.op_make_store_path("text", finalDrvHash, `${drvName}.drv`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result: NixAttrs = {
|
||||||
|
type: "derivation",
|
||||||
|
drvPath,
|
||||||
|
name: drvName,
|
||||||
|
builder,
|
||||||
|
system: platform,
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const [outputName, outputInfo] of outputInfos.entries()) {
|
||||||
|
result[outputName] = outputInfo.path;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (outputInfos.has("out")) {
|
||||||
|
result.outPath = outputInfos.get("out")!.path;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (drvArgs.length > 0) {
|
||||||
|
result.args = drvArgs;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!structuredAttrs) {
|
||||||
|
for (const [key, value] of env.entries()) {
|
||||||
|
if (!["name", "builder", "system", ...outputs].includes(key)) {
|
||||||
|
result[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const derivation = (args: NixValue): NixAttrs => {
|
||||||
|
return derivationStrict(args);
|
||||||
};
|
};
|
||||||
|
|||||||
71
nix-js/runtime-ts/src/derivation-helpers.ts
Normal file
71
nix-js/runtime-ts/src/derivation-helpers.ts
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
export interface OutputInfo {
|
||||||
|
path: string;
|
||||||
|
hashAlgo: string;
|
||||||
|
hash: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DerivationData {
|
||||||
|
name: string;
|
||||||
|
outputs: Map<string, OutputInfo>;
|
||||||
|
inputDrvs: Map<string, Set<string>>;
|
||||||
|
inputSrcs: Set<string>;
|
||||||
|
platform: string;
|
||||||
|
builder: string;
|
||||||
|
args: string[];
|
||||||
|
env: Map<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const escapeString = (s: string): string => {
|
||||||
|
let result = "";
|
||||||
|
for (const char of s) {
|
||||||
|
switch (char) {
|
||||||
|
case '"':
|
||||||
|
result += '\\"';
|
||||||
|
break;
|
||||||
|
case "\\":
|
||||||
|
result += "\\\\";
|
||||||
|
break;
|
||||||
|
case "\n":
|
||||||
|
result += "\\n";
|
||||||
|
break;
|
||||||
|
case "\r":
|
||||||
|
result += "\\r";
|
||||||
|
break;
|
||||||
|
case "\t":
|
||||||
|
result += "\\t";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
result += char;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return `"${result}"`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const quoteString = (s: string): string => `"${s}"`;
|
||||||
|
|
||||||
|
export const generateAterm = (drv: DerivationData): string => {
|
||||||
|
const outputEntries: string[] = [];
|
||||||
|
const sortedOutputs = Array.from(drv.outputs.entries()).sort();
|
||||||
|
for (const [name, info] of sortedOutputs) {
|
||||||
|
outputEntries.push(
|
||||||
|
`(${quoteString(name)},${quoteString(info.path)},${quoteString(info.hashAlgo)},${quoteString(info.hash)})`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const outputs = outputEntries.join(",");
|
||||||
|
|
||||||
|
const inputDrvEntries: string[] = [];
|
||||||
|
for (const [drvPath, outputs] of drv.inputDrvs) {
|
||||||
|
const outList = `[${Array.from(outputs).map(quoteString).join(",")}]`;
|
||||||
|
inputDrvEntries.push(`(${quoteString(drvPath)},${outList})`);
|
||||||
|
}
|
||||||
|
const inputDrvs = inputDrvEntries.join(",");
|
||||||
|
|
||||||
|
const inputSrcs = Array.from(drv.inputSrcs).map(quoteString).join(",");
|
||||||
|
|
||||||
|
const args = drv.args.map(escapeString).join(",");
|
||||||
|
const envs = Array.from(drv.env.entries())
|
||||||
|
.sort()
|
||||||
|
.map(([k, v]) => `(${escapeString(k)},${escapeString(v)})`);
|
||||||
|
|
||||||
|
return `Derive([${outputs}],[${inputDrvs}],[${inputSrcs}],${quoteString(drv.platform)},${quoteString(drv.builder)},[${args}],[${envs}])`;
|
||||||
|
};
|
||||||
9
nix-js/runtime-ts/src/types/global.d.ts
vendored
9
nix-js/runtime-ts/src/types/global.d.ts
vendored
@@ -9,6 +9,15 @@ declare global {
|
|||||||
function op_import(path: string): string;
|
function op_import(path: string): string;
|
||||||
function op_read_file(path: string): string;
|
function op_read_file(path: string): string;
|
||||||
function op_path_exists(path: string): boolean;
|
function op_path_exists(path: string): boolean;
|
||||||
|
function op_sha256_hex(data: string): string;
|
||||||
|
function op_make_store_path(ty: string, hash_hex: string, name: string): string;
|
||||||
|
function op_output_path_name(drv_name: string, output_name: string): string;
|
||||||
|
function op_make_fixed_output_path(
|
||||||
|
hash_algo: string,
|
||||||
|
hash: string,
|
||||||
|
hash_mode: string,
|
||||||
|
name: string,
|
||||||
|
): string;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ mod codegen;
|
|||||||
pub mod context;
|
pub mod context;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
pub mod ir;
|
pub mod ir;
|
||||||
|
mod nix_hash;
|
||||||
mod runtime;
|
mod runtime;
|
||||||
pub mod value;
|
pub mod value;
|
||||||
|
|
||||||
|
|||||||
123
nix-js/src/nix_hash.rs
Normal file
123
nix-js/src/nix_hash.rs
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
use sha2::{Digest, Sha256};
|
||||||
|
|
||||||
|
const NIX_BASE32_CHARS: &[u8; 32] = b"0123456789abcdfghijklmnpqrsvwxyz";
|
||||||
|
const STORE_DIR: &str = "/nix/store";
|
||||||
|
|
||||||
|
pub fn sha256_hex(data: &str) -> String {
|
||||||
|
let mut hasher = Sha256::new();
|
||||||
|
hasher.update(data.as_bytes());
|
||||||
|
hex::encode(hasher.finalize())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn compress_hash(hash: &[u8; 32], new_size: usize) -> Vec<u8> {
|
||||||
|
let mut result = vec![0u8; new_size];
|
||||||
|
for i in 0..32 {
|
||||||
|
result[i % new_size] ^= hash[i];
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn nix_base32_encode(bytes: &[u8]) -> String {
|
||||||
|
let len = (bytes.len() * 8 - 1) / 5 + 1;
|
||||||
|
let mut result = String::with_capacity(len);
|
||||||
|
|
||||||
|
for n in (0..len).rev() {
|
||||||
|
let b = n * 5;
|
||||||
|
let i = b / 8;
|
||||||
|
let j = b % 8;
|
||||||
|
|
||||||
|
let c = if i >= bytes.len() {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
let mut c = (bytes[i] as u16) >> j;
|
||||||
|
if j > 3 && i + 1 < bytes.len() {
|
||||||
|
c |= (bytes[i + 1] as u16) << (8 - j);
|
||||||
|
}
|
||||||
|
c
|
||||||
|
};
|
||||||
|
|
||||||
|
result.push(NIX_BASE32_CHARS[(c & 0x1f) as usize] as char);
|
||||||
|
}
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn make_store_path(ty: &str, hash_hex: &str, name: &str) -> String {
|
||||||
|
let s = format!("{}:sha256:{}:{}:{}", ty, hash_hex, STORE_DIR, name);
|
||||||
|
|
||||||
|
let mut hasher = Sha256::new();
|
||||||
|
hasher.update(s.as_bytes());
|
||||||
|
let hash: [u8; 32] = hasher.finalize().into();
|
||||||
|
|
||||||
|
let compressed = compress_hash(&hash, 20);
|
||||||
|
let encoded = nix_base32_encode(&compressed);
|
||||||
|
|
||||||
|
format!("{}/{}-{}", STORE_DIR, encoded, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn output_path_name(drv_name: &str, output_name: &str) -> String {
|
||||||
|
if output_name == "out" {
|
||||||
|
drv_name.to_string()
|
||||||
|
} else {
|
||||||
|
format!("{}-{}", drv_name, output_name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_nix_base32_encode() {
|
||||||
|
let bytes = [0xFF, 0xFF, 0xFF, 0xFF, 0xFF];
|
||||||
|
let encoded = nix_base32_encode(&bytes);
|
||||||
|
assert_eq!(encoded.len(), 8);
|
||||||
|
|
||||||
|
let bytes_zero = [0u8; 20];
|
||||||
|
let encoded_zero = nix_base32_encode(&bytes_zero);
|
||||||
|
assert_eq!(encoded_zero.len(), 32);
|
||||||
|
assert!(encoded_zero.chars().all(|c| c == '0'));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_compress_hash() {
|
||||||
|
let hash = [0u8; 32];
|
||||||
|
let compressed = compress_hash(&hash, 20);
|
||||||
|
assert_eq!(compressed.len(), 20);
|
||||||
|
assert!(compressed.iter().all(|&b| b == 0));
|
||||||
|
|
||||||
|
let hash_ones = [0xFF; 32];
|
||||||
|
let compressed_ones = compress_hash(&hash_ones, 20);
|
||||||
|
assert_eq!(compressed_ones.len(), 20);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sha256_hex() {
|
||||||
|
let data = "hello world";
|
||||||
|
let hash = sha256_hex(data);
|
||||||
|
assert_eq!(hash.len(), 64);
|
||||||
|
assert_eq!(
|
||||||
|
hash,
|
||||||
|
"b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_output_path_name() {
|
||||||
|
assert_eq!(output_path_name("hello", "out"), "hello");
|
||||||
|
assert_eq!(output_path_name("hello", "dev"), "hello-dev");
|
||||||
|
assert_eq!(output_path_name("hello", "doc"), "hello-doc");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_make_store_path() {
|
||||||
|
let path = make_store_path("output:out", "abc123", "hello");
|
||||||
|
assert!(path.starts_with("/nix/store/"));
|
||||||
|
assert!(path.ends_with("-hello"));
|
||||||
|
|
||||||
|
let hash_parts: Vec<&str> = path.split('/').collect();
|
||||||
|
assert_eq!(hash_parts.len(), 4);
|
||||||
|
let name_part = hash_parts[3];
|
||||||
|
assert!(name_part.contains('-'));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,6 +28,10 @@ fn runtime_extension<Ctx: RuntimeCtx>() -> Extension {
|
|||||||
op_read_file(),
|
op_read_file(),
|
||||||
op_path_exists(),
|
op_path_exists(),
|
||||||
op_resolve_path::<Ctx>(),
|
op_resolve_path::<Ctx>(),
|
||||||
|
op_sha256_hex(),
|
||||||
|
op_make_store_path(),
|
||||||
|
op_output_path_name(),
|
||||||
|
op_make_fixed_output_path(),
|
||||||
];
|
];
|
||||||
|
|
||||||
Extension {
|
Extension {
|
||||||
@@ -127,6 +131,51 @@ fn op_resolve_path<Ctx: RuntimeCtx>(
|
|||||||
.map_err(|e| format!("Failed to resolve path {}: {}", path, e))?)
|
.map_err(|e| format!("Failed to resolve path {}: {}", path, e))?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[deno_core::op2]
|
||||||
|
#[string]
|
||||||
|
fn op_sha256_hex(#[string] data: String) -> String {
|
||||||
|
crate::nix_hash::sha256_hex(&data)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[deno_core::op2]
|
||||||
|
#[string]
|
||||||
|
fn op_make_store_path(
|
||||||
|
#[string] ty: String,
|
||||||
|
#[string] hash_hex: String,
|
||||||
|
#[string] name: String,
|
||||||
|
) -> String {
|
||||||
|
crate::nix_hash::make_store_path(&ty, &hash_hex, &name)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[deno_core::op2]
|
||||||
|
#[string]
|
||||||
|
fn op_output_path_name(#[string] drv_name: String, #[string] output_name: String) -> String {
|
||||||
|
crate::nix_hash::output_path_name(&drv_name, &output_name)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[deno_core::op2]
|
||||||
|
#[string]
|
||||||
|
fn op_make_fixed_output_path(
|
||||||
|
#[string] hash_algo: String,
|
||||||
|
#[string] hash: String,
|
||||||
|
#[string] hash_mode: String,
|
||||||
|
#[string] name: String,
|
||||||
|
) -> String {
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
|
|
||||||
|
if hash_algo == "sha256" && hash_mode == "recursive" {
|
||||||
|
crate::nix_hash::make_store_path("source", &hash, &name)
|
||||||
|
} else {
|
||||||
|
let prefix = if hash_mode == "recursive" { "r:" } else { "" };
|
||||||
|
let inner_input = format!("fixed:out:{}{}:{}:", prefix, hash_algo, hash);
|
||||||
|
let mut hasher = Sha256::new();
|
||||||
|
hasher.update(inner_input.as_bytes());
|
||||||
|
let inner_hash = hex::encode(hasher.finalize());
|
||||||
|
|
||||||
|
crate::nix_hash::make_store_path("output:out", &inner_hash, &name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) struct Runtime<Ctx: RuntimeCtx> {
|
pub(crate) struct Runtime<Ctx: RuntimeCtx> {
|
||||||
js_runtime: JsRuntime,
|
js_runtime: JsRuntime,
|
||||||
is_thunk_symbol: v8::Global<v8::Symbol>,
|
is_thunk_symbol: v8::Global<v8::Symbol>,
|
||||||
|
|||||||
@@ -74,6 +74,18 @@ pub struct AttrSet {
|
|||||||
data: BTreeMap<Symbol, Value>,
|
data: BTreeMap<Symbol, Value>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl AttrSet {
|
||||||
|
/// Gets a value by key (string or Symbol).
|
||||||
|
pub fn get(&self, key: impl Into<Symbol>) -> Option<&Value> {
|
||||||
|
self.data.get(&key.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks if a key exists in the attribute set.
|
||||||
|
pub fn contains_key(&self, key: impl Into<Symbol>) -> bool {
|
||||||
|
self.data.contains_key(&key.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Debug for AttrSet {
|
impl Debug for AttrSet {
|
||||||
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
|
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
|
||||||
use Value::*;
|
use Value::*;
|
||||||
@@ -117,6 +129,18 @@ pub struct List {
|
|||||||
data: Vec<Value>,
|
data: Vec<Value>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl List {
|
||||||
|
/// Returns the number of elements in the list.
|
||||||
|
pub fn len(&self) -> usize {
|
||||||
|
self.data.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if the list is empty.
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
self.data.is_empty()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Display for List {
|
impl Display for List {
|
||||||
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
|
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
|
||||||
write!(f, "[ ")?;
|
write!(f, "[ ")?;
|
||||||
|
|||||||
743
nix-js/tests/derivation.rs
Normal file
743
nix-js/tests/derivation.rs
Normal file
@@ -0,0 +1,743 @@
|
|||||||
|
use nix_js::context::Context;
|
||||||
|
use nix_js::value::Value;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn derivation_minimal() {
|
||||||
|
let mut ctx = Context::new().unwrap();
|
||||||
|
let result = ctx
|
||||||
|
.eval_code(
|
||||||
|
r#"derivation { name = "hello"; builder = "/bin/sh"; system = "x86_64-linux"; }"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Value::AttrSet(attrs) => {
|
||||||
|
assert_eq!(attrs.get("type"), Some(&Value::String("derivation".into())));
|
||||||
|
assert_eq!(attrs.get("name"), Some(&Value::String("hello".into())));
|
||||||
|
assert_eq!(attrs.get("builder"), Some(&Value::String("/bin/sh".into())));
|
||||||
|
assert_eq!(
|
||||||
|
attrs.get("system"),
|
||||||
|
Some(&Value::String("x86_64-linux".into()))
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(attrs.contains_key("outPath"));
|
||||||
|
assert!(attrs.contains_key("drvPath"));
|
||||||
|
|
||||||
|
if let Some(Value::String(path)) = attrs.get("outPath") {
|
||||||
|
assert_eq!(path, "/nix/store/pnwh4xsfs4j508bs9iw6bpkyc4zw6ryx-hello");
|
||||||
|
} else {
|
||||||
|
panic!("outPath should be a string");
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(Value::String(path)) = attrs.get("drvPath") {
|
||||||
|
assert_eq!(
|
||||||
|
path,
|
||||||
|
"/nix/store/x0sj6ynccvc1a8kxr8fifnlf7qlxw6hd-hello.drv"
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
panic!("drvPath should be a string");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => panic!("Expected AttrSet, got {:?}", result),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn derivation_with_args() {
|
||||||
|
let mut ctx = Context::new().unwrap();
|
||||||
|
let result = ctx
|
||||||
|
.eval_code(
|
||||||
|
r#"derivation {
|
||||||
|
name = "test";
|
||||||
|
builder = "/bin/sh";
|
||||||
|
system = "x86_64-linux";
|
||||||
|
args = ["-c" "echo hello"];
|
||||||
|
}"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Value::AttrSet(attrs) => {
|
||||||
|
assert!(attrs.contains_key("args"));
|
||||||
|
if let Some(Value::List(args)) = attrs.get("args") {
|
||||||
|
assert_eq!(args.len(), 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => panic!("Expected AttrSet"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn derivation_to_string() {
|
||||||
|
let mut ctx = Context::new().unwrap();
|
||||||
|
let result = ctx
|
||||||
|
.eval_code(
|
||||||
|
r#"toString (derivation { name = "foo"; builder = "/bin/sh"; system = "x86_64-linux"; })"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Value::String(s) => assert_eq!(s, "/nix/store/xpcvxsx5sw4rbq666blz6sxqlmsqphmr-foo"),
|
||||||
|
_ => panic!("Expected String, got {:?}", result),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn derivation_missing_name() {
|
||||||
|
let mut ctx = Context::new().unwrap();
|
||||||
|
let result = ctx.eval_code(r#"derivation { builder = "/bin/sh"; system = "x86_64-linux"; }"#);
|
||||||
|
|
||||||
|
assert!(result.is_err());
|
||||||
|
let err_msg = result.unwrap_err().to_string();
|
||||||
|
assert!(err_msg.contains("missing required attribute 'name'"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn derivation_invalid_name_with_drv_suffix() {
|
||||||
|
let mut ctx = Context::new().unwrap();
|
||||||
|
let result = ctx.eval_code(
|
||||||
|
r#"derivation { name = "foo.drv"; builder = "/bin/sh"; system = "x86_64-linux"; }"#,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(result.is_err());
|
||||||
|
let err_msg = result.unwrap_err().to_string();
|
||||||
|
assert!(err_msg.contains("cannot end with .drv"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn derivation_missing_builder() {
|
||||||
|
let mut ctx = Context::new().unwrap();
|
||||||
|
let result = ctx.eval_code(r#"derivation { name = "test"; system = "x86_64-linux"; }"#);
|
||||||
|
|
||||||
|
assert!(result.is_err());
|
||||||
|
let err_msg = result.unwrap_err().to_string();
|
||||||
|
assert!(err_msg.contains("missing required attribute 'builder'"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn derivation_missing_system() {
|
||||||
|
let mut ctx = Context::new().unwrap();
|
||||||
|
let result = ctx.eval_code(r#"derivation { name = "test"; builder = "/bin/sh"; }"#);
|
||||||
|
|
||||||
|
assert!(result.is_err());
|
||||||
|
let err_msg = result.unwrap_err().to_string();
|
||||||
|
assert!(err_msg.contains("missing required attribute 'system'"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn derivation_with_env_vars() {
|
||||||
|
let mut ctx = Context::new().unwrap();
|
||||||
|
let result = ctx
|
||||||
|
.eval_code(
|
||||||
|
r#"derivation {
|
||||||
|
name = "test";
|
||||||
|
builder = "/bin/sh";
|
||||||
|
system = "x86_64-linux";
|
||||||
|
MY_VAR = "hello";
|
||||||
|
ANOTHER = "world";
|
||||||
|
}"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Value::AttrSet(attrs) => {
|
||||||
|
assert_eq!(attrs.get("MY_VAR"), Some(&Value::String("hello".into())));
|
||||||
|
assert_eq!(attrs.get("ANOTHER"), Some(&Value::String("world".into())));
|
||||||
|
}
|
||||||
|
_ => panic!("Expected AttrSet"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn derivation_strict() {
|
||||||
|
let mut ctx = Context::new().unwrap();
|
||||||
|
let result = ctx
|
||||||
|
.eval_code(
|
||||||
|
r#"builtins.derivationStrict { name = "test"; builder = "/bin/sh"; system = "x86_64-linux"; }"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Value::AttrSet(attrs) => {
|
||||||
|
assert_eq!(attrs.get("type"), Some(&Value::String("derivation".into())));
|
||||||
|
assert!(attrs.contains_key("drvPath"));
|
||||||
|
assert!(attrs.contains_key("outPath"));
|
||||||
|
}
|
||||||
|
_ => panic!("Expected AttrSet"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn derivation_deterministic_paths() {
|
||||||
|
let mut ctx = Context::new().unwrap();
|
||||||
|
|
||||||
|
let expr = r#"derivation { name = "hello"; builder = "/bin/sh"; system = "x86_64-linux"; }"#;
|
||||||
|
|
||||||
|
let result1 = ctx.eval_code(expr).unwrap();
|
||||||
|
let result2 = ctx.eval_code(expr).unwrap();
|
||||||
|
|
||||||
|
match (result1, result2) {
|
||||||
|
(Value::AttrSet(attrs1), Value::AttrSet(attrs2)) => {
|
||||||
|
assert_eq!(attrs1.get("drvPath"), attrs2.get("drvPath"));
|
||||||
|
assert_eq!(attrs1.get("outPath"), attrs2.get("outPath"));
|
||||||
|
}
|
||||||
|
_ => panic!("Expected AttrSet"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn derivation_escaping_in_aterm() {
|
||||||
|
let mut ctx = Context::new().unwrap();
|
||||||
|
let result = ctx
|
||||||
|
.eval_code(
|
||||||
|
r#"derivation {
|
||||||
|
name = "test";
|
||||||
|
builder = "/bin/sh";
|
||||||
|
system = "x86_64-linux";
|
||||||
|
args = ["-c" "echo \"hello\nworld\""];
|
||||||
|
}"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Value::AttrSet(attrs) => {
|
||||||
|
assert!(attrs.contains_key("drvPath"));
|
||||||
|
assert!(attrs.contains_key("outPath"));
|
||||||
|
}
|
||||||
|
_ => panic!("Expected AttrSet"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn multi_output_two_outputs() {
|
||||||
|
let mut ctx = Context::new().unwrap();
|
||||||
|
let result = ctx
|
||||||
|
.eval_code(
|
||||||
|
r#"derivation {
|
||||||
|
name = "multi";
|
||||||
|
builder = "/bin/sh";
|
||||||
|
system = "x86_64-linux";
|
||||||
|
outputs = ["out" "dev"];
|
||||||
|
}"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Value::AttrSet(attrs) => {
|
||||||
|
assert!(attrs.contains_key("out"));
|
||||||
|
assert!(attrs.contains_key("dev"));
|
||||||
|
assert!(attrs.contains_key("outPath"));
|
||||||
|
assert!(attrs.contains_key("drvPath"));
|
||||||
|
|
||||||
|
// Verify exact paths match CppNix
|
||||||
|
if let Some(Value::String(drv_path)) = attrs.get("drvPath") {
|
||||||
|
assert_eq!(
|
||||||
|
drv_path,
|
||||||
|
"/nix/store/vmyjryfipkn9ss3ya23hk8p3m58l6dsl-multi.drv"
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
panic!("drvPath should be a string");
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(Value::String(out_path)) = attrs.get("out") {
|
||||||
|
assert_eq!(
|
||||||
|
out_path,
|
||||||
|
"/nix/store/a3d95yg9d215c54n0ybr4npmpnj29229-multi"
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
panic!("out should be a string");
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(Value::String(dev_path)) = attrs.get("dev") {
|
||||||
|
assert_eq!(
|
||||||
|
dev_path,
|
||||||
|
"/nix/store/hq3b99lz71gwfq6x8lqwg14hf929q0d2-multi-dev"
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
panic!("dev should be a string");
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(Value::String(out_path)) = attrs.get("outPath") {
|
||||||
|
assert_eq!(
|
||||||
|
out_path,
|
||||||
|
"/nix/store/a3d95yg9d215c54n0ybr4npmpnj29229-multi"
|
||||||
|
);
|
||||||
|
assert_eq!(attrs.get("out"), Some(&Value::String(out_path.clone())));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => panic!("Expected AttrSet"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn multi_output_three_outputs() {
|
||||||
|
let mut ctx = Context::new().unwrap();
|
||||||
|
let result = ctx
|
||||||
|
.eval_code(
|
||||||
|
r#"derivation {
|
||||||
|
name = "three";
|
||||||
|
builder = "/bin/sh";
|
||||||
|
system = "x86_64-linux";
|
||||||
|
outputs = ["out" "dev" "doc"];
|
||||||
|
}"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Value::AttrSet(attrs) => {
|
||||||
|
assert!(attrs.contains_key("out"));
|
||||||
|
assert!(attrs.contains_key("dev"));
|
||||||
|
assert!(attrs.contains_key("doc"));
|
||||||
|
|
||||||
|
// Verify exact paths match CppNix
|
||||||
|
if let Some(Value::String(drv_path)) = attrs.get("drvPath") {
|
||||||
|
assert_eq!(
|
||||||
|
drv_path,
|
||||||
|
"/nix/store/w08rpwvs5j9yxvdx5f5yg0p5i3ncazdx-three.drv"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(Value::String(out_path)) = attrs.get("out") {
|
||||||
|
assert_eq!(
|
||||||
|
out_path,
|
||||||
|
"/nix/store/i479clih5pb6bn2d2b758sbaylvbs2cl-three"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if let Some(Value::String(dev_path)) = attrs.get("dev") {
|
||||||
|
assert_eq!(
|
||||||
|
dev_path,
|
||||||
|
"/nix/store/gg8v395vci5xg1i9grc8ifh5xagw5f2j-three-dev"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if let Some(Value::String(doc_path)) = attrs.get("doc") {
|
||||||
|
assert_eq!(
|
||||||
|
doc_path,
|
||||||
|
"/nix/store/p2avgz16qx5k2jgnq3ch04k154xj1ac0-three-doc"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => panic!("Expected AttrSet"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn multi_output_backward_compat() {
|
||||||
|
let mut ctx = Context::new().unwrap();
|
||||||
|
let result = ctx
|
||||||
|
.eval_code(
|
||||||
|
r#"derivation {
|
||||||
|
name = "compat";
|
||||||
|
builder = "/bin/sh";
|
||||||
|
system = "x86_64-linux";
|
||||||
|
outputs = ["out"];
|
||||||
|
}"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Value::AttrSet(attrs) => {
|
||||||
|
assert!(attrs.contains_key("outPath"));
|
||||||
|
assert!(attrs.contains_key("out"));
|
||||||
|
|
||||||
|
if let (Some(Value::String(out_path)), Some(Value::String(out))) =
|
||||||
|
(attrs.get("outPath"), attrs.get("out"))
|
||||||
|
{
|
||||||
|
assert_eq!(out_path, out);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => panic!("Expected AttrSet"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn multi_output_deterministic() {
|
||||||
|
let mut ctx = Context::new().unwrap();
|
||||||
|
let result1 = ctx
|
||||||
|
.eval_code(
|
||||||
|
r#"derivation {
|
||||||
|
name = "determ";
|
||||||
|
builder = "/bin/sh";
|
||||||
|
system = "x86_64-linux";
|
||||||
|
outputs = ["out" "dev"];
|
||||||
|
}"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let result2 = ctx
|
||||||
|
.eval_code(
|
||||||
|
r#"derivation {
|
||||||
|
name = "determ";
|
||||||
|
builder = "/bin/sh";
|
||||||
|
system = "x86_64-linux";
|
||||||
|
outputs = ["out" "dev"];
|
||||||
|
}"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(result1, result2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fixed_output_sha256_flat() {
|
||||||
|
let mut ctx = Context::new().unwrap();
|
||||||
|
let result = ctx
|
||||||
|
.eval_code(
|
||||||
|
r#"derivation {
|
||||||
|
name = "fixed";
|
||||||
|
builder = "/bin/sh";
|
||||||
|
system = "x86_64-linux";
|
||||||
|
outputHash = "0000000000000000000000000000000000000000000000000000000000000000";
|
||||||
|
outputHashAlgo = "sha256";
|
||||||
|
outputHashMode = "flat";
|
||||||
|
}"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Value::AttrSet(attrs) => {
|
||||||
|
assert!(attrs.contains_key("outPath"));
|
||||||
|
assert!(attrs.contains_key("drvPath"));
|
||||||
|
|
||||||
|
// Verify exact paths match CppNix
|
||||||
|
if let Some(Value::String(out_path)) = attrs.get("outPath") {
|
||||||
|
assert_eq!(
|
||||||
|
out_path,
|
||||||
|
"/nix/store/ap9h69qwrm5060ldi96axyklh3pr3yjn-fixed"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(Value::String(drv_path)) = attrs.get("drvPath") {
|
||||||
|
assert_eq!(
|
||||||
|
drv_path,
|
||||||
|
"/nix/store/kj9gsfz5cngc38n1xlf6ljlgvnsfg0cj-fixed.drv"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => panic!("Expected AttrSet"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fixed_output_default_algo() {
|
||||||
|
let mut ctx = Context::new().unwrap();
|
||||||
|
let result = ctx
|
||||||
|
.eval_code(
|
||||||
|
r#"derivation {
|
||||||
|
name = "default";
|
||||||
|
builder = "/bin/sh";
|
||||||
|
system = "x86_64-linux";
|
||||||
|
outputHash = "0000000000000000000000000000000000000000000000000000000000000000";
|
||||||
|
}"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Value::AttrSet(attrs) => {
|
||||||
|
assert!(attrs.contains_key("outPath"));
|
||||||
|
// Verify it defaults to sha256 (same as explicitly specifying it)
|
||||||
|
if let Some(Value::String(out_path)) = attrs.get("outPath") {
|
||||||
|
assert!(out_path.contains("/nix/store/"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => panic!("Expected AttrSet"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fixed_output_recursive_mode() {
|
||||||
|
let mut ctx = Context::new().unwrap();
|
||||||
|
let result = ctx
|
||||||
|
.eval_code(
|
||||||
|
r#"derivation {
|
||||||
|
name = "recursive";
|
||||||
|
builder = "/bin/sh";
|
||||||
|
system = "x86_64-linux";
|
||||||
|
outputHash = "1111111111111111111111111111111111111111111111111111111111111111";
|
||||||
|
outputHashAlgo = "sha256";
|
||||||
|
outputHashMode = "recursive";
|
||||||
|
}"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Value::AttrSet(attrs) => {
|
||||||
|
assert!(attrs.contains_key("outPath"));
|
||||||
|
assert!(attrs.contains_key("drvPath"));
|
||||||
|
|
||||||
|
// Verify exact path matches CppNix
|
||||||
|
if let Some(Value::String(out_path)) = attrs.get("outPath") {
|
||||||
|
assert_eq!(
|
||||||
|
out_path,
|
||||||
|
"/nix/store/qyal5s16hfwxhz5zwpf8h8yv2bs84z56-recursive"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => panic!("Expected AttrSet"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fixed_output_rejects_multi_output() {
|
||||||
|
let mut ctx = Context::new().unwrap();
|
||||||
|
let result = ctx.eval_code(
|
||||||
|
r#"derivation {
|
||||||
|
name = "invalid";
|
||||||
|
builder = "/bin/sh";
|
||||||
|
system = "x86_64-linux";
|
||||||
|
outputHash = "0000000000000000000000000000000000000000000000000000000000000000";
|
||||||
|
outputs = ["out" "dev"];
|
||||||
|
}"#,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(result.is_err());
|
||||||
|
let err_msg = result.unwrap_err().to_string();
|
||||||
|
assert!(err_msg.contains("fixed-output") && err_msg.contains("one"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fixed_output_invalid_hash_mode() {
|
||||||
|
let mut ctx = Context::new().unwrap();
|
||||||
|
let result = ctx.eval_code(
|
||||||
|
r#"derivation {
|
||||||
|
name = "invalid";
|
||||||
|
builder = "/bin/sh";
|
||||||
|
system = "x86_64-linux";
|
||||||
|
outputHash = "0000000000000000000000000000000000000000000000000000000000000000";
|
||||||
|
outputHashMode = "invalid";
|
||||||
|
}"#,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(result.is_err());
|
||||||
|
let err_msg = result.unwrap_err().to_string();
|
||||||
|
assert!(err_msg.contains("outputHashMode") && err_msg.contains("invalid"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn structured_attrs_basic() {
|
||||||
|
let mut ctx = Context::new().unwrap();
|
||||||
|
let result = ctx
|
||||||
|
.eval_code(
|
||||||
|
r#"derivation {
|
||||||
|
name = "struct";
|
||||||
|
builder = "/bin/sh";
|
||||||
|
system = "x86_64-linux";
|
||||||
|
__structuredAttrs = true;
|
||||||
|
foo = "bar";
|
||||||
|
count = 42;
|
||||||
|
enabled = true;
|
||||||
|
}"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Value::AttrSet(attrs) => {
|
||||||
|
assert!(attrs.contains_key("drvPath"));
|
||||||
|
assert!(attrs.contains_key("outPath"));
|
||||||
|
assert!(!attrs.contains_key("foo"));
|
||||||
|
assert!(!attrs.contains_key("count"));
|
||||||
|
}
|
||||||
|
_ => panic!("Expected AttrSet"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn structured_attrs_nested() {
|
||||||
|
let mut ctx = Context::new().unwrap();
|
||||||
|
let result = ctx
|
||||||
|
.eval_code(
|
||||||
|
r#"derivation {
|
||||||
|
name = "nested";
|
||||||
|
builder = "/bin/sh";
|
||||||
|
system = "x86_64-linux";
|
||||||
|
__structuredAttrs = true;
|
||||||
|
data = { x = 1; y = [2 3]; };
|
||||||
|
}"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Value::AttrSet(attrs) => {
|
||||||
|
assert!(attrs.contains_key("drvPath"));
|
||||||
|
assert!(!attrs.contains_key("data"));
|
||||||
|
}
|
||||||
|
_ => panic!("Expected AttrSet"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn structured_attrs_rejects_functions() {
|
||||||
|
let mut ctx = Context::new().unwrap();
|
||||||
|
let result = ctx.eval_code(
|
||||||
|
r#"derivation {
|
||||||
|
name = "invalid";
|
||||||
|
builder = "/bin/sh";
|
||||||
|
system = "x86_64-linux";
|
||||||
|
__structuredAttrs = true;
|
||||||
|
func = x: x + 1;
|
||||||
|
}"#,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(result.is_err());
|
||||||
|
let err_msg = result.unwrap_err().to_string();
|
||||||
|
assert!(err_msg.contains("function") && err_msg.contains("serialize"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn structured_attrs_false() {
|
||||||
|
let mut ctx = Context::new().unwrap();
|
||||||
|
let result = ctx
|
||||||
|
.eval_code(
|
||||||
|
r#"derivation {
|
||||||
|
name = "normal";
|
||||||
|
builder = "/bin/sh";
|
||||||
|
system = "x86_64-linux";
|
||||||
|
__structuredAttrs = false;
|
||||||
|
foo = "bar";
|
||||||
|
}"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Value::AttrSet(attrs) => {
|
||||||
|
assert!(attrs.contains_key("foo"));
|
||||||
|
if let Some(Value::String(val)) = attrs.get("foo") {
|
||||||
|
assert_eq!(val, "bar");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => panic!("Expected AttrSet"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ignore_nulls_true() {
|
||||||
|
let mut ctx = Context::new().unwrap();
|
||||||
|
let result = ctx
|
||||||
|
.eval_code(
|
||||||
|
r#"derivation {
|
||||||
|
name = "ignore";
|
||||||
|
builder = "/bin/sh";
|
||||||
|
system = "x86_64-linux";
|
||||||
|
__ignoreNulls = true;
|
||||||
|
foo = "bar";
|
||||||
|
nullValue = null;
|
||||||
|
}"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Value::AttrSet(attrs) => {
|
||||||
|
assert!(attrs.contains_key("foo"));
|
||||||
|
assert!(!attrs.contains_key("nullValue"));
|
||||||
|
}
|
||||||
|
_ => panic!("Expected AttrSet"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ignore_nulls_false() {
|
||||||
|
let mut ctx = Context::new().unwrap();
|
||||||
|
let result = ctx
|
||||||
|
.eval_code(
|
||||||
|
r#"derivation {
|
||||||
|
name = "keep";
|
||||||
|
builder = "/bin/sh";
|
||||||
|
system = "x86_64-linux";
|
||||||
|
__ignoreNulls = false;
|
||||||
|
nullValue = null;
|
||||||
|
}"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Value::AttrSet(attrs) => {
|
||||||
|
assert!(attrs.contains_key("nullValue"));
|
||||||
|
if let Some(Value::String(val)) = attrs.get("nullValue") {
|
||||||
|
assert_eq!(val, "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => panic!("Expected AttrSet"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ignore_nulls_with_structured_attrs() {
|
||||||
|
let mut ctx = Context::new().unwrap();
|
||||||
|
let result = ctx
|
||||||
|
.eval_code(
|
||||||
|
r#"derivation {
|
||||||
|
name = "combined";
|
||||||
|
builder = "/bin/sh";
|
||||||
|
system = "x86_64-linux";
|
||||||
|
__structuredAttrs = true;
|
||||||
|
__ignoreNulls = true;
|
||||||
|
foo = "bar";
|
||||||
|
nullValue = null;
|
||||||
|
}"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Value::AttrSet(attrs) => {
|
||||||
|
assert!(attrs.contains_key("drvPath"));
|
||||||
|
assert!(!attrs.contains_key("foo"));
|
||||||
|
assert!(!attrs.contains_key("nullValue"));
|
||||||
|
}
|
||||||
|
_ => panic!("Expected AttrSet"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn all_features_combined() {
|
||||||
|
let mut ctx = Context::new().unwrap();
|
||||||
|
let result = ctx
|
||||||
|
.eval_code(
|
||||||
|
r#"derivation {
|
||||||
|
name = "all";
|
||||||
|
builder = "/bin/sh";
|
||||||
|
system = "x86_64-linux";
|
||||||
|
outputs = ["out" "dev"];
|
||||||
|
__structuredAttrs = true;
|
||||||
|
__ignoreNulls = true;
|
||||||
|
data = { x = 1; };
|
||||||
|
nullValue = null;
|
||||||
|
}"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Value::AttrSet(attrs) => {
|
||||||
|
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"));
|
||||||
|
}
|
||||||
|
_ => panic!("Expected AttrSet"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fixed_output_with_structured_attrs() {
|
||||||
|
let mut ctx = Context::new().unwrap();
|
||||||
|
let result = ctx
|
||||||
|
.eval_code(
|
||||||
|
r#"derivation {
|
||||||
|
name = "fixstruct";
|
||||||
|
builder = "/bin/sh";
|
||||||
|
system = "x86_64-linux";
|
||||||
|
outputHash = "abc123";
|
||||||
|
__structuredAttrs = true;
|
||||||
|
data = { key = "value"; };
|
||||||
|
}"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Value::AttrSet(attrs) => {
|
||||||
|
assert!(attrs.contains_key("outPath"));
|
||||||
|
assert!(attrs.contains_key("drvPath"));
|
||||||
|
assert!(!attrs.contains_key("data"));
|
||||||
|
}
|
||||||
|
_ => panic!("Expected AttrSet"),
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user