cachix (empty) → 0.1.0.0
raw patch · 20 files changed
+1391/−0 lines, 20 filesdep +asyncdep +basedep +base16-bytestringsetup-changed
Dependencies added: async, base, base16-bytestring, base64-bytestring, bifunctors, bytestring, cachix, cachix-api, conduit, conduit-combinators, conduit-extra, cookie, cryptonite, dhall, directory, ed25519, fsnotify, here, hspec, http-client, http-client-tls, http-conduit, http-types, lzma-conduit, megaparsec, memory, mmorph, optparse-applicative, process, protolude, resourcet, servant, servant-auth, servant-auth-client, servant-client, servant-generic, servant-streaming-client, streaming, text, unix, uri-bytestring, versions
Files
- ChangeLog.md +13/−0
- LICENSE +13/−0
- README.md +1/−0
- Setup.hs +2/−0
- cachix.cabal +196/−0
- cachix/Main.hs +25/−0
- src/Cachix/Client.hs +41/−0
- src/Cachix/Client/Commands.hs +282/−0
- src/Cachix/Client/Config.hs +64/−0
- src/Cachix/Client/InstallationMode.hs +123/−0
- src/Cachix/Client/NixConf.hs +173/−0
- src/Cachix/Client/NixVersion.hs +37/−0
- src/Cachix/Client/OptionsParser.hs +85/−0
- src/Cachix/Client/Servant.hs +22/−0
- src/Cachix/Client/URI.hs +37/−0
- test/InstallationModeSpec.hs +94/−0
- test/Main.hs +13/−0
- test/NixConfSpec.hs +147/−0
- test/NixVersionSpec.hs +22/−0
- test/Spec.hs +1/−0
+ ChangeLog.md view
@@ -0,0 +1,13 @@+# Changelog++All notable changes to this project will be documented in this file.++The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)+and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).++## [Unreleased]++## [0.1.0.0] - 2018-07-01++### Added+- Initial release @domenkozar
+ LICENSE view
@@ -0,0 +1,13 @@+Copyright 2018 Domen Kožar++Licensed under the Apache License, Version 2.0 (the "License");+you may not use this file except in compliance with the License.+You may obtain a copy of the License at++ http://www.apache.org/licenses/LICENSE-2.0++Unless required by applicable law or agreed to in writing, software+distributed under the License is distributed on an "AS IS" BASIS,+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+See the License for the specific language governing permissions and+limitations under the License.
+ README.md view
@@ -0,0 +1,1 @@+https://github.com/cachix/cachix/blob/master/README.md
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ cachix.cabal view
@@ -0,0 +1,196 @@+-- This file has been generated from package.yaml by hpack version 0.21.2.+--+-- see: https://github.com/sol/hpack+--+-- hash: 4ecd761e07cb906fa23f7c69e728be19cfd4d7d067f5860ff640827b44f5dfaf++name: cachix+version: 0.1.0.0+description: Please see the README on Github at <https://github.com/cachix/cachix#readme>+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+cabal-version: >= 1.10++extra-source-files:+ ChangeLog.md+ README.md++source-repository head+ type: git+ location: https://github.com/cachix/cachix++library+ exposed-modules:+ Cachix.Client+ Cachix.Client.Commands+ Cachix.Client.Config+ Cachix.Client.InstallationMode+ Cachix.Client.NixConf+ Cachix.Client.NixVersion+ Cachix.Client.OptionsParser+ Cachix.Client.Servant+ Cachix.Client.URI+ other-modules:+ Paths_cachix+ hs-source-dirs:+ src+ default-extensions: OverloadedStrings NoImplicitPrelude 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+ build-depends:+ async+ , base >=4.7 && <5+ , base16-bytestring+ , base64-bytestring+ , bifunctors+ , bytestring+ , cachix-api+ , conduit+ , conduit-combinators+ , conduit-extra+ , cookie+ , cryptonite+ , dhall+ , directory+ , ed25519+ , fsnotify+ , here+ , http-client+ , http-client-tls+ , http-conduit+ , http-types+ , lzma-conduit+ , megaparsec+ , memory+ , mmorph+ , optparse-applicative+ , process+ , protolude+ , resourcet+ , servant >=0.14+ , servant-auth+ , servant-auth-client >=0.3.3.0+ , servant-client >=0.14+ , servant-generic+ , servant-streaming-client >=0.3.0.0+ , streaming+ , text+ , unix+ , uri-bytestring+ , versions+ default-language: Haskell2010++executable cachix+ main-is: Main.hs+ other-modules:+ Paths_cachix+ hs-source-dirs:+ cachix+ default-extensions: OverloadedStrings NoImplicitPrelude 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 -threaded -rtsopts -with-rtsopts=-N+ build-depends:+ async+ , base >=4.7 && <5+ , base16-bytestring+ , base64-bytestring+ , bifunctors+ , bytestring+ , cachix+ , cachix-api+ , conduit+ , conduit-combinators+ , conduit-extra+ , cookie+ , cryptonite+ , dhall+ , directory+ , ed25519+ , fsnotify+ , here+ , http-client+ , http-client-tls+ , http-conduit+ , http-types+ , lzma-conduit+ , megaparsec+ , memory+ , mmorph+ , optparse-applicative+ , process+ , protolude+ , resourcet+ , servant >=0.14+ , servant-auth+ , servant-auth-client >=0.3.3.0+ , servant-client >=0.14+ , servant-generic+ , servant-streaming-client >=0.3.0.0+ , streaming+ , text+ , unix+ , uri-bytestring+ , versions+ default-language: Haskell2010++test-suite cachix-test+ type: exitcode-stdio-1.0+ main-is: Main.hs+ other-modules:+ InstallationModeSpec+ NixConfSpec+ NixVersionSpec+ Spec+ Paths_cachix+ hs-source-dirs:+ test+ default-extensions: OverloadedStrings NoImplicitPrelude 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 -threaded -rtsopts -with-rtsopts=-N+ build-depends:+ async+ , base >=4.7 && <5+ , base16-bytestring+ , base64-bytestring+ , bifunctors+ , bytestring+ , cachix+ , cachix-api+ , conduit+ , conduit-combinators+ , conduit-extra+ , cookie+ , cryptonite+ , dhall+ , directory+ , ed25519+ , fsnotify+ , here+ , hspec+ , http-client+ , http-client-tls+ , http-conduit+ , http-types+ , lzma-conduit+ , megaparsec+ , memory+ , mmorph+ , optparse-applicative+ , process+ , protolude+ , resourcet+ , servant >=0.14+ , servant-auth+ , servant-auth-client >=0.3.3.0+ , servant-client >=0.14+ , servant-generic+ , servant-streaming-client >=0.3.0.0+ , streaming+ , text+ , unix+ , uri-bytestring+ , versions+ default-language: Haskell2010
+ cachix/Main.hs view
@@ -0,0 +1,25 @@+module Main (main) where++import Control.Exception (handle, SomeException)+import System.IO+import System.Exit (exitFailure)+import GHC.IO.Encoding++import qualified Cachix.Client as CC+++main :: IO ()+main = do+ setLocaleEncoding utf8+ hSetBuffering stdout LineBuffering+ hSetBuffering stderr LineBuffering+ handleExceptions CC.main++handleExceptions :: IO a -> IO a+handleExceptions = handle handler+ where+ handler :: SomeException -> IO a+ handler e = do+ hPutStrLn stderr ""+ hPrint stderr e+ exitFailure
+ src/Cachix/Client.hs view
@@ -0,0 +1,41 @@+module Cachix.Client+ ( main+ ) where++import Data.Version (showVersion)+import Paths_cachix (version)+import Protolude+import Network.HTTP.Simple ( setRequestHeader )+import Network.HTTP.Client.TLS ( newTlsManagerWith, tlsManagerSettings )+import Network.HTTP.Client ( managerResponseTimeout, managerModifyRequest+ , responseTimeoutNone, ManagerSettings)+import Servant.Client ( mkClientEnv)++import Cachix.Client.OptionsParser ( CachixCommand(..), CachixOptions(..), getOpts )+import Cachix.Client.Config ( readConfig )+import Cachix.Client.Commands as Commands+import Cachix.Client.URI ( getBaseUrl)+++main :: IO ()+main = do+ (CachixOptions{..}, command) <- getOpts+ config <- readConfig+ manager <- newTlsManagerWith customManagerSettings+ let env = mkClientEnv manager $ getBaseUrl host+ case command of -- TODO: might want readerT here with client, config and env and opts+ AuthToken token -> Commands.authtoken env config token+ Create name -> Commands.create env config name+ Push name paths watchStore -> Commands.push env config name paths watchStore+ Use name shouldEchoNixOS -> Commands.use env config name shouldEchoNixOS+ Version -> putText cachixVersion++customManagerSettings :: ManagerSettings+customManagerSettings = tlsManagerSettings+ { managerResponseTimeout = responseTimeoutNone+ -- managerModifyRequest :: Request -> IO Request+ , managerModifyRequest = \request -> return $ setRequestHeader "User-Agent" [toS cachixVersion] request+ }++cachixVersion :: Text+cachixVersion = "cachix " <> toS (showVersion version)
+ src/Cachix/Client/Commands.hs view
@@ -0,0 +1,282 @@+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE TypeOperators #-}+module Cachix.Client.Commands+ ( authtoken+ , create+ , push+ , use+ ) where++import Crypto.Sign.Ed25519+import Control.Concurrent (threadDelay)+import qualified Control.Concurrent.QSem as QSem+import Control.Concurrent.Async (mapConcurrently)+import Control.Monad (forever)+import Control.Monad.Trans.Resource (ResourceT)+import qualified Data.ByteString.Base64 as B64+import Data.Conduit+import Data.Conduit.Process+import qualified Data.Conduit.List as CL+import Data.Conduit.Lzma ( compress )+import Data.IORef+import Data.List ( isSuffixOf )+import Data.Maybe ( fromJust )+import Data.String.Here+import qualified Data.Text as T+import Network.HTTP.Types (status404)+import Protolude+import Servant.API+import Servant.Client+import Servant.Auth ()+import Servant.Auth.Client+import Servant.Streaming.Client ()+import Servant.Generic+import System.Directory ( doesFileExist )+import System.FSNotify+import System.IO ( stdin, hIsTerminalDevice )+import System.Process ( readProcessWithExitCode, readProcess )+import System.Environment ( lookupEnv )+import qualified Streaming.Prelude as S++import qualified Cachix.Api as Api+import Cachix.Api.Signing (fingerprint, passthroughSizeSink, passthroughHashSink)+import Cachix.Client.Config ( Config(..)+ , BinaryCacheConfig(..)+ , writeConfig+ , mkConfig+ )+import qualified Cachix.Client.Config as Config+import Cachix.Client.InstallationMode+import Cachix.Client.NixVersion ( getNixMode+ , NixMode+ )+import qualified Cachix.Client.NixConf as NixConf+import Cachix.Client.Servant+++cachixClient :: Api.CachixAPI AsClient+cachixClient = fromServant $ client Api.servantApi++cachixBCClient :: Text -> Api.BinaryCacheAPI AsClient+cachixBCClient name = fromServant $ Api.cache cachixClient name++authtoken :: ClientEnv -> Maybe Config -> Text -> IO ()+authtoken _ maybeConfig token = do+ -- TODO: check that token actually authenticates!+ writeConfig $ case maybeConfig of+ Just config -> config { authToken = token }+ Nothing -> mkConfig token+ putStrLn ([hereLit|+Continue by creating a binary cache with:++ $ cachix create <name>++and share it with others over https://<name>.cachix.org+ |] :: Text)++create :: ClientEnv -> Maybe Config -> Text -> IO ()+create _ Nothing _ = throwIO $ NoConfig "start with: $ cachix authtoken <token>"+create env (Just config@Config{..}) name = do+ (PublicKey pk, SecretKey sk) <- createKeypair++ let bc = Api.BinaryCacheCreate $ toS $ B64.encode pk+ res <- (`runClientM` env) $ Api.create (cachixBCClient name) (Token (toS authToken)) bc+ case res of+ -- TODO: handle all kinds of errors+ Left err -> panic $ show err+ Right _ -> do+ -- write signing key to config+ let signingKey = toS $ B64.encode sk+ bcc = BinaryCacheConfig name signingKey+ writeConfig $ config { binaryCaches = binaryCaches <> [bcc] }++ putStrLn ([iTrim|+Signing key has been saved on your local machine. To populate+your binary cache:++ $ nix-build | cachix push ${name}++Or if you'd like to use the signing key on another machine or CI:++ $ export CACHIX_SIGNING_KEY=${signingKey}+ $ nix-build | cachix push ${name}++To instruct Nix to use the binary cache:++ $ cachix use ${name}++ |] :: Text)++use :: ClientEnv -> Maybe Config -> Text -> Bool -> IO ()+use env _ name shouldEchoNixOS = do+ -- 1. get cache public key+ res <- (`runClientM` env) $ Api.get (cachixBCClient name)+ case res of+ -- TODO: handle 404+ Left err -> panic $ show err+ Right binaryCache -> do+ eitherNixMode <- getNixMode+ case eitherNixMode of+ Left err -> panic err+ Right nixMode -> do+ user <- getUser+ nc <- NixConf.read NixConf.Global+ isTrusted <- isTrustedUser $ NixConf.readLines (catMaybes [nc]) NixConf.isTrustedUsers+ isNixOS <- doesFileExist "/etc/NIXOS"+ let nixEnv = NixEnv+ { nixMode = nixMode+ , isRoot = user == "root"+ , isTrusted = isTrusted+ , isNixOS = isNixOS+ }+ addBinaryCache binaryCache $+ if shouldEchoNixOS+ then EchoNixOS+ else getInstallationMode nixEnv+++-- TODO: lots of room for perfomance improvements+push :: ClientEnv -> Maybe Config -> Text -> [Text] -> Bool -> IO ()+push env config name rawPaths False = do+ hasNoStdin <- hIsTerminalDevice stdin+ 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++ -- 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"++ -- Query list of paths+ -- TODO: split args if too big+ (exitcode, out, err) <- readProcessWithExitCode "nix-store" (fmap toS (["-qR"] <> inputStorePaths)) mempty++ -- TODO: make pool size configurable, on beefier machines this could be doubled+ _ <- mapConcurrentlyBounded 4 (pushStorePath env config name) (T.lines (toS out))+ putText "All done."+push env config name _ True = withManager $ \mgr -> do+ _ <- watchDir mgr "/nix/store" filterF action+ putText "Watching /nix/store for new builds ..."+ forever $ threadDelay 1000000+ where+ action :: Action+ action (Removed fp _) = pushStorePath env config name $ toS $ dropEnd 5 fp+ action _ = return ()++ filterF :: ActionPredicate+ filterF (Removed fp _) | ".lock" `isSuffixOf` fp = True+ filterF _ = False++ dropEnd :: Int -> [a] -> [a]+ dropEnd index xs = take (length xs - index) xs++pushStorePath :: ClientEnv -> Maybe Config -> Text -> Text -> IO ()+pushStorePath env config name storePath = do+ -- use secret key from config or env+ maybeEnvSK <- lookupEnv "CACHIX_SIGNING_KEY"+ let matches Config{..} = filter (\bc -> Config.name bc == name) binaryCaches+ maybeBCSK = case config of+ Nothing -> Nothing+ Just c -> Config.secretKey <$> head (matches c)+ -- TODO: better error msg+ sk = SecretKey $ toS $ B64.decodeLenient $ toS $ fromJust $ maybeBCSK <|> toS <$> maybeEnvSK <|> panic "You need to: export CACHIX_SIGNING_KEY=XXX"+ (storeHash, _) = splitStorePath $ toS storePath+ -- Check if narinfo already exists+ -- TODO: query also cache.nixos.org? server-side?+ res <- (`runClientM` env) $ Api.narinfoHead+ (cachixBCClient name)+ (Api.NarInfoC storeHash)+ case res of+ Right NoContent -> return ()+ Left err | isErr err status404 -> go sk+ | otherwise -> panic $ show err -- TODO: retry+ where+ go sk = do+ let (storeHash, storeSuffix) = splitStorePath $ toS storePath+ putStrLn $ "pushing " <> storePath++ narSizeRef <- liftIO $ newIORef 0+ fileSizeRef <- liftIO $ newIORef 0+ narHashRef <- liftIO $ newIORef ("" :: Text)+ fileHashRef <- liftIO $ newIORef ("" :: Text)++ -- stream store path as xz compressed nar file+ (ClosedStream, (source, close), ClosedStream, cph) <- streamingProcess $ shell ("nix-store --dump " <> toS storePath)+ let stream'+ = source+ .| passthroughSizeSink narSizeRef+ .| passthroughHashSink narHashRef+ .| compress Nothing+ .| passthroughSizeSink fileSizeRef+ .| passthroughHashSink fileHashRef++ conduitToStreaming :: S.Stream (S.Of ByteString) (ResourceT IO) ()+ conduitToStreaming = runConduit (transPipe lift stream' .| CL.mapM_ S.yield)+ -- for now we need to use letsencrypt domain instead of cloudflare due to its upload limits+ let newEnv = env {+ baseUrl = (baseUrl env) { baseUrlHost = toS name <> "." <> baseUrlHost (baseUrl env)}+ }+ -- TODO: http retry: retry package?+ res <- (`runClientM` newEnv) $ Api.createNar+ (cachixBCClient name)+ (contentType (Proxy :: Proxy Api.XNixNar), conduitToStreaming)+ close+ exitcode <- waitForStreamingProcess cph+ case res of+ Left err -> panic $ show err+ Right NoContent -> do+ 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++ (exitcode, out, err) <- readProcessWithExitCode "nix-store" ["-q", "--deriver", toS storePath] mempty+ let deriverRaw = T.strip $ toS out+ deriver = if deriverRaw == "unknown-deriver"+ then deriverRaw+ else T.drop 11 deriverRaw++ (exitcode, out, err) <- readProcessWithExitCode "nix-store" ["-q", "--references", toS storePath] mempty++ let+ references = sort $ T.lines $ T.strip $ toS out+ fp = fingerprint storePath narHash narSize references+ sig = dsign sk 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+ }+ -- Upload narinfo with signature+ res <- (`runClientM` env) $ Api.createNarinfo+ (cachixBCClient name)+ (Api.NarInfoC storeHash)+ nic+ case res of+ Left err -> panic $ show err -- TODO: handle json errors+ Right NoContent -> return ()+++-- Utils+++mapConcurrentlyBounded :: Traversable t => Int -> (a -> IO b) -> t a -> IO (t b)+mapConcurrentlyBounded bound action items = do+ qs <- QSem.newQSem bound+ let wrapped x = bracket_ (QSem.waitQSem qs) (QSem.signalQSem qs) (action x)+ mapConcurrently wrapped items++splitStorePath :: Text -> (Text, Text)+splitStorePath storePath =+ (T.take 32 (T.drop 11 storePath), T.drop 44 storePath)
+ src/Cachix/Client/Config.hs view
@@ -0,0 +1,64 @@+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE DeriveAnyClass #-}+module Cachix.Client.Config+ ( Config(..)+ , BinaryCacheConfig(..)+ , readConfig+ , writeConfig+ , mkConfig+ ) where++import Dhall hiding ( Text )+import Dhall.Pretty ( prettyExpr )+import GHC.Generics ( Generic )+import System.Directory ( doesFileExist, createDirectoryIfMissing+ , getXdgDirectory, XdgDirectory(..)+ )+import System.Posix.Files ( setFileMode, unionFileModes, ownerReadMode+ , ownerWriteMode )+import Protolude+++data BinaryCacheConfig = BinaryCacheConfig+ { name :: Text+ , secretKey :: Text+ } deriving (Show, Generic, Interpret, Inject)++data Config = Config+ { authToken :: Text+ , binaryCaches :: [BinaryCacheConfig]+ } deriving (Show, Generic, Interpret, Inject)++mkConfig :: Text -> Config+mkConfig authtoken = Config+ { authToken = authtoken+ , binaryCaches = []+ }++readConfig :: IO (Maybe Config)+readConfig = do+ filename <- getFilename+ doesExist <- doesFileExist filename+ if doesExist+ then Just <$> input auto (toS filename)+ else return Nothing+++getFilename :: IO FilePath+getFilename = do+ dir <- getXdgDirectory XdgConfig "cachix"+ createDirectoryIfMissing True dir+ return $ dir <> "/cachix.dhall"++writeConfig :: Config -> IO ()+writeConfig config = do+ filename <- getFilename+ let doc = prettyExpr $ embed inject config+ writeFile filename $ show doc+ assureFilePermissions filename+ putStrLn $ "Written to " <> filename++-- Does: chmod rw file+assureFilePermissions :: FilePath -> IO ()+assureFilePermissions fp =+ setFileMode fp $ unionFileModes ownerReadMode ownerWriteMode
+ src/Cachix/Client/InstallationMode.hs view
@@ -0,0 +1,123 @@+{-# LANGUAGE QuasiQuotes #-}+module Cachix.Client.InstallationMode+ ( InstallationMode(..)+ , NixEnv(..)+ , CachixException(..)+ , getInstallationMode+ , addBinaryCache+ , isTrustedUser+ , getUser+ ) where++import Protolude+import Data.String.Here+import qualified Data.Text as T+import qualified Cachix.Client.NixConf as NixConf+import Cachix.Client.NixVersion ( NixMode(..) )+import Cachix.Api as Api+import System.Directory ( getPermissions, writable )+import System.Environment ( lookupEnv )++data CachixException+ = UnsupportedNixVersion Text+ | UserEnvNotSet Text+ | MustBeRoot Text+ | NixOSInstructions Text+ | AmbiguousInput Text+ | NoInput Text+ | NoConfig Text+ deriving (Show, Typeable)++instance Exception CachixException++data NixEnv = NixEnv+ { nixMode :: NixMode+ , isTrusted :: Bool+ , isRoot :: Bool+ , isNixOS :: Bool+ }++data InstallationMode+ = Install NixConf.NixConfLoc+ | UnsupportedNix1X+ | EchoNixOS+ | EchoNixOSWithTrustedUser+ | UntrustedRequiresSudo+ | Nix20RequiresSudo+ deriving (Show, Eq)++getInstallationMode :: NixEnv -> InstallationMode+getInstallationMode NixEnv{..}+ | nixMode == Nix1XX = UnsupportedNix1X+ | isNixOS && isRoot = EchoNixOS+ | isNixOS && (not isTrusted) = EchoNixOSWithTrustedUser+ | (not isNixOS) && isRoot = Install NixConf.Global+ | nixMode == Nix20 = Nix20RequiresSudo+ | isTrusted = Install NixConf.Local+ | not isTrusted = UntrustedRequiresSudo+++-- | Add a Binary cache to nix.conf, print nixos config or fail+addBinaryCache :: Api.BinaryCache -> InstallationMode -> IO ()+addBinaryCache _ UnsupportedNix1X = throwIO $+ UnsupportedNixVersion "Nix 1.x is not supported, please upgrade to Nix 2.0.1 or greater"+addBinaryCache Api.BinaryCache{..} EchoNixOS = do+ putText [iTrim|+nix = {+ binaryCaches = [+ "${uri}"+ ];+ binaryCachePublicKeys = [+ ${T.intercalate " " (map (\s -> "\"" <> s <> "\"") publicSigningKeys)}+ ];+};+ |]+ throwIO $ NixOSInstructions "Add above lines to your NixOS configuration file"+addBinaryCache Api.BinaryCache{..} EchoNixOSWithTrustedUser = do+ -- TODO: DRY+ user <- getUser+ putText [iTrim|+nix = {+ binaryCaches = [+ "${uri}"+ ];+ binaryCachePublicKeys = [+ ${T.intercalate " " (map (\s -> "\"" <> s <> "\"") publicSigningKeys)}+ ];+ trustedUsers = [ "root" "${user}" ];+};+ |]+ throwIO $ NixOSInstructions "Add above lines to your NixOS configuration file"+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 bc@Api.BinaryCache{..} (Install ncl) = do+ -- TODO: might need locking one day+ gnc <- NixConf.read NixConf.Global+ lnc <- NixConf.read NixConf.Local+ let final = if ncl == NixConf.Global then gnc else lnc+ input = if ncl == NixConf.Global then [gnc] else [gnc, lnc]+ NixConf.write ncl $ NixConf.add bc (catMaybes input) (fromMaybe (NixConf.NixConf []) final)+ filename <- NixConf.getFilename ncl+ putStrLn $ "Configured " <> uri <> " binary cache in " <> toS filename++isTrustedUser :: [Text] -> IO Bool+isTrustedUser users = do+ user <- getUser+ -- to detect single user installations+ permissions <- getPermissions "/nix/store"+ unless (groups == []) $ do+ -- TODO: support Nix group syntax+ putText "Warn: cachix doesn't yet support checking if user is trusted via groups, so it will recommend sudo"+ putStrLn $ "Warn: groups found " <> T.intercalate "," groups+ return $ (writable permissions) || (user `elem` users)+ where+ groups = filter (\u -> T.head u == '@') users++getUser :: IO Text+getUser = do+ maybeUser <- lookupEnv "USER"+ case maybeUser of+ Nothing -> throwIO $ UserEnvNotSet "$USER must be set"+ Just user -> return $ toS user
+ src/Cachix/Client/NixConf.hs view
@@ -0,0 +1,173 @@+{- (Very limited) parser, rendered and modifier of nix.conf+-}+{-# 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+ ) where++import Control.Exception (catch)+import Data.Char (isSpace)+import Data.Text (unwords, unlines)+import Data.List (nub)+import Protolude+import qualified Text.Megaparsec as Mega+import Text.Megaparsec.Char+import System.Directory ( doesFileExist, createDirectoryIfMissing+ , getXdgDirectory, XdgDirectory(..)+ )+import Cachix.Api (BinaryCache(..))+++data NixConfLine+ = Substituters [Text]+ | TrustedUsers [Text]+ | TrustedPublicKeys [Text]+ | BinaryCachePublicKeys [Text]+ | BinaryCaches [Text]+ | Other Text+ deriving (Show, Eq)++newtype NixConfG a = NixConf a+ deriving (Show, Eq, Functor)++type NixConf = NixConfG [NixConfLine]++readLines :: [NixConf] -> (NixConfLine -> Maybe [Text]) -> [Text]+readLines nixconfs predicate = concat $ fmap f nixconfs+ where+ f (NixConf xs) = foldl foldIt [] xs++ foldIt :: [Text] -> NixConfLine -> [Text]+ foldIt prev new = prev <> (fromMaybe [] $ predicate new)++writeLines :: (NixConfLine -> Maybe [Text]) -> NixConfLine -> NixConf -> NixConf+writeLines predicate addition nixconf = fmap f nixconf+ where+ f x = filter (isNothing . predicate) x <> [addition]++isSubstituter :: NixConfLine -> Maybe [Text]+isSubstituter (Substituters xs) = Just xs+isSubstituter (BinaryCaches xs) = Just xs+isSubstituter _ = Nothing++isPublicKey :: NixConfLine -> Maybe [Text]+isPublicKey (TrustedPublicKeys xs) = Just xs+isPublicKey (BinaryCachePublicKeys xs) = Just xs+isPublicKey _ = Nothing++isTrustedUsers :: NixConfLine -> Maybe [Text]+isTrustedUsers (TrustedUsers xs) = Just xs+isTrustedUsers _ = Nothing++-- | Pure version of addIO+add :: BinaryCache -> [NixConf] -> NixConf -> NixConf+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]+ publicKeys = (defaultSigningKey : readLines toRead isPublicKey) <> publicSigningKeys++defaultPublicURI :: Text+defaultPublicURI = "https://cache.nixos.org"+defaultSigningKey :: Text+defaultSigningKey = "cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY="++render :: NixConf -> Text+render (NixConf nixconflines) = unlines $ fmap go nixconflines+ where+ go :: NixConfLine -> Text+ go (Substituters xs) = "substituters = " <> unwords xs+ go (BinaryCaches xs) = "substituters = " <> unwords xs+ go (TrustedUsers xs) = "trusted-users = " <> unwords xs+ go (TrustedPublicKeys xs) = "trusted-public-keys = " <> unwords xs+ go (BinaryCachePublicKeys xs) = "trusted-public-keys = " <> unwords xs+ go (Other line) = line++write :: NixConfLoc -> NixConf -> IO ()+write ncl nc = do+ filename <- getFilename ncl+ 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.parseErrorPretty err)+ panic $ toS filename <> " failed to parse, please copy the above error and contents of nix.conf and open an issue at https://github.com/cachix/cachix"+ Right conf -> return $ Just conf++update :: NixConfLoc -> (Maybe NixConf -> NixConf) -> IO ()+update ncl f = do+ nc <- f <$> read ncl+ write ncl nc++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"+ _ <- catch (createDirectoryIfMissing True dir) $ \e ->+ hPutStr stderr ("Warning: Couldn't create " <> dir <> " :" <> show (e :: IOException))+ 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 (satisfy (not . isSpace))) (some (char ' '))+ _ <- many spaceChar+ return $ constr (fmap toS values)++parseOther :: Parser NixConfLine+parseOther = Other . toS <$> Mega.manyTill anyChar eol++parseAltLine :: Parser NixConfLine+parseAltLine =+ parseLine Substituters "substituters"+ <|> parseLine TrustedPublicKeys "trusted-public-keys"+ <|> parseLine TrustedUsers "trusted-users"+ <|> parseLine BinaryCachePublicKeys "binary-cache-public-keys"+ <|> parseLine BinaryCaches "binary-caches"+ <|> parseOther++parser :: Parser NixConf+parser = NixConf <$> many parseAltLine++parse :: Text -> Either (Mega.ParseError (Mega.Token Text) Void) NixConf+parse = Mega.parse parser "nix.conf"
+ src/Cachix/Client/NixVersion.hs view
@@ -0,0 +1,37 @@+module Cachix.Client.NixVersion+ ( getNixMode+ , parseNixMode+ , NixMode(..)+ ) where++import Protolude+import System.Process (readProcessWithExitCode)+import Data.Versions+import Data.Text as T+++data NixMode+ = Nix20+ | Nix201+ | Nix1XX+ deriving (Show, Eq)++getNixMode :: IO (Either Text NixMode)+getNixMode = 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 -> parseNixMode $ toS out++parseNixMode :: Text -> Either Text NixMode+parseNixMode 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
+ src/Cachix/Client/OptionsParser.hs view
@@ -0,0 +1,85 @@+{-# LANGUAGE QuasiQuotes #-}+module Cachix.Client.OptionsParser+ ( CachixCommand(..)+ , CachixOptions(..)+ , BinaryCacheName+ , getOpts+ ) where++import Data.Bifunctor (first)+import Protolude hiding (option)+import URI.ByteString (URIRef, Absolute, parseURI, strictURIParserOptions+ , serializeURIRef')+import URI.ByteString.QQ+import Options.Applicative+++data CachixOptions = CachixOptions+ { host :: URIRef Absolute+ , verbose :: Bool+ } deriving Show++parserCachixOptions :: Parser CachixOptions+parserCachixOptions = CachixOptions+ <$> option uriOption ( long "host"+ <> short 'h'+ <> value [uri|https://cachix.org|]+ <> metavar "URI"+ <> showDefaultWith (toS . serializeURIRef')+ <> help "Host to connect to"+ )+ <*> switch ( long "verbose"+ <> short 'v'+ <> help "Verbose mode"+ )++uriOption :: ReadM (URIRef Absolute)+uriOption = eitherReader $ \s ->+ first show $ parseURI strictURIParserOptions $ toS s++type BinaryCacheName = Text++data CachixCommand+ = AuthToken Text+ | Create BinaryCacheName+ | Push BinaryCacheName [Text] Bool -- TODO: refactor to a record+ | Use BinaryCacheName Bool --- TODO: refactor to a record+ | Version+ deriving Show+++parserCachixCommand :: Parser CachixCommand+parserCachixCommand = subparser $+ command "authtoken" (infoH authtoken (progDesc "Configure token for authentication to cachix.org")) <>+ command "create" (infoH create (progDesc "Create a new 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+ authtoken = AuthToken <$> strArgument (metavar "TOKEN")+ create = Create <$> strArgument (metavar "NAME")+ push = Push <$> strArgument (metavar "NAME")+ <*> many (strArgument (metavar "PATHS..."))+ <*> switch (long "watch-store" <> short 'w' <> help "Run in daemon mode and push store paths as they are added to /nix/store")+ use = Use <$> strArgument (metavar "NAME")+ <*> switch (long "nixos" <> short 'n' <> help "Output NixOS configuration lines")++getOpts :: IO (CachixOptions, CachixCommand)+getOpts = customExecParser (prefs showHelpOnEmpty) opts++opts :: ParserInfo (CachixOptions, CachixCommand)+opts = infoH parser desc+ where parser = (,) <$> parserCachixOptions <*> (parserCachixCommand <|> versionParser)+ versionParser :: Parser CachixCommand+ versionParser = flag' Version ( long "version"+ <> short 'V'+ <> help "Show cachix version"+ )++desc :: InfoMod a+desc = fullDesc+ <> progDesc "Sign into https://cachix.org to get started."+ <> header "cachix.org command interface"+ -- TODO: usage footer++infoH :: Parser a -> InfoMod a -> ParserInfo a+infoH a = info (helper <*> a)
+ src/Cachix/Client/Servant.hs view
@@ -0,0 +1,22 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE TypeOperators #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE UndecidableInstances #-}++module Cachix.Client.Servant+ ( AsClient+ , isErr+ ) where++import Protolude+import Network.HTTP.Types (Status)+import Servant.Client+import Servant.Generic++-- TODO: servant-generic-client https://github.com/chpatrick/servant-generic/issues/10+data AsClient+type instance AsClient :- api = Client ClientM api++isErr :: ServantError -> Status -> Bool+isErr (FailureResponse resp) status | responseStatusCode resp == status = True+isErr _ _ = False
+ src/Cachix/Client/URI.hs view
@@ -0,0 +1,37 @@+-- | Ugly glue between URI and BaseUrl+-- | TODO: mark as Internal module+{-# LANGUAGE GADTs #-}+module Cachix.Client.URI+ ( getBaseUrl+ ) where++import Protolude+import qualified URI.ByteString as UBS+import URI.ByteString hiding (Scheme)+import Servant.Client+++getBaseUrl :: URIRef Absolute -> BaseUrl+getBaseUrl URI{..} =+ case uriAuthority of+ Nothing -> panic "missing host in url"+ Just authority ->+ BaseUrl+ getScheme+ (toS (hostBS (authorityHost authority)))+ getPort+ (toS uriPath)+ where+ getScheme :: Scheme+ getScheme = case uriScheme of+ 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+ Https -> 443
+ test/InstallationModeSpec.hs view
@@ -0,0 +1,94 @@+module InstallationModeSpec where++import Protolude+import Test.Hspec++import qualified Cachix.Client.NixConf as NixConf+import Cachix.Client.NixVersion ( NixMode(..) )+import Cachix.Client.InstallationMode+++spec :: Spec+spec = do+ describe "getInstallationMode" $ do+ it "Nix 1.X is unsupported" $+ let+ nixenv = NixEnv+ { nixMode = Nix1XX+ , isTrusted = False -- any+ , isRoot = False -- any+ , isNixOS = False -- any+ }+ in getInstallationMode nixenv `shouldBe` UnsupportedNix1X+ it "NixOS with root prints configuration" $+ let+ nixenv = NixEnv+ { nixMode = Nix20 -- any except 1.0+ , isTrusted = True -- any+ , isRoot = True+ , isNixOS = True+ }+ in getInstallationMode nixenv `shouldBe` EchoNixOS+ it "NixOS without trust prints configuration plus trust" $+ let+ nixenv = NixEnv+ { nixMode = Nix20+ , isTrusted = False+ , isRoot = False+ , isNixOS = True+ }+ in getInstallationMode nixenv `shouldBe` EchoNixOSWithTrustedUser+ it "NixOS non-root trusted results into local install" $+ let+ nixenv = NixEnv+ { nixMode = Nix201+ , isTrusted = True+ , isRoot = False+ , isNixOS = True+ }+ in getInstallationMode nixenv `shouldBe` Install NixConf.Local+ it "non-NixOS root results into global install" $+ let+ nixenv = NixEnv+ { nixMode = Nix201+ , isTrusted = True+ , isRoot = True+ , isNixOS = False+ }+ in getInstallationMode nixenv `shouldBe` Install NixConf.Global+ it "non-NixOS non-root trusted results into local install" $+ let+ nixenv = NixEnv+ { nixMode = Nix201+ , 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+ { nixMode = 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+ { nixMode = 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+ { nixMode = Nix20+ , isTrusted = True+ , isRoot = False+ , isNixOS = False+ }+ in getInstallationMode nixenv `shouldBe` Nix20RequiresSudo
+ test/Main.hs view
@@ -0,0 +1,13 @@+module Main where++import Protolude+import Test.Hspec.Runner+import Test.Hspec.Formatters+import qualified Spec++main :: IO ()+main = hspecWith config Spec.spec+ where+ config = defaultConfig+ { configColorMode = ColorAlways+ }
+ test/NixConfSpec.hs view
@@ -0,0 +1,147 @@+{-# LANGUAGE QuasiQuotes #-}+module NixConfSpec where++import Protolude+import Data.String.Here+import Test.Hspec++import Cachix.Client.NixConf as NixConf+import Cachix.Api (BinaryCache(..))+++property :: Text -> Expectation+property x = NixConf.render <$> parse x `shouldBe` Right x++bc = BinaryCache+ { name = "name"+ , uri = "https://name.cachix.org"+ , publicSigningKeys = ["pub"]+ }++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 "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"]+ , BinaryCaches ["bc1"]+ , TrustedPublicKeys ["pub1"]+ , BinaryCachePublicKeys ["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 "can mix and match old and new names" $+ let+ globalConf = NixConf+ [ BinaryCaches ["bc1"]+ , BinaryCachePublicKeys ["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 "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 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 = [hereLit|+substituters = a b c+trusted-users = him me+trusted-public-keys = a+blabla = asd+# comment+++|]
+ test/NixVersionSpec.hs view
@@ -0,0 +1,22 @@+module NixVersionSpec where++import Protolude+import Test.Hspec++import Cachix.Client.NixVersion++spec :: Spec+spec = do+ describe "parseNixMode" $ do+ it "parses 'nix-env (Nix) 2.0' as Nix20" $+ parseNixMode "nix-env (Nix) 2.0" `shouldBe` Right Nix20+ it "parses 'nix-env (Nix) 2.0.1' as Nix201" $+ parseNixMode "nix-env (Nix) 2.0.1" `shouldBe` Right Nix201+ it "parses 'nix-env (Nix) 1.11.13' as Nix1XX" $+ parseNixMode "nix-env (Nix) 1.11.13" `shouldBe` Right Nix1XX+ it "parses 'nix-env (Nix) 2.0.5' as Nix201" $+ parseNixMode "nix-env (Nix) 2.0.5" `shouldBe` Right Nix201+ it "parses 'nix-env (Nix) 2.0.1pre6053_444b921' as Nix201" $+ parseNixMode "nix-env (Nix) 2.0.1pre6053_444b921" `shouldBe` Right Nix201+ it "fails with unknown string 'foobar'" $+ parseNixMode "foobar" `shouldBe` Left "Couldn't parse 'nix-env --version' output: foobar"
+ test/Spec.hs view
@@ -0,0 +1,1 @@+{-# OPTIONS_GHC -F -pgmF hspec-discover -optF --module-name=Spec #-}