{-# LANGUAGE ApplicativeDo #-}
{-# LANGUAGE RecordWildCards #-}
module Main (
main,
) where
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)
import Distribution.Compat.NonEmptySet (singleton)
import Distribution.Pretty (pretty)
import Distribution.Simple.Flag (fromFlag)
import Distribution.Types.Dependency (Dependency (..))
import Distribution.Types.LibraryName (LibraryName (..))
import Distribution.Types.PackageName (PackageName, mkPackageName, unPackageName)
import Distribution.Types.VersionRange (VersionRange)
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 ((</>))
import System.IO (stdout)
data Config = Config
{ cnfIndexState :: !(Maybe UTCTime)
, cnfPackageNames :: !(NonEmpty PackageName)
, cnfTransitive :: !Bool
}
parseArgs :: Parser Config
parseArgs = do
cnfIndexState <-
optional $
option (fmap (`UTCTime` 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"
cnfTransitive <-
switch $
long "transitive"
<> help "Count transitive (both direct and indirect) 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 = S.fromList $ NE.toList cnfPackageNames
cnf <- loadConfig minBound mempty
let cacheDir = fromFlag $ globalCacheDir $ savedGlobalFlags cnf
idx = cacheDir </> hackageHaskellOrg </> "01-index.tar"
func = if cnfTransitive then getTransitiveReverseDependencies else getReverseDependencies
pkgs <- func args idx cnfIndexState
report args $ M.delete (mkPackageName "acme-everything") 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
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 =
flip M.restrictKeys args $
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
hackageHaskellOrg :: FilePath
hackageHaskellOrg = "hackage.haskell.org"