packages feed

cabal-debian-1.25: Distribution/Package/Debian/Dependencies.hs

{-# LANGUAGE MultiParamTypeClasses, StandaloneDeriving #-}
{-# OPTIONS -Wall -Wwarn -fno-warn-name-shadowing -fno-warn-orphans #-}
module Distribution.Package.Debian.Dependencies
    ( PackageType(..)
    , VersionSplits(..)
    , dependencies
    , mkPkgName
    , invertVersionRange
    -- , debianName
    , debianSourcePackageName
    , DebianBinPackageName
    , debianDevPackageName
    , debianProfPackageName
    , debianDocPackageName
    , debianExtraPackageName
    , debianUtilsPackageName
    ) where

import Data.Char (toLower)
import Data.Function (on)
import Data.List (intersperse, minimumBy)
import qualified Data.Map as Map
import Data.Maybe (catMaybes)
import Data.Version (showVersion)
import Debian.Relation (Relations, Relation, BinPkgName(BinPkgName), PkgName(PkgName), VersionReq(..), SrcPkgName(..))
import qualified Debian.Relation as D
import Debian.Version (DebianVersion, parseDebianVersion, prettyDebianVersion)
import Distribution.Package (PackageName(PackageName))
import Distribution.Package.Debian.Bundled (ghcBuiltIn)
import Distribution.Package.Debian.Interspersed (Interspersed(..))
import Distribution.Package.Debian.Setup (Flags(..))
import Distribution.Simple.Compiler (Compiler(..))
import Distribution.Version (Version(..), VersionRange(..), anyVersion, foldVersionRange', intersectVersionRanges, unionVersionRanges,
                             laterVersion, orLaterVersion, earlierVersion, orEarlierVersion, fromVersionIntervals, toVersionIntervals, withinVersion,
                             isNoVersion, asVersionIntervals, mkVersionIntervals, LowerBound(..), UpperBound(..), Bound(..))
import Text.PrettyPrint (Doc, text, hcat , (<>), empty)

data PackageType = Source | Development | Profiling | Documentation | Utilities | Extra deriving (Eq, Show)

data VersionSplits
    = VersionSplits {
        packageName :: PackageName
      , oldestPackage :: BinPkgName
      , splits :: [(Version, BinPkgName)] -- Assumed to be in version number order
      }

instance Interspersed VersionSplits BinPkgName Version where
    leftmost (VersionSplits {splits = []}) = error "Empty Interspersed instance"
    leftmost (VersionSplits {oldestPackage = p}) = p
    pairs (VersionSplits {splits = xs}) = xs

-- | Turn a cabal dependency into debian dependencies.  The result
-- needs to correspond to a single debian package to be installed,
-- so we will return just an OrRelation.
dependencies :: Flags -> Compiler -> (PackageType -> [VersionSplits]) -> PackageType -> Either BinPkgName PackageName -> VersionRange -> Relations
dependencies flags compiler versionSplits typ (Left name) cabalRange = [[D.Rel name Nothing Nothing]]
dependencies flags compiler versionSplits typ (Right name@(PackageName string)) cabalRange =
    map doBundled $ convert' (canonical (Or (catMaybes (map convert alts))))
    where

      -- Compute a list of alternative debian dependencies for
      -- satisfying a cabal dependency.  The only caveat is that
      -- we may need to distribute any "and" dependencies implied
      -- by a version range over these "or" dependences.
      alts :: [(BinPkgName, VersionRange)]
      alts = case Map.lookup name (packageSplits versionSplits typ) of
               -- If there are no splits for this package just return the single dependency for the package
               Nothing -> [(mkPkgName string typ, cabalRange')]
               -- If there are splits create a list of (debian package name, VersionRange) pairs
               Just splits -> packageRangesFromVersionSplits splits

      convert :: (BinPkgName, VersionRange) -> Maybe (Rels Relation)
      convert (dname, range) =
          if isNoVersion range'''
          then Nothing
          else Just $
               foldVersionRange'
                 (Rel (D.Rel dname Nothing Nothing))
                 (\ v -> Rel (D.Rel dname (Just (D.EEQ (dv v))) Nothing))
                 (\ v -> Rel (D.Rel dname (Just (D.SGR (dv v))) Nothing))
                 (\ v -> Rel (D.Rel dname (Just (D.SLT (dv v))) Nothing))
                 (\ v -> Rel (D.Rel dname (Just (D.GRE (dv v))) Nothing))
                 (\ v -> Rel (D.Rel dname (Just (D.LTE (dv v))) Nothing))
                 (\ x y -> And [Rel (D.Rel dname (Just (D.GRE (dv x))) Nothing), Rel (D.Rel dname (Just (D.SLT (dv y))) Nothing)])
                 (\ x y -> Or [x, y])
                 (\ x y -> And [x, y])
                 id
                 range'''
          where 
            -- Choose the simpler of the two
            range''' = canon (simpler range' range'')
            -- Unrestrict the range for versions that we know don't exist for this debian package
            range'' = canon (unionVersionRanges range' (invertVersionRange range))
            -- Restrict the range to the versions specified for this debian package
            range' = intersectVersionRanges cabalRange' range
            -- When we see a cabal equals dependency we need to turn it into
            -- a wildcard because the resulting debian version numbers have
            -- various suffixes added.
      cabalRange' =
          foldVersionRange'
            anyVersion
            withinVersion  -- <- Here we are turning equals into wildcard
            laterVersion
            earlierVersion
            orLaterVersion
            orEarlierVersion
            (\ lb ub -> intersectVersionRanges (orLaterVersion lb) (earlierVersion ub))
            unionVersionRanges
            intersectVersionRanges
            id
            cabalRange
      -- Convert a cabal version to a debian version, adding an epoch number if requested
      dv v = parseDebianVersion (maybe "" (\ n -> show n ++ ":") (Map.lookup name (epochMap flags)) ++ showVersion v)
      simpler v1 v2 = minimumBy (compare `on` (length . asVersionIntervals)) [v1, v2]
      -- Simplify a VersionRange
      canon = fromVersionIntervals . toVersionIntervals

      -- If a package is bundled with the compiler we make the
      -- compiler a substitute for that package.  If we were to
      -- specify the virtual package (e.g. libghc-base-dev) we would
      -- have to make sure not to specify a version number.
      doBundled :: [D.Relation] -> [D.Relation]
      doBundled rels | ghcBuiltIn compiler name = rels ++ [D.Rel (compilerPackageName typ) Nothing Nothing]
      doBundled rels = rels

      compilerPackageName Documentation = D.BinPkgName (D.PkgName "ghc-doc")
      compilerPackageName Profiling = D.BinPkgName (D.PkgName "ghc-prof")
      compilerPackageName Development = D.BinPkgName (D.PkgName "ghc")
      compilerPackageName _ = D.BinPkgName (D.PkgName "ghc") -- whatevs

data Rels a = And {unAnd :: [Rels a]} | Or {unOr :: [Rels a]} | Rel {unRel :: a} deriving Show

-- | The intent of this class is to be similar to Show, but only one
-- way, with no corresponding Read class.  To put something in a
-- pretty printing class implies that there is only one way to pretty
-- print it, which is not an assumption made by Text.PrettyPrint.  But
-- in practice this is often good enough.
class Pretty x where
    pretty :: x -> Doc

-- | return and of ors of rel
canonical :: Rels a -> Rels a
canonical (Rel rel) = And [Or [Rel rel]]
canonical (And rels) = And $ concatMap (unAnd . canonical) rels
canonical (Or rels) = And . map Or $ sequence $ map (concat . map unOr . unAnd . canonical) $ rels

convert' :: Rels a -> [[a]]
convert' = map (map unRel . unOr) . unAnd . canonical

packageSplits :: (PackageType -> [VersionSplits]) -> PackageType -> Map.Map PackageName VersionSplits
packageSplits versionSplits typ =
    foldr (\ splits mp -> Map.insertWith multipleSplitsError (packageName splits) splits mp)
          Map.empty
          (versionSplits typ)
    where
      multipleSplitsError (VersionSplits {packageName = PackageName p}) _s2 =
          error ("Multiple splits for package " ++ show p)

packageRangesFromVersionSplits :: VersionSplits -> [(BinPkgName, VersionRange)]
packageRangesFromVersionSplits splits =
    foldInverted (\ older dname newer more ->
                      (dname, intersectVersionRanges (maybe anyVersion orLaterVersion older) (maybe anyVersion earlierVersion newer)) : more)
                 []
                 splits

-- | Build a debian package name from a cabal package name and a
-- debian package type.
mkPkgName :: String -> PackageType -> BinPkgName
mkPkgName base typ =
    BinPkgName . PkgName $ prefix typ ++ map toLower base ++ suffix typ
    where
      suffix Source = ""
      suffix Documentation = "-doc"
      suffix Development = "-dev"
      suffix Profiling = "-prof"
      suffix Utilities = "-utils"
      suffix Extra = ""

      prefix Source = "haskell-"
      prefix Documentation = "libghc-"
      prefix Development = "libghc-"
      prefix Profiling = "libghc-"
      prefix Utilities = "haskell-"
      prefix Extra = ""

instance Pretty VersionRange where
    pretty range =
        foldVersionRange'
          (text "*")
          (\ v -> text "=" <> pretty v)
          (\ v -> text ">" <> pretty v)
          (\ v -> text "<" <> pretty v)
          (\ v -> text ">=" <> pretty v)
          (\ v -> text "<=" <> pretty v)
          (\ x _ -> text "=" <> pretty x <> text ".*") -- not exactly right
          (\ x y -> text "(" <> x <> text " || " <> y <> text ")")
          (\ x y -> text "(" <> x <> text " && " <> y <> text ")")
          (\ x -> text "(" <> x <> text ")")
          range

instance Pretty Version where
    pretty = text . showVersion

instance Pretty a => Pretty [a] where
    pretty xs = text "[" <> hcat (intersperse (text ", ") (map pretty xs)) <> text "]"

instance (Pretty a, Pretty b) => Pretty (a, b) where
    pretty (a, b) = text "(" <> pretty a <> text ", " <> pretty b <> text ")"

instance Pretty D.BinPkgName where
    pretty (D.BinPkgName p) = text "deb:" <> (pretty p)

instance Pretty D.PkgName where
    pretty (D.PkgName p) = text p

instance Pretty D.Relation where
    pretty (D.Rel name ver arch) =
        pretty name <> maybe empty pretty ver <> maybe empty pretty arch

instance Pretty D.VersionReq where
    pretty (D.EEQ v) = text "=" <> pretty v
    pretty (D.SLT v) = text "<" <> pretty v
    pretty (D.LTE v) = text "<=" <> pretty v
    pretty (D.GRE v) = text ">=" <> pretty v
    pretty (D.SGR v) = text ">" <> pretty v

instance Pretty D.ArchitectureReq where
    pretty (D.ArchOnly ss) = text "[" <> hcat (intersperse (text ",") (map text ss)) <> text "]"
    pretty (D.ArchExcept ss) = text "[!" <> hcat (intersperse (text ",") (map text ss)) <> text "]"

instance Pretty DebianVersion where
    pretty = text . show

instance Show D.Relation where
    show = show . pretty
instance Show D.ArchitectureReq where
    show = show . pretty

invertVersionRange :: VersionRange -> VersionRange
invertVersionRange = fromVersionIntervals . maybe (error "invertVersionRange") id . mkVersionIntervals . invertVersionIntervals . asVersionIntervals

invertVersionIntervals :: [(LowerBound, UpperBound)] -> [(LowerBound, UpperBound)]
invertVersionIntervals xs =
    case xs of
      [] -> [(lb0, NoUpperBound)]
      ((LowerBound (Version {versionBranch = [0], versionTags = []}) InclusiveBound, ub) : more) ->
          invertVersionIntervals' ub more
      ((lb, ub) : more) ->
          (lb0, invertLowerBound lb) : invertVersionIntervals' ub more
    where
      invertVersionIntervals' :: UpperBound -> [(LowerBound, UpperBound)] -> [(LowerBound, UpperBound)]
      invertVersionIntervals' NoUpperBound [] = []
      invertVersionIntervals' ub0 [] = [(invertUpperBound ub0, NoUpperBound)]
      invertVersionIntervals' ub0 [(lb, NoUpperBound)] = [(invertUpperBound ub0, invertLowerBound lb)]
      invertVersionIntervals' ub0 ((lb, ub1) : more) = (invertUpperBound ub0, invertLowerBound lb) : invertVersionIntervals' ub1 more

      invertLowerBound :: LowerBound -> UpperBound
      invertLowerBound (LowerBound v b) = UpperBound v (invertBound b)

      invertUpperBound :: UpperBound -> LowerBound
      invertUpperBound (UpperBound v b) = LowerBound v (invertBound b)
      invertUpperBound NoUpperBound = error "NoUpperBound: unexpected"

      invertBound :: Bound -> Bound
      invertBound ExclusiveBound = InclusiveBound
      invertBound InclusiveBound = ExclusiveBound

      lb0 :: LowerBound
      lb0 = LowerBound (Version {versionBranch = [0], versionTags = []}) InclusiveBound

deriving instance Show VersionReq
instance Show DebianVersion where
    show = show . prettyDebianVersion

debianSourcePackageName :: (PackageType -> [VersionSplits]) -> PackageName -> Maybe VersionReq -> SrcPkgName
debianSourcePackageName versionSplits name version = SrcPkgName (D.unBinPkgName (debianName Source versionSplits name version))

debianDevPackageName :: (PackageType -> [VersionSplits]) -> PackageName -> Maybe VersionReq -> BinPkgName
debianDevPackageName versionSplits name version = debianName Development versionSplits name version

debianProfPackageName :: (PackageType -> [VersionSplits]) -> PackageName -> Maybe VersionReq -> BinPkgName
debianProfPackageName versionSplits name version = debianName Profiling versionSplits name version

debianDocPackageName :: (PackageType -> [VersionSplits]) -> PackageName -> Maybe VersionReq -> BinPkgName
debianDocPackageName versionSplits name version = debianName Documentation versionSplits name version

type DebianBinPackageName = PackageName -> Maybe VersionReq -> BinPkgName

debianExtraPackageName :: (PackageType -> [VersionSplits]) -> PackageName -> Maybe VersionReq -> BinPkgName
debianExtraPackageName versionSplits name version = debianName Extra versionSplits name version

debianUtilsPackageName :: (PackageType -> [VersionSplits]) -> PackageName -> Maybe VersionReq -> BinPkgName
debianUtilsPackageName versionSplits name version = debianName Utilities versionSplits name version

-- | Return the basename of the debian package for a given version
-- relation.  If the version split happens at v, this will return the
-- ltName is < v and the geName if the relation is >= v.  It also handles
-- a special case for the name of the haskell-src-exts package.
debianName :: PackageType -> (PackageType -> [VersionSplits]) -> PackageName -> Maybe VersionReq -> BinPkgName
debianName typ versionSplits pname@(PackageName name) version =
    case filter (\ x -> pname == packageName x) (versionSplits typ) of
      [] -> def
      [splits] ->
          foldTriples' (\ ltName v geName debName ->
                           if pname /= packageName splits
                           then debName
                           else let split = parseDebianVersion (showVersion v) in
                                case version of
                                  Nothing -> geName
                                  Just (SLT v') | v' <= split -> ltName
                                  -- Otherwise use ltName only when the split is below v'
                                  Just (EEQ v') | v' < split -> ltName
                                  Just (LTE v') | v' < split -> ltName
                                  Just (GRE v') | v' < split -> ltName
                                  Just (SGR v') | v' < split -> ltName
                                  _ -> geName)
                       def
                       splits
      _ -> error $ "Multiple splits for cabal package " ++ name
    where
      foldTriples' :: (BinPkgName -> Version -> BinPkgName -> BinPkgName -> BinPkgName) -> BinPkgName -> VersionSplits -> BinPkgName
      foldTriples' = foldTriples
      def = mkPkgName (map fixChar name) typ

fixChar :: Char -> Char
fixChar '_' = '-'
fixChar c = toLower c