libnix 0.4.0.1 → 0.4.1.0
raw patch · 7 files changed
+145/−29 lines, 7 filesPVP: major bump suggested
API removals or changes: PVP suggests a major version bump
API changes (from Hackage documentation)
- Foreign.Nix.Shellout.Types: newtype RunOptions m
+ Foreign.Nix.Shellout.Types: Executables :: Maybe FilePath -> Maybe FilePath -> Maybe FilePath -> Maybe FilePath -> Executables
+ Foreign.Nix.Shellout.Types: [exeNixInstantiate] :: Executables -> Maybe FilePath
+ Foreign.Nix.Shellout.Types: [exeNixPrefetchGit] :: Executables -> Maybe FilePath
+ Foreign.Nix.Shellout.Types: [exeNixPrefetchUrl] :: Executables -> Maybe FilePath
+ Foreign.Nix.Shellout.Types: [exeNixStore] :: Executables -> Maybe FilePath
+ Foreign.Nix.Shellout.Types: [executables] :: RunOptions m -> Executables
+ Foreign.Nix.Shellout.Types: data Executables
+ Foreign.Nix.Shellout.Types: data RunOptions m
+ Foreign.Nix.Shellout.Types: defaultExecutables :: Executables
- Foreign.Nix.Shellout.Types: RunOptions :: LogFn m -> RunOptions m
+ Foreign.Nix.Shellout.Types: RunOptions :: LogFn m -> Executables -> RunOptions m
Files
- CHANGELOG.md +4/−0
- Foreign/Nix/Shellout.hs +24/−9
- Foreign/Nix/Shellout/Helpers.hs +26/−5
- Foreign/Nix/Shellout/Prefetch.hs +11/−8
- Foreign/Nix/Shellout/Types.hs +37/−3
- libnix.cabal +1/−1
- tests/TestShellout.hs +42/−3
CHANGELOG.md view
@@ -1,5 +1,9 @@ # libnix +## 0.4.1.0 -- 2021-11-23++* Add `executables` to the `RunOptions`. Backwards compatible if you use `defaultRunOptions`.+ ## 0.4.0.1 -- 2021-11-19 * Fix `Prefetch.url`. The constructed command was missing flags for `urlUnpack = True`.
Foreign/Nix/Shellout.hs view
@@ -58,17 +58,20 @@ deriving (Show, Eq) -- | Parse a nix expression and check for syntactic validity.++-- Runs @nix-instantiate@. parseNixExpr :: MonadIO m => Text -> NixAction ParseError m NixExpr-parseNixExpr e =+parseNixExpr e = do+ exec <- Helpers.getExecOr exeNixInstantiate "nix-instantiate" mapActionError parseParseError $ NixExpr- <$> evalNixOutput "nix-instantiate" [ "--parse", "-E", e ]+ <$> evalNixOutput exec [ "--parse", "-E", e ] parseParseError :: Text -> ParseError parseParseError (stripPrefix "error: syntax error, "- -> Just mes) = SyntaxError $ mes+ -> Just mes) = SyntaxError mes parseParseError _ = UnknownParseError ------------------------------------------------------------------------------@@ -82,18 +85,25 @@ deriving (Show, Eq) -- | Instantiate a parsed expression into a derivation.+--+-- Runs @nix-instantiate@. instantiate :: (MonadIO m) => NixExpr -> NixAction InstantiateError m (StorePath Derivation)-instantiate (NixExpr e) =+instantiate (NixExpr e) = do+ exec <- Helpers.getExecOr exeNixInstantiate "nix-instantiate" mapActionError parseInstantiateError- $ evalNixOutput "nix-instantiate" [ "-E", e ]+ $ evalNixOutput exec [ "-E", e ] >>= toNixFilePath StorePath -- | Just tests if the expression can be evaluated. -- That doesn’t mean it has to instantiate however.+--+-- Runs @nix-instantiate@. eval :: MonadIO m => NixExpr -> NixAction InstantiateError m () eval (NixExpr e) = do+ exec <- Helpers.getExecOr exeNixInstantiate "nix-instantiate"+ _instantiateOutput <- mapActionError parseInstantiateError- $ evalNixOutput "nix-instantiate" [ "--eval", "-E", e ]+ $ evalNixOutput exec [ "--eval", "-E", e ] pure () parseInstantiateError :: Text -> InstantiateError@@ -110,18 +120,23 @@ -- | Finally derivations are realized into full store outputs. -- This will typically take a while so it should be executed asynchronously.+--+-- Runs @nix-store@. realize :: MonadIO m => StorePath Derivation -> NixAction RealizeError m (StorePath Realized) realize (StorePath d) = storeOp [ "-r", Text.pack d ] -- | Copy the given file or folder to the nix store and return it’s path.+--+-- Runs @nix-store@. addToStore :: MonadIO m => FilePath -> NixAction RealizeError m (StorePath Realized) addToStore fp = storeOp [ "--add", Text.pack fp ] storeOp :: (MonadIO m) => [Text] -> NixAction RealizeError m (StorePath Realized)-storeOp op =+storeOp op = do+ exec <- Helpers.getExecOr exeNixInstantiate "nix-store" mapActionError (const UnknownRealizeError)- $ evalNixOutput "nix-store" op+ $ evalNixOutput exec op >>= toNixFilePath StorePath ------------------------------------------------------------------------------@@ -145,7 +160,7 @@ -- | Take args and return either error message or output path evalNixOutput :: (MonadIO m)- => Text+ => Helpers.Executable -- ^ name of executable -> [Text] -- ^ arguments
Foreign/Nix/Shellout/Helpers.hs view
@@ -3,7 +3,7 @@ {-# LANGUAGE FlexibleContexts #-} module Foreign.Nix.Shellout.Helpers where -import Foreign.Nix.Shellout.Types ( NixActionError(..), RunOptions (logFn), LogFn (LogFn), NixAction )+import Foreign.Nix.Shellout.Types ( NixActionError(..), RunOptions (logFn, executables), LogFn (LogFn), NixAction, Executables ) import qualified System.Process as P import qualified Data.Text.IO as TIO import qualified Data.Text as T@@ -26,26 +26,47 @@ import Control.Monad.Except (MonadError (throwError)) import Control.Monad.Reader (asks) import Control.Monad.Trans (lift)+import Data.Function ((&)) +-- | Something we can run+data Executable =+ ExeFromPathEnv Text+ -- ^ name of the executable, to be looked up in PATH+ | ExeFromFilePath FilePath+ -- ^ a file path to the executable (can be relative or absolute)++-- | Get an executable from the 'Executables' option (by its getter)+-- or if not set use the given 'Text' as the name of the excutable+-- to be looked up in @PATH@.+getExecOr :: Monad m => (Executables -> Maybe FilePath) -> Text -> NixAction e m Executable+getExecOr getter exeName =+ let f = \case+ Nothing -> ExeFromPathEnv exeName+ Just fp -> ExeFromFilePath fp+ in asks (f . getter . executables)+ -- | Read the output of a process into a NixAction. -- | Keeps stderr if process returns a failure exit code. -- | The text is decoded as @UTF-8@. readProcess :: (MonadIO m) => ((Text, Text) -> ExitCode -> ExceptT e m a) -- ^ handle (stdout, stderr) depending on the return value- -> Text- -- ^ name of executable+ -> Executable+ -- ^ executable to run -> [Text] -- ^ arguments -> NixAction e m a readProcess with exec args = do+ let exec' = case exec of+ ExeFromPathEnv name -> name+ ExeFromFilePath fp -> fp & Text.pack -- log every call based on the LogFn the user passed (LogFn l) <- asks logFn- lift $ l exec args+ lift $ l exec' args (exc, out, err) <- liftIO $ readCreateProcessWithExitCodeAndEncoding- (P.proc (Text.unpack exec) (map Text.unpack args)) SIO.utf8 ""+ (P.proc (Text.unpack exec') (map Text.unpack args)) SIO.utf8 "" lift (runExceptT (with (out, err) exc)) >>= \case Left e -> throwError $ NixActionError
Foreign/Nix/Shellout/Prefetch.hs view
@@ -40,6 +40,7 @@ import qualified Data.Text.Lazy.Encoding as Text.Lazy.Encoding import qualified Data.List as List import Control.Monad.IO.Class (MonadIO)+import Foreign.Nix.Shellout.Helpers (getExecOr) data PrefetchError = PrefetchOutputMalformed Text@@ -76,9 +77,10 @@ -- | Runs @nix-prefetch-url@. url :: (MonadIO m) => UrlOptions -> NixAction PrefetchError m (Sha256, StorePath Realized)-url UrlOptions{..} = Helpers.readProcess handler exec args+url UrlOptions{..} = do+ exec <- Helpers.getExecOr exeNixPrefetchUrl "nix-prefetch-url"+ Helpers.readProcess handler exec args where- exec = "nix-prefetch-url" args = bool [] ["--unpack"] urlUnpack <> maybe [] (\n -> ["--name", n]) urlName <> [ "--type", "sha256"@@ -89,8 +91,8 @@ handler (out, err) = \case ExitSuccess -> withExceptT PrefetchOutputMalformed $ do let ls = T.lines $ T.stripEnd out- path <- tryLast (exec <> " didn’t output a store path") ls- sha <- let errS = (exec <> " didn’t output a hash")+ path <- tryLast "nix-prefetch-url didn’t output a store path" ls+ sha <- let errS = "nix-prefetch-url didn’t output a hash" in tryInit errS ls >>= tryLast errS pure (Sha256 sha, StorePath $ Text.unpack path) ExitFailure _ -> throwE $@@ -137,9 +139,10 @@ -- | Runs @nix-prefetch-git@. git :: (MonadIO m) => GitOptions -> NixAction PrefetchError m GitOutput-git GitOptions{..} = Helpers.readProcess handler exec args+git GitOptions{..} = do+ exec <- getExecOr exeNixPrefetchGit "nix-prefetch-git"+ Helpers.readProcess handler exec args where- exec = "nix-prefetch-git" args = bool ["--no-deepClone"] ["--deepClone"] gitDeepClone <> bool [] ["--leave-dotGit"] gitLeaveDotGit <> bool [] ["--fetch-submodules"] gitFetchSubmodules@@ -153,7 +156,7 @@ handler (out, err) = \case ExitSuccess -> withExceptT PrefetchOutputMalformed $ do- let error' msg = exec <> " " <> msg+ let error' msg = "nix-prefetch-git " <> msg jsonError :: [Char] -> Text jsonError = \msg -> error' (T.intercalate "\n" [ "parsing json output failed:"@@ -181,6 +184,6 @@ pure GitOutput{..} ExitFailure _ -> throwE $- if ("hash mismatch for URL" `T.isInfixOf` err)+ if "hash mismatch for URL" `T.isInfixOf` err then ExpectedHashError else UnknownPrefetchError
Foreign/Nix/Shellout/Types.hs view
@@ -16,6 +16,8 @@ RunOptions(..), defaultRunOptions, LogFn(..),+ Executables(..),+ defaultExecutables, NixActionError(..), mapActionError, @@ -32,9 +34,11 @@ -- -- Might get more fields in the future, use 'defaultRunOptions' -- to be backwards-compatbile.-newtype RunOptions m = RunOptions {- logFn :: LogFn m+data RunOptions m = RunOptions {+ logFn :: LogFn m, -- ^ The command line logging function.+ executables :: Executables+ -- ^ A record of all executables this library could need. } -- | Logging function to call before running a command.@@ -44,13 +48,43 @@ -- the second argument is the list of arguments. newtype LogFn m = LogFn (Text -> [Text] -> m ()) +-- | All executables this library might need.+--+-- If you set an executable to @Just filepath@, the internal code will use the given path instead of looking up the executable name in @PATH@.+-- This is useful if you want to ensure that the executables always exist before calling into this library.+--+-- 'NixAction' functions document the executables they use in their docstrings.+--+-- If an executable can’t be found, an 'IOException' is thrown (by the process spawn function).+data Executables = Executables {+ exeNixInstantiate :: Maybe FilePath,+ -- ^ @nix-instantiate@; usually you can expect nix to exist on the system (but we don’t check before running the commands)+ exeNixStore :: Maybe FilePath,+ -- ^ @nix-store@+ exeNixPrefetchUrl :: Maybe FilePath,+ -- ^ @nix-prefetch-url@; as of nix @2.3@, this executable comes with the nix distribution, so if @nix-store@ is available, this should also be available+ exeNixPrefetchGit :: Maybe FilePath+ -- ^ @nix-prefetch-git@; This is usually provided by the @nix-prefetch-scripts@ package and /not/ installed with nix+ }++-- | all executables are taken from @PATH@+defaultExecutables :: Executables+defaultExecutables = Executables {+ exeNixInstantiate = Nothing,+ exeNixStore = Nothing,+ exeNixPrefetchUrl = Nothing,+ exeNixPrefetchGit = Nothing+ }+ -- | -- @ -- logFn = nothing is done/logged+-- executable = all executables are taken from @PATH@ -- @ defaultRunOptions :: Monad m => RunOptions m defaultRunOptions = RunOptions {- logFn = LogFn (\_prog _args -> pure ())+ logFn = LogFn (\_prog _args -> pure ()),+ executables = defaultExecutables } -- | Calls a command that returns an error and the whole stderr on failure.
libnix.cabal view
@@ -1,5 +1,5 @@ name: libnix-version: 0.4.0.1+version: 0.4.1.0 synopsis: Bindings to the nix package manager description: Use the nix package manager from Haskell. All modules are designed to be imported qualified. category: Foreign, Nix
tests/TestShellout.hs view
@@ -1,5 +1,8 @@ {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE NamedFieldPuns #-}+{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE LambdaCase #-} module TestShellout where import qualified Data.Text as T@@ -13,6 +16,9 @@ import Data.Text (Text) import Control.Monad.IO.Class (liftIO) import Control.Monad ((>=>))+import Control.Exception (SomeException, try, Exception (displayException))+import Data.Function ((&))+import qualified Data.List as List shelloutTests :: TestTree@@ -28,31 +34,37 @@ , multilineErrors , helloWorld , copyTempfileToStore ]+ , testGroup "executable paths"+ [ badInstantiateFilePath ] ] -syntaxError, infiniteRecursion, notADerivation, someDerivation :: TestTree-nixpkgsExists, multilineErrors, helloWorld, copyTempfileToStore :: TestTree +syntaxError :: TestTree syntaxError = testCase "syntax error" $ parseNixExpr ";" `isENoFail` Left ("", SyntaxError "unexpected ';', at (string):1:1") +infiniteRecursion :: TestTree infiniteRecursion = testCase "infinite recursion" $ parseInst "let a = a; in a" `isE` Left ( "error: infinite recursion encountered, at (string):1:10" , UnknownInstantiateError) +notADerivation :: TestTree notADerivation = testCase "not a derivation" $ parseInst "42" `isE` Left ("", NotADerivation) +someDerivation :: TestTree someDerivation = testCase "a basic derivation" $ assertNoFailure $ parseInst "derivation { name = \"foo\"; builder = \" \"; system = \" \"; }" +nixpkgsExists :: TestTree nixpkgsExists = testCase "nixpkgs is accessible" $ assertNoFailure $ parseEval "import <nixpkgs> {}" +multilineErrors :: TestTree multilineErrors = testCase "nixpkgs multiline stderr it parsed" $ parseEval "builtins.abort ''wow\nsuch error''" `isE` Left@@ -60,9 +72,11 @@ \message: 'wow\nsuch error'" , UnknownInstantiateError) +helloWorld :: TestTree helloWorld = testCase "build the GNU hello package" $ assertNoFailure $ parseInstRealize "with import <nixpkgs> {}; hello" +copyTempfileToStore :: TestTree copyTempfileToStore = testCase "copy a temporary file to store" $ assertNoFailure $ do (fp, h) <- liftIO $@@ -70,6 +84,15 @@ liftIO $ hClose h addToStore fp +badInstantiateFilePath :: TestTree+badInstantiateFilePath = testCase "not a derivation"+ $ isExcBad "No such file or directory" (parseInst "42")+ where+ isExcBad = isException (defaultRunOptions {+ executables = defaultExecutables {+ exeNixInstantiate = Just "/path/does/not/exist"+ }+ }) -------------------------------------------------------------------- -- Helpers@@ -92,10 +115,26 @@ -> Either (Text, e) a -- ^ Left (subset of stdout, error) -> Assertion-isE na match = runNixAction defaultRunOptions na >>= \res ->+isE = isE' defaultRunOptions++isE' :: (Eq a, Eq e, Show a, Show e)+ => RunOptions IO+ -> NixAction e IO a+ -> Either (Text, e) a+ -- ^ Left (subset of stdout, error)+ -> Assertion+isE' runOptions na match = runNixAction runOptions na >>= \res -> case (match, res) of (Right a, Right b) -> a @=? b (match', res') -> check match' res'++isException :: RunOptions IO -> [Char] -> NixAction e IO a -> IO ()+isException runOptions matchText na = try @SomeException (runNixAction runOptions na) >>= \case+ Left exc -> do+ let text = exc & displayException+ assertBool ("threw exception, but did not match the given substring\nshould have contained: " <> matchText <> "\nbut was: " <> text)+ $ matchText `List.isInfixOf` text+ Right _a -> assertFailure "runNixAction should have thrown an exception" isENoFail :: (Eq e, Show a, Show e) => NixAction e IO a