packages feed

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 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"