cabal-clean-0.1.20210815: src/Structure.hs
-- {-# LANGUAGE BlockArguments #-}
{-# LANGUAGE ScopedTypeVariables #-}
-- {-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE FlexibleContexts #-}
{-# OPTIONS_GHC -Wno-deprecations #-} -- versionTags
-- | Data structure for the `dist-newstyle/build` directory.
--
-- Example:
--
-- @
-- dist-newstyle/build/
-- └── x86_64-osx
-- ├── ghc-7.10.3
-- │ ├── BNFC-2.9.2
-- │ └── BNFC-2.9.3
-- ├── ghc-8.10.4
-- │ └── BNFC-2.9.2
-- ├── ghc-8.10.5
-- │ └── BNFC-2.9.3
-- └── ghc-9.0.1
-- ├── BNFC-2.9.2
-- └── BNFC-2.9.3
-- @
--
-- We organize the contents of the build directory
-- in the way we see fit:
--
-- * Level 1: package
-- * Level 2: package version
-- * Level 3: architecture
-- * Level 4: major compiler version
-- * Level 5: minor compiler version
module Structure where
import qualified Data.Map as Map
import Util
import Options
-- | The structure of the build directory.
newtype BuildTree = BuildTree
{ theBuildTree ::
Map Package
(Map PackageVersion
(Map Arch
(Map MajorVersion
(Map MinorVersion Entry))))
}
deriving Show
-- | The build directory is a map from 'Key' to 'Entry'.
data Key = Key
{ pkg :: Package
, pkgVer :: PackageVersion
, arch :: Arch
, ghcMaj :: MajorVersion
, ghcMin :: MinorVersion
}
data Entry = Entry
{ dir :: FilePath
, obsolete :: Bool
} deriving Show
-- | We treat the architecture identifier as opaque.
type Arch = String
-- | A package is given by its name.
type Package = String
-- | A package version is a list of natural numbers.
type PackageVersion = NumericVersion
-- | A GHC major version is a list of natural numbers.
type MajorVersion = NumericVersion
-- | A GHC minor version is a list of natural numbers.
type MinorVersion = NumericVersion
type CompilerVersion = (MajorVersion, MinorVersion)
type NumericVersion = [Int]
-- * Loading the build tree from disc.
type Warnings = [String]
-- | Load the build tree from e.g. @dist-newstyle/build@.
--
-- Generate warning for subdirectories we cannot parse,
-- but ignore them otherwise.
getBuildTree :: FilePath -> IO (BuildTree, Warnings)
getBuildTree root = runWriterT $ do
-- Traverse architectures
forMM (getSubdirectories root) $ \ arch -> do
let archroot = root </> arch
-- Traverse compilers
forMM (getSubdirectories archroot) $ \ hc -> do
let hcroot = archroot </> hc
case parseCompilerString hc of
Nothing -> warnIgnoring hcroot
Just (major, minor) -> do
-- Traverse packages
forMM (getSubdirectories hcroot) $ \ s -> do
let pkgdir = hcroot </> s
case parsePackageString s of
Nothing -> warnIgnoring pkgdir
Just (pkg, ver) -> do
return $ singleton (Key pkg ver arch major minor) pkgdir
where
warnIgnoring dir = do
tell [ unwords ["Ignoring", dir] ]
return mempty
-- | Precondition: argument must be a directory.
getSubdirectories :: MonadIO io => FilePath -> io [FilePath]
getSubdirectories root = liftIO $ do
filterM (doesDirectoryExist . (root </>)) =<< listDirectory root
-- * Mark obsolete entries of the build tree.
-- | Mark entries that are superseded by either a new package version
-- or a new compiler minor version.
markObsolete :: BuildTree -> BuildTree
markObsolete (BuildTree t) = BuildTree $
flip fmap t $ modifyDesc $ \case
[] -> []
(ver, m) : vms ->
(ver, flip (fmap . fmap) m $ modifyDesc $ \case
[] -> []
me : mes -> me : map (second markEntryObsolete) mes
) : map (second $ fmap $ fmap $ fmap markEntryObsolete) vms
where
modifyDesc f = Map.fromDescList . f . Map.toDescList
-- mapDesc f = Map.fromDescList . map (second f) . Map.toDescList
markEntryObsolete :: Entry -> Entry
markEntryObsolete (Entry dir _) = Entry dir True
-- | Remove directories marked as obsolete.
removeObsolete :: BuildTree -> IO ()
removeObsolete = foldMapEntry $ \ (Entry dir obsolete) -> do
when obsolete $ do
removeDirectoryRecursive dir
-- * Printing the build tree to the terminal.
-- | Print the build tree, coloring parts to keep in green, and parts to remove in red.
printBuildTree :: Options -> BuildTree -> IO ()
printBuildTree opts = foldMapEntry $ \ (Entry dir obsolete) -> do
(exitcode, stdout, _stderr) <- readProcessWithExitCode "du" ["-hs", dir] ""
let s = if exitcode == ExitSuccess then stdout else dir ++ "\n"
putStr $ colorize obsolete s
where
colorize True = colorOpt opts Red . ("---\t" ++)
colorize False = colorOpt opts Green . ("+++\t" ++)
-- * Mathematics of the build directory structure.
instance Semigroup BuildTree where
BuildTree t1 <> BuildTree t2 = BuildTree $
Map.unionWith (Map.unionWith $ Map.unionWith $ Map.unionWith $ Map.union) t1 t2
instance Monoid BuildTree where
mempty = BuildTree $ Map.empty
mappend = (<>)
-- | A build tree with a single entry.
singleton :: Key -> FilePath -> BuildTree
singleton (Key pkg ver arch major minor) dir = BuildTree $
Map.singleton pkg $
Map.singleton ver $
Map.singleton arch $
Map.singleton major $
Map.singleton minor $ Entry dir False
-- | Modify all entries of a build tree.
mapEntry :: (Entry -> Entry) -> BuildTree -> BuildTree
mapEntry f (BuildTree t) = BuildTree $ (fmap . fmap . fmap . fmap . fmap) f t
-- | Modify all entries of a build tree, accumulating effects left-to-right.
traverseEntry :: Applicative m => (Entry -> m Entry) -> BuildTree -> m BuildTree
traverseEntry f (BuildTree t) =
BuildTree <$> (traverse . traverse . traverse . traverse . traverse) f t
-- | Reduce build tree.
foldMapEntry :: Monoid m => (Entry -> m) -> BuildTree -> m
foldMapEntry f (BuildTree t) = (foldMap . foldMap . foldMap . foldMap . foldMap) f t
-- * Parsing directory names
parseKey :: Arch -> CompilerString -> PackageString -> Maybe Key
parseKey arch hc s = do
(major, minor) <- parseCompilerString hc
(pkg , ver ) <- parsePackageString s
return $ Key pkg ver arch major minor
type CompilerString = String
parseCompilerString :: CompilerString -> Maybe CompilerVersion
parseCompilerString s = do
n <- findIndex (== '-') s
case splitAt n s of
("ghc", _:v) -> splitAt 2 <$> parseVersionString v
_ -> Nothing
type PackageString = String
parsePackageString :: PackageString -> Maybe (Package, PackageVersion)
parsePackageString s = do
n <- findIndexEnd (== '-') s
let (p, _:v) = splitAt n s
(p,) <$> parseVersionString v
type VersionString = String
parseVersionString :: VersionString -> Maybe NumericVersion
parseVersionString = mapM readMaybe . splitWhen (== '.')