fix: drvDeep
This commit is contained in:
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -1956,6 +1956,7 @@ dependencies = [
|
|||||||
"nix-compat",
|
"nix-compat",
|
||||||
"nix-js-macros",
|
"nix-js-macros",
|
||||||
"nix-nar",
|
"nix-nar",
|
||||||
|
"num_enum",
|
||||||
"petgraph",
|
"petgraph",
|
||||||
"regex",
|
"regex",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
|
|||||||
@@ -32,6 +32,7 @@
|
|||||||
hyperfine
|
hyperfine
|
||||||
just
|
just
|
||||||
samply
|
samply
|
||||||
|
jq
|
||||||
|
|
||||||
nodejs
|
nodejs
|
||||||
nodePackages.npm
|
nodePackages.npm
|
||||||
|
|||||||
@@ -6,13 +6,13 @@ build = "build.rs"
|
|||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["daemon"]
|
default = ["daemon"]
|
||||||
daemon = ["dep:tokio", "dep:nix-compat"]
|
daemon = ["dep:tokio"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
mimalloc = "0.1"
|
mimalloc = "0.1"
|
||||||
|
|
||||||
tokio = { version = "1.41", features = ["rt-multi-thread", "sync", "net", "io-util"], optional = true }
|
tokio = { version = "1.41", features = ["rt-multi-thread", "sync", "net", "io-util"], optional = true }
|
||||||
nix-compat = { git = "https://git.snix.dev/snix/snix.git", version = "0.1.0", features = ["wire", "async"], optional = true }
|
nix-compat = { git = "https://git.snix.dev/snix/snix.git", version = "0.1.0", features = ["wire", "async"] }
|
||||||
|
|
||||||
# REPL
|
# REPL
|
||||||
anyhow = "1.0"
|
anyhow = "1.0"
|
||||||
@@ -63,6 +63,7 @@ rowan = "0.15"
|
|||||||
|
|
||||||
nix-js-macros = { path = "../nix-js-macros" }
|
nix-js-macros = { path = "../nix-js-macros" }
|
||||||
ere = "0.2.4"
|
ere = "0.2.4"
|
||||||
|
num_enum = "0.7.5"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
criterion = { version = "0.5", features = ["html_reports"] }
|
criterion = { version = "0.5", features = ["html_reports"] }
|
||||||
|
|||||||
@@ -167,11 +167,16 @@ export const coerceToString = (
|
|||||||
if ("outPath" in v) {
|
if ("outPath" in v) {
|
||||||
// Recursively coerce the outPath value
|
// Recursively coerce the outPath value
|
||||||
const outPath = coerceToString(v.outPath, mode, copyToStore, outContext);
|
const outPath = coerceToString(v.outPath, mode, copyToStore, outContext);
|
||||||
if ("type" in v && v.type === "derivation" && "drvPath" in v) {
|
if ("type" in v && v.type === "derivation" && "drvPath" in v && outContext) {
|
||||||
const drvPath = force(v.drvPath);
|
const drvPathValue = force(v.drvPath);
|
||||||
if (typeof drvPath === "string" && outContext) {
|
const drvPathStr = isStringWithContext(drvPathValue)
|
||||||
|
? drvPathValue.value
|
||||||
|
: typeof drvPathValue === "string"
|
||||||
|
? drvPathValue
|
||||||
|
: null;
|
||||||
|
if (drvPathStr) {
|
||||||
const outputName = "outputName" in v ? String(force(v.outputName)) : "out";
|
const outputName = "outputName" in v ? String(force(v.outputName)) : "out";
|
||||||
addBuiltContext(outContext, drvPath, outputName);
|
addBuiltContext(outContext, drvPathStr, outputName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return outPath;
|
return outPath;
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
import type { NixValue, NixAttrs } from "../types";
|
import type { NixValue, NixAttrs } from "../types";
|
||||||
import { forceStringValue, forceList } from "../type-assert";
|
import { forceStringValue, forceList } from "../type-assert";
|
||||||
import { force, createThunk } from "../thunk";
|
import { force, createThunk } from "../thunk";
|
||||||
import { type DerivationData, type OutputInfo, generateAterm } from "../derivation-helpers";
|
import {
|
||||||
|
type DerivationData,
|
||||||
|
type OutputInfo,
|
||||||
|
generateAterm,
|
||||||
|
generateAtermModulo,
|
||||||
|
} from "../derivation-helpers";
|
||||||
import { coerceToString, StringCoercionMode } from "./conversion";
|
import { coerceToString, StringCoercionMode } from "./conversion";
|
||||||
import {
|
import {
|
||||||
type NixStringContext,
|
type NixStringContext,
|
||||||
@@ -14,6 +19,8 @@ import {
|
|||||||
import { nixValueToJson } from "../conversion";
|
import { nixValueToJson } from "../conversion";
|
||||||
import { isNixPath } from "../types";
|
import { isNixPath } from "../types";
|
||||||
|
|
||||||
|
const drvHashCache = new Map<string, string>();
|
||||||
|
|
||||||
const forceAttrs = (value: NixValue): NixAttrs => {
|
const forceAttrs = (value: NixValue): NixAttrs => {
|
||||||
const forced = force(value);
|
const forced = force(value);
|
||||||
if (
|
if (
|
||||||
@@ -56,13 +63,7 @@ const validateSystem = (attrs: NixAttrs): string => {
|
|||||||
return forceStringValue(attrs.system);
|
return forceStringValue(attrs.system);
|
||||||
};
|
};
|
||||||
|
|
||||||
const extractOutputs = (attrs: NixAttrs): string[] => {
|
const validateOutputs = (outputs: string[]): void => {
|
||||||
if (!("outputs" in attrs)) {
|
|
||||||
return ["out"];
|
|
||||||
}
|
|
||||||
const outputsList = forceList(attrs.outputs);
|
|
||||||
const outputs = outputsList.map((o) => forceStringValue(o));
|
|
||||||
|
|
||||||
if (outputs.length === 0) {
|
if (outputs.length === 0) {
|
||||||
throw new Error("derivation: outputs list cannot be empty");
|
throw new Error("derivation: outputs list cannot be empty");
|
||||||
}
|
}
|
||||||
@@ -78,7 +79,34 @@ const extractOutputs = (attrs: NixAttrs): string[] => {
|
|||||||
}
|
}
|
||||||
seen.add(output);
|
seen.add(output);
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const extractOutputs = (attrs: NixAttrs, structuredAttrs: boolean): string[] => {
|
||||||
|
if (!("outputs" in attrs)) {
|
||||||
|
return ["out"];
|
||||||
|
}
|
||||||
|
|
||||||
|
let outputs: string[];
|
||||||
|
if (structuredAttrs) {
|
||||||
|
const outputsList = forceList(attrs.outputs);
|
||||||
|
outputs = outputsList.map((o) => forceStringValue(o));
|
||||||
|
} else {
|
||||||
|
const outputsStr = coerceToString(attrs.outputs, StringCoercionMode.ToString, false, new Set());
|
||||||
|
outputs = outputsStr.split(/\s+/).filter((s) => s.length > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
validateOutputs(outputs);
|
||||||
|
return outputs;
|
||||||
|
};
|
||||||
|
|
||||||
|
const extractOutputsForWrapper = (attrs: NixAttrs): string[] => {
|
||||||
|
if (!("outputs" in attrs)) {
|
||||||
|
return ["out"];
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: trace context?
|
||||||
|
const outputs = forceList(attrs.outputs).map(forceStringValue);
|
||||||
|
validateOutputs(outputs);
|
||||||
return outputs;
|
return outputs;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -94,7 +122,7 @@ const structuredAttrsExcludedKeys = new Set([
|
|||||||
"__structuredAttrs",
|
"__structuredAttrs",
|
||||||
"__ignoreNulls",
|
"__ignoreNulls",
|
||||||
"__contentAddressed",
|
"__contentAddressed",
|
||||||
"impure",
|
"__impure",
|
||||||
"args",
|
"args",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -107,7 +135,7 @@ const specialAttrs = new Set([
|
|||||||
"__structuredAttrs",
|
"__structuredAttrs",
|
||||||
"__ignoreNulls",
|
"__ignoreNulls",
|
||||||
"__contentAddressed",
|
"__contentAddressed",
|
||||||
"impure",
|
"__impure",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const sortedJsonStringify = (obj: Record<string, any>): string => {
|
const sortedJsonStringify = (obj: Record<string, any>): string => {
|
||||||
@@ -143,42 +171,42 @@ const extractEnv = (
|
|||||||
console.warn(
|
console.warn(
|
||||||
`In a derivation named '${drvName}', 'structuredAttrs' disables the effect of ` +
|
`In a derivation named '${drvName}', 'structuredAttrs' disables the effect of ` +
|
||||||
`the derivation attribute 'allowedReferences'; use ` +
|
`the derivation attribute 'allowedReferences'; use ` +
|
||||||
`'outputChecks.<output>.allowedReferences' instead`
|
`'outputChecks.<output>.allowedReferences' instead`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (key === "allowedRequisites") {
|
if (key === "allowedRequisites") {
|
||||||
console.warn(
|
console.warn(
|
||||||
`In a derivation named '${drvName}', 'structuredAttrs' disables the effect of ` +
|
`In a derivation named '${drvName}', 'structuredAttrs' disables the effect of ` +
|
||||||
`the derivation attribute 'allowedRequisites'; use ` +
|
`the derivation attribute 'allowedRequisites'; use ` +
|
||||||
`'outputChecks.<output>.allowedRequisites' instead`
|
`'outputChecks.<output>.allowedRequisites' instead`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (key === "disallowedReferences") {
|
if (key === "disallowedReferences") {
|
||||||
console.warn(
|
console.warn(
|
||||||
`In a derivation named '${drvName}', 'structuredAttrs' disables the effect of ` +
|
`In a derivation named '${drvName}', 'structuredAttrs' disables the effect of ` +
|
||||||
`the derivation attribute 'disallowedReferences'; use ` +
|
`the derivation attribute 'disallowedReferences'; use ` +
|
||||||
`'outputChecks.<output>.disallowedReferences' instead`
|
`'outputChecks.<output>.disallowedReferences' instead`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (key === "disallowedRequisites") {
|
if (key === "disallowedRequisites") {
|
||||||
console.warn(
|
console.warn(
|
||||||
`In a derivation named '${drvName}', 'structuredAttrs' disables the effect of ` +
|
`In a derivation named '${drvName}', 'structuredAttrs' disables the effect of ` +
|
||||||
`the derivation attribute 'disallowedRequisites'; use ` +
|
`the derivation attribute 'disallowedRequisites'; use ` +
|
||||||
`'outputChecks.<output>.disallowedRequisites' instead`
|
`'outputChecks.<output>.disallowedRequisites' instead`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (key === "maxSize") {
|
if (key === "maxSize") {
|
||||||
console.warn(
|
console.warn(
|
||||||
`In a derivation named '${drvName}', 'structuredAttrs' disables the effect of ` +
|
`In a derivation named '${drvName}', 'structuredAttrs' disables the effect of ` +
|
||||||
`the derivation attribute 'maxSize'; use ` +
|
`the derivation attribute 'maxSize'; use ` +
|
||||||
`'outputChecks.<output>.maxSize' instead`
|
`'outputChecks.<output>.maxSize' instead`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (key === "maxClosureSize") {
|
if (key === "maxClosureSize") {
|
||||||
console.warn(
|
console.warn(
|
||||||
`In a derivation named '${drvName}', 'structuredAttrs' disables the effect of ` +
|
`In a derivation named '${drvName}', 'structuredAttrs' disables the effect of ` +
|
||||||
`the derivation attribute 'maxClosureSize'; use ` +
|
`the derivation attribute 'maxClosureSize'; use ` +
|
||||||
`'outputChecks.<output>.maxClosureSize' instead`
|
`'outputChecks.<output>.maxClosureSize' instead`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -213,8 +241,9 @@ const extractFixedOutputInfo = (attrs: NixAttrs, ignoreNulls: boolean): FixedOut
|
|||||||
if (ignoreNulls && hashValue === null) {
|
if (ignoreNulls && hashValue === null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const hash = forceStringValue(attrs.outputHash);
|
const hashRaw = forceStringValue(attrs.outputHash);
|
||||||
|
|
||||||
|
// FIXME: default value?
|
||||||
let hashAlgo = "sha256";
|
let hashAlgo = "sha256";
|
||||||
if ("outputHashAlgo" in attrs) {
|
if ("outputHashAlgo" in attrs) {
|
||||||
const algoValue = force(attrs.outputHashAlgo);
|
const algoValue = force(attrs.outputHashAlgo);
|
||||||
@@ -235,7 +264,9 @@ const extractFixedOutputInfo = (attrs: NixAttrs, ignoreNulls: boolean): FixedOut
|
|||||||
throw new Error(`derivation: invalid outputHashMode '${hashMode}' (must be 'flat' or 'recursive')`);
|
throw new Error(`derivation: invalid outputHashMode '${hashMode}' (must be 'flat' or 'recursive')`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return { hash, hashAlgo, hashMode };
|
const parsed = Deno.core.ops.op_parse_hash(hashRaw, hashAlgo);
|
||||||
|
|
||||||
|
return { hash: parsed.hex, hashAlgo: parsed.algo, hashMode };
|
||||||
};
|
};
|
||||||
|
|
||||||
const validateFixedOutputConstraints = (fixedOutput: FixedOutputInfo | null, outputs: string[]) => {
|
const validateFixedOutputConstraints = (fixedOutput: FixedOutputInfo | null, outputs: string[]) => {
|
||||||
@@ -255,7 +286,7 @@ export const derivationStrict = (args: NixValue): NixAttrs => {
|
|||||||
const structuredAttrs = "__structuredAttrs" in attrs ? force(attrs.__structuredAttrs) === true : false;
|
const structuredAttrs = "__structuredAttrs" in attrs ? force(attrs.__structuredAttrs) === true : false;
|
||||||
const ignoreNulls = "__ignoreNulls" in attrs ? force(attrs.__ignoreNulls) === true : false;
|
const ignoreNulls = "__ignoreNulls" in attrs ? force(attrs.__ignoreNulls) === true : false;
|
||||||
|
|
||||||
const outputs = extractOutputs(attrs);
|
const outputs = extractOutputs(attrs, structuredAttrs);
|
||||||
const fixedOutputInfo = extractFixedOutputInfo(attrs, ignoreNulls);
|
const fixedOutputInfo = extractFixedOutputInfo(attrs, ignoreNulls);
|
||||||
validateFixedOutputConstraints(fixedOutputInfo, outputs);
|
validateFixedOutputConstraints(fixedOutputInfo, outputs);
|
||||||
|
|
||||||
@@ -263,7 +294,7 @@ export const derivationStrict = (args: NixValue): NixAttrs => {
|
|||||||
throw new Error("ca derivations are not supported");
|
throw new Error("ca derivations are not supported");
|
||||||
}
|
}
|
||||||
|
|
||||||
if ("impure" in attrs && force(attrs.impure) === true) {
|
if ("__impure" in attrs && force(attrs.__impure) === true) {
|
||||||
throw new Error("impure derivations are not supported");
|
throw new Error("impure derivations are not supported");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -328,8 +359,11 @@ export const derivationStrict = (args: NixValue): NixAttrs => {
|
|||||||
env,
|
env,
|
||||||
};
|
};
|
||||||
const finalAterm = generateAterm(finalDrv);
|
const finalAterm = generateAterm(finalDrv);
|
||||||
const finalDrvHash = Deno.core.ops.op_sha256_hex(finalAterm);
|
drvPath = Deno.core.ops.op_write_derivation(drvName, finalAterm, collectDrvReferences());
|
||||||
drvPath = Deno.core.ops.op_make_text_store_path(finalDrvHash, `${drvName}.drv`, collectDrvReferences());
|
|
||||||
|
const fixedHashFingerprint = `fixed:out:${hashAlgoPrefix}${fixedOutputInfo.hashAlgo}:${fixedOutputInfo.hash}:${outPath}`;
|
||||||
|
const fixedModuloHash = Deno.core.ops.op_sha256_hex(fixedHashFingerprint);
|
||||||
|
drvHashCache.set(drvPath, fixedModuloHash);
|
||||||
} else {
|
} else {
|
||||||
const maskedOutputs = new Map<string, OutputInfo>(
|
const maskedOutputs = new Map<string, OutputInfo>(
|
||||||
outputs.map((o) => [
|
outputs.map((o) => [
|
||||||
@@ -357,7 +391,16 @@ export const derivationStrict = (args: NixValue): NixAttrs => {
|
|||||||
env: maskedEnv,
|
env: maskedEnv,
|
||||||
};
|
};
|
||||||
|
|
||||||
const maskedAterm = generateAterm(maskedDrv);
|
const inputDrvHashes = new Map<string, string>();
|
||||||
|
for (const [drvPath, outputNames] of inputDrvs) {
|
||||||
|
const cachedHash = drvHashCache.get(drvPath);
|
||||||
|
if (!cachedHash) {
|
||||||
|
throw new Error(`Missing modulo hash for input derivation: ${drvPath}`);
|
||||||
|
}
|
||||||
|
inputDrvHashes.set(cachedHash, Array.from(outputNames).join(","));
|
||||||
|
}
|
||||||
|
|
||||||
|
const maskedAterm = generateAtermModulo(maskedDrv, inputDrvHashes);
|
||||||
const drvModuloHash = Deno.core.ops.op_sha256_hex(maskedAterm);
|
const drvModuloHash = Deno.core.ops.op_sha256_hex(maskedAterm);
|
||||||
|
|
||||||
outputInfos = new Map<string, OutputInfo>();
|
outputInfos = new Map<string, OutputInfo>();
|
||||||
@@ -378,9 +421,11 @@ export const derivationStrict = (args: NixValue): NixAttrs => {
|
|||||||
env,
|
env,
|
||||||
};
|
};
|
||||||
const finalAterm = generateAterm(finalDrv);
|
const finalAterm = generateAterm(finalDrv);
|
||||||
const finalDrvHash = Deno.core.ops.op_sha256_hex(finalAterm);
|
drvPath = Deno.core.ops.op_write_derivation(drvName, finalAterm, collectDrvReferences());
|
||||||
|
|
||||||
drvPath = Deno.core.ops.op_make_text_store_path(finalDrvHash, `${drvName}.drv`, collectDrvReferences());
|
const finalAtermModulo = generateAtermModulo(finalDrv, inputDrvHashes);
|
||||||
|
const cachedModuloHash = Deno.core.ops.op_sha256_hex(finalAtermModulo);
|
||||||
|
drvHashCache.set(drvPath, cachedModuloHash);
|
||||||
}
|
}
|
||||||
|
|
||||||
const result: NixAttrs = {};
|
const result: NixAttrs = {};
|
||||||
@@ -401,7 +446,7 @@ export const derivationStrict = (args: NixValue): NixAttrs => {
|
|||||||
export const derivation = (args: NixValue): NixAttrs => {
|
export const derivation = (args: NixValue): NixAttrs => {
|
||||||
const attrs = forceAttrs(args);
|
const attrs = forceAttrs(args);
|
||||||
|
|
||||||
const outputs: string[] = extractOutputs(attrs);
|
const outputs: string[] = extractOutputsForWrapper(attrs);
|
||||||
|
|
||||||
const strictThunk = createThunk(() => derivationStrict(args), "derivationStrict");
|
const strictThunk = createThunk(() => derivationStrict(args), "derivationStrict");
|
||||||
|
|
||||||
@@ -426,10 +471,7 @@ export const derivation = (args: NixValue): NixAttrs => {
|
|||||||
`output_${outputName}`,
|
`output_${outputName}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
commonAttrs.all = createThunk(
|
commonAttrs.all = createThunk(() => outputsList.map((o) => o.value), "all_outputs");
|
||||||
() => outputsList.map((o) => o.value),
|
|
||||||
"all_outputs",
|
|
||||||
);
|
|
||||||
commonAttrs.drvAttrs = attrs;
|
commonAttrs.drvAttrs = attrs;
|
||||||
|
|
||||||
for (const { value: outputObj } of outputsList) {
|
for (const { value: outputObj } of outputsList) {
|
||||||
@@ -439,13 +481,9 @@ export const derivation = (args: NixValue): NixAttrs => {
|
|||||||
`output_${outputName}`,
|
`output_${outputName}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
outputObj.all = createThunk(
|
outputObj.all = createThunk(() => outputsList.map((o) => o.value), "all_outputs");
|
||||||
() => outputsList.map((o) => o.value),
|
|
||||||
"all_outputs",
|
|
||||||
);
|
|
||||||
outputObj.drvAttrs = attrs;
|
outputObj.drvAttrs = attrs;
|
||||||
}
|
}
|
||||||
|
|
||||||
return outputsList[0].value;
|
return outputsList[0].value;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -18,12 +18,12 @@ export const seq =
|
|||||||
export const deepSeq =
|
export const deepSeq =
|
||||||
(e1: NixValue) =>
|
(e1: NixValue) =>
|
||||||
(e2: NixValue): NixValue => {
|
(e2: NixValue): NixValue => {
|
||||||
const seen: Set<NixValue> = new Set;
|
const seen: Set<NixValue> = new Set();
|
||||||
const recurse = (e: NixValue) => {
|
const recurse = (e: NixValue) => {
|
||||||
if (!seen.has(e)) {
|
if (!seen.has(e)) {
|
||||||
seen.add(e);
|
seen.add(e);
|
||||||
} else {
|
} else {
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
const forced = force(e);
|
const forced = force(e);
|
||||||
if (Array.isArray(forced)) {
|
if (Array.isArray(forced)) {
|
||||||
@@ -35,7 +35,7 @@ export const deepSeq =
|
|||||||
recurse(val);
|
recurse(val);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
recurse(e1);
|
recurse(e1);
|
||||||
return e2;
|
return e2;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -266,7 +266,7 @@ export const builtins: any = {
|
|||||||
storeDir: "INVALID_PATH",
|
storeDir: "INVALID_PATH",
|
||||||
|
|
||||||
__traceCaller: (e: NixValue) => {
|
__traceCaller: (e: NixValue) => {
|
||||||
console.log(`traceCaller: ${getTos()}`)
|
console.log(`traceCaller: ${getTos()}`);
|
||||||
return e
|
return e;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,7 +3,14 @@
|
|||||||
* Implemented via Rust ops exposed through deno_core
|
* Implemented via Rust ops exposed through deno_core
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { forceAttrs, forceBool, forceFunction, forceList, forceStringNoCtx, forceStringValue } from "../type-assert";
|
import {
|
||||||
|
forceAttrs,
|
||||||
|
forceBool,
|
||||||
|
forceFunction,
|
||||||
|
forceList,
|
||||||
|
forceStringNoCtx,
|
||||||
|
forceStringValue,
|
||||||
|
} from "../type-assert";
|
||||||
import type { NixValue, NixAttrs, NixPath } from "../types";
|
import type { NixValue, NixAttrs, NixPath } from "../types";
|
||||||
import { isNixPath, IS_PATH, CatchableError } from "../types";
|
import { isNixPath, IS_PATH, CatchableError } from "../types";
|
||||||
import { force } from "../thunk";
|
import { force } from "../thunk";
|
||||||
@@ -488,9 +495,8 @@ export const findFile =
|
|||||||
}
|
}
|
||||||
|
|
||||||
const resolvedPath = Deno.core.ops.op_resolve_path(pathVal, "");
|
const resolvedPath = Deno.core.ops.op_resolve_path(pathVal, "");
|
||||||
const candidatePath = suffix.length > 0
|
const candidatePath =
|
||||||
? Deno.core.ops.op_resolve_path(suffix, resolvedPath)
|
suffix.length > 0 ? Deno.core.ops.op_resolve_path(suffix, resolvedPath) : resolvedPath;
|
||||||
: resolvedPath;
|
|
||||||
|
|
||||||
if (Deno.core.ops.op_path_exists(candidatePath)) {
|
if (Deno.core.ops.op_path_exists(candidatePath)) {
|
||||||
return { [IS_PATH]: true, value: candidatePath };
|
return { [IS_PATH]: true, value: candidatePath };
|
||||||
|
|||||||
@@ -5,7 +5,14 @@
|
|||||||
import { force } from "../thunk";
|
import { force } from "../thunk";
|
||||||
import { CatchableError, ATTR_POSITIONS } from "../types";
|
import { CatchableError, ATTR_POSITIONS } from "../types";
|
||||||
import type { NixAttrs, NixBool, NixStrictValue, NixValue } from "../types";
|
import type { NixAttrs, NixBool, NixStrictValue, NixValue } from "../types";
|
||||||
import { forceList, forceAttrs, forceFunction, forceStringValue, forceString, forceStringNoCtx } from "../type-assert";
|
import {
|
||||||
|
forceList,
|
||||||
|
forceAttrs,
|
||||||
|
forceFunction,
|
||||||
|
forceStringValue,
|
||||||
|
forceString,
|
||||||
|
forceStringNoCtx,
|
||||||
|
} from "../type-assert";
|
||||||
import * as context from "./context";
|
import * as context from "./context";
|
||||||
import { compareValues } from "../operators";
|
import { compareValues } from "../operators";
|
||||||
import { isBool, isFloat, isInt, isList, isString, typeOf } from "./type-check";
|
import { isBool, isFloat, isInt, isList, isString, typeOf } from "./type-check";
|
||||||
@@ -242,7 +249,7 @@ export const parseDrvName = (s: NixValue): NixAttrs => {
|
|||||||
let name = fullName;
|
let name = fullName;
|
||||||
let version = "";
|
let version = "";
|
||||||
for (let i = 0; i < fullName.length; ++i) {
|
for (let i = 0; i < fullName.length; ++i) {
|
||||||
if (fullName[i] === '-' && i + 1 < fullName.length && !/[a-zA-Z]/.test(fullName[i + 1])) {
|
if (fullName[i] === "-" && i + 1 < fullName.length && !/[a-zA-Z]/.test(fullName[i + 1])) {
|
||||||
name = fullName.substring(0, i);
|
name = fullName.substring(0, i);
|
||||||
version = fullName.substring(i + 1);
|
version = fullName.substring(i + 1);
|
||||||
break;
|
break;
|
||||||
@@ -250,8 +257,8 @@ export const parseDrvName = (s: NixValue): NixAttrs => {
|
|||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
name,
|
name,
|
||||||
version
|
version,
|
||||||
}
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const parseFlakeName = (s: NixValue): never => {
|
export const parseFlakeName = (s: NixValue): never => {
|
||||||
|
|||||||
@@ -43,9 +43,12 @@ export const escapeString = (s: string): string => {
|
|||||||
|
|
||||||
const quoteString = (s: string): string => `"${s}"`;
|
const quoteString = (s: string): string => `"${s}"`;
|
||||||
|
|
||||||
|
const cmpByKey = <T>(a: [string, T], b: [string, T]): number =>
|
||||||
|
a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0;
|
||||||
|
|
||||||
export const generateAterm = (drv: DerivationData): string => {
|
export const generateAterm = (drv: DerivationData): string => {
|
||||||
const outputEntries: string[] = [];
|
const outputEntries: string[] = [];
|
||||||
const sortedOutputs = Array.from(drv.outputs.entries()).sort();
|
const sortedOutputs = Array.from(drv.outputs.entries()).sort(cmpByKey);
|
||||||
for (const [name, info] of sortedOutputs) {
|
for (const [name, info] of sortedOutputs) {
|
||||||
outputEntries.push(
|
outputEntries.push(
|
||||||
`(${quoteString(name)},${quoteString(info.path)},${quoteString(info.hashAlgo)},${quoteString(info.hash)})`,
|
`(${quoteString(name)},${quoteString(info.path)},${quoteString(info.hashAlgo)},${quoteString(info.hash)})`,
|
||||||
@@ -54,18 +57,51 @@ export const generateAterm = (drv: DerivationData): string => {
|
|||||||
const outputs = outputEntries.join(",");
|
const outputs = outputEntries.join(",");
|
||||||
|
|
||||||
const inputDrvEntries: string[] = [];
|
const inputDrvEntries: string[] = [];
|
||||||
for (const [drvPath, outputs] of drv.inputDrvs) {
|
const sortedInputDrvs = Array.from(drv.inputDrvs.entries()).sort(cmpByKey);
|
||||||
const outList = `[${Array.from(outputs).map(quoteString).join(",")}]`;
|
for (const [drvPath, outputs] of sortedInputDrvs) {
|
||||||
|
const sortedOuts = Array.from(outputs).sort();
|
||||||
|
const outList = `[${sortedOuts.map(quoteString).join(",")}]`;
|
||||||
inputDrvEntries.push(`(${quoteString(drvPath)},${outList})`);
|
inputDrvEntries.push(`(${quoteString(drvPath)},${outList})`);
|
||||||
}
|
}
|
||||||
const inputDrvs = inputDrvEntries.join(",");
|
const inputDrvs = inputDrvEntries.join(",");
|
||||||
|
|
||||||
const inputSrcs = Array.from(drv.inputSrcs).map(quoteString).join(",");
|
const sortedInputSrcs = Array.from(drv.inputSrcs).sort();
|
||||||
|
const inputSrcs = sortedInputSrcs.map(quoteString).join(",");
|
||||||
|
|
||||||
const args = drv.args.map(escapeString).join(",");
|
const args = drv.args.map(escapeString).join(",");
|
||||||
const envs = Array.from(drv.env.entries())
|
const envs = Array.from(drv.env.entries())
|
||||||
.sort()
|
.sort(cmpByKey)
|
||||||
.map(([k, v]) => `(${escapeString(k)},${escapeString(v)})`);
|
.map(([k, v]) => `(${escapeString(k)},${escapeString(v)})`);
|
||||||
|
|
||||||
return `Derive([${outputs}],[${inputDrvs}],[${inputSrcs}],${quoteString(drv.platform)},${quoteString(drv.builder)},[${args}],[${envs}])`;
|
return `Derive([${outputs}],[${inputDrvs}],[${inputSrcs}],${quoteString(drv.platform)},${escapeString(drv.builder)},[${args}],[${envs}])`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const generateAtermModulo = (drv: DerivationData, inputDrvHashes: Map<string, string>): string => {
|
||||||
|
const outputEntries: string[] = [];
|
||||||
|
const sortedOutputs = Array.from(drv.outputs.entries()).sort(cmpByKey);
|
||||||
|
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[] = [];
|
||||||
|
const sortedInputDrvHashes = Array.from(inputDrvHashes.entries()).sort(cmpByKey);
|
||||||
|
for (const [drvHash, outputs] of sortedInputDrvHashes) {
|
||||||
|
const sortedOuts = outputs.split(",").sort();
|
||||||
|
const outList = `[${sortedOuts.map(quoteString).join(",")}]`;
|
||||||
|
inputDrvEntries.push(`(${quoteString(drvHash)},${outList})`);
|
||||||
|
}
|
||||||
|
const inputDrvs = inputDrvEntries.join(",");
|
||||||
|
|
||||||
|
const sortedInputSrcs = Array.from(drv.inputSrcs).sort();
|
||||||
|
const inputSrcs = sortedInputSrcs.map(quoteString).join(",");
|
||||||
|
|
||||||
|
const args = drv.args.map(escapeString).join(",");
|
||||||
|
const envs = Array.from(drv.env.entries())
|
||||||
|
.sort(cmpByKey)
|
||||||
|
.map(([k, v]) => `(${escapeString(k)},${escapeString(v)})`);
|
||||||
|
|
||||||
|
return `Derive([${outputs}],[${inputDrvs}],[${inputSrcs}],${quoteString(drv.platform)},${escapeString(drv.builder)},[${args}],[${envs}])`;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ export const getTos = (): string => {
|
|||||||
const tos = callStack[callStack.length - 2];
|
const tos = callStack[callStack.length - 2];
|
||||||
const { file, line, column } = Deno.core.ops.op_decode_span(tos.span);
|
const { file, line, column } = Deno.core.ops.op_decode_span(tos.span);
|
||||||
return `${tos.message} at ${file}:${line}:${column}`;
|
return `${tos.message} at ${file}:${line}:${column}`;
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Push an error context onto the stack
|
* Push an error context onto the stack
|
||||||
|
|||||||
@@ -4,7 +4,16 @@
|
|||||||
* All functionality is exported via the global `Nix` object
|
* All functionality is exported via the global `Nix` object
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { createThunk, force, isThunk, IS_THUNK, DEBUG_THUNKS, forceDeep, IS_CYCLE, forceShallow } from "./thunk";
|
import {
|
||||||
|
createThunk,
|
||||||
|
force,
|
||||||
|
isThunk,
|
||||||
|
IS_THUNK,
|
||||||
|
DEBUG_THUNKS,
|
||||||
|
forceDeep,
|
||||||
|
IS_CYCLE,
|
||||||
|
forceShallow,
|
||||||
|
} from "./thunk";
|
||||||
import {
|
import {
|
||||||
select,
|
select,
|
||||||
selectWithDefault,
|
selectWithDefault,
|
||||||
|
|||||||
@@ -183,7 +183,9 @@ export const parseContextToInfoMap = (context: NixStringContext): Map<string, Pa
|
|||||||
*
|
*
|
||||||
* Context type handling:
|
* Context type handling:
|
||||||
* - Opaque: Added to inputSrcs
|
* - Opaque: Added to inputSrcs
|
||||||
* - DrvDeep: Added to inputSrcs (entire derivation + all outputs)
|
* - DrvDeep: Computes FS closure (like Nix's computeFSClosure) - adds all paths
|
||||||
|
* in the dependency graph to inputSrcs, and all derivations with their
|
||||||
|
* outputs to inputDrvs
|
||||||
* - Built: Added to inputDrvs with specific output name
|
* - Built: Added to inputDrvs with specific output name
|
||||||
*/
|
*/
|
||||||
export const extractInputDrvsAndSrcs = (
|
export const extractInputDrvsAndSrcs = (
|
||||||
@@ -198,9 +200,28 @@ export const extractInputDrvsAndSrcs = (
|
|||||||
case "opaque":
|
case "opaque":
|
||||||
inputSrcs.add(elem.path);
|
inputSrcs.add(elem.path);
|
||||||
break;
|
break;
|
||||||
case "drvDeep":
|
case "drvDeep": {
|
||||||
inputSrcs.add(elem.drvPath);
|
const closure: {
|
||||||
|
input_drvs: [string, string[]][];
|
||||||
|
input_srcs: string[];
|
||||||
|
} = Deno.core.ops.op_compute_fs_closure(elem.drvPath);
|
||||||
|
|
||||||
|
for (const src of closure.input_srcs) {
|
||||||
|
inputSrcs.add(src);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [drvPath, outputs] of closure.input_drvs) {
|
||||||
|
let existingOutputs = inputDrvs.get(drvPath);
|
||||||
|
if (!existingOutputs) {
|
||||||
|
existingOutputs = new Set<string>();
|
||||||
|
inputDrvs.set(drvPath, existingOutputs);
|
||||||
|
}
|
||||||
|
for (const output of outputs) {
|
||||||
|
existingOutputs.add(output);
|
||||||
|
}
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
case "built": {
|
case "built": {
|
||||||
let outputs = inputDrvs.get(elem.drvPath);
|
let outputs = inputDrvs.get(elem.drvPath);
|
||||||
if (!outputs) {
|
if (!outputs) {
|
||||||
|
|||||||
@@ -197,9 +197,9 @@ export const forceShallow = (value: NixValue): NixStrictValue => {
|
|||||||
return forced.map((item) => {
|
return forced.map((item) => {
|
||||||
const forcedItem = force(item);
|
const forcedItem = force(item);
|
||||||
if (typeof forcedItem === "object" && forcedItem === forced) {
|
if (typeof forcedItem === "object" && forcedItem === forced) {
|
||||||
return CYCLE_MARKER
|
return CYCLE_MARKER;
|
||||||
} else {
|
} else {
|
||||||
return forcedItem
|
return forcedItem;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,12 +39,7 @@ export const forceFunction = (value: NixValue): NixFunction => {
|
|||||||
if (isFunction(forced)) {
|
if (isFunction(forced)) {
|
||||||
return forced;
|
return forced;
|
||||||
}
|
}
|
||||||
if (
|
if (typeof forced === "object" && !Array.isArray(forced) && forced !== null && "__functor" in forced) {
|
||||||
typeof forced === "object" &&
|
|
||||||
!Array.isArray(forced) &&
|
|
||||||
forced !== null &&
|
|
||||||
"__functor" in forced
|
|
||||||
) {
|
|
||||||
const functorSet = forced as NixAttrs;
|
const functorSet = forced as NixAttrs;
|
||||||
const functor = forceFunction(functorSet.__functor);
|
const functor = forceFunction(functorSet.__functor);
|
||||||
return (arg: NixValue) => forceFunction(functor(functorSet))(arg);
|
return (arg: NixValue) => forceFunction(functor(functorSet))(arg);
|
||||||
@@ -100,10 +95,10 @@ export const forceStringNoCtx = (value: NixValue): string => {
|
|||||||
return forced;
|
return forced;
|
||||||
}
|
}
|
||||||
if (isStringWithContext(forced)) {
|
if (isStringWithContext(forced)) {
|
||||||
throw new TypeError(`the string '${forced.value}' is not allowed to refer to a store path`)
|
throw new TypeError(`the string '${forced.value}' is not allowed to refer to a store path`);
|
||||||
}
|
}
|
||||||
throw new TypeError(`Expected string, got ${typeOf(forced)}`);
|
throw new TypeError(`Expected string, got ${typeOf(forced)}`);
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Force a value and assert it's a boolean
|
* Force a value and assert it's a boolean
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ const ATTR_POSITIONS = Symbol("attrPositions");
|
|||||||
export const mkAttrsWithPos = (
|
export const mkAttrsWithPos = (
|
||||||
attrs: NixAttrs,
|
attrs: NixAttrs,
|
||||||
positions: Record<string, string>,
|
positions: Record<string, string>,
|
||||||
dyns?: { dynKeys: NixValue[]; dynVals: NixValue[]; dynSpans: string[] }
|
dyns?: { dynKeys: NixValue[]; dynVals: NixValue[]; dynSpans: string[] },
|
||||||
): NixAttrs => {
|
): NixAttrs => {
|
||||||
if (dyns) {
|
if (dyns) {
|
||||||
const len = dyns.dynKeys.length;
|
const len = dyns.dynKeys.length;
|
||||||
|
|||||||
13
nix-js/runtime-ts/src/types/global.d.ts
vendored
13
nix-js/runtime-ts/src/types/global.d.ts
vendored
@@ -44,10 +44,15 @@ declare global {
|
|||||||
function op_path_exists(path: string): boolean;
|
function op_path_exists(path: string): boolean;
|
||||||
function op_sha256_hex(data: string): string;
|
function op_sha256_hex(data: string): string;
|
||||||
function op_make_placeholder(output: string): string;
|
function op_make_placeholder(output: string): string;
|
||||||
function op_decode_span(span: string): { file: string | null; line: number | null; column: number | null };
|
function op_decode_span(span: string): {
|
||||||
|
file: string | null;
|
||||||
|
line: number | null;
|
||||||
|
column: number | null;
|
||||||
|
};
|
||||||
function op_make_store_path(ty: string, hash_hex: string, name: string): string;
|
function op_make_store_path(ty: string, hash_hex: string, name: string): string;
|
||||||
function op_make_text_store_path(hash_hex: string, name: string, references: string[]): string;
|
function op_make_text_store_path(hash_hex: string, name: string, references: string[]): string;
|
||||||
function op_output_path_name(drv_name: string, output_name: string): string;
|
function op_output_path_name(drv_name: string, output_name: string): string;
|
||||||
|
function op_parse_hash(hash_str: string, algo: string): { hex: string; algo: string };
|
||||||
function op_make_fixed_output_path(
|
function op_make_fixed_output_path(
|
||||||
hash_algo: string,
|
hash_algo: string,
|
||||||
hash: string,
|
hash: string,
|
||||||
@@ -84,6 +89,12 @@ declare global {
|
|||||||
): string;
|
): string;
|
||||||
function op_store_path(path: string): string;
|
function op_store_path(path: string): string;
|
||||||
function op_to_file(name: string, contents: string, references: string[]): string;
|
function op_to_file(name: string, contents: string, references: string[]): string;
|
||||||
|
function op_write_derivation(drv_name: string, aterm: string, references: string[]): string;
|
||||||
|
function op_read_derivation_outputs(drv_path: string): string[];
|
||||||
|
function op_compute_fs_closure(drv_path: string): {
|
||||||
|
input_drvs: [string, string[]][];
|
||||||
|
input_srcs: string[];
|
||||||
|
};
|
||||||
function op_copy_path_to_store(path: string): string;
|
function op_copy_path_to_store(path: string): string;
|
||||||
function op_get_env(key: string): string;
|
function op_get_env(key: string): string;
|
||||||
function op_walk_dir(path: string): [string, string][];
|
function op_walk_dir(path: string): [string, string][];
|
||||||
|
|||||||
@@ -57,10 +57,14 @@ fn runtime_extension<Ctx: RuntimeContext>() -> Extension {
|
|||||||
op_make_store_path::<Ctx>(),
|
op_make_store_path::<Ctx>(),
|
||||||
op_make_text_store_path::<Ctx>(),
|
op_make_text_store_path::<Ctx>(),
|
||||||
op_output_path_name(),
|
op_output_path_name(),
|
||||||
|
op_parse_hash(),
|
||||||
op_make_fixed_output_path::<Ctx>(),
|
op_make_fixed_output_path::<Ctx>(),
|
||||||
op_add_path::<Ctx>(),
|
op_add_path::<Ctx>(),
|
||||||
op_store_path::<Ctx>(),
|
op_store_path::<Ctx>(),
|
||||||
op_to_file::<Ctx>(),
|
op_to_file::<Ctx>(),
|
||||||
|
op_write_derivation::<Ctx>(),
|
||||||
|
op_read_derivation_outputs(),
|
||||||
|
op_compute_fs_closure(),
|
||||||
op_copy_path_to_store::<Ctx>(),
|
op_copy_path_to_store::<Ctx>(),
|
||||||
op_get_env(),
|
op_get_env(),
|
||||||
op_walk_dir(),
|
op_walk_dir(),
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use std::path::{Component, Path, PathBuf};
|
use std::path::{Component, Path, PathBuf};
|
||||||
|
use std::str::FromStr;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use hashbrown::hash_map::{Entry, HashMap};
|
use hashbrown::hash_map::{Entry, HashMap};
|
||||||
@@ -257,7 +258,7 @@ pub(super) fn op_make_placeholder(#[string] output: String) -> String {
|
|||||||
let mut hasher = Sha256::new();
|
let mut hasher = Sha256::new();
|
||||||
hasher.update(input.as_bytes());
|
hasher.update(input.as_bytes());
|
||||||
let hash: [u8; 32] = hasher.finalize().into();
|
let hash: [u8; 32] = hasher.finalize().into();
|
||||||
let encoded = crate::nix_hash::nix_base32_encode(&hash);
|
let encoded = nix_compat::nixbase32::encode(&hash);
|
||||||
format!("/{}", encoded)
|
format!("/{}", encoded)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -348,6 +349,34 @@ pub(super) fn op_output_path_name(
|
|||||||
crate::nix_hash::output_path_name(&drv_name, &output_name)
|
crate::nix_hash::output_path_name(&drv_name, &output_name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Serialize)]
|
||||||
|
pub(super) struct ParsedHash {
|
||||||
|
hex: String,
|
||||||
|
algo: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[deno_core::op2]
|
||||||
|
#[serde]
|
||||||
|
pub(super) fn op_parse_hash(
|
||||||
|
#[string] hash_str: String,
|
||||||
|
#[string] algo: String,
|
||||||
|
) -> std::result::Result<ParsedHash, NixRuntimeError> {
|
||||||
|
use nix_compat::nixhash::{HashAlgo, NixHash};
|
||||||
|
|
||||||
|
let hash_algo = HashAlgo::from_str(&algo).ok();
|
||||||
|
let nix_hash = NixHash::from_str(&hash_str, hash_algo).map_err(|e| {
|
||||||
|
NixRuntimeError::from(format!(
|
||||||
|
"invalid hash '{}' for algorithm '{}': {}",
|
||||||
|
hash_str, algo, e
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(ParsedHash {
|
||||||
|
hex: hex::encode(nix_hash.digest_as_bytes()),
|
||||||
|
algo,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
#[deno_core::op2]
|
#[deno_core::op2]
|
||||||
#[string]
|
#[string]
|
||||||
pub(super) fn op_make_fixed_output_path<Ctx: RuntimeContext>(
|
pub(super) fn op_make_fixed_output_path<Ctx: RuntimeContext>(
|
||||||
@@ -492,6 +521,268 @@ pub(super) fn op_to_file<Ctx: RuntimeContext>(
|
|||||||
Ok(store_path)
|
Ok(store_path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[deno_core::op2]
|
||||||
|
#[string]
|
||||||
|
pub(super) fn op_write_derivation<Ctx: RuntimeContext>(
|
||||||
|
state: &mut OpState,
|
||||||
|
#[string] drv_name: String,
|
||||||
|
#[string] aterm: String,
|
||||||
|
#[serde] references: Vec<String>,
|
||||||
|
) -> std::result::Result<String, NixRuntimeError> {
|
||||||
|
tracing::debug!(
|
||||||
|
"op_write_derivation: name={}.drv, references={:?}",
|
||||||
|
drv_name,
|
||||||
|
references
|
||||||
|
);
|
||||||
|
let ctx: &Ctx = state.get_ctx();
|
||||||
|
let store = ctx.get_store();
|
||||||
|
let store_path = store
|
||||||
|
.add_text_to_store(&format!("{}.drv", drv_name), &aterm, references)
|
||||||
|
.map_err(|e| NixRuntimeError::from(format!("failed to write derivation: {}", e)))?;
|
||||||
|
|
||||||
|
Ok(store_path)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[deno_core::op2]
|
||||||
|
#[serde]
|
||||||
|
pub(super) fn op_read_derivation_outputs(
|
||||||
|
#[string] drv_path: String,
|
||||||
|
) -> std::result::Result<Vec<String>, NixRuntimeError> {
|
||||||
|
let content = std::fs::read_to_string(&drv_path)
|
||||||
|
.map_err(|e| NixRuntimeError::from(format!("failed to read derivation {}: {}", drv_path, e)))?;
|
||||||
|
|
||||||
|
let outputs = parse_derivation_outputs(&content)
|
||||||
|
.ok_or_else(|| NixRuntimeError::from(format!("failed to parse derivation {}", drv_path)))?;
|
||||||
|
|
||||||
|
Ok(outputs)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_derivation_outputs(aterm: &str) -> Option<Vec<String>> {
|
||||||
|
let aterm = aterm.strip_prefix("Derive([")?;
|
||||||
|
let outputs_end = aterm.find("],[")?;
|
||||||
|
let outputs_section = &aterm[..outputs_end];
|
||||||
|
|
||||||
|
let mut outputs = Vec::new();
|
||||||
|
let mut pos = 0;
|
||||||
|
let bytes = outputs_section.as_bytes();
|
||||||
|
|
||||||
|
while pos < bytes.len() {
|
||||||
|
while pos < bytes.len() && bytes[pos] != b'(' {
|
||||||
|
pos += 1;
|
||||||
|
}
|
||||||
|
if pos >= bytes.len() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
pos += 1;
|
||||||
|
|
||||||
|
if pos >= bytes.len() || bytes[pos] != b'"' {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
pos += 1;
|
||||||
|
|
||||||
|
let name_start = pos;
|
||||||
|
while pos < bytes.len() && bytes[pos] != b'"' {
|
||||||
|
pos += 1;
|
||||||
|
}
|
||||||
|
let name = std::str::from_utf8(&bytes[name_start..pos]).ok()?;
|
||||||
|
outputs.push(name.to_string());
|
||||||
|
|
||||||
|
while pos < bytes.len() && bytes[pos] != b')' {
|
||||||
|
pos += 1;
|
||||||
|
}
|
||||||
|
pos += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(outputs)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Serialize)]
|
||||||
|
pub(super) struct DerivationInputs {
|
||||||
|
input_drvs: Vec<(String, Vec<String>)>,
|
||||||
|
input_srcs: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_derivation_inputs(aterm: &str) -> Option<DerivationInputs> {
|
||||||
|
let aterm = aterm.strip_prefix("Derive([")?;
|
||||||
|
|
||||||
|
let mut bracket_count = 1;
|
||||||
|
let mut pos = 0;
|
||||||
|
let bytes = aterm.as_bytes();
|
||||||
|
while pos < bytes.len() && bracket_count > 0 {
|
||||||
|
match bytes[pos] {
|
||||||
|
b'[' => bracket_count += 1,
|
||||||
|
b']' => bracket_count -= 1,
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
pos += 1;
|
||||||
|
}
|
||||||
|
if bracket_count != 0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let rest = &aterm[pos..];
|
||||||
|
let rest = rest.strip_prefix(",[")?;
|
||||||
|
|
||||||
|
let mut input_drvs = Vec::new();
|
||||||
|
let mut bracket_count = 1;
|
||||||
|
let mut start = 0;
|
||||||
|
pos = 0;
|
||||||
|
let bytes = rest.as_bytes();
|
||||||
|
|
||||||
|
while pos < bytes.len() && bracket_count > 0 {
|
||||||
|
match bytes[pos] {
|
||||||
|
b'[' => bracket_count += 1,
|
||||||
|
b']' => bracket_count -= 1,
|
||||||
|
b'(' if bracket_count == 1 => {
|
||||||
|
start = pos;
|
||||||
|
}
|
||||||
|
b')' if bracket_count == 1 => {
|
||||||
|
let entry = &rest[start + 1..pos];
|
||||||
|
if let Some((drv_path, outputs)) = parse_input_drv_entry(entry) {
|
||||||
|
input_drvs.push((drv_path, outputs));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
pos += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
let rest = &rest[pos..];
|
||||||
|
let rest = rest.strip_prefix(",[")?;
|
||||||
|
|
||||||
|
let mut input_srcs = Vec::new();
|
||||||
|
bracket_count = 1;
|
||||||
|
pos = 0;
|
||||||
|
let bytes = rest.as_bytes();
|
||||||
|
|
||||||
|
while pos < bytes.len() && bracket_count > 0 {
|
||||||
|
match bytes[pos] {
|
||||||
|
b'[' => bracket_count += 1,
|
||||||
|
b']' => bracket_count -= 1,
|
||||||
|
b'"' if bracket_count == 1 => {
|
||||||
|
pos += 1;
|
||||||
|
let src_start = pos;
|
||||||
|
while pos < bytes.len() && bytes[pos] != b'"' {
|
||||||
|
if bytes[pos] == b'\\' && pos + 1 < bytes.len() {
|
||||||
|
pos += 2;
|
||||||
|
} else {
|
||||||
|
pos += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let src = std::str::from_utf8(&bytes[src_start..pos]).ok()?;
|
||||||
|
input_srcs.push(src.to_string());
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
pos += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(DerivationInputs {
|
||||||
|
input_drvs,
|
||||||
|
input_srcs,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_input_drv_entry(entry: &str) -> Option<(String, Vec<String>)> {
|
||||||
|
let entry = entry.strip_prefix('"')?;
|
||||||
|
let quote_end = entry.find('"')?;
|
||||||
|
let drv_path = entry[..quote_end].to_string();
|
||||||
|
|
||||||
|
let rest = &entry[quote_end + 1..];
|
||||||
|
let rest = rest.strip_prefix(",[")?;
|
||||||
|
let rest = rest.strip_suffix(']')?;
|
||||||
|
|
||||||
|
let mut outputs = Vec::new();
|
||||||
|
for part in rest.split(',') {
|
||||||
|
let part = part.trim();
|
||||||
|
if let Some(name) = part.strip_prefix('"').and_then(|s| s.strip_suffix('"')) {
|
||||||
|
outputs.push(name.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Some((drv_path, outputs))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Serialize)]
|
||||||
|
pub(super) struct FsClosureResult {
|
||||||
|
input_drvs: Vec<(String, Vec<String>)>,
|
||||||
|
input_srcs: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[deno_core::op2]
|
||||||
|
#[serde]
|
||||||
|
pub(super) fn op_compute_fs_closure(
|
||||||
|
#[string] drv_path: String,
|
||||||
|
) -> std::result::Result<FsClosureResult, NixRuntimeError> {
|
||||||
|
use std::collections::{BTreeMap, BTreeSet, VecDeque};
|
||||||
|
|
||||||
|
let mut all_input_srcs: BTreeSet<String> = BTreeSet::new();
|
||||||
|
let mut all_input_drvs: BTreeMap<String, BTreeSet<String>> = BTreeMap::new();
|
||||||
|
|
||||||
|
let mut queue: VecDeque<String> = VecDeque::new();
|
||||||
|
let mut visited: BTreeSet<String> = BTreeSet::new();
|
||||||
|
|
||||||
|
queue.push_back(drv_path);
|
||||||
|
|
||||||
|
while let Some(current_path) = queue.pop_front() {
|
||||||
|
if visited.contains(¤t_path) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
visited.insert(current_path.clone());
|
||||||
|
|
||||||
|
all_input_srcs.insert(current_path.clone());
|
||||||
|
|
||||||
|
if !current_path.ends_with(".drv") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let content = match std::fs::read_to_string(¤t_path) {
|
||||||
|
Ok(c) => c,
|
||||||
|
Err(e) => {
|
||||||
|
return Err(NixRuntimeError::from(format!(
|
||||||
|
"failed to read derivation {}: {}",
|
||||||
|
current_path, e
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let inputs = parse_derivation_inputs(&content).ok_or_else(|| {
|
||||||
|
NixRuntimeError::from(format!("failed to parse derivation {}", current_path))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
for src in inputs.input_srcs {
|
||||||
|
all_input_srcs.insert(src.clone());
|
||||||
|
if !visited.contains(&src) {
|
||||||
|
queue.push_back(src);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (dep_drv, outputs) in inputs.input_drvs {
|
||||||
|
all_input_srcs.insert(dep_drv.clone());
|
||||||
|
|
||||||
|
let entry = all_input_drvs.entry(dep_drv.clone()).or_default();
|
||||||
|
for output in outputs {
|
||||||
|
entry.insert(output);
|
||||||
|
}
|
||||||
|
|
||||||
|
if !visited.contains(&dep_drv) {
|
||||||
|
queue.push_back(dep_drv);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let input_drvs: Vec<(String, Vec<String>)> = all_input_drvs
|
||||||
|
.into_iter()
|
||||||
|
.map(|(k, v)| (k, v.into_iter().collect()))
|
||||||
|
.collect();
|
||||||
|
let input_srcs: Vec<String> = all_input_srcs.into_iter().collect();
|
||||||
|
|
||||||
|
Ok(FsClosureResult {
|
||||||
|
input_drvs,
|
||||||
|
input_srcs,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
#[deno_core::op2]
|
#[deno_core::op2]
|
||||||
#[string]
|
#[string]
|
||||||
pub(super) fn op_copy_path_to_store<Ctx: RuntimeContext>(
|
pub(super) fn op_copy_path_to_store<Ctx: RuntimeContext>(
|
||||||
|
|||||||
@@ -9,9 +9,11 @@ use nix_compat::store_path::StorePath;
|
|||||||
use nix_compat::wire::ProtocolVersion;
|
use nix_compat::wire::ProtocolVersion;
|
||||||
use nix_compat::wire::de::{NixRead, NixReader};
|
use nix_compat::wire::de::{NixRead, NixReader};
|
||||||
use nix_compat::wire::ser::{NixSerialize, NixWrite, NixWriter, NixWriterBuilder};
|
use nix_compat::wire::ser::{NixSerialize, NixWrite, NixWriter, NixWriterBuilder};
|
||||||
|
use num_enum::{IntoPrimitive, TryFromPrimitive};
|
||||||
use tokio::io::{AsyncReadExt, AsyncWriteExt, ReadHalf, WriteHalf, split};
|
use tokio::io::{AsyncReadExt, AsyncWriteExt, ReadHalf, WriteHalf, split};
|
||||||
use tokio::net::UnixStream;
|
use tokio::net::UnixStream;
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
use crate::error::{Error, Result};
|
use crate::error::{Error, Result};
|
||||||
|
|
||||||
@@ -472,6 +474,7 @@ pub struct NixDaemonClient {
|
|||||||
protocol_version: ProtocolVersion,
|
protocol_version: ProtocolVersion,
|
||||||
reader: NixReader<ReadHalf<UnixStream>>,
|
reader: NixReader<ReadHalf<UnixStream>>,
|
||||||
writer: NixWriter<WriteHalf<UnixStream>>,
|
writer: NixWriter<WriteHalf<UnixStream>>,
|
||||||
|
_marker: std::marker::PhantomData<std::cell::Cell<()>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl NixDaemonClient {
|
impl NixDaemonClient {
|
||||||
@@ -503,18 +506,15 @@ impl NixDaemonClient {
|
|||||||
protocol_version,
|
protocol_version,
|
||||||
reader,
|
reader,
|
||||||
writer,
|
writer,
|
||||||
|
_marker: Default::default(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Execute an operation that returns a typed result
|
async fn execute<T, F1, F2>(&mut self, operation: Operation, write: F1, read: F2) -> IoResult<T>
|
||||||
///
|
|
||||||
/// This is the main method for implementing protocol operations:
|
|
||||||
/// 1. Send operation code
|
|
||||||
/// 2. Send operation parameters
|
|
||||||
/// 3. Receive response or error
|
|
||||||
async fn execute<T>(&mut self, operation: Operation) -> IoResult<T>
|
|
||||||
where
|
where
|
||||||
T: nix_compat::wire::de::NixDeserialize,
|
T: nix_compat::wire::de::NixDeserialize,
|
||||||
|
F1: FnOnce() -> IoResult<()>,
|
||||||
|
F2: FnOnce() -> IoResult<T>
|
||||||
{
|
{
|
||||||
// Send operation
|
// Send operation
|
||||||
self.writer.write_value(&operation).await?;
|
self.writer.write_value(&operation).await?;
|
||||||
@@ -542,7 +542,7 @@ impl NixDaemonClient {
|
|||||||
///
|
///
|
||||||
/// The daemon sends either:
|
/// The daemon sends either:
|
||||||
/// - STDERR_LAST followed by the result
|
/// - STDERR_LAST followed by the result
|
||||||
/// - STDERR_ERROR followed by an error message
|
/// - STDERR_ERROR followed by a structured error
|
||||||
async fn read_response<T>(&mut self) -> IoResult<T>
|
async fn read_response<T>(&mut self) -> IoResult<T>
|
||||||
where
|
where
|
||||||
T: nix_compat::wire::de::NixDeserialize,
|
T: nix_compat::wire::de::NixDeserialize,
|
||||||
@@ -551,23 +551,47 @@ impl NixDaemonClient {
|
|||||||
let msg = self.reader.read_number().await?;
|
let msg = self.reader.read_number().await?;
|
||||||
|
|
||||||
if msg == STDERR_LAST {
|
if msg == STDERR_LAST {
|
||||||
// Success, read the actual response
|
|
||||||
let result: T = self.reader.read_value().await?;
|
let result: T = self.reader.read_value().await?;
|
||||||
return Ok(result);
|
return Ok(result);
|
||||||
} else if msg == STDERR_ERROR {
|
} else if msg == STDERR_ERROR {
|
||||||
// IoError, read error message
|
let error_msg = self.read_daemon_error().await?;
|
||||||
// The error is sent as a NixIoError struct, but we just read the message
|
|
||||||
let error_msg: String = self.reader.read_value().await?;
|
|
||||||
return Err(IoError::other(error_msg));
|
return Err(IoError::other(error_msg));
|
||||||
} else {
|
} else {
|
||||||
// Other STDERR_* codes (logging, etc.) - for now, we ignore them
|
|
||||||
// Read and discard the associated data
|
|
||||||
let _data: String = self.reader.read_value().await?;
|
let _data: String = self.reader.read_value().await?;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn read_daemon_error(&mut self) -> IoResult<NixDaemonError> {
|
||||||
|
let type_marker: String = self.reader.read_value().await?;
|
||||||
|
assert_eq!(type_marker, "Error");
|
||||||
|
|
||||||
|
let level = NixDaemonErrorLevel::try_from_primitive(
|
||||||
|
self.reader.read_number().await?.try_into().unwrap(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
// removed
|
||||||
|
let _name: String = self.reader.read_value().await?;
|
||||||
|
let msg: String = self.reader.read_value().await?;
|
||||||
|
let have_pos: u64 = self.reader.read_number().await?;
|
||||||
|
assert_eq!(have_pos, 0);
|
||||||
|
|
||||||
|
let nr_traces: u64 = self.reader.read_number().await?;
|
||||||
|
let mut traces = Vec::new();
|
||||||
|
for _ in 0..nr_traces {
|
||||||
|
let _trace_pos: u64 = self.reader.read_number().await?;
|
||||||
|
let trace_hint: String = self.reader.read_value().await?;
|
||||||
|
traces.push(trace_hint);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(NixDaemonError {
|
||||||
|
level,
|
||||||
|
msg,
|
||||||
|
traces,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/// Check if a path is valid in the store
|
/// Check if a path is valid in the store
|
||||||
pub async fn is_valid_path(&mut self, path: &str) -> IoResult<bool> {
|
pub async fn is_valid_path(&mut self, path: &str) -> IoResult<bool> {
|
||||||
let store_path = StorePath::<String>::from_absolute_path(path.as_bytes())
|
let store_path = StorePath::<String>::from_absolute_path(path.as_bytes())
|
||||||
@@ -581,19 +605,15 @@ impl NixDaemonClient {
|
|||||||
let store_path = StorePath::<String>::from_absolute_path(path.as_bytes())
|
let store_path = StorePath::<String>::from_absolute_path(path.as_bytes())
|
||||||
.map_err(|e| IoError::new(IoErrorKind::InvalidInput, e.to_string()))?;
|
.map_err(|e| IoError::new(IoErrorKind::InvalidInput, e.to_string()))?;
|
||||||
|
|
||||||
// QueryPathInfo returns Option<UnkeyedValidPathInfo> which is serialized
|
|
||||||
// as a bool followed by the value if true
|
|
||||||
self.writer.write_value(&Operation::QueryPathInfo).await?;
|
self.writer.write_value(&Operation::QueryPathInfo).await?;
|
||||||
self.writer.write_value(&store_path).await?;
|
self.writer.write_value(&store_path).await?;
|
||||||
self.writer.flush().await?;
|
self.writer.flush().await?;
|
||||||
|
|
||||||
// Read response - it's serialized as bool + optional value
|
|
||||||
loop {
|
loop {
|
||||||
let msg = self.reader.read_number().await?;
|
let msg = self.reader.read_number().await?;
|
||||||
if msg == STDERR_LAST {
|
if msg == STDERR_LAST {
|
||||||
let has_value: bool = self.reader.read_value().await?;
|
let has_value: bool = self.reader.read_value().await?;
|
||||||
if has_value {
|
if has_value {
|
||||||
// Manually deserialize UnkeyedValidPathInfo
|
|
||||||
use nix_compat::narinfo::Signature;
|
use nix_compat::narinfo::Signature;
|
||||||
use nix_compat::nixhash::CAHash;
|
use nix_compat::nixhash::CAHash;
|
||||||
|
|
||||||
@@ -621,7 +641,7 @@ impl NixDaemonClient {
|
|||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
} else if msg == STDERR_ERROR {
|
} else if msg == STDERR_ERROR {
|
||||||
let error_msg: String = self.reader.read_value().await?;
|
let error_msg = self.read_daemon_error().await?;
|
||||||
return Err(IoError::other(error_msg));
|
return Err(IoError::other(error_msg));
|
||||||
} else {
|
} else {
|
||||||
let _data: String = self.reader.read_value().await?;
|
let _data: String = self.reader.read_value().await?;
|
||||||
@@ -635,18 +655,16 @@ impl NixDaemonClient {
|
|||||||
let store_path = StorePath::<String>::from_absolute_path(path.as_bytes())
|
let store_path = StorePath::<String>::from_absolute_path(path.as_bytes())
|
||||||
.map_err(|e| IoError::new(IoErrorKind::InvalidInput, e.to_string()))?;
|
.map_err(|e| IoError::new(IoErrorKind::InvalidInput, e.to_string()))?;
|
||||||
|
|
||||||
// EnsurePath returns void (no value)
|
|
||||||
self.writer.write_value(&Operation::EnsurePath).await?;
|
self.writer.write_value(&Operation::EnsurePath).await?;
|
||||||
self.writer.write_value(&store_path).await?;
|
self.writer.write_value(&store_path).await?;
|
||||||
self.writer.flush().await?;
|
self.writer.flush().await?;
|
||||||
|
|
||||||
// Read response - expect STDERR_LAST with no value
|
|
||||||
loop {
|
loop {
|
||||||
let msg = self.reader.read_number().await?;
|
let msg = self.reader.read_number().await?;
|
||||||
if msg == STDERR_LAST {
|
if msg == STDERR_LAST {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
} else if msg == STDERR_ERROR {
|
} else if msg == STDERR_ERROR {
|
||||||
let error_msg: String = self.reader.read_value().await?;
|
let error_msg = self.read_daemon_error().await?;
|
||||||
return Err(IoError::other(error_msg));
|
return Err(IoError::other(error_msg));
|
||||||
} else {
|
} else {
|
||||||
let _data: String = self.reader.read_value().await?;
|
let _data: String = self.reader.read_value().await?;
|
||||||
@@ -729,7 +747,7 @@ impl NixDaemonClient {
|
|||||||
if msg == STDERR_LAST {
|
if msg == STDERR_LAST {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
} else if msg == STDERR_ERROR {
|
} else if msg == STDERR_ERROR {
|
||||||
let error_msg: String = self.reader.read_value().await?;
|
let error_msg = self.read_daemon_error().await?;
|
||||||
return Err(IoError::other(error_msg));
|
return Err(IoError::other(error_msg));
|
||||||
} else {
|
} else {
|
||||||
let _data: String = self.reader.read_value().await?;
|
let _data: String = self.reader.read_value().await?;
|
||||||
@@ -787,3 +805,24 @@ impl NixDaemonConnection {
|
|||||||
client.add_to_store_nar(request, nar_data).await
|
client.add_to_store_nar(request, nar_data).await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, IntoPrimitive, TryFromPrimitive)]
|
||||||
|
#[repr(u8)]
|
||||||
|
pub enum NixDaemonErrorLevel {
|
||||||
|
Error = 0,
|
||||||
|
Warn,
|
||||||
|
Notice,
|
||||||
|
Info,
|
||||||
|
Talkative,
|
||||||
|
Chatty,
|
||||||
|
Debug,
|
||||||
|
Vomit,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
#[error("{msg}")]
|
||||||
|
pub struct NixDaemonError {
|
||||||
|
level: NixDaemonErrorLevel,
|
||||||
|
msg: String,
|
||||||
|
traces: Vec<String>,
|
||||||
|
}
|
||||||
|
|||||||
@@ -643,7 +643,7 @@ fn fixed_output_with_structured_attrs() {
|
|||||||
name = "fixstruct";
|
name = "fixstruct";
|
||||||
builder = "/bin/sh";
|
builder = "/bin/sh";
|
||||||
system = "x86_64-linux";
|
system = "x86_64-linux";
|
||||||
outputHash = "abc123";
|
outputHash = "0000000000000000000000000000000000000000000000000000000000000000";
|
||||||
__structuredAttrs = true;
|
__structuredAttrs = true;
|
||||||
data = { key = "value"; };
|
data = { key = "value"; };
|
||||||
}"#,
|
}"#,
|
||||||
|
|||||||
Reference in New Issue
Block a user