cachix-1.7.9: src/Cachix/Client/NixConf.hs
{-# LANGUAGE DeriveFunctor #-}
{- (Very limited) parser, renderer and modifier of nix.conf
Supports subset of nix.conf given Nix 2.0 or Nix 1.0
When reading config files, it normalizes Nix 1.0/2.0 names to unified naming,
then when it writes the config back, it uses naming depending what given Nix
version considers as recommended.
-}
module Cachix.Client.NixConf
( NixConf,
NixConfG (..),
NixConfLine (..),
NixConfSource,
NixConfSourceG (..),
NixConfLoc (..),
IncludeType (..),
new,
render,
add,
remove,
read,
readWithDefault,
resolveIncludes,
write,
getFilename,
parser,
parse,
readLines,
writeLines,
isTrustedUsers,
defaultPublicURI,
defaultSigningKey,
)
where
import Cachix.Client.Exception (CachixException (..))
import Cachix.Client.URI qualified as URI
import Cachix.Types.BinaryCache qualified as BinaryCache
import Control.Exception.Safe qualified as Safe
import Data.List (nub)
import Data.Text qualified as T
import Protolude hiding (toS)
import Protolude.Conv (toS)
import System.Directory
( XdgDirectory (..),
createDirectoryIfMissing,
getXdgDirectory,
)
import System.FilePath (normalise)
import System.FilePath.Posix (takeDirectory, (</>))
import System.IO.Error (isDoesNotExistError)
import System.IO.Error qualified
import Text.Megaparsec qualified as Mega
import Text.Megaparsec.Char
defaultPublicURI :: Text
defaultPublicURI = "https://cache.nixos.org"
defaultSigningKey :: Text
defaultSigningKey = "cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY="
data NixConfLine
= Substituters [Text]
| TrustedUsers [Text]
| TrustedPublicKeys [Text]
| NetRcFile Text
| Include IncludeType
| Other Text
deriving (Show, Eq)
data IncludeType
= RequiredInclude Text -- for "include"
| OptionalInclude Text -- for "!include"
deriving (Show, Eq)
-- | A list of conf lines
type NixConf = NixConfG [NixConfLine]
newtype NixConfG a = NixConf a
deriving stock (Show, Eq, Functor)
-- | A wrapper around NixConf that also tracks the path to the nix.conf file
type NixConfSource = NixConfSourceG NixConf
data NixConfSourceG a = NixConfSource
{ nixConfPath :: FilePath,
nixConfLines :: a
}
deriving stock (Show, Eq, Functor)
data NixConfError
= -- | Error when trying to read the nix.conf file or an include
IOError FilePath System.IO.Error.IOError
| -- | Failed to parse the nix.conf file
ParseError FilePath Text
deriving (Show, Typeable)
-- | Operations on nix.conf.
-- Helps to work with both NixConf and NixConfSource.
class NixConfOps a where
-- | Read the nix.conf lines that match the given predicate
readLines :: (NixConfLine -> Maybe [Text]) -> a -> [Text]
-- | Write the given lines to the nix.conf
writeLines :: (NixConfLine -> Maybe [Text]) -> NixConfLine -> a -> a
-- | Add the given binary cache to the nix.conf
add :: BinaryCache.BinaryCache -> [a] -> a -> a
-- | Remove the given binary cache from the nix.conf
remove :: URI.URI -> Text -> [a] -> a -> (a, Bool)
-- | Render the nix.conf to a Text
render :: a -> Text
instance NixConfOps NixConf where
readLines predicate (NixConf xs) = foldl f [] xs
where
f :: [Text] -> NixConfLine -> [Text]
f prev next = prev <> fromMaybe [] (predicate next)
writeLines predicate addition = fmap f
where
f x = filter (isNothing . predicate) x <> [addition]
add bc toRead toWrite =
writeLines isPublicKey (TrustedPublicKeys $ nub publicKeys) $
writeLines isSubstituter (Substituters $ nub substituters) toWrite
where
-- Note: some defaults are always appended since overriding some setttings in nix.conf overrides defaults otherwise
substituters = (defaultPublicURI : concatMap (readLines isSubstituter) toRead) <> [BinaryCache.uri bc]
publicKeys = (defaultSigningKey : concatMap (readLines isPublicKey) toRead) <> BinaryCache.publicSigningKeys bc
remove uri name toRead toWrite =
(newconf, oldsubstituters /= substituters)
where
newconf =
writeLines isPublicKey (TrustedPublicKeys $ nub publicKeys) $
writeLines isSubstituter (Substituters $ nub substituters) toWrite
oldsubstituters = concatMap (readLines isSubstituter) toRead
substituters = filter (toS (URI.toByteString fulluri) /=) oldsubstituters
oldpublicKeys = concatMap (readLines isPublicKey) toRead
publicKeys = filter (not . T.isPrefixOf (toS $ URI.hostBS $ URI.getHostname fulluri)) oldpublicKeys
fulluri = URI.appendSubdomain name uri
render (NixConf ls) = T.unlines $ fmap go ls
where
go :: NixConfLine -> Text
go (Substituters xs) = "substituters" <> " = " <> T.unwords xs
go (TrustedUsers xs) = "trusted-users = " <> T.unwords xs
go (TrustedPublicKeys xs) = "trusted-public-keys" <> " = " <> T.unwords xs
go (NetRcFile filename) = "netrc-file = " <> filename
go (Include (RequiredInclude path)) = "include " <> path
go (Include (OptionalInclude path)) = "!include " <> path
go (Other line) = line
instance NixConfOps NixConfSource where
readLines f = readLines f . nixConfLines
writeLines f = fmap . writeLines f
add bc toRead toWrite = toWrite {nixConfLines = add bc (fmap nixConfLines toRead) (nixConfLines toWrite)}
remove uri name toRead toWrite =
let (newLines, changed) = remove uri name (fmap nixConfLines toRead) (nixConfLines toWrite)
in (toWrite {nixConfLines = newLines}, changed)
render = render . nixConfLines
isSubstituter :: NixConfLine -> Maybe [Text]
isSubstituter (Substituters xs) = Just xs
isSubstituter _ = Nothing
isPublicKey :: NixConfLine -> Maybe [Text]
isPublicKey (TrustedPublicKeys xs) = Just xs
isPublicKey _ = Nothing
isTrustedUsers :: NixConfLine -> Maybe [Text]
isTrustedUsers (TrustedUsers xs) = Just xs
isTrustedUsers _ = Nothing
-- | Create a new, empty NixConfSource with the given path
new :: FilePath -> NixConfSource
new path = NixConfSource path (NixConf [])
write :: NixConfSource -> IO ()
write NixConfSource {nixConfPath, nixConfLines} = do
createDirectoryIfMissing True (takeDirectory nixConfPath)
writeFile nixConfPath $ render nixConfLines
-- | Resolves includes in the given NixConfSource, starting from the given source file.
resolveIncludes :: NixConfSource -> IO [NixConfSource]
resolveIncludes conf@NixConfSource {nixConfPath} =
resolveIncludesWithStack [normalise nixConfPath] nixConfPath conf
resolveIncludesWithStack :: [FilePath] -> FilePath -> NixConfSource -> IO [NixConfSource]
resolveIncludesWithStack stack baseFile baseConf@(NixConfSource {nixConfLines = NixConf ls}) = do
includedConfigs <- mapM resolveInclude [f | Include f <- ls]
return $ baseConf : concat includedConfigs
where
dir = takeDirectory baseFile
resolveInclude :: IncludeType -> IO [NixConfSource]
resolveInclude includeType = do
let path = case includeType of
RequiredInclude p -> p
OptionalInclude p -> p
fullPath = normalise $ dir </> toS path
if fullPath `elem` stack
then case includeType of
RequiredInclude _ ->
throwIO $ CircularInclude (formatCircularError fullPath)
OptionalInclude _ -> return []
else do
read' fullPath >>= \case
Left err@(IOError _ _) ->
if isRequired includeType
then do
printNixConfError err
throwIO $ IncludeNotFound ("Failed to read required include file: " <> toS fullPath)
else return []
Left err@(ParseError _ _) -> do
printNixConfError err
throwIO $ IncludeNotFound ("Failed to read required include file: " <> toS fullPath)
Right conf ->
resolveIncludesWithStack (fullPath : stack) baseFile conf
isRequired (RequiredInclude _) = True
isRequired (OptionalInclude _) = False
formatCircularError path =
"Circular include detected:\n" <> T.intercalate "\n" (formatChain (reverse stack) path)
formatChain :: [FilePath] -> FilePath -> [Text]
formatChain chain target =
case chain of
[] -> []
(p : ps) ->
format p
: map ((" -> includes " <>) . format) ps
++ [" -> includes " <> format target <> " (circular reference)\n"]
where
format = toS . normalise :: FilePath -> Text
data NixConfLoc = Global | Local | Custom FilePath
deriving stock (Show, Eq)
-- | Safely read a nix.conf file from the given location.
-- Prints errors to stderr.
read :: NixConfLoc -> IO (Maybe NixConfSource)
read ncl = do
filename <- getFilename ncl
read' filename >>= \case
Left err -> do
printNixConfError err
return Nothing
Right conf -> return $ Just conf
-- | Safely read a nix.conf file from the given location.
-- Return an empty NixConfSource if the file does not exist or cannot be read.
-- Prints errors to stderr.
readWithDefault :: NixConfLoc -> IO NixConfSource
readWithDefault ncl = do
filename <- getFilename ncl
read' filename >>= \case
Left err -> do
printNixConfError err
return $ new filename
Right conf -> return conf
-- | Safely read a nix.conf file from the given file path.
read' :: FilePath -> IO (Either NixConfError NixConfSource)
read' filename = do
econtent <- Safe.tryIO (readFile filename)
return $ case econtent of
Left err -> Left $ IOError filename err
Right content ->
case parse content of
Left err -> Left $ ParseError filename $ toS (Mega.errorBundlePretty err)
Right conf -> Right $ NixConfSource filename conf
getFilename :: NixConfLoc -> IO FilePath
getFilename ncl = do
dir <-
case ncl of
Global -> return "/etc/nix"
Local -> getXdgDirectory XdgConfig "nix"
Custom filepath -> return filepath
return $ dir <> "/nix.conf"
printNixConfError :: NixConfError -> IO ()
printNixConfError (IOError path err) | isDoesNotExistError err = do
putErrText $
unlines
[ "No config at " <> toS path <> ":",
"",
toS (displayException err)
]
printNixConfError (IOError path err) = do
putErrText $
unlines
[ "Failed to read " <> toS path <> ":",
"",
toS (displayException err)
]
printNixConfError (ParseError path err) = do
putErrText $
unlines
[ "Failed to parse " <> toS path <> ":",
"",
err
]
-- nix.conf Parser
type Parser = Mega.Parsec Void Text
-- TODO: handle comments
parseLine :: ([Text] -> NixConfLine) -> Text -> Parser NixConfLine
parseLine constr name = Mega.try $ do
_ <- optional (some (char ' '))
_ <- string name
_ <- many (char ' ')
_ <- char '='
_ <- many (char ' ')
values <- Mega.sepBy1 (many (Mega.satisfy (not . isSpace))) (some (char ' '))
_ <- many spaceChar
return $ constr (fmap toS values)
parseInclude :: (Text -> IncludeType) -> Text -> Parser NixConfLine
parseInclude constr name = Mega.try $ do
_ <- optional (some (char ' '))
_ <- string name
_ <- some (char ' ')
path <- many (Mega.satisfy (not . isSpace))
_ <- many spaceChar
return $ Include (constr (toS path))
parseOther :: Parser NixConfLine
parseOther = Mega.try $ Other . toS <$> Mega.someTill Mega.anySingle (void eol <|> Mega.eof)
parseAltLine :: Parser NixConfLine
parseAltLine =
(Other "" <$ eol)
<|> parseLine Substituters "substituters"
<|> parseLine TrustedPublicKeys "trusted-public-keys"
<|> parseLine TrustedUsers "trusted-users"
<|> parseLine TrustedPublicKeys "binary-cache-public-keys"
<|> parseLine Substituters "binary-caches"
-- NB: assume that space in this option means space in filename
<|> parseLine (NetRcFile . T.concat) "netrc-file"
<|> parseInclude RequiredInclude "include"
<|> parseInclude OptionalInclude "!include"
<|> parseOther
parser :: Parser NixConf
parser = NixConf <$> many parseAltLine
parse :: Text -> Either (Mega.ParseErrorBundle Text Void) NixConf
parse = Mega.parse parser "nix.conf"