{-# LANGUAGE CPP #-}
import Control.Applicative ((<|>)
#if (defined(MIN_VERSION_base) && MIN_VERSION_base(4,8,0))
#else
, (<$>), (<*>)
#endif
)
import Control.Concurrent.Async (concurrently)
import Control.Monad
import Data.List
import Data.Maybe
#if (defined(MIN_VERSION_base) && MIN_VERSION_base(4,11,0))
#else
import Data.Semigroup ((<>))
#endif
import Data.Text (Text)
import qualified Data.Text as T
import qualified Data.Text.IO as T
import Network.HTTP.Directory
import System.Directory (doesDirectoryExist, listDirectory)
import System.FilePath ((</>))
-- for warning
import System.IO (hPutStrLn, stderr)
import SimpleCmd (error', {-warning-})
import SimpleCmdArgs
import Paths_pkgtreediff (version)
main :: IO ()
main =
simpleCmdArgs (Just version) "Package tree comparison tool"
"pkgtreediff compares the packages in two OS trees" $
compareDirs <$> recursiveOpt <*> ignoreVR <*> modeOpt <*> strArg "URL/DIR1" <*> strArg "URL/DIR2"
data Mode = Default | Added | Removed | Updated
deriving Eq
modeOpt :: Parser Mode
modeOpt = flagWith' Added 'N' "new" "Show only added packages" <|>
flagWith' Removed 'D' "removed" "Show only removed packages" <|>
flagWith Default Updated 'U' "updated" "Show only updated packages"
data Ignore = IgnoreNone | IgnoreRelease | IgnoreVersion
deriving Eq
ignoreVR :: Parser Ignore
ignoreVR = flagWith' IgnoreRelease 'R' "ignore-release" "Only show version changes (ignore release)" <|>
flagWith IgnoreNone IgnoreVersion 'V' "ignore-version" "Only show package changes (ignore verrel)"
recursiveOpt :: Parser Bool
recursiveOpt = switchWith 'r' "recursive" "Recursive down into subdirectories"
compareDirs :: Bool -> Ignore -> Mode -> String -> String -> IO ()
compareDirs recursive ignore mode tree1 tree2 = do
(ps1,ps2) <- getTrees tree1 tree2
mapM_ T.putStrLn . mapMaybe (showPkgDiff mode) $ diffPkgs ignore ps1 ps2
where
getTrees :: String -> String -> IO ([Package],[Package])
getTrees t1 t2 = do
when (t1 == t2) $ warning "Comparing the same tree!"
let (isUrl1,isUrl2) = (isHttp t1, isHttp t2)
mmgr <- if isUrl1 || isUrl2 then Just <$> httpManager else return Nothing
concurrently (readPackages isUrl1 mmgr t1) (readPackages isUrl2 mmgr t2)
readPackages isUrl mmgr loc =
map readPkg <$> (if isUrl then httpPackages recursive (fromJust mmgr) else dirPackages) loc
httpPackages recurse mgr url = do
exists <- httpExists mgr url
fs <- if exists
then filter (not <$> \ f -> "/" `T.isPrefixOf` f || "?" `T.isPrefixOf` f || f == "../") <$> httpDirectory mgr url
else error' $ "Could not get " <> url
if recurse && all isDir fs then concatMapM (httpPackages False mgr) (map ((url </>) . T.unpack) fs) else return fs
dirPackages dir = do
fs <- sort <$> listDirectory dir
alldirs <- mapM doesDirectoryExist fs
if recursive && and alldirs then concatMapM dirPackages (map (dir </>) fs) else return $ map T.pack fs
isHttp :: String -> Bool
isHttp loc = "http:" `isPrefixOf` loc || "https:" `isPrefixOf` loc
isDir = ("/" `T.isSuffixOf`)
type Arch = Text
data NameArch = NA {name :: Text, _arch :: Arch}
deriving Eq
nameArch :: NameArch -> Text
nameArch (NA n a) = n <.> a
data VersionRelease = VerRel Text Text
deriving Eq
-- eqVR True ignore release
eqVR :: Ignore -> VersionRelease -> VersionRelease -> Bool
eqVR IgnoreNone vr vr' = vr == vr'
eqVR IgnoreRelease (VerRel v _) (VerRel v' _) = v == v'
eqVR IgnoreVersion _ _ = True
verRel :: VersionRelease -> Text
verRel (VerRel v r) = v <> "-" <> r
data Package = Pkg {_pkgNameArch :: NameArch, _pkgVerrel :: VersionRelease}
showPkg :: Package -> Text
showPkg (Pkg na vr) = nameArch na <> " " <> verRel vr
readPkg :: Text -> Package
readPkg t =
if compnts < 3 then error' $ T.unpack $ "Malformed rpm package name: " <> t
else Pkg (NA (intrclt ns) arch) (VerRel ver rel)
where
(nvr',arch) = T.breakOnEnd "." $ fromMaybe t $ T.stripSuffix ".rpm" t
pieces = reverse $ T.splitOn "-" $ T.dropEnd 1 nvr'
compnts = length pieces
(rel:ver:emaN) = pieces
ns = reverse emaN
intrclt :: [Text] -> Text
intrclt = T.intercalate "-"
data PackageDiff = PkgUpdate NameArch VersionRelease VersionRelease
| PkgAdd Package
| PkgDel Package
| PkgArch Text (Text,VersionRelease) (Text,VersionRelease)
showPkgDiff :: Mode -> PackageDiff -> Maybe Text
showPkgDiff Default (PkgAdd p) = Just $ "+ " <> showPkg p
showPkgDiff Default (PkgDel p) = Just $ "- " <> showPkg p
showPkgDiff Default (PkgUpdate na v v') = Just $ indent $ nameArch na <> ": " <> verRel v <> " -> " <> verRel v'
showPkgDiff Default (PkgArch n (a,v) (a',v')) = Just $ indent $ n <> ": " <> verRel v <.> a <> " -> " <> verRel v' <.> a'
showPkgDiff Added (PkgAdd p) = Just $ showPkg p
showPkgDiff Removed (PkgDel p) = Just $ showPkg p
showPkgDiff Updated (PkgUpdate na v v') = Just $ nameArch na <> ": " <> verRel v <> " -> " <> verRel v'
showPkgDiff Updated (PkgArch n (a,v) (a',v')) = Just $ n <> ": " <> verRel v <.> a <> " -> " <> verRel v' <.> a'
showPkgDiff _ _ = Nothing
indent :: Text -> Text
indent = (" " <>)
diffPkgs :: Ignore -> [Package] -> [Package] -> [PackageDiff]
diffPkgs _ [] [] = []
diffPkgs ignore (p:ps) [] = PkgDel p : diffPkgs ignore ps []
diffPkgs ignore [] (p:ps) = PkgAdd p : diffPkgs ignore [] ps
diffPkgs ignore (p1:ps1) (p2:ps2) =
case compareNames p1 p2 of
LT -> PkgDel p1 : diffPkgs ignore ps1 (p2:ps2)
EQ -> let diff = diffPkg ignore p1 p2
diffs = diffPkgs ignore ps1 ps2
in if isJust diff then fromJust diff : diffs else diffs
GT -> PkgAdd p2 : diffPkgs ignore (p1:ps1) ps2
diffPkg :: Ignore -> Package -> Package -> Maybe PackageDiff
diffPkg ignore (Pkg na1 v1) (Pkg na2 v2) | na1 == na2 =
if eqVR ignore v1 v2
then Nothing
else Just $ PkgUpdate na1 v1 v2
diffPkg _ (Pkg (NA n1 a1) v1) (Pkg (NA n2 a2) v2)
| n1 == n2 && "noarch" `elem` [a1,a2] = Just $ PkgArch n1 (a1,v1) (a2,v2)
diffPkg _ _ _ = Nothing
compareNames :: Package -> Package -> Ordering
compareNames (Pkg na1 _) (Pkg na2 _) = compare (name na1) (name na2)
infixr 4 <.>
(<.>) :: Text -> Text -> Text
s <.> t = s <> "." <> t
-- from next simple-cmd
warning :: String -> IO ()
warning = hPutStrLn stderr
-- borrowed straight from extra:Control.Monad.Extra
-- | A version of 'concatMap' that works with a monadic predicate.
concatMapM :: Monad m => (a -> m [b]) -> [a] -> m [b]
{-# INLINE concatMapM #-}
concatMapM op = foldr f (return [])
where f x xs = do x' <- op x; if null x' then xs else do xs' <- xs; return $ x'++xs'