{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE CPP #-}
module Main where
import Data.Maybe (listToMaybe, mapMaybe)
import Stackage.CLI
import Filesystem
import Control.Exception (Exception, catch)
import Control.Monad
import Control.Monad.Catch (MonadThrow, throwM)
import Control.Applicative
import Data.Monoid
import Data.Typeable (Typeable)
import Options.Applicative (Parser, flag, long, help)
import System.Process (readProcess)
import Data.Char (toLower)
import System.IO (stdout, stderr, hFlush, hPutStrLn)
import qualified Data.Text.Encoding as T
import qualified Data.Text as T
import System.Environment (getArgs)
import System.Exit (exitFailure)
import qualified Paths_stackage_cabal as CabalInfo
import Text.Parsec hiding ((<|>), many)
type ParsecParser = Parsec String ()
data Force = Prompt | Force
data PurgeOpts = PurgeOpts
{ purgeOptsForce :: Force }
data PackageGroup = PackageGroup
{ packageGroupDb :: String
, packageGroupPackages :: [String]
}
data PurgeException
= ParsePackagesError ParseError
deriving (Show, Typeable)
instance Exception PurgeException
prompt :: String -> IO String
prompt str = putStr str >> hFlush stdout >> getLine
whenJust :: Monad m => Maybe a -> (a -> m ()) -> m ()
whenJust (Just a) f = f a
whenJust Nothing _ = return ()
pluralize :: Int -> a -> a -> a
pluralize 1 a _ = a
pluralize _ _ a = a
unregisterPackages :: String -> [String] -> IO ()
unregisterPackages packageDb = mapM_ unregister where
unregister package = do
putStrLn $ "Unregistering: " <> package
_ <- readProcess "ghc-pkg" (args package) ""
return ()
args package =
[ "unregister"
, package
, "--force"
] <> dbToArgs (Just packageDb)
parsePackageDb :: IO (Maybe String)
parsePackageDb = do
cabalSandboxConfigExists <- isFile "cabal.sandbox.config"
if cabalSandboxConfigExists
then do
t <- Filesystem.readTextFile "cabal.sandbox.config"
let packageDbLine = T.stripPrefix "package-db: "
return $ fmap T.unpack $ listToMaybe $ mapMaybe packageDbLine $ T.lines t
else
return Nothing
dbToArgs :: Maybe String -> [String]
dbToArgs Nothing = []
dbToArgs (Just packageDb) =
[ "--package-db"
, packageDb
]
getGlobalPackageDb :: IO (Maybe String)
getGlobalPackageDb = do
let fakePackage = "asdklfjasdklfajsdlkghaiwojgadjfkq"
output <- readProcess "ghc-pkg" ["list", fakePackage] ""
return $ fmap init $ listToMaybe (lines output)
-- fmap init is to get rid of the trailing colon
getPackages :: Maybe String -> IO [PackageGroup]
getPackages mPackageDb = parsePackages =<< readProcess "ghc-pkg" args "" where
args = ["list"] <> dbToArgs mPackageDb
parsePackages :: MonadThrow m => String -> m [PackageGroup]
parsePackages
= either (throwM . ParsePackagesError) return
. parse packagesParser ""
-- #28
#if !MIN_VERSION_parsec(3,1,6)
crlf :: ParsecParser Char
crlf = char '\r' *> char '\n' <?> "crlf new-line"
endOfLine :: ParsecParser Char
endOfLine = newline <|> crlf <?> "new-line"
#endif
ending :: ParsecParser ()
ending = eof <|> void endOfLine
packagesParser :: ParsecParser [PackageGroup]
packagesParser = many1 parseGroup
parseGroup :: ParsecParser PackageGroup
parseGroup = PackageGroup <$> parseDb <*> parseDbPackages <* many endOfLine
parseDb :: ParsecParser String
parseDb = manyTill anyChar $ try (char ':' *> ending)
parseDbPackages :: ParsecParser [String]
parseDbPackages = try parseNoPackages <|> many1 parsePackage
parseNoPackages :: ParsecParser [String]
parseNoPackages = many1 (char ' ') *> string "(no packages)" *> ending *> pure []
parsePackage :: ParsecParser String
parsePackage = many1 (char ' ') *> manyTill anyChar ending
purge :: PurgeOpts -> IO ()
purge opts = do
cabalConfigExists <- isFile "cabal.config"
when cabalConfigExists $ do
removeFile "cabal.config"
globalPackageDbMay <- getGlobalPackageDb
sandboxPackageDbMay <- parsePackageDb
let displaySandbox s
| Just s == globalPackageDbMay =
"(Global) " <> s
| Just s == sandboxPackageDbMay =
"(Sandbox) " <> s
| otherwise = s
packages <- getPackages sandboxPackageDbMay
forM_ packages $ \(PackageGroup db packages) -> do
putStrLn $ displaySandbox db
let nPackages = length packages
let showNPackages
= show nPackages
<> " "
<> pluralize nPackages "package" "packages"
putStrLn
$ "Detected "
<> showNPackages
<> " to purge from this database"
when (nPackages > 0) $ do
when (nPackages < 15) $ mapM_ putStrLn packages
shouldUnregister <- case purgeOptsForce opts of
Force -> do
putStrLn $ "(--force) Unregistering " <> showNPackages
return True
Prompt -> do
line <- prompt
$ "Unregister " <> showNPackages <> " (y/n)? [default: n] "
case map toLower line of
"y" -> return True
"yes" -> return True
_ -> return False
when shouldUnregister $ unregisterPackages db packages
return ()
purgeOptsParser :: Parser PurgeOpts
purgeOptsParser = PurgeOpts <$> forceOpt where
forceOpt = flag Prompt Force mods
mods = long "force"
<> help "Purge all packages without prompt"
version :: String
version = $(simpleVersion CabalInfo.version)
header :: String
header = "Delete cabal.config and purge your package database"
progDesc :: String
progDesc = header
handlePurgeExceptions :: PurgeException -> IO ()
handlePurgeExceptions (ParsePackagesError _) = do
hPutStrLn stderr $ "Failed to parse ghc-pkg output"
exitFailure
main :: IO ()
main = do
(opts, ()) <- simpleOptions
version
header
progDesc
purgeOptsParser -- global parser
empty -- subcommands
purge opts `catch` handlePurgeExceptions