packages feed

rpmbuild-order-0.2: RpmbuildOrder.hs

{-# LANGUAGE LambdaCase #-}

module Main where

import System.Console.GetOpt
          (getOpt, ArgOrder(..), OptDescr(..), ArgDescr(..), usageInfo)
import System.Exit (exitSuccess, exitFailure)
import qualified System.Environment as Env
import System.FilePath

import System.Directory (doesDirectoryExist, doesFileExist,
#if (defined(MIN_VERSION_directory) && MIN_VERSION_directory(1,2,5))
                         listDirectory
#else
                         getDirectoryContents
#endif
  )
import System.IO (hPutStrLn, stderr)
import System.Process (readProcess)

import Data.Graph.Inductive.Query.DFS (xdfsWith, topsort', scc, components)
import Data.Graph.Inductive.Tree (Gr)
import qualified Data.Graph.Inductive.Graph as Graph

import qualified Control.Monad.Exception.Synchronous as E
import qualified Control.Monad.Trans.Class as T

import qualified Data.Set as Set
import Control.Monad (guard, when, unless)
import Data.Maybe (catMaybes, fromMaybe, mapMaybe)
import Data.List (delete, intersperse, stripPrefix)
import Data.Version  (showVersion)

#if (defined(MIN_VERSION_base) && MIN_VERSION_base(4,8,2))
#else
import Control.Applicative ((<$>))
#endif

#if (defined(MIN_VERSION_directory) && MIN_VERSION_directory(1,2,5))
#else
listDirectory :: FilePath -> IO [FilePath]
listDirectory path =
  filter f <$> getDirectoryContents path
  where f filename = filename /= "." && filename /= ".."
#endif

import Paths_rpmbuild_order (version)

main :: IO ()
main =
  E.resolveT handleException $ do
  argv <- T.lift Env.getArgs
  let (opts, args, errors) = getOpt Permute options argv
  flags <-
    E.ExceptionalT $ return $
        foldr (=<<)
           (return
            Flags {optHelp = False,
                   optVersion = False,
                   optVerbosity = False,
                   optFormat = package,
                   optParallel = Nothing,
                   optBranch = Nothing})
           opts
  if optHelp flags
    then T.lift $ help >> exitSuccess
    else
    if optVersion flags
    then T.lift $ putStrLn $ showVersion version
    else
      if length args < 2
      then T.lift $ help >> exitFailure
      else do
        let (com:pkgs) = args
        unless (null errors) $ E.throwT $ concat errors
        unless (com `elem` ["sort", "deps", "rdeps"]) $
          E.throwT $ "Unknown command " ++ com
        runCommand flags com $ map (removeSuffix "/") pkgs
  where
    help =
      Env.getProgName >>= \programName ->
        putStrLn
             (usageInfo ("Usage: " ++ programName ++
                         " [OPTIONS] [sort|deps|rdeps] PKG-SPEC-OR-DIR ...") options)

handleException :: String -> IO ()
handleException msg = do
   putStrLn $ "Aborted: " ++ msg
   exitFailure

findSpec :: Maybe FilePath -> FilePath -> IO (Maybe FilePath)
findSpec mdir file =
  if takeExtension file == ".spec"
    then checkFile file
    else do
    dirp <- doesDirectoryExist file
    if dirp
      then
      let dir = maybe file (file </>) mdir
          pkg = takeBaseName file in
        checkFile $ dir </> pkg ++ ".spec"
      else error $ "No spec file found for " ++ file
  where
    checkFile :: FilePath -> IO (Maybe FilePath)
    checkFile f = do
      e <- doesFileExist f
      if e
        then return $ Just f
        else return Nothing

data Flags =
   Flags {
      optHelp :: Bool,
      optVersion :: Bool,
      optVerbosity :: Bool,
      optFormat :: SourcePackage -> String,
      optParallel :: Maybe String,
      optBranch :: Maybe FilePath
   }

options :: [OptDescr (Flags -> E.Exceptional String Flags)]
options =
  [
    Option ['h'] ["help"]
      (NoArg (\flags -> return $ flags{optHelp = True}))
      "Show options"
  , Option ['V'] ["version"]
      (NoArg (\flags -> return $ flags{optVersion = True}))
      "Show version"
  , Option ['p'] ["parallel"]
      (OptArg
        (\mstr flags ->
            fmap (\cs -> flags{optParallel = Just cs})
            (E.Success (fromMaybe "" mstr)))
         "SEPARATOR")
      "Display independently buildable groups of packages, optionally with separator"
  , Option ['b'] ["branch"]
      (ReqArg
         (\str flags ->
            fmap (\mb -> flags{optBranch = mb})
            (E.Success (Just str)))
         "BRANCHDIR")
    "branch directory"
  , Option ['f'] ["format"]
      (ReqArg
         (\str flags ->
            fmap (\select -> flags{optFormat = select}) $
            case str of
               "package" -> E.Success package
               "spec" -> E.Success location
               "dir"  -> E.Success (takeDirectory . location)
               _ ->
                  E.Exception $
                  "unknown info type " ++ str)
         "KIND")
      "output format: 'package' (default), 'spec', or 'dir'"
  , Option ['v'] ["verbose"]
      (NoArg
         (\flags ->
            return flags{optVerbosity = True}))
      "verbose output"
  ]

type Package = String

data SourcePackage =
   SourcePackage {
      location :: FilePath,
      package :: Package,
      dependencies :: [Package]
   }
   deriving (Show, Eq)

type Command = String

runCommand :: Flags -> Command -> [Package] -> E.ExceptionalT String IO ()
runCommand flags "sort" pkgs = sortSpecFiles flags pkgs
runCommand flags "deps" pkgs = depsSpecFiles False flags pkgs
runCommand flags "rdeps" pkgs = depsSpecFiles True flags pkgs
runCommand _ _ _ = E.throwT "impossible happened"

createGraphNodes :: Flags -> [Package] -> [Package] ->
               E.ExceptionalT String IO (Gr SourcePackage (), [Graph.Node])
createGraphNodes flags pkgs subset = do
  unless (all (`elem` pkgs) subset) $
    E.throwT "Packages must be in the current directory"
  specPaths <- T.lift $ catMaybes <$> mapM (findSpec (optBranch flags)) (filter (/= fromMaybe "" (optParallel flags)) pkgs)
  let names = map takeBaseName specPaths
  provs <-
     T.lift $
     mapM (readProvides (optVerbosity flags)) specPaths
  let resolves = zip names provs
  deps <-
     T.lift $
     mapM (getDepsSrcResolved (optVerbosity flags) resolves) specPaths
  let spkgs = zipWith3 SourcePackage specPaths names deps
      graph = getBuildGraph spkgs
  checkForCycles graph
  let nodes = Graph.labNodes graph
      subnodes = mapMaybe (pkgNode nodes) subset
  return (graph, subnodes)
  where
    pkgNode [] _ = Nothing
    pkgNode ((i,l):ns) p = if p == package l then Just i else pkgNode ns p

sortSpecFiles :: Flags -> [Package] -> E.ExceptionalT String IO ()
sortSpecFiles flags pkgs = do
      (graph, _) <- createGraphNodes flags pkgs []
      T.lift $
         case optParallel flags of
           Just s ->
             mapM_ ((putStrLn . unwords . (if null s then id else intersperse s) . map (optFormat flags)) . topsort' . subgraph graph)
                 (components graph)
           Nothing ->
             mapM_ (putStrLn . optFormat flags) $ topsort' graph
 
depsSpecFiles :: Bool -> Flags -> [Package] -> E.ExceptionalT String IO ()
depsSpecFiles rev flags pkgs = do
  allpkgs <- T.lift $ filter (\ f -> head f /= '.') <$> listDirectory "."
  (graph, nodes) <- createGraphNodes flags allpkgs pkgs
  let dir = if rev then Graph.suc' else Graph.pre'
  sortSpecFiles flags $ map package $ xdfsWith dir third nodes graph
  where
    third (_, _, c, _) = c

readProvides :: Bool -> FilePath -> IO [String]
readProvides verbose file = do
  when verbose $ hPutStrLn stderr file
  pkgs <- lines <$>
    rpmspec ["--rpms", "--qf=%{name}\n", "--define", "ghc_version any"] Nothing file
  let pkg = takeBaseName file
  return $ delete pkg pkgs

getDepsSrcResolved :: Bool -> [(String,[String])] -> FilePath -> IO [String]
getDepsSrcResolved verbose provides file =
  map (resolveBase provides) <$> do
      when verbose $ hPutStrLn stderr file
      -- ignore version bounds
      map (head . words) . lines <$>
        rpmspec ["--buildrequires", "--define", "ghc_version any"] Nothing file
  where
    resolveBase :: [(String,[String])] -> String -> String
    resolveBase provs br =
      case mapMaybe (\ (pkg,subs) -> if br `elem` subs then Just pkg else Nothing) provs of
        [] -> br
        [p] -> p
        ps -> error $ br ++ "is provided by: " ++ unwords ps

removeSuffix :: String -> String -> String
removeSuffix suffix orig =
  fromMaybe orig $ stripSuffix suffix orig
  where
    stripSuffix sf str = reverse <$> stripPrefix (reverse sf) (reverse str)

cmdStdIn :: String -> [String] -> String -> IO String
cmdStdIn c as inp = removeTrailingNewline <$> readProcess c as inp
  where
    removeTrailingNewline :: String -> String
    removeTrailingNewline "" = ""
    removeTrailingNewline str =
      if last str == '\n'
      then init str
      else str

cmd :: String -> [String] -> IO String
cmd c as = cmdStdIn c as ""

rpmspec :: [String] -> Maybe String -> FilePath -> IO String
rpmspec args mqf spec = do
  let qf = maybe [] (\ q -> ["--queryformat", q]) mqf
  cmd "rpmspec" (["-q"] ++ args ++ qf ++ [spec])

getDeps :: Gr SourcePackage () -> [(SourcePackage, [SourcePackage])]
getDeps gr =
    let c2dep :: Graph.Context SourcePackage () -> (SourcePackage, [SourcePackage])
        c2dep ctx =
           (Graph.lab' ctx,
            map (Graph.lab' . Graph.context gr) (Graph.pre gr . Graph.node' $ ctx))
    in  Graph.ufold (\ctx ds -> c2dep ctx : ds) [] gr

getBuildGraph :: [SourcePackage] -> Gr SourcePackage ()
getBuildGraph srcPkgs =
   let nodes = zip [0..] srcPkgs
       nodeDict = zip (map package srcPkgs) [0..]
       edges = do
          (srcNode,srcPkg) <- nodes
          dstNode <- mapMaybe (`lookup` nodeDict) (dependencies srcPkg)
          guard (dstNode /= srcNode)
          return (dstNode, srcNode, ())
   in Graph.mkGraph nodes edges

checkForCycles ::
   Monad m =>
   Gr SourcePackage () ->
   E.ExceptionalT String m ()
checkForCycles graph =
   case getCycles graph of
      [] -> return ()
      cycles ->
         E.throwT $ unlines $
         "Cycles in dependencies:" :
         map (unwords . map location . nodeLabels graph) cycles

nodeLabels :: Gr a b -> [Graph.Node] -> [a]
nodeLabels graph =
   map (fromMaybe (error "node not found in graph") .
        Graph.lab graph)

subgraph :: Gr a b -> [Graph.Node] -> Gr a b
subgraph graph nodes =
   let nodeSet = Set.fromList nodes
       edges = do
           from <- nodes
           (to, lab) <- Graph.lsuc graph from
           guard $ Set.member from nodeSet && Set.member to nodeSet
           return (from,to,lab)
   in  Graph.mkGraph (zip nodes $ nodeLabels graph nodes) edges

getCycles :: Gr a b -> [[Graph.Node]]
getCycles =
   filter (\case
              _:_:_ -> True
              _ -> False)
   . scc