haskell-updater-1.2.9: Distribution/Gentoo/Packages.hs
{-# Language TupleSections #-}
{- |
Module : Distribution.Gentoo.Packages
Description : Dealing with installed packages on Gentoo.
Copyright : (c) Ivan Lazar Miljenovic, Lennart Kolmodin 2009
License : GPL-2 or later
This module defines helper functions that deal with installed
packages in Gentoo.
-}
module Distribution.Gentoo.Packages
( Package
, Content
, notGHC
, printPkg
, resolveFiles
, pkgsHaveContent
, hasDirMatching
) where
import Data.Char(isDigit, isAlphaNum)
import Data.List(isPrefixOf)
import Data.Maybe
import qualified Data.ByteString.Char8 as BS
import Data.ByteString.Char8(ByteString)
import qualified Data.Set as S
import System.Directory( doesDirectoryExist
, doesFileExist)
import System.FilePath((</>))
import Control.Monad
import Distribution.Gentoo.Util
-- -----------------------------------------------------------------------------
-- Representation of a cat/pkgname in Gentoo. Note that this is
-- overly simplified.
type Category = String
type Pkg = String -- Package name.
type VerPkg = String -- Package name with version.
type VCatPkg = (Category, VerPkg)
type Slot = String
-- When we are (re-)building packages, we don't care about the
-- version, just the slot.
data Package = Package Category Pkg (Maybe Slot)
deriving(Eq, Ord, Show, Read)
-- Package equality, ignoring the Slot (i.e. same category and package
-- name).
samePackageAs :: Package -> Package -> Bool
samePackageAs (Package c1 p1 _) (Package c2 p2 _)
= c1 == c2 && p1 == p2
ghcPkg :: Package
ghcPkg = Package "dev-lang" "ghc" Nothing
-- Return all packages that are not a version of GHC.
notGHC :: [Package] -> [Package]
notGHC = filter (isNot ghcPkg)
where
isNot p1 = not . samePackageAs p1
-- Pretty-print the Package name based on how PMs expect it
printPkg :: Package -> String
printPkg (Package c p s) = addS cp
where
addS = maybe id (flip (++) . (:) ':') s
cp = c ++ '/' : p
-- Determine which slot the specific version of the package is in and
-- create the appropriate Package value.
toPackage :: VCatPkg -> IO Package
toPackage cp@(c,vp) = do sl <- getSlot cp
let p = stripVersion vp
return $ Package c p sl
-- Determine which slot the specific version of the package is in.
getSlot :: VCatPkg -> IO (Maybe Slot)
getSlot cp = do ex <- doesFileExist sFile
if ex
then parse
else return Nothing
where
sFile = pkgPath cp </> "SLOT"
-- EAPI=5 defines subslots
split_slot_subslot = break (== '/')
parse = do fl <- BS.unpack `fmap` BS.readFile sFile
-- Don't want the trailing newline
return $ listToMaybe $ map (fst . split_slot_subslot) $ lines fl
-- | Remove the version information from the package name.
stripVersion :: VerPkg -> Pkg
stripVersion = concat . takeUntilVer . breakAll partSep
where
partSep x = x `elem` ['-', '_']
-- Only the last bit that matches isVer is the real version bit.
-- Note that this doesn't check that the last non-version bit is
-- not a hyphen followed by digits.
takeUntilVer = concat . init . breakAll isVer
isVer as = isVerFront (init as) && isAlphaNum (last as)
isVerFront ('-':as) = all (\a -> isDigit a || a == '.') as
isVerFront _ = False
pkgPath :: VCatPkg -> FilePath
pkgPath (c, vp) = pkgDBDir </> c </> vp
pkgDBDir :: FilePath
pkgDBDir = "/var/db/pkg"
-- -----------------------------------------------------------------------------
-- Parsing the CONTENTS file of installed packages.
-- Representation of individual lines in a CONTENTS file.
data Content = Dir BSFilePath
| Obj BSFilePath
deriving (Eq, Show, Ord)
isDir :: Content -> Bool
isDir (Dir _) = True
isDir _ = False
pathOf :: Content -> BSFilePath
pathOf (Dir dir) = dir
pathOf (Obj obj) = obj
-- Searching predicates.
hasContentMatching :: (BSFilePath -> Bool) -> [Content] -> Bool
hasContentMatching p = any p . map pathOf
hasDirMatching :: (BSFilePath -> Bool) -> [Content] -> Bool
hasDirMatching p = hasContentMatching p . filter isDir
-- Parse the CONTENTS file.
parseContents :: VCatPkg -> IO [Content]
parseContents cp = do ex <- doesFileExist cFile
if ex
then parse
else return []
where
cFile = pkgPath cp </> "CONTENTS"
parse = do lns <- liftM BS.lines $ BS.readFile cFile
return $ mapMaybe (parseCLine . BS.words) lns
-- Use unwords of list rather than taking next element because of
-- how spaces are represented in file names.
-- This might cause a problem if there is more than a single
-- space (or a tab) in the filename...
-- Also require at least 3 words in case of an object, as the CONTENTS
-- file can be corrupt (fixes actual problem reported by user).
parseCLine :: [ByteString] -> Maybe Content
parseCLine (tp:ln)
| tp == dir = Just . Dir . BS.unwords $ ln
| tp == obj && length ln >= 3 = Just . Obj . BS.unwords $ dropLastTwo ln
| otherwise = Nothing
parseCLine [] = Nothing
dropLastTwo :: [a] -> [a]
dropLastTwo = init . init
obj = BS.pack "obj"
dir = BS.pack "dir"
-- -----------------------------------------------------------------------------
-- Find all the packages that contain given files.
resolveFiles :: [FilePath] -> IO [(FilePath, Package)]
resolveFiles fps = liftM expand $ forPkg grep
where
fps' = S.fromList $ map (Obj . BS.pack) fps
expand pfs = [ (BS.unpack fn, pn)
| (pn, conts) <- pfs
, Obj fn <- conts
]
grep pn cont = Just (pn, filter (\e -> S.member e fps') cont)
-- | Run predecate 'p' for each installed package
-- and gather all 'Just' values
forPkg :: (Package -> [Content] -> Maybe a) -> IO [a]
forPkg p = do
categories <- installedCats
liftM catMaybes $ do
(flip concatMapM) categories $ \cat -> do
maybe_pkgs <- getDirectoryContents' (pkgDBDir </> cat)
packages <- filterM (isPackage . (cat,)) maybe_pkgs
forM packages $ \pkg -> do
let cp = (cat, pkg)
cpn <- toPackage cp
cont <- parseContents cp
return $ p cpn cont
where
isPackage :: VCatPkg -> IO Bool
isPackage vcp@(_, vp) = do
c1 <- doesDirectoryExist $ pkgPath vcp
let c2 = not $ "-MERGING-" `isPrefixOf` vp
return $ c1 && c2
-- Find which packages have Content information that matches the
-- provided predicate; to be used with the searching predicates
-- above.
pkgsHaveContent :: ([Content] -> Bool) -> IO [Package]
pkgsHaveContent p = forPkg p'
where p' pn cont = if p cont
then Just pn
else Nothing
-- Determine if this is a valid Category (such that at least one
-- package in that category has been installed).
isCat :: String -> IO Bool
isCat fp = do isD <- doesDirectoryExist (pkgDBDir </> fp)
return $ isD && isCat' fp
where
isCat' ('.':_) = False
isCat' "world" = False
isCat' _ = True
-- Return all Categories known in this system.
installedCats :: IO [Category]
installedCats = filterM isCat =<< getDirectoryContents' pkgDBDir