hackport-0.7.1.2: src/Portage/PackageId.hs
{-# LANGUAGE CPP #-}
{-|
Module : Portage.PackageId
License : GPL-3+
Maintainer : haskell@gentoo.org
Portage package identifiers, which unlike Cabal ones include a category.
-}
module Portage.PackageId (
Category(..),
PackageName(..),
PackageId(..),
Portage.Version(..),
mkPackageName,
fromCabalPackageId,
toCabalPackageId,
parseFriendlyPackage,
normalizeCabalPackageName,
normalizeCabalPackageId,
filePathToPackageId,
packageIdToFilePath,
cabal_pn_to_PN
) where
import qualified Distribution.Compat.CharParsing as P
import qualified Distribution.Package as Cabal
import Distribution.Parsec (CabalParsing(..), Parsec(..), explicitEitherParsec)
import Distribution.Pretty (Pretty(..), prettyShow)
import qualified Portage.Version as Portage
import Control.DeepSeq (NFData(..))
import qualified Data.Char as Char
import qualified Text.PrettyPrint as Disp
import Text.PrettyPrint ((<>))
import System.FilePath ((</>))
#if MIN_VERSION_base(4,11,0)
import Prelude hiding ((<>))
#endif
newtype Category = Category { unCategory :: String }
deriving (Eq, Ord, Show, Read)
-- | Portage-style 'PackageName', containing a 'Category' and a 'Cabal.PackageName'.
data PackageName = PackageName { category :: Category, cabalPkgName :: Cabal.PackageName }
deriving (Eq, Ord, Show, Read)
-- | Portage-style 'PackageId', containing a 'PackageName' and a 'Portage.Version'.
data PackageId = PackageId { packageId :: PackageName, pkgVersion :: Portage.Version }
deriving (Eq, Ord, Show, Read)
instance NFData Category where
rnf (Category c) = rnf c
instance Pretty Category where
pretty (Category c) = Disp.text c
instance Parsec Category where
parsec = Category <$> P.munch1 categoryChar
where
categoryChar c = Char.isAlphaNum c || c == '-'
instance NFData PackageName where
rnf (PackageName c pn) = rnf c `seq` rnf pn
instance Pretty PackageName where
pretty (PackageName cat name) =
pretty cat <> Disp.char '/' <> pretty name
instance Parsec PackageName where
parsec = do
cat <- parsec
_ <- P.char '/'
name <- parseCabalPackageName
return $ PackageName cat name
instance NFData PackageId where
rnf (PackageId pId pv) = rnf pId `seq` rnf pv
instance Pretty PackageId where
pretty (PackageId name version) =
pretty name <> Disp.char '-' <> pretty version
instance Parsec PackageId where
parsec = do
name <- parsec
_ <- P.char '-'
version <- parsec
return $ PackageId name version
-- | Transform a 'PackageId' into a 'FilePath'.
--
-- >>> packageIdToFilePath (PackageId (PackageName (Category "dev-haskell") (Cabal.mkPackageName "foo-bar2")) (Portage.Version [3,0,0] (Just 'b') [Portage.RC 2] 1 ))
-- "dev-haskell/foo-bar2/foo-bar2-3.0.0b_rc2-r1.ebuild"
packageIdToFilePath :: PackageId -> FilePath
packageIdToFilePath (PackageId (PackageName cat pn) version) =
prettyShow cat </> prettyShow pn </> prettyShow pn <-> prettyShow version <.> "ebuild"
where
a <-> b = a ++ '-':b
a <.> b = a ++ '.':b
-- | Maybe generate a 'PackageId' from a 'FilePath'. Note that the 'FilePath' must have its
-- file extension stripped before being passed to 'filePathToPackageId'.
--
-- >>> filePathToPackageId (Category "dev-haskell") "foo-bar2-3.0.0b_rc2-r1"
-- Just (PackageId {packageId = PackageName {category = Category {unCategory = "dev-haskell"}, cabalPkgName = PackageName "foo-bar2"}, pkgVersion = Version {versionNumber = [3,0,0], versionChar = Just 'b', versionSuffix = [RC 2], versionRevision = 1}})
filePathToPackageId :: Category -> FilePath -> Maybe PackageId
filePathToPackageId cat fp =
case explicitEitherParsec parser fp of
Right x -> Just x
_ -> Nothing
where
parser = do
pn <- parseCabalPackageName
_ <- P.char '-'
v <- parsec
return $ PackageId (PackageName cat pn) v
-- | Create a 'PackageName' from supplied category and package name 'String's.
mkPackageName :: String -> String -> PackageName
mkPackageName cat package = PackageName (Category cat) (Cabal.mkPackageName package)
-- | Create a 'PackageId' from a 'Category' and 'Cabal.PackageIdentifier'.
fromCabalPackageId :: Category -> Cabal.PackageIdentifier -> PackageId
fromCabalPackageId cat (Cabal.PackageIdentifier name version) =
PackageId (PackageName cat (normalizeCabalPackageName name))
(Portage.fromCabalVersion version)
-- | Convert a 'Cabal.PackageName' into lowercase. Internally uses
-- 'cabal_pn_to_PN'.
--
-- >>> normalizeCabalPackageName (Cabal.mkPackageName "FooBar1")
-- PackageName "foobar1"
normalizeCabalPackageName :: Cabal.PackageName -> Cabal.PackageName
normalizeCabalPackageName =
Cabal.mkPackageName . cabal_pn_to_PN
-- | Apply 'normalizeCabalPackageName' to the 'Cabal.PackageName' of
-- a supplied 'Cabal.PackageIdentifier'.
normalizeCabalPackageId :: Cabal.PackageIdentifier -> Cabal.PackageIdentifier
normalizeCabalPackageId (Cabal.PackageIdentifier name version) =
Cabal.PackageIdentifier (normalizeCabalPackageName name) version
-- | Convert a 'PackageId' into a 'Maybe' 'Cabal.PackageIdentifier'.
toCabalPackageId :: PackageId -> Maybe Cabal.PackageIdentifier
toCabalPackageId (PackageId (PackageName _cat name) version) =
fmap (Cabal.PackageIdentifier name)
(Portage.toCabalVersion version)
-- | Parse a 'String' as a package in the form of @[category\/]name[-version]@:
--
-- Note that we /cannot/ use the 'parsec' function to parse the 'Cabal.PackageName',
-- since it fails the entire parse if it tries to parse a 'Version'.
-- See 'parseCabalPackageName' below.
--
-- If parsing a valid package string:
--
-- >>> parseFriendlyPackage "category-name/package-name1-0.0.0.1a_beta2-r4"
-- Right (Just (Category {unCategory = "category-name"}),PackageName "package-name1",Just (Version {versionNumber = [0,0,0,1], versionChar = Just 'a', versionSuffix = [Beta 2], versionRevision = 4}))
--
-- If malformed, return an error string:
--
-- >>> parseFriendlyPackage "category-name/package-name-1-0.0.0.1a_beta2-r4"
-- Left ...
parseFriendlyPackage :: String -> Either String (Maybe Category, Cabal.PackageName, Maybe Portage.Version)
parseFriendlyPackage str = explicitEitherParsec parser str
where
parser = do
mc <- P.optional . P.try $ do
c <- parsec
_ <- P.char '/'
return c
p <- parseCabalPackageName
mv <- P.optional $ do
_ <- P.char '-'
v <- parsec
return v
return (mc, p, mv)
-- | Parse a 'Cabal.PackageName'.
--
-- This parser is a replacement for 'parsecUnqualComponentName' which fails when
-- trying to parse a 'Version'.
parseCabalPackageName :: CabalParsing m => m Cabal.PackageName
parseCabalPackageName = do
pn <- P.some . P.try $
P.choice
[ P.alphaNum
, P.char '+'
, P.char '-' <* P.notFollowedBy (P.some P.digit <* P.notFollowedBy P.letter)
]
return $ Cabal.mkPackageName pn
-- | Pretty-print a lowercase 'Cabal.PackageName'.
--
-- Note the difference between this function and 'normalizeCabalPackageName':
-- this function returns a 'String', the other a 'Cabal.PackageName'.
--
-- >>> cabal_pn_to_PN (Cabal.mkPackageName "FooBar1")
-- "foobar1"
cabal_pn_to_PN :: Cabal.PackageName -> String
cabal_pn_to_PN = map Char.toLower . prettyShow