packages feed

cachix 1.7.5 → 1.7.6

raw patch · 6 files changed

+323/−95 lines, 6 files

Files

CHANGELOG.md view
@@ -5,6 +5,12 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## [1.7.6] - 2025-01-23++### Fixed++- cachix use: support `include` statements when adding caches to `nix.conf`+ ## [1.7.5] - 2024-10-11  ### Fixed
cachix.cabal view
@@ -1,6 +1,6 @@ cabal-version:      2.2 name:               cachix-version:            1.7.5+version:            1.7.6 synopsis:   Command-line client for Nix binary cache hosting https://cachix.org @@ -263,6 +263,7 @@     , dhall     , directory     , extra+    , filepath     , hercules-ci-cnix-store     , here     , hspec
src/Cachix/Client/Exception.hs view
@@ -12,6 +12,8 @@   | NoInput Text   | NoConfig Text   | NetRcParseError Text+  | IncludeNotFound Text+  | CircularInclude Text   | NarStreamingError ExitCode Text   | NarHashMismatch Text   | DeprecatedCommand Text@@ -33,6 +35,8 @@   displayException (ArtifactNotFound s) = toS s   displayException (NoSigningKey s) = toS s   displayException (NetRcParseError s) = toS s+  displayException (IncludeNotFound s) = toS s+  displayException (CircularInclude s) = toS s   displayException (NarStreamingError _ s) = toS s   displayException (NarHashMismatch s) = toS s   displayException (DeprecatedCommand s) = toS s
src/Cachix/Client/InstallationMode.hs view
@@ -81,8 +81,8 @@ getNixEnv :: IO NixEnv getNixEnv = do   user <- getUser-  nc <- NixConf.read NixConf.Global-  isTrusted <- isTrustedUser $ NixConf.readLines (catMaybes [nc]) NixConf.isTrustedUsers+  ncs <- NixConf.resolveIncludes =<< NixConf.readWithDefault NixConf.Global+  isTrusted <- isTrustedUser $ concatMap (NixConf.readLines NixConf.isTrustedUsers) ncs   isNixOS <- doesFileExist "/run/current-system/nixos-version"   return $     NixEnv@@ -139,37 +139,55 @@ addBinaryCache config bc _ (Install ncl) = do   (input, output) <- prepareNixConf ncl   netrcLocMaybe <- forM (guard $ not (BinaryCache.isPublic bc)) $ const $ addPrivateBinaryCacheNetRC config bc ncl-  let addNetRCLine :: NixConf.NixConf -> NixConf.NixConf+  let addNetRCLine :: NixConf.NixConfSource -> NixConf.NixConfSource       addNetRCLine = fromMaybe identity $ do         netrcLoc <- netrcLocMaybe :: Maybe FilePath         -- We only add the netrc line for local user configs for now.         -- On NixOS we assume it will be picked up from the default location.         guard (ncl == NixConf.Local)-        pure (NixConf.setNetRC $ toS netrcLoc)-  NixConf.write ncl $ addNetRCLine $ NixConf.add bc input output+        pure $ setNetRC (toS netrcLoc)+  NixConf.write $ addNetRCLine $ NixConf.add bc input output   filename <- NixConf.getFilename ncl   putStrLn $ "Configured " <> BinaryCache.uri bc <> " binary cache in " <> toS filename -prepareNixConf :: NixConf.NixConfLoc -> IO ([NixConf.NixConf], NixConf.NixConf)+-- | Resolve and read the nix.conf.+--+-- Returns a set of "input" confs and the "output" conf.+--+-- The output is the parsed conf file at the location specified by the NixConfLoc.+--+-- Inputs are any confs included by the output conf, plus any additional external resolutions.+-- For example, for the local Nix conf, we also return the global one.+prepareNixConf :: NixConf.NixConfLoc -> IO ([NixConf.NixConfSource], NixConf.NixConfSource) prepareNixConf ncl = do+  outputPath <- NixConf.getFilename ncl   -- TODO: might need locking one day   gnc <- NixConf.read NixConf.Global+  gncInputs <- traverse NixConf.resolveIncludes gnc+   (input, output) <-     case ncl of-      NixConf.Global -> return ([gnc], gnc)+      NixConf.Global -> do+        return (gncInputs, gnc)       NixConf.Local -> do         lnc <- NixConf.read NixConf.Local-        return ([gnc, lnc], lnc)+        lncInputs <- traverse NixConf.resolveIncludes lnc+        return (gncInputs <> lncInputs, lnc)       NixConf.Custom _ -> do         lnc <- NixConf.read ncl-        return ([lnc], lnc)-  return (catMaybes input, fromMaybe (NixConf.NixConf []) output)+        lncInputs <- traverse NixConf.resolveIncludes lnc+        return (lncInputs, lnc) +  return+    ( fromMaybe [] input,+      fromMaybe (NixConf.new outputPath) output+    )+ removeBinaryCache :: URI.URI -> Text -> InstallationMode -> IO () removeBinaryCache uri name (Install ncl) = do-  contents <- fromMaybe (NixConf.NixConf []) <$> NixConf.read ncl+  contents <- NixConf.readWithDefault ncl   let (final, removed) = NixConf.remove uri name [contents] contents-  NixConf.write ncl final+  NixConf.write final   filename <- NixConf.getFilename ncl   if removed     then putStrLn $ "Removed " <> host <> " binary cache in " <> toS filename@@ -178,6 +196,12 @@     host = URI.toByteString (URI.appendSubdomain name uri) removeBinaryCache _ _ _ = do   throwIO $ RemoveCacheUnsupported "Removing binary caches is only supported for nix.conf"++setNetRC :: Text -> NixConf.NixConfSource -> NixConf.NixConfSource+setNetRC netrc conf = (fmap . fmap) (\ls -> filter noNetRc ls ++ [NixConf.NetRcFile netrc]) conf+  where+    noNetRc (NixConf.NetRcFile _) = False+    noNetRc _ = True  nixosBinaryCache :: Config -> BinaryCache.BinaryCache -> UseOptions -> IO () nixosBinaryCache config bc UseOptions {useNixOSFolder = baseDirectory} = do
src/Cachix/Client/NixConf.hs view
@@ -1,7 +1,6 @@ {-# LANGUAGE DeriveFunctor #-}-{-# LANGUAGE GeneralizedNewtypeDeriving #-} -{- (Very limited) parser, rendered and modifier of nix.conf+{- (Very limited) parser, renderer and modifier of nix.conf  Supports subset of nix.conf given Nix 2.0 or Nix 1.0 @@ -14,12 +13,17 @@   ( NixConf,     NixConfG (..),     NixConfLine (..),+    NixConfSource,+    NixConfSourceG (..),     NixConfLoc (..),+    IncludeType (..),+    new,     render,     add,     remove,     read,-    update,+    readWithDefault,+    resolveIncludes,     write,     getFilename,     parser,@@ -29,10 +33,10 @@     isTrustedUsers,     defaultPublicURI,     defaultSigningKey,-    setNetRC,   ) where +import Cachix.Client.Exception (CachixException (..)) import Cachix.Client.URI qualified as URI import Cachix.Types.BinaryCache qualified as BinaryCache import Data.List (nub)@@ -45,35 +49,114 @@     doesFileExist,     getXdgDirectory,   )-import System.FilePath.Posix (takeDirectory)+import System.FilePath (normalise)+import System.FilePath.Posix (takeDirectory, (</>)) 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) -newtype NixConfG a = NixConf a-  deriving (Show, Eq, Functor)+data IncludeType+  = RequiredInclude Text -- for "include"+  | OptionalInclude Text -- for "!include"+  deriving (Show, Eq) +-- | A list of conf lines type NixConf = NixConfG [NixConfLine] -readLines :: [NixConf] -> (NixConfLine -> Maybe [Text]) -> [Text]-readLines nixconfs predicate = concatMap f nixconfs-  where-    f (NixConf xs) = foldl foldIt [] xs-    foldIt :: [Text] -> NixConfLine -> [Text]-    foldIt prev new = prev <> fromMaybe [] (predicate new)+newtype NixConfG a = NixConf a+  deriving stock (Show, Eq, Functor) -writeLines :: (NixConfLine -> Maybe [Text]) -> NixConfLine -> NixConf -> NixConf-writeLines predicate addition = fmap f-  where-    f x = filter (isNothing . predicate) x <> [addition]+-- | 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)++-- | 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@@ -86,54 +169,75 @@ isTrustedUsers (TrustedUsers xs) = Just xs isTrustedUsers _ = Nothing --- | Pure version of addIO-add :: BinaryCache.BinaryCache -> [NixConf] -> NixConf -> NixConf-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 : readLines toRead isSubstituter) <> [BinaryCache.uri bc]-    publicKeys = (defaultSigningKey : readLines toRead isPublicKey) <> BinaryCache.publicSigningKeys bc+-- | Create a new, empty NixConfSource with the given path+new :: FilePath -> NixConfSource+new path = NixConfSource path (NixConf []) -remove :: URI.URI -> Text -> [NixConf] -> NixConf -> (NixConf, Bool)-remove uri name toRead toWrite =-  (newconf, oldsubstituters /= substituters)+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-    newconf =-      writeLines isPublicKey (TrustedPublicKeys $ nub publicKeys) $-        writeLines isSubstituter (Substituters $ nub substituters) toWrite-    oldsubstituters = readLines toRead isSubstituter-    substituters = filter (toS (URI.toByteString fulluri) /=) oldsubstituters-    oldpublicKeys = readLines toRead isPublicKey-    publicKeys = filter (not . T.isPrefixOf (toS $ URI.hostBS $ URI.getHostname fulluri)) oldpublicKeys-    fulluri = URI.appendSubdomain name uri+    dir = takeDirectory baseFile -defaultPublicURI :: Text-defaultPublicURI = "https://cache.nixos.org"+    resolveInclude :: IncludeType -> IO [NixConfSource]+    resolveInclude includeType = do+      let path = case includeType of+            RequiredInclude p -> p+            OptionalInclude p -> p+          fullPath = normalise $ dir </> toS path -defaultSigningKey :: Text-defaultSigningKey = "cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY="+      if fullPath `elem` stack+        then case includeType of+          RequiredInclude _ -> throwIO $ CircularInclude (formatCircularError fullPath)+          OptionalInclude _ -> return []+        else do+          exists <- doesFileExist fullPath+          if not exists && isRequired includeType+            then throwIO $ IncludeNotFound $ toS fullPath+            else do+              content <- readFile fullPath+              case parse content of+                Left _err -> return []+                Right conf -> resolveIncludesWithStack (fullPath : stack) baseFile (NixConfSource fullPath conf) -render :: NixConf -> Text-render (NixConf nixconflines) = T.unlines $ fmap go nixconflines-  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 (Other line) = line+    isRequired (RequiredInclude _) = True+    isRequired (OptionalInclude _) = False -write :: NixConfLoc -> NixConf -> IO ()-write ncl nc = do-  filename <- getFilename ncl-  createDirectoryIfMissing True (takeDirectory filename)-  writeFile filename $ render nc+    formatCircularError path =+      "Circular include detected:\n" <> T.intercalate "\n" (formatChain (reverse stack) path) -read :: NixConfLoc -> IO (Maybe NixConf)+    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)++read :: NixConfLoc -> IO (Maybe NixConfSource) read ncl = do   filename <- getFilename ncl+  read' filename++read' :: FilePath -> IO (Maybe NixConfSource)+read' filename = do   doesExist <- doesFileExist filename   if not doesExist     then return Nothing@@ -143,21 +247,12 @@         Left err -> do           putStrLn (Mega.errorBundlePretty err)           panic $ toS filename <> " failed to parse, please copy the above error and contents of nix.conf and open an issue at https://github.com/cachix/cachix"-        Right conf -> return $ Just conf--update :: NixConfLoc -> (Maybe NixConf -> NixConf) -> IO ()-update ncl f = do-  nc <- f <$> read ncl-  write ncl nc--setNetRC :: Text -> NixConf -> NixConf-setNetRC netrc (NixConf nc) = NixConf $ filter noNetRc nc ++ [NetRcFile netrc]-  where-    noNetRc (NetRcFile _) = False-    noNetRc _ = True+        Right conf -> return (Just (NixConfSource filename conf)) -data NixConfLoc = Global | Local | Custom FilePath-  deriving (Show, Eq)+readWithDefault :: NixConfLoc -> IO NixConfSource+readWithDefault ncl = do+  filename <- getFilename ncl+  fromMaybe (new filename) <$> read' filename  getFilename :: NixConfLoc -> IO FilePath getFilename ncl = do@@ -183,6 +278,15 @@   _ <- 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) @@ -196,6 +300,8 @@     <|> 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
test/NixConfSpec.hs view
@@ -2,11 +2,14 @@  module NixConfSpec where +import Cachix.Client.Exception (CachixException (CircularInclude)) import Cachix.Client.NixConf as NixConf import Cachix.Types.BinaryCache (BinaryCache (..), CompressionMethod (..)) import Cachix.Types.Permission (Permission (..)) import Data.String.Here import Protolude+import System.FilePath ((</>))+import System.IO.Temp (withTempDirectory) import Test.Hspec  property :: Text -> Expectation@@ -33,8 +36,11 @@       property "substituters = a b c\n"     it "handles all known keys" $       property "substituters = a b c\ntrusted-users = him me\ntrusted-public-keys = a\n"+    it "handles includes" $+      property "include /etc/nix/nix.conf\n!include /etc/nix/nix.conf\n"     it "random content" $       property "blabla = foobar\nfoo = bar\n"+   describe "add" $ do     it "merges binary caches from both files" $       let globalConf =@@ -53,6 +59,7 @@                 TrustedPublicKeys [defaultSigningKey, "pub1", "pub2", "pub"]               ]        in add bc [globalConf, localConf] localConf `shouldBe` result+     it "is noop if binary cache exists in one file" $       let globalConf =             NixConf@@ -66,6 +73,7 @@                 TrustedPublicKeys [defaultSigningKey, "pub"]               ]        in add bc [globalConf, localConf] localConf `shouldBe` result+     it "preserves other nixconf entries" $       let globalConf =             NixConf@@ -87,6 +95,7 @@                 TrustedPublicKeys [defaultSigningKey, "pub1", "pub"]               ]        in add bc [globalConf, localConf] localConf `shouldBe` result+     it "removed duplicates" $       let globalConf =             NixConf@@ -104,6 +113,7 @@                 TrustedPublicKeys [defaultSigningKey, "pub1", "pub2", "pub"]               ]        in add bc [globalConf, localConf] localConf `shouldBe` result+     it "adds binary cache and defaults if no existing entries exist" $       let globalConf = NixConf []           localConf = NixConf []@@ -113,6 +123,7 @@                 TrustedPublicKeys [defaultSigningKey, "pub"]               ]        in add bc [globalConf, localConf] localConf `shouldBe` result+   describe "remove" $ do     it "removes a binary cache" $       let localConf =@@ -126,6 +137,7 @@                 TrustedPublicKeys [defaultSigningKey]               ]        in remove "https://cachix.org" "name" [localConf] localConf `shouldBe` (result, True)+     it "removes nothing if the binary cache is missing" $       let globalConf =             NixConf@@ -135,37 +147,99 @@           localConf = globalConf           result = localConf        in remove "https://cachix.org" "name" [globalConf, localConf] localConf `shouldBe` (result, False)+   describe "parse" $ do     it "parses substituters" $       parse "substituters = a\n"         `shouldBe` Right (NixConf [Substituters ["a"]])+     it "parses long key" $       parse "binary-caches-parallel-connections = 40\n"         `shouldBe` Right (NixConf [Other "binary-caches-parallel-connections = 40"])+     it "parses substituters with multiple values" $       parse "substituters = a b c\n"         `shouldBe` Right (NixConf [Substituters ["a", "b", "c"]])+     it "parses equal sign after the first key as literal" $       parse "substituters = a b c= d\n"         `shouldBe` Right (NixConf [Substituters ["a", "b", "c=", "d"]])+     it "parses with missing endline" $       parse "allowed-users = *"         `shouldBe` Right (NixConf [Other "allowed-users = *"])++    it "parses include" $+      parse "include /etc/nix/nix.conf\n"+        `shouldBe` Right (NixConf [Include (RequiredInclude "/etc/nix/nix.conf")])++    it "parses !include" $+      parse "!include /etc/nix/nix.conf\n"+        `shouldBe` Right (NixConf [Include (OptionalInclude "/etc/nix/nix.conf")])+     it "parses a complex example" $       parse realExample-        `shouldBe` Right-          ( NixConf-              [ Other "",-                Substituters ["a", "b", "c"],-                TrustedUsers ["him", "me"],-                TrustedPublicKeys ["a"],-                Other "blabla =  asd",-                Other "# comment",-                Other "",-                Other ""-              ]-          )+        `shouldBe` Right parsedRealExample +  describe "NixConfSource" $ do+    it "write . read" $ do+      withTempDirectory "/tmp" "nixconf" $ \temp -> do+        let confPath = temp </> "nix.conf"+        let subConfPath = temp </> "sub.conf"+        let confContents = "include " <> toS subConfPath <> "\n"+        let parsedConfContents = NixConf [Include (RequiredInclude (toS subConfPath))]+        writeFile confPath confContents+        writeFile subConfPath realExample++        Just conf <- NixConf.read (Custom temp)+        conf `shouldBe` NixConfSource confPath parsedConfContents++        NixConf.write conf+        readFile confPath `shouldReturn` confContents++    it "resolves includes" $ do+      withTempDirectory "/tmp" "nixconf" $ \temp -> do+        let confPath = temp </> "nix.conf"+            subConfPath = temp </> "sub.conf"+            confContents = "include " <> toS subConfPath <> "\n"+            parsedConfContents = NixConf [Include (RequiredInclude (toS subConfPath))]+        writeFile confPath confContents+        writeFile subConfPath realExample++        Just conf <- NixConf.read (Custom temp)+        NixConf.resolveIncludes conf+          `shouldReturn` [ NixConfSource confPath parsedConfContents,+                           NixConfSource subConfPath parsedRealExample+                         ]++    it "detects cycles" $ do+      withTempDirectory "/tmp" "nixconf" $ \temp -> do+        let confPath = temp </> "nix.conf"+            subConfPath = temp </> "sub.conf"+            confContents :: Text = "include " <> toS subConfPath <> "\n"+            subConfContents :: Text = "include " <> toS confPath <> "\n"+        writeFile confPath confContents+        writeFile subConfPath subConfContents++        Just conf <- NixConf.read (Custom temp)++        let isCircularInclude = \case+              CircularInclude _ -> True+              _ -> False+        NixConf.resolveIncludes conf `shouldThrow` isCircularInclude++    it "detects trusted-users through includes" $ do+      withTempDirectory "/tmp" "nixconf" $ \temp -> do+        let confPath = temp </> "nix.conf"+            subConfPath = temp </> "sub.conf"+            confContents :: Text = "include " <> toS subConfPath <> "\n"+            subConfContents :: Text = "trusted-users = @wheel"+        writeFile confPath confContents+        writeFile subConfPath subConfContents++        ncs <- NixConf.resolveIncludes =<< NixConf.readWithDefault (NixConf.Custom temp)+        concatMap (NixConf.readLines NixConf.isTrustedUsers) ncs `shouldBe` ["@wheel"]+ realExample :: Text realExample =   [hereLit|@@ -177,3 +251,16 @@   |]++parsedRealExample :: NixConf+parsedRealExample =+  NixConf+    [ Other "",+      Substituters ["a", "b", "c"],+      TrustedUsers ["him", "me"],+      TrustedPublicKeys ["a"],+      Other "blabla =  asd",+      Other "# comment",+      Other "",+      Other ""+    ]