packages feed

koji-install (empty) → 0.2.0

raw patch · 7 files changed

+480/−0 lines, 7 filesdep +basedep +directorydep +extra

Dependencies added: base, directory, extra, filepath, koji, optparse-applicative, rpm-nvr, simple-cmd, simple-cmd-args, xdg-userdirs

Files

+ ChangeLog.md view
@@ -0,0 +1,15 @@+# Release history for koji-install++## 0.2.0 (2021-12-03)+- initial Hackage release+- `--hub` to select hub+- `--pkgsurl` to override kojifiles url+- override `--disttag`+- select specific build with `--nvr` or `--nv`+- `--debug` output++## 0.1.0 (2021-08-12)+- pre-release on copr+- initial options:+  --all/--ask/--base-only/--exclude-devel --dry-run+- only supports Fedora Koji
+ LICENSE view
@@ -0,0 +1,30 @@+Copyright (c) 2021, Jens Petersen++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++    * Redistributions of source code must retain the above copyright+      notice, this list of conditions and the following disclaimer.++    * Redistributions in binary form must reproduce the above+      copyright notice, this list of conditions and the following+      disclaimer in the documentation and/or other materials provided+      with the distribution.++    * Neither the name of Jens Petersen nor the names of other+      contributors may be used to endorse or promote products derived+      from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ README.md view
@@ -0,0 +1,62 @@+# koji-install++A CLI tool to download and install rpms from a Koji build.++Koji is a package buildsystem used by Fedora, Centos, and some other projects.++## Usage++By default it uses Fedora Koji.++$ koji-install podman++Will download the latest build for your Fedora version,+and try to install it.++You can specify a different hub using `--hub`.++### Selecting subpackages++By default only installed subpackages are and downloaded and updated,+but the following options change the behavior:++`--all`: install all subpackages++`--ask`: ask about each subpackage++`--base-only`: only install base package++`--exclude-devel`: skip devel subpackages++### Help+```shellsession+$ koji-install+Install latest build from Koji++Usage: koji-install [--version] [-n|--dry-run] [-D|--debug] [-H|--hub HUB]+                    [-P|--packages-url URL]+                    [(-a|--all) | (-A|--ask) | (-b|--base-only) |+                      (-D|--exclude-devel)] [-d|--disttag DISTTAG]+                    [(-R|--nvr) | (-V|--nv)] PACKAGE+  Download and install latest package build from Koji tag.+  HUB = fedora, stream, mbox, rpmfusion, URL++Available options:+  -h,--help                Show this help text+  --version                Show version+  -n,--dry-run             Don't actually download anything+  -D,--debug               More detailed output+  -H,--hub HUB             KojiHub shortname or url [default: fedora]+  -P,--packages-url URL    KojiFiles packages url [default: fedora]+  -a,--all                 all subpackages+  -A,--ask                 ask for each subpackge+  -b,--base-only           only base package+  -D,--exclude-devel       Skip devel packages+  -d,--disttag DISTTAG     Use a different disttag [default: .fc35]+  -R,--nvr                 Give an N-V-R instead of package name+  -V,--nv                  Give an N-V instead of package name+```++## Installation++`cabal-rpm builddep && cabal install` or `stack install`.
+ koji-install.cabal view
@@ -0,0 +1,71 @@+name:                koji-install+version:             0.2.0+synopsis:            CLI tool for installing rpms directly from Fedora Koji+description:+        koji-install can install the latest koji build of a package locally.+        By default it only downloads newer binaries of the subpackages+        already installed, but there are options to override that.+license:             BSD3+license-file:        LICENSE+author:              Jens Petersen <juhpetersen@gmail.com>+maintainer:          Jens Petersen <juhpetersen@gmail.com>+copyright:           2021  Jens Petersen <juhpetersen@gmail.com>+category:            Utility+homepage:            https://github.com/juhp/koji-install+bug-reports:         https://github.com/juhp/koji-install/issues+build-type:          Simple+extra-doc-files:     README.md+                     ChangeLog.md+cabal-version:       1.18+tested-with:         GHC== 8.4.4+                     || == 8.6.5+                     || == 8.8.4+                     || == 8.10.5+                     || == 9.0.1++source-repository head+  type:                git+  location:            https://github.com/juhp/koji-install.git++executable koji-install+  main-is:             Main.hs+  other-modules:       Paths_koji_install+                       DownloadDir+  hs-source-dirs:      src+  build-depends:       base < 5,+                       directory,+                       extra,+                       filepath,+                       koji >= 0.0.2,+                       optparse-applicative,+                       rpm-nvr,+                       simple-cmd,+                       simple-cmd-args,+                       xdg-userdirs+  default-language:    Haskell2010+  ghc-options:         -Wall -threaded+  if impl(ghc >= 8.0)+    ghc-options:       -Wcompat+                       -Widentities+                       -Wincomplete-uni-patterns+                       -Wincomplete-record-updates+                       -Wredundant-constraints+  if impl(ghc >= 8.2)+    ghc-options:       -fhide-source-paths+  if impl(ghc >= 8.4)+    ghc-options:       -Wmissing-export-lists+                       -Wpartial-fields+  if impl(ghc >= 8.10)+    ghc-options:       -Wunused-packages++test-suite test+    main-is: tests.hs+    type: exitcode-stdio-1.0+    hs-source-dirs: test++    default-language: Haskell2010++    ghc-options:   -Wall+    build-depends: base >= 4 && < 5+                 , simple-cmd+    build-tools:   koji-install
+ src/DownloadDir.hs view
@@ -0,0 +1,43 @@+module DownloadDir (+  setDownloadDir)+where++import Control.Monad+import SimpleCmd (error')+import System.Directory (createDirectoryIfMissing,+                         doesDirectoryExist, getHomeDirectory,+                         setCurrentDirectory)+import System.Environment.XDG.UserDir (getUserDir)+import System.FilePath++-- FIXME check writeable+setDownloadDir :: Bool -> String -> IO FilePath+setDownloadDir dryrun subdir = do+  home <- getHomeDirectory+  dlDir <- getUserDir "DOWNLOAD"+  dirExists <- doesDirectoryExist dlDir+  -- is this really necessary?+  unless (dryrun || dirExists) $+    when (home == dlDir) $+      error' "HOME directory does not exist!"+  let filesDir = dlDir </> subdir+  filesExists <- doesDirectoryExist filesDir+  dir <-+    if filesExists+    then setCWD filesDir+    else+    if dirExists+      then setCWD dlDir+      else do+      if dryrun+        then return dlDir+        else do+        createDirectoryIfMissing True dlDir+        setCWD dlDir+  let path = makeRelative home dir+  return $ if isRelative path then "~" </> path else path+  where+    setCWD :: FilePath -> IO FilePath+    setCWD dir = do+      setCurrentDirectory dir+      return dir
+ src/Main.hs view
@@ -0,0 +1,239 @@+-- SPDX-License-Identifier: BSD-3-Clause++module Main (main) where++import Control.Monad.Extra+import Data.Char+import Data.List.Extra+import Data.Maybe+import Data.RPM+import Distribution.Koji+import qualified Distribution.Koji.API as Koji+import Options.Applicative (fullDesc, header, progDescDoc)+import qualified Options.Applicative.Help.Pretty as P+import SimpleCmd+import SimpleCmdArgs+import System.Directory+import System.FilePath ((<.>))+import System.IO++import DownloadDir+import Paths_koji_install (version)++data InstallMode = Update | All | Ask | Base | NoDevel++data Request = ReqName | ReqNV | ReqNVR++-- FIXME --include devel, --exclude *+-- FIXME specify tag or task+-- FIXME support enterprise builds+-- FIXME --arch (including src)+-- FIXME --debuginfo+-- FIXME --delete after installing++main :: IO ()+main = do+  sysdisttag <- cmd "rpm" ["--eval", "%{dist}"]+  let pdoc = Just $ P.vcat+             [ P.text "Download and install latest package build from Koji tag.",+               P.text ("HUB = " <> intercalate ", " knownHubs)+             ]+  simpleCmdArgsWithMods (Just Paths_koji_install.version)+    (fullDesc <> header "Install latest build from Koji" <> progDescDoc pdoc) $+    program+    <$> switchWith 'n' "dry-run" "Don't actually download anything"+    <*> switchWith 'D' "debug" "More detailed output"+    <*> optional (strOptionWith 'H' "hub" "HUB"+                  "KojiHub shortname or url [default: fedora]")+    <*> optional (strOptionWith 'P' "packages-url" "URL"+                  "KojiFiles packages url [default: fedora]")+    <*> modeOpt+    <*> disttagOpt sysdisttag+    <*> (flagWith' ReqNVR 'R' "nvr" "Give an N-V-R instead of package name"+         <|> flagWith ReqName ReqNVR 'V' "nv" "Give an N-V instead of package name")+    <*> some (strArg "PACKAGE")+  where+    modeOpt :: Parser InstallMode+    modeOpt =+      flagWith' All 'a' "all" "all subpackages" <|>+      flagWith' Ask 'A' "ask" "ask for each subpackge" <|>+      flagWith' Base 'b' "base-only" "only base package" <|>+      flagWith' NoDevel 'D' "exclude-devel" "Skip devel packages" <|>+      pure Update++    disttagOpt :: String -> Parser String+    disttagOpt disttag = startingDot <$> strOptionalWith 'd' "disttag" "DISTTAG" ("Use a different disttag [default: " ++ disttag ++ "]") disttag++    startingDot cs =+      case cs of+        "" -> error' "empty disttag"+        (c:_) -> if c == '.' then cs else '.' : cs++knownHubs :: [String]+knownHubs = ["fedora","stream","mbox","rpmfusion", "URL"]++hubURL :: String -> String+hubURL "fedora" = fedoraKojiHub+-- later use centosKojiHub+hubURL "stream" = "https://kojihub.stream.centos.org/kojihub"+hubURL "mbox" = "https://koji.mbox.centos.org/kojihub"+hubURL "rpmfusion" = "https://koji.rpmfusion.org/kojihub"+hubURL "fusion" = "https://koji.rpmfusion.org/kojihub"+hubURL hub =+  if "http" `isPrefixOf` hub+  then hub+  else error' $ "unknown hub: try " ++ show knownHubs++defaultPkgsURL :: Maybe String -> String+defaultPkgsURL Nothing =+  "https://kojipkgs.fedoraproject.org/packages"+defaultPkgsURL (Just url) =+  case url of+    "https://kojihub.stream.centos.org/kojihub" ->+      "https://kojihub.stream.centos.org/kojifiles/packages"+    _ ->+      if "kojihub" `isSuffixOf` url+      then replace "kojihub" "kojifiles" url+      else error' "use --files-url to specify kojifiles url for this kojihub"++program :: Bool -> Bool -> Maybe String -> Maybe String -> InstallMode+        -> String -> Request -> [String] -> IO ()+program dryrun debug mhuburl mpkgsurl mode disttag request pkgs = do+  let huburl = maybe fedoraKojiHub hubURL mhuburl+      pkgsurl = fromMaybe (defaultPkgsURL mhuburl) mpkgsurl+  when debug $ do+    putStrLn huburl+    putStrLn pkgsurl+  -- FIXME use this?+  dlDir <- setDownloadDir dryrun "rpms"+  when debug $ putStrLn dlDir+  setNoBuffering+  mapM (kojiLatestRPMs huburl pkgsurl dlDir) pkgs+    >>= installRPMs dryrun . mconcat+  where+    kojiLatestRPMs :: String -> String -> String -> String -> IO [String]+    kojiLatestRPMs huburl pkgsurl dlDir pkg = do+      mnvr <- kojiLatestOSBuild huburl disttag request pkg+      case mnvr of+        Nothing -> error' $ "latest " ++ pkg ++ " not found"+        Just nvr -> do+          putStrLn $ nvr ++ "\n"+          allRpms <- map (<.> "rpm") . sort . filter (not . debugPkg) <$> kojiGetBuildRPMs huburl nvr+          dlRpms <- decideRpms mode allRpms+          unless (dryrun || null dlRpms) $ do+            mapM_ (downloadRpm pkgsurl (readNVR nvr)) dlRpms+            -- FIXME once we check file size - can skip if no downloads+            putStrLn $ "Packages downloaded to " ++ dlDir+          return dlRpms+      where+        decideRpms :: InstallMode -> [String] -> IO [String]+        decideRpms mode' allRpms =+          case mode' of+            All -> return allRpms+            Ask -> mapMaybeM rpmPrompt allRpms+            Base -> return $ pure $ minimumOn length $ filter (pkg `isPrefixOf`) allRpms+            Update -> do+              rpms <- filterM (isInstalled . rpmName . readNVRA) allRpms+              if null rpms+                then decideRpms Ask allRpms+                else return rpms+            NoDevel -> return $ filter (not . ("-devel-" `isInfixOf`)) allRpms++    rpmPrompt :: String -> IO (Maybe String)+    rpmPrompt rpm = do+      putStr $ rpm ++ " [y/n]: "+      c <- getChar+      putStrLn ""+      case toLower c of+        'y' -> return $ Just rpm+        'n' -> return Nothing+        _ -> rpmPrompt rpm++    isInstalled :: String -> IO Bool+    isInstalled rpm = cmdBool "rpm" ["--quiet", "-q", rpm]++    debugPkg :: String -> Bool+    debugPkg p = "-debuginfo-" `isInfixOf` p || "-debugsource-" `isInfixOf` p++kojiLatestOSBuild :: String -> String -> Request -> String -> IO (Maybe String)+kojiLatestOSBuild hub disttag request pkgpat = do+  let (pkg,full) = packageOfPattern pkgpat+  mpkgid <- Koji.getPackageID hub pkg+  case mpkgid of+    Nothing -> error $ "package not found: " ++ pkg+    Just pkgid -> do+      let opts = [("packageID", ValueInt pkgid),+                  ("state", ValueInt (fromEnum BuildComplete)),+                  ("queryOpts",ValueStruct [("limit",ValueInt 1),+                                            ("order",ValueString "-build_id")])]+      res <- Koji.listBuilds hub $+             ("pattern", ValueString (if full then pkgpat else dropSuffix "*" pkgpat ++ "*" ++ disttag)) : opts+      case res of+        [] -> return Nothing+        [bld] -> return $ lookupStruct "nvr" bld+        _ -> error $ "more than one latest build found for " ++ pkg+  where+    packageOfPattern :: String -> (String, Bool)+    packageOfPattern 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 -> String -> IO [String]+kojiGetBuildRPMs huburl nvr = do+  mbid <- kojiGetBuildID huburl nvr+  case mbid of+    Nothing -> error $ "Build id not found for " ++ nvr+    Just (BuildId bid) -> do+      rpms <- Koji.listBuildRPMs huburl bid+      return $ map getNVRA $ filter (forArch "x86_64") 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: " ++ 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 -> [FilePath] -> IO ()+installRPMs _ [] = return ()+installRPMs dryrun pkgs =+  unless dryrun $+  sudo_ "dnf" ("install" : map ("./" ++) pkgs)++-- FIXME check file size+downloadRpm :: String -> NVR -> String -> IO ()+downloadRpm pkgsurl (NVR n (VerRel v r)) rpm = do+  unlessM (doesFileExist rpm) $ do+    let arch = rpmArch (readNVRA rpm)+        url = pkgsurl +/+ n  +/+ v +/+ r +/+ arch +/+ rpm+    putStrLn $ "Downloading " ++ rpm+    cmd_ "curl" ["--fail", "--silent", "-C-", "--show-error", "--remote-name", url]++-- from next http-directory or http-query+infixr 5 +/++(+/+) :: String -> String -> String+"" +/+ s = s+s +/+ "" = s+s +/+ t | last s == '/' = init s +/+ t+        | head t == '/' = s +/+ tail t+s +/+ t = s ++ "/" ++ t
+ test/tests.hs view
@@ -0,0 +1,20 @@+import SimpleCmd+import System.IO++program :: [String] -> IO ()+program args =+  putStrLn "" >> cmdLog "koji-install" ("-n" : "-b" : args)++tests :: [[String]]+tests =+  [["podman"]+  ,["-H", "https://kojihub.stream.centos.org/kojihub", "-d", "el9", "bash"]+  ,["-H", "stream", "-d", "el9", "kernel"]+--  ,["-H", "rpmfusion", "ffmpeg"]+  ]++main :: IO ()+main = do+  hSetBuffering stdout NoBuffering+  mapM_ program tests+  putStrLn $ show (length tests) ++ " tests run"