cabal-fix-0.0.0.1: src/CabalFix/Archive.hs
{-# LANGUAGE OverloadedLabels #-}
{-# LANGUAGE OverloadedStrings #-}
{-# OPTIONS_GHC -Wno-name-shadowing #-}
-- | Archive investigation
module CabalFix.Archive where
import Algebra.Graph
import Algebra.Graph.ToGraph qualified as ToGraph
import CabalFix
import CabalFix.FlatParse (depP, runParser_, untilP)
import Codec.Archive.Tar qualified as Tar
import Control.Category ((>>>))
import Data.Bifunctor
import Data.Bool
import Data.ByteString (ByteString)
import Data.ByteString.Lazy qualified as BSL
import Data.Either
import Data.Foldable
import Data.List qualified as List
import Data.Map.Strict qualified as Map
import Data.Maybe
import Data.Set qualified as Set
import Distribution.Parsec
import Distribution.Version
import DotParse qualified as Dot
import FlatParse.Basic qualified as FP
import GHC.Generics
import Optics.Extra
import System.Directory
-- | the cabal index
cabalIndex :: IO FilePath
cabalIndex = do
h <- getHomeDirectory
pure $ h <> "/.cabal/packages/hackage.haskell.org/01-index.tar"
-- | all the tar entries that represent packages of some kind.
cabalEntries :: IO [Tar.Entry]
cabalEntries = entryList . Tar.read <$> (BSL.readFile =<< cabalIndex)
where
entryList es = Tar.foldEntries (:) [] (error . show) es
-- | The naming convention in 01-index.tar
data FileName = FileName {nameFN :: ByteString, versionFN :: ByteString, filenameFN :: ByteString} deriving (Generic, Eq, Ord, Show)
-- | Convert a ByteString to a FileName. Errors on failure.
filename :: ByteString -> FileName
filename = runParser_ filenameP
-- | FileName parser
filenameP :: FP.Parser e FileName
filenameP = FileName <$> untilP '/' <*> untilP '/' <*> FP.takeRest
-- | cabal files
--
-- Discards stale versions with later revisions
cabals :: IO [(FileName, ByteString)]
cabals = do
es <- cabalEntries
let cs = first (runParser_ filenameP . FP.strToUtf8) <$> filter ((/= "package.json") . filenameFN . runParser_ filenameP . FP.strToUtf8 . fst) (filter (not . List.isSuffixOf "preferred-versions" . fst) $ [(fp, BSL.toStrict bs) | (fp, Tar.NormalFile bs _) <- (\e -> (Tar.entryPath e, Tar.entryContent e)) <$> es])
pure $ Map.toList $ Map.fromList cs
-- | Assumes cabal entries are in chronological order and that the last version encountered is the
-- latest valid one.
latestCabals :: IO (Map.Map ByteString (Version, ByteString))
latestCabals = do
cs <- CabalFix.Archive.cabals
pure $ Map.fromListWith (\new old -> bool old new (fst new >= fst old)) $ (\(fn, bs) -> (nameFN fn, (getVersion fn, bs))) <$> cs
where
getVersion = fromMaybe undefined . simpleParsecBS . versionFN
-- | Latest successfully parsing 'CabalFields'
latestCabalFields :: Config -> IO (Map.Map ByteString (Version, CabalFields))
latestCabalFields cfg = do
lcs <- latestCabals
let lcs' = second (parseCabalFields cfg) <$> lcs
pure (second (fromRight undefined) <$> Map.filter (snd >>> isRight) lcs')
-- | extract library build-deps from a Field list, also looking in common stanzas
libDeps :: CabalFields -> [Dep]
libDeps cf = deps
where
libFields = cf & foldOf (#fields % fieldList' % section' "library" % each % secFields')
libBds = libFields & foldOf (fieldValues' "build-depends")
libDeps = runParser_ (FP.many depP) libBds
libImports = libFields & toListOf (fieldValues' "import")
cs = cf & foldOf (#fields % fieldList' % section' "common")
libCommons = cs & filter (all (`elem` libImports) . toListOf (secArgs' % each % secArgBS' % _2))
commonsBds = libCommons & foldOf (fieldValues' "build-depends")
commonsDeps = runParser_ (FP.many depP) commonsBds
deps = fmap (uncurry Dep) (libDeps <> commonsDeps)
-- | Map of valid dependencies
validLibDeps :: Map.Map ByteString CabalFields -> Map.Map ByteString [ByteString]
validLibDeps cs = ldeps
where
vlls = cs & Map.filter (view (#fields % fieldList' % section' "library") >>> length >>> (> 0))
ldeps' = vlls & fmap (libDeps >>> fmap dep >>> List.nub)
bdnames = List.nub $ mconcat (snd <$> Map.toList ldeps')
-- dependencies that do not exist in the main library list
bdnames0 = filter (not . (`elem` Map.keys ldeps')) bdnames
-- Exclude any library that has dependencies outside the universe.
ldeps = ldeps' & Map.filter (any (`List.elem` bdnames0) >>> not)
-- | Graph of all valid dependencies
allDepGraph :: Map.Map ByteString CabalFields -> Graph ByteString
allDepGraph cs = transpose $ stars (Map.toList (validLibDeps cs))
-- | count distinct elements of a list.
count_ :: (Ord a) => [a] -> Map.Map a Int
count_ = foldl' (\x a -> Map.insertWith (+) a 1 x) Map.empty
-- | collect distinct monoidal values
collect_ :: (Ord k) => [(k, v)] -> Map.Map k [v]
collect_ = foldl' (\x (k, v) -> Map.insertWith (<>) k [v] x) Map.empty
-- | Get the set of upstream projects
upstreams :: ByteString -> Graph ByteString -> Set.Set ByteString
upstreams x g = Set.delete "base" $ ToGraph.preSet x g
-- | Get the set of downstream projects.
downstreams :: ByteString -> Graph ByteString -> Set.Set ByteString
downstreams x g = ToGraph.postSet x g
-- | Get the upstream graph of a library. text, for example:
upstreamG :: ByteString -> Graph ByteString -> Graph ByteString
upstreamG lib g = induce (`elem` toList supers) g
where
supers = upstreams lib g <> Set.singleton "text"
-- | Create a dot graph from an algebraic graph of dependencies
dotUpstream :: Graph ByteString -> ByteString
dotUpstream g = Dot.dotPrint Dot.defaultDotConfig g'
where
baseGraph = Dot.defaultGraph & Dot.attL Dot.GraphType (Dot.ID "size") .~ Just (Dot.IDQuoted "5!") & Dot.attL Dot.NodeType (Dot.ID "shape") .~ Just (Dot.ID "box") & Dot.attL Dot.NodeType (Dot.ID "height") .~ Just (Dot.ID "2") & Dot.gattL (Dot.ID "rankdir") .~ Just (Dot.IDQuoted "TB")
g' = Dot.toDotGraphWith Dot.Directed baseGraph g
-- | make an svg file of a dependency graph
--
-- 
dotUpstreamSvg :: Graph ByteString -> FilePath -> IO ByteString
dotUpstreamSvg g svg = Dot.processDotWith Dot.Directed ["-Tsvg", "-o" <> svg] (dotUpstream g)