koji-tool-0.8.7: src/Install.hs
{-# LANGUAGE CPP #-}
-- SPDX-License-Identifier: BSD-3-Clause
module Install (
Mode(..),
Request(..),
installCmd,
knownHubs,
Yes(..)
)
where
import Control.Monad.Extra
import Data.Char
import Data.List.Extra
import Data.Maybe
import Data.RPM.NV hiding (name)
import Data.RPM.NVR
import Data.RPM.NVRA
import Distribution.Koji
import qualified Distribution.Koji.API as Koji
import Network.HTTP.Directory (httpFileSize', httpLastModified', (+/+))
import SimpleCmd
import System.Directory
import System.FilePath
import System.FilePath.Glob
import System.IO
import Text.Read (readMaybe)
import Common
import DownloadDir
import Utils
defaultPkgsURL :: String -> String
defaultPkgsURL url =
case dropSuffix "/" url of
"https://koji.fedoraproject.org/kojihub" ->
"https://kojipkgs.fedoraproject.org/packages"
"https://kojihub.stream.centos.org/kojihub" ->
"https://kojihub.stream.centos.org/kojifiles/packages"
_ ->
if "kojihub" `isSuffixOf` url
then replace "kojihub" "kojifiles" url +/+ "packages"
else error' $ "use --files-url to specify kojifiles url for " ++ url
data Yes = No | Yes
deriving Eq
data Mode = All
| Ask
-- distinguish except and exclude
| PkgsReq [String] [String] -- ^ include, except/exclude
deriving Eq
data Request = ReqName | ReqNV | ReqNVR
deriving Eq
-- FIXME --include devel, --exclude *
-- FIXME specify tag or task
-- FIXME support enterprise builds
-- FIXME --arch (including src)
-- FIXME --debuginfo
-- FIXME --delete after installing
-- FIXME --dnf to install selected packages using default dnf repo instead
installCmd :: Bool -> Bool -> Yes -> Maybe String -> Maybe String -> Bool
-> Bool -> Bool -> Maybe String -> Mode -> String -> Request
-> [String] -> IO ()
installCmd dryrun debug yes mhuburl mpkgsurl listmode latest noreinstall mprefix mode disttag request pkgbldtsks = do
let huburl = maybe fedoraKojiHub hubURL mhuburl
pkgsurl = fromMaybe (defaultPkgsURL huburl) mpkgsurl
when debug $ do
putStrLn huburl
putStrLn pkgsurl
printDlDir <- setDownloadDir dryrun "rpms"
when debug printDlDir
setNoBuffering
mapM (kojiRPMs huburl pkgsurl printDlDir) pkgbldtsks
>>= installRPMs dryrun noreinstall yes . mconcat
where
kojiRPMs :: String -> String -> IO () -> String -> IO [(Existence,NVRA)]
kojiRPMs huburl pkgsurl printDlDir bldtask =
case readMaybe bldtask of
Just taskid -> kojiTaskRPMs dryrun debug yes huburl pkgsurl listmode noreinstall mprefix mode printDlDir taskid
Nothing -> kojiBuildRPMs huburl pkgsurl printDlDir bldtask
kojiBuildRPMs :: String -> String -> IO () -> String
-> IO [(Existence,NVRA)]
kojiBuildRPMs huburl pkgsurl printDlDir pkgbld = do
nvrs <- map readNVR <$> kojiBuildOSBuilds debug huburl listmode latest disttag request pkgbld
if listmode
then do
if mode /= PkgsReq [] []
then error' "modes not supported for listing build" -- FIXME
else case nvrs of
[nvr] -> do
putStrLn (showNVR nvr)
putStrLn ""
kojiGetBuildRPMs huburl nvr >>=
mapM_ putStrLn . sort . filter notDebugPkg
_ -> mapM_ (putStrLn . showNVR) nvrs
return []
else
case nvrs of
[] -> error' $ pkgbld ++ " not found for " ++ disttag
[nvr] -> do
putStrLn $ showNVR nvr ++ "\n"
nvras <- sort . map readNVRA . filter notDebugPkg <$> kojiGetBuildRPMs huburl nvr
when debug $ mapM_ (putStrLn . showNVRA) nvras
let prefix = fromMaybe (nvrName nvr) mprefix
dlRpms <- decideRpms yes listmode noreinstall mode prefix nvras
when debug $ mapM_ printInstalled dlRpms
unless (dryrun || null dlRpms) $ do
downloadRpms debug (buildURL nvr) dlRpms
-- FIXME once we check file size - can skip if no downloads
printDlDir
return dlRpms
_ -> error $ "multiple build founds for " ++ pkgbld ++ ": " ++
unwords (map showNVR nvrs)
where
buildURL :: NVR -> String -> String
buildURL (NVR n (VerRel v r)) rpm =
let arch = rpmArch (readNVRA rpm)
in pkgsurl +/+ n +/+ v +/+ r +/+ arch +/+ rpm
notDebugPkg :: String -> Bool
notDebugPkg p =
not ("-debuginfo-" `isInfixOf` p || "-debugsource-" `isInfixOf` p)
kojiTaskRPMs :: Bool -> Bool -> Yes -> String -> String -> Bool -> Bool
-> Maybe String -> Mode -> IO () -> Int -> IO [(Existence,NVRA)]
kojiTaskRPMs dryrun debug yes huburl pkgsurl listmode noreinstall mprefix mode printDlDir taskid = do
mtaskinfo <- Koji.getTaskInfo huburl taskid True
tasks <- case mtaskinfo of
Nothing -> error' "failed to get taskinfo"
Just taskinfo -> do
when debug $ mapM_ print taskinfo
case lookupStruct "method" taskinfo :: Maybe String of
Nothing -> error' $ "no method found for " ++ show taskid
Just method ->
case method of
"build" -> Koji.getTaskChildren huburl taskid False
"buildArch" -> return [taskinfo]
_ -> error' $ "unsupport method: " ++ method
sysarch <- cmd "rpm" ["--eval", "%{_arch}"]
let (archtid,archtask) =
case find (\t -> lookupStruct "arch" t == Just sysarch) tasks of
Nothing -> error' $ "no " ++ sysarch ++ " task found"
Just task' ->
case lookupStruct "id" task' of
Nothing -> error' "task id not found"
Just tid -> (tid,task')
nvras <- getTaskNVRAs archtid
prefix <- case mprefix of
Just pref -> return pref
Nothing ->
case find ((== "src") . rpmArch) nvras of
Just src -> return $ rpmName src
Nothing ->
return $
either id nvrName $
kojiTaskRequestPkgNVR $
fromMaybe archtask mtaskinfo
if listmode
then decideRpms yes listmode noreinstall mode prefix nvras
else
if null nvras
then do
kojiTaskRPMs dryrun debug yes huburl pkgsurl True noreinstall mprefix mode printDlDir archtid >>= mapM_ printInstalled
return []
else do
when debug $ print $ map showNVRA nvras
dlRpms <- decideRpms yes listmode noreinstall mode prefix $
filter ((/= "src") . rpmArch) nvras
when debug $ mapM_ printInstalled dlRpms
unless (dryrun || null dlRpms) $ do
downloadRpms debug (taskRPMURL archtid) dlRpms
printDlDir
return dlRpms
where
getTaskNVRAs :: Int -> IO [NVRA]
getTaskNVRAs taskid' =
sort . map readNVRA . filter notDebugPkg . filter (".rpm" `isExtensionOf`) . map fst <$>
-- FIXME get stats to show size
Koji.listTaskOutput huburl taskid' False True False
taskRPMURL :: Int -> String -> String
taskRPMURL taskid' rpm =
let lastFew =
let few = dropWhile (== '0') $ takeEnd 4 (show taskid') in
if null few then "0" else few
in dropSuffix "packages" pkgsurl +/+ "work/tasks/" ++ lastFew +/+ show taskid' +/+ rpm
data Existence = NotInstalled | NVRInstalled | NVRChanged
deriving (Eq, Ord, Show)
decideRpms :: Yes -> Bool -> Bool -> Mode -> String -> [NVRA]
-> IO [(Existence,NVRA)]
decideRpms yes listmode noreinstall mode prefix nvras = do
classified <- mapM installExists (filter isBinaryRpm nvras)
if listmode
then mapM_ printInstalled classified >> return []
else
case mode of
All -> do
mapM_ printInstalled classified
ok <- prompt yes "install above"
return $ if ok then classified else []
Ask -> mapMaybeM (rpmPrompt yes) classified
PkgsReq [] [] ->
if all ((== NotInstalled) . fst) classified && yes /= Yes
then decideRpms yes listmode noreinstall Ask prefix nvras
else do
let install = filter ((/= NotInstalled) . fst) classified
if yes == Yes
then return install
else do
mapM_ printInstalled install
ok <- prompt yes "install above"
return $ if ok then install else []
PkgsReq subpkgs exclpkgs -> do
let install = selectRPMs False prefix (subpkgs,exclpkgs) classified
mapM_ printInstalled install
ok <- prompt yes "install above"
return $ if ok then install else []
where
installExists :: NVRA -> IO (Existence, NVRA)
installExists nvra = do
minstalled <- cmdMaybe "rpm" ["-q", rpmName nvra]
return
(case minstalled of
Nothing -> NotInstalled
Just installed ->
if installed == showNVRA nvra then NVRInstalled else NVRChanged,
nvra)
renderInstalled :: (Existence, NVRA) -> String
renderInstalled (exist, nvra) = showNVRA nvra ++ " (" ++ show exist ++ ")"
printInstalled :: (Existence, NVRA) -> IO ()
printInstalled = putStrLn . renderInstalled
selectRPMs :: Bool -> String -> ([String],[String]) -> [(Existence,NVRA)]
-> [(Existence,NVRA)]
selectRPMs recurse prefix (subpkgs,[]) rpms =
sort . mconcat $
flip map subpkgs $ \ pkgpat ->
case filter (match (compile pkgpat) . rpmName . snd) rpms of
[] -> if head pkgpat /= '*' && not recurse
then selectRPMs True prefix ([prefix ++ '-' : pkgpat],[]) rpms
else error' $ "no subpackage match for " ++ pkgpat
result -> result
selectRPMs _ prefix ([], subpkgs) rpms =
-- FIXME somehow determine unused excludes
foldl' (exclude subpkgs) [] rpms
where
rpmnames = map (rpmName . snd) rpms
exclude :: [String] -> [(Existence,NVRA)] -> (Existence,NVRA)
-> [(Existence,NVRA)]
exclude [] acc rpm = acc ++ [rpm]
exclude (pat:pats) acc rpm =
if checkMatch (rpmName (snd rpm))
then acc
else exclude pats acc rpm
where
checkMatch :: String -> Bool
checkMatch rpmname =
let comppat = compile pat
in if isLiteral comppat
then pat == rpmname ||
pat `notElem` rpmnames &&
(prefix ++ '-' : pat) == rpmname
else match comppat rpmname
selectRPMs recurse prefix (subpkgs,exclpkgs) rpms =
let needed = selectRPMs recurse prefix (subpkgs,[]) rpms
excluded = selectRPMs recurse prefix ([], exclpkgs) rpms
in nub . sort $ needed ++ excluded
prompt :: Yes -> String -> IO Bool
prompt yes str = do
if yes == Yes
then return True
else do
putStr $ str ++ " [y/n]: "
c <- getChar
unless (c == '\n') $ putStrLn ""
case toLower c of
'y' -> return True
'n' -> return False
_ -> prompt yes str
rpmPrompt :: Yes -> (Existence,NVRA) -> IO (Maybe (Existence,NVRA))
rpmPrompt yes (exist,nvra) = do
ok <- prompt yes $ renderInstalled (exist,nvra)
return $
if ok
then Just (exist,nvra)
else Nothing
kojiBuildOSBuilds :: Bool -> String -> Bool -> Bool -> String -> Request
-> String -> IO [String]
kojiBuildOSBuilds debug hub listmode latest disttag request pkgpat = do
when debug $ putStrLn pkgpat
let (pkg,full) = packageOfPattern request pkgpat
oldkoji = "rpmfusion" `isInfixOf` hub
when debug $ print (pkg,full)
when debug $ putStrLn pkg
when (latest && request == ReqNVR) $
error' "cannot use --latest with --nvr"
when (latest && not listmode) $
putStrLn "--latest is implied when not using --list"
when (oldkoji && ("*" `isInfixOf` pkgpat || request /= ReqName)) $
error' "cannot use pattern with this kojihub"
mpkgid <- Koji.getPackageID hub pkg
case mpkgid of
Nothing -> error' $ "package not found: " ++ pkg
Just pkgid -> do
-- strictly should getAPIVersion
let opts = (if oldkoji
then id
else (("pattern", ValueString (if full then pkgpat else dropSuffix "*" pkgpat ++ "*" ++ disttag ++ "*")) :))
[("packageID", ValueInt pkgid),
("state", ValueInt (fromEnum BuildComplete)),
commonBuildQueryOptions
(if listmode && not latest || oldkoji then 20 else 1)]
when debug $ print opts
nvrs <- mapMaybe (lookupStruct "nvr") <$> Koji.listBuilds hub opts
if null nvrs
then error' $ "no builds found for " ++ disttag
else
return $
if oldkoji
then case filter (disttag `isInfixOf`) nvrs of
[] -> error' $ "no builds found for " ++ disttag
[res] -> [res]
rs@(r:_) ->
if listmode then rs else [r]
else nvrs
packageOfPattern :: Request -> String -> (String, Bool)
packageOfPattern request pat =
case request of
ReqName -> (dropSuffix "-" $ takeWhile (/= '*') pat, False)
ReqNV ->
case readNV pat of
NV n _ -> (n, False)
ReqNVR ->
case readNVR pat of
NVR n _ -> (n, True)
kojiGetBuildRPMs :: String -> NVR -> IO [String]
kojiGetBuildRPMs huburl nvr = do
mbid <- kojiGetBuildID huburl (showNVR nvr)
case mbid of
Nothing -> error $ "Build id not found for " ++ showNVR nvr
Just (BuildId bid) -> do
rpms <- Koji.listBuildRPMs huburl bid
sysarch <- cmd "rpm" ["--eval", "%{_arch}"]
return $ map getNVRA $ filter (forArch sysarch) rpms
where
forArch :: String -> Struct -> Bool
forArch sysarch st =
case lookupStruct "arch" st of
Just arch -> arch `elem` [sysarch, "noarch"]
Nothing -> error $ "No arch found for rpm for: " ++ showNVR nvr
getNVRA :: Struct -> String
getNVRA st =
case lookupStruct "nvr" st of
Nothing -> error' "NVR not found"
Just pnvr ->
case lookupStruct "arch" st of
Nothing -> error "arch not found"
Just arch ->
pnvr <.> arch
setNoBuffering :: IO ()
setNoBuffering = do
hSetBuffering stdin NoBuffering
hSetBuffering stdout NoBuffering
installRPMs :: Bool -> Bool -> Yes -> [(Existence,NVRA)] -> IO ()
installRPMs _ _ _ [] = return ()
installRPMs dryrun noreinstall yes classified = do
forM_ (groupSort classified) $ \(cl,pkgs) ->
unless (null pkgs) $
let mdnfcmd =
case cl of
NVRInstalled -> if noreinstall then Nothing else Just "reinstall"
_ -> Just "localinstall"
in whenJust mdnfcmd $ \dnfcmd ->
if dryrun
then mapM_ putStrLn $ ("would " ++ dnfcmd ++ ":") : map showRpmFile pkgs
else sudo_ "dnf" $ dnfcmd : map showRpmFile pkgs ++ ["--assumeyes" | yes == Yes]
showRpmFile :: NVRA -> FilePath
showRpmFile nvra = showNVRA nvra <.> "rpm"
downloadRpms :: Bool -> (String -> String) -> [(Existence,NVRA)] -> IO ()
downloadRpms debug urlOf rpms = do
urls <- fmap catMaybes <$>
forM (map snd rpms) $ \nvra -> do
let rpm = showRpmFile nvra
exists <- doesFileExist rpm
let url = urlOf rpm
notfile <-
if exists
then do
old <- outOfDate rpm url
when old $ removeFile rpm
return old
else return True
when notfile $ putStrLn $ if debug then url else rpm
return $ if notfile then Just url else Nothing
unless (null urls) $ do
mapM_ putStrLn urls
putStrLn "downloading..."
cmd_ "curl" $ ["--remote-time", "--fail", "-C-", "--show-error", "--remote-name-all", "--progress-bar"] ++ urls
where
outOfDate :: String -> String -> IO Bool
outOfDate file url = do
mremotetime <- httpLastModified' url
case mremotetime of
Just remotetime -> do
localtime <- getModificationTime file
if localtime < remotetime
then return True
else sizeOk file url
Nothing -> sizeOk file url
sizeOk :: String -> String -> IO Bool
sizeOk file url = do
remotesize <- httpFileSize' url
localsize <- getFileSize file
return $ remotesize /= Just localsize
-- showTask :: Struct -> Maybe String
-- showTask struct = do
-- state <- getTaskState struct
-- request <- lookupStruct "request" struct
-- method <- lookupStruct "method" struct
-- let mparent = lookupStruct "parent" struct :: Maybe Int
-- showreq = takeWhileEnd (/= '/') . unwords . mapMaybe getString . take 3
-- return $ showreq request +-+ method +-+ (if state == TaskClosed then "" else show state) +-+ maybe "" (\p -> "(" ++ show p ++ ")") mparent
-- showChildTask :: Struct -> Maybe String
-- showChildTask struct = do
-- arch <- lookupStruct "arch" struct
-- state <- getTaskState struct
-- method <- lookupStruct "method" struct
-- taskid <- lookupStruct "id" struct
-- return $ arch ++ replicate (8 - length arch) ' ' +-+ show (taskid :: Int) +-+ method +-+ show state
isBinaryRpm :: NVRA -> Bool
isBinaryRpm = (/= "src") . rpmArch
#if !MIN_VERSION_filepath(1,4,2)
isExtensionOf :: String -> FilePath -> Bool
isExtensionOf ext@('.':_) = isSuffixOf ext . takeExtensions
isExtensionOf ext = isSuffixOf ('.':ext) . takeExtensions
#endif