packages feed

fbrnch-1.6: src/Cmd/Copr.hs

{-# LANGUAGE CPP #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}

module Cmd.Copr (
  BuildBy(..),
  coprCmd,
  CoprMode(..)
  )
where

import Data.Aeson.Types (Object)
#if MIN_VERSION_aeson(2,0,0)
import Data.Aeson.Key
import qualified Data.Aeson.KeyMap as M
#else
import qualified Data.HashMap.Strict as M
#endif
import Data.Char (isDigit)
import  Data.Either.Combinators (whenLeft)
import Data.Ini.Config
import Data.RPM.NVR (nvrVerRel)
import Data.RPM.VerRel (showVerRel)
import Data.Tuple.Extra (first)
import Distribution.Fedora.Branch (getActiveBranches, getActiveBranched)
import Network.HTTP.Query (lookupKey, lookupKey')
import Safe (headDef)
import System.Environment.XDG.BaseDir (getUserConfigDir)
import System.Time.Extra (sleep)
import Web.Fedora.Copr (coprChroots, fedoraCopr)
import Web.Fedora.Copr.API (coprGetBuild, coprGetBuildList, coprMonitorProject)

import Branches
import Common
import Common.System
import qualified Common.Text as T
import Package
import RpmBuild (generateSrpmNoDist)
import Types (Archs(..))

data BuildBy = SingleBuild
             | ValidateByRelease
             | ValidateByArch
             | BuildByRelease
  deriving Eq

coprServer :: String
coprServer = "copr.fedorainfracloud.org"

data Chroot = Chroot { chrootBranch :: Branch
                     , chrootArch :: Arch }
  deriving Eq

releaseToBranch :: String -> Branch
releaseToBranch release
  | "fedora-" `isPrefixOf` release =
    case dropPrefix "fedora-" release of
      "rawhide" -> Rawhide
      n | all isDigit n -> Fedora (read n)
        | otherwise -> error' $ "unknown fedora release:" +-+ release
  | "epel-" `isPrefixOf` release =
    let next = "-next" `isSuffixOf` release
    in case dropSuffix "-next" (dropPrefix "epel-" release) of
         n | all isDigit n -> (if next then EPELNext else EPEL) $ read n
           | otherwise -> error' $ "unknown epel release:" +-+ release
  | otherwise = error' $ "unknown release:" +-+ release

branchToRelease :: Branch -> String
branchToRelease Rawhide = "fedora-rawhide"
branchToRelease (Fedora n) = "fedora-" ++ show n
branchToRelease (EPEL n) = "epel-" ++ show n
branchToRelease (EPELNext n) = "epel-" ++ show n ++ "-next"

readChroot :: String -> Chroot
readChroot ch =
  case stripInfixEnd "-" ch of
    Nothing -> error' $ "invalid chroot:" +-+ ch
    Just (b,a) -> Chroot (releaseToBranch b) (readArch a)

showChroot :: Chroot -> String
showChroot (Chroot br ar) = branchToRelease br ++ '-' : showArch ar

data Arch = X86_64 | I686 | AARCH64 | PPC64LE | S390X
  deriving (Eq, Enum)

allCoprArchs :: [Arch]
allCoprArchs = [X86_64 ..]

readArch :: String -> Arch
readArch "x86_64" = X86_64
readArch "i686" = I686
readArch "aarch64" = AARCH64
readArch "ppc64le" = PPC64LE
readArch "s390x" = S390X
readArch a = error' $ "unknown arch:" +-+ a

showArch :: Arch -> String
showArch X86_64 = "x86_64"
showArch I686 = "i686"
showArch AARCH64 = "aarch64"
showArch PPC64LE = "ppc64le"
showArch S390X = "s390x"

data CoprMode = ListChroots | CoprMonitor | CoprBuild | CoprNew

-- FIXME take ExclusiveArch/ExcludeArch into account
-- FIXME -1 for only first unbuilt chroot
-- FIXME check branch
-- FIXME handle fedora-eln
-- FIXME make project optional (if same as pkg??) or configurable ?
-- FIXME repo config with a setup command?
-- FIXME interact with copr dist-git
-- FIXME parallel copr builds
-- FIXME time builds?
coprCmd :: Bool -> CoprMode -> Bool -> Maybe BuildBy -> Maybe Archs -> String
        -> (BranchesReq,[String]) -> IO ()
coprCmd dryrun mode force mbuildBy marchs project (breq, pkgs) = do
  user <- getUsername
  let buildBy = fromMaybe ValidateByRelease mbuildBy
  case mode of
    ListChroots -> coprGetChroots user >>= mapM_ (putStrLn . showChroot)
    CoprMonitor -> coprMonitorPackages user project >>= mapM_ printPkgRes
    CoprNew -> coprNewProject dryrun project marchs breq pkgs
    CoprBuild -> do
      chroots <- coprGetChroots user
      if null pkgs
        then coprBuildPkg user buildBy chroots False
        else
        mapM_ (\(n,p) -> withExistingDirectory p $ coprBuildPkg user buildBy chroots (n>0)) $ zip (reverse [0..length pkgs - 1]) pkgs
  where
    coprGetChroots :: String -> IO [Chroot]
    coprGetChroots user = do
      chroots <- map (readChroot . T.unpack) <$> coprChroots coprServer user project
      when (null chroots) $
        error' $ "No chroots found for" +-+ user ++ "/" ++ project
      branches <-
        case breq of
          Branches brs ->
            if null brs
            then return $ (nub . map chrootBranch) chroots
            else listOfBranches False False breq
          BranchOpt AllFedora -> filter isFedoraBranch <$> getActiveBranches
          BranchOpt AllEPEL -> filter isEPELBranch <$> getActiveBranched
          _ -> listOfBranches False False breq
      let buildroots =
            -- FIXME sort archs appropriately
            case marchs of
              Nothing ->
                [chroot | arch <- sortArchs allCoprArchs, br <- branches, let chroot = Chroot br arch, chroot `elem` chroots]
              -- FIXME warn about missing arch chroots
              Just (Archs archs) ->
                [chroot | arch <- sortArchs (map readArch archs), br <- branches, let chroot = Chroot br arch, chroot `elem` chroots]
              -- FIXME warn about missing arch chroots
              Just (ExcludedArchs exarchs) ->
                [chroot | arch <- sortArchs allCoprArchs, arch `notElem` map readArch exarchs, br <- branches, let chroot = Chroot br arch, chroot `elem` chroots]
      if null buildroots
        then error' "No valid chroots"
        else return buildroots

    coprBuildPkg :: String -> BuildBy -> [Chroot] -> Bool -> IO ()
    coprBuildPkg user buildBy chroots morepkgs = do
      -- FIXME check is pkg.spec
      -- was: localBranchSpecFile pkg (RelBranch Rawhide)
      spec <- findSpecfile
      -- pkg <- takeFileName <$> getCurrentDirectory
      srpm <- if not dryrun
              then generateSrpmNoDist True False Nothing spec
              else return spec -- hack to avoid generating srpm for dryrun
      verrel <- showVerRel . nvrVerRel <$> pkgNameVerRelNodist spec
      actualpkg <- cmd "rpmspec" ["-q", "--srpm", "--qf", "%{name}", spec]
      builtChroots <- existingChrootBuilds dryrun user project (Package actualpkg) verrel chroots
      let finalChroots = chroots \\ if force then [] else map taskChroot builtChroots
      case finalChroots of
        [] -> putStrLn $ actualpkg ++ '-' : verrel +-+ "built"
        (final:_) ->
          case buildBy of
            SingleBuild -> coprBuild dryrun user project srpm spec finalChroots
            -- FIXME or default to secondary parallel to previous primary
            ValidateByRelease -> do
              let initialChroots =
                    let primaryArch = chrootArch final
                    in map pure $ filter (isArch primaryArch) finalChroots
                  remainingChroots = finalChroots \\ concat initialChroots
              staggerBuilds srpm spec initialChroots remainingChroots
            ValidateByArch -> do
              let initialChroots =
                    let newestRelease = chrootBranch final
                    in map pure $ filter ((== newestRelease) . chrootBranch) finalChroots
                  remainingChroots = finalChroots \\ concat initialChroots
              staggerBuilds srpm spec initialChroots remainingChroots
            BuildByRelease -> do
              let releaseChroots = groupBy sameRelease finalChroots
              staggerBuilds srpm spec releaseChroots []
      when morepkgs putNewLn
      where
        staggerBuilds :: FilePath -> FilePath -> [[Chroot]] -> [Chroot] -> IO ()
        staggerBuilds srpm spec initialChroots remainingChroots = do
          mapM_ (coprBuild dryrun user project srpm spec) initialChroots
          unless (null remainingChroots) $
            coprBuild dryrun user project srpm spec remainingChroots

    isArch arch release = chrootArch release == arch

    sameRelease r1 r2 = chrootBranch r1 == chrootBranch r2

getUsername :: IO String
getUsername = do
  rc <- getUserConfigDir "copr"
  readIniConfig rc rcParser id
  where
    rcParser :: IniParser String
    rcParser =
      section "copr-cli" $
      fieldOf "username" string

-- changed from Bugzilla.hs
readIniConfig :: FilePath -> IniParser a -> (a -> b) -> IO b
readIniConfig inifile iniparser record = do
  havefile <- doesFileExist inifile
  if not havefile
    then error' $ inifile +-+ "not found: try https://" ++ coprServer ++ "/api"
    else do
    ini <- T.readFile inifile
    let config = parseIniFile ini iniparser
    return $ either error' record config

-- from copr/frontend/coprs_frontend/coprs/logic/builds_logic.py
-- FIXME after adding to copr-api
coprProcessingStates :: [String]
coprProcessingStates =
  ["running", "pending", "starting", "importing", "waiting"]

-- coprEndedStates :: [String]
-- coprEndedStates =
--   ["canceled", "failed", "skipped", "succeeded"]

-- FIXME restrict to requested chroots?
existingChrootBuilds :: Bool -> String -> String -> Package -> String
                     -> [Chroot] -> IO [CoprTask]
existingChrootBuilds dryrun user project actualpkg verrel chroots = do
  monitorPkgs <- coprMonitorPackages user project
  let pkgmonitor = fromMaybe [] $ lookup actualpkg monitorPkgs
  let buildingChroots =
        filterTasks verrel (`elem` coprProcessingStates) pkgmonitor
      builds = filterTasks verrel (`notElem` ["failed","skipped"]) pkgmonitor
  if null buildingChroots
    then return builds
    else do
    mapM_ printCoprTask buildingChroots
    putNewLn
    unless dryrun $
      forM_ buildingChroots $ \building ->
      when (taskChroot building `elem` chroots) $
      -- FIXME check failure
      void $ coprWatchBuild Nothing $ Left building
    return builds

filterTasks :: String -> (String -> Bool) -> [CoprTask] -> [CoprTask]
filterTasks verrel statustest =
  filter (\CoprTask{..} -> taskVerRel == verrel && statustest taskStatus)

coprBuild :: Bool -> String -> String -> FilePath -> FilePath -> [Chroot] -> IO ()
coprBuild _ _ _ _ _ [] = error' "No chroots chosen"
coprBuild dryrun user project srpm spec chroots = do
  let chrootargs = mconcat [["-r", showChroot bldrt] | bldrt <- chroots]
      buildargs = ["build", "--nowait"] ++ chrootargs ++ [project, srpm]
  putNewLn
  putStrLn $ unwords $ map showChroot chroots
  unless dryrun $ do
    output <- cmd "copr" buildargs
    putStrLn output
    let bid = read $ last $ words $ last $ lines output
    -- FIXME get the actual build time
    ok <- timeIO $ coprWatchBuild Nothing $ Right bid
    unless ok $ do
      putStrLn $ "Failed: copr" +-+ unwords buildargs
      -- FIXME determine which chroot(s) failed
      -- eg 06482247
      let zbid =
            let s = show bid
            in (if length s < 8 then ('0' :) else id) s
      actualpkg <- cmd "rpmspec" ["-q", "--srpm", "--qf", "%{name}", spec]
      -- FIXME which chroot?
      -- FIXME print buildlog size
      error' $ "https://download.copr.fedorainfracloud.org/results" +/+ user +/+ project +/+ showChroot (headDef (error' " nochroot!") chroots) +/+ zbid ++ "-" ++ actualpkg +/+ "builder-live.log.gz"

