fix: preserve string context in builtins.{path,fetch*}

This commit is contained in:
2026-02-14 20:01:39 +08:00
parent 31c7a62311
commit cf4dd6c379
2 changed files with 98 additions and 23 deletions

View File

@@ -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 =

View File

@@ -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);
}
}