packages feed

packdeps 0.4.3 → 0.4.4

raw patch · 5 files changed

+410/−370 lines, 5 filesdep +optparse-applicativedep +processdep +semigroupsdep ~Cabaldep ~containers

Dependencies added: optparse-applicative, process, semigroups

Dependency ranges changed: Cabal, containers

Files

ChangeLog.md view
@@ -1,3 +1,8 @@+## 0.4.4++* Switch over to Cabal 2.0. (Normally it would be better to keep+  backwards compat, but the API breakage is too high.)+ ## 0.4.3  * Updates (better cabal config parsing) [#30](https://github.com/snoyberg/packdeps/pull/30)
− Distribution/PackDeps.hs
@@ -1,331 +0,0 @@-module Distribution.PackDeps-    ( -- * Data types-      Newest-    , CheckDepsRes (..)-    , DescInfo-      -- * Read package database-    , loadNewest-    , loadNewestFrom-    , parseNewest-      -- * Check a package-    , checkDeps-    , checkLibDeps-      -- * Get a single package-    , getPackage-    , parsePackage-    , loadPackage-      -- * Get multiple packages-    , filterPackages-    , deepDeps-    , deepLibDeps-      -- * Reverse dependencies-    , Reverses-    , getReverses-      -- * Helpers-    , diName-      -- * Internal-    , PackInfo (..)-    , piRevision-    , DescInfo (..)-    ) where--import Control.Applicative as A ((<$>))-import System.Directory (getAppUserDataDirectory, doesFileExist)-import System.FilePath ((</>))-import qualified Data.Map as Map-import Data.List (foldl', group, sort, isInfixOf)-import Data.Time (UTCTime (UTCTime), addUTCTime)-import Data.Maybe (mapMaybe, fromMaybe)-import Control.Exception (throw)--import Distribution.Package-import Distribution.PackageDescription-import Distribution.PackageDescription.Parse-import Distribution.Version-import Distribution.Text-import qualified Distribution.ParseUtils as PU--import Data.Char (toLower)-import qualified Data.Text.Lazy as T-import qualified Data.Text.Lazy.Encoding as T-import qualified Data.Text.Encoding.Error as T-import qualified Data.ByteString.Lazy as L--import Data.List.Split (splitOn)-import qualified Codec.Archive.Tar as Tar-import qualified Codec.Archive.Tar.Entry as Tar--import Data.Function (on)-import Control.Arrow ((&&&))-import Data.List (groupBy, sortBy)-import Data.Ord (comparing)-import qualified Data.Set as Set-import Text.Read (readMaybe)--- import Data.Monoid ((<>))--loadNewest :: IO Newest-loadNewest = do-    c <- getAppUserDataDirectory "cabal"-    cfg' <- readFile (c </> "config")-    cfg <- parseResult (fail . show) return $ PU.readFields cfg'-    let repos        = reposFromConfig cfg-        repoCache    = case lookupInConfig "remote-repo-cache" cfg of-            []        -> c </> "packages"  -- Default-            (rrc : _) -> rrc               -- User-specified-        tarNames repo = [ pfx </> "01-index.tar", pfx </> "00-index.tar" ]-          where pfx = repoCache </> repo-    fmap (Map.unionsWith maxVersion) . mapM (loadNewestFrom . tarNames) $ repos---- | Takes a list of possible pathes, tries them in order until one exists.-loadNewestFrom :: [FilePath] -> IO Newest-loadNewestFrom []         = fail "loadNewestFrom: no index tarball"-loadNewestFrom (fp : fps) = do-    e <- doesFileExist fp-    if e-        then fmap parseNewest (L.readFile fp)-        else loadNewestFrom fps--parseNewest :: L.ByteString -> Newest-parseNewest = foldl' addPackage Map.empty . entriesToList . Tar.read--entriesToList :: Tar.Entries Tar.FormatError -> [Tar.Entry]-entriesToList Tar.Done = []-entriesToList (Tar.Fail s) = throw s-entriesToList (Tar.Next e es) = e : entriesToList es--addPackage :: Newest -> Tar.Entry -> Newest-addPackage m entry =-    case splitOn "/" $ Tar.fromTarPathToPosixPath (Tar.entryTarPath entry) of-        [package', versionS, _] ->-            case simpleParse versionS of-                Just version ->-                    case Map.lookup package' m of-                        Nothing -> go package' version-                        Just PackInfo { piVersion = oldv } ->-                            -- in 01-index.tar there are entries with the same versions-                            if version >= oldv-                                then go package' version-                                else m-                Nothing -> m-        _ -> m-  where-    go package' version =-        case Tar.entryContent entry of-            Tar.NormalFile bs _ ->-                Map.insertWith maxVersion package' PackInfo-                        { piVersion = version-                        , piDesc = parsePackage bs-                        , piEpoch = Tar.entryTime entry-                        } m-            _ -> m--data PackInfo = PackInfo-    { piVersion  :: Version-    , piDesc     :: Maybe DescInfo-    , piEpoch    :: Tar.EpochTime-    }-    deriving (Show, Read)---- Extract revision from PackInfo, default to 0-piRevision :: PackInfo -> Int-piRevision = fromMaybe 0 . fmap diRevision . piDesc---- We should compare revisions as well,--- but we don't do that, as it would force `piDesc` and make `packdeps`--- executable increadibly slow (parse all cabal files on Hackage)--- Instead we rely on the fact that newer revisions are always later in--- 01-index.tar-maxVersion :: PackInfo -> PackInfo -> PackInfo-maxVersion pi1 pi2 = if piVersion pi1 <= piVersion pi2 then pi2 else pi1---- | The newest version of every package.-type Newest = Map.Map String PackInfo--type Reverses = Map.Map String (Version, [(String, VersionRange)])--getReverses :: Newest -> Reverses-getReverses newest =-    Map.fromList withVersion-  where-    -- dep = dependency, rel = relying package-    toTuples (_, PackInfo { piDesc = Nothing }) = []-    toTuples (rel, PackInfo { piDesc = Just DescInfo { diDeps = deps } }) =-        map (toTuple rel) deps-    toTuple rel (Dependency (PackageName dep) range) = (dep, (rel, range))-    hoist :: Ord a => [(a, b)] -> [(a, [b])]-    hoist = map ((fst . head) &&& map snd)-          . groupBy ((==) `on` fst)-          . sortBy (comparing fst)-    hoisted = hoist $ concatMap toTuples $ Map.toList newest-    withVersion = mapMaybe addVersion hoisted-    addVersion (dep, rels) =-        case Map.lookup dep newest of-            Nothing -> Nothing-            Just PackInfo { piVersion = v} -> Just (dep, (v, rels))---- | Information on a single package.-data DescInfo = DescInfo-    { diHaystack :: String-    , diDeps     :: [Dependency]-    , diLibDeps  :: [Dependency]-    , diPackage  :: PackageIdentifier-    , diRevision :: Int-    , diSynopsis :: String-    }-    deriving (Show, Read)---- | Return revision and DescInfo-getDescInfo :: GenericPackageDescription -> DescInfo-getDescInfo gpd = DescInfo-    { diHaystack = map toLower $ author p ++ maintainer p ++ name-    , diDeps     = getDeps gpd-    , diLibDeps  = getLibDeps gpd-    , diPackage  = pi'-    , diRevision = rev-    , diSynopsis = synopsis p-    }-  where-    p = packageDescription gpd-    pi'@(PackageIdentifier (PackageName name) _) = package p-    rev = fromMaybe 0 $ do-        r <- lookup "x-revision" (customFieldsPD p)-        readMaybe r--getDeps :: GenericPackageDescription -> [Dependency]-getDeps x = getLibDeps x ++ concat-    [ concatMap (condTreeConstraints . snd) (condExecutables x)-    , concatMap (condTreeConstraints . snd) (condTestSuites x)-    , concatMap (condTreeConstraints . snd) (condBenchmarks x)-    ]--getLibDeps :: GenericPackageDescription -> [Dependency]-getLibDeps gpd = maybe [] condTreeConstraints (condLibrary gpd)--checkDeps :: Newest -> DescInfo-          -> (PackageName, Version, CheckDepsRes)-checkDeps = checkDepsImpl diDeps--checkLibDeps :: Newest -> DescInfo-             -> (PackageName, Version, CheckDepsRes)-checkLibDeps = checkDepsImpl diLibDeps--checkDepsImpl :: (DescInfo -> [Dependency]) -> Newest -> DescInfo-              -> (PackageName, Version, CheckDepsRes)-checkDepsImpl deps newest desc =-    case mapMaybe (notNewest newest) $ deps desc of-        [] -> (name, version, AllNewest)-        x -> let y = map head $ group $ sort $ map fst x-                 et = maximum $ map snd x-              in (name, version, WontAccept y $ epochToTime et)-  where-    PackageIdentifier name version = diPackage desc---- | Whether or not a package can accept all of the newest versions of its--- dependencies. If not, it returns a list of packages which are not accepted,--- and a timestamp of the most recently updated package.-data CheckDepsRes = AllNewest-                  | WontAccept [(String, String)] UTCTime-    deriving Show--epochToTime :: Tar.EpochTime -> UTCTime-epochToTime e = addUTCTime (fromIntegral e) $ UTCTime (read "1970-01-01") 0--notNewest :: Newest -> Dependency -> Maybe ((String, String), Tar.EpochTime)-notNewest newest (Dependency (PackageName s) range) =-    case Map.lookup s newest of-        --Nothing -> Just ((s, " no version found"), 0)-        Nothing -> Nothing-        Just PackInfo { piVersion = version, piEpoch = e } ->-            if withinRange version range-                then Nothing-                else Just ((s, display version), e)---- | Loads up the newest version of a package from the 'Newest' list, if--- available.-getPackage :: String -> Newest -> Maybe DescInfo-getPackage s n = Map.lookup s n >>= piDesc---- | Parse information on a package from the contents of a cabal file.-parsePackage :: L.ByteString -> Maybe DescInfo-parsePackage lbs =-    case parsePackageDescription $ T.unpack-       $ T.decodeUtf8With T.lenientDecode lbs of-        ParseOk _ x -> Just $ getDescInfo x-        _ -> Nothing---- | Load a single package from a cabal file.-loadPackage :: FilePath -> IO (Maybe DescInfo)-loadPackage = fmap parsePackage . L.readFile---- | Find all of the packages matching a given search string.-filterPackages :: String -> Newest -> [DescInfo]-filterPackages needle =-    mapMaybe go . Map.elems-  where-    needle' = map toLower needle-    go PackInfo { piDesc = Just desc } =-        if needle' `isInfixOf` diHaystack desc &&-           not ("(deprecated)" `isInfixOf` diSynopsis desc)-            then Just desc-            else Nothing-    go _ = Nothing---- | Find all packages depended upon by the given list of packages.-deepDeps :: Newest -> [DescInfo] -> [DescInfo]-deepDeps = deepDepsImpl diDeps---- | Find all packages depended upon by the given list of packages.-deepLibDeps :: Newest -> [DescInfo] -> [DescInfo]-deepLibDeps = deepDepsImpl diLibDeps--deepDepsImpl :: (DescInfo -> [Dependency]) -> Newest -> [DescInfo] -> [DescInfo]-deepDepsImpl deps newest dis0 =-    go Set.empty dis0-  where-    go _ [] = []-    go viewed (di:dis)-        | name `Set.member` viewed = go viewed dis-        | otherwise = di : go viewed' (newDis ++ dis)-      where-        PackageIdentifier (PackageName name) _ = diPackage di-        viewed' = Set.insert name viewed-        newDis = mapMaybe getDI $ deps di-        getDI :: Dependency -> Maybe DescInfo-        getDI (Dependency (PackageName name') _) = do-            pi' <- Map.lookup name' newest-            piDesc pi'--diName :: DescInfo -> String-diName =-    unPN . pkgName . diPackage-  where-    unPN (PackageName pn) = pn------------------------------------------------------------------------------------ ~/.cabal/config parsing----------------------------------------------------------------------------------reposFromConfig :: [PU.Field] -> [String]-reposFromConfig fields = takeWhile (/= ':') A.<$> mapMaybe f fields-  where-    f (PU.F _lineNo name value)-        | name == "remote-repo"-        = Just value-    f (PU.Section _lineNo secName arg _fields)-        | secName == "repository"-        = Just arg-    f _ = Nothing---- | Looks up the given key in the cabal configuration file-lookupInConfig :: String -> [PU.Field] -> [String]-lookupInConfig key = mapMaybe f-  where-    f (PU.F _lineNo name value)-        | name == key-        = Just value-    f _ = Nothing---- | Like 'either', but for 'ParseResult'-parseResult :: (PU.PError -> r) -> (a -> r) -> ParseResult a -> r-parseResult l _ (PU.ParseFailed e)    = l e-parseResult _ r (PU.ParseOk _warns x) = r x
packdeps.cabal view
@@ -1,5 +1,5 @@ name:            packdeps-version:         0.4.3+version:         0.4.4 license:         BSD3 license-file:    LICENSE author:          Michael Snoyman <michael@snoyman.com>@@ -17,12 +17,13 @@ extra-source-files: README.md ChangeLog.md  library+    hs-source-dirs: src     build-depends:   base                      >= 4.6      && < 5                    , tar                       >= 0.4      && < 0.6                    , split                     >= 0.1.2.3                    , bytestring                >= 0.9                    , text                      >= 0.7-                   , Cabal                     >= 1.14     && <1.25+                   , Cabal                     >= 2.0      && < 2.1                    , time                      >= 1.1.4                    , containers                >= 0.2                    , directory                 >= 1.0@@ -38,6 +39,10 @@      build-depends:   base                      >= 4        && < 5                    , Cabal+                   , optparse-applicative      >=0.14 && <0.15+                   , containers+                   , semigroups+                   , process                    , packdeps  source-repository head
+ src/Distribution/PackDeps.hs view
@@ -0,0 +1,329 @@+{-# LANGUAGE ViewPatterns #-}+module Distribution.PackDeps+    ( -- * Data types+      Newest+    , CheckDepsRes (..)+    , DescInfo+      -- * Read package database+    , loadNewest+    , loadNewestFrom+    , parseNewest+      -- * Check a package+    , checkDeps+    , checkLibDeps+      -- * Get a single package+    , getPackage+    , parsePackage+    , loadPackage+      -- * Get multiple packages+    , filterPackages+    , deepDeps+    , deepLibDeps+      -- * Reverse dependencies+    , Reverses+    , getReverses+      -- * Helpers+    , diName+      -- * Internal+    , PackInfo (..)+    , piRevision+    , DescInfo (..)+    ) where++import Control.Applicative as A ((<$>))+import System.Directory (getAppUserDataDirectory, doesFileExist)+import System.FilePath ((</>))+import qualified Data.Map as Map+import Data.List (foldl', group, sort, isInfixOf)+import Data.Time (UTCTime (UTCTime), addUTCTime)+import Data.Maybe (mapMaybe, fromMaybe)+import Control.Exception (throw)++import Distribution.Package+import Distribution.PackageDescription+import Distribution.PackageDescription.Parse+import Distribution.Version+import Distribution.Text+import qualified Distribution.ParseUtils as PU++import Data.Char (toLower)+import qualified Data.Text.Lazy as T+import qualified Data.Text.Lazy.Encoding as T+import qualified Data.Text.Encoding.Error as T+import qualified Data.ByteString.Lazy as L++import Data.List.Split (splitOn)+import qualified Codec.Archive.Tar as Tar+import qualified Codec.Archive.Tar.Entry as Tar++import Data.Function (on)+import Control.Arrow ((&&&))+import Data.List (groupBy, sortBy)+import Data.Ord (comparing)+import qualified Data.Set as Set+import Text.Read (readMaybe)+-- import Data.Monoid ((<>))++loadNewest :: IO Newest+loadNewest = do+    c <- getAppUserDataDirectory "cabal"+    cfg' <- readFile (c </> "config")+    cfg <- parseResult (fail . show) return $ PU.readFields cfg'+    let repos        = reposFromConfig cfg+        repoCache    = case lookupInConfig "remote-repo-cache" cfg of+            []        -> c </> "packages"  -- Default+            (rrc : _) -> rrc               -- User-specified+        tarNames repo = [ pfx </> "01-index.tar", pfx </> "00-index.tar" ]+          where pfx = repoCache </> repo+    fmap (Map.unionsWith maxVersion) . mapM (loadNewestFrom . tarNames) $ repos++-- | Takes a list of possible pathes, tries them in order until one exists.+loadNewestFrom :: [FilePath] -> IO Newest+loadNewestFrom []         = fail "loadNewestFrom: no index tarball"+loadNewestFrom (fp : fps) = do+    e <- doesFileExist fp+    if e+        then fmap parseNewest (L.readFile fp)+        else loadNewestFrom fps++parseNewest :: L.ByteString -> Newest+parseNewest = foldl' addPackage Map.empty . entriesToList . Tar.read++entriesToList :: Tar.Entries Tar.FormatError -> [Tar.Entry]+entriesToList Tar.Done = []+entriesToList (Tar.Fail s) = throw s+entriesToList (Tar.Next e es) = e : entriesToList es++addPackage :: Newest -> Tar.Entry -> Newest+addPackage m entry =+    case splitOn "/" $ Tar.fromTarPathToPosixPath (Tar.entryTarPath entry) of+        [package', versionS, _] ->+            case simpleParse versionS of+                Just version ->+                    case Map.lookup package' m of+                        Nothing -> go package' version+                        Just PackInfo { piVersion = oldv } ->+                            -- in 01-index.tar there are entries with the same versions+                            if version >= oldv+                                then go package' version+                                else m+                Nothing -> m+        _ -> m+  where+    go package' version =+        case Tar.entryContent entry of+            Tar.NormalFile bs _ ->+                Map.insertWith maxVersion package' PackInfo+                        { piVersion = version+                        , piDesc = parsePackage bs+                        , piEpoch = Tar.entryTime entry+                        } m+            _ -> m++data PackInfo = PackInfo+    { piVersion  :: Version+    , piDesc     :: Maybe DescInfo+    , piEpoch    :: Tar.EpochTime+    }+    deriving (Show, Read)++-- Extract revision from PackInfo, default to 0+piRevision :: PackInfo -> Int+piRevision = fromMaybe 0 . fmap diRevision . piDesc++-- We should compare revisions as well,+-- but we don't do that, as it would force `piDesc` and make `packdeps`+-- executable increadibly slow (parse all cabal files on Hackage)+-- Instead we rely on the fact that newer revisions are always later in+-- 01-index.tar+maxVersion :: PackInfo -> PackInfo -> PackInfo+maxVersion pi1 pi2 = if piVersion pi1 <= piVersion pi2 then pi2 else pi1++-- | The newest version of every package.+type Newest = Map.Map String PackInfo++type Reverses = Map.Map String (Version, [(String, VersionRange)])++getReverses :: Newest -> Reverses+getReverses newest =+    Map.fromList withVersion+  where+    -- dep = dependency, rel = relying package+    toTuples (_, PackInfo { piDesc = Nothing }) = []+    toTuples (rel, PackInfo { piDesc = Just DescInfo { diDeps = deps } }) =+        map (toTuple rel) deps+    toTuple rel (Dependency (unPackageName -> dep) range) = (dep, (rel, range))+    hoist :: Ord a => [(a, b)] -> [(a, [b])]+    hoist = map ((fst . head) &&& map snd)+          . groupBy ((==) `on` fst)+          . sortBy (comparing fst)+    hoisted = hoist $ concatMap toTuples $ Map.toList newest+    withVersion = mapMaybe addVersion hoisted+    addVersion (dep, rels) =+        case Map.lookup dep newest of+            Nothing -> Nothing+            Just PackInfo { piVersion = v} -> Just (dep, (v, rels))++-- | Information on a single package.+data DescInfo = DescInfo+    { diHaystack :: String+    , diDeps     :: [Dependency]+    , diLibDeps  :: [Dependency]+    , diPackage  :: PackageIdentifier+    , diRevision :: Int+    , diSynopsis :: String+    }+    deriving (Show, Read)++-- | Return revision and DescInfo+getDescInfo :: GenericPackageDescription -> DescInfo+getDescInfo gpd = DescInfo+    { diHaystack = map toLower $ author p ++ maintainer p ++ name+    , diDeps     = getDeps gpd+    , diLibDeps  = getLibDeps gpd+    , diPackage  = pi'+    , diRevision = rev+    , diSynopsis = synopsis p+    }+  where+    p = packageDescription gpd+    pi'@(PackageIdentifier (unPackageName -> name) _) = package p+    rev = fromMaybe 0 $ do+        r <- lookup "x-revision" (customFieldsPD p)+        readMaybe r++getDeps :: GenericPackageDescription -> [Dependency]+getDeps x = getLibDeps x ++ concat+    [ concatMap (condTreeConstraints . snd) (condExecutables x)+    , concatMap (condTreeConstraints . snd) (condTestSuites x)+    , concatMap (condTreeConstraints . snd) (condBenchmarks x)+    ]++getLibDeps :: GenericPackageDescription -> [Dependency]+getLibDeps gpd = maybe [] condTreeConstraints (condLibrary gpd)++checkDeps :: Newest -> DescInfo+          -> (PackageName, Version, CheckDepsRes)+checkDeps = checkDepsImpl diDeps++checkLibDeps :: Newest -> DescInfo+             -> (PackageName, Version, CheckDepsRes)+checkLibDeps = checkDepsImpl diLibDeps++checkDepsImpl :: (DescInfo -> [Dependency]) -> Newest -> DescInfo+              -> (PackageName, Version, CheckDepsRes)+checkDepsImpl deps newest desc =+    case mapMaybe (notNewest newest) $ deps desc of+        [] -> (name, version, AllNewest)+        x -> let y = map head $ group $ sort $ map fst x+                 et = maximum $ map snd x+              in (name, version, WontAccept y $ epochToTime et)+  where+    PackageIdentifier name version = diPackage desc++-- | Whether or not a package can accept all of the newest versions of its+-- dependencies. If not, it returns a list of packages which are not accepted,+-- and a timestamp of the most recently updated package.+data CheckDepsRes = AllNewest+                  | WontAccept [(String, String)] UTCTime+    deriving Show++epochToTime :: Tar.EpochTime -> UTCTime+epochToTime e = addUTCTime (fromIntegral e) $ UTCTime (read "1970-01-01") 0++notNewest :: Newest -> Dependency -> Maybe ((String, String), Tar.EpochTime)+notNewest newest (Dependency (unPackageName -> s) range) =+    case Map.lookup s newest of+        --Nothing -> Just ((s, " no version found"), 0)+        Nothing -> Nothing+        Just PackInfo { piVersion = version, piEpoch = e } ->+            if withinRange version range+                then Nothing+                else Just ((s, display version), e)++-- | Loads up the newest version of a package from the 'Newest' list, if+-- available.+getPackage :: String -> Newest -> Maybe DescInfo+getPackage s n = Map.lookup s n >>= piDesc++-- | Parse information on a package from the contents of a cabal file.+parsePackage :: L.ByteString -> Maybe DescInfo+parsePackage lbs =+    case parseGenericPackageDescription $ T.unpack+       $ T.decodeUtf8With T.lenientDecode lbs of+        ParseOk _ x -> Just $ getDescInfo x+        _ -> Nothing++-- | Load a single package from a cabal file.+loadPackage :: FilePath -> IO (Maybe DescInfo)+loadPackage = fmap parsePackage . L.readFile++-- | Find all of the packages matching a given search string.+filterPackages :: String -> Newest -> [DescInfo]+filterPackages needle =+    mapMaybe go . Map.elems+  where+    needle' = map toLower needle+    go PackInfo { piDesc = Just desc } =+        if needle' `isInfixOf` diHaystack desc &&+           not ("(deprecated)" `isInfixOf` diSynopsis desc)+            then Just desc+            else Nothing+    go _ = Nothing++-- | Find all packages depended upon by the given list of packages.+deepDeps :: Newest -> [DescInfo] -> [DescInfo]+deepDeps = deepDepsImpl diDeps++-- | Find all packages depended upon by the given list of packages.+deepLibDeps :: Newest -> [DescInfo] -> [DescInfo]+deepLibDeps = deepDepsImpl diLibDeps++deepDepsImpl :: (DescInfo -> [Dependency]) -> Newest -> [DescInfo] -> [DescInfo]+deepDepsImpl deps newest dis0 =+    go Set.empty dis0+  where+    go _ [] = []+    go viewed (di:dis)+        | name `Set.member` viewed = go viewed dis+        | otherwise = di : go viewed' (newDis ++ dis)+      where+        PackageIdentifier name _ = diPackage di+        viewed' = Set.insert name viewed+        newDis = mapMaybe getDI $ deps di+        getDI :: Dependency -> Maybe DescInfo+        getDI (Dependency name' _) = do+            pi' <- Map.lookup (unPackageName name') newest+            piDesc pi'++diName :: DescInfo -> String+diName = unPackageName . pkgName . diPackage++-------------------------------------------------------------------------------+-- ~/.cabal/config parsing+-------------------------------------------------------------------------------++reposFromConfig :: [PU.Field] -> [String]+reposFromConfig fields = takeWhile (/= ':') A.<$> mapMaybe f fields+  where+    f (PU.F _lineNo name value)+        | name == "remote-repo"+        = Just value+    f (PU.Section _lineNo secName arg _fields)+        | secName == "repository"+        = Just arg+    f _ = Nothing++-- | Looks up the given key in the cabal configuration file+lookupInConfig :: String -> [PU.Field] -> [String]+lookupInConfig key = mapMaybe f+  where+    f (PU.F _lineNo name value)+        | name == key+        = Just value+    f _ = Nothing++-- | Like 'either', but for 'ParseResult'+parseResult :: (PU.PError -> r) -> (a -> r) -> ParseResult a -> r+parseResult l _ (PU.ParseFailed e)    = l e+parseResult _ r (PU.ParseOk _warns x) = r x
tools/packdeps-cli.hs view
@@ -1,28 +1,46 @@-import Distribution.PackDeps+{-# LANGUAGE ViewPatterns #-}+import Control.Applicative (many, some) import Control.Monad (forM_, foldM, when)-import System.Environment (getArgs, getProgName)-import System.Exit (exitFailure, exitSuccess)-import Distribution.Text (display)-import Distribution.Package (PackageName (PackageName))-import Distribution.Version (Version) import Control.Monad (liftM)+import Data.List (foldl')+import Data.Maybe (mapMaybe)+import Data.Semigroup ((<>))+import Distribution.Compat.ReadP (readP_to_S)+import Distribution.PackDeps+import Distribution.Package (PackageIdentifier (PackageIdentifier), PackageName, unPackageName)+import Distribution.Text (display, parse)+import Distribution.Version (Version)+import System.Exit (exitFailure, exitSuccess)+import System.Process (readProcess) +import qualified Data.Map as Map+import qualified Options.Applicative as O+ main :: IO () main = do-    args <- getArgs-    case args of-        [] -> usageExit-        ["help"] -> usageExit-        _ | "-h" `elem` args || "--help" `elem` args -> usageExit-        _ -> do-            isGood <- run ("--recursive" `elem` args) ("--quiet" `elem` args) (filter (\arg -> arg /= "--recursive" && arg /= "--quiet") args)-            if isGood then exitSuccess else exitFailure+    run' <- O.execParser $ O.info (O.helper <*> opts) $ mconcat+        [ O.fullDesc+        , O.progDesc $ unwords+            [ "Check the given cabal file's dependency list to make sure that it does not exclude"+            , "the newest package available. It's probably worth running the 'cabal update' command"+            , "immediately before running this program."+            ]+        ]+    isGood <- run'+    if isGood then exitSuccess else exitFailure+  where+    opts = run+        <$> O.switch (O.short 'r' <> O.long "recursive" <> O.help "Check transitive dependencies as well.")+        <*> O.switch (O.short 'q' <> O.long "quiet" <> O.help "Suppress output for .cabal files which can accept the newest packages available.")+        <*> O.switch (O.short 'g' <> O.long "ghc-pkg" <> O.help "Use ghc-pkg list to additionally populate the newest packages database.")+        <*> many (O.strOption (O.short 'e' <> O.long "exclude" <> O.metavar "pkgname" <> O.help "Exclude the package from the output."))+        <*> some (O.strArgument (O.metavar "pkgname.cabal"))  type CheckDeps = Newest -> DescInfo -> (PackageName, Version, CheckDepsRes) -checkDepsCli :: Bool -> CheckDeps -> Newest -> DescInfo -> IO Bool-checkDepsCli quiet cd newest di =-    case cd newest di of+checkDepsCli :: Bool -> [String] -> CheckDeps -> Newest -> DescInfo -> IO Bool+checkDepsCli quiet excludes cd newest di =+    case overTrd filterExcludes $ cd newest di of         (pn, v, AllNewest)           | quiet -> return True           | otherwise -> do@@ -42,13 +60,43 @@                 ]             forM_ p $ \(x, y) -> putStrLn $ x ++ " " ++ y             return False+  where+    overTrd :: (a -> b) -> (x, y, a) -> (x, y, b)+    overTrd f (x, y, a) = (x, y, f a) +    filterExcludes :: CheckDepsRes -> CheckDepsRes+    filterExcludes AllNewest = AllNewest+    filterExcludes (WontAccept p t) =+        case filter (\(n, _) -> n `notElem` excludes) p of+            [] -> AllNewest+            p' -> WontAccept p' t++-- TODO+updateWithGhcPkg :: Newest -> IO Newest+updateWithGhcPkg newest = do+    output <- readProcess "ghc-pkg" ["list", "--simple-output"] ""+    let pkgs = mapMaybe parseSimplePackInfo (words output)+    return $ foldl' apply newest pkgs+  where+    apply :: Newest -> (String, PackInfo) -> Newest+    apply m (k, v) = Map.insert k v m++    parseSimplePackInfo :: String -> Maybe (String, PackInfo)+    parseSimplePackInfo str =+        case filter ((== "") . snd) $ readP_to_S parse str of+            ((PackageIdentifier (unPackageName -> name) ver, _) : _) ->+                Just (name, PackInfo ver Nothing 0)+            _ -> Nothing+ run :: Bool        -- ^ Check transitive dependencies     -> Bool        -- ^ Quiet -- only report packages that are not up to date+    -> Bool        -- ^ ghc-pkg -- use ghc-pkg list to see whether there are even newer packages.+    -> [String]    -- ^ packages to exclude     -> [FilePath]  -- ^ .cabal filenames     -> IO Bool-run deep quiet args = do-    newest <- loadNewest+run deep quiet ghcpkg excludes args = do+    newest' <- loadNewest+    newest <- if ghcpkg then updateWithGhcPkg newest' else return newest'     foldM (go newest) True args   where     go newest wasAllGood fp = do@@ -56,29 +104,13 @@         di <- case mdi of              Just di -> return di              Nothing -> error $ "Could not parse cabal file: " ++ fp-        allGood <- checkDepsCli quiet checkDeps newest di+        allGood <- checkDepsCli quiet excludes checkDeps newest di         depsGood <- if deep                        then do putStrLn $ "\nTransitive dependencies:"-                               allM (checkDepsCli quiet checkLibDeps newest) (deepLibDeps newest [di])+                               allM (checkDepsCli quiet excludes checkLibDeps newest) (deepLibDeps newest [di])                        else return True         when (not (allGood && depsGood)) $ putStrLn ""         return $ wasAllGood && allGood && depsGood--unPackageName :: PackageName -> String-unPackageName (PackageName n) = n---usageExit :: IO a-usageExit = do-    pname <- getProgName-    putStrLn $ "\n"-        ++ "Usage: " ++ pname ++ " [--recursive] [--quiet] pkgname.cabal pkgname2.cabal...\n\n"-        ++ "Check the given cabal file's dependency list to make sure that it does not exclude\n"-        ++ "the newest package available. Its probably worth running the 'cabal update' command\n"-        ++ "immediately before running this program.\n\n"-        ++ "  --quiet\n"-        ++ "      Suppress output for .cabal files which can accept the newest packages available.\n"-    exitSuccess  -- | Non short-circuiting monadic version of 'all' -- Duh, pre-AMP code.