-- FIXME idea: Maybe Seconds to increment FIXME
-- sleep should have all chroots in pending CoprTask build
coprWatchBuild :: Maybe String -> Either CoprTask Int -> IO Bool
coprWatchBuild mstate ebuild = do
  let bid = either taskBuild id ebuild
  res <- coprGetBuild fedoraCopr bid
  case lookupKey "state" res of
    Just state ->
      if mstate == Just state
      then sleep 20 >> coprWatchBuild mstate (Right bid)
      else do
        whenLeft ebuild $ \t -> putStrLn $ "#" +-+ showChroot (taskChroot t)
        logMsg $ "Build" +-+ show bid +-+ state +-+ either taskVerRel (const "") ebuild
        case state of
          "succeeded" -> return True
          "skipped" -> return True
          "canceled" -> return False
          "failed" -> return False
          _ -> sleep 1 >> coprWatchBuild (Just state) (Right bid)
    Nothing -> do
      let err = lookupKey' "error" res
      logMsg $ "Error:" +-+ err
      return False

archPriorities :: [(Arch,Int)]
archPriorities = [(X86_64,1), (AARCH64,2)]

sortArchs :: [Arch] -> [Arch]
sortArchs = sortBy priority
  where
    priority a1 a2 =
      let mp1 = lookup a1 archPriorities
          mp2 = lookup a2 archPriorities
      in
        case (mp1,mp2) of
          (Just p1,Just p2) ->
            compare p1 p2
          (Just _, Nothing) -> LT
          (Nothing, Just _) -> GT
          (Nothing,Nothing) -> EQ

