diff --git a/nix-js/runtime-ts/src/builtins/io.ts b/nix-js/runtime-ts/src/builtins/io.ts index 5a3bc20..40b7dc6 100644 --- a/nix-js/runtime-ts/src/builtins/io.ts +++ b/nix-js/runtime-ts/src/builtins/io.ts @@ -11,13 +11,13 @@ import { forceStringNoCtx, forceStringValue, } from "../type-assert"; -import type { NixValue, NixAttrs, NixPath } from "../types"; +import type { NixValue, NixAttrs, NixPath, NixString } from "../types"; import { isNixPath, IS_PATH, CatchableError } from "../types"; import { force } from "../thunk"; import { coerceToPath, coerceToString, StringCoercionMode } from "./conversion"; import { getPathValue } from "../path"; import type { NixStringContext, StringWithContext } from "../string-context"; -import { mkStringWithContext } from "../string-context"; +import { mkStringWithContext, addOpaqueContext } from "../string-context"; import { isAttrs, isPath } from "./type-check"; import { baseNameOf } from "./path"; @@ -151,7 +151,7 @@ const resolvePseudoUrl = (url: string) => { } } -export const fetchurl = (args: NixValue): string => { +export const fetchurl = (args: NixValue): NixString => { const { url, hash, name, executable } = normalizeUrlInput(args); const result: FetchUrlResult = Deno.core.ops.op_fetch_url( url, @@ -159,13 +159,17 @@ export const fetchurl = (args: NixValue): string => { name ?? null, executable ?? false, ); - return result.store_path; + const context: NixStringContext = new Set(); + addOpaqueContext(context, result.store_path); + return mkStringWithContext(result.store_path, context); }; -export const fetchTarball = (args: NixValue): string => { +export const fetchTarball = (args: NixValue): NixString => { const { url, name, sha256 } = normalizeTarballInput(args); const result: FetchTarballResult = Deno.core.ops.op_fetch_tarball(url, name ?? null, sha256 ?? null); - return result.store_path; + const context: NixStringContext = new Set(); + addOpaqueContext(context, result.store_path); + return mkStringWithContext(result.store_path, context); }; export const fetchGit = (args: NixValue): NixAttrs => { @@ -173,8 +177,10 @@ export const fetchGit = (args: NixValue): NixAttrs => { if (typeof forced === "string" || isPath(forced)) { const path = coerceToPath(forced); const result: FetchGitResult = Deno.core.ops.op_fetch_git(path, null, null, false, false, false, null); + const outContext: NixStringContext = new Set(); + addOpaqueContext(outContext, result.out_path); return { - outPath: result.out_path, + outPath: mkStringWithContext(result.out_path, outContext), rev: result.rev, shortRev: result.short_rev, revCount: BigInt(result.rev_count), @@ -203,8 +209,10 @@ export const fetchGit = (args: NixValue): NixAttrs => { name, ); + const outContext: NixStringContext = new Set(); + addOpaqueContext(outContext, result.out_path); return { - outPath: result.out_path, + outPath: mkStringWithContext(result.out_path, outContext), rev: result.rev, shortRev: result.short_rev, revCount: BigInt(result.rev_count), @@ -338,10 +346,9 @@ export const pathExists = (path: NixValue): boolean => { * * Returns: Store path string */ -export const path = (args: NixValue): string => { +export const path = (args: NixValue): NixString => { const attrs = forceAttrs(args); - // Required: path parameter if (!("path" in attrs)) { throw new TypeError("builtins.path: 'path' attribute is required"); } @@ -349,23 +356,18 @@ export const path = (args: NixValue): string => { const pathValue = force(attrs.path); let pathStr: string; - // Accept both Path values and strings if (isNixPath(pathValue)) { pathStr = getPathValue(pathValue); } else { pathStr = forceStringValue(pathValue); } - // Optional: name parameter (defaults to basename in Rust) const name = "name" in attrs ? forceStringValue(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 ? forceStringValue(attrs.sha256) : null; - // Handle filter parameter + let storePath: string; + if ("filter" in attrs) { const filterFn = forceFunction(attrs.filter); @@ -381,20 +383,20 @@ export const path = (args: NixValue): string => { } } - const storePath: string = Deno.core.ops.op_add_filtered_path( + storePath = Deno.core.ops.op_add_filtered_path( pathStr, name, recursive, sha256, includePaths, ); - return storePath; + } else { + storePath = Deno.core.ops.op_add_path(pathStr, name, recursive, sha256); } - // Call Rust op to add path to store - const storePath: string = Deno.core.ops.op_add_path(pathStr, name, recursive, sha256); - - return storePath; + const context: NixStringContext = new Set(); + addOpaqueContext(context, storePath); + return mkStringWithContext(storePath, context); }; export const toFile = diff --git a/nix-js/tests/string_context.rs b/nix-js/tests/string_context.rs index edf1d85..7331a7a 100644 --- a/nix-js/tests/string_context.rs +++ b/nix-js/tests/string_context.rs @@ -489,3 +489,76 @@ fn split_no_match_preserves_context() { ); assert_eq!(result, Value::Bool(true)); } + +#[test] +fn builtins_path_has_context() { + let temp_dir = tempfile::tempdir().unwrap(); + let test_file = temp_dir.path().join("test.txt"); + std::fs::write(&test_file, "hello").unwrap(); + + let expr = format!( + r#"builtins.hasContext (builtins.path {{ path = {}; name = "test-ctx"; }})"#, + test_file.display() + ); + let result = eval(&expr); + assert_eq!(result, Value::Bool(true)); +} + +#[test] +fn builtins_path_context_tracked_in_structured_attrs_derivation() { + let temp_dir = tempfile::tempdir().unwrap(); + let test_file = temp_dir.path().join("test-patch.txt"); + std::fs::write(&test_file, "patch content").unwrap(); + + let expr = format!( + r#" + let + patch = builtins.path {{ path = {}; name = "test-patch"; }}; + in + (derivation {{ + __structuredAttrs = true; + name = "test-input-srcs"; + system = "x86_64-linux"; + builder = "/bin/sh"; + patches = [ patch ]; + }}).drvPath + "#, + test_file.display() + ); + let result = eval(&expr); + + if let Value::String(s) = &result { + assert!(s.contains("/nix/store/"), "drvPath should be a store path"); + } else { + panic!("Expected string, got {:?}", result); + } +} + +#[test] +fn builtins_path_context_tracked_in_non_structured_derivation() { + let temp_dir = tempfile::tempdir().unwrap(); + let test_file = temp_dir.path().join("dep.txt"); + std::fs::write(&test_file, "dependency content").unwrap(); + + let expr = format!( + r#" + let + dep = builtins.path {{ path = {}; name = "dep-file"; }}; + in + (derivation {{ + name = "test-non-structured"; + system = "x86_64-linux"; + builder = "/bin/sh"; + myDep = dep; + }}).drvPath + "#, + test_file.display() + ); + let result = eval(&expr); + + if let Value::String(s) = &result { + assert!(s.contains("/nix/store/"), "drvPath should be a store path"); + } else { + panic!("Expected string, got {:?}", result); + } +}