packages feed

hackage-revdeps (empty) → 0.1

raw patch · 7 files changed

+462/−0 lines, 7 filesdep +Cabaldep +Cabal-syntaxdep +alfred-margaret

Dependencies added: Cabal, Cabal-syntax, alfred-margaret, ansi-terminal, base, bytestring, cabal-install-parsers, containers, filepath, hackage-revdeps, optparse-applicative, tar, text, time

Files

+ CHANGELOG.md view
@@ -0,0 +1,3 @@+## 0.1++* Initial release.
+ LICENSE view
@@ -0,0 +1,29 @@+Copyright (c) 2024, Bodigrim+++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++    * Redistributions of source code must retain the above copyright+      notice, this list of conditions and the following disclaimer.++    * Redistributions in binary form must reproduce the above+      copyright notice, this list of conditions and the following+      disclaimer in the documentation and/or other materials provided+      with the distribution.++    * Neither the name of the copyright holder nor the names of its+      contributors may be used to endorse or promote products derived+      from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ README.md view
@@ -0,0 +1,5 @@+# hackage-revdeps [![Hackage](http://img.shields.io/hackage/v/hackage-revdeps.svg)](https://hackage.haskell.org/package/hackage-revdeps) [![Stackage LTS](http://stackage.org/package/hackage-revdeps/badge/lts)](http://stackage.org/lts/package/hackage-revdeps) [![Stackage Nightly](http://stackage.org/package/hackage-revdeps/badge/nightly)](http://stackage.org/nightly/package/hackage-revdeps)++Command-line tool to list Hackage reverse dependencies.++It is different from how Hackage itself tracks them: this tool accounts for all package components, including tests and benchmarks, and counts dependencies only across the latest releases. The approach is roughly equivalent to what [packdeps.haskellers.com](https://packdeps.haskellers.com) used to do.
+ app/History.hs view
@@ -0,0 +1,93 @@+{-# LANGUAGE ApplicativeDo #-}+{-# LANGUAGE RecordWildCards #-}++module Main (+  main,+) where++import Cabal.Config (cfgRepoIndex, hackageHaskellOrg, readConfig)+import Data.ByteString.Char8 qualified as B+import Data.Foldable (for_)+import Data.List qualified as L+import Data.List.NonEmpty (NonEmpty)+import Data.List.NonEmpty qualified as NE+import Data.Map.Strict qualified as M+import Data.Time (Day, UTCTime (..), addDays, getCurrentTime)+import Distribution.Types.PackageName (PackageName, unPackageName)+import Hackage.RevDeps (extractDependencies, latestReleases)+import Options.Applicative (Parser, auto, execParser, fullDesc, help, helper, info, long, metavar, option, progDesc, showDefault, strArgument, value)+import Options.Applicative.NonEmpty (some1)+import System.Console.ANSI (hSupportsANSI, hyperlinkCode)+import System.Exit (die)+import System.IO (stdout)++data Config = Config+  { cnfStart :: !Day+  , cnfFinish :: !Day+  , cnfStep :: !Word+  , cnfPackageNames :: !(NonEmpty PackageName)+  }++parseArgs :: Day -> Parser Config+parseArgs today = do+  cnfStart <-+    option auto $+      long "start"+        <> help "Start date, YYYY-MM-DD"+        <> value (read "2006-09-01")+        <> showDefault+  cnfFinish <-+    option auto $+      long "finish"+        <> help "Finish date, YYYY-MM-DD"+        <> value today+        <> showDefault+  cnfStep <-+    option auto $+      long "step"+        <> help "Step in days"+        <> value 1000+        <> showDefault+  cnfPackageNames <-+    some1 $+      strArgument $+        metavar "PKGS"+          <> help "Package names to scan Hackage for their reverse dependencies"+  pure Config {..}++main :: IO ()+main = do+  let desc = "Count Hackage reverse dependencies for given dates, using local package index. Consider running 'cabal update' beforehand."+  today <- utctDay <$> getCurrentTime+  Config {..} <-+    execParser $+      info (helper <*> parseArgs today) (fullDesc <> progDesc desc)+  let args = NE.toList cnfPackageNames+      dates = L.nub $ [cnfStart, addDays (fromIntegral cnfStep) cnfStart .. cnfFinish] ++ [cnfFinish]+  supportsAnsi <- hSupportsANSI stdout++  cnf <- readConfig+  case cfgRepoIndex cnf hackageHaskellOrg of+    Nothing -> die $ hackageHaskellOrg ++ " not found in cabal.config, aborting"+    Just idx -> do+      putStrLn $ unwords $ "Date      " : map (showPackage supportsAnsi) args+      let needles = map (B.pack . unPackageName) args+      for_ dates $ \date -> do+        releases <- latestReleases needles idx (Just $ UTCTime date 0)+        let pkgs = fmap (extractDependencies args) releases+            pkgs' = M.mapWithKey (\k v -> M.delete k v) pkgs+            counters = M.unionsWith (+) $ fmap (fmap (const (1 :: Int))) pkgs'+        putStrLn $ unwords $ show date : map (\pkg -> showPair (pkg, M.findWithDefault 0 pkg counters)) args++showPair :: Show v => (PackageName, v) -> String+showPair (k, v) =+  replicate (length (unPackageName k) - length (show v)) ' '+    ++ show v++showPackage :: Bool -> PackageName -> String+showPackage supportsAnsi p =+  if supportsAnsi+    then hyperlinkCode ("https://" ++ hackageHaskellOrg ++ "/package/" ++ xs) xs+    else xs+  where+    xs = unPackageName p
+ app/Main.hs view
@@ -0,0 +1,91 @@+{-# LANGUAGE ApplicativeDo #-}+{-# LANGUAGE RecordWildCards #-}++module Main (+  main,+) where++import Cabal.Config (cfgRepoIndex, hackageHaskellOrg, readConfig)+import Data.ByteString.Char8 qualified as B+import Data.List.NonEmpty (NonEmpty)+import Data.List.NonEmpty qualified as NE+import Data.Map.Strict (Map)+import Data.Map.Strict qualified as M+import Data.Time (Day, UTCTime (..))+import Distribution.Compat.NonEmptySet (singleton)+import Distribution.Pretty (pretty)+import Distribution.Types.Dependency (Dependency (..))+import Distribution.Types.LibraryName (LibraryName (..))+import Distribution.Types.PackageName (PackageName, unPackageName)+import Distribution.Types.VersionRange (VersionRange)+import Hackage.RevDeps (extractDependencies, latestReleases)+import Options.Applicative (Parser, ReadM, auto, execParser, fullDesc, help, helper, info, long, metavar, option, optional, progDesc, strArgument)+import Options.Applicative.NonEmpty (some1)+import System.Console.ANSI (hSupportsANSI, hyperlinkCode)+import System.Exit (die)+import System.IO (stdout)++data Config = Config+  { cnfIndexState :: !(Maybe UTCTime)+  , cnfPackageNames :: !(NonEmpty PackageName)+  }++parseArgs :: Parser Config+parseArgs = do+  cnfIndexState <-+    optional $+      option (fmap (\d -> UTCTime d 0) (auto :: ReadM Day)) $+        long "index-state"+          <> help "Timestamp of index state at which to stop scanning, YYYY-MM-DD"+  cnfPackageNames <-+    some1 $+      strArgument $+        metavar "PKGS"+          <> help "Package names to scan Hackage for their reverse dependencies"+  pure Config {..}++main :: IO ()+main = do+  let desc = "List Hackage reverse dependencies, using local package index. Consider running 'cabal update' beforehand."+  Config {..} <-+    execParser $+      info (helper <*> parseArgs) (fullDesc <> progDesc desc)+  let args = NE.toList cnfPackageNames++  cnf <- readConfig+  case cfgRepoIndex cnf hackageHaskellOrg of+    Nothing -> die $ hackageHaskellOrg ++ " not found in cabal.config, aborting"+    Just idx -> do+      let needles = map (B.pack . unPackageName) args+      releases <- latestReleases needles idx cnfIndexState+      let pkgs = fmap (extractDependencies args) releases+          pkgs' = M.mapWithKey (\k v -> M.delete k v) pkgs+      report $ M.filter (not . null) pkgs'++report :: Map PackageName (Map PackageName VersionRange) -> IO ()+report pkgs = do+  supportsAnsi <- hSupportsANSI stdout+  putStrLn "Reverse dependencies:"+  let prettify (k, v) = pretty $ Dependency k v (singleton LMainLibName)+      pkgs' = fmap (map prettify . M.toAscList) pkgs+  reportTable supportsAnsi pkgs'+  putStrLn "Total count:"+  let counters = M.unionsWith (+) $ fmap (fmap (const (1 :: Int))) pkgs+  reportTable supportsAnsi counters++reportTable :: Show v => Bool -> Map PackageName v -> IO ()+reportTable supportsAnsi kvs = putStrLn $ unlines $ map showPair $ M.toAscList kvs+  where+    longestKey = maximum $ 0 : map (length . unPackageName) (M.keys kvs)+    showPair (k, v) =+      showPackage supportsAnsi k+        ++ replicate (longestKey + 1 - length (unPackageName k)) ' '+        ++ show v++showPackage :: Bool -> PackageName -> String+showPackage supportsAnsi p =+  if supportsAnsi+    then hyperlinkCode ("https://" ++ hackageHaskellOrg ++ "/package/" ++ xs) xs+    else xs+  where+    xs = unPackageName p
+ hackage-revdeps.cabal view
@@ -0,0 +1,95 @@+cabal-version:   2.2+name:            hackage-revdeps+version:         0.1+license:         BSD-3-Clause+license-file:    LICENSE+maintainer:      andrew.lelechenko@gmail.com+author:          Bodigrim+tested-with:+    ghc ==9.12.1 ghc ==9.10.1 ghc ==9.8.4 ghc ==9.6.6 ghc ==9.4.8+    ghc ==9.2.8++synopsis:        List Hackage reverse dependencies+description:+    Command-line tool to list Hackage reverse dependencies.+    It is different from how Hackage itself tracks them:+    this tool accounts for all package components, including+    tests and benchmarks, and counts dependencies only+    across the latest releases. The approach is roughly+    equivalent to what <https://packdeps.haskellers.com> used to do.++category:        Development+build-type:      Simple+extra-doc-files:+    CHANGELOG.md+    README.md++source-repository head+    type:     git+    location: https://github.com/Bodigrim/hackage-revdeps.git++flag cabal-syntax+    default: False++library+    exposed-modules:  Hackage.RevDeps+    hs-source-dirs:   src+    default-language: GHC2021+    ghc-options:      -Wall -Wunused-packages+    build-depends:+        alfred-margaret >=2.0 && <2.2,+        base >=4.16 && <5,+        bytestring <0.13,+        containers <0.8,+        filepath <1.6,+        tar <0.7,+        text >=2.0 && <2.2,+        time <1.15++    if flag(cabal-syntax)+        build-depends: Cabal-syntax >=3.8 && <3.15++    else+        build-depends: Cabal <3.7++executable hackage-revdeps+    main-is:          Main.hs+    hs-source-dirs:   app+    default-language: GHC2021+    ghc-options:      -Wall -Wunused-packages+    build-depends:+        base,+        ansi-terminal >=0.11.3 && <1.2,+        bytestring,+        cabal-install-parsers <0.7,+        containers,+        hackage-revdeps,+        optparse-applicative >=0.16 && <0.19,+        time <1.15++    if flag(cabal-syntax)+        build-depends: Cabal-syntax >=3.8 && <3.15++    else+        build-depends: Cabal <3.7++executable hackage-revdeps-history+    main-is:          History.hs+    hs-source-dirs:   app+    default-language: GHC2021+    ghc-options:      -Wall -Wunused-packages+    build-depends:+        base,+        ansi-terminal >=0.11.3 && <1.2,+        bytestring,+        cabal-install-parsers <0.7,+        containers,+        hackage-revdeps,+        optparse-applicative >=0.16 && <0.19,+        time <1.15++    if flag(cabal-syntax)+        build-depends: Cabal-syntax >=3.8 && <3.15++    else+        build-depends: Cabal <3.7
+ src/Hackage/RevDeps.hs view
@@ -0,0 +1,146 @@+-- | Functions to list Hackage reverse dependencies.+module Hackage.RevDeps (+  latestReleases,+  extractDependencies,+) where++import Codec.Archive.Tar qualified as Tar+import Codec.Archive.Tar.Entry qualified as Tar+import Control.Exception (throwIO)+import Data.ByteString (ByteString)+import Data.ByteString.Lazy qualified as BL+import Data.Char (isPunctuation, isSpace)+import Data.List (isSuffixOf)+import Data.Map.Strict (Map)+import Data.Map.Strict qualified as M+import Data.Maybe (mapMaybe)+import Data.Text (Text)+import Data.Text qualified as T+import Data.Text.AhoCorasick.Automaton qualified as Aho+import Data.Text.Encoding (decodeUtf8Lenient)+import Data.Text.Unsafe qualified as T+import Data.Time (UTCTime)+import Data.Time.Clock.POSIX (utcTimeToPOSIXSeconds)+import Distribution.Compat.Lens (toListOf)+import Distribution.PackageDescription.Parsec (parseGenericPackageDescriptionMaybe)+import Distribution.Types.BuildInfo (targetBuildDepends)+import Distribution.Types.BuildInfo.Lens qualified as Lens+import Distribution.Types.Dependency (Dependency (..))+import Distribution.Types.PackageName (PackageName, mkPackageName)+import Distribution.Types.VersionRange (VersionRange)+import Distribution.Version (intersectVersionRanges, simplifyVersionRange)+import System.FilePath (isPathSeparator)++-- | Scan Cabal index @01-index.tar@ and return Cabal files+-- of latest releases (not necessarily largest versions), which+-- contain one of the needles as an entire word (separated by spaces+-- or punctuation).+--+-- To avoid ambiguity: we first select the latest releases,+-- then filter them by needles.+latestReleases+  :: [ByteString]+  -- ^ Needles to search in Cabal files.+  -> FilePath+  -- ^ Path to @01-index.tar@.+  -- One can use @Cabal.Config.cfgRepoIndex@ from @cabal-install-parsers@+  -- to obtain it.+  -> Maybe UTCTime+  -- ^ Timestamp of index state at which to stop scanning.+  -> IO (Map PackageName ByteString)+  -- ^ Map from latest releases to their Cabal files.+latestReleases needles idx indexState =+  M.filter (containsAnyAsWholeWord machine . decodeUtf8Lenient)+    <$> getLatestReleases idx indexState+  where+    machine = Aho.build (map ((\x -> (x, x)) . decodeUtf8Lenient) needles)++-- | Strip revisions and releases except the latest one.+getLatestReleases+  :: FilePath+  -> Maybe UTCTime+  -> IO (Map PackageName ByteString)+getLatestReleases idx indexState = foldCabalFilesInIndex idx indexState mempty M.insert++containsAnyAsWholeWord :: Aho.AcMachine Text -> Text -> Bool+containsAnyAsWholeWord machine hay = Aho.runText False go machine hay+  where+    isWordBoundary c = isSpace c || isPunctuation c++    go :: Bool -> Aho.Match Text -> Aho.Next Bool+    go _ (Aho.Match pos val) =+      if startsWithBoundary && endsWithBoundary+        then Aho.Done True+        else Aho.Step False+      where+        pref =+          T.dropEnd (T.length val) $+            T.takeWord8 (fromIntegral (Aho.codeUnitIndex pos)) hay+        startsWithBoundary = maybe True (isWordBoundary . snd) (T.unsnoc pref)+        suff = T.dropWord8 (fromIntegral (Aho.codeUnitIndex pos)) hay+        endsWithBoundary = maybe True (isWordBoundary . fst) (T.uncons suff)++-- | Inspired by @Cabal.Index.foldIndex@ from @cabal-install-parsers@.+foldCabalFilesInIndex+  :: FilePath+  -> Maybe UTCTime+  -> a+  -> (PackageName -> ByteString -> a -> a)+  -> IO a+foldCabalFilesInIndex fp indexState ini action = do+  contents <- BL.readFile fp+  let entries' = Tar.read contents+      entries = case indexState of+        Nothing -> entries'+        Just t ->+          tarTakeWhile+            (\e -> fromIntegral (Tar.entryTime e) <= utcTimeToPOSIXSeconds t)+            entries'+  case Tar.foldlEntries go ini entries of+    Left (err, _) -> throwIO err+    Right res -> pure res+  where+    go acc entry =+      case Tar.entryContent entry of+        Tar.NormalFile contents _ ->+          if isCabalFile then action pkgName bs acc else acc+          where+            bs = BL.toStrict contents+            fpath = Tar.entryPath entry+            isCabalFile = ".cabal" `isSuffixOf` fpath+            pkgName = mkPackageName $ takeWhile (not . isPathSeparator) fpath+        _ -> acc++tarTakeWhile+  :: (Tar.Entry -> Bool)+  -> Tar.Entries a+  -> Tar.Entries a+tarTakeWhile p =+  Tar.foldEntries+    (\x xs -> if p x then Tar.Next x xs else Tar.Done)+    Tar.Done+    Tar.Fail++-- | Scan Cabal file looking for package names,+-- coalescing version bounds from all components and under all conditions.+extractDependencies+  :: [PackageName]+  -- ^ Needles to search.+  -> ByteString+  -- ^ Content of a Cabal file.+  -> Map PackageName VersionRange+  -- ^ Needles found in the Cabal file and their version bounds.+extractDependencies needles = relevantDeps needles . extractDeps++extractDeps :: ByteString -> [Dependency]+extractDeps cnt = case parseGenericPackageDescriptionMaybe cnt of+  Nothing -> mempty+  Just descr -> foldMap targetBuildDepends $ toListOf Lens.traverseBuildInfos descr++relevantDeps :: [PackageName] -> [Dependency] -> Map PackageName VersionRange+relevantDeps needles =+  fmap simplifyVersionRange . M.fromListWith intersectVersionRanges . mapMaybe go+  where+    go (Dependency pkg ver _)+      | pkg `elem` needles = Just (pkg, ver)+      | otherwise = Nothing