type CoprPackage = (Package,[CoprTask])

data CoprTask = CoprTask {taskChroot :: Chroot,
                          taskBuild :: Int,
                          taskStatus :: String,
                          taskVerRel :: String
                         }

-- FIXME wait for None package to appear
coprMonitorPackages :: String -> String -> IO [CoprPackage]
coprMonitorPackages user proj = do
  builds <- coprGetBuildList fedoraCopr user proj []
  mapM_ coprWaitPackage (lookupKey' "items" builds :: [Object])
  mapMaybe pkgResults . lookupKey' "packages" <$>
    coprMonitorProject fedoraCopr user proj []
  where
    pkgResults :: Object -> Maybe CoprPackage
    pkgResults obj = do
      name <- lookupKey "name" obj
      chroots <- map (first toText) . M.toList <$> lookupKey "chroots" obj
      return (Package name, mapMaybe chrootResult chroots)
#if !MIN_VERSION_aeson(2,0,0)
      where toText = id
#endif

    chrootResult :: (T.Text,Object) -> Maybe CoprTask
    chrootResult (chroot, obj) = do
      state <- lookupKey "state" obj
      version <- lookupKey "pkg_version" obj
      build <- lookupKey "build_id" obj
      return $ CoprTask (readChroot (T.unpack chroot)) build state version

printPkgRes :: (Package, [CoprTask]) -> IO ()
printPkgRes (pkg,chroots) = do
  putStrLn $ "# " <> unPackage pkg
  mapM_ printCoprTask chroots
  putNewLn

printCoprTask :: CoprTask -> IO ()
printCoprTask (CoprTask chr build status version) =
  putStrLn $ showChroot chr +-+ show build ++ ":" +-+ status +-+ version

coprWaitPackage :: Object -> IO ()
coprWaitPackage build = do
  case buildResult of
    Nothing -> error' "unknown source_package"
    Just (_, "failed", _) -> return ()
    Just (_, _, Just _pkg) -> return ()
    Just (bid, _, Nothing) -> do
      sleep 5
      print bid
      coprGetBuild fedoraCopr bid >>= coprWaitPackage
  where
    buildResult = do
      bid <- lookupKey "id" build
      source <- lookupKey "source_package" build
      state <- lookupKey "state" build
      let mname = lookupKey "name" source
      return (bid :: Int, state :: String, mname :: Maybe String)

coprNewProject :: Bool -> String -> Maybe Archs -> BranchesReq -> [String]
               -> IO ()
coprNewProject dryrun project marchs breq pkgs = do
  branches <- listOfBranches False False breq
  let archs =
        case marchs of
          Nothing -> [X86_64]
          Just (Archs as) -> map readArch as
          Just (ExcludedArchs eas) -> allCoprArchs \\ map readArch eas
  let chroots = [ Chroot b a | b <- branches, a <- archs]
  (if dryrun then cmdN else cmd_) "copr" $ "create" : concatMap (\ch -> ["--chroot", showChroot ch]) chroots ++ [project]
  unless (null pkgs) $
    coprCmd False CoprBuild False Nothing Nothing project (breq, pkgs)