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