diff --git a/Cargo.lock b/Cargo.lock index f7f279e..4200f80 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1927,11 +1927,11 @@ dependencies = [ "rnix", "rowan", "rusqlite", + "rust-embed", "rustyline", "serde", "serde_json", "sha2", - "sourcemap", "string-interner", "tar", "tempfile", @@ -2587,6 +2587,40 @@ dependencies = [ "smallvec", ] +[[package]] +name = "rust-embed" +version = "8.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04113cb9355a377d83f06ef1f0a45b8ab8cd7d8b1288160717d66df5c7988d27" +dependencies = [ + "rust-embed-impl", + "rust-embed-utils", + "walkdir", +] + +[[package]] +name = "rust-embed-impl" +version = "8.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0902e4c7c8e997159ab384e6d0fc91c221375f6894346ae107f47dd0f3ccaa" +dependencies = [ + "proc-macro2", + "quote", + "rust-embed-utils", + "syn", + "walkdir", +] + +[[package]] +name = "rust-embed-utils" +version = "8.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bcdef0be6fe7f6fa333b1073c949729274b05f123a0ad7efcb8efd878e5c3b1" +dependencies = [ + "sha2", + "walkdir", +] + [[package]] name = "rustc-demangle" version = "0.1.27" diff --git a/nix-js/Cargo.toml b/nix-js/Cargo.toml index 4445a9b..6f98400 100644 --- a/nix-js/Cargo.toml +++ b/nix-js/Cargo.toml @@ -30,6 +30,8 @@ hashbrown = "0.16" petgraph = "0.8" string-interner = "0.19" +rust-embed="8.11" + itertools = "0.14" regex = "1.11" @@ -41,7 +43,6 @@ nix-nar = "0.3" sha2 = "0.10" hex = "0.4" -sourcemap = "9.0" base64 = "0.22" # Fetcher dependencies diff --git a/nix-js/runtime-ts/src/builtins/conversion.ts b/nix-js/runtime-ts/src/builtins/conversion.ts index e1fbf37..65d18c1 100644 --- a/nix-js/runtime-ts/src/builtins/conversion.ts +++ b/nix-js/runtime-ts/src/builtins/conversion.ts @@ -8,7 +8,7 @@ import { force } from "../thunk"; import { type NixStringContext, mkStringWithContext, addBuiltContext } from "../string-context"; import { forceFunction } from "../type-assert"; import { nixValueToJson } from "../conversion"; -import { typeOf } from "./type-check"; +import { isAttrs, isPath, typeOf } from "./type-check"; const convertJsonToNix = (json: unknown): NixValue => { if (json === null) { @@ -283,6 +283,16 @@ export const coerceToStringWithContext = ( * - Preserves string context if present */ export const coerceToPath = (value: NixValue, outContext?: NixStringContext): string => { + const forced = force(value); + + if (isPath(forced)) { + return forced.value; + } + if (isAttrs(forced) && Object.hasOwn(forced, "__toString")) { + const toStringFunc = forceFunction(forced.__toString); + return coerceToPath(toStringFunc(forced), outContext); + } + const pathStr = coerceToString(value, StringCoercionMode.Base, false, outContext); if (pathStr === "") { diff --git a/nix-js/runtime-ts/src/builtins/io.ts b/nix-js/runtime-ts/src/builtins/io.ts index 308c196..b1b3b31 100644 --- a/nix-js/runtime-ts/src/builtins/io.ts +++ b/nix-js/runtime-ts/src/builtins/io.ts @@ -3,15 +3,16 @@ * Implemented via Rust ops exposed through deno_core */ -import { forceAttrs, forceBool, forceStringValue } from "../type-assert"; -import type { NixValue, NixAttrs } from "../types"; -import { isNixPath } from "../types"; +import { forceAttrs, forceBool, forceList, forceStringNoCtx, forceStringValue } from "../type-assert"; +import type { NixValue, NixAttrs, NixPath } 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 { isPath } from "./type-check"; +import { getCorepkg } from "../corepkgs"; export const importFunc = (path: NixValue): NixValue => { const context: NixStringContext = new Set(); @@ -390,10 +391,70 @@ export const filterSource = (args: NixValue): never => { throw new Error("Not implemented: filterSource"); }; +const suffixIfPotentialMatch = (prefix: string, path: string): string | null => { + const n = prefix.length; + + const needSeparator = n > 0 && n < path.length; + + if (needSeparator && path[n] !== "/") { + return null; + } + + if (!path.startsWith(prefix)) { + return null; + } + + return needSeparator ? path.substring(n + 1) : path.substring(n); +}; + export const findFile = - (search: NixValue) => - (lookup: NixValue): never => { - throw new Error("Not implemented: findFile"); + (searchPath: NixValue) => + (lookupPath: NixValue): NixPath => { + const forcedSearchPath = forceList(searchPath); + const lookupPathStr = forceStringNoCtx(lookupPath); + + for (const item of forcedSearchPath) { + const attrs = forceAttrs(item); + + const prefix = "prefix" in attrs ? forceStringNoCtx(attrs.prefix) : ""; + + if (!("path" in attrs)) { + throw new Error("findFile: search path element is missing 'path' attribute"); + } + + const suffix = suffixIfPotentialMatch(prefix, lookupPathStr); + if (suffix === null) { + continue; + } + + const context: NixStringContext = new Set(); + const pathVal = coerceToString(attrs.path, StringCoercionMode.Interpolation, false, context); + + if (context.size > 0) { + throw new Error("findFile: path with string context is not yet supported"); + } + + const resolvedPath = Deno.core.ops.op_resolve_path(pathVal, ""); + const candidatePath = suffix.length > 0 + ? Deno.core.ops.op_resolve_path(suffix, resolvedPath) + : resolvedPath; + + if (Deno.core.ops.op_path_exists(candidatePath)) { + return { [IS_PATH]: true, value: candidatePath }; + } + } + + if (lookupPathStr.startsWith("nix/")) { + const corepkgName = lookupPathStr.substring(4); + const corepkgContent = getCorepkg(corepkgName); + + if (corepkgContent !== undefined) { + // FIXME: special path type + return { [IS_PATH]: true, value: `` }; + } + } + + throw new CatchableError(`file '${lookupPathStr}' was not found in the Nix search path`); }; export const getEnv = (s: NixValue): string => { diff --git a/nix-js/runtime-ts/src/corepkgs/fetchurl.nix.ts b/nix-js/runtime-ts/src/corepkgs/fetchurl.nix.ts new file mode 100644 index 0000000..35be5cf --- /dev/null +++ b/nix-js/runtime-ts/src/corepkgs/fetchurl.nix.ts @@ -0,0 +1,73 @@ +export const FETCHURL_NIX = `{ + system ? "", # obsolete + url, + hash ? "", # an SRI hash + + # Legacy hash specification + md5 ? "", + sha1 ? "", + sha256 ? "", + sha512 ? "", + outputHash ? + if hash != "" then + hash + else if sha512 != "" then + sha512 + else if sha1 != "" then + sha1 + else if md5 != "" then + md5 + else + sha256, + outputHashAlgo ? + if hash != "" then + "" + else if sha512 != "" then + "sha512" + else if sha1 != "" then + "sha1" + else if md5 != "" then + "md5" + else + "sha256", + + executable ? false, + unpack ? false, + name ? baseNameOf (toString url), + impure ? false, +}: + +derivation ( + { + builder = "builtin:fetchurl"; + + # New-style output content requirements. + outputHashMode = if unpack || executable then "recursive" else "flat"; + + inherit + name + url + executable + unpack + ; + + system = "builtin"; + + # No need to double the amount of network traffic + preferLocalBuild = true; + + # This attribute does nothing; it's here to avoid changing evaluation results. + impureEnvVars = [ + "http_proxy" + "https_proxy" + "ftp_proxy" + "all_proxy" + "no_proxy" + ]; + + # To make "nix-prefetch-url" work. + urls = [ url ]; + } + // (if impure then { __impure = true; } else { inherit outputHashAlgo outputHash; }) +) +`; diff --git a/nix-js/runtime-ts/src/corepkgs/index.ts b/nix-js/runtime-ts/src/corepkgs/index.ts new file mode 100644 index 0000000..261f35b --- /dev/null +++ b/nix-js/runtime-ts/src/corepkgs/index.ts @@ -0,0 +1,9 @@ +import { FETCHURL_NIX } from "./fetchurl.nix"; + +export const COREPKGS: Record = { + "fetchurl.nix": FETCHURL_NIX, +}; + +export const getCorepkg = (name: string): string | undefined => { + return COREPKGS[name]; +}; diff --git a/nix-js/runtime-ts/src/helpers.ts b/nix-js/runtime-ts/src/helpers.ts index fca9d92..ab4549b 100644 --- a/nix-js/runtime-ts/src/helpers.ts +++ b/nix-js/runtime-ts/src/helpers.ts @@ -169,7 +169,15 @@ export const concatStringsWithContext = (parts: NixValue[]): NixString | NixPath * @returns NixPath object with absolute path */ export const resolvePath = (currentDir: string, path: NixValue): NixPath => { - const pathStr = forceStringValue(path); + const forced = force(path); + let pathStr: string; + + if (isNixPath(forced)) { + pathStr = forced.value; + } else { + pathStr = forceStringValue(path); + } + const resolved = Deno.core.ops.op_resolve_path(currentDir, pathStr); return mkPath(resolved); }; diff --git a/nix-js/runtime-ts/src/type-assert.ts b/nix-js/runtime-ts/src/type-assert.ts index a02f9bb..e3fe2e9 100644 --- a/nix-js/runtime-ts/src/type-assert.ts +++ b/nix-js/runtime-ts/src/type-assert.ts @@ -85,6 +85,17 @@ export const forceString = (value: NixValue): NixString => { throw new TypeError(`Expected string, got ${typeOf(forced)}`); }; +export const forceStringNoCtx = (value: NixValue): string => { + const forced = force(value); + if (typeof forced === "string") { + return forced; + } + if (isStringWithContext(forced)) { + throw new TypeError(`the string '${forced.value}' is not allowed to refer to a store path`) + } + throw new TypeError(`Expected string, got ${typeOf(forced)}`); +} + /** * Force a value and assert it's a boolean * @throws TypeError if value is not a boolean after forcing diff --git a/nix-js/runtime-ts/src/types.ts b/nix-js/runtime-ts/src/types.ts index ab4982a..86b6439 100644 --- a/nix-js/runtime-ts/src/types.ts +++ b/nix-js/runtime-ts/src/types.ts @@ -5,7 +5,7 @@ import { force, IS_THUNK } from "./thunk"; import { type StringWithContext, HAS_CONTEXT, isStringWithContext, getStringContext } from "./string-context"; import { op } from "./operators"; -import { forceAttrs } from "./type-assert"; +import { forceAttrs, forceStringNoCtx } from "./type-assert"; import { isString, typeOf } from "./builtins/type-check"; export { HAS_CONTEXT, isStringWithContext }; export type { StringWithContext }; @@ -81,13 +81,8 @@ export const mkAttrs = (attrs: NixAttrs, keys: NixValue[], values: NixValue[]): if (key === null) { continue; } - if (!isString(key)) { - throw `Expected string, got ${typeOf(key)}` - } - if (isStringWithContext(key)) { - throw new TypeError(`the string '${key.value}' is not allowed to refer to a store path`) - } - attrs[key] = values[i]; + const str = forceStringNoCtx(key); + attrs[str] = values[i]; } return attrs; }; diff --git a/nix-js/src/ir/downgrade.rs b/nix-js/src/ir/downgrade.rs index 3d9b4eb..35eb135 100644 --- a/nix-js/src/ir/downgrade.rs +++ b/nix-js/src/ir/downgrade.rs @@ -103,7 +103,19 @@ impl Downgrade for ast::Path { .collect::>>()?; let expr = if parts.len() == 1 { - parts.into_iter().next().unwrap() + let part = parts.into_iter().next().unwrap(); + if let &Ir::Str(Str { ref val, span }) = ctx.get_ir(part) + && let Some(path) = val.strip_prefix("<").map(|path| &path[..path.len() - 1]) { + ctx.replace_ir(part, Str { val: path.to_string(), span }.to_ir()); + let sym = ctx.new_sym("findFile".into()); + let find_file = ctx.new_expr(Builtin { inner: sym, span }.to_ir()); + let sym = ctx.new_sym("nixPath".into()); + let nix_path = ctx.new_expr(Builtin { inner: sym, span }.to_ir()); + let call = ctx.new_expr(Call { func: find_file, arg: nix_path, span }.to_ir()); + return Ok(ctx.new_expr(Call { func: call, arg: part, span }.to_ir())); + } else { + part + } } else { ctx.new_expr(ConcatStrings { parts, span }.to_ir()) }; diff --git a/nix-js/src/runtime.rs b/nix-js/src/runtime.rs index 7a93793..6727ebb 100644 --- a/nix-js/src/runtime.rs +++ b/nix-js/src/runtime.rs @@ -1,11 +1,12 @@ use std::borrow::Cow; use std::marker::PhantomData; use std::path::{Component, Path, PathBuf}; -use std::sync::Once; +use std::sync::{Arc, Once}; use deno_core::{Extension, ExtensionFileSource, JsRuntime, OpState, RuntimeOptions, v8}; use deno_error::JsErrorClass; use itertools::Itertools as _; +use rust_embed::Embed; use crate::error::{Error, Result, Source}; use crate::store::Store; @@ -96,6 +97,10 @@ mod private { } pub(crate) use private::NixError; +#[derive(Embed)] +#[folder = "src/runtime/corepkgs"] +pub(crate) struct CorePkgs; + #[deno_core::op2] #[string] fn op_import( @@ -105,6 +110,24 @@ fn op_import( let _span = tracing::info_span!("op_import", path = %path).entered(); let ctx: &mut Ctx = state.get_ctx_mut(); + // FIXME: special path type + if path.starts_with("") { + let corepkg_name = &path[5..path.len() - 1]; + if let Some(file) = CorePkgs::get(corepkg_name) { + tracing::info!("Importing corepkg: {}", corepkg_name); + let source = Source { + ty: crate::error::SourceType::Eval(Arc::new(ctx.get_current_dir().to_path_buf())), + src: str::from_utf8(&file.data) + .expect("corrupted corepkgs file") + .into(), + }; + ctx.add_source(source.clone()); + return Ok(ctx.compile_code(source).map_err(|err| err.to_string())?); + } else { + return Err(format!("Corepkg not found: {}", corepkg_name).into()); + } + } + let current_dir = ctx.get_current_dir(); let mut absolute_path = current_dir .join(&path) diff --git a/nix-js/src/runtime/corepkgs/fetchurl.nix b/nix-js/src/runtime/corepkgs/fetchurl.nix new file mode 100644 index 0000000..6701140 --- /dev/null +++ b/nix-js/src/runtime/corepkgs/fetchurl.nix @@ -0,0 +1,76 @@ +{ + system ? "", # obsolete + url, + hash ? "", # an SRI hash + + # Legacy hash specification + md5 ? "", + sha1 ? "", + sha256 ? "", + sha512 ? "", + outputHash ? + if hash != "" then + hash + else if sha512 != "" then + sha512 + else if sha1 != "" then + sha1 + else if md5 != "" then + md5 + else + sha256, + outputHashAlgo ? + if hash != "" then + "" + else if sha512 != "" then + "sha512" + else if sha1 != "" then + "sha1" + else if md5 != "" then + "md5" + else + "sha256", + + executable ? false, + unpack ? false, + name ? baseNameOf (toString url), + # still translates to __impure to trigger derivationStrict error checks. + impure ? false, +}: + +derivation ( + { + builder = "builtin:fetchurl"; + + # New-style output content requirements. + outputHashMode = if unpack || executable then "recursive" else "flat"; + + inherit + name + url + executable + unpack + ; + + system = "builtin"; + + # No need to double the amount of network traffic + preferLocalBuild = true; + + impureEnvVars = [ + # We borrow these environment variables from the caller to allow + # easy proxy configuration. This is impure, but a fixed-output + # derivation like fetchurl is allowed to do so since its result is + # by definition pure. + "http_proxy" + "https_proxy" + "ftp_proxy" + "all_proxy" + "no_proxy" + ]; + + # To make "nix-prefetch-url" work. + urls = [ url ]; + } + // (if impure then { __impure = true; } else { inherit outputHashAlgo outputHash; }) +) diff --git a/nix-js/tests/findfile.rs b/nix-js/tests/findfile.rs new file mode 100644 index 0000000..11a0388 --- /dev/null +++ b/nix-js/tests/findfile.rs @@ -0,0 +1,38 @@ +mod utils; + +use utils::eval; + +#[test] +fn test_find_file_corepkg_fetchurl() { + let result = eval( + r#" + let + searchPath = []; + lookupPath = "nix/fetchurl.nix"; + in + builtins.findFile searchPath lookupPath + "#, + ); + + assert!(result.to_string().contains("fetchurl.nix")); +} + +#[test] +fn test_lookup_path_syntax() { + let result = eval(r#""#); + assert!(result.to_string().contains("fetchurl.nix")); +} + +#[test] +fn test_import_corepkg() { + let result = eval( + r#" + let + fetchurl = import ; + in + builtins.typeOf fetchurl + "#, + ); + + assert_eq!(result.to_string(), "\"lambda\""); +}