{-# language CPP #-} module Nix.Effects.Basic where import Nix.Prelude hiding ( head ) import Relude.Unsafe ( head ) import GHC.Exception ( ErrorCall(ErrorCall) ) import Control.Monad ( foldM ) import qualified Data.HashMap.Lazy as M import Data.List.Split ( splitOn ) import qualified Data.Text as Text import Prettyprinter ( fillSep ) import Nix.Convert import Nix.Effects import Nix.Exec ( MonadNix , evalExprLoc , nixInstantiateExpr ) import Nix.Expr.Types import Nix.Expr.Types.Annotated import Nix.Frames import Nix.Parser import Nix.Render import Nix.Scope import Nix.String import Nix.Value import Nix.Value.Monad #ifdef MIN_VERSION_ghc_datasize import GHC.DataSize #endif defaultToAbsolutePath :: forall e t f m . MonadNix e t f m => Path -> m Path defaultToAbsolutePath origPath = do origPathExpanded <- expandHomePath origPath fmap removeDotDotIndirections . canonicalizePath =<< bool (fmap ( origPathExpanded) $ maybe getCurrentDirectory ( (\case NVPath s -> pure $ takeDirectory s val -> throwError $ ErrorCall $ "when resolving relative path, __cur_file is in scope, but is not a path; it is: " <> show val ) <=< demand ) =<< lookupVar "__cur_file" ) (pure origPathExpanded) (isAbsolute origPathExpanded) expandHomePath :: MonadFile m => Path -> m Path expandHomePath (coerce -> ('~' : xs)) = (<> coerce xs) <$> getHomeDirectory expandHomePath p = pure p -- | Incorrectly normalize paths by rewriting patterns like @a/b/..@ to @a@. -- This is incorrect on POSIX systems, because if @b@ is a symlink, its -- parent may be a different directory from @a@. See the discussion at -- https://hackage.haskell.org/package/directory-1.3.1.5/docs/System-Directory.html#v:canonicalizePath removeDotDotIndirections :: Path -> Path removeDotDotIndirections = coerce . intercalate "/" . go mempty . splitOn "/" . coerce where go s [] = reverse s go (_ : s) (".." : rest) = go s rest go s (this : rest) = go (this : s) rest infixr 9 () :: Path -> Path -> Path x y | isAbsolute y || "." `isPrefixOf` coerce y = x y | otherwise = joinByLargestOverlap x y where joinByLargestOverlap :: Path -> Path -> Path joinByLargestOverlap (splitDirectories -> xs) (splitDirectories -> ys) = joinPath $ head [ xs <> drop (length tx) ys | tx <- tails xs, tx `elem` inits ys ] defaultFindEnvPath :: MonadNix e t f m => String -> m Path defaultFindEnvPath = findEnvPathM . coerce findEnvPathM :: forall e t f m . MonadNix e t f m => Path -> m Path findEnvPathM name = maybe (fail "impossible") (\ v -> do l <- fromValue @[NValue t f m] =<< demand v findPathBy nixFilePath l name ) =<< lookupVar "__nixPath" where nixFilePath :: MonadEffects t f m => Path -> m (Maybe Path) nixFilePath path = do absPath <- toAbsolutePath @t @f path isDir <- doesDirectoryExist absPath absFile <- bool (pure absPath) (toAbsolutePath @t @f $ absPath "default.nix") isDir (pure absFile `whenTrue`) <$> doesFileExist absFile findPathBy :: forall e t f m . MonadNix e t f m => (Path -> m (Maybe Path)) -> [NValue t f m] -> Path -> m Path findPathBy finder ls name = maybe (throwError $ ErrorCall $ "file ''" <> coerce name <> "'' was not found in the Nix search path (add it's using $NIX_PATH or -I)") pure =<< foldM fun mempty ls where fun :: MonadNix e t f m => Maybe Path -> NValue t f m -> m (Maybe Path) fun = maybe (\ nv -> do (s :: HashMap VarName (NValue t f m)) <- fromValue =<< demand nv p <- resolvePath s path <- fromValue =<< demand p maybe (tryPath path mempty) (\ nv' -> do mns <- fromValueMay @NixString =<< demand nv' tryPath path $ whenJust (\ nsPfx -> let pfx = ignoreContext nsPfx in pure $ coerce $ toString pfx `whenFalse` Text.null pfx ) mns ) (M.lookup "prefix" s) ) (const . pure . pure) tryPath :: Path -> Maybe Path -> m (Maybe Path) tryPath p (Just n) | n' : ns <- splitDirectories name, n == n' = finder $ p joinPath ns tryPath p _ = finder $ p name resolvePath :: HashMap VarName (NValue t f m) -> m (NValue t f m) resolvePath s = maybe (maybe (throwError $ ErrorCall $ "__nixPath must be a list of attr sets with 'path' elements, but received: " <> show s) (defer . fetchTarball) (M.lookup "uri" s) ) pure (M.lookup "path" s) fetchTarball :: forall e t f m . MonadNix e t f m => NValue t f m -> m (NValue t f m) fetchTarball = \case NVSet _ s -> maybe (throwError $ ErrorCall "builtins.fetchTarball: Missing url attribute") (fetchFromString (M.lookup "sha256" s) <=< demand) (M.lookup "url" s) v@NVStr{} -> fetchFromString Nothing v v -> throwError $ ErrorCall $ "builtins.fetchTarball: Expected URI or set, got " <> show v <=< demand where fetchFromString :: Maybe (NValue t f m) -> NValue t f m -> m (NValue t f m) fetchFromString msha = \case NVStr ns -> fetch (ignoreContext ns) msha v -> throwError $ ErrorCall $ "builtins.fetchTarball: Expected URI or string, got " <> show v {- jww (2018-04-11): This should be written using pipes in another module fetch :: Text -> Maybe (NThunk m) -> m (NValue t f m) fetch uri msha = case takeExtension uri of ".tgz" -> undefined ".gz" -> undefined ".bz2" -> undefined ".xz" -> undefined ".tar" -> undefined ext -> throwError $ ErrorCall $ "builtins.fetchTarball: Unsupported extension '" <> ext <> "'" -} fetch :: Text -> Maybe (NValue t f m) -> m (NValue t f m) fetch uri = maybe (nixInstantiateExpr $ "builtins.fetchTarball \"" <> uri <> "\"" ) (\ v -> do nsSha <- fromValue =<< demand v let sha = ignoreContext nsSha nixInstantiateExpr $ "builtins.fetchTarball { " <> "url = \"" <> uri <> "\"; " <> "sha256 = \"" <> sha <> "\"; }" ) defaultFindPath :: MonadNix e t f m => [NValue t f m] -> Path -> m Path defaultFindPath = findPathM findPathM :: forall e t f m . MonadNix e t f m => [NValue t f m] -> Path -> m Path findPathM = findPathBy existingPath where existingPath :: MonadEffects t f m => Path -> m (Maybe Path) existingPath path = do apath <- toAbsolutePath @t @f path doesExist <- doesPathExist apath pure $ pure apath `whenTrue` doesExist defaultImportPath :: (MonadNix e t f m, MonadState (HashMap Path NExprLoc, b) m) => Path -> m (NValue t f m) defaultImportPath path = do traceM $ "Importing file " <> coerce path withFrame Info (ErrorCall $ "While importing file " <> show path) $ evalExprLoc =<< (maybe (either (\ err -> throwError $ ErrorCall . show $ fillSep ["Parse during import failed:", err]) (\ expr -> do modify $ first $ M.insert path expr pure expr ) =<< parseNixFileLoc path ) pure -- return expr . M.lookup path ) =<< gets fst defaultPathToDefaultNix :: MonadNix e t f m => Path -> m Path defaultPathToDefaultNix = pathToDefaultNixFile -- Given a path, determine the nix file to load pathToDefaultNixFile :: MonadFile m => Path -> m Path pathToDefaultNixFile p = do isDir <- doesDirectoryExist p pure $ p "default.nix" `whenTrue` isDir defaultTraceEffect :: MonadPutStr m => String -> m () defaultTraceEffect = Nix.Effects.putStrLn