rpmbuild-order 0.4.3.2 → 0.4.4
raw patch · 7 files changed
+127/−58 lines, 7 filesdep +graphvizdep ~basedep ~fglPVP ok
version bump matches the API change (PVP)
Dependencies added: graphviz
Dependency ranges changed: base, fgl
API changes (from Hackage documentation)
+ Distribution.RPM.Build.Graph: createGraph'''' :: Bool -> [String] -> [String] -> Bool -> Bool -> Bool -> Maybe FilePath -> [FilePath] -> IO PackageGraph
+ Distribution.RPM.Build.Graph: printGraph :: PackageGraph -> IO ()
+ Distribution.RPM.Build.Graph: renderGraph :: PackageGraph -> IO ()
Files
- ChangeLog.md +7/−0
- README.md +14/−4
- TODO +0/−5
- rpmbuild-order.cabal +16/−15
- src/Distribution/RPM/Build/Graph.hs +69/−28
- src/Main.hs +11/−1
- test/Spec.hs +10/−5
ChangeLog.md view
@@ -1,3 +1,10 @@+# 0.4.4 (2021-05-16)+- add 'render' command to display graph graphically with graphviz+ with an option to output dot format+- fix subcycle algorithm: now lists correct shortest path subcycles+- improve cycles output (#2)+- library Graph: add printGraph, renderGraph, createGraph''''+ # 0.4.3.2 (2021-02-03) - intercalate newlines between cycles - only list subcycles with over 2 packages
README.md view
@@ -4,20 +4,30 @@ # rpmbuild-order -This package originally based on code from [cabal-sort](http://hackage.haskell.org/package/cabal-sort), sorts rpm package spec files by build order.+This is a tool to sort RPM source packages in build dependency order.+This code is originally derived from+[cabal-sort](http://hackage.haskell.org/package/cabal-sort)+by Henning Thielemann. +## Usage+ $ rpmbuild-order --help+ : $ rpmbuild-order sort mycore mylib myapp mylib mycore myapp -The arguments passed can either be directories containing the package, or spec files.+The arguments passed can either be directories containing the package+or spec files. If the dependency graph has cycles then an error will be output with-a list of cycles and any subcycles.+a list of cycles and any shortest path subcycles. Using the rpmbuild-order `deps` and `rdeps` commands the ordered dependencies and reverse dependencies of a package can be obtained-from the current set of checked out package sources.+within the current set of checked out package sources.++The `render` command displays a graph of package dependencies+using graphviz and X11 or optionally instead prints the dot format to stdout. ## Library As of version 0.4, a library is also provided.
TODO view
@@ -1,11 +1,6 @@ - try topograph- - use Set?- * warn about uninstalled (specific?) macros packages- - handle duplicate packages?- - (r)deps: optimize using actual deps- - option to somehow display subpackage deps for cycles
rpmbuild-order.cabal view
@@ -1,9 +1,9 @@ Name: rpmbuild-order-Version: 0.4.3.2+Version: 0.4.4 License: BSD3 License-File: LICENSE-Author: Henning Thielemann <haskell@henning-thielemann.de>,- Jens Petersen <petersen@redhat.com>+Author: Jens Petersen <petersen@redhat.com>,+ Henning Thielemann <haskell@henning-thielemann.de> Maintainer: Jens Petersen <petersen@redhat.com> Copyright: 2010-2018 Henning Thielemann, 2018-2021 Jens Petersen <petersen@redhat.com>@@ -12,21 +12,21 @@ Bug-reports: https://github.com/juhp/rpmbuild-order/issues Synopsis: Order RPM packages by dependencies Description:- The rpmbuild-order tool orders RPM packages by dependencies, so that they can- be built in the correct order. It does this by reading RPM package spec files- and then topologically sorts them according to their dependencies.- The code evolved from cabal-sort by Henning Thielemann.- It can also output the ordered dependencies or reverse depends- for one or more packages, provided all the packages are checked out- in neighboring directories. This is also useful to see what packages- are affected when a low-level package changes. It also has support- for setting RPM options for bcond etc, which can affect dependencies.+ The rpmbuild-order tool sorts source RPM packages by build dependencies,+ so that they can be built in a correct order. It does this by reading RPM+ package spec files and then topologically sorts them according to their+ build dependencies. The code evolved from cabal-sort by Henning Thielemann.+ It can also order the dependencies or reverse depends of one or more packages+ among the packages checked out in neighboring directories (which can be+ useful to see what packages are affected when a low-level package changes).+ It also has support for setting RPM options for bcond etc,+ which can affect dependencies. It can also output dependency graphs. Since version 0.4, a library API is also provided. Tested-with: GHC == 7.4.2, GHC == 7.6.3, GHC == 7.8.4, GHC == 7.10.3, GHC == 8.0.2, GHC == 8.2.2, GHC == 8.4.4, GHC == 8.6.5, GHC == 8.8.4,- GHC == 8.10.2+ GHC == 8.10.4, GHC == 9.0.1 Cabal-Version: 1.18 Build-Type: Simple Extra-doc-files: README.md TODO ChangeLog.md@@ -48,7 +48,6 @@ optparse-applicative, rpmbuild-order, simple-cmd-args- Main-Is: src/Main.hs Other-modules: Paths_rpmbuild_order GHC-Options: -Wall@@ -74,7 +73,8 @@ directory, extra >= 1.6.4, filepath,- fgl,+ fgl >= 5.5.4,+ graphviz, process HS-Source-Dirs: src GHC-Options: -Wall@@ -106,3 +106,4 @@ , rpmbuild-order , simple-cmd , unix+ build-tools: rpmbuild-order
src/Distribution/RPM/Build/Graph.hs view
@@ -18,13 +18,16 @@ createGraph', createGraph'', createGraph''',+ createGraph'''', dependencyNodes, subgraph', packageLayers, lowestLayer, lowestLayer', packageLeaves,- separatePackages+ separatePackages,+ printGraph,+ renderGraph ) where import qualified Data.CaseInsensitive as CI@@ -36,9 +39,10 @@ #if !MIN_VERSION_base(4,8,0) import Control.Applicative ((<$>)) #endif-import Control.Monad (guard, when, unless)+import Control.Monad (forM_, guard, when, unless) import Data.Maybe (catMaybes, fromMaybe, mapMaybe)-import Data.List.Extra (dropSuffix, find, intercalate, nubOrdOn, sort, sortOn)+import Data.List.Extra (dropSuffix, find, intercalate, nub, nubOrdOn, sort, sortOn)+import Data.GraphViz import System.Directory (doesDirectoryExist, doesFileExist, #if MIN_VERSION_directory(1,2,5) listDirectory@@ -146,13 +150,30 @@ -> Maybe FilePath -- ^ look for spec file in a subdirectory -> [FilePath] -- ^ package paths (directories or spec filepaths) -> IO PackageGraph -- ^ dependency graph labelled by package paths-createGraph''' ignoredBRs rpmopts verbose lenient rev mdir paths = do+createGraph''' = createGraph'''' True++-- | Create a directed dependency graph for a set of packages+--+-- Like createGraph''' but can disable check for cycles+--+-- @since 0.4.4+createGraph'''' :: Bool -- ^ check for cycles+ -> [String] -- ^ ignored BuildRequires+ -> [String] -- ^ rpmspec options+ -> Bool -- ^ verbose+ -> Bool -- ^ lenient (skip rpmspec failures)+ -> Bool -- ^ reverse dependency graph+ -> Maybe FilePath -- ^ look for spec file in a subdirectory+ -> [FilePath] -- ^ package paths (directories or spec filepaths)+ -> IO PackageGraph -- ^ dependency graph labelled by package paths+createGraph'''' checkcycles ignoredBRs rpmopts verbose lenient rev mdir paths = do metadata <- catMaybes <$> mapM readSpecMetadata paths let realpkgs = map fst3 metadata deps = mapMaybe (getDepsSrcResolved metadata) realpkgs spkgs = zipWith SourcePackage realpkgs deps graph = getBuildGraph spkgs- checkForCycles graph+ when checkcycles $+ checkForCycles graph return graph where readSpecMetadata :: FilePath -> IO (Maybe (FilePath,[String],[String]))@@ -191,10 +212,11 @@ case specs of [spec] -> return $ Just spec _ -> if lenient then return Nothing- else error $ if null specs- then "No spec file found in " ++ path- else "More than one .spec file found in " ++ dir- else error $ "No spec file found for " ++ path+ else errorWithoutStackTrace $+ if null specs+ then "No spec file found in " ++ path+ else "More than one .spec file found in " ++ dir+ else errorWithoutStackTrace $ "No spec file found for " ++ path where checkFile :: Bool -> FilePath -> IO (Maybe FilePath) checkFile may f = do@@ -203,7 +225,7 @@ then return $ Just f else return $ if may then Nothing- else error $ f ++ " not found"+ else errorWithoutStackTrace $ f ++ " not found" filesWithExtension :: FilePath -> String -> IO [FilePath] filesWithExtension dir ext =@@ -242,33 +264,32 @@ return $ if rev then (dstNode, srcNode, ()) else (srcNode, dstNode, ())- in G.mkGraph (map (fmap packagePath) nodes) edges+ in G.mkGraph (map (fmap packagePath) nodes) $ nub edges - checkForCycles :: Monad m => PackageGraph -> m ()- checkForCycles graph =- unless (null cycles) $- error $ unlines $- "Cycles in dependencies:" :- intercalate [""] (map (renderCycles . subcycles) cycles)+ checkForCycles :: PackageGraph -> IO ()+ checkForCycles graph = do+ let cycles = filter ((>= 2) . length) (scc graph)+ unless (null cycles) $ do+ let plural = if length cycles > 1 then "s" else ""+ errorWithoutStackTrace $ unlines $+ ("ordering not possible due to build dependency cycle" ++ plural ++ ":\n") : intercalate [""] (map (renderCycles . subcycles) cycles) where- cycles :: [[G.Node]]- cycles =- (filter ((>= 2) . length) . scc) graph- -- shortest subcycle subcycles :: [G.Node] -> ([FilePath],[[FilePath]]) subcycles [] = error "cyclic graph with no nodes!"- subcycles cycle'@(n:ns) =- let sg = G.emap (const (1 :: Int)) $ G.subgraph cycle' graph- shorter = nubOrdOn sort $ sortOn length $ filter ((\l -> l < length ns && l > 2) . length) $ catMaybes $ mapAdjacent (\ i j-> sp j i sg) (cycle' ++ [n])+ subcycles cycle' =+ let shorter = nubOrdOn sort $ sortOn length $ filter ((< length cycle') . length) $ mapMaybe findSp $ G.labEdges sg in (nodeLabels graph cycle', map (nodeLabels sg) shorter)+ where+ sg = G.emap (const (1 :: Int)) $ G.subgraph cycle' graph - mapAdjacent :: (a -> a -> b) -> [a] -> [b]- mapAdjacent f xs = zipWith f xs (tail xs)+ findSp (i,j,_) | G.hasEdge sg (i,j) = sp j i sg+ | G.hasEdge sg (j,i) = sp i j sg+ | otherwise = Nothing renderCycles :: ([FilePath],[[FilePath]]) -> [String] renderCycles (c,sc) =- unwords c : if null sc then [] else "Subcycles: " : map unwords sc+ unwords c : if null sc then [] else "\nShortest path subcycles: " : map unwords sc getDepsSrcResolved :: [(FilePath,[String],[String])] -> FilePath -> Maybe [FilePath] getDepsSrcResolved metadata pkg =@@ -279,7 +300,7 @@ case mapMaybe (\ (p,provs,_) -> if br `elem` provs then Just p else Nothing) metadata of [] -> br [p] -> p- ps -> error $ pkg ++ ": " ++ br ++ " is provided by: " ++ unwords ps+ ps -> errorWithoutStackTrace $ pkg ++ ": " ++ br ++ " is provided by: " ++ unwords ps thd (_,_,c) = c @@ -333,3 +354,23 @@ separatePackages :: PackageGraph -> [FilePath] separatePackages graph = map snd $ G.labNodes $ G.nfilter ((==0) . G.deg graph) graph++-- | Return graphviz dot format of graph+printGraph :: PackageGraph -> IO ()+printGraph g = do+ putStrLn "digraph {"+ forM_ (G.labNodes g) $ \ (n,l) -> do+ putStr $ show l+ putStrLn $ renderDeps $ map show $ mapMaybe (G.lab g . fst) $ G.lsuc g n+ putStrLn "}"+ where+ renderDeps :: [String] -> String+ renderDeps [] = ""+ renderDeps [d] = " -> " ++ d+ renderDeps ds = " -> {" ++ unwords ds ++ "}"++-- | Render graph with graphviz X11 preview+renderGraph :: PackageGraph -> IO ()+renderGraph graph =+ let g = G.emap (const ("" :: String)) graph+ in runGraphvizCanvas' (setDirectedness graphToDot quickParams g) Xlib
src/Main.hs view
@@ -1,5 +1,7 @@ {-# LANGUAGE CPP #-} +module Main (main) where+ import Control.Applicative ( #if !MIN_VERSION_simple_cmd_args(0,1,3) (<|>),@@ -51,6 +53,8 @@ leavesPackages <$> rpmOpts <*> verboseOpt <*> lenientOpt <*> subdirOpt <*> pkgArgs , Subcommand "roots" "List lowest root packages" $ rootPackages <$> rpmOpts <*> verboseOpt <*> lenientOpt <*> subdirOpt <*> pkgArgs+ , Subcommand "render" "Show graph with graphviz" $+ renderPkgGraph <$> switchWith 'o' "output" "Print graph in dot format" <*> rpmOpts <*> verboseOpt <*> lenientOpt <*> subdirOpt <*> pkgArgs ] where verboseOpt = switchWith 'v' "verbose" "Verbose output for debugging"@@ -73,7 +77,7 @@ depsPackages :: Bool -> [String] -> Bool-> [String] -> [String] -> Bool -> Bool -> Maybe FilePath -> [FilePath] -> IO () depsPackages rev rpmopts verbose excludedPkgs ignoredBRs lenient parallel mdir pkgs = do unlessM (and <$> mapM doesDirectoryExist pkgs) $- error "Please use package directory paths"+ errorWithoutStackTrace "Please use package directory paths" listDirectory "." >>= -- filter out dotfiles createGraph''' ignoredBRs rpmopts verbose lenient (not rev) mdir . filter ((/= '.') . head) . filter (`notElem` excludedPkgs) >>=@@ -115,3 +119,9 @@ rootPackages rpmopts verbose lenient mdir pkgs = do graph <- createGraph'' rpmopts verbose lenient True mdir pkgs mapM_ putStrLn $ lowestLayer graph++renderPkgGraph :: Bool -> [String] -> Bool -> Bool -> Maybe FilePath+ -> [FilePath] -> IO ()+renderPkgGraph dot rpmopts verbose lenient mdir pkgs =+ createGraph'''' False [] rpmopts verbose lenient True mdir pkgs >>=+ if dot then printGraph else renderGraph
test/Spec.hs view
@@ -1,18 +1,23 @@ import Test.Hspec import Control.Monad.Extra+import Data.Version.Extra --import Distribution.RPM.Build.Graph import Distribution.RPM.Build.Order import SimpleCmd import System.Posix.Files main :: IO ()-main = setupSymlinks >> hspec spec+main = do+ setupSymlinks+ -- rpmspec < 4.15 does not support "--with"+ rpmver <- readVersion . last . words <$> cmd "rpmspec" ["--version"]+ hspec (spec rpmver) pkg :: FilePath -> FilePath pkg = ("test/pkgs/" ++) -spec :: Spec-spec = do+spec :: Version -> Spec+spec rpmver = do describe "sorting" $ do it "sort A B" $ dependencySort [pkg "A", pkg "B"] >>=@@ -50,7 +55,8 @@ dependencySort [pkg "A", pkg "B/", pkg "D1.0"] >>= (`shouldBe` [pkg "B/", pkg "D1.0", pkg "A"]) - it "circular A B C boot" $+ when (rpmver > makeVersion [4,15]) $+ it "circular A B C boot" $ dependencySortRpmOpts ["--with=boot"] [pkg "A", pkg "B", pkg "C"] >>= (`shouldBe` [pkg "C", pkg "B", pkg "A"]) @@ -64,7 +70,6 @@ leafPackages [pkg "A", pkg "B", pkg "D1.0"] >>= (`shouldBe` [pkg "A", pkg "D1.0"]) - -- NB hack: this requires it to be "cabal install"'ed already describe "rpmbuild-order" $ do it "sort A B" $ cmd "rpmbuild-order" ["sort", pkg "A", pkg "B"] >>=