packages feed

nix-freeze-tree-0.1.0.0: src/NixHash.hs

{-# LANGUAGE DataKinds         #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeApplications  #-}

module NixHash (mkFileAttrs, replaceLeadingDot) where

import           BuilderDirs (BuilderDirs, currentDir)
import           Control.Monad.IO.Class (MonadIO(..))
import           Control.Monad.Trans.Reader
import           Data.Binary.Put (runPut)
import           Data.Bool (bool)
import qualified Data.ByteString.Lazy as BL
import           Data.Text (Text)
import qualified Data.Text as T
import           Nix
import           Path (Abs, File, Path, Rel, (</>), toFilePath)
import qualified System.Directory as D
import           System.Nix.Hash (HashAlgorithm(..), encodeBase32, hashLazy)
import           System.Nix.Nar
  ( FileSystemObject(..)
  , IsExecutable(..)
  , Nar(..)
  , putNar
  )

-- | Build a set of @filename -> derivation@ attributes.
mkFileAttrs :: [Path Rel File] -> ReaderT BuilderDirs IO [(Text, NExpr)]
mkFileAttrs = traverse $ \f -> do
  h <- mkFileAttr f
  pure (T.pack (toFilePath f), h)

-- | Hash the file, using Nix's NAR hash (the default to
-- @builtins.path@ is NAR-style recursive hashing), and build a call
-- to Nix's @builtins.path@ function. If @"file1"@ is a regular file,
-- @mkHashEntry "file1"@ will generate:
--
-- @
-- builtins.path {
--   path = ./file1;
--   sha256 = "<<hash of file1>>";
-- }
-- @
--
-- If @".file2"@ starts with a dot, that's an illegal Nix derivation
-- name, so @mkHashEntry ".file2"@ will generate:
--
-- @
-- builtins.path {
--   path = ./.file2;
--   name = "_file2";
--   sha256 = "<<hash of .file2>>"
-- }
-- @
mkFileAttr :: Path Rel File -> ReaderT BuilderDirs IO NExpr
mkFileAttr f = do
  absF <- (</> f) <$> currentDir
  h <- recursiveHash absF

  let bindings =
        [ "path" $= mkPath False (toFilePath absF)
        , "sha256" $= mkStr h
        ] ++ [ "name" $= mkStr (T.pack . replaceLeadingDot $ toFilePath f)
             | head (toFilePath f) == '.'
             ]
  pure $ "builtins" @. "path" @@ mkNonRecSet bindings


-- | Serialise as per
-- https://gist.github.com/jbeda/5c79d2b1434f0018d693 , which is an
-- extract from Figure 5.2 of http://nixos.org/~eelco/pubs/phd-thesis.pdf ,
-- which is the Nix thesis.
recursiveHash :: MonadIO m => Path Abs File -> m Text
recursiveHash absF = do
  b <- liftIO . BL.readFile $ toFilePath absF
  executable <- bool NonExecutable Executable . D.executable
    <$> liftIO (D.getPermissions $ toFilePath absF)
  let nar = runPut . putNar . Nar $ Regular executable (BL.length b) b
  pure . encodeBase32 $ hashLazy @'SHA256 nar

-- | A leading dot is illegal in Nix derivation names. Replace it with '_'.
replaceLeadingDot :: FilePath -> FilePath
replaceLeadingDot ('.':f) = '_':f
replaceLeadingDot f = f