-- | -- Module : Build -- Copyright : (C) 2014-2017 Jens Petersen -- -- Maintainer : Jens Petersen -- Stability : alpha -- -- Explanation: Main entry point for building RPM packages. -- This program is free software: you can redistribute it and/or modify -- it under the terms of the GNU General Public License as published by -- the Free Software Foundation, either version 3 of the License, or -- (at your option) any later version. module Main where #if (defined(MIN_VERSION_base) && MIN_VERSION_base(4,8,2)) #else import Control.Applicative ((<$>)) #endif import Control.Monad (filterM, unless, when) import Data.Maybe import Data.List (intercalate, isPrefixOf, isSuffixOf, nub, (\\)) import System.Directory (doesDirectoryExist, doesFileExist, getCurrentDirectory, setCurrentDirectory) import System.Environment (getArgs, getProgName) import System.Exit (ExitCode (..), exitWith) import System.FilePath ((), dropExtension) import System.IO (hPutStrLn, stderr) import Dists (Dist, dists, distBranch, distOverride, distTag, distTarget, releaseVersion) import Koji (kojiBuilding, kojiCheckFHBuilt, kojiLatestPkg, kojiWaitPkg, notInKoji, pkgDir) import RPM (packageManager, rpmInstall, repoquerySrc, rpmspec) import Utils ((+-+), checkFedoraPkgGit, cmd, cmd_, cmdBool, cmdMaybe, cmdlog, logMsg, removePrefix, removeSuffix, sudo) data Command = Install | Mock | Koji | Chain | Pending | Changed | Built | Bump deriving (Eq) main :: IO () main = do margs <- getArgs >>= parseArgs case margs of Nothing -> return () Just (com, dist, pkgs) -> do cwd <- getCurrentDirectory build cwd (mode com) dist Nothing Nothing False pkgs where mode "install" = Install mode "mock" = Mock mode "koji" = Koji mode "chain" = Chain mode "pending" = Pending mode "changed" = Changed mode "built" = Built mode "bump" = Bump mode _ = error "Unknown command" commands :: [String] commands = ["install", "mock" , "koji", "chain", "pending", "changed", "built", "bump"] help :: IO () help = do progName <- getProgName hPutStrLn stderr $ "Usage:" +-+ progName +-+ "CMD DIST PKG...\n" ++ "\n" ++ "Commands:\n" ++ " install\t- build locally and install\n" ++ " mock\t\t- build in mock\n" ++ " koji\t\t- build in Koji\n" ++ " chain\t\t- build deps recursively in Koji\n" ++ " pending\t- show planned changes\n" ++ " changed\t- show changed pkgs\n" ++ " built\t\t- show pkgs whose NVR already built\n" ++ " bump\t\t- bump release for NVRs already built\n" exitWith (ExitFailure 1) type Arguments = Maybe (String, Dist, [String]) parseArgs :: [String] -> IO Arguments parseArgs (c:dist:pkgs) | dist `notElem` dists = giveUp $ "Unknown dist '" ++ dist ++ "'" | null pkgs = giveUp "Please specify a package." | otherwise = return $ Just (c, dist, map (removeSuffix "/") pkgs) parseArgs _ = help >> return Nothing giveUp :: String -> IO Arguments giveUp err = do hPutStrLn stderr err return Nothing build :: FilePath -> Command -> Dist -> Maybe String -> Maybe (String, String) -> Bool -> [String] -> IO () build _ _ _ _ _ _ [] = return () build topdir mode dist msubpkg mlast waitrepo (pkg:rest) = do setCurrentDirectory topdir let branch = distBranch dist unless (mode `elem` [Pending, Changed, Built]) $ putStrLn $ "\n==" +-+ pkg ++ ":" ++ branch +-+ "==" dirExists <- doesDirectoryExist pkg unless dirExists $ do let anon = ["-a" | mode `notElem` [Koji, Chain]] cmdlog "fedpkg" $ ["clone", "-b", branch, pkg] ++ anon wd <- pkgDir pkg branch "" setCurrentDirectory wd ignore <- doesFileExist ".fhbuild-ignore" if ignore && mode `elem` [Install, Koji, Chain, Mock] then putStrLn "ignored" else do failing <- doesFileExist ".fhbuild-fail" when (failing && mode /= Pending) $ do putStrLn "skipped: found '.fhbuild-fail' file" exitWith (ExitFailure 1) retired <- doesFileExist "dead.package" if retired then do unless (mode == Pending) $ putStrLn "skipping dead.package" build topdir mode dist Nothing Nothing False rest else do pkggit <- do gd <- doesFileExist ".git/config" if gd then checkFedoraPkgGit else return False if not pkggit then if mode `elem` [Install, Koji, Chain] then error $ "not a Fedora pkg git dir!:" +-+ wd else build topdir mode dist Nothing Nothing False rest else do when dirExists $ do actual <- gitBranch when (branch /= actual) $ cmd_ "fedpkg" ["switch-branch", branch] cmd_ "git" ["pull", "-q"] -- noupdate <- doesFileExist ".noupdate" -- unless noupdate $ -- void $ cmdBool "cabal-rpm" ["update"] let spec = pkg ++ ".spec" hasSpec <- doesFileExist spec if not hasSpec then putStrLn $ "No" +-+ spec else do nvr <- cmd "fedpkg" ["verrel"] let verrel = removePrefix (pkg ++ "-") nvr release = tail $ dropWhile (/= '-') verrel tag = distTag dist relver = releaseVersion dist case mode of Install -> do let req = fromMaybe pkg msubpkg installed <- cmdMaybe "rpm" ["-q", "--qf", "%{name}-%{version}-%{release}", req] if Just (req ++ "-" ++ verrel) == installed then putStrLn $ nvr +-+ "already installed!\n" else do putStrLn $ fromMaybe "Not installed" installed +-+ "->" +-+ nvr cmd_ "git" ["--no-pager", "log", "-1"] putStrLn "" missing <- nub <$> (buildRequires spec >>= filterM notInstalled) -- FIXME sort into build order let hmissing = filter (\ dp -> "ghc-" `isPrefixOf` dp || dp `elem` ["alex", "cabal-install", "gtk2hs-buildtools", "happy"]) missing srcs <- nub <$> mapM (derefSrcPkg topdir dist relver) hmissing unless (null srcs) $ do putStrLn "Missing:" mapM_ putStrLn srcs build topdir Install dist Nothing Nothing False srcs setCurrentDirectory $ topdir wd stillMissing <- filterM notInstalled missing unless (null stillMissing) $ do putStrLn $ "Installing:" +-+ intercalate ", " stillMissing rpmInstall stillMissing putStrLn "" putStrLn $ "Building" +-+ nvr -- note "fedpkg --path dir local" saves .build.log in cwd success <- cmdBool "fedpkg" ["local"] unless success fhbuildFail opkgs <- lines <$> rpmspec ["--builtrpms"] (Just "%{name}\n") spec rpms <- lines <$> rpmspec ["--builtrpms"] (Just ("%{arch}/%{name}-%{version}-" ++ release ++ ".%{arch}.rpm\n")) spec putStrLn $ nvr +-+ "built\n" instpkgs <- lines <$> cmd "rpm" ("-qa":opkgs) if null instpkgs -- maybe filter out pandoc-pdf if not installed then rpmInstall rpms else do pkgmgr <- packageManager -- sudo pkgmgr ("--setopt=clean_requirements_on_remove=no":"remove":"-y":instpkgs) sudo pkgmgr ("install":"-y":rpms) setCurrentDirectory topdir build topdir Install dist Nothing Nothing False rest Mock -> do putStrLn $ "Mock building" +-+ nvr cmdlog "fedpkg" ["mockbuild"] build topdir Mock dist Nothing Nothing False rest Koji -> do putStrLn "'koji' mode is deprecated, please use 'chain'" unless (null rest) $ do putStrLn $ show (length rest) +-+ "more packages" putStrLn "" latest <- kojiLatestPkg tag pkg if nvr == latest then do putStrLn $ nvr +-+ "already built!" kojiWaitPkg topdir tag nvr build topdir Koji dist Nothing mlast False rest else do building <- kojiBuilding pkg nvr if building then do putStrLn $ nvr +-+ "is already building" kojiWaitPkg topdir tag nvr build topdir Koji dist Nothing Nothing False rest else do case mlast of Nothing -> return () Just (pkg', nvr') -> do dep <- dependent pkg' pkg branch topdir when dep $ do putStrLn $ "Waiting for" +-+ nvr' kojiWaitPkg topdir tag nvr' showChange latest nvr cmd_ "git" ["push"] fedpkgBuild topdir dist nvr (if waitrepo then Just tag else Nothing) bodhiOverride dist nvr unless (null rest) $ do dep <- dependent pkg (head rest) branch topdir when dep $ do putStrLn $ "Waiting for" +-+ nvr kojiWaitPkg topdir tag nvr build topdir Koji dist Nothing (if dep then Just (pkg, nvr) else Nothing) False rest Pending -> do latest <- kojiLatestPkg tag pkg unless (eqNVR nvr latest) $ putStrLn $ latest +-+ "->" +-+ nvr build topdir Pending dist Nothing Nothing False rest Changed -> do latest <- kojiLatestPkg tag pkg unless (eqNVR nvr latest) $ putStrLn pkg build topdir Changed dist Nothing Nothing False rest Built -> do latest <- kojiLatestPkg tag pkg when (eqNVR nvr latest) $ putStrLn pkg build topdir Built dist Nothing Nothing False rest Bump -> do latest <- kojiLatestPkg tag pkg when (eqNVR nvr latest) $ do putStrLn pkg cmd_ "rpmdev-bumpspec" ["-c", "rebuild", spec] cmd_ "fedpkg" ["commit", "-m", "bump release"] build topdir Bump dist Nothing Nothing False rest Chain -> do fhbuilt <- kojiCheckFHBuilt topdir nvr latest <- if fhbuilt then return nvr else kojiLatestPkg tag pkg if nvr == latest then do putStrLn $ nvr +-+ "already built!" unless (null rest) $ --do --kojiWaitPkg topdir tag nvr build topdir Chain dist Nothing Nothing False rest else do building <- kojiBuilding pkg nvr if building then do putStrLn $ nvr +-+ "is already building" kojiWaitPkg topdir tag nvr unless (null rest) $ build topdir Chain dist Nothing Nothing False rest else do showChange latest nvr brs <- buildRequires spec --print brs ghcLibs <- do ghcDir <- pkgDir "ghc" branch topdir filter isHaskellDevelPkg . words <$> rpmspec [] (Just "%{name}\n") (ghcDir "ghc.spec") -- FIXME sort into build order let hdeps = filter (\ dp -> "ghc-" `isPrefixOf` dp || dp `elem` ["alex", "cabal-install", "gtk2hs-buildtools", "happy"]) (brs \\ (["ghc-rpm-macros", "ghc-rpm-macros-extra"] ++ ghcLibs)) --print hdeps srcs <- filter (`notElem` ["ghc"]) . nub <$> mapM (derefSrcPkg topdir dist relver) hdeps --print srcs hmissing <- nub <$> filterM (notInKoji branch topdir tag) srcs putStrLn "" unless (null hmissing) $ do putStrLn "Missing:" mapM_ putStrLn hmissing build topdir Chain dist Nothing Nothing True hmissing setCurrentDirectory $ topdir wd putStrLn "" -- note "fedpkg --path dir local" saves .build.log in cwd cmd_ "git" ["push"] putStrLn "" fedpkgBuild topdir dist nvr (if waitrepo then Just tag else Nothing) bodhiOverride dist nvr unless (null rest) $ do putStrLn "" putStrLn $ show (length rest) +-+ "packages left" build topdir Chain dist Nothing (Just (pkg, nvr)) waitrepo rest maybePkgVer :: String -> Maybe String -> String maybePkgVer pkg mver = pkg ++ maybe "" ("-" ++) mver notInstalled :: String -> IO Bool notInstalled pkg = not <$> cmdBool "rpm" ["--quiet", "-q", pkg] showChange :: String -> String -> IO () showChange latest nvr = do cmd_ "git" ["--no-pager", "log", "-1"] putStrLn "" putStrLn $ latest +-+ "->" +-+ nvr ++ "\n" fedpkgBuild :: FilePath -> Dist -> String -> Maybe String -> IO () fedpkgBuild topdir dist nvr waittag = do giturl <- cmd "fedpkg" ["giturl"] success <- cmdBool "koji" ["build", "--fail-fast", distTarget dist, giturl] unless success fhbuildFail logMsg $ nvr +-+ "built" maybe (return ()) (\ t -> kojiWaitPkg topdir t nvr) waittag bodhiOverride :: Dist -> String -> IO () bodhiOverride dist nvr = when (distOverride dist) $ -- FIXME: improve Notes with recursive info cmd_ "bodhi" ["overrides", "save", "--notes", "Haskell stack", nvr] -- -- dereference meta BRs -- whatProvides :: String -> String -> IO String -- whatProvides relver pkg = do -- res <- repoquery relver ["--qf", "%{name}", "--whatprovides", pkg] -- --print res -- when (null res) $ do -- installed <- not <$> notInstalled pkg -- unless installed $ putStrLn $ "Warning:" +-+ pkg +-+ "not found by repoquery" -- return $ if null res then pkg else res buildRequires :: FilePath -> IO [String] buildRequires spec = -- FIXME should resolve "pkgconfig()" etc (map (head . words) . lines) <$> rpmspec ["--buildrequires"] Nothing spec -- >>= mapM (whatProvides relver) derefSrcPkg :: FilePath -> Dist -> String -> String -> IO String derefSrcPkg topdir dist relver pkg = if isHaskellDevelPkg pkg then do let base = removeSuffix "-devel" pkg dirExists <- doesDirectoryExist $ topdir base if dirExists then return base else derefSrcPkg topdir dist relver base else do putStrLn $ "Repoquerying" +-+ pkg res <- repoquerySrc dist relver pkg putStrLn $ pkg +-+ "->" +-+ show res case res of Nothing -> do putStrLn $ "Unknown package" +-+ removeSuffix "-devel" pkg fhbuildFail Just s -> return s gitBranch :: IO String gitBranch = (removePrefix "* " . head . filter (isPrefixOf "* ") . lines) <$> cmd "git" ["branch"] eqNVR :: String -> String -> Bool eqNVR p1 p2 = dropExtension p1 == dropExtension p2 -- "pkg = X.Y" -> ["pkg", "=", "X.Y"] -> ("pkg", Just "X.Y") processDeps :: [String] -> (String, Maybe String) processDeps [p, "=", v] = (p, Just v) processDeps (p:_) = (p, Nothing) processDeps [] = error "processDeps: empty string!" dependent :: String -> String -> String -> FilePath -> IO Bool dependent dep pkg branch topdir = do pkgpath <- pkgDir pkg branch topdir cmdBool "grep" ["-q", dep, pkgpath pkg ++ ".spec"] displayLogTail :: FilePath -> IO () displayLogTail f = do ls <- lines <$> readFile f let foot = 3 disp = 12 start = length ls - (disp + foot) mapM_ putStrLn $ take disp $ drop start ls fhbuildFail :: IO a fhbuildFail = --do --cmdlog "touch" [".fhbuild-fail"] exitWith (ExitFailure 1) isHaskellDevelPkg :: String -> Bool isHaskellDevelPkg pkg = "ghc-" `isPrefixOf` pkg && ("-devel" `isSuffixOf` pkg)