packages feed

stack-3.9.1: src/Stack/Clean.hs

{-# LANGUAGE NoImplicitPrelude   #-}
{-# LANGUAGE OverloadedRecordDot #-}
{-# LANGUAGE OverloadedStrings   #-}

{-|
Module      : Stack.Clean
Description : Types and functions related to Stack's @clean@ and @purge@ commands.
License     : BSD-3-Clause

Types and functions related to Stack's @clean@ and @purge@ commands.
-}

module Stack.Clean
  ( CleanOpts (..)
  , CleanDepth (..)
  , CleanCommand (..)
  , cleanCmd
  , clean
  ) where

import           Control.Monad.Extra ( concatMapM )
import           Data.List ( (\\) )
import qualified Data.Map.Strict as Map
import           Path ( (</>), isProperPrefixOf )
import           Path.IO ( ignoringAbsence, listDirRecur, removeDirRecur )
import           Stack.Config ( withBuildConfig )
import           Stack.Constants.Config
                   ( distRelativeDir, rootDistDirFromDir, workDirFromDir )
import           Stack.Prelude
import           Stack.Runners
                   ( ShouldReexec (..), withConfig, withDefaultEnvConfig )
import           Stack.Types.BuildConfig
                   ( BuildConfig (..), HasBuildConfig (..), getWorkDir )
import           Stack.Types.Config ( Config )
import           Stack.Types.EnvConfig ( EnvConfig )
import           Stack.Types.Runner ( Runner )
import           Stack.Types.SourceMap ( ProjectPackage, SMWanted (..), ppRoot )

-- | Type representing \'pretty\' exceptions thrown by functions exported by the
-- "Stack.Clean" module.
data CleanPrettyException
  = NonLocalPackages [PackageName]
  | DeletionFailures [(Path Abs Dir, SomeException)]
  deriving Show

instance Pretty CleanPrettyException where
  pretty (NonLocalPackages pkgs) =
    "[S-9463]"
    <> line
    <> fillSep
         ( flow "The following are not project packages:"
         : mkNarrativeList (Just Current) False
             (map fromPackageName pkgs :: [StyleDoc])
         )
  pretty (DeletionFailures failures) =
    "[S-6321]"
    <> line
    <> flow "Exception while recursively deleting:"
    <> line
    <> mconcat (map prettyFailure failures)
    <> flow "Perhaps you do not have permission to delete these files or they \
            \are in use?"
   where
    prettyFailure (dir, e) =
         pretty dir
      <> line
      <> string (displayException e)
      <> line

instance Exception CleanPrettyException

-- | Type representing command line options for the @stack clean@ command.
data CleanOpts = CleanOpts
  { depth    :: !CleanDepth
  , omitThis :: !Bool
  }

-- | Type representing depths of cleaning for the @stack clean@ command.
data CleanDepth
  = CleanShallow [PackageName]
    -- ^ Delete the "dist directories" as defined in
    -- 'Stack.Constants.Config.distRelativeDir' for the given project packages.
    -- If no project packages are given, all project packages should be cleaned.
  | CleanFull
    -- ^ Delete all work directories in the project.

-- | Type representing Stack's cleaning commands.
data CleanCommand
  = Clean
  | Purge

-- | Function underlying the @stack clean@ command.
cleanCmd :: CleanOpts -> RIO Runner ()
cleanCmd = withConfig NoReexec . clean

-- | Deletes build artifacts in the current project.
clean :: CleanOpts -> RIO Config ()
clean cleanOpts = do
  toDelete <- if cleanOpts.omitThis
    then
      withDefaultEnvConfig $ dirsToDeleteGivenConfig cleanOpts.depth
    else
      withBuildConfig $ dirsToDeleteSimple cleanOpts.depth
  logDebug $ "Need to delete: " <> fromString (show (map toFilePath toDelete))
  failures <- catMaybes <$> mapM cleanDir toDelete
  case failures of
    [] -> pure ()
    _  -> prettyThrowIO $ DeletionFailures failures

cleanDir :: Path Abs Dir -> RIO Config (Maybe (Path Abs Dir, SomeException))
cleanDir dir = do
  logDebug $ "Deleting directory: " <> fromString (toFilePath dir)
  liftIO (ignoringAbsence (removeDirRecur dir) >> pure Nothing) `catchAny` \ex ->
    pure $ Just (dir, ex)

dirsToDeleteSimple :: CleanDepth -> RIO BuildConfig [Path Abs Dir]
dirsToDeleteSimple depth = do
  packages <- view $ buildConfigL . to (.smWanted.project)
  case depth of
    CleanShallow [] -> do
      -- Filter out packages listed as extra-deps
      let pkgNames = Map.elems packages
      mapM (rootDistDirFromDir . ppRoot) pkgNames
    CleanShallow targets -> do
      let localPkgNames = Map.keys packages
          getPkgDir pkgName' = fmap ppRoot (Map.lookup pkgName' packages)
          pkgNames = mapMaybe getPkgDir targets
      case targets \\ localPkgNames of
        [] -> mapM rootDistDirFromDir pkgNames
        xs -> prettyThrowM (NonLocalPackages xs)
    CleanFull -> allWorkDirs $ Map.elems packages

dirsToDeleteGivenConfig :: CleanDepth -> RIO EnvConfig [Path Abs Dir]
dirsToDeleteGivenConfig depth = do
  packages <- view $ buildConfigL . to (.smWanted.project)
  case depth of
    CleanShallow [] -> do
      -- Filter out packages listed as extra-deps
      let pkgNames = Map.elems packages
      concatMapM (unusedRootDistDirsFromDir . ppRoot) pkgNames
    CleanShallow targets -> do
      let localPkgNames = Map.keys packages
          getPkgDir pkgName' = fmap ppRoot (Map.lookup pkgName' packages)
          pkgNames = mapMaybe getPkgDir targets
      case targets \\ localPkgNames of
        [] -> concatMapM unusedRootDistDirsFromDir pkgNames
        xs -> prettyThrowM (NonLocalPackages xs)
    CleanFull -> allWorkDirs $ Map.elems packages

allWorkDirs :: HasBuildConfig env => [ProjectPackage] -> RIO env [Path Abs Dir]
allWorkDirs pps = do
  pkgWorkDirs <- mapM (workDirFromDir . ppRoot) pps
  projectWorkDir <- getWorkDir
  pure (projectWorkDir : pkgWorkDirs)

unusedRootDistDirsFromDir :: Path Abs Dir -> RIO EnvConfig [Path Abs Dir]
unusedRootDistDirsFromDir pkgDir = do
  rootDistDir <- rootDistDirFromDir pkgDir
  omitDir <- fmap (pkgDir </>) distRelativeDir
  allDirsOmittingDirs rootDistDir omitDir

allDirsOmittingDirs ::
     MonadIO m
  => Path Abs Dir
  -> Path Abs Dir
  -> m [Path Abs Dir]
allDirsOmittingDirs topDir subDir = do
  allDirs <- (topDir :) . fst <$> listDirRecur topDir
  let isNotInSubDir dir = not
        ( isProperPrefixOf dir subDir
        || subDir == dir
        || isProperPrefixOf subDir dir
        )
  pure $ filter isNotInSubDir allDirs