hackage-revdeps 0.1.1 → 0.2
raw patch · 5 files changed
+176/−71 lines, 5 filesdep ~Cabaldep ~basedep ~bytestringPVP ok
version bump matches the API change (PVP)
Dependency ranges changed: Cabal, base, bytestring, containers, tar, time
API changes (from Hackage documentation)
- Hackage.RevDeps: latestReleases :: [ByteString] -> FilePath -> Maybe UTCTime -> IO (Map PackageName ByteString)
+ Hackage.RevDeps: allLastVersions :: FilePath -> Maybe UTCTime -> IO (Map PackageName ByteString)
+ Hackage.RevDeps: getReverseDependencies :: Set PackageName -> FilePath -> Maybe UTCTime -> IO (Map PackageName (Map PackageName VersionRange))
+ Hackage.RevDeps: getTransitiveReverseDependencies :: Set PackageName -> FilePath -> Maybe UTCTime -> IO (Map PackageName (Map PackageName VersionRange))
+ Hackage.RevDeps: lastVersions :: Set ByteString -> FilePath -> Maybe UTCTime -> IO (Map PackageName ByteString)
- Hackage.RevDeps: extractDependencies :: [PackageName] -> ByteString -> Map PackageName VersionRange
+ Hackage.RevDeps: extractDependencies :: Set PackageName -> ByteString -> Map PackageName VersionRange
Files
- CHANGELOG.md +4/−0
- app/History.hs +13/−8
- app/Main.hs +20/−13
- hackage-revdeps.cabal +16/−27
- src/Hackage/RevDeps.hs +123/−23
CHANGELOG.md view
@@ -1,3 +1,7 @@+## 0.2++* Overhaul the API.+ ## 0.1.1 * Replace dependency on `cabal-install-parsers` with `cabal-install` proper.
app/History.hs view
@@ -5,19 +5,19 @@ main, ) where -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.Set qualified as S import Data.Time (Day, UTCTime (..), addDays, getCurrentTime) import Distribution.Client.Config (loadConfig, savedGlobalFlags) import Distribution.Client.GlobalFlags (globalCacheDir) import Distribution.Simple.Flag (fromFlag)-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 Distribution.Types.PackageName (PackageName, mkPackageName, unPackageName)+import Hackage.RevDeps (getReverseDependencies, getTransitiveReverseDependencies)+import Options.Applicative (Parser, auto, execParser, fullDesc, help, helper, info, long, metavar, option, progDesc, showDefault, strArgument, switch, value) import Options.Applicative.NonEmpty (some1) import System.Console.ANSI (hSupportsANSI, hyperlinkCode) import System.FilePath ((</>))@@ -28,6 +28,7 @@ , cnfFinish :: !Day , cnfStep :: !Word , cnfPackageNames :: !(NonEmpty PackageName)+ , cnfTransitive :: !Bool } parseArgs :: Day -> Parser Config@@ -55,6 +56,10 @@ strArgument $ metavar "PKGS" <> help "Package names to scan Hackage for their reverse dependencies"+ cnfTransitive <-+ switch $+ long "transitive"+ <> help "Count transitive (both direct and indirect) dependencies" pure Config {..} main :: IO ()@@ -72,11 +77,11 @@ let cacheDir = fromFlag $ globalCacheDir $ savedGlobalFlags cnf idx = cacheDir </> hackageHaskellOrg </> "01-index.tar" 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 M.delete pkgs+ let utcTime = Just $ UTCTime date 0+ func = if cnfTransitive then getTransitiveReverseDependencies else getReverseDependencies+ pkgs <- func (S.fromList args) idx utcTime+ let pkgs' = M.delete (mkPackageName "acme-everything") pkgs counters = M.unionsWith (+) $ fmap (fmap (const (1 :: Int))) pkgs' putStrLn $ unwords $ show date : map (\pkg -> showPair (pkg, M.findWithDefault 0 pkg counters)) args
app/Main.hs view
@@ -5,11 +5,12 @@ main, ) where -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.Set (Set)+import Data.Set qualified as S import Data.Time (Day, UTCTime (..)) import Distribution.Client.Config (loadConfig, savedGlobalFlags) import Distribution.Client.GlobalFlags (globalCacheDir)@@ -18,10 +19,10 @@ import Distribution.Simple.Flag (fromFlag) import Distribution.Types.Dependency (Dependency (..)) import Distribution.Types.LibraryName (LibraryName (..))-import Distribution.Types.PackageName (PackageName, unPackageName)+import Distribution.Types.PackageName (PackageName, mkPackageName, 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 Hackage.RevDeps (getReverseDependencies, getTransitiveReverseDependencies)+import Options.Applicative (Parser, ReadM, auto, execParser, fullDesc, help, helper, info, long, metavar, option, optional, progDesc, strArgument, switch) import Options.Applicative.NonEmpty (some1) import System.Console.ANSI (hSupportsANSI, hyperlinkCode) import System.FilePath ((</>))@@ -30,6 +31,7 @@ data Config = Config { cnfIndexState :: !(Maybe UTCTime) , cnfPackageNames :: !(NonEmpty PackageName)+ , cnfTransitive :: !Bool } parseArgs :: Parser Config@@ -44,6 +46,10 @@ strArgument $ metavar "PKGS" <> help "Package names to scan Hackage for their reverse dependencies"+ cnfTransitive <-+ switch $+ long "transitive"+ <> help "Count transitive (both direct and indirect) dependencies" pure Config {..} main :: IO ()@@ -52,19 +58,17 @@ Config {..} <- execParser $ info (helper <*> parseArgs) (fullDesc <> progDesc desc)- let args = NE.toList cnfPackageNames+ let args = S.fromList $ NE.toList cnfPackageNames cnf <- loadConfig minBound mempty let cacheDir = fromFlag $ globalCacheDir $ savedGlobalFlags cnf idx = cacheDir </> hackageHaskellOrg </> "01-index.tar"- needles = map (B.pack . unPackageName) args- releases <- latestReleases needles idx cnfIndexState- let pkgs = fmap (extractDependencies args) releases- pkgs' = M.mapWithKey M.delete pkgs- report $ M.filter (not . null) pkgs'+ func = if cnfTransitive then getTransitiveReverseDependencies else getReverseDependencies+ pkgs <- func args idx cnfIndexState+ report args $ M.delete (mkPackageName "acme-everything") pkgs -report :: Map PackageName (Map PackageName VersionRange) -> IO ()-report pkgs+report :: Set PackageName -> Map PackageName (Map PackageName VersionRange) -> IO ()+report args pkgs | M.null pkgs = putStrLn "No reverse dependencies found" | otherwise = do supportsAnsi <- hSupportsANSI stdout@@ -73,7 +77,10 @@ pkgs' = fmap (map prettify . M.toAscList) pkgs reportTable supportsAnsi pkgs' putStrLn "Total count:"- let counters = M.unionsWith (+) $ fmap (fmap (const (1 :: Int))) pkgs+ let counters =+ flip M.restrictKeys args $+ M.unionsWith (+) $+ fmap (fmap (const (1 :: Int))) pkgs reportTable supportsAnsi counters reportTable :: Show v => Bool -> Map PackageName v -> IO ()
hackage-revdeps.cabal view
@@ -1,13 +1,12 @@ cabal-version: 2.2 name: hackage-revdeps-version: 0.1.1+version: 0.2 license: BSD-3-Clause license-file: LICENSE maintainer: andrew.lelechenko@gmail.com author: Bodigrim tested-with:- ghc ==9.12.2 ghc ==9.10.1 ghc ==9.8.4 ghc ==9.6.7 ghc ==9.4.8- ghc ==9.2.8+ ghc ==9.12.2 ghc ==9.10.2 ghc ==9.8.4 ghc ==9.6.7 ghc ==9.4.8 synopsis: List Hackage reverse dependencies description:@@ -28,9 +27,6 @@ type: git location: https://github.com/Bodigrim/hackage-revdeps.git -flag cabal-syntax- default: False- library exposed-modules: Hackage.RevDeps hs-source-dirs: src@@ -38,19 +34,14 @@ ghc-options: -Wall -Wunused-packages build-depends: alfred-margaret >=2.0 && <2.2,- base >=4.16 && <5,+ base >=4.17 && <5, bytestring <0.13,- containers <0.9,+ containers >=0.5.8 && <0.9, 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+ time <1.16,+ Cabal-syntax >=3.8 && <3.17 executable hackage-revdeps main-is: Main.hs@@ -60,15 +51,14 @@ build-depends: base, ansi-terminal >=0.11.3 && <1.2,- bytestring,- cabal-install >=3.8 && <3.15,+ cabal-install >=3.8 && <3.17, containers, filepath, hackage-revdeps,- optparse-applicative >=0.16 && <0.19,- time <1.15,- Cabal-syntax >=3.8 && <3.15,- Cabal >=3.8 && <3.15+ optparse-applicative >=0.16 && <0.20,+ time <1.16,+ Cabal-syntax >=3.8 && <3.17,+ Cabal >=3.8 && <3.17 executable hackage-revdeps-history main-is: History.hs@@ -78,12 +68,11 @@ build-depends: base, ansi-terminal >=0.11.3 && <1.2,- bytestring,- cabal-install >=3.8 && <3.15,+ cabal-install >=3.8 && <3.17, containers, filepath, hackage-revdeps,- optparse-applicative >=0.16 && <0.19,- time <1.15,- Cabal-syntax >=3.8 && <3.15,- Cabal >=3.8 && <3.15+ optparse-applicative >=0.16 && <0.20,+ time <1.16,+ Cabal-syntax >=3.8 && <3.17,+ Cabal >=3.8 && <3.17
src/Hackage/RevDeps.hs view
@@ -1,19 +1,26 @@ -- | Functions to list Hackage reverse dependencies. module Hackage.RevDeps (- latestReleases,+ lastVersions,+ allLastVersions, extractDependencies,+ getReverseDependencies,+ getTransitiveReverseDependencies, ) 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.Char8 qualified as B import Data.ByteString.Lazy qualified as BL-import Data.Char (isPunctuation, isSpace)+import Data.Char (isPunctuation, isSpace, ord)+import Data.Foldable (fold) import Data.List (isSuffixOf) import Data.Map.Strict (Map) import Data.Map.Strict qualified as M import Data.Maybe (mapMaybe)+import Data.Set (Set)+import Data.Set qualified as S import Data.Text (Text) import Data.Text qualified as T import Data.Text.AhoCorasick.Automaton qualified as Aho@@ -26,22 +33,22 @@ 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.PackageName (PackageName, mkPackageName, unPackageName) import Distribution.Types.VersionRange (VersionRange)-import Distribution.Version (intersectVersionRanges, simplifyVersionRange)+import Distribution.Version (Version, intersectVersionRanges, mkVersion, simplifyVersionRange) import System.FilePath (isPathSeparator) -- | Scan Cabal index @01-index.tar@ and return Cabal files--- of latest releases / revisions (not necessarily largest versions), which+-- of last versions (not necessarily latest releases or revisions), which -- contain one of the needles as an entire word (separated by spaces -- or punctuation). ----- To avoid ambiguity: we first select the latest releases,+-- To avoid ambiguity: we first select the last versions, -- then filter them by needles. ----- @since 0.1-latestReleases- :: [ByteString]+-- @since 0.2+lastVersions+ :: Set ByteString -- ^ Needles to search in Cabal files. -> FilePath -- ^ Path to @01-index.tar@.@@ -50,25 +57,39 @@ -> 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)- <$> allLatestReleases idx indexState+ -- ^ Map from packages with largest versions to their Cabal files.+lastVersions needles idx indexState =+ filterByNeedles needles <$> allLastVersions idx indexState++filterByNeedles+ :: Set ByteString+ -> Map k ByteString+ -> Map k ByteString+filterByNeedles needles = M.filter (containsAnyAsWholeWord machine . decodeUtf8Lenient) where- machine = Aho.build (map ((\x -> (x, x)) . decodeUtf8Lenient) needles)+ machine = Aho.build (map ((\x -> (x, x)) . decodeUtf8Lenient) (S.toList needles)) -- | Scan Cabal index @01-index.tar@ and return Cabal files--- of latest releases / revisions (not necessarily largest versions).-allLatestReleases+-- of last versions (not necessarily latest releases or revisions).+--+-- @since 0.2+allLastVersions :: FilePath -> Maybe UTCTime -> IO (Map PackageName ByteString)-allLatestReleases idx indexState = foldCabalFilesInIndex idx indexState mempty M.insert+allLastVersions idx indexState =+ fmap (fmap snd) $+ foldCabalFilesInIndex idx indexState mempty go+ where+ go pkg ver cnt = M.alter (Just . f) pkg+ where+ new = (ver, cnt)+ f = maybe new (\old@(ver', _) -> if ver' <= ver then new else old) containsAnyAsWholeWord :: Aho.AcMachine Text -> Text -> Bool containsAnyAsWholeWord machine hay = Aho.runText False go machine hay where- isWordBoundary c = isSpace c || isPunctuation c+ isWordBoundary c = (isSpace c || isPunctuation c || c `elem` "^>=<") && c /= '-' go :: Bool -> Aho.Match Text -> Aho.Next Bool go _ (Aho.Match pos val) =@@ -88,7 +109,7 @@ :: FilePath -> Maybe UTCTime -> a- -> (PackageName -> ByteString -> a -> a)+ -> (PackageName -> Version -> ByteString -> a -> a) -> IO a foldCabalFilesInIndex fp indexState ini action = do contents <- BL.readFile fp@@ -106,14 +127,26 @@ go acc entry = case Tar.entryContent entry of Tar.NormalFile contents _ ->- if isCabalFile then action pkgName bs acc else acc+ if isCabalFile then action pkgName version bs acc else acc where bs = BL.toStrict contents fpath = Tar.entryPath entry isCabalFile = ".cabal" `isSuffixOf` fpath- pkgName = mkPackageName $ takeWhile (not . isPathSeparator) fpath+ (rawPkgName, fpath') = break isPathSeparator fpath+ pkgName = mkPackageName rawPkgName+ rawVersion = takeWhile (not . isPathSeparator) $ dropWhile isPathSeparator fpath'+ version = mkVersion $ readVersion rawVersion _ -> acc +readVersion :: String -> [Int]+readVersion = (\(acc, _mult, rest) -> acc : rest) . foldr go (0, 1, [])+ where+ go c (acc, mult, rest)+ | fromIntegral d < (10 :: Word) = (acc + d * mult, mult * 10, rest)+ | otherwise = (0, 1, acc : rest)+ where+ d = ord c - ord '0'+ tarTakeWhile :: (Tar.Entry -> Bool) -> Tar.Entries a@@ -129,7 +162,7 @@ -- -- @since 0.1 extractDependencies- :: [PackageName]+ :: Set PackageName -- ^ Needles to search. -> ByteString -- ^ Content of a Cabal file.@@ -142,10 +175,77 @@ Nothing -> mempty Just descr -> foldMap targetBuildDepends $ toListOf Lens.traverseBuildInfos descr -relevantDeps :: [PackageName] -> [Dependency] -> Map PackageName VersionRange+relevantDeps :: Set 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++-- | Combination of 'lastVersions' and 'extractDependencies'.+--+-- @since 0.2+getReverseDependencies+ :: Set PackageName+ -- ^ 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 (Map PackageName VersionRange))+ -- ^ Outer keys are reverse dependencies,+ -- inner keys are needles,+ -- inner values are accepted version ranges.+getReverseDependencies args fp indexState = do+ releases <- lastVersions (S.map (B.pack . unPackageName) args) fp indexState+ pure $ extractDependencies' args releases++extractDependencies'+ :: Set PackageName+ -> Map PackageName ByteString+ -> Map PackageName (Map PackageName VersionRange)+extractDependencies' args releases =+ M.filter (not . null) $+ -- Most of packages trivially depend on themselves+ -- (e. g., test suite depends on a library); skip it.+ M.mapWithKey M.delete $+ fmap (extractDependencies args) releases++-- | Same as 'getReverseDependencies', but+-- returns not only direct, but also indirect reverse dependencies.+--+-- @since 0.2+getTransitiveReverseDependencies+ :: Set PackageName+ -- ^ 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 (Map PackageName VersionRange))+ -- ^ Reverse dependencies, including transitive ones.+getTransitiveReverseDependencies args fp indexState = do+ releases <- allLastVersions fp indexState+ go mempty releases args+ where+ go+ :: Map PackageName (Map PackageName VersionRange)+ -> Map PackageName ByteString+ -> Set PackageName+ -> IO (Map PackageName (Map PackageName VersionRange))+ go acc releases xs = do+ let rawRevDeps = extractDependencies' xs releases+ revDeps = M.map (\ys -> ys <> fold (M.restrictKeys acc (M.keysSet ys))) rawRevDeps+ revDepsKeys = M.keysSet revDeps+ if revDepsKeys `S.isSubsetOf` M.keysSet acc+ then pure acc+ else+ go+ (M.unionWith (M.unionWith intersectVersionRanges) acc revDeps)+ (M.withoutKeys releases revDepsKeys)+ revDepsKeys