packages feed

nova-nix 0.1.4.0 → 0.1.5.0

raw patch · 7 files changed

+248/−124 lines, 7 filesPVP: major bump suggested

API removals or changes: PVP suggests a major version bump

API changes (from Hackage documentation)

+ Nix.Eval: copyPathToStore :: MonadEval m => Text -> Text -> m Text
+ Nix.Eval: traceMessage :: MonadEval m => Text -> m ()
+ Nix.Eval.Types: copyPathToStore :: MonadEval m => Text -> Text -> m Text
+ Nix.Eval.Types: traceMessage :: MonadEval m => Text -> m ()
- Nix.Eval.StringInterp: coerceToString :: MonadEval m => NixValue -> m (Text, StringContext)
+ Nix.Eval.StringInterp: coerceToString :: MonadEval m => Force m -> Apply m -> NixValue -> m (Text, StringContext)
- Nix.Eval.StringInterp: evalIndStringParts :: MonadEval m => Eval m -> Env -> [StringPart] -> m (Text, StringContext)
+ Nix.Eval.StringInterp: evalIndStringParts :: MonadEval m => Eval m -> Force m -> Apply m -> Env -> [StringPart] -> m (Text, StringContext)
- Nix.Eval.StringInterp: evalStringParts :: MonadEval m => Eval m -> Env -> [StringPart] -> m (Text, StringContext)
+ Nix.Eval.StringInterp: evalStringParts :: MonadEval m => Eval m -> Force m -> Apply m -> Env -> [StringPart] -> m (Text, StringContext)

Files

