packages feed

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