feat: initial path implementation
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -1,3 +1,7 @@
|
|||||||
target/
|
target/
|
||||||
|
|
||||||
/.direnv/
|
/.direnv/
|
||||||
|
|
||||||
|
# Profiling
|
||||||
|
flamegraph*.svg
|
||||||
|
perf.data*
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
* Conversion and serialization builtin functions
|
* Conversion and serialization builtin functions
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { NixValue, NixString } from "../types";
|
import type { NixValue, NixString, NixPath } from "../types";
|
||||||
import { isStringWithContext } from "../types";
|
import { isStringWithContext, isNixPath } from "../types";
|
||||||
import { force } from "../thunk";
|
import { force } from "../thunk";
|
||||||
import { type NixStringContext, mkStringWithContext, addBuiltContext } from "../string-context";
|
import { type NixStringContext, mkStringWithContext, addBuiltContext } from "../string-context";
|
||||||
import { forceFunction } from "../type-assert";
|
import { forceFunction } from "../type-assert";
|
||||||
@@ -150,6 +150,13 @@ export const coerceToString = (
|
|||||||
return v.value;
|
return v.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Paths coerce to their string value
|
||||||
|
if (isNixPath(v)) {
|
||||||
|
// TODO: Implement copyToStore when needed (copy path to Nix store)
|
||||||
|
// For now, just return the raw path string
|
||||||
|
return v.value;
|
||||||
|
}
|
||||||
|
|
||||||
if (typeof v === "object" && v !== null && !Array.isArray(v)) {
|
if (typeof v === "object" && v !== null && !Array.isArray(v)) {
|
||||||
// First, try the __toString method if present
|
// First, try the __toString method if present
|
||||||
// This allows custom types to define their own string representation
|
// This allows custom types to define their own string representation
|
||||||
@@ -265,6 +272,36 @@ export const coerceToStringWithContext = (
|
|||||||
return mkStringWithContext(str, context);
|
return mkStringWithContext(str, context);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Coerce a Nix value to an absolute path string.
|
||||||
|
* This implements the same behavior as Lix's EvalState::coerceToPath.
|
||||||
|
*
|
||||||
|
* @param value - The value to coerce
|
||||||
|
* @param outContext - Optional context set to collect string contexts
|
||||||
|
* @returns The absolute path string
|
||||||
|
* @throws TypeError if the value cannot be coerced to a string
|
||||||
|
* @throws Error if the result is not an absolute path
|
||||||
|
*
|
||||||
|
* Semantics:
|
||||||
|
* - Coerces to string using Strict mode (same as coerceToString with Base mode)
|
||||||
|
* - Validates the result is non-empty and starts with '/'
|
||||||
|
* - Returns the path string (not a NixPath object)
|
||||||
|
* - Preserves string context if present
|
||||||
|
*/
|
||||||
|
export const coerceToPath = (value: NixValue, outContext?: NixStringContext): string => {
|
||||||
|
const pathStr = coerceToString(value, StringCoercionMode.Base, false, outContext);
|
||||||
|
|
||||||
|
if (pathStr === "") {
|
||||||
|
throw new Error("string doesn't represent an absolute path: empty string");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pathStr[0] !== "/") {
|
||||||
|
throw new Error(`string '${pathStr}' doesn't represent an absolute path`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return pathStr;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* builtins.toString - Convert a value to a string
|
* builtins.toString - Convert a value to a string
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -5,10 +5,17 @@ import { type DerivationData, type OutputInfo, generateAterm } from "../derivati
|
|||||||
import { coerceToString, StringCoercionMode } from "./conversion";
|
import { coerceToString, StringCoercionMode } from "./conversion";
|
||||||
import { type NixStringContext, extractInputDrvsAndSrcs, isStringWithContext } from "../string-context";
|
import { type NixStringContext, extractInputDrvsAndSrcs, isStringWithContext } from "../string-context";
|
||||||
import { nixValueToJson } from "../conversion";
|
import { nixValueToJson } from "../conversion";
|
||||||
|
import { isNixPath } from "../types";
|
||||||
|
|
||||||
const forceAttrs = (value: NixValue): NixAttrs => {
|
const forceAttrs = (value: NixValue): NixAttrs => {
|
||||||
const forced = force(value);
|
const forced = force(value);
|
||||||
if (typeof forced !== "object" || forced === null || Array.isArray(forced) || isStringWithContext(forced)) {
|
if (
|
||||||
|
typeof forced !== "object" ||
|
||||||
|
forced === null ||
|
||||||
|
Array.isArray(forced) ||
|
||||||
|
isStringWithContext(forced) ||
|
||||||
|
isNixPath(forced)
|
||||||
|
) {
|
||||||
throw new TypeError(`Expected attribute set for derivation, got ${typeof forced}`);
|
throw new TypeError(`Expected attribute set for derivation, got ${typeof forced}`);
|
||||||
}
|
}
|
||||||
return forced;
|
return forced;
|
||||||
|
|||||||
@@ -104,6 +104,7 @@ import * as typeCheck from "./type-check";
|
|||||||
import * as list from "./list";
|
import * as list from "./list";
|
||||||
import * as attrs from "./attrs";
|
import * as attrs from "./attrs";
|
||||||
import * as string from "./string";
|
import * as string from "./string";
|
||||||
|
import * as pathOps from "./path";
|
||||||
import * as functional from "./functional";
|
import * as functional from "./functional";
|
||||||
import * as io from "./io";
|
import * as io from "./io";
|
||||||
import * as conversion from "./conversion";
|
import * as conversion from "./conversion";
|
||||||
@@ -174,7 +175,9 @@ export const builtins: any = {
|
|||||||
stringLength: mkPrimop(string.stringLength, "stringLength", 1),
|
stringLength: mkPrimop(string.stringLength, "stringLength", 1),
|
||||||
substring: mkPrimop(string.substring, "substring", 3),
|
substring: mkPrimop(string.substring, "substring", 3),
|
||||||
concatStringsSep: mkPrimop(string.concatStringsSep, "concatStringsSep", 2),
|
concatStringsSep: mkPrimop(string.concatStringsSep, "concatStringsSep", 2),
|
||||||
baseNameOf: mkPrimop(string.baseNameOf, "baseNameOf", 1),
|
baseNameOf: mkPrimop(pathOps.baseNameOf, "baseNameOf", 1),
|
||||||
|
dirOf: mkPrimop(pathOps.dirOf, "dirOf", 1),
|
||||||
|
toPath: mkPrimop(pathOps.toPath, "toPath", 1),
|
||||||
match: mkPrimop(string.match, "match", 2),
|
match: mkPrimop(string.match, "match", 2),
|
||||||
split: mkPrimop(string.split, "split", 2),
|
split: mkPrimop(string.split, "split", 2),
|
||||||
|
|
||||||
@@ -204,7 +207,6 @@ export const builtins: any = {
|
|||||||
pathExists: mkPrimop(io.pathExists, "pathExists", 1),
|
pathExists: mkPrimop(io.pathExists, "pathExists", 1),
|
||||||
path: mkPrimop(io.path, "path", 1),
|
path: mkPrimop(io.path, "path", 1),
|
||||||
toFile: mkPrimop(io.toFile, "toFile", 2),
|
toFile: mkPrimop(io.toFile, "toFile", 2),
|
||||||
toPath: mkPrimop(io.toPath, "toPath", 1),
|
|
||||||
filterSource: mkPrimop(io.filterSource, "filterSource", 2),
|
filterSource: mkPrimop(io.filterSource, "filterSource", 2),
|
||||||
findFile: mkPrimop(io.findFile, "findFile", 2),
|
findFile: mkPrimop(io.findFile, "findFile", 2),
|
||||||
getEnv: mkPrimop(io.getEnv, "getEnv", 1),
|
getEnv: mkPrimop(io.getEnv, "getEnv", 1),
|
||||||
@@ -231,7 +233,6 @@ export const builtins: any = {
|
|||||||
unsafeGetAttrPos: mkPrimop(misc.unsafeGetAttrPos, "unsafeGetAttrPos", 2),
|
unsafeGetAttrPos: mkPrimop(misc.unsafeGetAttrPos, "unsafeGetAttrPos", 2),
|
||||||
addDrvOutputDependencies: mkPrimop(misc.addDrvOutputDependencies, "addDrvOutputDependencies", 2),
|
addDrvOutputDependencies: mkPrimop(misc.addDrvOutputDependencies, "addDrvOutputDependencies", 2),
|
||||||
compareVersions: mkPrimop(misc.compareVersions, "compareVersions", 2),
|
compareVersions: mkPrimop(misc.compareVersions, "compareVersions", 2),
|
||||||
dirOf: mkPrimop(misc.dirOf, "dirOf", 1),
|
|
||||||
flakeRefToString: mkPrimop(misc.flakeRefToString, "flakeRefToString", 1),
|
flakeRefToString: mkPrimop(misc.flakeRefToString, "flakeRefToString", 1),
|
||||||
functionArgs: mkPrimop(misc.functionArgs, "functionArgs", 1),
|
functionArgs: mkPrimop(misc.functionArgs, "functionArgs", 1),
|
||||||
genericClosure: mkPrimop(misc.genericClosure, "genericClosure", 1),
|
genericClosure: mkPrimop(misc.genericClosure, "genericClosure", 1),
|
||||||
@@ -260,5 +261,5 @@ export const builtins: any = {
|
|||||||
langVersion: 6,
|
langVersion: 6,
|
||||||
nixPath: [],
|
nixPath: [],
|
||||||
nixVersion: "2.31.2",
|
nixVersion: "2.31.2",
|
||||||
storeDir: "/nix/store",
|
storeDir: "/home/imxyy/.cache/nix-js/fetchers/store",
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,10 +3,12 @@
|
|||||||
* Implemented via Rust ops exposed through deno_core
|
* Implemented via Rust ops exposed through deno_core
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { forceAttrs, forceBool, forceString } from "../type-assert";
|
import { forceAttrs, forceBool, forceString, forceNixPath } from "../type-assert";
|
||||||
import type { NixValue, NixAttrs } from "../types";
|
import type { NixValue, NixAttrs } from "../types";
|
||||||
|
import { isNixPath } from "../types";
|
||||||
import { force } from "../thunk";
|
import { force } from "../thunk";
|
||||||
import { coerceToString, StringCoercionMode } from "./conversion";
|
import { coerceToPath, coerceToString, StringCoercionMode } from "./conversion";
|
||||||
|
import { getPathValue } from "../path";
|
||||||
|
|
||||||
// Declare Deno.core.ops global (provided by deno_core runtime)
|
// Declare Deno.core.ops global (provided by deno_core runtime)
|
||||||
|
|
||||||
@@ -270,7 +272,7 @@ export const readDir = (path: NixValue): never => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const readFile = (path: NixValue): string => {
|
export const readFile = (path: NixValue): string => {
|
||||||
const pathStr = forceString(path);
|
const pathStr = coerceToPath(path);
|
||||||
return Deno.core.ops.op_read_file(pathStr);
|
return Deno.core.ops.op_read_file(pathStr);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -279,12 +281,59 @@ export const readFileType = (path: NixValue): never => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const pathExists = (path: NixValue): boolean => {
|
export const pathExists = (path: NixValue): boolean => {
|
||||||
const pathStr = forceString(path);
|
const pathStr = coerceToPath(path);
|
||||||
return Deno.core.ops.op_path_exists(pathStr);
|
return Deno.core.ops.op_path_exists(pathStr);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const path = (args: NixValue): never => {
|
/**
|
||||||
throw new Error("Not implemented: path");
|
* builtins.path
|
||||||
|
* Add a path to the Nix store with fine-grained control
|
||||||
|
*
|
||||||
|
* Parameters (attribute set):
|
||||||
|
* - path (required): Path to add to the store
|
||||||
|
* - name (optional): Name to use in store path (defaults to basename)
|
||||||
|
* - filter (optional): Function (path, type) -> bool (NOT IMPLEMENTED YET)
|
||||||
|
* - recursive (optional): Boolean, default true (NAR vs flat hashing)
|
||||||
|
* - sha256 (optional): Expected SHA-256 hash (hex-encoded)
|
||||||
|
*
|
||||||
|
* Returns: Store path string
|
||||||
|
*/
|
||||||
|
export const path = (args: NixValue): string => {
|
||||||
|
const attrs = forceAttrs(args);
|
||||||
|
|
||||||
|
// Required: path parameter
|
||||||
|
if (!("path" in attrs)) {
|
||||||
|
throw new TypeError("builtins.path: 'path' attribute is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
const pathValue = force(attrs.path);
|
||||||
|
let pathStr: string;
|
||||||
|
|
||||||
|
// Accept both Path values and strings
|
||||||
|
if (isNixPath(pathValue)) {
|
||||||
|
pathStr = getPathValue(pathValue);
|
||||||
|
} else {
|
||||||
|
pathStr = forceString(pathValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optional: name parameter (defaults to basename in Rust)
|
||||||
|
const name = "name" in attrs ? forceString(attrs.name) : null;
|
||||||
|
|
||||||
|
// Optional: recursive parameter (default: true)
|
||||||
|
const recursive = "recursive" in attrs ? forceBool(attrs.recursive) : true;
|
||||||
|
|
||||||
|
// Optional: sha256 parameter
|
||||||
|
const sha256 = "sha256" in attrs ? forceString(attrs.sha256) : null;
|
||||||
|
|
||||||
|
// TODO: Handle filter parameter
|
||||||
|
if ("filter" in attrs) {
|
||||||
|
throw new Error("builtins.path: 'filter' parameter is not yet implemented");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call Rust op to add path to store
|
||||||
|
const storePath: string = Deno.core.ops.op_add_path(pathStr, name, recursive, sha256);
|
||||||
|
|
||||||
|
return storePath;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const toFile = (name: NixValue, s: NixValue): never => {
|
export const toFile = (name: NixValue, s: NixValue): never => {
|
||||||
|
|||||||
@@ -191,9 +191,7 @@ export const replaceStrings =
|
|||||||
const inputStr = forceString(s);
|
const inputStr = forceString(s);
|
||||||
|
|
||||||
if (fromList.length !== toList.length) {
|
if (fromList.length !== toList.length) {
|
||||||
throw new Error(
|
throw new Error("'from' and 'to' arguments passed to builtins.replaceStrings have different lengths");
|
||||||
"'from' and 'to' arguments passed to builtins.replaceStrings have different lengths"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const toCache = new Map<number, string>();
|
const toCache = new Map<number, string>();
|
||||||
@@ -240,7 +238,6 @@ export const replaceStrings =
|
|||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export const splitVersion = (s: NixValue): NixValue => {
|
export const splitVersion = (s: NixValue): NixValue => {
|
||||||
const version = forceString(s);
|
const version = forceString(s);
|
||||||
const components: string[] = [];
|
const components: string[] = [];
|
||||||
|
|||||||
127
nix-js/runtime-ts/src/builtins/path.ts
Normal file
127
nix-js/runtime-ts/src/builtins/path.ts
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
/**
|
||||||
|
* Path-related builtin functions
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { NixValue, NixString, NixPath } from "../types";
|
||||||
|
import { isNixPath, isStringWithContext } from "../types";
|
||||||
|
import { force } from "../thunk";
|
||||||
|
import { mkPath } from "../path";
|
||||||
|
import { coerceToString, StringCoercionMode, coerceToPath } from "./conversion";
|
||||||
|
import { mkStringWithContext, type NixStringContext } from "../string-context";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* builtins.baseNameOf
|
||||||
|
* Get the last component of a path or string
|
||||||
|
* Always returns a string (coerces paths)
|
||||||
|
*
|
||||||
|
* Examples:
|
||||||
|
* - baseNameOf ./foo/bar → "bar"
|
||||||
|
* - baseNameOf "/foo/bar/" → "bar"
|
||||||
|
* - baseNameOf "foo" → "foo"
|
||||||
|
*/
|
||||||
|
export const baseNameOf = (s: NixValue): string => {
|
||||||
|
const forced = force(s);
|
||||||
|
|
||||||
|
let pathStr: string;
|
||||||
|
if (isNixPath(forced)) {
|
||||||
|
pathStr = forced.value;
|
||||||
|
} else {
|
||||||
|
pathStr = coerceToString(s, StringCoercionMode.Base, false) as string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastSlash = pathStr.lastIndexOf("/");
|
||||||
|
if (lastSlash === -1) {
|
||||||
|
return pathStr;
|
||||||
|
}
|
||||||
|
|
||||||
|
return pathStr.slice(lastSlash + 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* builtins.dirOf
|
||||||
|
* Get the directory part of a path or string
|
||||||
|
* TYPE-PRESERVING: path → path, string → string
|
||||||
|
*
|
||||||
|
* Examples:
|
||||||
|
* - dirOf ./foo/bar → ./foo (path)
|
||||||
|
* - dirOf "/foo/bar" → "/foo" (string)
|
||||||
|
* - dirOf "/" → "/" (same type as input)
|
||||||
|
*/
|
||||||
|
export const dirOf = (s: NixValue): NixPath | NixString => {
|
||||||
|
const forced = force(s);
|
||||||
|
|
||||||
|
// Path input → path output
|
||||||
|
if (isNixPath(forced)) {
|
||||||
|
const pathStr = forced.value;
|
||||||
|
const lastSlash = pathStr.lastIndexOf("/");
|
||||||
|
|
||||||
|
if (lastSlash === -1) {
|
||||||
|
return mkPath(".");
|
||||||
|
}
|
||||||
|
if (lastSlash === 0) {
|
||||||
|
return mkPath("/");
|
||||||
|
}
|
||||||
|
|
||||||
|
return mkPath(pathStr.slice(0, lastSlash));
|
||||||
|
}
|
||||||
|
|
||||||
|
// String input → string output
|
||||||
|
const strValue: NixString = coerceToString(s, StringCoercionMode.Base, false) as NixString;
|
||||||
|
|
||||||
|
let pathStr: string;
|
||||||
|
let hasContext = false;
|
||||||
|
let originalContext: Set<string> | undefined;
|
||||||
|
|
||||||
|
if (typeof strValue === "string") {
|
||||||
|
pathStr = strValue;
|
||||||
|
} else if (isStringWithContext(strValue)) {
|
||||||
|
pathStr = strValue.value;
|
||||||
|
hasContext = strValue.context.size > 0;
|
||||||
|
originalContext = strValue.context;
|
||||||
|
} else {
|
||||||
|
pathStr = strValue as string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastSlash = pathStr.lastIndexOf("/");
|
||||||
|
|
||||||
|
if (lastSlash === -1) {
|
||||||
|
return ".";
|
||||||
|
}
|
||||||
|
if (lastSlash === 0) {
|
||||||
|
return "/";
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = pathStr.slice(0, lastSlash);
|
||||||
|
|
||||||
|
// Preserve string context if present
|
||||||
|
if (hasContext && originalContext) {
|
||||||
|
return mkStringWithContext(result, originalContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* builtins.toPath
|
||||||
|
* Convert a value to an absolute path string.
|
||||||
|
* DEPRECATED: Use `/. + "/path"` to convert a string into an absolute path.
|
||||||
|
*
|
||||||
|
* This validates that the input can be coerced to an absolute path.
|
||||||
|
* Returns a **string** (not a NixPath), with context preserved.
|
||||||
|
*
|
||||||
|
* Examples:
|
||||||
|
* - toPath "/foo" → "/foo" (string)
|
||||||
|
* - toPath "/foo/bar" → "/foo/bar" (string)
|
||||||
|
* - toPath "foo" → ERROR (not absolute)
|
||||||
|
* - toPath "" → ERROR (empty)
|
||||||
|
*/
|
||||||
|
export const toPath = (s: NixValue): NixString => {
|
||||||
|
const context: NixStringContext = new Set();
|
||||||
|
const pathStr = coerceToPath(s, context);
|
||||||
|
|
||||||
|
if (context.size === 0) {
|
||||||
|
return pathStr;
|
||||||
|
}
|
||||||
|
|
||||||
|
return mkStringWithContext(pathStr, context);
|
||||||
|
};
|
||||||
@@ -95,7 +95,6 @@ function posixToJsRegex(pattern: string, fullMatch: boolean = false): RegExp {
|
|||||||
return new RegExp(jsPattern, "u");
|
return new RegExp(jsPattern, "u");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export const match =
|
export const match =
|
||||||
(regex: NixValue) =>
|
(regex: NixValue) =>
|
||||||
(str: NixValue): NixValue => {
|
(str: NixValue): NixValue => {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
HAS_CONTEXT,
|
HAS_CONTEXT,
|
||||||
|
isNixPath,
|
||||||
type NixAttrs,
|
type NixAttrs,
|
||||||
type NixBool,
|
type NixBool,
|
||||||
type NixFloat,
|
type NixFloat,
|
||||||
@@ -39,8 +40,9 @@ export const isList = (e: NixValue): e is NixList => Array.isArray(force(e));
|
|||||||
|
|
||||||
export const isNull = (e: NixValue): e is NixNull => force(e) === null;
|
export const isNull = (e: NixValue): e is NixNull => force(e) === null;
|
||||||
|
|
||||||
export const isPath = (e: NixValue): never => {
|
export const isPath = (e: NixValue): boolean => {
|
||||||
throw new Error("Not implemented: isPath");
|
const val = force(e);
|
||||||
|
return isNixPath(val);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isString = (e: NixValue): e is NixString => typeof force(e) === "string";
|
export const isString = (e: NixValue): e is NixString => typeof force(e) === "string";
|
||||||
@@ -48,6 +50,7 @@ export const isString = (e: NixValue): e is NixString => typeof force(e) === "st
|
|||||||
export const typeOf = (e: NixValue): string => {
|
export const typeOf = (e: NixValue): string => {
|
||||||
const val = force(e);
|
const val = force(e);
|
||||||
|
|
||||||
|
if (isNixPath(val)) return "path";
|
||||||
if (typeof val === "bigint") return "int";
|
if (typeof val === "bigint") return "int";
|
||||||
if (typeof val === "number") return "float";
|
if (typeof val === "number") return "float";
|
||||||
if (typeof val === "boolean") return "bool";
|
if (typeof val === "boolean") return "bool";
|
||||||
|
|||||||
@@ -2,27 +2,84 @@
|
|||||||
* Helper functions for nix-js runtime
|
* Helper functions for nix-js runtime
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { NixValue, NixAttrs, NixBool, NixString } from "./types";
|
import type { NixValue, NixAttrs, NixBool, NixString, NixPath } from "./types";
|
||||||
import { forceAttrs, forceFunction, forceString, typeName } from "./type-assert";
|
import { forceAttrs, forceFunction, forceString, typeName } from "./type-assert";
|
||||||
import { isAttrs } from "./builtins/type-check";
|
import { isAttrs } from "./builtins/type-check";
|
||||||
import { coerceToString, StringCoercionMode } from "./builtins/conversion";
|
import { coerceToString, StringCoercionMode } from "./builtins/conversion";
|
||||||
import { type NixStringContext, mkStringWithContext } from "./string-context";
|
import {
|
||||||
|
type NixStringContext,
|
||||||
|
mkStringWithContext,
|
||||||
|
isStringWithContext,
|
||||||
|
getStringContext,
|
||||||
|
} from "./string-context";
|
||||||
import { force } from "./thunk";
|
import { force } from "./thunk";
|
||||||
|
import { mkPath } from "./path";
|
||||||
|
import { isNixPath } from "./types";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Concatenate multiple values into a string with context
|
* Concatenate multiple values into a string or path with context
|
||||||
* This is used for string interpolation like "hello ${world}"
|
* This is used for string interpolation like "hello ${world}"
|
||||||
|
* If first element is a path, result is a path (with constraint: no store context allowed)
|
||||||
*
|
*
|
||||||
* @param parts - Array of values to concatenate
|
* @param parts - Array of values to concatenate
|
||||||
* @returns String with merged contexts from all parts
|
* @returns String or Path with merged contexts from all parts
|
||||||
*/
|
*/
|
||||||
export const concatStringsWithContext = (parts: NixValue[]): NixString => {
|
export const concatStringsWithContext = (parts: NixValue[]): NixString | NixPath => {
|
||||||
|
if (parts.length === 0) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const forced = parts.map(force);
|
||||||
|
|
||||||
|
// Check if first element is a path
|
||||||
|
const firstIsPath = isNixPath(forced[0]);
|
||||||
|
|
||||||
|
if (firstIsPath) {
|
||||||
|
// Path concatenation mode: result will be a path
|
||||||
|
let result = (forced[0] as NixPath).value;
|
||||||
|
|
||||||
|
for (let i = 1; i < forced.length; i++) {
|
||||||
|
const part = forced[i];
|
||||||
|
|
||||||
|
if (isNixPath(part)) {
|
||||||
|
result += part.value;
|
||||||
|
} else if (typeof part === "string") {
|
||||||
|
result += part;
|
||||||
|
} else if (isStringWithContext(part)) {
|
||||||
|
// Lix constraint: cannot mix store context with paths
|
||||||
|
if (part.context.size > 0) {
|
||||||
|
throw new TypeError("a string that refers to a store path cannot be appended to a path");
|
||||||
|
}
|
||||||
|
result += part.value;
|
||||||
|
} else {
|
||||||
|
// Coerce to string
|
||||||
|
const tempContext: NixStringContext = new Set();
|
||||||
|
const coerced = coerceToString(part, StringCoercionMode.Interpolation, false, tempContext);
|
||||||
|
|
||||||
|
if (tempContext.size > 0) {
|
||||||
|
throw new TypeError("a string that refers to a store path cannot be appended to a path");
|
||||||
|
}
|
||||||
|
|
||||||
|
result += coerced;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return mkPath(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
// String concatenation mode
|
||||||
const context: NixStringContext = new Set();
|
const context: NixStringContext = new Set();
|
||||||
const strParts: string[] = [];
|
const strParts: string[] = [];
|
||||||
|
|
||||||
for (const part of parts) {
|
for (const part of parts) {
|
||||||
const str = coerceToString(part, StringCoercionMode.Interpolation, false, context);
|
// Handle path coercion to string
|
||||||
strParts.push(str);
|
const forced = force(part);
|
||||||
|
if (isNixPath(forced)) {
|
||||||
|
strParts.push(forced.value);
|
||||||
|
} else {
|
||||||
|
const str = coerceToString(forced, StringCoercionMode.Interpolation, false, context);
|
||||||
|
strParts.push(str);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const value = strParts.join("");
|
const value = strParts.join("");
|
||||||
@@ -39,11 +96,12 @@ export const concatStringsWithContext = (parts: NixValue[]): NixString => {
|
|||||||
* For relative paths, resolves against current import stack
|
* For relative paths, resolves against current import stack
|
||||||
*
|
*
|
||||||
* @param path - Path string (may be relative or absolute)
|
* @param path - Path string (may be relative or absolute)
|
||||||
* @returns Absolute path string
|
* @returns NixPath object with absolute path
|
||||||
*/
|
*/
|
||||||
export const resolvePath = (path: NixValue): string => {
|
export const resolvePath = (path: NixValue): NixPath => {
|
||||||
const path_str = forceString(path);
|
const path_str = forceString(path);
|
||||||
return Deno.core.ops.op_resolve_path(path_str);
|
const resolved = Deno.core.ops.op_resolve_path(path_str);
|
||||||
|
return mkPath(resolved);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const select = (obj: NixValue, attrpath: NixValue[]): NixValue => {
|
export const select = (obj: NixValue, attrpath: NixValue[]): NixValue => {
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import { op } from "./operators";
|
|||||||
import { builtins, PRIMOP_METADATA } from "./builtins";
|
import { builtins, PRIMOP_METADATA } from "./builtins";
|
||||||
import { coerceToString, StringCoercionMode } from "./builtins/conversion";
|
import { coerceToString, StringCoercionMode } from "./builtins/conversion";
|
||||||
import { HAS_CONTEXT } from "./string-context";
|
import { HAS_CONTEXT } from "./string-context";
|
||||||
|
import { IS_PATH } from "./types";
|
||||||
|
|
||||||
export type NixRuntime = typeof Nix;
|
export type NixRuntime = typeof Nix;
|
||||||
|
|
||||||
@@ -30,6 +31,7 @@ export const Nix = {
|
|||||||
isThunk,
|
isThunk,
|
||||||
IS_THUNK,
|
IS_THUNK,
|
||||||
HAS_CONTEXT,
|
HAS_CONTEXT,
|
||||||
|
IS_PATH,
|
||||||
DEBUG_THUNKS,
|
DEBUG_THUNKS,
|
||||||
|
|
||||||
call,
|
call,
|
||||||
|
|||||||
@@ -3,12 +3,13 @@
|
|||||||
* Implements all binary and unary operators used by codegen
|
* Implements all binary and unary operators used by codegen
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { NixValue, NixList, NixAttrs, NixString } from "./types";
|
import type { NixValue, NixList, NixAttrs, NixString, NixPath } from "./types";
|
||||||
import { isStringWithContext } from "./types";
|
import { isStringWithContext, isNixPath } from "./types";
|
||||||
import { force } from "./thunk";
|
import { force } from "./thunk";
|
||||||
import { forceNumeric, forceList, forceAttrs, coerceNumeric } from "./type-assert";
|
import { forceNumeric, forceList, forceAttrs, coerceNumeric } from "./type-assert";
|
||||||
import { getStringValue, getStringContext, mergeContexts, mkStringWithContext } from "./string-context";
|
import { getStringValue, getStringContext, mergeContexts, mkStringWithContext } from "./string-context";
|
||||||
import { coerceToString, StringCoercionMode } from "./builtins/conversion";
|
import { coerceToString, StringCoercionMode } from "./builtins/conversion";
|
||||||
|
import { mkPath } from "./path";
|
||||||
|
|
||||||
const isNixString = (v: unknown): v is NixString => {
|
const isNixString = (v: unknown): v is NixString => {
|
||||||
return typeof v === "string" || isStringWithContext(v);
|
return typeof v === "string" || isStringWithContext(v);
|
||||||
@@ -28,10 +29,44 @@ const canCoerceToString = (v: NixValue): boolean => {
|
|||||||
* All operators referenced by codegen (e.g., Nix.op.add, Nix.op.eq)
|
* All operators referenced by codegen (e.g., Nix.op.add, Nix.op.eq)
|
||||||
*/
|
*/
|
||||||
export const op = {
|
export const op = {
|
||||||
add: (a: NixValue, b: NixValue): bigint | number | NixString => {
|
add: (a: NixValue, b: NixValue): bigint | number | NixString | NixPath => {
|
||||||
const av = force(a);
|
const av = force(a);
|
||||||
const bv = force(b);
|
const bv = force(b);
|
||||||
|
|
||||||
|
// Path concatenation: path + string = path
|
||||||
|
if (isNixPath(av)) {
|
||||||
|
if (isNixString(bv)) {
|
||||||
|
const strB = getStringValue(bv);
|
||||||
|
const ctxB = getStringContext(bv);
|
||||||
|
|
||||||
|
// Lix constraint: cannot append string with store context to path
|
||||||
|
if (ctxB.size > 0) {
|
||||||
|
throw new TypeError("a string that refers to a store path cannot be appended to a path");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Concatenate paths
|
||||||
|
return mkPath(av.value + strB);
|
||||||
|
}
|
||||||
|
|
||||||
|
// path + path: concatenate
|
||||||
|
if (isNixPath(bv)) {
|
||||||
|
return mkPath(av.value + bv.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// String + path: result is string (path coerces to string)
|
||||||
|
if (isNixString(av) && isNixPath(bv)) {
|
||||||
|
const strA = getStringValue(av);
|
||||||
|
const ctxA = getStringContext(av);
|
||||||
|
const result = strA + bv.value;
|
||||||
|
|
||||||
|
if (ctxA.size === 0) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
return mkStringWithContext(result, ctxA);
|
||||||
|
}
|
||||||
|
|
||||||
|
// String concatenation
|
||||||
if (isNixString(av) && isNixString(bv)) {
|
if (isNixString(av) && isNixString(bv)) {
|
||||||
const strA = getStringValue(av);
|
const strA = getStringValue(av);
|
||||||
const strB = getStringValue(bv);
|
const strB = getStringValue(bv);
|
||||||
@@ -45,12 +80,14 @@ export const op = {
|
|||||||
return mkStringWithContext(strA + strB, mergeContexts(ctxA, ctxB));
|
return mkStringWithContext(strA + strB, mergeContexts(ctxA, ctxB));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Auto-coerce to string if possible
|
||||||
if (canCoerceToString(a) && canCoerceToString(b)) {
|
if (canCoerceToString(a) && canCoerceToString(b)) {
|
||||||
const strA = coerceToString(a, StringCoercionMode.Interpolation, false);
|
const strA = coerceToString(a, StringCoercionMode.Interpolation, false);
|
||||||
const strB = coerceToString(b, StringCoercionMode.Interpolation, false);
|
const strB = coerceToString(b, StringCoercionMode.Interpolation, false);
|
||||||
return strA + strB;
|
return strA + strB;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Numeric addition
|
||||||
const [numA, numB] = coerceNumeric(forceNumeric(a), forceNumeric(b));
|
const [numA, numB] = coerceNumeric(forceNumeric(a), forceNumeric(b));
|
||||||
return (numA as any) + (numB as any);
|
return (numA as any) + (numB as any);
|
||||||
},
|
},
|
||||||
@@ -79,10 +116,17 @@ export const op = {
|
|||||||
const av = force(a);
|
const av = force(a);
|
||||||
const bv = force(b);
|
const bv = force(b);
|
||||||
|
|
||||||
|
// Path comparison
|
||||||
|
if (isNixPath(av) && isNixPath(bv)) {
|
||||||
|
return av.value === bv.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// String comparison
|
||||||
if (isNixString(av) && isNixString(bv)) {
|
if (isNixString(av) && isNixString(bv)) {
|
||||||
return getStringValue(av) === getStringValue(bv);
|
return getStringValue(av) === getStringValue(bv);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Numeric comparison with type coercion
|
||||||
if (typeof av === "bigint" && typeof bv === "number") {
|
if (typeof av === "bigint" && typeof bv === "number") {
|
||||||
return Number(av) === bv;
|
return Number(av) === bv;
|
||||||
}
|
}
|
||||||
@@ -90,6 +134,7 @@ export const op = {
|
|||||||
return av === Number(bv);
|
return av === Number(bv);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// List comparison
|
||||||
if (Array.isArray(av) && Array.isArray(bv)) {
|
if (Array.isArray(av) && Array.isArray(bv)) {
|
||||||
if (av.length !== bv.length) return false;
|
if (av.length !== bv.length) return false;
|
||||||
for (let i = 0; i < av.length; i++) {
|
for (let i = 0; i < av.length; i++) {
|
||||||
@@ -98,6 +143,7 @@ export const op = {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Attrset comparison
|
||||||
if (
|
if (
|
||||||
typeof av === "object" &&
|
typeof av === "object" &&
|
||||||
av !== null &&
|
av !== null &&
|
||||||
@@ -106,7 +152,9 @@ export const op = {
|
|||||||
bv !== null &&
|
bv !== null &&
|
||||||
!Array.isArray(bv) &&
|
!Array.isArray(bv) &&
|
||||||
!isNixString(av) &&
|
!isNixString(av) &&
|
||||||
!isNixString(bv)
|
!isNixString(bv) &&
|
||||||
|
!isNixPath(av) &&
|
||||||
|
!isNixPath(bv)
|
||||||
) {
|
) {
|
||||||
const keysA = Object.keys(av);
|
const keysA = Object.keys(av);
|
||||||
const keysB = Object.keys(bv);
|
const keysB = Object.keys(bv);
|
||||||
@@ -127,10 +175,17 @@ export const op = {
|
|||||||
const av = force(a);
|
const av = force(a);
|
||||||
const bv = force(b);
|
const bv = force(b);
|
||||||
|
|
||||||
|
// Path comparison
|
||||||
|
if (isNixPath(av) && isNixPath(bv)) {
|
||||||
|
return av.value < bv.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// String comparison
|
||||||
if (isNixString(av) && isNixString(bv)) {
|
if (isNixString(av) && isNixString(bv)) {
|
||||||
return getStringValue(av) < getStringValue(bv);
|
return getStringValue(av) < getStringValue(bv);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Numeric comparison
|
||||||
const [numA, numB] = coerceNumeric(forceNumeric(a), forceNumeric(b));
|
const [numA, numB] = coerceNumeric(forceNumeric(a), forceNumeric(b));
|
||||||
return (numA as any) < (numB as any);
|
return (numA as any) < (numB as any);
|
||||||
},
|
},
|
||||||
@@ -138,10 +193,17 @@ export const op = {
|
|||||||
const av = force(a);
|
const av = force(a);
|
||||||
const bv = force(b);
|
const bv = force(b);
|
||||||
|
|
||||||
|
// Path comparison
|
||||||
|
if (isNixPath(av) && isNixPath(bv)) {
|
||||||
|
return av.value <= bv.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// String comparison
|
||||||
if (isNixString(av) && isNixString(bv)) {
|
if (isNixString(av) && isNixString(bv)) {
|
||||||
return getStringValue(av) <= getStringValue(bv);
|
return getStringValue(av) <= getStringValue(bv);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Numeric comparison
|
||||||
const [numA, numB] = coerceNumeric(forceNumeric(a), forceNumeric(b));
|
const [numA, numB] = coerceNumeric(forceNumeric(a), forceNumeric(b));
|
||||||
return (numA as any) <= (numB as any);
|
return (numA as any) <= (numB as any);
|
||||||
},
|
},
|
||||||
@@ -149,10 +211,17 @@ export const op = {
|
|||||||
const av = force(a);
|
const av = force(a);
|
||||||
const bv = force(b);
|
const bv = force(b);
|
||||||
|
|
||||||
|
// Path comparison
|
||||||
|
if (isNixPath(av) && isNixPath(bv)) {
|
||||||
|
return av.value > bv.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// String comparison
|
||||||
if (isNixString(av) && isNixString(bv)) {
|
if (isNixString(av) && isNixString(bv)) {
|
||||||
return getStringValue(av) > getStringValue(bv);
|
return getStringValue(av) > getStringValue(bv);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Numeric comparison
|
||||||
const [numA, numB] = coerceNumeric(forceNumeric(a), forceNumeric(b));
|
const [numA, numB] = coerceNumeric(forceNumeric(a), forceNumeric(b));
|
||||||
return (numA as any) > (numB as any);
|
return (numA as any) > (numB as any);
|
||||||
},
|
},
|
||||||
@@ -160,10 +229,17 @@ export const op = {
|
|||||||
const av = force(a);
|
const av = force(a);
|
||||||
const bv = force(b);
|
const bv = force(b);
|
||||||
|
|
||||||
|
// Path comparison
|
||||||
|
if (isNixPath(av) && isNixPath(bv)) {
|
||||||
|
return av.value >= bv.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// String comparison
|
||||||
if (isNixString(av) && isNixString(bv)) {
|
if (isNixString(av) && isNixString(bv)) {
|
||||||
return getStringValue(av) >= getStringValue(bv);
|
return getStringValue(av) >= getStringValue(bv);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Numeric comparison
|
||||||
const [numA, numB] = coerceNumeric(forceNumeric(a), forceNumeric(b));
|
const [numA, numB] = coerceNumeric(forceNumeric(a), forceNumeric(b));
|
||||||
return (numA as any) >= (numB as any);
|
return (numA as any) >= (numB as any);
|
||||||
},
|
},
|
||||||
|
|||||||
9
nix-js/runtime-ts/src/path.ts
Normal file
9
nix-js/runtime-ts/src/path.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { IS_PATH, type NixPath } from "./types";
|
||||||
|
|
||||||
|
export const mkPath = (value: string): NixPath => {
|
||||||
|
return { [IS_PATH]: true, value };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getPathValue = (p: NixPath): string => {
|
||||||
|
return p.value;
|
||||||
|
};
|
||||||
@@ -32,15 +32,11 @@ export class NixThunk implements NixThunkInterface {
|
|||||||
func: (() => NixValue) | undefined;
|
func: (() => NixValue) | undefined;
|
||||||
result: NixStrictValue | undefined;
|
result: NixStrictValue | undefined;
|
||||||
readonly label: string | undefined;
|
readonly label: string | undefined;
|
||||||
readonly creationStack: string | undefined;
|
|
||||||
|
|
||||||
constructor(func: () => NixValue, label?: string) {
|
constructor(func: () => NixValue, label?: string) {
|
||||||
this.func = func;
|
this.func = func;
|
||||||
this.result = undefined;
|
this.result = undefined;
|
||||||
this.label = label;
|
this.label = label;
|
||||||
if (DEBUG_THUNKS.enabled) {
|
|
||||||
this.creationStack = new Error().stack?.split("\n").slice(2).join("\n");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
toString(): string {
|
toString(): string {
|
||||||
@@ -82,23 +78,13 @@ export const force = (value: NixValue): NixStrictValue => {
|
|||||||
if (value.result === undefined) {
|
if (value.result === undefined) {
|
||||||
const thunk = value as NixThunk;
|
const thunk = value as NixThunk;
|
||||||
let msg = `infinite recursion encountered at ${thunk}\n`;
|
let msg = `infinite recursion encountered at ${thunk}\n`;
|
||||||
msg += "Force chain (most recent first):\n";
|
if (DEBUG_THUNKS.enabled) {
|
||||||
for (let i = forceStack.length - 1; i >= 0; i--) {
|
msg += "Force chain (most recent first):\n";
|
||||||
const t = forceStack[i];
|
for (let i = forceStack.length - 1; i >= 0; i--) {
|
||||||
msg += ` ${i + 1}. ${t}`;
|
const t = forceStack[i];
|
||||||
if (DEBUG_THUNKS.enabled && t.creationStack) {
|
msg += ` ${i + 1}. ${t}`;
|
||||||
msg += `\n Created at:\n${t.creationStack
|
msg += "\n";
|
||||||
.split("\n")
|
|
||||||
.map((l) => " " + l)
|
|
||||||
.join("\n")}`;
|
|
||||||
}
|
}
|
||||||
msg += "\n";
|
|
||||||
}
|
|
||||||
if (DEBUG_THUNKS.enabled && thunk.creationStack) {
|
|
||||||
msg += `\nBlackhole thunk created at:\n${thunk.creationStack
|
|
||||||
.split("\n")
|
|
||||||
.map((l) => " " + l)
|
|
||||||
.join("\n")}`;
|
|
||||||
}
|
}
|
||||||
throw new Error(msg);
|
throw new Error(msg);
|
||||||
}
|
}
|
||||||
@@ -109,7 +95,9 @@ export const force = (value: NixValue): NixStrictValue => {
|
|||||||
const func = thunk.func!;
|
const func = thunk.func!;
|
||||||
thunk.func = undefined;
|
thunk.func = undefined;
|
||||||
|
|
||||||
forceStack.push(thunk);
|
if (DEBUG_THUNKS.enabled) {
|
||||||
|
forceStack.push(thunk);
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const result = force(func());
|
const result = force(func());
|
||||||
thunk.result = result;
|
thunk.result = result;
|
||||||
|
|||||||
@@ -12,14 +12,16 @@ import type {
|
|||||||
NixFloat,
|
NixFloat,
|
||||||
NixNumber,
|
NixNumber,
|
||||||
NixString,
|
NixString,
|
||||||
|
NixPath,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import { isStringWithContext } from "./types";
|
import { isStringWithContext, isNixPath } from "./types";
|
||||||
import { force } from "./thunk";
|
import { force } from "./thunk";
|
||||||
import { getStringValue } from "./string-context";
|
import { getStringValue } from "./string-context";
|
||||||
|
|
||||||
export const typeName = (value: NixValue): string => {
|
export const typeName = (value: NixValue): string => {
|
||||||
const val = force(value);
|
const val = force(value);
|
||||||
|
|
||||||
|
if (isNixPath(val)) return "path";
|
||||||
if (typeof val === "bigint") return "int";
|
if (typeof val === "bigint") return "int";
|
||||||
if (typeof val === "number") return "float";
|
if (typeof val === "number") return "float";
|
||||||
if (typeof val === "boolean") return "boolean";
|
if (typeof val === "boolean") return "boolean";
|
||||||
@@ -63,7 +65,13 @@ export const forceFunction = (value: NixValue): NixFunction => {
|
|||||||
*/
|
*/
|
||||||
export const forceAttrs = (value: NixValue): NixAttrs => {
|
export const forceAttrs = (value: NixValue): NixAttrs => {
|
||||||
const forced = force(value);
|
const forced = force(value);
|
||||||
if (typeof forced !== "object" || Array.isArray(forced) || forced === null || isStringWithContext(forced)) {
|
if (
|
||||||
|
typeof forced !== "object" ||
|
||||||
|
Array.isArray(forced) ||
|
||||||
|
forced === null ||
|
||||||
|
isStringWithContext(forced) ||
|
||||||
|
isNixPath(forced)
|
||||||
|
) {
|
||||||
throw new TypeError(`Expected attribute set, got ${typeName(forced)}`);
|
throw new TypeError(`Expected attribute set, got ${typeName(forced)}`);
|
||||||
}
|
}
|
||||||
return forced;
|
return forced;
|
||||||
@@ -171,3 +179,15 @@ export const coerceNumeric = (a: NixNumber, b: NixNumber): [NixFloat, NixFloat]
|
|||||||
// Both are integers
|
// Both are integers
|
||||||
return [a, b];
|
return [a, b];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Force a value and assert it's a path
|
||||||
|
* @throws TypeError if value is not a path after forcing
|
||||||
|
*/
|
||||||
|
export const forceNixPath = (value: NixValue): NixPath => {
|
||||||
|
const forced = force(value);
|
||||||
|
if (isNixPath(forced)) {
|
||||||
|
return forced;
|
||||||
|
}
|
||||||
|
throw new TypeError(`Expected path, got ${typeName(forced)}`);
|
||||||
|
};
|
||||||
|
|||||||
@@ -7,6 +7,17 @@ import { type StringWithContext, HAS_CONTEXT, isStringWithContext } from "./stri
|
|||||||
export { HAS_CONTEXT, isStringWithContext };
|
export { HAS_CONTEXT, isStringWithContext };
|
||||||
export type { StringWithContext };
|
export type { StringWithContext };
|
||||||
|
|
||||||
|
export const IS_PATH = Symbol("IS_PATH");
|
||||||
|
|
||||||
|
export interface NixPath {
|
||||||
|
readonly [IS_PATH]: true;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isNixPath = (v: NixStrictValue): v is NixPath => {
|
||||||
|
return typeof v === "object" && v !== null && IS_PATH in v && (v as NixPath)[IS_PATH] === true;
|
||||||
|
};
|
||||||
|
|
||||||
// Nix primitive types
|
// Nix primitive types
|
||||||
export type NixInt = bigint;
|
export type NixInt = bigint;
|
||||||
export type NixFloat = number;
|
export type NixFloat = number;
|
||||||
@@ -37,7 +48,7 @@ export type NixPrimitive = NixNull | NixBool | NixInt | NixFloat | NixString;
|
|||||||
* NixValue: Union type representing any possible Nix value
|
* NixValue: Union type representing any possible Nix value
|
||||||
* This is the core type used throughout the runtime
|
* This is the core type used throughout the runtime
|
||||||
*/
|
*/
|
||||||
export type NixValue = NixPrimitive | NixList | NixAttrs | NixFunction | NixThunkInterface;
|
export type NixValue = NixPrimitive | NixPath | NixList | NixAttrs | NixFunction | NixThunkInterface;
|
||||||
|
|
||||||
export type NixStrictValue = Exclude<NixValue, NixThunkInterface>;
|
export type NixStrictValue = Exclude<NixValue, NixThunkInterface>;
|
||||||
|
|
||||||
|
|||||||
6
nix-js/runtime-ts/src/types/global.d.ts
vendored
6
nix-js/runtime-ts/src/types/global.d.ts
vendored
@@ -70,6 +70,12 @@ declare global {
|
|||||||
name: string | null,
|
name: string | null,
|
||||||
): FetchGitResult;
|
): FetchGitResult;
|
||||||
function op_fetch_hg(url: string, rev: string | null, name: string | null): FetchHgResult;
|
function op_fetch_hg(url: string, rev: string | null, name: string | null): FetchHgResult;
|
||||||
|
function op_add_path(
|
||||||
|
path: string,
|
||||||
|
name: string | null,
|
||||||
|
recursive: boolean,
|
||||||
|
sha256: string | null,
|
||||||
|
): string;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,8 +77,9 @@ impl<Ctx: CodegenContext> Compile<Ctx> for Ir {
|
|||||||
Ir::HasAttr(x) => x.compile(ctx),
|
Ir::HasAttr(x) => x.compile(ctx),
|
||||||
&Ir::Assert(Assert { assertion, expr }) => {
|
&Ir::Assert(Assert { assertion, expr }) => {
|
||||||
let assertion = ctx.get_ir(assertion).compile(ctx);
|
let assertion = ctx.get_ir(assertion).compile(ctx);
|
||||||
|
let expr_dbg = ctx.get_ir(expr);
|
||||||
let expr = ctx.get_ir(expr).compile(ctx);
|
let expr = ctx.get_ir(expr).compile(ctx);
|
||||||
format!("({assertion})?({expr}):(()=>{{throw new Error(\"assertion failed\")}})()")
|
format!("({assertion})?({expr}):(()=>{{throw new Error(`assertion failed ({expr_dbg:#?})`)}})()")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ fn runtime_extension<Ctx: RuntimeCtx>() -> Extension {
|
|||||||
op_make_store_path(),
|
op_make_store_path(),
|
||||||
op_output_path_name(),
|
op_output_path_name(),
|
||||||
op_make_fixed_output_path(),
|
op_make_fixed_output_path(),
|
||||||
|
op_add_path(),
|
||||||
];
|
];
|
||||||
ops.extend(crate::fetcher::register_ops());
|
ops.extend(crate::fetcher::register_ops());
|
||||||
|
|
||||||
@@ -186,11 +187,104 @@ fn op_make_fixed_output_path(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[deno_core::op2]
|
||||||
|
#[string]
|
||||||
|
fn op_add_path(
|
||||||
|
#[string] path: String,
|
||||||
|
#[string] name: Option<String>,
|
||||||
|
recursive: bool,
|
||||||
|
#[string] sha256: Option<String>,
|
||||||
|
) -> std::result::Result<String, NixError> {
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
|
use std::fs;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
let path_obj = Path::new(&path);
|
||||||
|
|
||||||
|
if !path_obj.exists() {
|
||||||
|
return Err(NixError::from(format!("path '{}' does not exist", path)));
|
||||||
|
}
|
||||||
|
|
||||||
|
let computed_name = name.unwrap_or_else(|| {
|
||||||
|
path_obj
|
||||||
|
.file_name()
|
||||||
|
.and_then(|n| n.to_str())
|
||||||
|
.unwrap_or("source")
|
||||||
|
.to_string()
|
||||||
|
});
|
||||||
|
|
||||||
|
let computed_hash = if recursive {
|
||||||
|
compute_nar_hash(path_obj)?
|
||||||
|
} else {
|
||||||
|
if !path_obj.is_file() {
|
||||||
|
return Err(NixError::from(
|
||||||
|
"when 'recursive' is false, path must be a regular file",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let contents = fs::read(path_obj)
|
||||||
|
.map_err(|e| NixError::from(format!("failed to read '{}': {}", path, e)))?;
|
||||||
|
|
||||||
|
let mut hasher = Sha256::new();
|
||||||
|
hasher.update(&contents);
|
||||||
|
hex::encode(hasher.finalize())
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(expected_hash) = sha256 {
|
||||||
|
if computed_hash != expected_hash {
|
||||||
|
return Err(NixError::from(format!(
|
||||||
|
"hash mismatch for path '{}': expected {}, got {}",
|
||||||
|
path, expected_hash, computed_hash
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let store_path = crate::nix_hash::make_store_path("source", &computed_hash, &computed_name);
|
||||||
|
|
||||||
|
Ok(store_path)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn compute_nar_hash(path: &std::path::Path) -> std::result::Result<String, NixError> {
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
|
use std::fs;
|
||||||
|
|
||||||
|
if path.is_file() {
|
||||||
|
let contents = fs::read(path)
|
||||||
|
.map_err(|e| NixError::from(format!("failed to read file: {}", e)))?;
|
||||||
|
let mut hasher = Sha256::new();
|
||||||
|
hasher.update(&contents);
|
||||||
|
Ok(hex::encode(hasher.finalize()))
|
||||||
|
} else if path.is_dir() {
|
||||||
|
let mut entries: Vec<_> = fs::read_dir(path)
|
||||||
|
.map_err(|e| NixError::from(format!("failed to read directory: {}", e)))?
|
||||||
|
.filter_map(std::result::Result::ok)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
entries.sort_by_key(|e| e.file_name());
|
||||||
|
|
||||||
|
let mut hasher = Sha256::new();
|
||||||
|
for entry in entries {
|
||||||
|
let entry_path = entry.path();
|
||||||
|
let entry_name = entry.file_name();
|
||||||
|
|
||||||
|
hasher.update(entry_name.to_string_lossy().as_bytes());
|
||||||
|
|
||||||
|
let entry_hash = compute_nar_hash(&entry_path)?;
|
||||||
|
hasher.update(entry_hash.as_bytes());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(hex::encode(hasher.finalize()))
|
||||||
|
} else {
|
||||||
|
Ok(String::new())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
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>,
|
||||||
primop_metadata_symbol: v8::Global<v8::Symbol>,
|
primop_metadata_symbol: v8::Global<v8::Symbol>,
|
||||||
has_context_symbol: v8::Global<v8::Symbol>,
|
has_context_symbol: v8::Global<v8::Symbol>,
|
||||||
|
is_path_symbol: v8::Global<v8::Symbol>,
|
||||||
_marker: PhantomData<Ctx>,
|
_marker: PhantomData<Ctx>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -210,7 +304,7 @@ impl<Ctx: RuntimeCtx> Runtime<Ctx> {
|
|||||||
..Default::default()
|
..Default::default()
|
||||||
});
|
});
|
||||||
|
|
||||||
let (is_thunk_symbol, primop_metadata_symbol, has_context_symbol) = {
|
let (is_thunk_symbol, primop_metadata_symbol, has_context_symbol, is_path_symbol) = {
|
||||||
deno_core::scope!(scope, &mut js_runtime);
|
deno_core::scope!(scope, &mut js_runtime);
|
||||||
Self::get_symbols(scope)?
|
Self::get_symbols(scope)?
|
||||||
};
|
};
|
||||||
@@ -220,6 +314,7 @@ impl<Ctx: RuntimeCtx> Runtime<Ctx> {
|
|||||||
is_thunk_symbol,
|
is_thunk_symbol,
|
||||||
primop_metadata_symbol,
|
primop_metadata_symbol,
|
||||||
has_context_symbol,
|
has_context_symbol,
|
||||||
|
is_path_symbol,
|
||||||
_marker: PhantomData,
|
_marker: PhantomData,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -238,6 +333,7 @@ impl<Ctx: RuntimeCtx> Runtime<Ctx> {
|
|||||||
let is_thunk_symbol = v8::Local::new(scope, &self.is_thunk_symbol);
|
let is_thunk_symbol = v8::Local::new(scope, &self.is_thunk_symbol);
|
||||||
let primop_metadata_symbol = v8::Local::new(scope, &self.primop_metadata_symbol);
|
let primop_metadata_symbol = v8::Local::new(scope, &self.primop_metadata_symbol);
|
||||||
let has_context_symbol = v8::Local::new(scope, &self.has_context_symbol);
|
let has_context_symbol = v8::Local::new(scope, &self.has_context_symbol);
|
||||||
|
let is_path_symbol = v8::Local::new(scope, &self.is_path_symbol);
|
||||||
|
|
||||||
Ok(to_value(
|
Ok(to_value(
|
||||||
local_value,
|
local_value,
|
||||||
@@ -245,6 +341,7 @@ impl<Ctx: RuntimeCtx> Runtime<Ctx> {
|
|||||||
is_thunk_symbol,
|
is_thunk_symbol,
|
||||||
primop_metadata_symbol,
|
primop_metadata_symbol,
|
||||||
has_context_symbol,
|
has_context_symbol,
|
||||||
|
is_path_symbol,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -255,6 +352,7 @@ impl<Ctx: RuntimeCtx> Runtime<Ctx> {
|
|||||||
v8::Global<v8::Symbol>,
|
v8::Global<v8::Symbol>,
|
||||||
v8::Global<v8::Symbol>,
|
v8::Global<v8::Symbol>,
|
||||||
v8::Global<v8::Symbol>,
|
v8::Global<v8::Symbol>,
|
||||||
|
v8::Global<v8::Symbol>,
|
||||||
)> {
|
)> {
|
||||||
let global = scope.get_current_context().global(scope);
|
let global = scope.get_current_context().global(scope);
|
||||||
let nix_key = v8::String::new(scope, "Nix")
|
let nix_key = v8::String::new(scope, "Nix")
|
||||||
@@ -305,7 +403,19 @@ impl<Ctx: RuntimeCtx> Runtime<Ctx> {
|
|||||||
})?;
|
})?;
|
||||||
let has_context = v8::Global::new(scope, has_context);
|
let has_context = v8::Global::new(scope, has_context);
|
||||||
|
|
||||||
Ok((is_thunk, primop_metadata, has_context))
|
let is_path_sym_key = v8::String::new(scope, "IS_PATH")
|
||||||
|
.ok_or_else(|| Error::internal("failed to create V8 String".into()))?;
|
||||||
|
let is_path_sym = nix_obj
|
||||||
|
.get(scope, is_path_sym_key.into())
|
||||||
|
.ok_or_else(|| Error::internal("failed to get IS_PATH Symbol".into()))?;
|
||||||
|
let is_path = is_path_sym.try_cast::<v8::Symbol>().map_err(|err| {
|
||||||
|
Error::internal(format!(
|
||||||
|
"failed to convert IS_PATH Value to Symbol ({err})"
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
let is_path = v8::Global::new(scope, is_path);
|
||||||
|
|
||||||
|
Ok((is_thunk, primop_metadata, has_context, is_path))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -315,6 +425,7 @@ fn to_value<'a>(
|
|||||||
is_thunk_symbol: LocalSymbol<'a>,
|
is_thunk_symbol: LocalSymbol<'a>,
|
||||||
primop_metadata_symbol: LocalSymbol<'a>,
|
primop_metadata_symbol: LocalSymbol<'a>,
|
||||||
has_context_symbol: LocalSymbol<'a>,
|
has_context_symbol: LocalSymbol<'a>,
|
||||||
|
is_path_symbol: LocalSymbol<'a>,
|
||||||
) -> Value {
|
) -> Value {
|
||||||
match () {
|
match () {
|
||||||
_ if val.is_big_int() => {
|
_ if val.is_big_int() => {
|
||||||
@@ -350,6 +461,7 @@ fn to_value<'a>(
|
|||||||
is_thunk_symbol,
|
is_thunk_symbol,
|
||||||
primop_metadata_symbol,
|
primop_metadata_symbol,
|
||||||
has_context_symbol,
|
has_context_symbol,
|
||||||
|
is_path_symbol,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
@@ -367,6 +479,10 @@ fn to_value<'a>(
|
|||||||
return Value::Thunk;
|
return Value::Thunk;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(path_val) = extract_path(val, scope, is_path_symbol) {
|
||||||
|
return Value::Path(path_val);
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(string_val) = extract_string_with_context(val, scope, has_context_symbol) {
|
if let Some(string_val) = extract_string_with_context(val, scope, has_context_symbol) {
|
||||||
return Value::String(string_val);
|
return Value::String(string_val);
|
||||||
}
|
}
|
||||||
@@ -391,6 +507,7 @@ fn to_value<'a>(
|
|||||||
is_thunk_symbol,
|
is_thunk_symbol,
|
||||||
primop_metadata_symbol,
|
primop_metadata_symbol,
|
||||||
has_context_symbol,
|
has_context_symbol,
|
||||||
|
is_path_symbol,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@@ -436,6 +553,32 @@ fn extract_string_with_context<'a>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn extract_path<'a>(
|
||||||
|
val: LocalValue<'a>,
|
||||||
|
scope: &ScopeRef<'a, '_>,
|
||||||
|
symbol: LocalSymbol<'a>,
|
||||||
|
) -> Option<String> {
|
||||||
|
if !val.is_object() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let obj = val.to_object(scope).expect("infallible conversion");
|
||||||
|
let is_path = obj.get(scope, symbol.into())?;
|
||||||
|
|
||||||
|
if !is_path.is_true() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let value_key = v8::String::new(scope, "value")?;
|
||||||
|
let value = obj.get(scope, value_key.into())?;
|
||||||
|
|
||||||
|
if value.is_string() {
|
||||||
|
Some(value.to_rust_string_lossy(scope))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn to_primop<'a>(
|
fn to_primop<'a>(
|
||||||
val: LocalValue<'a>,
|
val: LocalValue<'a>,
|
||||||
scope: &ScopeRef<'a, '_>,
|
scope: &ScopeRef<'a, '_>,
|
||||||
|
|||||||
@@ -177,6 +177,8 @@ pub enum Value {
|
|||||||
Null,
|
Null,
|
||||||
/// A string value.
|
/// A string value.
|
||||||
String(String),
|
String(String),
|
||||||
|
/// A path value (absolute path string).
|
||||||
|
Path(String),
|
||||||
/// An attribute set.
|
/// An attribute set.
|
||||||
AttrSet(AttrSet),
|
AttrSet(AttrSet),
|
||||||
/// A list.
|
/// A list.
|
||||||
@@ -203,6 +205,7 @@ impl Display for Value {
|
|||||||
&Bool(x) => write!(f, "{x}"),
|
&Bool(x) => write!(f, "{x}"),
|
||||||
Null => write!(f, "null"),
|
Null => write!(f, "null"),
|
||||||
String(x) => write!(f, r#""{x}""#),
|
String(x) => write!(f, r#""{x}""#),
|
||||||
|
Path(x) => write!(f, "{x}"),
|
||||||
AttrSet(x) => write!(f, "{x}"),
|
AttrSet(x) => write!(f, "{x}"),
|
||||||
List(x) => write!(f, "{x}"),
|
List(x) => write!(f, "{x}"),
|
||||||
Thunk => write!(f, "«code»"),
|
Thunk => write!(f, "«code»"),
|
||||||
|
|||||||
@@ -100,3 +100,186 @@ fn import_with_complex_dependency_graph() {
|
|||||||
let expr = format!(r#"import "{}""#, main_path.display());
|
let expr = format!(r#"import "{}""#, main_path.display());
|
||||||
assert_eq!(ctx.eval_code(&expr).unwrap(), Value::Int(15));
|
assert_eq!(ctx.eval_code(&expr).unwrap(), Value::Int(15));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Tests for builtins.path
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_path_with_file() {
|
||||||
|
let mut ctx = Context::new().unwrap();
|
||||||
|
let temp_dir = tempfile::tempdir().unwrap();
|
||||||
|
let test_file = temp_dir.path().join("test.txt");
|
||||||
|
std::fs::write(&test_file, "Hello, World!").unwrap();
|
||||||
|
|
||||||
|
let expr = format!(
|
||||||
|
r#"builtins.path {{ path = {}; }}"#,
|
||||||
|
test_file.display()
|
||||||
|
);
|
||||||
|
let result = ctx.eval_code(&expr).unwrap();
|
||||||
|
|
||||||
|
// Should return a store path string
|
||||||
|
if let Value::String(store_path) = result {
|
||||||
|
assert!(store_path.starts_with("/nix/store/"));
|
||||||
|
assert!(store_path.contains("test.txt"));
|
||||||
|
} else {
|
||||||
|
panic!("Expected string, got {:?}", result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_path_with_custom_name() {
|
||||||
|
let mut ctx = Context::new().unwrap();
|
||||||
|
let temp_dir = tempfile::tempdir().unwrap();
|
||||||
|
let test_file = temp_dir.path().join("original.txt");
|
||||||
|
std::fs::write(&test_file, "Content").unwrap();
|
||||||
|
|
||||||
|
let expr = format!(
|
||||||
|
r#"builtins.path {{ path = {}; name = "custom-name"; }}"#,
|
||||||
|
test_file.display()
|
||||||
|
);
|
||||||
|
let result = ctx.eval_code(&expr).unwrap();
|
||||||
|
|
||||||
|
if let Value::String(store_path) = result {
|
||||||
|
assert!(store_path.contains("custom-name"));
|
||||||
|
assert!(!store_path.contains("original.txt"));
|
||||||
|
} else {
|
||||||
|
panic!("Expected string, got {:?}", result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_path_with_directory_recursive() {
|
||||||
|
let mut ctx = Context::new().unwrap();
|
||||||
|
let temp_dir = tempfile::tempdir().unwrap();
|
||||||
|
let test_dir = temp_dir.path().join("mydir");
|
||||||
|
std::fs::create_dir_all(&test_dir).unwrap();
|
||||||
|
std::fs::write(test_dir.join("file1.txt"), "Content 1").unwrap();
|
||||||
|
std::fs::write(test_dir.join("file2.txt"), "Content 2").unwrap();
|
||||||
|
|
||||||
|
let expr = format!(
|
||||||
|
r#"builtins.path {{ path = {}; recursive = true; }}"#,
|
||||||
|
test_dir.display()
|
||||||
|
);
|
||||||
|
let result = ctx.eval_code(&expr).unwrap();
|
||||||
|
|
||||||
|
if let Value::String(store_path) = result {
|
||||||
|
assert!(store_path.starts_with("/nix/store/"));
|
||||||
|
assert!(store_path.contains("mydir"));
|
||||||
|
} else {
|
||||||
|
panic!("Expected string, got {:?}", result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_path_flat_with_file() {
|
||||||
|
let mut ctx = Context::new().unwrap();
|
||||||
|
let temp_dir = tempfile::tempdir().unwrap();
|
||||||
|
let test_file = temp_dir.path().join("flat.txt");
|
||||||
|
std::fs::write(&test_file, "Flat content").unwrap();
|
||||||
|
|
||||||
|
let expr = format!(
|
||||||
|
r#"builtins.path {{ path = {}; recursive = false; }}"#,
|
||||||
|
test_file.display()
|
||||||
|
);
|
||||||
|
let result = ctx.eval_code(&expr).unwrap();
|
||||||
|
|
||||||
|
if let Value::String(store_path) = result {
|
||||||
|
assert!(store_path.starts_with("/nix/store/"));
|
||||||
|
} else {
|
||||||
|
panic!("Expected string, got {:?}", result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_path_flat_with_directory_fails() {
|
||||||
|
let mut ctx = Context::new().unwrap();
|
||||||
|
let temp_dir = tempfile::tempdir().unwrap();
|
||||||
|
let test_dir = temp_dir.path().join("mydir");
|
||||||
|
std::fs::create_dir_all(&test_dir).unwrap();
|
||||||
|
|
||||||
|
let expr = format!(
|
||||||
|
r#"builtins.path {{ path = {}; recursive = false; }}"#,
|
||||||
|
test_dir.display()
|
||||||
|
);
|
||||||
|
let result = ctx.eval_code(&expr);
|
||||||
|
|
||||||
|
assert!(result.is_err());
|
||||||
|
let err_msg = result.unwrap_err().to_string();
|
||||||
|
assert!(err_msg.contains("recursive") || err_msg.contains("regular file"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_path_nonexistent_fails() {
|
||||||
|
let mut ctx = Context::new().unwrap();
|
||||||
|
|
||||||
|
let expr = r#"builtins.path { path = "/nonexistent/path/that/should/not/exist"; }"#;
|
||||||
|
let result = ctx.eval_code(expr);
|
||||||
|
|
||||||
|
assert!(result.is_err());
|
||||||
|
let err_msg = result.unwrap_err().to_string();
|
||||||
|
assert!(err_msg.contains("does not exist"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_path_missing_path_param() {
|
||||||
|
let mut ctx = Context::new().unwrap();
|
||||||
|
|
||||||
|
let expr = r#"builtins.path { name = "test"; }"#;
|
||||||
|
let result = ctx.eval_code(expr);
|
||||||
|
|
||||||
|
assert!(result.is_err());
|
||||||
|
let err_msg = result.unwrap_err().to_string();
|
||||||
|
assert!(err_msg.contains("path") && err_msg.contains("required"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_path_with_sha256() {
|
||||||
|
let mut ctx = Context::new().unwrap();
|
||||||
|
let temp_dir = tempfile::tempdir().unwrap();
|
||||||
|
let test_file = temp_dir.path().join("hash_test.txt");
|
||||||
|
std::fs::write(&test_file, "Test content for hashing").unwrap();
|
||||||
|
|
||||||
|
// First, get the hash by calling without sha256
|
||||||
|
let expr1 = format!(
|
||||||
|
r#"builtins.path {{ path = {}; }}"#,
|
||||||
|
test_file.display()
|
||||||
|
);
|
||||||
|
let result1 = ctx.eval_code(&expr1).unwrap();
|
||||||
|
let store_path1 = match result1 {
|
||||||
|
Value::String(s) => s,
|
||||||
|
_ => panic!("Expected string"),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Compute the actual hash (for testing, we'll just verify the same path is returned)
|
||||||
|
// In real usage, the user would know the hash beforehand
|
||||||
|
let expr2 = format!(
|
||||||
|
r#"builtins.path {{ path = {}; }}"#,
|
||||||
|
test_file.display()
|
||||||
|
);
|
||||||
|
let result2 = ctx.eval_code(&expr2).unwrap();
|
||||||
|
let store_path2 = match result2 {
|
||||||
|
Value::String(s) => s,
|
||||||
|
_ => panic!("Expected string"),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Same input should produce same output
|
||||||
|
assert_eq!(store_path1, store_path2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_path_deterministic() {
|
||||||
|
let mut ctx = Context::new().unwrap();
|
||||||
|
let temp_dir = tempfile::tempdir().unwrap();
|
||||||
|
let test_file = temp_dir.path().join("deterministic.txt");
|
||||||
|
std::fs::write(&test_file, "Same content").unwrap();
|
||||||
|
|
||||||
|
let expr = format!(
|
||||||
|
r#"builtins.path {{ path = {}; name = "myfile"; }}"#,
|
||||||
|
test_file.display()
|
||||||
|
);
|
||||||
|
|
||||||
|
let result1 = ctx.eval_code(&expr).unwrap();
|
||||||
|
let result2 = ctx.eval_code(&expr).unwrap();
|
||||||
|
|
||||||
|
// Same inputs should produce same store path
|
||||||
|
assert_eq!(result1, result2);
|
||||||
|
}
|
||||||
|
|||||||
118
nix-js/tests/path_operations.rs
Normal file
118
nix-js/tests/path_operations.rs
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
mod utils;
|
||||||
|
|
||||||
|
use nix_js::value::Value;
|
||||||
|
use utils::{eval, eval_result};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_path_type_of() {
|
||||||
|
let result = eval("builtins.typeOf ./foo");
|
||||||
|
assert_eq!(result, Value::String("path".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_is_path_true() {
|
||||||
|
let result = eval("builtins.isPath ./foo");
|
||||||
|
assert_eq!(result, Value::Bool(true));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_is_path_false_string() {
|
||||||
|
let result = eval(r#"builtins.isPath "./foo""#);
|
||||||
|
assert_eq!(result, Value::Bool(false));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_is_path_false_number() {
|
||||||
|
let result = eval("builtins.isPath 42");
|
||||||
|
assert_eq!(result, Value::Bool(false));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_path_concat_type() {
|
||||||
|
// path + string = path
|
||||||
|
let result = eval(r#"builtins.typeOf (./foo + "/bar")"#);
|
||||||
|
assert_eq!(result, Value::String("path".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_string_path_concat_type() {
|
||||||
|
// string + path = string
|
||||||
|
let result = eval(r#"builtins.typeOf ("prefix-" + ./foo)"#);
|
||||||
|
assert_eq!(result, Value::String("string".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_basename_of_path() {
|
||||||
|
let result = eval("builtins.baseNameOf ./path/to/file.nix");
|
||||||
|
assert!(matches!(result, Value::String(s) if s == "file.nix"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_basename_of_string() {
|
||||||
|
let result = eval(r#"builtins.baseNameOf "/path/to/file.nix""#);
|
||||||
|
assert_eq!(result, Value::String("file.nix".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_dir_of_path_type() {
|
||||||
|
// dirOf preserves path type
|
||||||
|
let result = eval("builtins.typeOf (builtins.dirOf ./path/to/file.nix)");
|
||||||
|
assert_eq!(result, Value::String("path".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_dir_of_string_type() {
|
||||||
|
// dirOf preserves string type
|
||||||
|
let result = eval(r#"builtins.typeOf (builtins.dirOf "/path/to/file.nix")"#);
|
||||||
|
assert_eq!(result, Value::String("string".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_path_equality() {
|
||||||
|
// Same path should be equal
|
||||||
|
let result = eval("./foo == ./foo");
|
||||||
|
assert_eq!(result, Value::Bool(true));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_path_not_equal_string() {
|
||||||
|
// Paths and strings are different types - should not be equal
|
||||||
|
let result = eval(r#"./foo == "./foo""#);
|
||||||
|
assert_eq!(result, Value::Bool(false));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_to_path_absolute() {
|
||||||
|
// toPath with absolute path returns string
|
||||||
|
let result = eval(r#"builtins.toPath "/foo/bar""#);
|
||||||
|
assert_eq!(result, Value::String("/foo/bar".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_to_path_type_is_string() {
|
||||||
|
// toPath returns a string, not a path
|
||||||
|
let result = eval(r#"builtins.typeOf (builtins.toPath "/foo")"#);
|
||||||
|
assert_eq!(result, Value::String("string".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_to_path_relative_fails() {
|
||||||
|
// toPath with relative path should fail
|
||||||
|
let result = eval_result(r#"builtins.toPath "foo/bar""#);
|
||||||
|
assert!(result.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_to_path_empty_fails() {
|
||||||
|
// toPath with empty string should fail
|
||||||
|
let result = eval_result(r#"builtins.toPath """#);
|
||||||
|
assert!(result.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_to_path_from_path_value() {
|
||||||
|
// toPath can accept a path value too (coerces to string first)
|
||||||
|
let result = eval("builtins.toPath ./foo");
|
||||||
|
// Should succeed and return the absolute path as a string
|
||||||
|
assert!(matches!(result, Value::String(s) if s.starts_with("/")));
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user