packages feed

nix-tree-0.5.0: src/NixTree/Main.hs

{-# LANGUAGE CPP #-}

module Main where

import Control.Concurrent.Async
import Control.Exception (evaluate)
import NixTree.App
import NixTree.PathStats
import qualified Options.Applicative as Opts
import qualified Options.Applicative.Help.Pretty as Opts
import System.Directory (XdgDirectory (XdgState), doesDirectoryExist, getHomeDirectory, getXdgDirectory)
import System.Exit (ExitCode (..))
import System.FilePath ((</>))
import System.IO (hPutStrLn)
import System.ProgressBar hiding (msg)

version :: Text
version = VERSION_nix_tree

data Opts = Opts
  { oInstallables :: [Installable],
    oStore :: Maybe String,
    oFile :: Maybe FilePath,
    oVersion :: Bool,
    oDerivation :: Bool,
    oImpure :: Bool,
    oDot :: Bool
  }

optsParser :: Opts.ParserInfo Opts
optsParser =
  Opts.info (parser <**> Opts.helper) $
    mconcat
      [ Opts.progDesc "Interactively browse dependency graphs of Nix derivations.",
        Opts.fullDesc,
        Opts.footerDoc (Just keybindingsHelp)
      ]
  where
    parser :: Opts.Parser Opts
    parser =
      Opts
        <$> many
          ( Installable
              <$> Opts.strArgument @Text
                ( Opts.metavar "INSTALLABLE"
                    <> Opts.helpDoc
                      ( Just $
                          Opts.vsep
                            [ "A store path or a flake reference.",
                              "Paths default to \"~/.nix-profile\" and \"/var/run/current-system\""
                            ]
                      )
                )
          )
        <*> optional
          ( Opts.strOption
              ( Opts.long "store"
                  <> Opts.metavar "STORE"
                  <> Opts.helpDoc
                    ( Just $
                        Opts.vsep
                          [ "The URL of the Nix store, e.g. \"daemon\" or \"https://cache.nixos.org\"",
                            "See \"nix help-stores\" for supported store types and settings."
                          ]
                    )
              )
          )
        <*> optional
          ( Opts.strOption
              ( Opts.long "file"
                  <> Opts.metavar "FILE"
                  <> Opts.helpDoc
                    ( Just $ Opts.vsep ["Interpret installables as attribute paths relative to the Nix expression stored in file."]
                    )
              )
          )
        <*> Opts.switch (Opts.long "version" <> Opts.help "Show the nix-tree version")
        <*> Opts.switch (Opts.long "derivation" <> Opts.help "Operate on the store derivation rather than its outputs")
        <*> Opts.switch (Opts.long "impure" <> Opts.help "Allow access to mutable paths and repositories")
        <*> Opts.switch (Opts.long "dot" <> Opts.help "Print the dependency graph in dot format")

    keybindingsHelp :: Opts.Doc
    keybindingsHelp =
      Opts.vsep
        [ "Keybindings:",
          Opts.indent 2 . Opts.vsep $ map Opts.pretty (lines helpText)
        ]

showAndFail :: Text -> IO a
showAndFail msg = do
  hPutStrLn stderr . toString $ "Error: " <> msg
  exitWith (ExitFailure 1)

main :: IO ()
main = do
  opts <-
    Opts.customExecParser
      (Opts.prefs $ Opts.columns 120)
      optsParser

  when (opts & oVersion) $ do
    putTextLn $ "nix-tree " <> version
    exitSuccess

  installables <- case opts & oInstallables of
    p : ps ->
      return $ p :| ps
    [] -> do
      home <- getHomeDirectory
      nixXdgDirectory <- getXdgDirectory XdgState "nix/profile"
      roots <-
        filterM
          doesDirectoryExist
          [ home </> ".nix-profile",
            nixXdgDirectory,
            "/var/run/current-system"
          ]
      case roots of
        [] -> showAndFail "No store path given."
        p : ps -> return . fmap (Installable . toText) $ p :| ps

  let seo =
        StoreEnvOptions
          { seoIsDerivation = opts & oDerivation,
            seoIsImpure = opts & oImpure,
            seoStoreURL = opts & oStore,
            seoFile = opts & oFile
          }

  withStoreEnv seo installables $ \env' -> do
    let env = calculatePathStats env'
        allPaths = seAll env

    bar <- newProgressBar defStyle {stylePostfix = exact} 4 (Progress 0 (length allPaths) ())
    allPaths
      & toList
      & chunks 50
      & mapConcurrently_ (mapM_ (\p -> evaluate (rnf p) >> incProgress bar 1))

    if opts & oDot
      then putTextLn $ storeEnvToDot env
      else run env

chunks :: Int -> [a] -> [[a]]
chunks _ [] = []
chunks n xs =
  let (ys, zs) = splitAt n xs
   in ys : chunks n zs