{-# LANGUAGE DataKinds #-}
{-# LANGUAGE OverloadedStrings #-}
module NixHash (mkFileAttrs, replaceUnsafeChars) where
import BuilderDirs (BuilderDirs, currentDir)
import Control.Monad.IO.Class (MonadIO (..))
import Control.Monad.Trans.Reader
import Crypto.Hash (Digest, SHA256, hash)
import qualified Data.ByteString as B
import Data.Char (isAsciiLower, isAsciiUpper, isDigit)
import Data.Text (Text)
import qualified Data.Text as T
import Nix
import Path (Abs, File, Path, Rel, toFilePath, (</>))
import Path.IO (withSystemTempFile)
import System.IO
import System.Nix.Hash (BaseEncoding (..), encodeDigestWith)
import System.Nix.Nar (buildNarIO, narEffectsIO)
-- | 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 name = toFilePath f
sanitizedName = replaceUnsafeChars name
bindings =
[ "path" $= mkStr (T.pack (toFilePath absF)),
"sha256" $= mkStr h
]
++ ["name" $= mkStr (T.pack sanitizedName) | name /= sanitizedName]
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 =
liftIO . withSystemTempFile "nix-freeze-tree.nar" $ \_ h -> do
buildNarIO narEffectsIO (toFilePath absF) h
hSeek h AbsoluteSeek 0
contents <- B.hGetContents h
pure $ encodeDigestWith NixBase32 (hash contents :: Digest SHA256)
-- | Replace various characters which don't work in Nix derivation names
replaceUnsafeChars :: FilePath -> FilePath
replaceUnsafeChars = map replaceUnsafeChar . replaceLeadingDot
where
replaceLeadingDot ('.' : f) = '_' : f
replaceLeadingDot f = f
replaceUnsafeChar ch
| isAsciiLower ch = ch
| isAsciiUpper ch = ch
| isDigit ch = ch
| ch `elem` ['\'', '_', '-', '.'] = ch
| otherwise = '_'