packages feed

nova-nix 0.1.7.1 → 0.1.8.0

raw patch · 8 files changed

+115/−45 lines, 8 filesdep ~crypton

Dependency ranges changed: crypton

Files

CHANGELOG.md view
@@ -1,5 +1,15 @@ # Changelog +## 0.1.8.0 — 2026-03-08++### Builder Correctness, Windows Store Paths, Hackage Fix++- **Fix #1: builder no longer pre-creates output paths** — `buildDerivationInner` no longer calls `createDirectoryIfMissing` for `$out`, `$dev`, etc. before running the builder. The builder is now responsible for creating its own outputs, matching real Nix semantics. Builds fail with `"builder succeeded but outputs missing"` if the builder exits successfully but expected outputs don't exist.+- **File outputs supported** — `$out` can now be a regular file, not just a directory. `registerSingleOutput`, `addToStore`, `scanReferences`, and `pathExists` all use `doesPathExist` instead of `doesDirectoryExist`. `moveOutput` (renamed from `moveDirectory`) handles both files and directories in cross-device fallback. `collectRegularFiles` accepts file paths directly for reference scanning.+- **Fix #2: Windows store paths use native separators** — CLI now uses `platformStoreDir` (`C:\nix\store` on Windows, `/nix/store` on Unix) for filesystem operations and display. Evaluator internals keep canonical `/nix/store` for ATerm hash compatibility.+- **Fix: `crypton < 1.1` in .cabal file** — Previous `crypton` pin was only in `cabal.project` (which Hackage ignores). Moved to `.cabal` so the Hackage solver respects it. `crypton >= 1.1` switched from `memory` to `ram` for `ByteArrayAccess`, breaking `http-client-tls`.+- 108 builtins, 526 tests, -Werror clean, ormolu clean, hlint clean+ ## 0.1.7.1 — 2026-03-07  ### Hackage Build Fix
README.md view
@@ -260,7 +260,7 @@ **Key numbers:**  - **24 modules** — all implemented-- **524 tests** — hand-rolled harness, no framework dependencies+- **526 tests** — hand-rolled harness, no framework dependencies - **Zero partial functions** — total by construction, `T.uncons` over `T.head`/`T.tail` - **Strict by default** — bang patterns on all data fields (except Thunk's Env, which is lazy for knot-tying) @@ -306,7 +306,7 @@  ```bash cabal build                              # Build library + CLI-cabal test                               # Run all 524 tests+cabal test                               # Run all 526 tests cabal build --ghc-options="-Werror"      # Warnings as errors (CI default) cabal haddock                            # Generate API docs ```
app/Main.hs view
@@ -27,7 +27,7 @@ import Nix.Eval.IO (EvalState (..), newEvalState, runEvalIO) import Nix.Parser (parseNix, readFileAutoEncoding) import Nix.Store (Store, closeStore, openStore, writeDrv)-import Nix.Store.Path (StorePath, defaultStoreDir, parseStorePath, storePathToFilePath)+import Nix.Store.Path (StorePath, defaultStoreDir, parseStorePath, platformStoreDir, storePathToFilePath) import Paths_nova_nix (getDataDir) import System.Directory (getCurrentDirectory, getTemporaryDirectory) import System.Environment (getArgs)@@ -165,12 +165,12 @@           exitFailure         Right val -> do           (drv, drvSP) <- extractDerivation val-          store <- openStore defaultStoreDir+          store <- openStore platformStoreDir           buildResult <- buildAndRegister store drv drvSP           closeStore store           case buildResult of             BuildSuccess sp ->-              TIO.putStrLn (T.pack (storePathToFilePath defaultStoreDir sp))+              TIO.putStrLn (T.pack (storePathToFilePath platformStoreDir sp))             BuildFailure msg code -> do               TIO.hPutStrLn stderr ("build failed (exit " <> T.pack (show code) <> "): " <> msg)               exitFailure@@ -219,7 +219,7 @@   -- Build with dependency resolution   tmpDir <- getTemporaryDirectory   let config =-        (defaultBuildConfig defaultStoreDir)+        (defaultBuildConfig platformStoreDir)           { bcTmpDir = tmpDir           }   buildWithDeps config store drv drvSP@@ -272,7 +272,7 @@ prettyValue (VCompiledRegex _) = "«compiled-regex»" prettyValue (VDerivation drv) =   case drvOutputs drv of-    (out : _) -> "«derivation " <> T.pack (storePathToFilePath defaultStoreDir (doPath out)) <> "»"+    (out : _) -> "«derivation " <> T.pack (storePathToFilePath platformStoreDir (doPath out)) <> "»"     [] -> "«derivation»"  -- | Pretty-print a thunk.  After deep-forcing, all thunks should be
nova-nix.cabal view
@@ -1,6 +1,6 @@ cabal-version:      3.0 name:               nova-nix-version:            0.1.7.1+version:            0.1.8.0 synopsis:           Windows-native Nix implementation in pure Haskell description:   A pure Haskell implementation of the Nix package manager that runs natively@@ -63,7 +63,7 @@     , array               >= 0.5 && < 0.6     , bytestring          >= 0.11 && < 0.13     , containers          >= 0.6 && < 0.8-    , crypton             >= 1.0 && < 2+    , crypton             >= 1.0 && < 1.1     , directory           >= 1.3 && < 1.4     , filepath            >= 1.4 && < 1.6     , http-client         >= 0.7 && < 0.8
src/Nix/Builder.hs view
@@ -43,7 +43,7 @@ where  import Control.Exception (SomeException, try)-import Control.Monad (when)+import Control.Monad (filterM, when) import Data.Char (toLower) import Data.Map.Strict (Map) import qualified Data.Map.Strict as Map@@ -56,7 +56,7 @@ import Nix.Store (Store (..), addToStore, isValid, scanReferences) import Nix.Store.Path (StoreDir (..), StorePath (..), storePathToFilePath) import Nix.Substituter (CacheConfig, SubstResult (..), trySubstitute)-import System.Directory (createDirectoryIfMissing, doesDirectoryExist, removeDirectoryRecursive)+import System.Directory (createDirectoryIfMissing, doesDirectoryExist, doesPathExist, removeDirectoryRecursive) import qualified System.Environment import System.Exit (ExitCode (..)) import System.FilePath (takeDirectory, takeFileName, (</>))@@ -155,9 +155,9 @@       let buildDir = computeBuildDir config drv       createDirectoryIfMissing True buildDir -      -- 3. Create output directories inside build dir+      -- 3. Compute output paths (but do NOT pre-create them — the builder+      --    is responsible for creating $out, $dev, etc.)       let outputDirs = [(doName out, buildDir </> T.unpack (doName out)) | out <- drvOutputs drv]-      mapM_ (createDirectoryIfMissing True . snd) outputDirs        -- 4. Set up environment       let builderPath = T.unpack (drvBuilder drv)@@ -172,11 +172,19 @@           cleanupBuildDir buildDir           pure (BuildFailure ("builder failed: " <> stderrText) exitCode)         Right () -> do-          -- 7. Success: register each output-          registerResult <- registerOutputs config store drv buildDir outputDirs-          -- Clean up build dir-          cleanupBuildDir buildDir-          pure registerResult+          -- 7. Success: validate that the builder created all expected outputs.+          --    Outputs may be files or directories — both are valid (real Nix+          --    allows $out to be a single file, a directory tree, or a symlink).+          missing <- filterM (fmap not . doesPathExist . snd) outputDirs+          case missing of+            [] -> do+              registerResult <- registerOutputs config store drv buildDir outputDirs+              cleanupBuildDir buildDir+              pure registerResult+            _ -> do+              cleanupBuildDir buildDir+              let names = T.intercalate ", " (map fst missing)+              pure (BuildFailure ("builder succeeded but outputs missing: " <> names) 1)  -- --------------------------------------------------------------------------- -- Input validation@@ -398,15 +406,15 @@ registerSingleOutput config store candidates drvPathText (output, (_outName, outDir)) = do   let targetSP = doPath output       targetPath = storePathToFilePath (bcStoreDir config) targetSP-  -- Check if output directory exists (builder should have created it)-  exists <- doesDirectoryExist outDir+  -- Check if output exists (file or directory — builder creates it)+  exists <- doesPathExist outDir   if not exists-    then pure (Left ("output directory missing: " <> T.pack outDir))+    then pure (Left ("output missing: " <> T.pack outDir))     else do       -- Scan for references in the output       refs <- scanReferences (bcStoreDir config) candidates outDir       -- Check if already in store (e.g. from a previous build)-      alreadyExists <- doesDirectoryExist targetPath+      alreadyExists <- doesPathExist targetPath       if alreadyExists         then pure (Right ())         else do
src/Nix/Store.hs view
@@ -69,8 +69,10 @@     createDirectoryIfMissing,     doesDirectoryExist,     doesFileExist,+    doesPathExist,     listDirectory,     removeDirectoryRecursive,+    removeFile,     renamePath,     setPermissions,   )@@ -99,18 +101,18 @@ isValid :: Store -> StorePath -> IO Bool isValid = isValidPath . stDB --- | Check if a store path directory exists on disk (regardless of DB).+-- | Check if a store path exists on disk (file or directory, regardless of DB). pathExists :: Store -> StorePath -> IO Bool-pathExists store sp = doesDirectoryExist (storePathToFilePath (stDir store) sp)+pathExists store sp = doesPathExist (storePathToFilePath (stDir store) sp)  -- --------------------------------------------------------------------------- -- Store operations -- --------------------------------------------------------------------------- --- | Move an output directory to the store path, set read-only, and register.+-- | Move a build output (file or directory) to the store path, set read-only,+-- and register. ----- If @renameDirectory@ fails (cross-device move), falls back to--- recursive copy + remove.+-- If @renamePath@ fails (cross-device move), falls back to copy + remove. addToStore ::   Store ->   FilePath ->@@ -118,10 +120,10 @@   Maybe Text ->   [StorePath] ->   IO ()-addToStore store srcDir sp deriver refs = do+addToStore store srcPath sp deriver refs = do   let destPath = storePathToFilePath (stDir store) sp   -- Move (or copy) source to store-  moveDirectory srcDir destPath+  moveOutput srcPath destPath   -- Set read-only permissions   setReadOnly destPath   -- Register in database@@ -135,13 +137,19 @@         prReferences = refs       } --- | Cross-device safe directory move.+-- | Cross-device safe move for files or directories. -- Tries 'renamePath' first; on IOException falls back to copy + remove.-moveDirectory :: FilePath -> FilePath -> IO ()-moveDirectory src dest =+moveOutput :: FilePath -> FilePath -> IO ()+moveOutput src dest =   renamePath src dest `catch` \(_ :: IOException) -> do-    copyDirectoryRecursive src dest-    removeDirectoryRecursive src+    isDir <- doesDirectoryExist src+    if isDir+      then do+        copyDirectoryRecursive src dest+        removeDirectoryRecursive src+      else do+        copyFile src dest+        removeFile src  -- | Recursively copy a directory tree. copyDirectoryRecursive :: FilePath -> FilePath -> IO ()@@ -176,15 +184,18 @@     pure (scanBytes prefixBytes prefixLen hashLen contents acc)   pure [sp | (h, sp) <- Set.toList candidateSet, Set.member h foundHashes] --- | Collect all regular files under a directory, recursively.+-- | Collect all regular files under a path, recursively.+-- If the path is itself a regular file, returns it directly. collectRegularFiles :: FilePath -> IO [FilePath]-collectRegularFiles dir = do-  exists <- doesDirectoryExist dir-  if not exists-    then pure []+collectRegularFiles path = do+  isDir <- doesDirectoryExist path+  if isDir+    then do+      entries <- listDirectory path+      concat <$> mapM (classifyAndCollect path) entries     else do-      entries <- listDirectory dir-      concat <$> mapM (classifyAndCollect dir) entries+      isFile <- doesFileExist path+      pure [path | isFile]   where     classifyAndCollect parent name = do       let fullPath = parent </> name
src/Nix/Store/Path.hs view
@@ -33,6 +33,7 @@     StoreDir (..),     defaultStoreDir,     defaultStoreDirText,+    platformStoreDir,     platformStoreDirText,     windowsStoreDir, @@ -64,15 +65,20 @@ defaultStoreDirText :: Text defaultStoreDirText = T.pack (unStoreDir defaultStoreDir) +-- | Platform-appropriate store directory.+-- Returns @C:\\nix\\store@ on Windows, @\/nix\/store@ on Unix.+-- Use this for filesystem operations and user-facing output.+-- Use 'defaultStoreDir' only for Nix-internal canonical paths (ATerm hashing).+platformStoreDir :: StoreDir+platformStoreDir = case System.Info.os of+  "mingw32" -> windowsStoreDir+  _ -> defaultStoreDir+ -- | Platform-appropriate store directory as 'Text'. -- Returns @C:\\nix\\store@ on Windows, @\/nix\/store@ on Unix. -- Used for user-facing values like @builtins.storeDir@. platformStoreDirText :: Text platformStoreDirText = T.pack (unStoreDir platformStoreDir)-  where-    platformStoreDir = case System.Info.os of-      "mingw32" -> windowsStoreDir-      _ -> defaultStoreDir  -- | Default store directory on Windows: @C:\\nix\\store@. windowsStoreDir :: StoreDir
test/Main.hs view
@@ -3037,6 +3037,41 @@         forceRemoveIfExists tmpStore         forceRemoveIfExists (bcTmpDir config)         pure ret,+      -- Builder succeeds but doesn't create $out -> build fails+      runTestM "missing output fails build" $ do+        tmpBase <- getTemporaryDirectory+        let tmpStore = tmpBase </> "nova-nix-test-builder-noout"+        forceRemoveIfExists tmpStore+        store <- openStore (StoreDir tmpStore)+        let outSP = StorePath "iiiiiiiiiiiiiiiiiiiiiiiiiiiiiiii" "nooutput"+            drv = mkTestBuildDrv shell outSP "echo 'forgot to create output'"+            config = (defaultBuildConfig (stDir store)) {bcTmpDir = tmpBase </> "nova-nix-test-builder-noout-tmp"}+        result <- buildDerivation config store drv+        closeStore store+        forceRemoveIfExists tmpStore+        forceRemoveIfExists (bcTmpDir config)+        pure $ case result of+          BuildFailure msg _ ->+            if T.isInfixOf "outputs missing" msg+              then Pass+              else Fail ("expected 'outputs missing' error, got: " <> msg)+          BuildSuccess _ -> Fail "expected failure when builder doesn't create $out",+      -- File output (not directory) should succeed+      runTestM "file output succeeds" $ do+        tmpBase <- getTemporaryDirectory+        let tmpStore = tmpBase </> "nova-nix-test-builder-fileout"+        forceRemoveIfExists tmpStore+        store <- openStore (StoreDir tmpStore)+        let outSP = StorePath "jjjjjjjjjjjjjjjjjjjjjjjjjjjjjj" "fileout"+            drv = mkTestBuildDrv shell outSP "echo 'I am a file output' > $out"+            config = (defaultBuildConfig (stDir store)) {bcTmpDir = tmpBase </> "nova-nix-test-builder-fileout-tmp"}+        result <- buildDerivation config store drv+        closeStore store+        forceRemoveIfExists tmpStore+        forceRemoveIfExists (bcTmpDir config)+        pure $ case result of+          BuildSuccess sp -> assertEqual "file output path" outSP sp+          BuildFailure msg code -> Fail ("file output build failed (" <> T.pack (show code) <> "): " <> msg),       -- Cleanup after failure       runTestM "cleanup after failure" $ do         tmpBase <- getTemporaryDirectory