cachix 0.2.1 → 0.3.0
raw patch · 27 files changed
+1393/−1049 lines, 27 filesdep +containersdep +hnix-store-coredep +inline-cdep −base16-bytestringsetup-changed
Dependencies added: containers, hnix-store-core, inline-c, inline-c-cpp
Dependencies removed: base16-bytestring
Files
- CHANGELOG.md +18/−1
- Setup.hs +1/−0
- cachix.cabal +101/−75
- cachix/Main.hs +7/−11
- cbits/aliases.h +8/−0
- src/Cachix/Client.hs +5/−6
- src/Cachix/Client/Commands.hs +124/−129
- src/Cachix/Client/Config.hs +45/−45
- src/Cachix/Client/Config/Orphans.hs +28/−0
- src/Cachix/Client/Env.hs +42/−35
- src/Cachix/Client/Exception.hs +5/−2
- src/Cachix/Client/InstallationMode.hs +97/−71
- src/Cachix/Client/NetRc.hs +31/−34
- src/Cachix/Client/NixConf.hs +79/−86
- src/Cachix/Client/NixVersion.hs +17/−25
- src/Cachix/Client/OptionsParser.hs +112/−90
- src/Cachix/Client/Push.hs +180/−151
- src/Cachix/Client/Secrets.hs +15/−17
- src/Cachix/Client/Servant.hs +1/−1
- src/Cachix/Client/Store.hs +193/−0
- src/Cachix/Client/Store/Context.hs +41/−0
- src/Cachix/Client/URI.hs +8/−9
- test/InstallationModeSpec.hs +58/−95
- test/Main.hs +5/−4
- test/NetRcSpec.hs +26/−32
- test/NixConfSpec.hs +127/−116
- test/NixVersionSpec.hs +19/−14
CHANGELOG.md view
@@ -7,6 +7,21 @@ ## Unreleased +## [0.3.0] - 2019-09-03++### Changed++- #222 allow NixOS installation only as root+- #215 LTS-14.0 and replace stack2nix with haskell.nix+- #216 add bulk store path querying as a performance optimization+- #222 drop support for Nix 2.0.1 or lower++### Fixed++- #222 Provide guidance if /etc/nixos is not writable+- #222 Human friendly exception reporting+- #212 Use C++ to determine closure, avoids shell argument limit size+ ## [0.2.1] - 2019-07-05 ### Added@@ -94,7 +109,9 @@ - Initial release @domenkozar -[Unreleased]: https://github.com/cachix/cachix/compare/v0.1.4...HEAD+[Unreleased]: https://github.com/cachix/cachix/compare/v0.3.0...HEAD+[0.3.0]: https://github.com/cachix/cachix/compare/v0.2.1...v0.3.0+[0.2.1]: https://github.com/cachix/cachix/compare/v0.2.0...v0.2.1 [0.2.0]: https://github.com/cachix/cachix/compare/v0.1.3...v0.2.0 [0.1.3]: https://github.com/cachix/cachix/compare/v0.1.2...v0.1.3 [0.1.2]: https://github.com/cachix/cachix/compare/v0.1.1...v0.1.2
Setup.hs view
@@ -1,2 +1,3 @@ import Distribution.Simple+ main = defaultMain
cachix.cabal view
@@ -1,62 +1,76 @@-cabal-version: 2.2-name: cachix-version: 0.2.1-synopsis: Command line client for Nix binary cache hosting https://cachix.org-homepage: https://github.com/cachix/cachix#readme-bug-reports: https://github.com/cachix/cachix/issues-author: Domen Kožar-maintainer: domen@enlambda.com-copyright: 2018 Domen Kožar-license: Apache-2.0-license-file: LICENSE-build-type: Simple-data-files: *.input, *.output-data-dir: test/data+cabal-version: 2.2+name: cachix+version: 0.3.0+synopsis:+ Command line client for Nix binary cache hosting https://cachix.org++homepage: https://github.com/cachix/cachix#readme+bug-reports: https://github.com/cachix/cachix/issues+author: Domen Kožar+maintainer: domen@enlambda.com+copyright: 2018 Domen Kožar+license: Apache-2.0+license-file: LICENSE+build-type: Simple extra-source-files:- CHANGELOG.md- README.md+ CHANGELOG.md+ README.md+ cbits/aliases.h+ test/data/*.input+ test/data/*.output common defaults- default-extensions: OverloadedStrings NoImplicitPrelude RecordWildCards DeriveGeneric- ghc-options: -Wall -Wcompat -Wincomplete-record-updates -Wincomplete-uni-patterns -Wredundant-constraints -fwarn-tabs -fwarn-unused-imports -fwarn-missing-signatures -fwarn-name-shadowing -fwarn-incomplete-patterns- default-language: Haskell2010+ default-extensions:+ NoImplicitPrelude+ DeriveGeneric+ OverloadedStrings+ RecordWildCards + ghc-options:+ -Wall -Wcompat -Wincomplete-record-updates+ -Wincomplete-uni-patterns -Wredundant-constraints -fwarn-tabs+ -fwarn-unused-imports -fwarn-missing-signatures+ -fwarn-name-shadowing -fwarn-incomplete-patterns++ default-language: Haskell2010+ source-repository head- type: git+ type: git location: https://github.com/cachix/cachix library- import: defaults+ import: defaults exposed-modules:- Cachix.Client- Cachix.Client.Commands- Cachix.Client.Config- Cachix.Client.Env- Cachix.Client.Exception- Cachix.Client.InstallationMode- Cachix.Client.NetRc- Cachix.Client.NixConf- Cachix.Client.NixVersion- Cachix.Client.OptionsParser- Cachix.Client.Push- Cachix.Client.Secrets- Cachix.Client.Servant- Cachix.Client.URI- other-modules:- Paths_cachix- autogen-modules:- Paths_cachix- hs-source-dirs:- src+ Cachix.Client+ Cachix.Client.Commands+ Cachix.Client.Config+ Cachix.Client.Config.Orphans+ Cachix.Client.Env+ Cachix.Client.Exception+ Cachix.Client.InstallationMode+ Cachix.Client.NetRc+ Cachix.Client.NixConf+ Cachix.Client.NixVersion+ Cachix.Client.OptionsParser+ Cachix.Client.Push+ Cachix.Client.Secrets+ Cachix.Client.Servant+ Cachix.Client.Store+ Cachix.Client.Store.Context+ Cachix.Client.URI++ other-modules: Paths_cachix+ autogen-modules: Paths_cachix+ hs-source-dirs: src build-depends:- async- , base >=4.7 && <5- , base16-bytestring+ , async+ , base >=4.7 && <5 , base64-bytestring , bytestring , cachix-api- , conduit >=1.3.0+ , conduit >=1.3.0 , conduit-extra+ , containers , cookie , cryptonite , dhall@@ -65,12 +79,15 @@ , filepath , fsnotify , here+ , hnix-store-core , http-client , http-client-tls , http-conduit , http-types+ , inline-c+ , inline-c-cpp , lzma-conduit- , megaparsec >= 7.0.0+ , megaparsec >=7.0.0 , memory , mmorph , netrc@@ -80,53 +97,62 @@ , resourcet , retry , safe-exceptions- , servant >=0.15+ , servant >=0.15 , servant-auth- , servant-auth-client >=0.3.3.0- , servant-client >=0.15- , servant-client-core >=0.15+ , servant-auth-client >=0.3.3.0+ , servant-client >=0.15+ , servant-client-core >=0.15 , servant-conduit , text , unix , uri-bytestring , versions + ghc-options: -optc=-std=c++14++ -- match what Nix is using+ if os(osx)+ ghc-options: -pgmc=clang++++ cc-options: -Wall+ include-dirs: cbits+ extra-libraries: stdc+++ pkgconfig-depends: nix-store ==2.0 || >2.0, nix-main ==2.0 || >2.0+ executable cachix- import: defaults- main-is: Main.hs- other-modules:- Paths_cachix- autogen-modules:- Paths_cachix- hs-source-dirs:- cachix- ghc-options: -threaded -rtsopts -with-rtsopts=-N+ import: defaults+ main-is: Main.hs+ other-modules: Paths_cachix+ autogen-modules: Paths_cachix+ hs-source-dirs: cachix+ ghc-options: -threaded -rtsopts -with-rtsopts=-N build-depends:- base >=4.7 && <5+ , base >=4.7 && <5 , cachix , cachix-api- build-tool-depends: hspec-discover:hspec-discover + build-tool-depends: hspec-discover:hspec-discover -any+ test-suite cachix-test- import: defaults- type: exitcode-stdio-1.0- main-is: Main.hs+ import: defaults+ type: exitcode-stdio-1.0+ main-is: Main.hs other-modules:- InstallationModeSpec- NetRcSpec- NixConfSpec- NixVersionSpec- Spec- Paths_cachix- hs-source-dirs:- test- ghc-options: -threaded -rtsopts -with-rtsopts=-N+ InstallationModeSpec+ NetRcSpec+ NixConfSpec+ NixVersionSpec+ Paths_cachix+ Spec++ hs-source-dirs: test+ ghc-options: -threaded -rtsopts -with-rtsopts=-N build-depends:- base >=4.7 && <5+ , base >=4.7 && <5 , cachix , cachix-api , directory+ , here , hspec , protolude- , here , temporary
cachix/Main.hs view
@@ -1,13 +1,11 @@ module Main (main) where -import Prelude-import Control.Exception (handle, SomeException, fromException)-import System.IO-import System.Exit (exitFailure, exitWith)-import GHC.IO.Encoding- import qualified Cachix.Client as CC-+import Cachix.Client.Exception (CachixException)+import Control.Exception (displayException, handle)+import GHC.IO.Encoding+import System.Exit (exitFailure)+import System.IO main :: IO () main = do@@ -19,10 +17,8 @@ handleExceptions :: IO a -> IO a handleExceptions = handle handler where- handler :: SomeException -> IO a- handler e | (Just ee) <- fromException e = exitWith ee+ handler :: CachixException -> IO a handler e = do hPutStrLn stderr ""- -- TODO: pretty print the record once fixed https://github.com/haskell-servant/servant/issues/807- hPrint stderr e+ hPutStr stderr (displayException e) exitFailure
+ cbits/aliases.h view
@@ -0,0 +1,8 @@+#pragma once++// inline-c-cpp doesn't seem to handle namespace operator or template+// syntax so we help it a bit for now. This definition can be inlined+// when it is supported by inline-c-cpp.+typedef nix::ref<nix::Store> refStore;+typedef nix::ref<const nix::ValidPathInfo> refValidPathInfo;+typedef nix::PathSet::iterator PathSetIterator;
src/Cachix/Client.hs view
@@ -1,13 +1,12 @@ module Cachix.Client ( main- ) where+ )+where +import Cachix.Client.Commands as Commands+import Cachix.Client.Env (cachixVersion, mkEnv)+import Cachix.Client.OptionsParser (CachixCommand (..), getOpts) import Protolude--import Cachix.Client.OptionsParser ( CachixCommand(..), getOpts )-import Cachix.Client.Commands as Commands-import Cachix.Client.Env ( mkEnv, cachixVersion )- main :: IO () main = do
src/Cachix/Client/Commands.hs view
@@ -1,95 +1,100 @@-{-# LANGUAGE QuasiQuotes #-}-{-# LANGUAGE CPP #-} {-# LANGUAGE DataKinds #-}-{-# LANGUAGE TypeOperators #-}+{-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE ScopedTypeVariables #-}-module Cachix.Client.Commands- ( authtoken- , create- , generateKeypair- , push- , use- ) where+{-# LANGUAGE TypeOperators #-} -import Crypto.Sign.Ed25519-import Control.Concurrent (threadDelay)-import Control.Exception.Safe (throwM)-import Control.Retry (RetryStatus(rsIterNumber))-import qualified Data.ByteString.Base64 as B64-import Data.List ( isSuffixOf )-import Data.Maybe ( fromJust )-import Data.String.Here-import qualified Data.Text as T-import Network.HTTP.Types (status404, status401)-import Protolude-import Servant.Auth.Client-import Servant.API ( NoContent )-import Servant.Client.Streaming-import Servant.Conduit ()-import System.Directory ( doesFileExist )-import System.FSNotify-import System.IO ( stdin, hIsTerminalDevice )-import System.Environment ( lookupEnv )+module Cachix.Client.Commands+ ( authtoken,+ create,+ generateKeypair,+ push,+ use+ )+where -import qualified Cachix.Api as Api-import Cachix.Api.Error+import qualified Cachix.Api as Api+import Cachix.Api.Error+import Cachix.Client.Config+ ( BinaryCacheConfig (..),+ Config (..),+ mkConfig,+ writeConfig+ )+import qualified Cachix.Client.Config as Config+import Cachix.Client.Env (Env (..))+import Cachix.Client.Exception (CachixException (..))+import Cachix.Client.InstallationMode+import qualified Cachix.Client.NixConf as NixConf+import Cachix.Client.NixVersion (assertNixVersion)+import Cachix.Client.OptionsParser+ ( CachixOptions (..),+ PushArguments (..),+ PushOptions (..)+ )+import Cachix.Client.Push+import Cachix.Client.Secrets+ ( SigningKey (SigningKey),+ exportSigningKey,+ parseSigningKeyLenient+ )+import Cachix.Client.Servant import qualified Cachix.Types.SigningKeyCreate as SigningKeyCreate-import Cachix.Client.Config ( Config(..)- , BinaryCacheConfig(..)- , writeConfig- , mkConfig- )-import qualified Cachix.Client.Config as Config-import Cachix.Client.Env ( Env(..) )-import Cachix.Client.Exception ( CachixException(..) )-import Cachix.Client.OptionsParser ( CachixOptions(..), UseOptions(..)- , PushArguments(..), PushOptions(..)- )-import Cachix.Client.InstallationMode-import Cachix.Client.NixVersion ( getNixVersion )-import qualified Cachix.Client.NixConf as NixConf-import Cachix.Client.Push-import Cachix.Client.Secrets ( SigningKey(SigningKey)- , parseSigningKeyLenient- , exportSigningKey- )-import Cachix.Client.Servant-+import Control.Concurrent (threadDelay)+import Control.Exception.Safe (throwM)+import Control.Retry (RetryStatus (rsIterNumber))+import Crypto.Sign.Ed25519+import qualified Data.ByteString.Base64 as B64+import Data.List (isSuffixOf)+import Data.Maybe (fromJust)+import Data.String.Here+import qualified Data.Text as T+import Network.HTTP.Types (status401, status404)+import Protolude+import Servant.API (NoContent)+import Servant.Auth.Client+import Servant.Client.Streaming+import Servant.Conduit ()+import System.Directory (doesFileExist)+import System.Environment (lookupEnv)+import System.FSNotify+import System.IO (hIsTerminalDevice, stdin) authtoken :: Env -> Text -> IO () authtoken env token = do -- TODO: check that token actually authenticates! writeConfig (configPath (cachixoptions env)) $ case config env of- Just config -> config { authToken = Token (toS token) }+ Just config -> config {authToken = Token (toS token)} Nothing -> mkConfig token- putStrLn ([hereLit|+ putStrLn+ ( [hereLit| Continue by creating a binary cache at https://cachix.org- |] :: Text)+ |]+ :: Text+ ) create :: Env -> Text -> IO () create _ _ = throwIO $ DeprecatedCommand "Create command has been deprecated. Please visit https://cachix.org to create a binary cache." generateKeypair :: Env -> Text -> IO ()-generateKeypair Env { config = Nothing } _ = throwIO $ NoConfig "Start with visiting https://cachix.org and copying the token to $ cachix authtoken <token>"-generateKeypair env@Env { config = Just config } name = do+generateKeypair Env {config = Nothing} _ = throwIO $ NoConfig "Start with visiting https://cachix.org and copying the token to $ cachix authtoken <token>"+generateKeypair env@Env {config = Just config} name = do (PublicKey pk, sk) <- createKeypair- let signingKey = exportSigningKey $ SigningKey sk signingKeyCreate = SigningKeyCreate.SigningKeyCreate (toS $ B64.encode pk) bcc = BinaryCacheConfig name signingKey- -- we first validate if key can be added to the binary cache- (_ :: NoContent) <- escalate- =<< ((`runClientM` clientenv env)- $ Api.createKey (cachixBCClient name) (authToken config) signingKeyCreate)-+ (_ :: NoContent) <-+ escalate+ =<< ( (`runClientM` clientenv env)+ $ Api.createKey (cachixBCClient name) (authToken config) signingKeyCreate+ ) -- if key was successfully added, write it to the config -- TODO: this breaks if more than one key is added, see #27- writeConfig (configPath (cachixoptions env)) $- config { binaryCaches = binaryCaches config <> [bcc] }-- putStrLn ([iTrim|+ writeConfig (configPath (cachixoptions env))+ $ config {binaryCaches = binaryCaches config <> [bcc]}+ putStrLn+ ( [iTrim| Secret signing key has been saved in the file above. To populate your binary cache: @@ -105,7 +110,9 @@ $ cachix use ${name} IMPORTANT: Make sure to make a backup for the signing key above, as you have the only copy.- |] :: Text)+ |]+ :: Text+ ) envToToken :: Env -> Token envToToken env =@@ -124,27 +131,24 @@ -- 1. get cache public key res <- (`runClientM` clientenv env) $ Api.get (cachixBCClient name) (envToToken env) case res of- Left err | isErr err status401 && isJust (config env) -> throwM $ accessDeniedBinaryCache name- | isErr err status401 -> throwM notAuthenticatedBinaryCache- | isErr err status404 -> throwM $ BinaryCacheNotFound $ "Binary cache" <> name <> " does not exist."- | otherwise -> throwM err+ Left err+ | isErr err status401 && isJust (config env) -> throwM $ accessDeniedBinaryCache name+ | isErr err status401 -> throwM notAuthenticatedBinaryCache+ | isErr err status404 -> throwM $ BinaryCacheNotFound $ "Binary cache" <> name <> " does not exist."+ | otherwise -> throwM err Right binaryCache -> do- nixVersion <- escalateAs UnsupportedNixVersion =<< getNixVersion+ () <- escalateAs UnsupportedNixVersion =<< assertNixVersion user <- getUser nc <- NixConf.read NixConf.Global isTrusted <- isTrustedUser $ NixConf.readLines (catMaybes [nc]) NixConf.isTrustedUsers isNixOS <- doesFileExist "/etc/NIXOS" let nixEnv = NixEnv- { nixVersion = nixVersion- , isRoot = user == "root"- , isTrusted = isTrusted- , isNixOS = isNixOS- }- addBinaryCache (config env) binaryCache useOptions $- if useNixOS useOptions- then EchoNixOS nixVersion- else getInstallationMode nixEnv-+ { isRoot = user == "root",+ isTrusted = isTrusted,+ isNixOS = isNixOS+ }+ addBinaryCache (config env) binaryCache useOptions+ $ fromMaybe (getInstallationMode nixEnv) (useMode useOptions) -- TODO: lots of room for perfomance improvements push :: Env -> PushArguments -> IO ()@@ -153,97 +157,88 @@ when (not hasNoStdin && not (null rawPaths)) $ throwIO $ AmbiguousInput "You provided both stdin and store path arguments, pick only one to proceed." inputStorePaths <- if hasNoStdin- then return rawPaths- else T.words <$> getContents-+ then return rawPaths+ else T.words <$> getContents -- TODO: if empty, take whole nix store and warn: nix store-path --all when (null inputStorePaths) $ throwIO $ NoInput "You need to specify store paths either as stdin or as a cli argument" sk <- findSigningKey env name-- void $ pushClosure- (mapConcurrentlyBounded 4)- (clientenv env)- PushCache- { pushCacheToken = envToToken env- , pushCacheName = name- , pushCacheSigningKey = sk- }- (pushStrategy env opts name)- inputStorePaths+ store <- wait (storeAsync env)+ void+ $ pushClosure+ (mapConcurrentlyBounded 4)+ (clientenv env)+ store PushCache+ { pushCacheToken = envToToken env,+ pushCacheName = name,+ pushCacheSigningKey = sk+ }+ (pushStrategy env opts name)+ inputStorePaths putText "All done."- push env (PushWatchStore opts name) = withManager $ \mgr -> do _ <- watchDir mgr "/nix/store" filterF action+ _ <- wait (storeAsync env) putText "Watching /nix/store for new builds ..." forever $ threadDelay 1000000 where action :: Action-#if MIN_VERSION_fsnotify(0,3,0) action (Removed fp _ _) =-#else- action (Removed fp _) =-#endif pushStorePath env opts name $ toS $ dropEnd 5 fp action _ = return ()- filterF :: ActionPredicate-#if MIN_VERSION_fsnotify(0,3,0) filterF (Removed fp _ _)-#else- filterF (Removed fp _)-#endif | ".lock" `isSuffixOf` fp = True filterF _ = False- dropEnd :: Int -> [a] -> [a] dropEnd index xs = take (length xs - index) xs -- | Find a secret key in the 'Config' or environment variable findSigningKey :: Env- -> Text -- ^ Cache name- -> IO SigningKey -- ^ Secret key or exception+ -> Text -- ^ Cache name+ -> IO SigningKey -- ^ Secret key or exception findSigningKey env name = do maybeEnvSK <- lookupEnv "CACHIX_SIGNING_KEY"- let matches Config{..} = filter (\bc -> Config.name bc == name) binaryCaches+ let matches Config {..} = filter (\bc -> Config.name bc == name) binaryCaches maybeBCSK = case config env of Nothing -> Nothing Just c -> Config.secretKey <$> head (matches c)- -- TODO: better error msg+ -- TODO: better error msg escalateAs FatalError $ parseSigningKeyLenient $ fromJust $ maybeBCSK <|> toS <$> maybeEnvSK <|> panic "You need to: export CACHIX_SIGNING_KEY=XXX" retryText :: RetryStatus -> Text retryText retrystatus = if rsIterNumber retrystatus == 0- then ""- else "(retry #" <> show (rsIterNumber retrystatus) <> ") "+ then ""+ else "(retry #" <> show (rsIterNumber retrystatus) <> ") " pushStrategy :: Env -> PushOptions -> Text -> Text -> PushStrategy IO () pushStrategy env opts name storePath = PushStrategy- { onAlreadyPresent = pass- , on401 = if isJust (config env)- then throwM $ accessDeniedBinaryCache name- else throwM notAuthenticatedBinaryCache- , onError = throwM- , onAttempt = \retrystatus -> + { onAlreadyPresent = pass,+ on401 =+ if isJust (config env)+ then throwM $ accessDeniedBinaryCache name+ else throwM notAuthenticatedBinaryCache,+ onError = throwM,+ onAttempt = \retrystatus -> -- we append newline instead of putStrLn due to https://github.com/haskell/text/issues/242- putStr $ "pushing " <> retryText retrystatus <> storePath <> "\n"- , onDone = pass- , withXzipCompressor = defaultWithXzipCompressorWithLevel (compressionLevel opts)- }+ putStr $ "pushing " <> retryText retrystatus <> storePath <> "\n",+ onDone = pass,+ withXzipCompressor = defaultWithXzipCompressorWithLevel (compressionLevel opts)+ } pushStorePath :: Env -> PushOptions -> Text -> Text -> IO () pushStorePath env opts name storePath = do sk <- findSigningKey env name -- use secret key from config or env -- TODO: this shouldn't happen for each store path-+ store <- wait (storeAsync env) pushSingleStorePath (clientenv env)- PushCache- { pushCacheToken = envToToken env- , pushCacheName = name- , pushCacheSigningKey = sk+ store PushCache+ { pushCacheToken = envToToken env,+ pushCacheName = name,+ pushCacheSigningKey = sk } (pushStrategy env opts name storePath) storePath
src/Cachix/Client/Config.hs view
@@ -1,55 +1,55 @@ {-# LANGUAGE DeriveAnyClass #-}+ module Cachix.Client.Config- ( Config(..)- , BinaryCacheConfig(..)- , readConfig- , writeConfig- , getDefaultFilename- , ConfigPath- , mkConfig- ) where+ ( Config (..),+ BinaryCacheConfig (..),+ readConfig,+ writeConfig,+ getDefaultFilename,+ ConfigPath,+ mkConfig+ )+where -import Dhall hiding ( Text )-import Dhall.Pretty ( prettyExpr )-import Dhall.Core (Expr(..), Chunks(..))-import GHC.Generics ( Generic )-import Servant.Auth.Client-import System.Directory ( doesFileExist, createDirectoryIfMissing- , getXdgDirectory, XdgDirectory(..)- )-import System.Posix.Files ( setFileMode, unionFileModes, ownerReadMode- , ownerWriteMode )-import System.FilePath.Posix ( takeDirectory )+import Cachix.Client.Config.Orphans ()+import Dhall hiding (Text)+import Dhall.Pretty (prettyExpr)+import GHC.Generics (Generic) import Protolude---data BinaryCacheConfig = BinaryCacheConfig- { name :: Text- , secretKey :: Text- } deriving (Show, Generic, Interpret, Inject)--data Config = Config- { authToken :: Token- , binaryCaches :: [BinaryCacheConfig]- } deriving (Show, Generic, Interpret, Inject)+import Servant.Auth.Client+import System.Directory+ ( XdgDirectory (..),+ createDirectoryIfMissing,+ doesFileExist,+ getXdgDirectory+ )+import System.FilePath.Posix (takeDirectory)+import System.Posix.Files+ ( ownerReadMode,+ ownerWriteMode,+ setFileMode,+ unionFileModes+ ) -instance Interpret Token where- autoWith _ = Type- { extract = \(TextLit (Chunks [] t)) -> pure (Token (toS t))- , expected = Text- }+data BinaryCacheConfig+ = BinaryCacheConfig+ { name :: Text,+ secretKey :: Text+ }+ deriving (Show, Generic, Interpret, Inject) -instance Inject Token where- injectWith _ = InputType- { embed = TextLit . Chunks [] . toS . getToken- , declared = Text- }+data Config+ = Config+ { authToken :: Token,+ binaryCaches :: [BinaryCacheConfig]+ }+ deriving (Show, Generic, Interpret, Inject) mkConfig :: Text -> Config mkConfig token = Config- { authToken = Token (toS token)- , binaryCaches = []- }+ { authToken = Token (toS token),+ binaryCaches = []+ } type ConfigPath = FilePath @@ -57,8 +57,8 @@ readConfig filename = do doesExist <- doesFileExist filename if doesExist- then Just <$> input auto (toS filename)- else return Nothing+ then Just <$> input auto (toS filename)+ else return Nothing getDefaultFilename :: IO FilePath getDefaultFilename = do
+ src/Cachix/Client/Config/Orphans.hs view
@@ -0,0 +1,28 @@+{-# OPTIONS_GHC -Wno-orphans #-}++module Cachix.Client.Config.Orphans+ (+ )+where++import Dhall hiding (Text)+import Dhall.Core (Chunks (..), Expr (..))+import Protolude+import Servant.Auth.Client++instance Interpret Token where++ autoWith _ = Type+ { extract = ex,+ expected = Text+ }+ where+ ex (TextLit (Chunks [] t)) = pure (Token (toS t))+ ex _ = panic "Unexpected Dhall value. Did it typecheck?"++instance Inject Token where++ injectWith _ = InputType+ { embed = TextLit . Chunks [] . toS . getToken,+ declared = Text+ }
src/Cachix/Client/Env.hs view
@@ -1,53 +1,60 @@ module Cachix.Client.Env- ( Env(..)- , mkEnv- , cachixVersion- ) where+ ( Env (..),+ mkEnv,+ cachixVersion+ )+where +import Cachix.Client.Config (Config, readConfig)+import Cachix.Client.OptionsParser (CachixOptions (..))+import Cachix.Client.Store (Store, openStore)+import Cachix.Client.URI (getBaseUrl)+import Control.Concurrent.Async (Async, async)+import Data.Version (showVersion)+import Network.HTTP.Client+ ( ManagerSettings,+ managerModifyRequest,+ managerResponseTimeout,+ responseTimeoutNone+ )+import Network.HTTP.Client.TLS (newTlsManagerWith, tlsManagerSettings)+import Network.HTTP.Simple (setRequestHeader)+import Paths_cachix (version) import Protolude--import Data.Version (showVersion)-import Paths_cachix (version)--import Network.HTTP.Simple ( setRequestHeader )-import Network.HTTP.Client.TLS ( newTlsManagerWith, tlsManagerSettings )-import Network.HTTP.Client ( managerResponseTimeout, managerModifyRequest- , responseTimeoutNone, ManagerSettings)-import Servant.Client ( mkClientEnv, ClientEnv )-import System.Directory ( canonicalizePath )--import Cachix.Client.Config ( readConfig, Config )-import Cachix.Client.OptionsParser ( CachixOptions(..) )-import Cachix.Client.URI ( getBaseUrl )---data Env = Env- { config :: Maybe Config- , clientenv :: ClientEnv- , cachixoptions :: CachixOptions- }+import Servant.Client (ClientEnv, mkClientEnv)+import System.Directory (canonicalizePath) +data Env+ = Env+ { config :: Maybe Config,+ clientenv :: ClientEnv,+ cachixoptions :: CachixOptions,+ storeAsync :: Async Store+ } mkEnv :: CachixOptions -> IO Env mkEnv rawcachixoptions = do+ store <- async openStore -- make sure path to the config is passed as absolute to dhall logic canonicalConfigPath <- canonicalizePath (configPath rawcachixoptions)- let cachixoptions = rawcachixoptions { configPath = canonicalConfigPath }+ let cachixoptions = rawcachixoptions {configPath = canonicalConfigPath} config <- readConfig $ configPath cachixoptions manager <- newTlsManagerWith customManagerSettings let clientenv = mkClientEnv manager $ getBaseUrl (host cachixoptions) return Env- { config = config- , clientenv = clientenv- , cachixoptions = cachixoptions- }+ { config = config,+ clientenv = clientenv,+ cachixoptions = cachixoptions,+ storeAsync = store+ } customManagerSettings :: ManagerSettings-customManagerSettings = tlsManagerSettings- { managerResponseTimeout = responseTimeoutNone- -- managerModifyRequest :: Request -> IO Request- , managerModifyRequest = return . setRequestHeader "User-Agent" [toS cachixVersion]- }+customManagerSettings =+ tlsManagerSettings+ { managerResponseTimeout = responseTimeoutNone,+ -- managerModifyRequest :: Request -> IO Request+ managerModifyRequest = return . setRequestHeader "User-Agent" [toS cachixVersion]+ } cachixVersion :: Text cachixVersion = "cachix " <> toS (showVersion version)
src/Cachix/Client/Exception.hs view
@@ -1,4 +1,4 @@-module Cachix.Client.Exception (CachixException(..)) where+module Cachix.Client.Exception (CachixException (..)) where import Protolude @@ -17,4 +17,7 @@ | BinaryCacheNotFound Text deriving (Show, Typeable) -instance Exception CachixException+instance Exception CachixException where++ displayException (NixOSInstructions txt) = toS txt+ displayException e = show e
src/Cachix/Client/InstallationMode.hs view
@@ -1,93 +1,116 @@ {-# LANGUAGE QuasiQuotes #-}+ module Cachix.Client.InstallationMode- ( InstallationMode(..)- , NixEnv(..)- , getInstallationMode- , addBinaryCache- , isTrustedUser- , getUser- ) where+ ( InstallationMode (..),+ NixEnv (..),+ getInstallationMode,+ addBinaryCache,+ isTrustedUser,+ getUser,+ fromString,+ toString,+ UseOptions (..)+ )+where -import Protolude-import Data.String.Here-import qualified Data.Text as T-import Cachix.Client.Config (Config)-import Cachix.Client.Exception (CachixException(..))+import Cachix.Api as Api+import Cachix.Client.Config (Config)+import Cachix.Client.Exception (CachixException (..)) import qualified Cachix.Client.NetRc as NetRc import qualified Cachix.Client.NixConf as NixConf-import Cachix.Client.NixVersion ( NixVersion(..) )-import Cachix.Client.OptionsParser ( UseOptions(..) )-import Cachix.Api as Api-import System.Directory ( getPermissions, writable, createDirectoryIfMissing )-import System.Environment ( lookupEnv )-import System.FilePath ( replaceFileName, (</>) )+import Data.String.Here+import qualified Data.Text as T+import Protolude+import System.Directory (Permissions, createDirectoryIfMissing, getPermissions, writable)+import System.Environment (lookupEnv)+import System.FilePath ((</>), replaceFileName)+import Prelude (String) -data NixEnv = NixEnv- { nixVersion :: NixVersion- , isTrusted :: Bool- , isRoot :: Bool- , isNixOS :: Bool- }+data NixEnv+ = NixEnv+ { isTrusted :: Bool,+ isRoot :: Bool,+ isNixOS :: Bool+ } +-- NOTE: update the list of options for --mode argument in OptionsParser.hs data InstallationMode- = Install NixVersion NixConf.NixConfLoc- | EchoNixOS NixVersion- | EchoNixOSWithTrustedUser NixVersion+ = Install NixConf.NixConfLoc+ | WriteNixOS | UntrustedRequiresSudo- | Nix20RequiresSudo deriving (Show, Eq) +data UseOptions+ = UseOptions+ { useMode :: Maybe InstallationMode,+ useNixOSFolder :: FilePath+ }+ deriving (Show)++fromString :: String -> Maybe InstallationMode+fromString "root-nixconf" = Just $ Install NixConf.Global+fromString "user-nixconf" = Just $ Install NixConf.Local+fromString "nixos" = Just WriteNixOS+fromString "untrusted-requires-sudo" = Just UntrustedRequiresSudo+fromString _ = Nothing++toString :: InstallationMode -> String+toString (Install NixConf.Global) = "root-nixconf"+toString (Install NixConf.Local) = "user-nixconf"+toString WriteNixOS = "nixos"+toString UntrustedRequiresSudo = "untrusted-requires-sudo"+ getInstallationMode :: NixEnv -> InstallationMode-getInstallationMode NixEnv{..}- | isNixOS && (isRoot || nixVersion /= Nix201) = EchoNixOS nixVersion- | isNixOS && not isTrusted = EchoNixOSWithTrustedUser nixVersion- | not isNixOS && isRoot = Install nixVersion NixConf.Global- | nixVersion /= Nix201 = Nix20RequiresSudo- | isTrusted = Install nixVersion NixConf.Local+getInstallationMode NixEnv {..}+ | isNixOS && isRoot = WriteNixOS+ | not isNixOS && isRoot = Install NixConf.Global+ | isTrusted = Install NixConf.Local | otherwise = UntrustedRequiresSudo - -- | Add a Binary cache to nix.conf, print nixos config or fail addBinaryCache :: Maybe Config -> Api.BinaryCache -> UseOptions -> InstallationMode -> IO ()-addBinaryCache _ _ _ UntrustedRequiresSudo = throwIO $- MustBeRoot "Run command as root OR execute: $ echo \"trusted-users = root $USER\" | sudo tee -a /etc/nix/nix.conf && sudo pkill nix-daemon"-addBinaryCache _ _ _ Nix20RequiresSudo = throwIO $- MustBeRoot "Run command as root OR upgrade to latest Nix - to be able to use it without root (recommended)"-addBinaryCache maybeConfig bc useOptions (EchoNixOS _) =- nixosBinaryCache maybeConfig Nothing bc useOptions-addBinaryCache maybeConfig bc useOptions (EchoNixOSWithTrustedUser _) = do- user <- getUser- nixosBinaryCache maybeConfig (Just user) bc useOptions-addBinaryCache maybeConfig bc@Api.BinaryCache{..} _ (Install nixversion ncl) = do+addBinaryCache _ _ _ UntrustedRequiresSudo =+ throwIO+ $ MustBeRoot "Run command as root OR execute: $ echo \"trusted-users = root $USER\" | sudo tee -a /etc/nix/nix.conf && sudo pkill nix-daemon"+addBinaryCache maybeConfig bc useOptions WriteNixOS =+ nixosBinaryCache maybeConfig bc useOptions+addBinaryCache maybeConfig bc@Api.BinaryCache {..} _ (Install ncl) = do -- TODO: might need locking one day gnc <- NixConf.read NixConf.Global- (input, output) <- case ncl of- NixConf.Global -> return ([gnc], gnc)- NixConf.Local -> do- lnc <- NixConf.read NixConf.Local- return ([gnc, lnc], lnc)+ (input, output) <-+ case ncl of+ NixConf.Global -> return ([gnc], gnc)+ NixConf.Local -> do+ lnc <- NixConf.read NixConf.Local+ return ([gnc, lnc], lnc) let nixconf = fromMaybe (NixConf.NixConf []) output- netrcLocMaybe <- forM (guard $ not isPublic) $ const $ addPrivateBinaryCacheNetRC maybeConfig bc ncl- let- addNetRCLine :: NixConf.NixConf -> NixConf.NixConf- addNetRCLine = fromMaybe identity $ do+ netrcLocMaybe <- forM (guard $ not isPublic) $ const $ addPrivateBinaryCacheNetRC maybeConfig bc ncl+ let addNetRCLine :: NixConf.NixConf -> NixConf.NixConf+ 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 nixversion ncl $ addNetRCLine $ NixConf.add bc (catMaybes input) nixconf+ NixConf.write ncl $ addNetRCLine $ NixConf.add bc (catMaybes input) nixconf filename <- NixConf.getFilename ncl putStrLn $ "Configured " <> uri <> " binary cache in " <> toS filename -nixosBinaryCache :: Maybe Config -> Maybe Text -> Api.BinaryCache -> UseOptions -> IO ()-nixosBinaryCache maybeConfig maybeUser bc@Api.BinaryCache{..} UseOptions { useNixOSFolder=baseDirectory } = do- createDirectoryIfMissing True $ toS toplevel- writeFile (toS glueModuleFile) glueModule- writeFile (toS cacheModuleFile) cacheModule- unless isPublic $ void $ addPrivateBinaryCacheNetRC maybeConfig bc NixConf.Global- putText instructions+nixosBinaryCache :: Maybe Config -> Api.BinaryCache -> UseOptions -> IO ()+nixosBinaryCache maybeConfig bc@Api.BinaryCache {..} UseOptions {useNixOSFolder = baseDirectory} = do+ eitherPermissions <- try $ getPermissions (toS toplevel) :: IO (Either SomeException Permissions)+ case eitherPermissions of+ Left _ -> throwIO $ NixOSInstructions noEtcPermissionInstructions+ Right permissions+ | writable permissions -> installFiles+ | otherwise -> throwIO $ NixOSInstructions noEtcPermissionInstructions where+ installFiles = do+ createDirectoryIfMissing True $ toS toplevel+ writeFile (toS glueModuleFile) glueModule+ writeFile (toS cacheModuleFile) cacheModule+ unless isPublic $ void $ addPrivateBinaryCacheNetRC maybeConfig bc NixConf.Global+ putText instructions configurationNix :: Text configurationNix = toS $ toS baseDirectory </> "configuration.nix" namespace :: Text@@ -98,12 +121,16 @@ glueModuleFile = toplevel <> ".nix" cacheModuleFile :: Text cacheModuleFile = toplevel <> "/" <> toS name <> ".nix"-- extra :: Text- extra = maybe "" (\user -> [i|trustedUsers = [ "root" "${user}" ];|]) maybeUser+ noEtcPermissionInstructions :: Text+ noEtcPermissionInstructions =+ [iTrim|+Could not install NixOS configuration to /etc/nixos/ due to lack of write permissions. +Pass `--nixos-folder /etc/mynixos/` as an alternative location with write permissions.+ |] instructions :: Text- instructions = [iTrim|+ instructions =+ [iTrim| Cachix configuration written to ${glueModuleFile}. Binary cache ${name} configuration written to ${cacheModuleFile}. @@ -115,9 +142,9 @@ $ nixos-rebuild switch |]- glueModule :: Text- glueModule = [i|+ glueModule =+ [i| # WARN: this file will get overwritten by $ cachix use <name> { pkgs, lib, ... }: @@ -131,9 +158,9 @@ nix.binaryCaches = ["https://cache.nixos.org/"]; } |]- cacheModule :: Text- cacheModule = [i|+ cacheModule =+ [i| { nix = { binaryCaches = [@@ -142,7 +169,6 @@ binaryCachePublicKeys = [ ${T.intercalate " " (map (\s -> "\"" <> s <> "\"") publicSigningKeys)} ];- ${extra} }; } |]
src/Cachix/Client/NetRc.hs view
@@ -1,60 +1,57 @@ -- Deals with adding private caches to netrc module Cachix.Client.NetRc ( add- ) where--import qualified Data.ByteString as BS-import Data.List (nubBy)-import qualified Data.Text as T-import Network.NetRc-import Protolude-import System.FilePath ( takeDirectory )-import System.Directory ( doesFileExist, createDirectoryIfMissing )-import Servant.Auth.Client (getToken)+ )+where import qualified Cachix.Api as Api-import Cachix.Api.Error (escalateAs)-import Cachix.Client.Config (Config, authToken)-import Cachix.Client.Exception (CachixException(NetRcParseError))-+import Cachix.Api.Error (escalateAs)+import Cachix.Client.Config (Config, authToken)+import Cachix.Client.Exception (CachixException (NetRcParseError))+import qualified Data.ByteString as BS+import Data.List (nubBy)+import qualified Data.Text as T+import Network.NetRc+import Protolude+import Servant.Auth.Client (getToken)+import System.Directory (createDirectoryIfMissing, doesFileExist)+import System.FilePath (takeDirectory) -- | Add a list of binary caches to netrc under `filename`. -- Makes sure there are no duplicate entries (using domain as a key). -- If file under filename doesn't exist it's created.-add :: Config- -> [Api.BinaryCache]- -> FilePath- -> IO ()+add+ :: Config+ -> [Api.BinaryCache]+ -> FilePath+ -> IO () add config binarycaches filename = do doesExist <- doesFileExist filename- netrc <- if doesExist- then BS.readFile filename >>= parse- else return $ NetRc [] []+ netrc <-+ if doesExist+ then BS.readFile filename >>= parse+ else return $ NetRc [] [] createDirectoryIfMissing True (takeDirectory filename) BS.writeFile filename $ netRcToByteString $ uniqueAppend netrc where parse :: ByteString -> IO NetRc parse contents = escalateAs (NetRcParseError . show) $ parseNetRc filename contents- -- O(n^2) but who cares? uniqueAppend :: NetRc -> NetRc uniqueAppend (NetRc hosts macdefs) =- let- f :: NetRcHost -> NetRcHost -> Bool- f x y = nrhName x == nrhName y- in NetRc (nubBy f (new ++ hosts)) macdefs-+ let f :: NetRcHost -> NetRcHost -> Bool+ f x y = nrhName x == nrhName y+ in NetRc (nubBy f (new ++ hosts)) macdefs new :: [NetRcHost] new = map mkHost $ filter (not . Api.isPublic) binarycaches- mkHost :: Api.BinaryCache -> NetRcHost mkHost bc = NetRcHost- { nrhName = toS $ stripPrefix "http://" $ stripPrefix "https://" (Api.uri bc)- , nrhLogin = ""- , nrhPassword = getToken (authToken config)- , nrhAccount = ""- , nrhMacros = []- }+ { nrhName = toS $ stripPrefix "http://" $ stripPrefix "https://" (Api.uri bc),+ nrhLogin = "",+ nrhPassword = getToken (authToken config),+ nrhAccount = "",+ nrhMacros = []+ } where -- stripPrefix that either strips or returns the same string stripPrefix :: Text -> Text -> Text
src/Cachix/Client/NixConf.hs view
@@ -1,3 +1,6 @@+{-# LANGUAGE DeriveFunctor #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+ {- (Very limited) parser, rendered and modifier of nix.conf Supports subset of nix.conf given Nix 2.0 or Nix 1.0@@ -7,45 +10,43 @@ version considers as recommended. -}-{-# LANGUAGE GeneralizedNewtypeDeriving #-}-{-# LANGUAGE DeriveFunctor #-} module Cachix.Client.NixConf- ( NixConf- , NixConfG(..)- , NixConfLine(..)- , NixConfLoc(..)- , render- , add- , read- , update- , write- , getFilename- , parser- , parse- , readLines- , writeLines- , isTrustedUsers- , defaultPublicURI- , defaultSigningKey- , substitutersKey- , trustedPublicKeysKey- , setNetRC- ) where+ ( NixConf,+ NixConfG (..),+ NixConfLine (..),+ NixConfLoc (..),+ render,+ add,+ read,+ update,+ write,+ getFilename,+ parser,+ parse,+ readLines,+ writeLines,+ isTrustedUsers,+ defaultPublicURI,+ defaultSigningKey,+ setNetRC+ )+where +import Cachix.Api (BinaryCache (..)) import Data.Char (isSpace)-import qualified Data.Text as T import Data.List (nub)+import qualified Data.Text as T import Protolude+import System.Directory+ ( XdgDirectory (..),+ createDirectoryIfMissing,+ doesFileExist,+ getXdgDirectory+ )+import System.FilePath.Posix (takeDirectory) import qualified Text.Megaparsec as Mega import Text.Megaparsec.Char-import System.Directory ( doesFileExist, createDirectoryIfMissing- , getXdgDirectory, XdgDirectory(..)- )-import System.FilePath.Posix ( takeDirectory )-import Cachix.Api (BinaryCache(..))-import Cachix.Client.NixVersion (NixVersion(..)) - data NixConfLine = Substituters [Text] | TrustedUsers [Text]@@ -63,7 +64,6 @@ readLines nixconfs predicate = concatMap f nixconfs where f (NixConf xs) = foldl foldIt [] xs- foldIt :: [Text] -> NixConfLine -> [Text] foldIt prev new = prev <> fromMaybe [] (predicate new) @@ -86,9 +86,9 @@ -- | Pure version of addIO add :: BinaryCache -> [NixConf] -> NixConf -> NixConf-add BinaryCache{..} toRead toWrite =- writeLines isPublicKey (TrustedPublicKeys $ nub publicKeys) $- writeLines isSubstituter (Substituters $ nub substituters) toWrite+add BinaryCache {..} 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) <> [uri]@@ -96,99 +96,92 @@ defaultPublicURI :: Text defaultPublicURI = "https://cache.nixos.org"+ defaultSigningKey :: Text defaultSigningKey = "cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY=" -render :: NixVersion -> NixConf -> Text-render nixversion (NixConf nixconflines) = T.unlines $ fmap go nixconflines+render :: NixConf -> Text+render (NixConf nixconflines) = T.unlines $ fmap go nixconflines where go :: NixConfLine -> Text- go (Substituters xs) = substitutersKey nixversion <> " = " <> T.unwords xs+ go (Substituters xs) = "substituters" <> " = " <> T.unwords xs go (TrustedUsers xs) = "trusted-users = " <> T.unwords xs- go (TrustedPublicKeys xs) = trustedPublicKeysKey nixversion <> " = " <> T.unwords xs+ go (TrustedPublicKeys xs) = "trusted-public-keys" <> " = " <> T.unwords xs go (NetRcFile filename) = "netrc-file = " <> filename go (Other line) = line -substitutersKey :: NixVersion -> Text-substitutersKey Nix1XX = "binary-caches"-substitutersKey Nix20 = "substituters"-substitutersKey Nix201 = "substituters"--trustedPublicKeysKey :: NixVersion -> Text-trustedPublicKeysKey Nix1XX = "binary-cache-public-keys"-trustedPublicKeysKey Nix20 = "trusted-public-keys"-trustedPublicKeysKey Nix201 = "trusted-public-keys"--write :: NixVersion -> NixConfLoc -> NixConf -> IO ()-write nixversion ncl nc = do+write :: NixConfLoc -> NixConf -> IO ()+write ncl nc = do filename <- getFilename ncl createDirectoryIfMissing True (takeDirectory filename)- writeFile filename $ render nixversion nc+ writeFile filename $ render nc read :: NixConfLoc -> IO (Maybe NixConf) read ncl = do filename <- getFilename ncl doesExist <- doesFileExist filename if not doesExist- then return Nothing- else do- result <- parse <$> readFile filename- case result of- 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+ then return Nothing+ else+ do+ result <- parse <$> readFile filename+ case result of+ 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 :: NixVersion -> NixConfLoc -> (Maybe NixConf -> NixConf) -> IO ()-update nixversion ncl f = do+update :: NixConfLoc -> (Maybe NixConf -> NixConf) -> IO ()+update ncl f = do nc <- f <$> read ncl- write nixversion ncl nc+ write ncl nc setNetRC :: Text -> NixConf -> NixConf setNetRC netrc (NixConf nc) = NixConf $ filter noNetRc nc ++ [NetRcFile netrc]- where noNetRc (NetRcFile _) = False- noNetRc _ = True+ where+ noNetRc (NetRcFile _) = False+ noNetRc _ = True data NixConfLoc = Global | Local deriving (Show, Eq) getFilename :: NixConfLoc -> IO FilePath getFilename ncl = do- dir <- case ncl of- Global -> return "/etc/nix"- Local -> getXdgDirectory XdgConfig "nix"+ dir <-+ case ncl of+ Global -> return "/etc/nix"+ Local -> getXdgDirectory XdgConfig "nix" return $ dir <> "/nix.conf" -- 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)+ _ <- 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) 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"- <|> parseOther+ (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"+ <|> parseOther parser :: Parser NixConf parser = NixConf <$> many parseAltLine
src/Cachix/Client/NixVersion.hs view
@@ -1,37 +1,29 @@ module Cachix.Client.NixVersion- ( getNixVersion- , parseNixVersion- , NixVersion(..)- ) where+ ( assertNixVersion,+ parseNixVersion+ )+where +import Data.Text as T+import Data.Versions import Protolude import System.Process (readProcessWithExitCode)-import Data.Versions-import Data.Text as T --data NixVersion- = Nix20- | Nix201- | Nix1XX- deriving (Show, Eq)--getNixVersion :: IO (Either Text NixVersion)-getNixVersion = do+assertNixVersion :: IO (Either Text ())+assertNixVersion = do (exitcode, out, err) <- readProcessWithExitCode "nix-env" ["--version"] mempty unless (err == "") $ putStrLn $ "nix-env stderr: " <> err return $ case exitcode of ExitFailure i -> Left $ "'nix-env --version' exited with " <> show i ExitSuccess -> parseNixVersion $ toS out -parseNixVersion :: Text -> Either Text NixVersion+parseNixVersion :: Text -> Either Text () parseNixVersion output =- let- verStr = T.drop 14 $ T.strip output- err = "Couldn't parse 'nix-env --version' output: " <> output- in case versioning verStr of- Left _ -> Left err- Right ver | verStr == "" -> Left err- | ver < Ideal (SemVer 1 99 0 [] []) -> Right Nix1XX- | ver < Ideal (SemVer 2 0 1 [] []) -> Right Nix20- | otherwise -> Right Nix201+ let verStr = T.drop 14 $ T.strip output+ err = "Couldn't parse 'nix-env --version' output: " <> output+ in case versioning verStr of+ Left _ -> Left err+ Right ver+ | verStr == "" -> Left err+ | ver < Ideal (SemVer 2 0 1 [] []) -> Left "Nix 2.0.2 or lower is not supported. Please upgrade: https://nixos.org/nix/"+ | otherwise -> Right ()
src/Cachix/Client/OptionsParser.hs view
@@ -1,47 +1,58 @@ module Cachix.Client.OptionsParser- ( CachixCommand(..)- , CachixOptions(..)- , UseOptions(..)- , PushArguments(..)- , PushOptions(..)- , BinaryCacheName- , getOpts- ) where--import Data.Bifunctor (first)-import Protolude hiding (option)-import URI.ByteString (URIRef, Absolute, parseURI, strictURIParserOptions- , serializeURIRef')-import Options.Applicative+ ( CachixCommand (..),+ CachixOptions (..),+ PushArguments (..),+ PushOptions (..),+ BinaryCacheName,+ getOpts+ )+where import qualified Cachix.Client.Config as Config-import Cachix.Client.URI (defaultCachixURI)+import qualified Cachix.Client.InstallationMode as InstallationMode+import Cachix.Client.URI (defaultCachixURI)+import Data.Bifunctor (first)+import Options.Applicative+import Protolude hiding (option)+import URI.ByteString+ ( Absolute,+ URIRef,+ parseURI,+ serializeURIRef',+ strictURIParserOptions+ ) -data CachixOptions = CachixOptions- { host :: URIRef Absolute- , configPath :: Config.ConfigPath- , verbose :: Bool- } deriving Show+data CachixOptions+ = CachixOptions+ { host :: URIRef Absolute,+ configPath :: Config.ConfigPath,+ verbose :: Bool+ }+ deriving (Show) parserCachixOptions :: Config.ConfigPath -> Parser CachixOptions-parserCachixOptions defaultConfigPath = CachixOptions- <$> option uriOption ( long "host"- <> value defaultCachixURI- <> metavar "URI"- <> showDefaultWith (toS . serializeURIRef')- <> help "Host to connect to"- )- <*> strOption ( long "config"+parserCachixOptions defaultConfigPath =+ CachixOptions+ <$> option uriOption+ ( long "host"+ <> value defaultCachixURI+ <> metavar "URI"+ <> showDefaultWith (toS . serializeURIRef')+ <> help "Host to connect to"+ )+ <*> strOption+ ( long "config" <> short 'c' <> value defaultConfigPath <> metavar "CONFIGPATH" <> showDefault <> help "Cachix configuration file"- )- <*> switch ( long "verbose"- <> short 'v'- <> help "Verbose mode"- )+ )+ <*> switch+ ( long "verbose"+ <> short 'v'+ <> help "Verbose mode"+ ) uriOption :: ReadM (URIRef Absolute) uriOption = eitherReader $ \s ->@@ -54,90 +65,101 @@ | Create BinaryCacheName | GenerateKeypair BinaryCacheName | Push PushArguments- | Use BinaryCacheName UseOptions+ | Use BinaryCacheName InstallationMode.UseOptions | Version- deriving Show--data UseOptions = UseOptions- { useNixOS :: Bool- , useNixOSFolder :: FilePath- } deriving Show+ deriving (Show) data PushArguments = PushPaths PushOptions Text [Text] | PushWatchStore PushOptions Text- deriving Show+ deriving (Show) -data PushOptions = PushOptions- { compressionLevel :: Int- } deriving Show+data PushOptions+ = PushOptions+ { compressionLevel :: Int+ }+ deriving (Show) parserCachixCommand :: Parser CachixCommand-parserCachixCommand = subparser $- command "authtoken" (infoH authtoken (progDesc "Configure token for authentication to cachix.org")) <>- command "create" (infoH create (progDesc "DEPRECATED: Go to https://cachix.org instead")) <>- command "generate-keypair" (infoH generateKeypair (progDesc "Generate keypair for a binary cache")) <>- command "push" (infoH push (progDesc "Upload Nix store paths to the binary cache")) <>- command "use" (infoH use (progDesc "Configure nix.conf to enable binary cache during builds"))+parserCachixCommand =+ subparser+ $ command "authtoken" (infoH authtoken (progDesc "Configure token for authentication to cachix.org"))+ <> command "create" (infoH create (progDesc "DEPRECATED: Go to https://cachix.org instead"))+ <> command "generate-keypair" (infoH generateKeypair (progDesc "Generate keypair for a binary cache"))+ <> command "push" (infoH push (progDesc "Upload Nix store paths to the binary cache"))+ <> command "use" (infoH use (progDesc "Configure nix.conf to enable binary cache during builds")) where nameArg = strArgument (metavar "NAME") authtoken = AuthToken <$> strArgument (metavar "TOKEN") create = Create <$> nameArg generateKeypair = GenerateKeypair <$> nameArg- validatedLevel l = l <$ unless (l `elem` [0 .. 9]) (readerError $ "value " <> show l <> " not in expected range: [0..9]")- pushOptions :: Parser PushOptions- pushOptions = PushOptions- <$> option (auto >>= validatedLevel)- ( long "compression-level"- <> metavar "[0..9]"- <> help "The compression level for XZ compression.\+ pushOptions =+ PushOptions+ <$> option (auto >>= validatedLevel)+ ( long "compression-level"+ <> metavar "[0..9]"+ <> help+ "The compression level for XZ compression.\ \ Take compressor *and* decompressor memory usage into account before using [7..9]!"- <> showDefault- <> value 2- )-+ <> showDefault+ <> value 2+ ) push = (\opts cache f -> Push $ f opts cache) <$> pushOptions <*> nameArg <*> (pushPaths <|> pushWatchStore)- pushPaths = (\paths opts cache -> PushPaths opts cache paths) <$>- many (strArgument (metavar "PATHS..."))- pushWatchStore = (\() opts cache -> PushWatchStore opts cache) <$>- flag' () ( long "watch-store"- <> short 'w'- <> help "Run in daemon mode and push store paths as they are added to /nix/store"- )-- use = Use <$> nameArg- <*> (UseOptions <$> switch ( long "nixos"- <> short 'n'- <> help "Write NixOS modules")- <*> strOption ( long "nixos-folder"- <> short 'd'- <> help "Base directory for NixOS configuration"- <> value "/etc/nixos/"- <> showDefault- ))+ pushPaths =+ (\paths opts cache -> PushPaths opts cache paths)+ <$> many (strArgument (metavar "PATHS..."))+ pushWatchStore =+ (\() opts cache -> PushWatchStore opts cache)+ <$> flag' ()+ ( long "watch-store"+ <> short 'w'+ <> help "Run in daemon mode and push store paths as they are added to /nix/store"+ )+ use =+ Use <$> nameArg+ <*> ( InstallationMode.UseOptions+ <$> optional+ ( option (maybeReader InstallationMode.fromString)+ ( long "mode"+ <> short 'm'+ <> help "Mode in which to configure binary caches for Nix. Supported values: nixos, root-nixconf, user-nixconf"+ )+ )+ <*> strOption+ ( long "nixos-folder"+ <> short 'd'+ <> help "Base directory for NixOS configuration"+ <> value "/etc/nixos/"+ <> showDefault+ )+ ) getOpts :: IO (CachixOptions, CachixCommand) getOpts = do configpath <- Config.getDefaultFilename- customExecParser (prefs showHelpOnEmpty) (opts configpath)+ customExecParser (prefs showHelpOnEmpty) (optsInfo configpath) -opts :: Config.ConfigPath -> ParserInfo (CachixOptions, CachixCommand)-opts configpath = infoH parser desc- where parser = (,) <$> parserCachixOptions configpath <*> (parserCachixCommand <|> versionParser)- versionParser :: Parser CachixCommand- versionParser = flag' Version ( long "version"- <> short 'V'- <> help "Show cachix version"- )+optsInfo :: Config.ConfigPath -> ParserInfo (CachixOptions, CachixCommand)+optsInfo configpath = infoH parser desc+ where+ parser = (,) <$> parserCachixOptions configpath <*> (parserCachixCommand <|> versionParser)+ versionParser :: Parser CachixCommand+ versionParser =+ flag' Version+ ( long "version"+ <> short 'V'+ <> help "Show cachix version"+ ) desc :: InfoMod a-desc = fullDesc+desc =+ fullDesc <> progDesc "Sign into https://cachix.org to get started." <> header "cachix.org command interface"- -- TODO: usage footer +-- TODO: usage footer infoH :: Parser a -> InfoMod a -> ParserInfo a infoH a = info (helper <*> a)
src/Cachix/Client/Push.hs view
@@ -1,64 +1,70 @@ {-# LANGUAGE DataKinds #-}-{-# LANGUAGE TypeOperators #-} {-# LANGUAGE Rank2Types #-} {-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeOperators #-}+ module Cachix.Client.Push ( -- * Pushing a single path- pushSingleStorePath- , PushCache(..)- , PushStrategy(..)- , defaultWithXzipCompressor- , defaultWithXzipCompressorWithLevel-+ pushSingleStorePath,+ PushCache (..),+ PushStrategy (..),+ defaultWithXzipCompressor,+ defaultWithXzipCompressorWithLevel, -- * Pushing a closure of store paths- , pushClosure- , mapConcurrentlyBounded- ) where--import Crypto.Sign.Ed25519-import qualified Control.Concurrent.QSem as QSem-import Control.Concurrent.Async (mapConcurrently)-import Control.Monad ((>=>))-import Control.Monad.Trans.Resource (ResourceT)-import Control.Exception.Safe (MonadMask, throwM)-import Control.Retry (recoverAll, RetryStatus, RetryPolicy, constantDelay, limitRetries)-import qualified Data.ByteString.Base64 as B64-import Data.Conduit-import Data.Conduit.Process-import Data.Conduit.Lzma ( compress )-import Data.IORef-import qualified Data.Text as T-import Network.HTTP.Types (status404, status401)-import Protolude-import Servant.API-import Servant.Auth ()-import Servant.Auth.Client-import Servant.Client.Streaming hiding (ClientError)-import Servant.Conduit ()-import System.Process ( readProcess )+ pushClosure,+ mapConcurrentlyBounded+ )+where -import qualified Cachix.Api as Api-import Cachix.Api.Error-import Cachix.Api.Signing (fingerprint, passthroughSizeSink, passthroughHashSink)-import qualified Cachix.Types.NarInfoCreate as Api-import Cachix.Client.Exception ( CachixException(..) )-import Cachix.Client.Servant-import Cachix.Client.Secrets+import qualified Cachix.Api as Api+import Cachix.Api.Error+import Cachix.Api.Signing (fingerprint, passthroughHashSink, passthroughHashSinkB16, passthroughSizeSink)+import Cachix.Client.Exception (CachixException (..))+import Cachix.Client.Secrets+import Cachix.Client.Servant+import Cachix.Client.Store (Store)+import qualified Cachix.Client.Store as Store+import qualified Cachix.Types.NarInfoCreate as Api+import Control.Concurrent.Async (mapConcurrently)+import qualified Control.Concurrent.QSem as QSem+import Control.Exception.Safe (MonadMask, throwM)+import Control.Monad ((>=>))+import Control.Monad.Trans.Resource (ResourceT)+import Control.Retry (RetryPolicy, RetryStatus, constantDelay, limitRetries, recoverAll)+import Crypto.Sign.Ed25519+import qualified Data.ByteString.Base64 as B64+import Data.Conduit+import Data.Conduit.Lzma (compress)+import Data.Conduit.Process+import Data.IORef+import qualified Data.Set as Set+import qualified Data.Text as T+import Network.HTTP.Types (status401, status404)+import Protolude+import Servant.API+import Servant.Auth ()+import Servant.Auth.Client+import Servant.Client.Streaming hiding (ClientError)+import Servant.Conduit ()+import qualified System.Nix.Base32+import System.Process (readProcess) +data PushCache+ = PushCache+ { pushCacheName :: Text,+ pushCacheSigningKey :: SigningKey,+ pushCacheToken :: Token+ } -data PushCache = PushCache- { pushCacheName :: Text- , pushCacheSigningKey :: SigningKey- , pushCacheToken :: Token- }-data PushStrategy m r = PushStrategy- { onAlreadyPresent :: m r -- ^ Called when a path is already in the cache.- , onAttempt :: RetryStatus -> m ()- , on401 :: m r- , onError :: ClientError -> m r- , onDone :: m r- , withXzipCompressor :: forall a. (ConduitM ByteString ByteString (ResourceT IO) () -> m a) -> m a- }+data PushStrategy m r+ = PushStrategy+ { onAlreadyPresent :: m r, -- ^ Called when a path is already in the cache.+ onAttempt :: RetryStatus -> m (),+ on401 :: m r,+ onError :: ClientError -> m r,+ onDone :: m r,+ withXzipCompressor :: forall a. (ConduitM ByteString ByteString (ResourceT IO) () -> m a) -> m a+ } defaultWithXzipCompressor :: forall m a. (ConduitM ByteString ByteString (ResourceT IO) () -> m a) -> m a defaultWithXzipCompressor = ($ compress (Just 2))@@ -68,107 +74,112 @@ pushSingleStorePath :: (MonadMask m, MonadIO m)- => ClientEnv -- ^ cachix base url, connection manager, see 'Cachix.Client.URI.defaultCachixBaseUrl', 'Servant.Client.mkClientEnv'+ => ClientEnv -- ^ cachix base url, connection manager, see 'Cachix.Client.URI.defaultCachixBaseUrl', 'Servant.Client.mkClientEnv'+ -> Store -> PushCache -- ^ details for pushing to cache -> PushStrategy m r -- ^ how to report results, (some) errors, and do some things -> Text -- ^ store path -> m r -- ^ r is determined by the 'PushStrategy'-pushSingleStorePath ce cache cb storePath = retryPath $ \retrystatus -> do-- let (storeHash, storeSuffix) = splitStorePath $ toS storePath+pushSingleStorePath clientEnv _store cache cb storePath = retryAll $ \retrystatus -> do+ let storeHash = fst $ splitStorePath $ toS storePath name = pushCacheName cache- -- Check if narinfo already exists -- TODO: query also cache.nixos.org? server-side?- res <- liftIO $ (`runClientM` ce) $ Api.narinfoHead- (cachixBCClient name)- (pushCacheToken cache)- (Api.NarInfoC storeHash)+ res <-+ liftIO $ (`runClientM` clientEnv)+ $ Api.narinfoHead+ (cachixBCClient name)+ (pushCacheToken cache)+ (Api.NarInfoC storeHash) case res of Right NoContent -> onAlreadyPresent cb -- we're done as store path is already in the cache- Left err | isErr err status404 -> doUpload- | isErr err status401 -> on401 cb- | otherwise -> onError cb err- where- doUpload = do- onAttempt cb retrystatus-- narSizeRef <- liftIO $ newIORef 0- fileSizeRef <- liftIO $ newIORef 0- narHashRef <- liftIO $ newIORef ("" :: Text)- fileHashRef <- liftIO $ newIORef ("" :: Text)-- -- stream store path as xz compressed nar file- let cmd = proc "nix-store" ["--dump", toS storePath]- -- TODO: print process stderr?- (ClosedStream, (stdoutStream, closeStdout), ClosedStream, cph) <- liftIO $ streamingProcess cmd- withXzipCompressor cb $ \xzCompressor -> do- let stream'- = stdoutStream- .| passthroughSizeSink narSizeRef- .| passthroughHashSink narHashRef- .| xzCompressor- .| passthroughSizeSink fileSizeRef- .| passthroughHashSink fileHashRef-- -- for now we need to use letsencrypt domain instead of cloudflare due to its upload limits- let newClientEnv = ce {- baseUrl = (baseUrl ce) { baseUrlHost = toS name <> "." <> baseUrlHost (baseUrl ce)}- }- (_ :: NoContent) <- liftIO $ (`withClientM` newClientEnv)- (Api.createNar (cachixBCStreamingClient name) stream')- $ escalate >=> \NoContent -> do- closeStdout- exitcode <- waitForStreamingProcess cph- when (exitcode /= ExitSuccess) $ throwM $ NarStreamingError exitcode $ show cmd-- -- TODO: we're done, so we can leave withClientM. Doing so- -- will allow more concurrent requests when the number- -- of XZ compressors is limited- narSize <- readIORef narSizeRef- narHashB16 <- readIORef narHashRef- fileHash <- readIORef fileHashRef- fileSize <- readIORef fileSizeRef-- -- TODO: #3: implement using pure haskell- narHash <- ("sha256:" <>) . T.strip . toS <$> readProcess "nix-hash" ["--type", "sha256", "--to-base32", toS narHashB16] mempty-- deriverRaw <- T.strip . toS <$> readProcess "nix-store" ["-q", "--deriver", toS storePath] mempty- let deriver = if deriverRaw == "unknown-deriver"- then deriverRaw- else T.drop 11 deriverRaw-- references <- sort . T.lines . T.strip . toS <$> readProcess "nix-store" ["-q", "--references", toS storePath] mempty-- let- fp = fingerprint storePath narHash narSize references- sig = dsign (signingSecretKey $ pushCacheSigningKey cache) fp- nic = Api.NarInfoCreate- { cStoreHash = storeHash- , cStoreSuffix = storeSuffix- , cNarHash = narHash- , cNarSize = narSize- , cFileSize = fileSize- , cFileHash = fileHash- , cReferences = fmap (T.drop 11) references- , cDeriver = deriver- , cSig = toS $ B64.encode $ unSignature sig- }+ Left err+ | isErr err status404 -> uploadStorePath clientEnv _store cache cb storePath retrystatus+ | isErr err status401 -> on401 cb+ | otherwise -> onError cb err - escalate $ Api.isNarInfoCreateValid nic+uploadStorePath+ :: (MonadMask m, MonadIO m)+ => ClientEnv -- ^ cachix base url, connection manager, see 'Cachix.Client.URI.defaultCachixBaseUrl', 'Servant.Client.mkClientEnv'+ -> Store+ -> PushCache -- ^ details for pushing to cache+ -> PushStrategy m r -- ^ how to report results, (some) errors, and do some things+ -> Text -- ^ store path+ -> RetryStatus+ -> m r -- ^ r is determined by the 'PushStrategy'+uploadStorePath clientEnv _store cache cb storePath retrystatus = do+ let (storeHash, storeSuffix) = splitStorePath $ toS storePath+ name = pushCacheName cache+ onAttempt cb retrystatus+ narSizeRef <- liftIO $ newIORef 0+ fileSizeRef <- liftIO $ newIORef 0+ narHashRef <- liftIO $ newIORef ("" :: ByteString)+ fileHashRef <- liftIO $ newIORef ("" :: ByteString)+ -- stream store path as xz compressed nar file+ let cmd = proc "nix-store" ["--dump", toS storePath]+ -- TODO: print process stderr?+ (ClosedStream, (stdoutStream, closeStdout), ClosedStream, cph) <- liftIO $ streamingProcess cmd+ withXzipCompressor cb $ \xzCompressor -> do+ let stream' =+ stdoutStream+ .| passthroughSizeSink narSizeRef+ .| passthroughHashSink narHashRef+ .| xzCompressor+ .| passthroughSizeSink fileSizeRef+ .| passthroughHashSinkB16 fileHashRef+ -- for now we need to use letsencrypt domain instead of cloudflare due to its upload limits+ let newClientEnv =+ clientEnv+ { baseUrl = (baseUrl clientEnv) {baseUrlHost = toS name <> "." <> baseUrlHost (baseUrl clientEnv)}+ }+ (_ :: NoContent) <-+ liftIO+ $ (`withClientM` newClientEnv)+ (Api.createNar (cachixBCStreamingClient name) stream')+ $ escalate+ >=> \NoContent -> do+ closeStdout+ exitcode <- waitForStreamingProcess cph+ when (exitcode /= ExitSuccess) $ throwM $ NarStreamingError exitcode $ show cmd+ -- TODO: we're done, so we can leave withClientM. Doing so+ -- will allow more concurrent requests when the number+ -- of XZ compressors is limited+ narSize <- readIORef narSizeRef+ narHash <- ("sha256:" <>) . System.Nix.Base32.encode <$> readIORef narHashRef+ fileHash <- readIORef fileHashRef+ fileSize <- readIORef fileSizeRef+ deriverRaw <- T.strip . toS <$> readProcess "nix-store" ["-q", "--deriver", toS storePath] mempty+ let deriver =+ if deriverRaw == "unknown-deriver"+ then deriverRaw+ else T.drop 11 deriverRaw+ references <- sort . T.lines . T.strip . toS <$> readProcess "nix-store" ["-q", "--references", toS storePath] mempty+ let fp = fingerprint storePath narHash narSize references+ sig = dsign (signingSecretKey $ pushCacheSigningKey cache) fp+ nic = Api.NarInfoCreate+ { cStoreHash = storeHash,+ cStoreSuffix = storeSuffix,+ cNarHash = narHash,+ cNarSize = narSize,+ cFileSize = fileSize,+ cFileHash = toS fileHash,+ cReferences = fmap (T.drop 11) references,+ cDeriver = deriver,+ cSig = toS $ B64.encode $ unSignature sig+ }+ escalate $ Api.isNarInfoCreateValid nic+ -- Upload narinfo with signature+ escalate <=< (`runClientM` clientEnv)+ $ Api.createNarinfo+ (cachixBCClient name)+ (Api.NarInfoC storeHash)+ nic+ onDone cb - -- Upload narinfo with signature- escalate <=< (`runClientM` ce) $ Api.createNarinfo- (cachixBCClient name)- (Api.NarInfoC storeHash)- nic- onDone cb+-- Catches all exceptions except skipAsyncExceptions+retryAll :: (MonadIO m, MonadMask m) => (RetryStatus -> m a) -> m a+retryAll = recoverAll defaultRetryPolicy where- -- Retry up to 5 times for each store path.- -- Catches all exceptions except skipAsyncExceptions- retryPath :: (MonadIO m, MonadMask m) => (RetryStatus -> m a) -> m a- retryPath = recoverAll defaultRetryPolicy- defaultRetryPolicy :: RetryPolicy defaultRetryPolicy = constantDelay 50000 <> limitRetries 3@@ -179,22 +190,41 @@ pushClosure :: (MonadIO m, MonadMask m) => (forall a b. (a -> m b) -> [a] -> m [b])- -- ^ Traverse paths, responsible for bounding parallel processing of paths- --- -- For example: @'mapConcurrentlyBounded' 4@+ -- ^ Traverse paths, responsible for bounding parallel processing of paths+ --+ -- For example: @'mapConcurrentlyBounded' 4@ -> ClientEnv -- ^ See 'pushSingleStorePath'+ -> Store -> PushCache -> (Text -> PushStrategy m r) -> [Text] -- ^ Initial store paths -> m [r] -- ^ Every @r@ per store path of the entire closure of store paths-pushClosure traversal clientEnv pushCache pushStrategy inputStorePaths = do- +pushClosure traversal clientEnv store pushCache pushStrategy inputStorePaths = do -- Get the transitive closure of dependencies- -- TODO: split args if too big- paths <- liftIO $ T.lines . toS <$> readProcess "nix-store" (fmap toS (["-qR"] <> inputStorePaths)) mempty-+ paths <-+ liftIO $ do+ inputs <- Store.newEmptyPathSet+ for_ inputStorePaths $ \path -> do+ normalized <- Store.followLinksToStorePath store (encodeUtf8 path)+ Store.addToPathSet normalized inputs+ closure <- Store.computeFSClosure store Store.defaultClosureParams inputs+ Store.traversePathSet (pure . toSL) closure+ -- Check what store paths are missing+ -- TODO: query also cache.nixos.org? server-side?+ missingHashesList <-+ retryAll $ \_ ->+ escalate+ =<< liftIO+ ( (`runClientM` clientEnv)+ $ Api.narinfoBulk+ (cachixBCClient (pushCacheName pushCache))+ (pushCacheToken pushCache)+ (fst . splitStorePath <$> paths)+ )+ let missingHashes = Set.fromList missingHashesList+ missingPaths = filter (\path -> Set.member (fst (splitStorePath path)) missingHashes) paths -- TODO: make pool size configurable, on beefier machines this could be doubled- traversal (\path -> pushSingleStorePath clientEnv pushCache (pushStrategy path) path) paths+ traversal (\path -> retryAll $ \retrystatus -> uploadStorePath clientEnv store pushCache (pushStrategy path) path retrystatus) missingPaths mapConcurrentlyBounded :: Traversable t => Int -> (a -> IO b) -> t a -> IO (t b) mapConcurrentlyBounded bound action items = do@@ -204,7 +234,6 @@ ------------------- -- Private terms --- splitStorePath :: Text -> (Text, Text) splitStorePath storePath = (T.take 32 (T.drop 11 storePath), T.drop 44 storePath)
src/Cachix/Client/Secrets.hs view
@@ -1,24 +1,22 @@ -- A facade for working with secrets and their representations, without delving -- into cryptography libraries.-module Cachix.Client.Secrets (-- -- * NAR signing- SigningKey(..)- , parseSigningKeyLenient- , exportSigningKey-- -- TODO: * Auth token--) where+module Cachix.Client.Secrets+ ( -- * NAR signing+ SigningKey (..),+ parseSigningKeyLenient,+ exportSigningKey+ )+where -import Crypto.Sign.Ed25519-import qualified Data.ByteString.Base64 as B64-import qualified Data.ByteString.Char8 as BC-import Data.Char ( isSpace )-import Protolude+-- TODO: * Auth token+import Crypto.Sign.Ed25519+import qualified Data.ByteString.Base64 as B64+import qualified Data.ByteString.Char8 as BC+import Data.Char (isSpace)+import Protolude -- | A secret key for signing nars.-newtype SigningKey = SigningKey { signingSecretKey :: SecretKey }+newtype SigningKey = SigningKey {signingSecretKey :: SecretKey} parseSigningKeyLenientBS :: ByteString -- ^ ASCII (Base64)@@ -28,7 +26,7 @@ bcDropAround f = bcDropWhileEnd f . BC.dropWhile f stripped = bcDropAround isSpace raw nonNull = if BC.null stripped then Left "A signing key must not be empty" else pure stripped- in SigningKey . SecretKey . B64.decodeLenient <$> nonNull+ in SigningKey . SecretKey . B64.decodeLenient <$> nonNull parseSigningKeyLenient :: Text -- ^ Base64
src/Cachix/Client/Servant.hs view
@@ -27,7 +27,7 @@ import Servant.Auth.Client (Token) import qualified Servant.Client import Servant.Client.Generic (AsClientT)-import Servant.Client.Streaming+import Servant.Client.Streaming hiding (ClientError) import Servant.Conduit () type ClientError =
+ src/Cachix/Client/Store.hs view
@@ -0,0 +1,193 @@+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TemplateHaskell #-}++module Cachix.Client.Store+ ( Store,+ -- * Getting a Store+ openStore,+ releaseStore,+ -- * Query a path+ followLinksToStorePath,+ queryPathInfo,+ -- * Get closures+ computeFSClosure,+ ClosureParams (..),+ defaultClosureParams,+ PathSet,+ newEmptyPathSet,+ addToPathSet,+ traversePathSet,+ -- * Miscellaneous+ storeUri+ )+where++import Cachix.Client.Store.Context (NixStore, Ref, ValidPathInfo, context)+import qualified Cachix.Client.Store.Context as C hiding (context)+import Data.ByteString.Unsafe (unsafePackMallocCString)+import Data.Coerce+import Foreign.ForeignPtr+import qualified Language.C.Inline.Cpp as C+import qualified Language.C.Inline.Cpp.Exceptions as C+import Protolude+import System.IO.Unsafe (unsafePerformIO)++C.context context++C.include "<cstring>"++C.include "<nix/config.h>"++C.include "<nix/shared.hh>"++C.include "<nix/store-api.hh>"++C.include "<nix/get-drvs.hh>"++C.include "<nix/derivations.hh>"++C.include "<nix/affinity.hh>"++C.include "<nix/globals.hh>"++C.include "aliases.h"++C.using "namespace nix"++-- | TODO: foreignptr+newtype Store = Store (Ptr (Ref NixStore))++openStore :: IO Store+openStore =+ coerce+ [C.throwBlock| refStore* {+ refStore s = openStore();+ return new refStore(s);+ } |]++releaseStore :: Store -> IO ()+releaseStore (Store store) = [C.exp| void { delete $(refStore* store) } |]++-- | Follow symlinks to the store and chop off the parts after the top-level store name+followLinksToStorePath :: Store -> ByteString -> IO ByteString+followLinksToStorePath (Store store) bs =+ unsafePackMallocCString+ =<< [C.throwBlock| const char *{+ return strdup((*$(refStore* store))->followLinksToStorePath(std::string($bs-ptr:bs, $bs-len:bs)).c_str());+ }|]++storeUri :: Store -> IO ByteString+storeUri (Store store) =+ unsafePackMallocCString+ =<< [C.throwBlock| const char* {+ std::string uri = (*$(refStore* store))->getUri();+ return strdup(uri.c_str());+ } |]++-- TODO: test queryPathInfo+queryPathInfo :: Store -> ByteString -> IO (ForeignPtr (Ref ValidPathInfo))+queryPathInfo (Store store) path = do+ vpi <-+ [C.exp| refValidPathInfo* {+ new refValidPathInfo((*$(refStore* store))->queryPathInfo($bs-cstr:path))+ } |]+ newForeignPtr finalizeRefValidPathInfo vpi++finalizeRefValidPathInfo :: FinalizerPtr (Ref ValidPathInfo)++{-# NOINLINE finalizeRefValidPathInfo #-}+finalizeRefValidPathInfo =+ unsafePerformIO+ [C.exp|+ void (*)(refValidPathInfo *) {+ [](refValidPathInfo *v){ delete v; }+ } |]++----- PathSet -----+newtype PathSet = PathSet (ForeignPtr (C.Set C.CxxString))++finalizePathSet :: FinalizerPtr C.PathSet++{-# NOINLINE finalizePathSet #-}+finalizePathSet =+ unsafePerformIO+ [C.exp|+ void (*)(PathSet *) {+ [](PathSet *v){+ delete v;+ }+ } |]++newEmptyPathSet :: IO PathSet+newEmptyPathSet = do+ ptr <- [C.exp| PathSet *{ new PathSet() }|]+ fptr <- newForeignPtr finalizePathSet ptr+ pure $ PathSet fptr++addToPathSet :: ByteString -> PathSet -> IO ()+addToPathSet bs pathSet_ = withPathSet pathSet_ $ \pathSet ->+ [C.throwBlock| void { + $(PathSet *pathSet)->insert(std::string($bs-ptr:bs, $bs-len:bs));+ }|]++withPathSet :: PathSet -> (Ptr C.PathSet -> IO b) -> IO b+withPathSet (PathSet pathSetFptr) = withForeignPtr pathSetFptr++traversePathSet :: forall a. (ByteString -> IO a) -> PathSet -> IO [a]+traversePathSet f pathSet_ = withPathSet pathSet_ $ \pathSet -> do+ i <- [C.exp| PathSetIterator *{ new PathSetIterator($(PathSet *pathSet)->begin()) }|]+ end <- [C.exp| PathSetIterator *{ new PathSetIterator ($(PathSet *pathSet)->end()) }|]+ let cleanup =+ [C.throwBlock| void {+ delete $(PathSetIterator *i);+ delete $(PathSetIterator *end);+ }|]+ flip finally cleanup+ $ let go :: ([a] -> [a]) -> IO [a]+ go acc = do+ isDone <-+ [C.exp| int {+ *$(PathSetIterator *i) == *$(PathSetIterator *end)+ }|]+ if isDone /= 0+ then pure $ acc []+ else+ do+ somePath <- unsafePackMallocCString =<< [C.exp| const char *{ strdup((*$(PathSetIterator *i))->c_str()) } |]+ a <- f somePath+ [C.throwBlock| void { (*$(PathSetIterator *i))++; } |]+ go (acc . (a :))+ in go identity++----- computeFSClosure -----+data ClosureParams+ = ClosureParams+ { flipDirection :: Bool,+ includeOutputs :: Bool,+ includeDerivers :: Bool+ }++defaultClosureParams :: ClosureParams+defaultClosureParams = ClosureParams+ { flipDirection = False,+ includeOutputs = False,+ includeDerivers = False+ }++computeFSClosure :: Store -> ClosureParams -> PathSet -> IO PathSet+computeFSClosure (Store store) params startingSet_ = withPathSet startingSet_ $ \startingSet -> do+ let countTrue :: Bool -> C.CInt+ countTrue True = 1+ countTrue False = 0+ flipDir = countTrue $ flipDirection params+ inclOut = countTrue $ includeOutputs params+ inclDrv = countTrue $ includeDerivers params+ ps <-+ [C.throwBlock| PathSet* {+ PathSet *r = new PathSet();+ (*$(refStore* store))->computeFSClosure(*$(PathSet *startingSet), *r, $(int flipDir), $(int inclOut), $(int inclDrv));+ return r;+ } |]+ fp <- newForeignPtr finalizePathSet ps+ pure $ PathSet fp
+ src/Cachix/Client/Store/Context.hs view
@@ -0,0 +1,41 @@+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE TemplateHaskell #-}++module Cachix.Client.Store.Context where++import qualified Data.Map as M+import qualified Language.C.Inline.Context as C+import qualified Language.C.Inline.Cpp as C+import qualified Language.C.Types as C+import Protolude hiding (Set)++-- | A Nix @ref@+data Ref a++-- | A Nix @PathSet@ aka @std::set<Path>@ aka @std::set<std::string>>@+type PathSet = Set CxxString++data NixStore++data ValidPathInfo++-- | An STL @set@+data Set a++-- | An STL @::iterator@+data Iterator a++-- | An @std::string@+data CxxString++context :: C.Context+context =+ C.cppCtx <> C.fptrCtx <> C.bsCtx+ <> mempty+ { C.ctxTypesTable =+ M.singleton (C.TypeName "refStore") [t|Ref NixStore|]+ <> M.singleton (C.TypeName "refValidPathInfo")+ [t|Ref ValidPathInfo|]+ <> M.singleton (C.TypeName "PathSet") [t|PathSet|]+ <> M.singleton (C.TypeName "PathSetIterator") [t|Iterator PathSet|]+ }
src/Cachix/Client/URI.hs view
@@ -1,24 +1,25 @@ {-# LANGUAGE GADTs #-} {-# LANGUAGE QuasiQuotes #-}+ module Cachix.Client.URI- ( getBaseUrl- , defaultCachixURI- , defaultCachixBaseUrl- ) where+ ( getBaseUrl,+ defaultCachixURI,+ defaultCachixBaseUrl+ )+where import Protolude+import Servant.Client import qualified URI.ByteString as UBS import URI.ByteString hiding (Scheme)-import Servant.Client import URI.ByteString.QQ - -- TODO: make getBaseUrl internal -- | Partial function from URI to BaseUrl -- getBaseUrl :: URIRef Absolute -> BaseUrl-getBaseUrl URI{..} =+getBaseUrl URI {..} = case uriAuthority of Nothing -> panic "missing host in url" Just authority ->@@ -33,10 +34,8 @@ UBS.Scheme "http" -> Http UBS.Scheme "https" -> Https _ -> panic "uri can only be http/https"- getPort :: Int getPort = maybe defaultPort portNumber $ authorityPort authority- defaultPort :: Int defaultPort = case getScheme of Http -> 80
test/InstallationModeSpec.hs view
@@ -1,102 +1,65 @@ module InstallationModeSpec where +import Cachix.Client.InstallationMode+import qualified Cachix.Client.NixConf as NixConf import Protolude import Test.Hspec -import qualified Cachix.Client.NixConf as NixConf-import Cachix.Client.NixVersion ( NixVersion(..) )-import Cachix.Client.InstallationMode-- spec :: Spec spec = describe "getInstallationMode" $ do- it "NixOS with root prints configuration" $- let- nixenv = NixEnv- { nixVersion = Nix20 -- any except 1.0- , isTrusted = True -- any- , isRoot = True- , isNixOS = True- }- in getInstallationMode nixenv `shouldBe` EchoNixOS Nix20- it "NixOS without trust prints configuration plus trust" $- let- nixenv = NixEnv- { nixVersion = Nix201- , isTrusted = False- , isRoot = False- , isNixOS = True- }- in getInstallationMode nixenv `shouldBe` EchoNixOSWithTrustedUser Nix201- it "NixOS without trust prints configuration on older Nix" $- let- nixenv = NixEnv- { nixVersion = Nix20- , isTrusted = False- , isRoot = False- , isNixOS = True- }- in getInstallationMode nixenv `shouldBe` EchoNixOS Nix20- it "NixOS non-root trusted results into local install" $- let- nixenv = NixEnv- { nixVersion = Nix201- , isTrusted = True- , isRoot = False- , isNixOS = True- }- in getInstallationMode nixenv `shouldBe` Install Nix201 NixConf.Local- it "non-NixOS root results into global install" $- let- nixenv = NixEnv- { nixVersion = Nix201- , isTrusted = True- , isRoot = True- , isNixOS = False- }- in getInstallationMode nixenv `shouldBe` Install Nix201 NixConf.Global- it "non-NixOS with Nix 1.X root results into global install" $- let- nixenv = NixEnv- { nixVersion = Nix1XX- , isTrusted = True- , isRoot = True- , isNixOS = False -- any- }- in getInstallationMode nixenv `shouldBe` Install Nix1XX NixConf.Global- it "non-NixOS non-root trusted results into local install" $- let- nixenv = NixEnv- { nixVersion = Nix201- , isTrusted = True- , isRoot = False- , isNixOS = False- }- in getInstallationMode nixenv `shouldBe` Install Nix201 NixConf.Local- it "non-NixOS non-root non-trusted results into required sudo" $- let- nixenv = NixEnv- { nixVersion = Nix201- , isTrusted = False- , isRoot = False- , isNixOS = False- }- in getInstallationMode nixenv `shouldBe` UntrustedRequiresSudo- it "non-NixOS non-root non-trusted on Nix 2.0.0 results into required sudo with upgrade warning" $- let- nixenv = NixEnv- { nixVersion = Nix20- , isTrusted = False- , isRoot = False- , isNixOS = False- }- in getInstallationMode nixenv `shouldBe` Nix20RequiresSudo- it "non-NixOS non-root trusted on Nix 2.0.0 results into required sudo with upgrade warning" $- let- nixenv = NixEnv- { nixVersion = Nix20- , isTrusted = True- , isRoot = False- , isNixOS = False- }- in getInstallationMode nixenv `shouldBe` Nix20RequiresSudo+ it "NixOS with root prints configuration"+ $ let nixenv = NixEnv+ { isTrusted = True, -- any+ isRoot = True,+ isNixOS = True+ }+ in getInstallationMode nixenv `shouldBe` WriteNixOS+ it "NixOS without trust prints configuration plus trust"+ $ let nixenv = NixEnv+ { isTrusted = False,+ isRoot = False,+ isNixOS = True+ }+ in getInstallationMode nixenv `shouldBe` UntrustedRequiresSudo+ it "NixOS without trust prints configuration on older Nix"+ $ let nixenv = NixEnv+ { isTrusted = False,+ isRoot = False,+ isNixOS = True+ }+ in getInstallationMode nixenv `shouldBe` UntrustedRequiresSudo+ it "NixOS non-root trusted results into local install"+ $ let nixenv = NixEnv+ { isTrusted = True,+ isRoot = False,+ isNixOS = True+ }+ in getInstallationMode nixenv `shouldBe` Install NixConf.Local+ it "non-NixOS root results into global install"+ $ let nixenv = NixEnv+ { isTrusted = True,+ isRoot = True,+ isNixOS = False+ }+ in getInstallationMode nixenv `shouldBe` Install NixConf.Global+ it "non-NixOS with Nix 1.X root results into global install"+ $ let nixenv = NixEnv+ { isTrusted = True,+ isRoot = True,+ isNixOS = False -- any+ }+ in getInstallationMode nixenv `shouldBe` Install NixConf.Global+ it "non-NixOS non-root trusted results into local install"+ $ let nixenv = NixEnv+ { isTrusted = True,+ isRoot = False,+ isNixOS = False+ }+ in getInstallationMode nixenv `shouldBe` Install NixConf.Local+ it "non-NixOS non-root non-trusted results into required sudo"+ $ let nixenv = NixEnv+ { isTrusted = False,+ isRoot = False,+ isNixOS = False+ }+ in getInstallationMode nixenv `shouldBe` UntrustedRequiresSudo
test/Main.hs view
@@ -1,12 +1,13 @@ module Main where import Protolude-import Test.Hspec.Runner import qualified Spec+import Test.Hspec.Runner main :: IO () main = hspecWith config Spec.spec where- config = defaultConfig- { configColorMode = ColorAlways- }+ config =+ defaultConfig+ { configColorMode = ColorAlways+ }
test/NetRcSpec.hs view
@@ -1,53 +1,47 @@ module NetRcSpec where -import Paths_cachix+import Cachix.Api (BinaryCache (..))+import Cachix.Client.Config (Config (..))+import qualified Cachix.Client.NetRc as NetRc import Protolude import System.Directory (copyFile)-import System.IO.Temp (withSystemTempFile)+import System.IO.Temp (withSystemTempFile) import Test.Hspec -import Cachix.Api (BinaryCache(..))-import qualified Cachix.Client.NetRc as NetRc-import Cachix.Client.Config (Config(..))-- bc1 :: BinaryCache bc1 = BinaryCache- { name = "name"- , uri = "https://name.cachix.org"- , publicSigningKeys = ["pub"]- , isPublic = False- , githubUsername = "foobar"- }+ { name = "name",+ uri = "https://name.cachix.org",+ publicSigningKeys = ["pub"],+ isPublic = False,+ githubUsername = "foobar"+ } bc2 :: BinaryCache bc2 = BinaryCache- { name = "name2"- , uri = "https://name2.cachix.org"- , publicSigningKeys = ["pub2"]- , isPublic = False- , githubUsername = "foobar2"- }+ { name = "name2",+ uri = "https://name2.cachix.org",+ publicSigningKeys = ["pub2"],+ isPublic = False,+ githubUsername = "foobar2"+ } config :: Config config = Config- { authToken = "token123"- , binaryCaches = []- }+ { authToken = "token123",+ binaryCaches = []+ } -- TODO: poor man's golden tests, use https://github.com/stackbuilders/hspec-golden test :: [BinaryCache] -> Text -> Expectation test binaryCaches goldenName = withSystemTempFile "hspec-netrc" $ \filepath _ -> do- input <- getInput- output <- getOutput- copyFile input filepath- NetRc.add config binaryCaches filepath- real <- readFile filepath- expected <- readFile output- real `shouldBe` expected- where- getInput = getDataFileName $ toS goldenName <> ".input"- getOutput = getDataFileName $ toS goldenName <> ".output"+ let input = "test/data/" <> toS goldenName <> ".input"+ output = "test/data/" <> toS goldenName <> ".output"+ copyFile input filepath+ NetRc.add config binaryCaches filepath+ real <- readFile filepath+ expected <- readFile output+ real `shouldBe` expected spec :: Spec spec =
test/NixConfSpec.hs view
@@ -1,137 +1,148 @@ {-# LANGUAGE QuasiQuotes #-}+ module NixConfSpec where -import Protolude+import Cachix.Api (BinaryCache (..))+import Cachix.Client.NixConf as NixConf import Data.String.Here+import Protolude import Test.Hspec -import Cachix.Client.NixConf as NixConf-import Cachix.Client.NixVersion (NixVersion(..))-import Cachix.Api (BinaryCache(..))-- property :: Text -> Expectation-property x = NixConf.render Nix20 <$> parse x `shouldBe` Right x--propertyNix1 :: Text -> Expectation-propertyNix1 x = NixConf.render Nix1XX <$> parse x `shouldBe` Right x+property x = NixConf.render <$> parse x `shouldBe` Right x bc :: BinaryCache bc = BinaryCache- { name = "name"- , uri = "https://name.cachix.org"- , isPublic = True- , publicSigningKeys = ["pub"]- , githubUsername = "foobar"- }+ { name = "name",+ uri = "https://name.cachix.org",+ isPublic = True,+ publicSigningKeys = ["pub"],+ githubUsername = "foobar"+ } spec :: Spec spec = do describe "render . parse" $ do- it "handles single value substituters" $- property "substituters = a\n"- it "handles multi value substituters" $- 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 all known keys for Nix 1.0" $- propertyNix1 "binary-caches = a b c\ntrusted-users = him me\nbinary-cache-public-keys = a\n"- it "random content" $- property "blabla = foobar\nfoo = bar\n"+ it "handles single value substituters"+ $ property "substituters = a\n"+ it "handles multi value substituters"+ $ 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 "random content"+ $ property "blabla = foobar\nfoo = bar\n" describe "add" $ do- it "merges binary caches from both files" $- let- globalConf = NixConf- [ Substituters ["bc1"]- , TrustedPublicKeys ["pub1"]- ]- localConf = NixConf- [ Substituters ["bc2"]- , TrustedPublicKeys ["pub2"]- ]- result = NixConf- [ Substituters [defaultPublicURI, "bc1", "bc2", "https://name.cachix.org"]- , 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- [ Substituters [defaultPublicURI, "https://name.cachix.org"]- , TrustedPublicKeys [defaultSigningKey, "pub"]- ]- localConf = NixConf [ ]- result = NixConf- [ Substituters [defaultPublicURI, "https://name.cachix.org"]- , TrustedPublicKeys [defaultSigningKey, "pub"]- ]- in add bc [globalConf, localConf] localConf `shouldBe` result- it "preserves other nixconf entries" $- let- globalConf = NixConf- [ Substituters ["http"]- , TrustedPublicKeys ["pub1"]- , TrustedUsers ["user"]- , Other "foo"- ]- localConf = NixConf- [ TrustedUsers ["user2"]- , Other "bar"- ]- result = NixConf- [ TrustedUsers ["user2"]- , Other "bar"- , Substituters [defaultPublicURI, "http", "https://name.cachix.org"]- , TrustedPublicKeys [defaultSigningKey, "pub1", "pub"]- ]- in add bc [globalConf, localConf] localConf `shouldBe` result- it "removed duplicates" $- let- globalConf = NixConf- [ Substituters ["bc1", "bc1"]- , TrustedPublicKeys ["pub1", "pub1"]- ]- localConf = NixConf- [ Substituters ["bc2", "bc2"]- , TrustedPublicKeys ["pub2", "pub2"]- ]- result = NixConf- [ Substituters [defaultPublicURI, "bc1", "bc2", "https://name.cachix.org"]- , 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 []- result = NixConf- [ Substituters [defaultPublicURI, "https://name.cachix.org"]- , TrustedPublicKeys [defaultSigningKey, "pub"]- ]- in add bc [globalConf, localConf] localConf `shouldBe` result+ it "merges binary caches from both files"+ $ let globalConf =+ NixConf+ [ Substituters ["bc1"],+ TrustedPublicKeys ["pub1"]+ ]+ localConf =+ NixConf+ [ Substituters ["bc2"],+ TrustedPublicKeys ["pub2"]+ ]+ result =+ NixConf+ [ Substituters [defaultPublicURI, "bc1", "bc2", "https://name.cachix.org"],+ 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+ [ Substituters [defaultPublicURI, "https://name.cachix.org"],+ TrustedPublicKeys [defaultSigningKey, "pub"]+ ]+ localConf = NixConf []+ result =+ NixConf+ [ Substituters [defaultPublicURI, "https://name.cachix.org"],+ TrustedPublicKeys [defaultSigningKey, "pub"]+ ]+ in add bc [globalConf, localConf] localConf `shouldBe` result+ it "preserves other nixconf entries"+ $ let globalConf =+ NixConf+ [ Substituters ["http"],+ TrustedPublicKeys ["pub1"],+ TrustedUsers ["user"],+ Other "foo"+ ]+ localConf =+ NixConf+ [ TrustedUsers ["user2"],+ Other "bar"+ ]+ result =+ NixConf+ [ TrustedUsers ["user2"],+ Other "bar",+ Substituters [defaultPublicURI, "http", "https://name.cachix.org"],+ TrustedPublicKeys [defaultSigningKey, "pub1", "pub"]+ ]+ in add bc [globalConf, localConf] localConf `shouldBe` result+ it "removed duplicates"+ $ let globalConf =+ NixConf+ [ Substituters ["bc1", "bc1"],+ TrustedPublicKeys ["pub1", "pub1"]+ ]+ localConf =+ NixConf+ [ Substituters ["bc2", "bc2"],+ TrustedPublicKeys ["pub2", "pub2"]+ ]+ result =+ NixConf+ [ Substituters [defaultPublicURI, "bc1", "bc2", "https://name.cachix.org"],+ 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 []+ result =+ NixConf+ [ Substituters [defaultPublicURI, "https://name.cachix.org"],+ TrustedPublicKeys [defaultSigningKey, "pub"]+ ]+ in add bc [globalConf, localConf] localConf `shouldBe` result 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 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 ""])+ 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 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 ""+ ]+ ) realExample :: Text-realExample = [hereLit|+realExample =+ [hereLit| substituters = a b c trusted-users = him me trusted-public-keys = a
test/NixVersionSpec.hs view
@@ -1,21 +1,26 @@ module NixVersionSpec where +import Cachix.Client.NixVersion import Protolude import Test.Hspec -import Cachix.Client.NixVersion- spec :: Spec spec = describe "parseNixVersion" $ do- it "parses 'nix-env (Nix) 2.0' as Nix20" $- parseNixVersion "nix-env (Nix) 2.0" `shouldBe` Right Nix20- it "parses 'nix-env (Nix) 2.0.1' as Nix201" $- parseNixVersion "nix-env (Nix) 2.0.1" `shouldBe` Right Nix201- it "parses 'nix-env (Nix) 1.11.13' as Nix1XX" $- parseNixVersion "nix-env (Nix) 1.11.13" `shouldBe` Right Nix1XX- it "parses 'nix-env (Nix) 2.0.5' as Nix201" $- parseNixVersion "nix-env (Nix) 2.0.5" `shouldBe` Right Nix201- it "parses 'nix-env (Nix) 2.0.1pre6053_444b921' as Nix201" $- parseNixVersion "nix-env (Nix) 2.0.1pre6053_444b921" `shouldBe` Right Nix201- it "fails with unknown string 'foobar'" $- parseNixVersion "foobar" `shouldBe` Left "Couldn't parse 'nix-env --version' output: foobar"+ it "parses 'nix-env (Nix) 2.0' as Nix20"+ $ parseNixVersion "nix-env (Nix) 2.0"+ `shouldBe` Left "Nix 2.0.2 or lower is not supported. Please upgrade: https://nixos.org/nix/"+ it "parses 'nix-env (Nix) 2.0.1' as Nix201"+ $ parseNixVersion "nix-env (Nix) 2.0.1"+ `shouldBe` Right ()+ it "parses 'nix-env (Nix) 1.11.13' as Nix1XX"+ $ parseNixVersion "nix-env (Nix) 1.11.13"+ `shouldBe` Left "Nix 2.0.2 or lower is not supported. Please upgrade: https://nixos.org/nix/"+ it "parses 'nix-env (Nix) 2.0.5' as Nix201"+ $ parseNixVersion "nix-env (Nix) 2.0.5"+ `shouldBe` Right ()+ it "parses 'nix-env (Nix) 2.0.1pre6053_444b921' as Nix201"+ $ parseNixVersion "nix-env (Nix) 2.0.1pre6053_444b921"+ `shouldBe` Right ()+ it "fails with unknown string 'foobar'"+ $ parseNixVersion "foobar"+ `shouldBe` Left "Couldn't parse 'nix-env --version' output: foobar"