packages feed

koji-tool-1.3: src/Builds.hs

{-# LANGUAGE BangPatterns, CPP #-}

-- SPDX-License-Identifier: BSD-3-Clause

module Builds (
  BuildReq(..),
  Details(..),
  buildsCmd,
  parseBuildState',
  fedoraKojiHub,
  kojiBuildTypes,
  latestCmd,
  taggedCmd
  )
where

import Control.Monad.Extra

import Data.Char (isDigit, toUpper)
import Data.List.Extra
import Data.Maybe
import Data.RPM.NVR
import Data.Time.Clock
import Data.Time.LocalTime
import Distribution.Koji
import Distribution.Koji.API
import Safe (headMay)
import SelectRPMs (selectDefault)
import SimpleCmd
import Text.Pretty.Simple

import Common
import Install
import qualified Tasks
import Time
import User
import Utils (buildOutputURL)

-- FIXME NonEmptyString's
data BuildReq = BuildBuild String | BuildPackage String
              | BuildQuery | BuildPattern String
  deriving Eq

getTimedate :: Tasks.BeforeAfter -> String
getTimedate (Tasks.Before s) = s
getTimedate (Tasks.After s) = s

capitalize :: String -> String
capitalize "" = ""
capitalize (h:t) = toUpper h : t

data Details = Detailed | DetailedTasks
  deriving Eq

buildsCmd :: Maybe String -> Maybe UserOpt -> Maybe Limit -> [BuildState]
          -> Maybe Tasks.BeforeAfter -> Maybe String -> Maybe Details -> Bool
          -> Maybe Select -> Bool -> BuildReq -> IO ()
buildsCmd mhub museropt mlimit !states mdate mtype mdetails tail' minstall debug buildreq = do
  when (hub /= fedoraKojiHub && museropt == Just UserSelf) $
    error' "--mine currently only works with Fedora Koji: use --user instead"
  tz <- getCurrentTimeZone
  case buildreq of
    BuildBuild bld -> do
      when (isJust mdate) $
        error' "cannot use buildinfo together with timedate"
      let bldinfo = if all isDigit bld
                    then InfoID (read bld)
                    else InfoString bld
      mbld <- getBuild hub bldinfo
      whenJust (mbld >>= maybeBuildResult) $ printBuild hub tz mdetails tail' debug minstall
    BuildPackage pkg -> do
      when (headMay pkg == Just '-') $
        error' $ "bad combination: not a package: " ++ pkg
      when (isJust mdate) $
        error' "cannot use --package together with timedate"
      mpkgid <- getPackageID hub pkg
      case mpkgid of
        Nothing -> error' $ "no package id found for " ++ pkg
        Just pkgid -> do
          query <- setupQuery
          let fullquery = [("packageID", ValueInt pkgid),
                           commonBuildQueryOptions $
                           maybeLimit defaultBuildsLimit mlimit]
                          ++ query
          when debug $ print fullquery
          builds <- listBuilds hub fullquery
          when debug $ mapM_ pPrintCompact builds
          if isJust mdetails || length builds == 1
            then mapM_ (printBuild hub tz mdetails tail' debug minstall) $ mapMaybe maybeBuildResult builds
            else mapM_ putStrLn $ mapMaybe (shortBuildResult tz) builds
    _ -> do
      query <- setupQuery
      let fullquery = query ++ [commonBuildQueryOptions $
                                maybeLimit defaultBuildsLimit mlimit]
      when debug $ print fullquery
      builds <- listBuilds hub fullquery
      when debug $ mapM_ pPrintCompact builds
      if isJust mdetails || length builds == 1
        then mapM_ (printBuild hub tz mdetails tail' debug minstall) $ mapMaybe maybeBuildResult builds
        else mapM_ putStrLn $ mapMaybe (shortBuildResult tz) builds
  where
    hub = maybe fedoraKojiHub hubURL mhub

    shortBuildResult :: TimeZone -> Struct -> Maybe String
    shortBuildResult tz bld = do
      nvr <- lookupStruct "nvr" bld
      state <- readBuildState <$> lookupStruct "state" bld
      let date =
            case lookupStartEndTimes bld of
              Nothing -> ""
              Just (start,mend) ->
                compactZonedTime tz $ fromMaybe start mend
          mbid = lookupStruct "build_id" bld
      return $ date +-+ maybe "" (buildinfoUrl hub) mbid +-+ show state +-+ nvr

    setupQuery = do
      mdatestring <-
        case mdate of
          Nothing -> return Nothing
          Just date -> Just <$> cmd "date" ["+%F %T%z", "--date=" ++ dateString date]
      -- FIXME better output including user
      whenJust mdatestring $ \date ->
        putStrLn $ maybe "" show mdate +-+ date
      mowner <- maybeGetKojiUser hub museropt
      pat <-
        case buildreq of
          BuildPattern pat -> do
            -- FIXME remove after moving to newer koji-hs
            -- rpmfusion koji still doesn't support patterns (2024-09-20)
            when ("rpmfusion" `isInfixOf` hub) $
              error' "cannot use pattern with this kojihub"
            return [("pattern", ValueString pat)]
          _ -> return []
      return $
        [("complete" ++ (capitalize . show) date, ValueString datestring) | Just date <- [mdate], Just datestring <- [mdatestring]]
        ++ [("userID", ValueInt (getID owner)) | Just owner <- [mowner]]
        ++ [("state", ValueArray (map buildStateToValue states)) | notNull states]
        ++ [("type", ValueString typ) | Just typ <- [mtype]]
        ++ pat

    dateString :: Tasks.BeforeAfter -> String
    -- make time refer to past not future
    dateString beforeAfter =
      let timedate = getTimedate beforeAfter
      in case words timedate of
           [t] | t `elem` ["hour", "day", "week", "month", "year"] ->
                 "last " ++ t
           [t] | t `elem` ["today", "yesterday"] ->
                 t ++ " 00:00"
           [t] | any (lower t `isPrefixOf`) ["monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday"] ->
                 "last " ++ t ++ " 00:00"
           [n,_unit] | all isDigit n -> timedate ++ " ago"
           _ -> timedate

    pPrintCompact =
#if MIN_VERSION_pretty_simple(4,0,0)
      pPrintOpt CheckColorTty
      (defaultOutputOptionsDarkBg {outputOptionsCompact = True,
                                   outputOptionsCompactParens = True})
#else
      pPrint
#endif

buildinfoUrl :: String -> Int -> String
buildinfoUrl hub bid =
  webUrl hub ++ "/buildinfo?buildID=" ++ show bid

data BuildResult =
  BuildResult {buildNVR :: NVR,
               buildState :: BuildState,
               _buildId :: Int,
               mbuildTaskId :: Maybe Int,
               _buildStartTime :: UTCTime,
               mbuildEndTime :: Maybe UTCTime,
               _buildOwner :: String
              }

maybeBuildResult :: Struct -> Maybe BuildResult
maybeBuildResult st = do
  (start,mend) <- lookupStartEndTimes st
  buildid <- lookupStruct "build_id" st
  -- buildContainer has no task_id
  let mtaskid = lookupStruct "task_id" st
  state <- getBuildState st
  nvr <- lookupStruct "nvr" st >>= maybeNVR
  owner <- lookupStruct "owner_name" st
  return $
    BuildResult nvr state buildid mtaskid start mend owner

printBuild :: String -> TimeZone -> Maybe Details -> Bool -> Bool
           -> Maybe Select -> BuildResult -> IO ()
printBuild hub tz mdetails tail' debug minstall build = do
  putStrLn ""
  let mendtime = mbuildEndTime build
  time <- maybe getCurrentTime return mendtime
  (mapM_ putStrLn . formatBuildResult hub (isJust mendtime) tz) (build {mbuildEndTime = Just time})
  when (buildState build == BuildComplete) $
    putStrLn $ buildOutputURL hub $ buildNVR build
  whenJust (mbuildTaskId build) $ \taskid -> do
    if mdetails == Just DetailedTasks
      then do
      putStrLn ""
      Tasks.tasksCmd (Just hub) Tasks.emptyQueryOpts Nothing tail' Tasks.BuildLog Nothing minstall (Tasks.ChildrenOf taskid)
      else
      when tail' $
      void $ cmdBool "koji" ["watch-task", show taskid]
    whenJust minstall $ \installopts -> do
      putStrLn ""
      installCmd False debug No (Just hub) Nothing False False False Nothing [] Nothing Nothing installopts Nothing (Right ReqNVR) [showNVR (buildNVR build)]

formatBuildResult :: String -> Bool -> TimeZone -> BuildResult -> [String]
formatBuildResult hub ended tz (BuildResult nvr state buildid mtaskid start mendtime owner) =
  [ showNVR nvr +-+ show state +-+ '(' : owner ++ ")"
  , buildinfoUrl hub buildid]
  ++ [Tasks.taskinfoUrl hub taskid | Just taskid <- [mtaskid]]
  ++ [formatLocalTime StartEvent tz start]
  ++
  case mendtime of
    Nothing -> []
    Just end ->
      [formatLocalTime CompletionEvent tz end | ended]
#if MIN_VERSION_time(1,9,1)
      ++
      let dur = diffUTCTime end start
      in [(if not ended then "current " else "") ++ "duration: " ++ renderDuration False dur]
#endif

#if !MIN_VERSION_koji(0,0,3)
buildStateToValue :: BuildState -> Value
buildStateToValue = ValueInt . fromEnum
#endif

parseBuildState' :: String -> BuildState
parseBuildState' s =
  case lower s of
    "build" -> BuildBuilding
    "building" -> BuildBuilding
    "open" -> BuildBuilding
    "run" -> BuildBuilding
    "complete" -> BuildComplete
    "deleted" -> BuildDeleted
    "fail" -> BuildFailed
    "failed" -> BuildFailed
    "cancel" -> BuildCanceled
    "canceled" -> BuildCanceled
    _ -> error' $! "unknown build state: " ++ s ++
         "\nknown states are: build(ing), complete, deleted, fail(ed), cancel(ed)"

kojiBuildTypes :: [String]
kojiBuildTypes = ["all", "image", "maven", "module", "rpm", "win"]

latestCmd :: Maybe String -> Bool -> String -> String -> IO ()
latestCmd mhub debug tag pkg = do
  let hub = maybe fedoraKojiHub hubURL mhub
  mbld <- kojiLatestBuild hub tag pkg
  when debug $ print mbld
  tz <- getCurrentTimeZone
  whenJust (mbld >>= maybeBuildResult) $ printBuild hub tz (Just Detailed) False debug Nothing

-- FIXME select packages and rpms
taggedCmd :: Maybe String -> Bool -> String -> IO ()
taggedCmd mhub install tag = do
  let hub = maybe fedoraKojiHub hubURL mhub
  kblds <- kojiListTaggedBuilds hub install tag
--  when debug $ mapM_ print blds
--  tz <- getCurrentTimeZone
  -- forM_ (mapMaybe maybeBuildResult blds) $
  --   printBuild hub tz (Just Detailed) False debug Nothing
  mapM_ putKojiBuild kblds
  when install $
    installCmd False False No (Just hub) Nothing False False False Nothing [] Nothing Nothing selectDefault Nothing (Right ReqNVR) $ map kbNvr kblds
  where
    putKojiBuild :: KojiBuild -> IO ()
    putKojiBuild (KojiBuild _ _ owner nvr) =
      putStrLn $ nvr +-+ owner