packages feed

nix-freeze-tree-0.1.0.1: src/NixFreezeTree.hs

{-# LANGUAGE QuasiQuotes     #-}

module NixFreezeTree (main) where

import           BuilderDirs (BuilderDirs(..))
import           Config (Config(..))
import qualified Config as C
import           Control.Exception (catch)
import           Control.Monad (when)
import           Control.Monad.IO.Class (MonadIO(..))
import           Control.Monad.Trans.Reader (ReaderT(..), asks, local)
import           Data.Bool (bool)
import           Data.Functor (void)
import           Data.Functor.Identity (Identity(..))
import           Data.List (isSuffixOf)
import           Data.Text (Text)
import qualified Data.Text.IO as T
import           Data.Text.Prettyprint.Doc (defaultLayoutOptions, layoutPretty)
import           Data.Text.Prettyprint.Doc.Render.Text (renderStrict)
import           Nix.Pretty (prettyNix)
import           NixBuilder (mkBuildExpr)
import           Options.Applicative (execParser)
import           Path (Abs, Dir, Path, Rel, (</>))
import qualified Path as P
import qualified System.Directory as D
import           System.Directory.Tree
  ( AnchoredDirTree(..)
  , DirTree(..)
  , FileName
  )
import qualified System.Directory.Tree as DT
import           System.Exit (exitFailure)
import           System.IO (hPutStrLn, stderr)

buildExprTree :: Path Abs Dir -> IO (DirTree Text)
buildExprTree p = flip runReaderT (BuilderDirs p [P.reldir|.|]) $ do
  (_ :/ inDirTree) <- P.toFilePath <$> asks _root >>= liftIO . DT.build

  case inDirTree of
    Dir _ fs -> go (".", fs) -- Use "." to avoid the root in output paths
    _ -> fail "Should never happen"

  where
    -- Walk the directory tree, building the @default.nix@ files, and
    -- keeping an absolute path to where we are in the ReaderT
    -- context.
    go :: (FileName, [DirTree a]) -> ReaderT BuilderDirs IO (DirTree Text)
    go (dirName, dirFiles) = do
      relDirName <- P.parseRelDir dirName
      local (\b -> b { _subdir = _subdir b </> relDirName }) $ do
        files <- traverse P.parseRelFile [ f | File f _ <- dirFiles ]
        dirs <- traverse P.parseRelDir [ d | Dir d _ <- dirFiles ]

        defaultNix <- renderNix <$> mkBuildExpr files dirs
        subdirs <- traverse go [ (d, fs) | Dir d fs <- dirFiles ]
        pure . Dir dirName $ File "default.nix" defaultNix:subdirs

    renderNix = renderStrict . layoutPretty defaultLayoutOptions . prettyNix

data DirConstraint = MustNameDirectory | MustNameDirectoryIfExists

toAbsDir :: DirConstraint -> FilePath -> IO (Path Abs Dir)
toAbsDir constraint d = liftIO $ do
  absD <- (D.canonicalizePath d >>= P.parseAbsDir) `catch`
    \(P.InvalidAbsDir _) -> dieD "does not name a directory"

  isDir <- D.doesDirectoryExist d
  if isDir
    then pure absD
    else case constraint of
           MustNameDirectory -> dieD "does not name a directory"
           MustNameDirectoryIfExists -> D.doesPathExist d >>=
             bool (pure absD) (dieD "names something other than a directory")

  where
    dieD m = die $ show d <> " " <> m

writeNixTree :: Config Identity -> IO ()
writeNixTree cfg = do
  outRoot <- toAbsDir MustNameDirectoryIfExists $ get _outputRoot
  inDir <- toAbsDir MustNameDirectory $ get _inDir

  let outRootPath = P.toFilePath outRoot
  outPathTree@(_ :/ outFiles) <- liftIO $ DT.build outRootPath

  when (defaultNixExists outFiles) $
    if get _force
    then do
      vPutStrLn "a default.nix exists somewhere in the output tree, cleaning"
      void . liftIO $ flip DT.writeDirectoryWith outPathTree $ \fp _ ->
        when ("default.nix" `isSuffixOf` fp) $ do
          vPutStrLn $ "deleting " <> fp
          D.removePathForcibly fp
    else die "a default.nix exists somewhere in the output tree, cannot proceed"

  outTree <- buildExprTree inDir
  void $ flip DT.writeDirectoryWith (outRootPath :/ outTree) $ \fp t -> do
    vPutStrLn . ("writing " <>) =<< D.canonicalizePath fp
    T.writeFile fp t

  where
    get f = runIdentity $ f cfg

    defaultNixExists (File n _) = n == "default.nix"
    defaultNixExists (Dir _ cs) = any defaultNixExists cs
    defaultNixExists Failed{} = False

    vPutStrLn msg = when (get _verbose) $ putStrLn msg

main :: IO ()
main = C.checkConfig <$> execParser C.configParserInfo >>=
  maybe (fail "Should be impossible") writeNixTree

die :: MonadIO m => String -> m a
die msg = liftIO $ hPutStrLn stderr ("FATAL: " <> msg) *> exitFailure