purescript-0.15.15: src/Language/PureScript/Make/Cache.hs
module Language.PureScript.Make.Cache
( ContentHash
, hash
, CacheDb
, CacheInfo(..)
, checkChanged
, removeModules
, normaliseForCache
) where
import Prelude
import Control.Category ((>>>))
import Control.Monad ((>=>))
import Crypto.Hash (HashAlgorithm, Digest, SHA512)
import Crypto.Hash qualified as Hash
import Data.Aeson qualified as Aeson
import Data.Align (align)
import Data.ByteArray.Encoding (Base(Base16), convertToBase, convertFromBase)
import Data.ByteString qualified as BS
import Data.Map (Map)
import Data.Map qualified as Map
import Data.Maybe (fromMaybe)
import Data.Monoid (All(..))
import Data.Set (Set)
import Data.Text (Text)
import Data.Text.Encoding (encodeUtf8, decodeUtf8)
import Data.These (These(..))
import Data.Time.Clock (UTCTime)
import Data.Traversable (for)
import System.FilePath qualified as FilePath
import Language.PureScript.Names (ModuleName)
digestToHex :: Digest a -> Text
digestToHex = decodeUtf8 . convertToBase Base16
digestFromHex :: forall a. HashAlgorithm a => Text -> Maybe (Digest a)
digestFromHex =
encodeUtf8
>>> either (const Nothing) Just . convertFromBase Base16
>=> (Hash.digestFromByteString :: BS.ByteString -> Maybe (Digest a))
-- | Defines the hash algorithm we use for cache invalidation of input files.
newtype ContentHash = ContentHash
{ unContentHash :: Digest SHA512 }
deriving (Show, Eq, Ord)
instance Aeson.ToJSON ContentHash where
toJSON = Aeson.toJSON . digestToHex . unContentHash
instance Aeson.FromJSON ContentHash where
parseJSON x = do
str <- Aeson.parseJSON x
case digestFromHex str of
Just digest ->
pure $ ContentHash digest
Nothing ->
fail "Unable to decode ContentHash"
hash :: BS.ByteString -> ContentHash
hash = ContentHash . Hash.hash
type CacheDb = Map ModuleName CacheInfo
-- | A CacheInfo contains all of the information we need to store about a
-- particular module in the cache database.
newtype CacheInfo = CacheInfo
{ unCacheInfo :: Map FilePath (UTCTime, ContentHash) }
deriving stock (Show)
deriving newtype (Eq, Ord, Semigroup, Monoid, Aeson.FromJSON, Aeson.ToJSON)
-- | Given a module name, and a map containing the associated input files
-- together with current metadata i.e. timestamps and hashes, check whether the
-- input files have changed, based on comparing with the database stored in the
-- monadic state.
--
-- The CacheInfo in the return value should be stored in the cache for future
-- builds.
--
-- The Bool in the return value indicates whether it is safe to use existing
-- build artifacts for this module, at least based on the timestamps and hashes
-- of the module's input files.
--
-- If the timestamps are the same as those in the database, assume the file is
-- unchanged, and return True without checking hashes.
--
-- If any of the timestamps differ from what is in the database, check the
-- hashes of those files. In this case, update the database with any changed
-- timestamps and hashes, and return True if and only if all of the hashes are
-- unchanged.
checkChanged
:: Monad m
=> CacheDb
-> ModuleName
-> FilePath
-> Map FilePath (UTCTime, m ContentHash)
-> m (CacheInfo, Bool)
checkChanged cacheDb mn basePath currentInfo = do
let dbInfo = unCacheInfo $ fromMaybe mempty (Map.lookup mn cacheDb)
(newInfo, isUpToDate) <-
fmap mconcat $
for (Map.toList (align dbInfo currentInfo)) $ \(normaliseForCache basePath -> fp, aligned) -> do
case aligned of
This _ -> do
-- One of the input files listed in the cache no longer exists;
-- remove that file from the cache and note that the module needs
-- rebuilding
pure (Map.empty, All False)
That (timestamp, getHash) -> do
-- The module has a new input file; add it to the cache and
-- note that the module needs rebuilding.
newHash <- getHash
pure (Map.singleton fp (timestamp, newHash), All False)
These db@(dbTimestamp, _) (newTimestamp, _) | dbTimestamp == newTimestamp -> do
-- This file exists both currently and in the cache database,
-- and the timestamp is unchanged, so we skip checking the
-- hash.
pure (Map.singleton fp db, mempty)
These (_, dbHash) (newTimestamp, getHash) -> do
-- This file exists both currently and in the cache database,
-- but the timestamp has changed, so we need to check the hash.
newHash <- getHash
pure (Map.singleton fp (newTimestamp, newHash), All (dbHash == newHash))
pure (CacheInfo newInfo, getAll isUpToDate)
-- | Remove any modules from the given set from the cache database; used when
-- they failed to build.
removeModules :: Set ModuleName -> CacheDb -> CacheDb
removeModules = flip Map.withoutKeys
-- | 1. Any path that is beneath our current working directory will be
-- stored as a normalised relative path
-- 2. Any path that isn't will be stored as an absolute path
normaliseForCache :: FilePath -> FilePath -> FilePath
normaliseForCache basePath fp =
if FilePath.isRelative fp then
FilePath.normalise fp
else
let relativePath = FilePath.makeRelative basePath fp in
if FilePath.isRelative relativePath then
FilePath.normalise relativePath
else
-- If the path is still absolute after trying to make it
-- relative to the base that means it is not underneath
-- the base path
FilePath.normalise fp