{-# LANGUAGE AllowAmbiguousTypes #-} {-# LANGUAGE CPP #-} {-# LANGUAGE DefaultSignatures #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE DataKinds #-} {-# LANGUAGE TypeApplications #-} {-# LANGUAGE StandaloneDeriving #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE UndecidableInstances #-} {-# OPTIONS_GHC -Wno-orphans #-} module Nix.Effects where import Prelude hiding ( traceM , putStr , putStrLn , print ) import qualified Prelude import Nix.Utils import qualified Data.HashSet as HS import qualified Data.Text as Text import Network.HTTP.Client hiding ( path, Proxy ) import Network.HTTP.Client.TLS import Network.HTTP.Types import Nix.Utils.Fix1 import Nix.Expr import Nix.Frames hiding ( Proxy ) import Nix.Parser import Nix.Render import Nix.Value import qualified Paths_hnix import System.Exit import qualified System.Environment as Env import System.FilePath ( takeFileName ) import qualified System.Info import System.Process import qualified System.Nix.Hash as Store import qualified System.Nix.Store.Remote as Store.Remote import qualified System.Nix.StorePath as Store -- | A path into the nix store newtype StorePath = StorePath { unStorePath :: FilePath } -- All of the following type classes defer to the underlying 'm'. -- * @class MonadEffects t f m@ class ( MonadFile m , MonadStore m , MonadPutStr m , MonadHttp m , MonadEnv m , MonadPaths m , MonadInstantiate m , MonadExec m , MonadIntrospect m ) => MonadEffects t f m where -- | Determine the absolute path of relative path in the current context makeAbsolutePath :: FilePath -> m FilePath findEnvPath :: String -> m FilePath -- | Having an explicit list of sets corresponding to the NIX_PATH -- and a file path try to find an existing path findPath :: [NValue t f m] -> FilePath -> m FilePath importPath :: FilePath -> m (NValue t f m) pathToDefaultNix :: FilePath -> m FilePath derivationStrict :: NValue t f m -> m (NValue t f m) -- 2021-04-01: for trace, so leaving String here traceEffect :: String -> m () -- ** Instances instance ( MonadFix1T t m , MonadStore m ) => MonadStore (Fix1T t m) where addToStore a b c d = lift $ addToStore a b c d addTextToStore' a b c d = lift $ addTextToStore' a b c d -- * @class MonadIntrospect m@ class Monad m => MonadIntrospect m where recursiveSize :: a -> m Word default recursiveSize :: (MonadTrans t, MonadIntrospect m', m ~ t m') => a -> m Word recursiveSize = lift . recursiveSize -- ** Instances instance MonadIntrospect IO where recursiveSize = #ifdef MIN_VERSION_ghc_datasize #if MIN_VERSION_ghc_datasize(0,2,0) recursiveSize #else \_ -> pure 0 #endif #else \_ -> pure 0 #endif deriving instance MonadIntrospect (t (Fix1 t)) => MonadIntrospect (Fix1 t) deriving instance MonadIntrospect (t (Fix1T t m) m) => MonadIntrospect (Fix1T t m) -- * @class MonadExec m@ class Monad m => MonadExec m where exec' :: [Text] -> m (Either ErrorCall NExprLoc) default exec' :: (MonadTrans t, MonadExec m', m ~ t m') => [Text] -> m (Either ErrorCall NExprLoc) exec' = lift . exec' -- ** Instances instance MonadExec IO where exec' = \case [] -> pure $ Left $ ErrorCall "exec: missing program" (prog : args) -> do (exitCode, out, _) <- liftIO $ readProcessWithExitCode (toString prog) (toString <$> args) "" let t = Text.strip (toText out) let emsg = "program[" <> prog <> "] args=" <> show args case exitCode of ExitSuccess -> if Text.null t then pure $ Left $ ErrorCall $ toString $ "exec has no output :" <> emsg else either (\ err -> pure $ Left $ ErrorCall $ toString $ "Error parsing output of exec: " <> show err <> " " <> emsg) (pure . pure) (parseNixTextLoc t) err -> pure $ Left $ ErrorCall $ toString $ "exec failed: " <> show err <> " " <> emsg deriving instance MonadExec (t (Fix1 t)) => MonadExec (Fix1 t) deriving instance MonadExec (t (Fix1T t m) m) => MonadExec (Fix1T t m) -- * @class MonadInstantiate m@ class Monad m => MonadInstantiate m where instantiateExpr :: Text -> m (Either ErrorCall NExprLoc) default instantiateExpr :: (MonadTrans t, MonadInstantiate m', m ~ t m') => Text -> m (Either ErrorCall NExprLoc) instantiateExpr = lift . instantiateExpr -- ** Instances instance MonadInstantiate IO where instantiateExpr expr = do traceM $ "Executing: " <> show ["nix-instantiate", "--eval", "--expr ", expr] (exitCode, out, err) <- readProcessWithExitCode "nix-instantiate" ["--eval", "--expr", toString expr] "" pure $ case exitCode of ExitSuccess -> either (\ e -> Left $ ErrorCall $ "Error parsing output of nix-instantiate: " <> show e) pure (parseNixTextLoc (toText out)) status -> Left $ ErrorCall $ "nix-instantiate failed: " <> show status <> ": " <> err deriving instance MonadInstantiate (t (Fix1 t)) => MonadInstantiate (Fix1 t) deriving instance MonadInstantiate (t (Fix1T t m) m) => MonadInstantiate (Fix1T t m) -- * @class MonadEnv m@ class Monad m => MonadEnv m where getEnvVar :: Text -> m (Maybe Text) default getEnvVar :: (MonadTrans t, MonadEnv m', m ~ t m') => Text -> m (Maybe Text) getEnvVar = lift . getEnvVar getCurrentSystemOS :: m Text default getCurrentSystemOS :: (MonadTrans t, MonadEnv m', m ~ t m') => m Text getCurrentSystemOS = lift getCurrentSystemOS getCurrentSystemArch :: m Text default getCurrentSystemArch :: (MonadTrans t, MonadEnv m', m ~ t m') => m Text getCurrentSystemArch = lift getCurrentSystemArch -- ** Instances instance MonadEnv IO where getEnvVar = (<<$>>) toText . Env.lookupEnv . toString getCurrentSystemOS = pure $ toText System.Info.os -- Invert the conversion done by GHC_CONVERT_CPU in GHC's aclocal.m4 getCurrentSystemArch = pure $ toText $ case System.Info.arch of "i386" -> "i686" arch -> arch deriving instance MonadEnv (t (Fix1 t)) => MonadEnv (Fix1 t) deriving instance MonadEnv (t (Fix1T t m) m) => MonadEnv (Fix1T t m) -- * @class MonadPaths m@ class Monad m => MonadPaths m where getDataDir :: m FilePath default getDataDir :: (MonadTrans t, MonadPaths m', m ~ t m') => m FilePath getDataDir = lift getDataDir -- ** Instances instance MonadPaths IO where getDataDir = Paths_hnix.getDataDir deriving instance MonadPaths (t (Fix1 t)) => MonadPaths (Fix1 t) deriving instance MonadPaths (t (Fix1T t m) m) => MonadPaths (Fix1T t m) -- * @class MonadHttp m@ class Monad m => MonadHttp m where getURL :: Text -> m (Either ErrorCall StorePath) default getURL :: (MonadTrans t, MonadHttp m', m ~ t m') => Text -> m (Either ErrorCall StorePath) getURL = lift . getURL -- ** Instances instance MonadHttp IO where getURL url = do let urlstr = toString url traceM $ "fetching HTTP URL: " <> urlstr req <- parseRequest urlstr manager <- if secure req then newTlsManager else newManager defaultManagerSettings -- print req response <- httpLbs (req { method = "GET" }) manager let status = statusCode (responseStatus response) pure $ Left $ ErrorCall $ if status /= 200 then "fail, got " <> show status <> " when fetching url:" <> urlstr else -- do -- let bstr = responseBody response "success in downloading but hnix-store is not yet ready; url = " <> urlstr deriving instance MonadHttp (t (Fix1 t)) => MonadHttp (Fix1 t) deriving instance MonadHttp (t (Fix1T t m) m) => MonadHttp (Fix1T t m) -- * @class MonadPutStr m@ class Monad m => MonadPutStr m where --TODO: Should this be used *only* when the Nix to be evaluated invokes a --`trace` operation? -- 2021-04-01: Due to trace operation here, leaving it as String. putStr :: String -> m () default putStr :: (MonadTrans t, MonadPutStr m', m ~ t m') => String -> m () putStr = lift . putStr -- ** Instances instance MonadPutStr IO where putStr = Prelude.putStr deriving instance MonadPutStr (t (Fix1 t)) => MonadPutStr (Fix1 t) deriving instance MonadPutStr (t (Fix1T t m) m) => MonadPutStr (Fix1T t m) -- ** Functions putStrLn :: MonadPutStr m => String -> m () putStrLn = putStr . (<> "\n") print :: (MonadPutStr m, Show a) => a -> m () print = putStrLn . show -- * Store effects -- ** Data type synonyms type RecursiveFlag = Bool type RepairFlag = Bool type StorePathName = Text type FilePathFilter m = FilePath -> m Bool type StorePathSet = HS.HashSet StorePath -- ** @class MonadStore m@ class Monad m => MonadStore m where -- | Copy the contents of a local path to the store. The resulting store -- path is returned. Note: This does not support yet support the expected -- `filter` function that allows excluding some files. addToStore :: StorePathName -> FilePath -> RecursiveFlag -> RepairFlag -> m (Either ErrorCall StorePath) default addToStore :: (MonadTrans t, MonadStore m', m ~ t m') => StorePathName -> FilePath -> RecursiveFlag -> RepairFlag -> m (Either ErrorCall StorePath) addToStore a b c d = lift $ addToStore a b c d -- | Like addToStore, but the contents written to the output path is a -- regular file containing the given string. addTextToStore' :: StorePathName -> Text -> Store.StorePathSet -> RepairFlag -> m (Either ErrorCall StorePath) default addTextToStore' :: (MonadTrans t, MonadStore m', m ~ t m') => StorePathName -> Text -> Store.StorePathSet -> RepairFlag -> m (Either ErrorCall StorePath) addTextToStore' a b c d = lift $ addTextToStore' a b c d -- *** Instances instance MonadStore IO where addToStore name path recursive repair = either (\ err -> pure $ Left $ ErrorCall $ "String '" <> show name <> "' is not a valid path name: " <> err) (\ pathName -> do -- TODO: redesign the filter parameter res <- Store.Remote.runStore $ Store.Remote.addToStore @'Store.SHA256 pathName path recursive (const False) repair either Left -- err (pure . StorePath . decodeUtf8 . Store.storePathToRawFilePath) -- store path <$> parseStoreResult "addToStore" res ) (Store.makeStorePathName name) addTextToStore' name text references repair = do res <- Store.Remote.runStore $ Store.Remote.addTextToStore name text references repair either Left -- err (pure . StorePath . decodeUtf8 . Store.storePathToRawFilePath) -- path <$> parseStoreResult "addTextToStore" res -- ** Functions parseStoreResult :: Monad m => Text -> (Either String a, [Store.Remote.Logger]) -> m (Either ErrorCall a) parseStoreResult name res = pure $ either (\ msg -> Left $ ErrorCall $ "Failed to execute '" <> toString name <> "': " <> msg <> "\n" <> show logs) pure -- result (fst res) where logs = snd res addTextToStore :: (Framed e m, MonadStore m) => StorePathName -> Text -> Store.StorePathSet -> RepairFlag -> m StorePath addTextToStore a b c d = either throwError pure =<< addTextToStore' a b c d addPath :: (Framed e m, MonadStore m) => FilePath -> m StorePath addPath p = either throwError pure =<< addToStore (toText $ takeFileName p) p True False toFile_ :: (Framed e m, MonadStore m) => FilePath -> String -> m StorePath toFile_ p contents = addTextToStore (toText p) (toText contents) HS.empty False -- * misc -- Please, get rid of pathExists in favour of @doesPathExist@ pathExists :: MonadFile m => FilePath -> m Bool pathExists = doesPathExist