{-# LANGUAGE DerivingStrategies #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE TemplateHaskell #-} module Niv.Sources where import Data.Aeson (FromJSON, FromJSONKey, ToJSON, ToJSONKey) import qualified Data.Aeson as Aeson import qualified Data.Aeson.Extended as Aeson import Data.Bifunctor (first) import qualified Data.ByteString as B import qualified Data.ByteString.Lazy.Char8 as BL8 import qualified Data.Digest.Pure.MD5 as MD5 import Data.FileEmbed (embedFile) import qualified Data.HashMap.Strict as HMS import Data.Hashable (Hashable) import Data.List import Data.String.QQ (s) import qualified Data.Text as T import Data.Text.Extended import Niv.Logger import Niv.Update import qualified System.Directory as Dir import System.FilePath (()) import UnliftIO ------------------------------------------------------------------------------- -- sources.json related ------------------------------------------------------------------------------- -- | Where to find the sources.json data FindSourcesJson = -- | use the default (nix/sources.json) Auto | -- | use the specified file path AtPath FilePath data SourcesError = SourcesDoesntExist | SourceIsntJSON | SpecIsntAMap newtype Sources = Sources {unSources :: HMS.HashMap PackageName PackageSpec} deriving newtype (FromJSON, ToJSON) getSourcesEither :: FindSourcesJson -> IO (Either SourcesError Sources) getSourcesEither fsj = do Dir.doesFileExist (pathNixSourcesJson fsj) >>= \case False -> pure $ Left SourcesDoesntExist True -> Aeson.decodeFileStrict (pathNixSourcesJson fsj) >>= \case Just value -> case valueToSources value of Nothing -> pure $ Left SpecIsntAMap Just srcs -> pure $ Right srcs Nothing -> pure $ Left SourceIsntJSON where valueToSources :: Aeson.Value -> Maybe Sources valueToSources = \case Aeson.Object obj -> fmap (Sources . mapKeys PackageName) $ traverse ( \case Aeson.Object obj' -> Just (PackageSpec obj') _ -> Nothing ) obj _ -> Nothing mapKeys :: (Eq k2, Hashable k2) => (k1 -> k2) -> HMS.HashMap k1 v -> HMS.HashMap k2 v mapKeys f = HMS.fromList . map (first f) . HMS.toList getSources :: FindSourcesJson -> IO Sources getSources fsj = do warnIfOutdated getSourcesEither fsj >>= either ( \case SourcesDoesntExist -> (abortSourcesDoesntExist fsj) SourceIsntJSON -> (abortSourcesIsntJSON fsj) SpecIsntAMap -> (abortSpecIsntAMap fsj) ) pure setSources :: FindSourcesJson -> Sources -> IO () setSources fsj sources = Aeson.encodeFilePretty (pathNixSourcesJson fsj) sources newtype PackageName = PackageName {unPackageName :: T.Text} deriving newtype (Eq, Hashable, FromJSONKey, ToJSONKey, Show) newtype PackageSpec = PackageSpec {unPackageSpec :: Aeson.Object} deriving newtype (FromJSON, ToJSON, Show, Semigroup, Monoid) -- | Simply discards the 'Freedom' attrsToSpec :: Attrs -> PackageSpec attrsToSpec = PackageSpec . fmap snd -- | @nix/sources.json@ or pointed at by 'FindSourcesJson' pathNixSourcesJson :: FindSourcesJson -> FilePath pathNixSourcesJson = \case Auto -> "nix" "sources.json" AtPath f -> f -- -- ABORT messages -- abortSourcesDoesntExist :: FindSourcesJson -> IO a abortSourcesDoesntExist fsj = abort $ T.unlines [line1, line2] where line1 = "Cannot use " <> T.pack (pathNixSourcesJson fsj) line2 = [s| The sources file does not exist! You may need to run 'niv init'. |] abortSourcesIsntJSON :: FindSourcesJson -> IO a abortSourcesIsntJSON fsj = abort $ T.unlines [line1, line2] where line1 = "Cannot use " <> T.pack (pathNixSourcesJson fsj) line2 = "The sources file should be JSON." abortSpecIsntAMap :: FindSourcesJson -> IO a abortSpecIsntAMap fsj = abort $ T.unlines [line1, line2] where line1 = "Cannot use " <> T.pack (pathNixSourcesJson fsj) line2 = [s| The package specifications in the sources file should be JSON maps from attribute name to attribute value, e.g.: { "nixpkgs": { "foo": "bar" } } |] ------------------------------------------------------------------------------- -- sources.nix related ------------------------------------------------------------------------------- -- | All the released versions of nix/sources.nix data SourcesNixVersion = V1 | V2 | V3 | V4 | V5 | V6 | V7 | V8 | V9 | V10 | V11 | V12 | V13 | V14 | V15 | V16 | V17 | -- prettify derivation name -- add 'local' type of sources V18 | -- add NIV_OVERRIDE_{name} V19 deriving stock (Bounded, Enum, Eq) -- | A user friendly version sourcesVersionToText :: SourcesNixVersion -> T.Text sourcesVersionToText = \case V1 -> "1" V2 -> "2" V3 -> "3" V4 -> "4" V5 -> "5" V6 -> "6" V7 -> "7" V8 -> "8" V9 -> "9" V10 -> "10" V11 -> "11" V12 -> "12" V13 -> "13" V14 -> "14" V15 -> "15" V16 -> "16" V17 -> "17" V18 -> "18" V19 -> "19" latestVersionMD5 :: T.Text latestVersionMD5 = sourcesVersionToMD5 maxBound -- | Find a version based on the md5 of the nix/sources.nix md5ToSourcesVersion :: T.Text -> Maybe SourcesNixVersion md5ToSourcesVersion md5 = find (\snv -> sourcesVersionToMD5 snv == md5) [minBound .. maxBound] -- | The MD5 sum of a particular version sourcesVersionToMD5 :: SourcesNixVersion -> T.Text sourcesVersionToMD5 = \case V1 -> "a7d3532c70fea66ffa25d6bc7ee49ad5" V2 -> "24cc0719fa744420a04361e23a3598d0" V3 -> "e01ed051e2c416e0fc7355fc72aeee3d" V4 -> "f754fe0e661b61abdcd32cb4062f5014" V5 -> "c34523590ff7dec7bf0689f145df29d1" V6 -> "8143f1db1e209562faf80a998be4929a" V7 -> "00a02cae76d30bbef96f001cabeed96f" V8 -> "e8b860753dd7fa1fd7b805dd836eb607" V9 -> "87149616c1b3b1e5aa73178f91c20b53" V10 -> "d8625c0a03dd935e1c79f46407faa8d3" V11 -> "8a95b7d93b16f7c7515d98f49b0ec741" V12 -> "2f9629ad9a8f181ed71d2a59b454970c" V13 -> "5e23c56b92eaade4e664cb16dcac1e0a" V14 -> "b470e235e7bcbf106d243fea90b6cfc9" V15 -> "dc11af910773ec9b4e505e0f49ebcfd2" V16 -> "2d93c52cab8e960e767a79af05ca572a" V17 -> "149b8907f7b08dc1c28164dfa55c7fad" V18 -> "bc5e6aefcaa6f9e0b2155ca4f44e5a33" V19 -> "543621698065cfc6a4a7985af76df718" -- | The MD5 sum of ./nix/sources.nix sourcesNixMD5 :: IO T.Text sourcesNixMD5 = T.pack . show . MD5.md5 <$> BL8.readFile pathNixSourcesNix -- | @nix/sources.nix@ pathNixSourcesNix :: FilePath pathNixSourcesNix = "nix" "sources.nix" warnIfOutdated :: IO () warnIfOutdated = do tryAny (BL8.readFile pathNixSourcesNix) >>= \case Left e -> tsay $ T.unlines -- warn with tsay [ T.unwords [tyellow "WARNING:", "Could not read", T.pack pathNixSourcesNix], T.unwords [" ", "(", tshow e, ")"] ] Right content -> do case md5ToSourcesVersion (T.pack $ show $ MD5.md5 content) of -- This is a custom or newer version, we don't do anything Nothing -> pure () Just v -- The file is the latest | v == maxBound -> pure () -- The file is older than than latest | otherwise -> do tsay $ T.unlines [ T.unwords [ tbold $ tblue "INFO:", "new sources.nix available:", sourcesVersionToText v, "->", sourcesVersionToText maxBound ], " Please run 'niv init' or add the following line in the " <> T.pack pathNixSourcesNix <> " file:", " # niv: no_update" ] -- | Glue code between nix and sources.json initNixSourcesNixContent :: B.ByteString initNixSourcesNixContent = $(embedFile "nix/sources.nix") -- | Empty JSON map initNixSourcesJsonContent :: B.ByteString initNixSourcesJsonContent = "{}"