CHANGELOG.md view
@@ -1,5 +1,19 @@ # Changelog +## 0.1.5.0 — 2026-02-26++### New Builtins, Coercion Fixes, CI Cleanup++- **`builtins.warn`** — prints warning to stderr, returns second arg+- **`builtins.path`** — copies file/dir to store with `SCPlain` context+- **`builtins.seq` fix** — now forces first arg to WHNF (was silent no-op)+- **`builtins.trace`/`traceVerbose` fix** — print to stderr via `MonadEval.traceMessage`+- **`coerceToString` fix** — handle `VAttrs` with `__toString` and `outPath`, enabling `"${pkgs.hello}/bin/hello"` interpolation (was type error on all attrsets)+- **`fetchTarball` fix** — downloads and extracts via curl|tar pipeline (was returning raw .tar.gz)+- **`MonadEval`** — added `traceMessage`, `copyPathToStore` methods+- CI: switched to `haskell-actions/run-ormolu@v16` (matching all Novavero repos)+- 108 builtins, 511 tests, -Werror clean, ormolu clean, hlint clean+ ## 0.1.4.0 — 2026-02-26  ### Memory Optimization + Lazy Non-Rec Attrsets@@ -48,8 +62,6 @@  ## 0.1.1.0 — 2026-02-24 -### Phase 4: nixpkgs Compatibility- - **Thunk memoization** — Per-thunk `IORef` memo cells (matching real Nix in-place mutation). `forceThunk` is a `MonadEval` method: IO evaluators memoize per-allocation, pure evaluators re-evaluate. GC reclaims dead thunks naturally — no unbounded global cache. `unsafePerformIO` CAF float-out prevented via `NOINLINE` + `seq` pattern. - **8.4x builtin dispatch speedup** — Case dispatch replacing polymorphic `Map` reconstruction on every call. Direct pattern match on builtin name for zero-allocation dispatch in the hot path. - **Regex builtins** — `builtins.match` and `builtins.split` via `regex-tdfa` (pure Haskell POSIX ERE, cross-platform)@@ -81,7 +93,7 @@ - Demo `test.nix` included: derivations, lambdas, builtins.map, arithmetic — runs on all platforms - 16 builtins exposed at top level without `builtins.` prefix (`toString`, `map`, `throw`, `import`, `derivation`, `abort`, `baseNameOf`, `dirOf`, `isNull`, `removeAttrs`, `placeholder`, `scopedImport`, `fetchTarball`, `fetchGit`, `fetchurl`, `toFile`) — matches real Nix language spec, required for nixpkgs compatibility -### Phase 3: String Contexts + Dependency Resolution + Substituter+### String Contexts, Dependency Resolution, Binary Substituter  - String context tracking on all `VStr` values: `SCPlain`, `SCDrvOutput`, `SCAllOutputs` - Context propagation through interpolation, string concatenation, `replaceStrings`, `substring`, `concatStringsSep`, and all string builtins@@ -95,7 +107,7 @@ - Cleanup pass: eliminated all partial functions (`T.head`/`T.tail` to `T.uncons`, `!!` to safe lookup, `last` to pattern match), flattened deeply nested code into composed functions, `Data.Sequence` BFS queues throughout, semantic section organization - 494 tests (68 new: string context, context propagation, dependency graph, substituter, build orchestrator) -### Phase 2: Store + Builder+### Content-Addressed Store + Derivation Builder  - Real SQLite-backed store database (`ValidPaths` + `Refs` tables, WAL mode) - Store operations: `addToStore` (cross-device safe), `scanReferences` (byte-scan for store path references), `setReadOnly` (recursive), `writeDrv`@@ -106,7 +118,7 @@ - CLI `nova-nix build FILE.nix`: evaluate, extract derivation, write `.drv`, build, print output path - 426 tests (45 new: 10 store DB, 13 store ops, 10 ATerm parser, 8 builder, 4 CLI end-to-end) -### Phase 1: Parser + Evaluator + Builtins+### Parser, Lazy Evaluator, 85 Builtins  - Full Nix expression parser (hand-rolled recursive descent, 13 precedence levels) - Lazy evaluator with thunk-based evaluation, knot-tying for recursive bindings
README.md view
@@ -174,39 +174,39 @@  ### Parser -| Module | Purpose | Status |-|--------|---------|--------|-| `Nix.Expr.Types` | Complete Nix AST — 17 expression constructors (including `ESearchPath`), atoms, formals, operators, string parts, source locations | Done |-| `Nix.Parser` | Hand-rolled recursive descent parser + lexer. Direct `Text` consumption, source position tracking | Done |-| `Nix.Parser.Lexer` | Tokenizer — integers, floats, strings with interpolation, paths, URIs, search paths, all operators/keywords | Done |-| `Nix.Parser.Expr` | Expression parser — 13 precedence levels, left/right/non-associative operators, application, selection, dynamic keys | Done |-| `Nix.Parser.Internal` | Parser state and combinator internals | Done |-| `Nix.Parser.ParseError` | Structured parse errors with source positions | Done |+| Module | Purpose |+|--------|---------|+| `Nix.Expr.Types` | Complete Nix AST — 17 expression constructors (including `ESearchPath`), atoms, formals, operators, string parts, source locations |+| `Nix.Parser` | Hand-rolled recursive descent parser + lexer. Direct `Text` consumption, source position tracking |+| `Nix.Parser.Lexer` | Tokenizer — integers, floats, strings with interpolation, paths, URIs, search paths, all operators/keywords |+| `Nix.Parser.Expr` | Expression parser — 13 precedence levels, left/right/non-associative operators, application, selection, dynamic keys |+| `Nix.Parser.Internal` | Parser state and combinator internals |+| `Nix.Parser.ParseError` | Structured parse errors with source positions |  ### Evaluator -| Module | Purpose | Status |-|--------|---------|--------|-| `Nix.Eval` | Lazy evaluator — all 17 AST constructors, thunk forcing, env operations, 101-builtin dispatch, `__functor` callable sets, search path resolution, dynamic attribute keys. Polymorphic via `MonadEval` | Done |-| `Nix.Eval.Types` | Shared types — `NixValue` (11 constructors), `Thunk` (lazy env for knot-tying), `Env` (lexical + with-scope chain), `StringContext` (store path tracking), `MonadEval` typeclass, `PureEval` runner | Done |-| `Nix.Eval.Operator` | Binary/unary operators — arithmetic with float promotion, deep structural equality, division-by-zero checks | Done |-| `Nix.Eval.StringInterp` | String interpolation — value coercion with context propagation, indented string whitespace stripping | Done |-| `Nix.Eval.Context` | String context construction, queries, extraction — pure helpers for building and inspecting store path references | Done |-| `Nix.Eval.IO` | IO evaluation monad — real filesystem access, import cache (with directory import), process execution, store writes, NIX_PATH parsing, per-thunk IORef memoization (matching real Nix in-place mutation) | Done |-| `Nix.Builtins` | Built-in function environment — 106 builtins, search path plumbing (`parseNixPath`), top-level builtin exposure | Done |+| Module | Purpose |+|--------|---------|+| `Nix.Eval` | Lazy evaluator — all 17 AST constructors, thunk forcing, env operations, 106-builtin dispatch, `__functor` callable sets, search path resolution, dynamic attribute keys. Polymorphic via `MonadEval` |+| `Nix.Eval.Types` | Shared types — `NixValue` (11 constructors), `Thunk` (lazy env for knot-tying), `Env` (lexical + with-scope chain), `StringContext` (store path tracking), `MonadEval` typeclass, `PureEval` runner |+| `Nix.Eval.Operator` | Binary/unary operators — arithmetic with float promotion, deep structural equality, division-by-zero checks |+| `Nix.Eval.StringInterp` | String interpolation — value coercion with context propagation, indented string whitespace stripping |+| `Nix.Eval.Context` | String context construction, queries, extraction — pure helpers for building and inspecting store path references |+| `Nix.Eval.IO` | IO evaluation monad — real filesystem access, import cache (with directory import), process execution, store writes, NIX_PATH parsing, per-thunk IORef memoization (matching real Nix in-place mutation) |+| `Nix.Builtins` | Built-in function environment — 106 builtins, search path plumbing (`parseNixPath`), top-level builtin exposure |  ### Store + Builder -| Module | Purpose | Status |-|--------|---------|--------|-| `Nix.Derivation` | Derivation type, ATerm serialization + parsing (`toATerm`/`fromATerm`), platform detection | Done |-| `Nix.Hash` | Derivation hashing, store path computation, shared hex/base-32 utilities | Done |-| `Nix.Store.Path` | Store path types — `StoreDir`, `StorePath`, `parseStorePath`, Windows/Unix support | Done |-| `Nix.Store.DB` | SQLite store database — `ValidPaths` + `Refs` tables, WAL mode, path registration, reference/deriver queries | Done |-| `Nix.Store` | High-level store operations — `addToStore`, `scanReferences`, `setReadOnly`, `writeDrv` | Done |-| `Nix.Builder` | Derivation builder — dependency graph construction, topological sort, binary cache substitution, local build with output registration | Done |-| `Nix.DependencyGraph` | Dependency graph construction (BFS with `Seq` queue) and topological sort (Kahn's algorithm, O(V+E)), cycle detection | Done |-| `Nix.Substituter` | Binary cache substituter — HTTP narinfo fetch, signature verification, NAR download/decompress/unpack, store registration. Multi-cache with priority ordering | Done |+| Module | Purpose |+|--------|---------|+| `Nix.Derivation` | Derivation type, ATerm serialization + parsing (`toATerm`/`fromATerm`), platform detection |+| `Nix.Hash` | Derivation hashing, store path computation, shared hex/base-32 utilities |+| `Nix.Store.Path` | Store path types — `StoreDir`, `StorePath`, `parseStorePath`, Windows/Unix support |+| `Nix.Store.DB` | SQLite store database — `ValidPaths` + `Refs` tables, WAL mode, path registration, reference/deriver queries |+| `Nix.Store` | High-level store operations — `addToStore`, `scanReferences`, `setReadOnly`, `writeDrv` |+| `Nix.Builder` | Derivation builder — dependency graph construction, topological sort, binary cache substitution, local build with output registration |+| `Nix.DependencyGraph` | Dependency graph construction (BFS with `Seq` queue) and topological sort (Kahn's algorithm, O(V+E)), cycle detection |+| `Nix.Substituter` | Binary cache substituter — HTTP narinfo fetch, signature verification, NAR download/decompress/unpack, store registration. Multi-cache with priority ordering |  --- @@ -284,40 +284,9 @@  ## Roadmap -### Done--- [x] **Lexer** — Full Nix tokenization (integers, floats, strings with interpolation, paths, URIs, search paths, operators, keywords)-- [x] **Parser** — 13 precedence levels, all Nix syntax, structured error reporting-- [x] **Evaluator** — All 17 AST constructors, lazy thunks, recursive let/rec via knot-tying, with-scope chain, dynamic attribute keys-- [x] **106 builtins** — Type checks, arithmetic, bitwise, strings, lists, attrsets, higher-order, JSON, hashing, regex (`match`/`split` via `regex-tdfa`), version parsing, `setFunctionArgs`/`functionArgs`, tryEval, deepSeq, genericClosure, string context introspection, all IO builtins, derivation-- [x] **MonadEval refactor** — Evaluator polymorphic in effect monad (`PureEval` for tests, `EvalIO` for real evaluation)-- [x] **IO builtins** — `import` (with directory import support), `readFile`, `pathExists`, `readDir`, `getEnv`, `toPath`, `toFile`, `findFile`, `scopedImport`, `fetchurl`, `fetchTarball`, `fetchGit`, `currentTime`-- [x] **`derivation`** — Attrset to `.drv` build recipe with computed `drvPath` and `outPath`, context-aware input population-- [x] **String context tracking** — `SCPlain`, `SCDrvOutput`, `SCAllOutputs` on every `VStr`, propagated through interpolation, operators, and all string builtins. `hasContext`, `getContext`, `appendContext` introspection builtins.-- [x] **ATerm serialization + parsing** — Full `.drv` round-trip with `toATerm`/`fromATerm`, string escaping, sorted environments-- [x] **SQLite store DB** — `ValidPaths` + `Refs` tables, WAL mode, registration, validity checks, reference/deriver queries-- [x] **Store operations** — `parseStorePath`, `addToStore` (cross-device safe), `scanReferences` (byte-scan), `setReadOnly`, `writeDrv`-- [x] **Dependency graph** — BFS construction with `Data.Sequence` (O(V+E)), topological sort via Kahn's algorithm, cycle detection-- [x] **Builder** — Full build loop with recursive dependency resolution: topo sort, cache check, binary substitution, local build, output registration-- [x] **Binary substituter** — HTTP binary cache protocol: narinfo fetch/parse, Ed25519 signature verification via nova-cache, NAR download/decompress/unpack, store DB registration. Multi-cache with priority ordering.-- [x] **NIX_PATH / search path resolution** — `<nixpkgs>` desugars to `builtins.findFile builtins.nixPath name`. `NIX_PATH` parsed at startup. `--nix-path` CLI flag for additional entries.-- [x] **Dynamic attribute keys** — `{ ${expr} = val; }` works in all binding contexts with monadic key resolution preserving knot-tying-- [x] **Directory imports** — `import ./dir` resolves to `./dir/default.nix` automatically-- [x] **CLI** — `nova-nix eval FILE.nix`, `nova-nix eval --expr 'EXPR'`, `nova-nix build FILE.nix`, `--nix-path NAME=PATH`-- [x] **Thunk memoization** — Per-thunk `IORef` memo cells matching real Nix in-place mutation. GC reclaims dead thunks naturally.-- [x] **Regex builtins** — `builtins.match` and `builtins.split` (POSIX ERE via `regex-tdfa`, pure Haskell, cross-platform)-- [x] **Callable attribute sets** — `__functor` dispatch: sets with `__functor` are callable, enabling `lib.makeOverridable`, `lib.makeExtensible`, and the nixpkgs override system-- [x] **Lazy builtins** — `map`, `genList`, `mapAttrs` return deferred thunks (O(1) until demanded). `concatMap` is semi-lazy. Critical for nixpkgs which maps over 80,000+ packages.-- [x] **Null dynamic keys** — `{ ${null} = val; }` skips the binding, used by the nixpkgs module system for conditional attributes-- [x] **Bundled `<nix/fetchurl.nix>`** — Ships as Cabal data-file for nixpkgs stdenv bootstrap-- [x] **nixpkgs module system** — `lib.evalModules`, `lib.mkOption`, `lib.mkIf`, `lib.types.*` all working-- [x] **nixpkgs lib fully working** — trivial, strings, lists, attrsets, systems, generators, functional patterns (`fix`, `makeOverridable`, `makeExtensible`)-- [x] **511 tests** — parser, evaluator, store, builder, substituter, dependency graph, search paths, dynamic keys, directory imports, laziness, CLI end-to-end- ### Next -- [ ] **Full `import <nixpkgs> {}` performance** — nixpkgs lib layer evaluates correctly; stdenv bootstrap runs but needs performance optimization for the full 80,000+ package set-- [ ] **Remaining builtins** — 13 missing (`fromTOML`, `hashFile`, `readFileType`, `traceVerbose`, `break`, `filterSource`, `outputOf`, and fetch variants)+- [ ] **Full `import <nixpkgs> {}` performance** — nixpkgs lib layer evaluates correctly; stdenv bootstrap runs but needs memory optimization for the full 80,000+ package set (currently OOM at 2GB, investigating space leak) - [ ] **`nova-nix shell`** — Enter a development shell (like `nix shell`) - [ ] **`nova-nix repl`** — Interactive evaluator 
nova-nix.cabal view
@@ -1,6 +1,6 @@ cabal-version:      3.0 name:               nova-nix-version:            0.1.4.0+version:            0.1.5.0 synopsis:           Windows-native Nix implementation in pure Haskell description:   A pure Haskell implementation of the Nix package manager that runs natively
src/Nix/Eval.hs view
@@ -68,11 +68,11 @@ import qualified Data.ByteArray as BA import qualified Data.ByteString as BS import Data.Char (chr, digitToInt, isAlpha, isDigit, isHexDigit, isOctDigit, ord)-import Data.List (foldl')+import Data.List (find, foldl') import qualified Data.Map.Lazy as MapL import Data.Map.Strict (Map) import qualified Data.Map.Strict as Map-import Data.Maybe (catMaybes, isJust, isNothing)+import Data.Maybe (catMaybes, fromMaybe, isJust, isNothing) import Data.Sequence (Seq (..)) import qualified Data.Sequence as Seq import qualified Data.Set as Set@@ -80,7 +80,7 @@ import qualified Data.Text as T import qualified Data.Text.Encoding as TE import Nix.Derivation (Derivation (..), DerivationOutput (..), textToPlatform, toATerm)-import Nix.Eval.Context (extractInputDrvs, extractInputSrcs)+import Nix.Eval.Context (extractInputDrvs, extractInputSrcs, plainContext) import Nix.Eval.Operator (evalBinary, evalUnary, nixCompare, nixEqual) import Nix.Eval.StringInterp (coerceToString, evalIndStringParts, evalStringParts) import Nix.Eval.Types@@ -142,8 +142,8 @@ eval :: (MonadEval m) => Env -> Expr -> m NixValue eval env expr = case expr of   ELit atom -> evalLit atom-  EStr parts -> uncurry VStr <$> evalStringParts eval env parts-  EIndStr parts -> uncurry VStr <$> evalIndStringParts eval env parts+  EStr parts -> uncurry VStr <$> evalStringParts eval force applyValue env parts+  EIndStr parts -> uncurry VStr <$> evalIndStringParts eval force applyValue env parts   EVar name -> evalVar env name   EAttrs isRec bindings -> evalAttrs env isRec bindings   EList exprs -> pure (VList (map (mkThunk env) exprs))@@ -768,8 +768,9 @@       builtin1 "isPath" (pure . VBool . isPathVal),       builtin1 "ceil" builtinCeil,       builtin1 "floor" builtinFloor,-      builtin2 "seq" (\_ b -> pure b),-      builtin2 "trace" (\_ b -> pure b),+      builtin2 "seq" builtinSeq,+      builtin2 "trace" builtinTrace,+      builtin2 "warn" builtinWarn,       builtin1 "unsafeDiscardStringContext" builtinDiscardContext,       builtin1 "unsafeDiscardOutputDependency" builtinDiscardOutputDep,       -- String context introspection@@ -835,8 +836,8 @@       builtin2 "addErrorContext" (\_ val -> pure val),       -- Attr position (return null — nixpkgs handles this gracefully)       builtin2 "unsafeGetAttrPos" (\_ _ -> pure VNull),-      -- Debugging (no-ops without --trace-verbose / --debugger)-      builtin2 "traceVerbose" (\_ b -> pure b),+      -- Debugging (traceVerbose: same as trace for now, --trace-verbose not yet gated)+      builtin2 "traceVerbose" builtinTrace,       builtin1 "break" pure,       -- IO: file hashing + type detection       builtin2 "hashFile" builtinHashFile,@@ -847,7 +848,8 @@       builtin1 "convertHash" builtinConvertHash,       -- XML serialization       builtin1 "toXML" builtinToXML,-      -- Source filtering+      -- Source filtering + path import+      builtin1 "path" builtinPath,       builtin2 "filterSource" builtinFilterSource,       -- Experimental feature stubs       builtin2 "outputOf" builtinOutputOf,@@ -947,8 +949,9 @@   "isPath" -> apply1 (pure . VBool . isPathVal)   "ceil" -> apply1 builtinCeil   "floor" -> apply1 builtinFloor-  "seq" -> apply2 (\_ b -> pure b)-  "trace" -> apply2 (\_ b -> pure b)+  "seq" -> apply2 builtinSeq+  "trace" -> apply2 builtinTrace+  "warn" -> apply2 builtinWarn   "unsafeDiscardStringContext" -> apply1 builtinDiscardContext   "unsafeDiscardOutputDependency" -> apply1 builtinDiscardOutputDep   -- String context introspection@@ -1014,8 +1017,8 @@   "addErrorContext" -> apply2 (\_ val -> pure val)   -- Attr position (return null — nixpkgs handles this gracefully)   "unsafeGetAttrPos" -> apply2 (\_ _ -> pure VNull)-  -- Debugging (no-ops without --trace-verbose / --debugger)-  "traceVerbose" -> apply2 (\_ b -> pure b)+  -- Debugging (traceVerbose: same as trace for now)+  "traceVerbose" -> apply2 builtinTrace   "break" -> apply1 pure   -- IO: file hashing + type detection   "hashFile" -> apply2 builtinHashFile@@ -1026,7 +1029,8 @@   "convertHash" -> apply1 builtinConvertHash   -- XML serialization   "toXML" -> apply1 builtinToXML-  -- Source filtering+  -- Source filtering + path import+  "path" -> apply1 builtinPath   "filterSource" -> apply2 builtinFilterSource   -- Experimental feature stubs   "outputOf" -> apply2 builtinOutputOf@@ -1544,7 +1548,7 @@     coerceThunk thunk = do       val <- force thunk       coerceToStringPermissive val-coerceToStringPermissive other = coerceToString other+coerceToStringPermissive other = coerceToString force applyValue other  -- | The current system platform string. currentSystemStr :: Text@@ -2329,6 +2333,28 @@ deepForce (VAttrs attrs) = mapM_ (force >=> deepForce) (attrSetElems attrs) deepForce _ = pure () +-- | @builtins.seq a b@ — evaluate @a@ to WHNF, then return @b@.+builtinSeq :: (MonadEval m) => NixValue -> NixValue -> m NixValue+builtinSeq !_first = pure++-- | @builtins.trace msg val@ — print @msg@ to stderr, return @val@.+builtinTrace :: (MonadEval m) => NixValue -> NixValue -> m NixValue+builtinTrace msgVal result = do+  msg <- case msgVal of+    VStr s _ -> pure s+    other -> pure (typeName other)+  traceMessage ("trace: " <> msg)+  pure result++-- | @builtins.warn msg val@ — print warning to stderr, return @val@.+builtinWarn :: (MonadEval m) => NixValue -> NixValue -> m NixValue+builtinWarn msgVal result = do+  msg <- case msgVal of+    VStr s _ -> pure s+    other -> pure (typeName other)+  traceMessage ("warning: " <> msg)+  pure result+ -- --------------------------------------------------------------------------- -- Builtin implementations — graph traversal -- ---------------------------------------------------------------------------@@ -2567,37 +2593,59 @@   throwEvalError ("builtins.fetchurl: expected a string or set, got " <> typeName other)  builtinFetchTarball :: (MonadEval m) => NixValue -> m NixValue-builtinFetchTarball (VStr url _) = fetchUrlSimple url Nothing+builtinFetchTarball (VStr url _) = fetchAndExtractTarball url builtinFetchTarball (VAttrs attrs) = do   url <- forceAttrStr "builtins.fetchTarball" "url" attrs-  sha256 <- forceOptionalAttrStr attrs "sha256"-  fetchUrlSimple url sha256+  fetchAndExtractTarball url builtinFetchTarball other =   throwEvalError ("builtins.fetchTarball: expected a string or set, got " <> typeName other) +-- | Download a tarball, extract it, and return the path to the extracted+-- directory.  Uses a content-hashed temp directory.  Downloads and extracts+-- in a single shell pipeline to avoid binary-as-text encoding issues.+fetchAndExtractTarball :: (MonadEval m) => Text -> m NixValue+fetchAndExtractTarball url = do+  sysTmp <- getTempDir+  let urlHash = sha256Hex (TE.encodeUtf8 url)+      extractDir = sysTmp <> "/nova-nix-tarball-" <> urlHash+  -- Single pipeline: mkdir, download, extract with --strip-components=1+  -- The -- separator prevents argument injection from the URL.+  (code, _, errOut) <-+    runProcess+      "sh"+      [ "-c",+        "mkdir -p \"$1\" && curl -sSfL -- \"$2\" | tar -xz -C \"$1\" --strip-components=1",+        "--",+        extractDir,+        url+      ]+      ""+  case code of+    0 -> pure (VPath extractDir)+    _ -> throwEvalError ("builtins.fetchTarball: " <> errOut)+ builtinFetchGit :: (MonadEval m) => NixValue -> m NixValue builtinFetchGit (VStr url _) = do-  -- Use a content-based temp dir to avoid predictable paths+  sysTmp <- getTempDir   let urlHash = sha256Hex (TE.encodeUtf8 url)-  sysTmpRaw <- getEnvVar "TMPDIR"-  -- TMPDIR on Unix, TEMP/TMP on Windows; fall back to /tmp-  sysTmp <--    if T.null sysTmpRaw-      then do-        winTmp <- getEnvVar "TEMP"-        pure (if T.null winTmp then "/tmp" else winTmp)-      else pure sysTmpRaw-  let tmpDir = sysTmp <> "/nova-nix-fetchgit-" <> urlHash-  (code, _stdout, stderr) <- runProcess "git" ["clone", "--depth", "1", "--", url, tmpDir] ""-  if code /= 0-    then throwEvalError ("builtins.fetchGit: git clone failed: " <> stderr)-    else pure (VPath tmpDir)+      cloneDir = sysTmp <> "/nova-nix-fetchgit-" <> urlHash+  (code, _, errOut) <- runProcess "git" ["clone", "--depth", "1", "--", url, cloneDir] ""+  case code of+    0 -> pure (VPath cloneDir)+    _ -> throwEvalError ("builtins.fetchGit: git clone failed: " <> errOut) builtinFetchGit (VAttrs attrs) = do   url <- forceAttrStr "builtins.fetchGit" "url" attrs   builtinFetchGit (mkStr url) builtinFetchGit other =   throwEvalError ("builtins.fetchGit: expected a string or set, got " <> typeName other) +-- | Resolve the system temp directory.  Checks @TMPDIR@ (Unix), then+-- @TEMP@ (Windows), falls back to @\/tmp@.+getTempDir :: (MonadEval m) => m Text+getTempDir = do+  candidates <- mapM getEnvVar ["TMPDIR", "TEMP"]+  pure (fromMaybe "/tmp" (find (not . T.null) candidates))+ -- | Fetch a URL and optionally verify its hash. fetchUrlSimple :: (MonadEval m) => Text -> Maybe Text -> m NixValue fetchUrlSimple url _sha256 = do@@ -3415,6 +3463,38 @@     escapeChar '&' = "&amp;"     escapeChar '"' = "&quot;"     escapeChar c = T.singleton c++-- ---------------------------------------------------------------------------+-- Builtin implementations — builtins.path+-- ---------------------------------------------------------------------------++-- | @builtins.path { path; name?; filter?; sha256?; recursive?; }@+--+-- Copy a path to the store and return the store path as a string with+-- context.  @name@ defaults to the basename of @path@.  @filter@ is+-- accepted but not yet applied (copies everything).+builtinPath :: (MonadEval m) => NixValue -> m NixValue+builtinPath (VAttrs attrs) = do+  pathStr <- forceAttrStr "builtins.path" "path" attrs+  nameOverride <- forceOptionalAttrStr attrs "name"+  let name = fromMaybe (extractBaseName pathStr) nameOverride+  storePathText <- copyPathToStore pathStr name+  -- Parse the store path to construct proper SCPlain context+  case parseStorePath defaultStoreDir storePathText of+    Just sp -> pure (VStr storePathText (plainContext sp))+    Nothing -> pure (VStr storePathText emptyContext)+builtinPath other =+  throwEvalError ("builtins.path: expected an attribute set, got " <> typeName other)++-- | Extract the last path component from a path string.+extractBaseName :: Text -> Text+extractBaseName path =+  let stripped = T.dropWhileEnd (\c -> c == '/' || c == '\\') path+   in case T.breakOnEnd "/" stripped of+        ("", _) -> case T.breakOnEnd "\\" stripped of+          ("", _) -> stripped+          (_, name) -> name+        (_, name) -> name  -- --------------------------------------------------------------------------- -- Builtin implementations — filterSource
src/Nix/Eval/IO.hs view
@@ -27,7 +27,7 @@ where  import Control.Exception (Exception, SomeException, displayException, fromException, throwIO, try)-import Control.Monad (when)+import Control.Monad (unless, when) import Control.Monad.IO.Class (liftIO) import Control.Monad.Reader (ReaderT (..), ask, asks, local) import qualified Data.ByteString as BS@@ -49,6 +49,7 @@ import System.Environment (lookupEnv) import System.Exit (ExitCode (..)) import System.FilePath (isRelative, takeDirectory, (</>))+import System.IO (hPutStrLn, stderr) import qualified System.Process as Proc  -- ---------------------------------------------------------------------------@@ -237,6 +238,17 @@           ExitFailure n -> n     pure (code, T.pack stdoutStr, T.pack stderrStr) +  copyPathToStore srcPath name = do+    storeDir <- EvalIO (asks esStoreDir)+    let contentHash = sha256Hex (encodeUtf8 ("source:" <> srcPath <> ":" <> name))+        inner = "nix-store:sha256:" <> contentHash <> ":" <> T.pack storeDir <> ":" <> name+        pathHash = truncatedBase32 (encodeUtf8 inner)+        destPath = T.pack storeDir <> "/" <> pathHash <> "-" <> name+    wrapIO (copyToStoreIfMissing (T.unpack srcPath) (T.unpack destPath) storeDir)+    pure destPath++  traceMessage msg = EvalIO (liftIO (hPutStrLn stderr (T.unpack msg)))+   resolvePathLiteral path = do     baseDir <- EvalIO (asks esBaseDir)     let raw = T.unpack path@@ -370,3 +382,25 @@       FormalNamedSet n fs ellipsis -> FormalNamedSet n (map goFormal fs) ellipsis      goFormal (Formal n mDef) = Formal n (fmap goExpr mDef)++-- ---------------------------------------------------------------------------+-- Store copy helpers+-- ---------------------------------------------------------------------------++-- | Copy a source path (file or directory) to the store if not already present.+copyToStoreIfMissing :: FilePath -> FilePath -> FilePath -> IO ()+copyToStoreIfMissing src dest storeDir = do+  Dir.createDirectoryIfMissing True storeDir+  alreadyExists <- Dir.doesPathExist dest+  unless alreadyExists (copyPath src dest)++-- | Copy a file or directory tree to a destination.+copyPath :: FilePath -> FilePath -> IO ()+copyPath src dest = do+  isDir <- Dir.doesDirectoryExist src+  if isDir+    then do+      Dir.createDirectoryIfMissing True dest+      entries <- Dir.listDirectory src+      mapM_ (\entry -> copyPath (src </> entry) (dest </> entry)) entries+    else Dir.copyFile src dest
src/Nix/Eval/StringInterp.hs view
@@ -14,17 +14,23 @@ import Data.Text (Text) import qualified Data.Text as T import Nix.Eval.Context (concatStrings)-import Nix.Eval.Types (Env, MonadEval (..), NixValue (..), StringContext, emptyContext, typeName)+import Nix.Eval.Types (Env, MonadEval (..), NixValue (..), StringContext, Thunk, attrSetLookup, emptyContext, typeName) import Nix.Expr.Types (Expr, StringPart (..))  -- | The evaluator function, passed as a parameter to avoid cyclic imports. type Eval m = Env -> Expr -> m NixValue +-- | Force a thunk to a value.+type Force m = Thunk -> m NixValue++-- | Apply a function value to an argument value.+type Apply m = NixValue -> NixValue -> m NixValue+ -- | Evaluate the parts of a regular string (double-quoted). -- Returns the concatenated text and the merged context from all parts.-evalStringParts :: (MonadEval m) => Eval m -> Env -> [StringPart] -> m (Text, StringContext)-evalStringParts evalFn env parts = do-  chunks <- mapM (evalOnePart evalFn env) parts+evalStringParts :: (MonadEval m) => Eval m -> Force m -> Apply m -> Env -> [StringPart] -> m (Text, StringContext)+evalStringParts evalFn forceFn applyFn env parts = do+  chunks <- mapM (evalOnePart evalFn forceFn applyFn env) parts   pure (concatStrings chunks)  -- | Evaluate the parts of an indented string (double single-quoted).@@ -32,34 +38,47 @@ -- After interpolation, strips the common leading whitespace from all -- non-empty lines (the standard Nix indented-string semantics). -- Context is preserved through indentation stripping.-evalIndStringParts :: (MonadEval m) => Eval m -> Env -> [StringPart] -> m (Text, StringContext)-evalIndStringParts evalFn env parts = do-  (raw, ctx) <- evalStringParts evalFn env parts+evalIndStringParts :: (MonadEval m) => Eval m -> Force m -> Apply m -> Env -> [StringPart] -> m (Text, StringContext)+evalIndStringParts evalFn forceFn applyFn env parts = do+  (raw, ctx) <- evalStringParts evalFn forceFn applyFn env parts   pure (stripIndentation raw, ctx)  -- | Evaluate a single string part, returning its text and context.-evalOnePart :: (MonadEval m) => Eval m -> Env -> StringPart -> m (Text, StringContext)-evalOnePart _ _ (StrLit txt) = pure (txt, emptyContext)-evalOnePart evalFn env (StrInterp expr) = do+evalOnePart :: (MonadEval m) => Eval m -> Force m -> Apply m -> Env -> StringPart -> m (Text, StringContext)+evalOnePart _ _ _ _ (StrLit txt) = pure (txt, emptyContext)+evalOnePart evalFn forceFn applyFn env (StrInterp expr) = do   val <- evalFn env expr-  coerceToString val+  coerceToString forceFn applyFn val  -- | Coerce a Nix value to a string for interpolation. ----- Strict coercion: strings, ints, floats, paths, null, bools.--- Lists, sets (without @__toString@/@outPath@), and functions are errors.--- Used by string interpolation (@"${...}"@).-coerceToString :: (MonadEval m) => NixValue -> m (Text, StringContext)-coerceToString val = case val of-  VStr s ctx -> pure (s, ctx)-  VInt n -> pure (T.pack (show n), emptyContext)-  VFloat n -> pure (T.pack (show n), emptyContext)-  VPath p -> pure (p, emptyContext)-  VNull -> pure ("", emptyContext)-  VBool True -> pure ("1", emptyContext)-  VBool False -> pure ("", emptyContext)-  other ->-    throwEvalError ("cannot coerce " <> typeName other <> " to a string")+-- Strict coercion: strings, ints, floats, paths, null, bools, and+-- attribute sets with @__toString@ or @outPath@.+-- Lists and functions without coercion metadata are errors.+-- Used by string interpolation (@"${...}"@) and @builtins.toString@.+coerceToString :: (MonadEval m) => Force m -> Apply m -> NixValue -> m (Text, StringContext)+coerceToString _ _ (VStr s ctx) = pure (s, ctx)+coerceToString _ _ (VInt n) = pure (T.pack (show n), emptyContext)+coerceToString _ _ (VFloat n) = pure (T.pack (show n), emptyContext)+coerceToString _ _ (VPath p) = pure (p, emptyContext)+coerceToString _ _ VNull = pure ("", emptyContext)+coerceToString _ _ (VBool True) = pure ("1", emptyContext)+coerceToString _ _ (VBool False) = pure ("", emptyContext)+-- Attribute sets: try __toString first, then outPath+coerceToString forceFn applyFn (VAttrs attrs) =+  case attrSetLookup "__toString" attrs of+    Just toStrThunk -> do+      toStrFn <- forceFn toStrThunk+      result <- applyFn toStrFn (VAttrs attrs)+      coerceToString forceFn applyFn result+    Nothing -> case attrSetLookup "outPath" attrs of+      Just outPathThunk -> do+        outPathVal <- forceFn outPathThunk+        coerceToString forceFn applyFn outPathVal+      Nothing ->+        throwEvalError "cannot coerce a set to a string (missing __toString or outPath)"+coerceToString _ _ other =+  throwEvalError ("cannot coerce " <> typeName other <> " to a string")  -- --------------------------------------------------------------------------- -- Indented string whitespace stripping
src/Nix/Eval/Types.hs view
@@ -560,6 +560,14 @@   -- closures remain valid after the import scope ends.   resolvePathLiteral :: Text -> m Text +  -- | Copy a path (file or directory) to the store, returning the store path.+  -- First argument is the source path, second is the store name.+  copyPathToStore :: Text -> Text -> m Text++  -- | Print a trace/warning message.+  -- IO evaluators write to stderr; pure evaluators silently discard.+  traceMessage :: Text -> m ()+   -- | Force a thunk to a value, with memoization.   -- IO evaluators should cache results (via per-thunk @IORef@).   -- Pure evaluators re-evaluate each time.@@ -587,6 +595,8 @@   readFileBytes _ = throwEvalError "hashFile: not available in pure evaluation"   getFileType _ = throwEvalError "readFileType: not available in pure evaluation"   runProcess _ _ _ = throwEvalError "runProcess: not available in pure evaluation"+  copyPathToStore _ _ = throwEvalError "builtins.path: not available in pure evaluation"+  traceMessage _ = pure ()   resolvePathLiteral = pure   forceThunk _ (Evaluated val) = pure val   forceThunk evalFn (ThunkRef ref) =