dnf-repo (empty) → 0.1
raw patch · 10 files changed
+557/−0 lines, 10 filesdep +basedep +directorydep +extra
Dependencies added: base, directory, extra, filepath, simple-cmd, simple-cmd-args
Files
- ChangeLog.md +6/−0
- LICENSE +30/−0
- README.md +73/−0
- data/copr.fedorainfracloud.orgCOLONOWNERCOLONREPO.repo +10/−0
- data/koji-REPO.repo +10/−0
- dnf-repo.cabal +60/−0
- src/ExpireRepos.hs +27/−0
- src/Main.hs +185/−0
- src/Sudo.hs +9/−0
- src/YumRepoFile.hs +147/−0
+ ChangeLog.md view
@@ -0,0 +1,6 @@+# dnf-repo releases++## 0.1 (2022-06-20)+- initial version with basic functionality: --add-copr, --add-koji,+ --disable repo, --enable repo, --enable/disable-{testing,modular}+ and --save & --expire
+ LICENSE view
@@ -0,0 +1,30 @@+Copyright (c) 2022, 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,73 @@+# dnf-repo++A wrapper of the dnf package manager for fine control of enabled/disabled yum repos (eg coprs).++DNF is not terribly fast at handling repos partly because it likes+to refresh the cached repodata frequently. So it can be advantageous+to disable smaller repos by default and only enable them as needed.++This tool can temporarily enable/disable repos selected by substring.+Repo states can also be saved or their cache individually expired.++There are options to enable/disable testing/modular repos,+and also to create a repo file for a Copr or Koji repo.++## Help++```shellsession+$ dnf-repo --version+0.1+$ dnf-repo --help+DNF wrapper repo tool++Usage: dnf-repo [--version] [-n|--dryrun] [-D|--debug] [-s|--save]+ [(-c|--add-copr COPR) | (-k|--add-koji REPO) |+ (-d|--disable REPOPAT) | (-e|--enable REPOPAT) |+ (-x|--expire REPOPAT) | (-E|--delete-repo REPOPAT)]+ [(-t|--enable-testing) | (-T|--disable-testing)]+ [(-m|--enable-modular) | (-M|--disable-modular)] [DNFARGS]+ see https://github.com/juhp/dnf-repo#readme++Available options:+ -h,--help Show this help text+ --version Show version+ -n,--dryrun Dry run+ -D,--debug Debug output+ -s,--save Save the repo enable/disable state+ -c,--add-copr COPR Create repo file for copr repo+ -k,--add-koji REPO Create repo file for koji repo+ -d,--disable REPOPAT Disable repos+ -e,--enable REPOPAT Enable repos+ -x,--expire REPOPAT Expire repo cache+ -E,--delete-repo REPOPAT Remove unwanted .repo file+ -t,--enable-testing Enable testing repos+ -T,--disable-testing Disable testing repos+ -m,--enable-modular Enable modular repos+ -M,--disable-modular Disable modular repos+```++## Usage+List repos:+```shellsession+$ dnf-repo+```++Update with --enable-testing:+```shellsession+$ sudo dnf-repo -t update+```++List disabled copr repos:+```shellsession+$ dnf-repo -e copr+```++Disable copr repos for update:+```shellsession+$ sudo dnf-repo -d copr update+```++Disable modular repos permanently:+```shellsession+$ sudo dnf-repo --disable-modular --save+```
+ data/copr.fedorainfracloud.orgCOLONOWNERCOLONREPO.repo view
@@ -0,0 +1,10 @@+[copr:copr.fedorainfracloud.org:@COPR_OWNER@:@COPR_REPO@]+name=Copr repo for @COPR_REPO@ owned by @COPR_OWNER@+baseurl=https://download.copr.fedorainfracloud.org/results/@COPR_OWNER@/@COPR_REPO@/fedora-$releasever-$basearch/+type=rpm-md+skip_if_unavailable=True+gpgcheck=1+gpgkey=https://download.copr.fedorainfracloud.org/results/@COPR_OWNER@/@COPR_REPO@/pubkey.gpg+repo_gpgcheck=0+enabled=0+enabled_metadata=1
+ data/koji-REPO.repo view
@@ -0,0 +1,10 @@+[koji-@REPO@]+name=Fedora - Koji @REPO@+failovermethod=priority+baseurl=https://kojipkgs.fedoraproject.org/repos/@REPO@/latest/x86_64/+enabled=0+metadata_expire=1h+repo_gpgcheck=0+type=rpm+gpgcheck=0+skip_if_unavailable=False
+ dnf-repo.cabal view
@@ -0,0 +1,60 @@+name: dnf-repo+version: 0.1+synopsis: DNF wrapper tool to control repos+description:+ A command-line wrapper of the dnf package manager to+ control the repos enabled for a dnf invocation.+license: BSD3+license-file: LICENSE+author: Jens Petersen <juhpetersen@gmail.com>+maintainer: Jens Petersen <juhpetersen@gmail.com>+copyright: 2022 Jens Petersen <juhpetersen@gmail.com>+category: Utility+homepage: https://github.com/juhp/dnf-repo+bug-reports: https://github.com/juhp/dnf-repo/issues+build-type: Simple+extra-doc-files: README.md+ ChangeLog.md+cabal-version: 2.0+tested-with: GHC == 8.6.5+ || == 8.8.4+ || == 8.10.7+ || == 9.0.2+ || == 9.2.3+data-dir: data+data-files: copr.fedorainfracloud.orgCOLONOWNERCOLONREPO.repo+ koji-REPO.repo++source-repository head+ type: git+ location: https://github.com/juhp/dnf-repo.git++executable dnf-repo+ main-is: Main.hs+ other-modules: Paths_dnf_repo+ ExpireRepos+ Sudo+ YumRepoFile+ autogen-modules: Paths_dnf_repo+ hs-source-dirs: src+ build-depends: base < 5,+ directory,+ extra,+ filepath,+ simple-cmd,+ simple-cmd-args+ default-language: Haskell2010+ ghc-options: -Wall+ 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
+ src/ExpireRepos.hs view
@@ -0,0 +1,27 @@+module ExpireRepos (expireRepos) where++import Data.List (nub)+import SimpleCmd (error')++import Sudo++expiredFile :: FilePath+expiredFile = "/var/cache/dnf/expired_repos.json"++expireRepos :: Bool -> [String] -> IO ()+expireRepos _ [] = error' "no repos to expire given"+expireRepos dryrun repos = do+ old <- read <$> readFile expiredFile :: IO [String]+ let expired = nub $ old ++ repos+ doSudo dryrun "sed" ["-i", "-e",+ "s/" ++ renderShow old ++ "/" ++ renderShow expired ++ "/",+ expiredFile]+ putStrLn $ "expired now: " ++ show expired+ where+ renderShow :: [String] -> String+ renderShow = render . show++ render :: String -> String+ render "" = ""+ render (c:cs) =+ (if c `elem` "[]" then ['\\',c] else [c]) ++ render cs
+ src/Main.hs view
@@ -0,0 +1,185 @@+{-# LANGUAGE CPP #-}++-- SPDX-License-Identifier: BSD-3-Clause++module Main (main) where++import Control.Monad.Extra+import Data.Bifunctor (bimap)+import Data.List.Extra+import Data.Maybe (mapMaybe)+import Data.Tuple.Extra (fst3,snd3)+import SimpleCmd+import SimpleCmdArgs+import System.Directory+import System.FilePath+import System.IO (hSetBuffering, stdout, BufferMode(NoBuffering))+import System.IO.Extra (withTempDir)+import System.Time.Extra (sleep)++import Paths_dnf_repo (getDataFileName, version)+import ExpireRepos (expireRepos)+import Sudo+import YumRepoFile++main :: IO ()+main = do+ simpleCmdArgs' (Just version)+ "DNF wrapper repo tool"+ "see https://github.com/juhp/dnf-repo#readme" $+ runMain+ <$> switchWith 'n' "dryrun" "Dry run"+ <*> switchWith 'D' "debug" "Debug output"+ <*> switchWith 's' "save" "Save the repo enable/disable state"+ <*> modeOpt+ <*> optional testingOpt+ <*> optional modularOpt+ <*> many (strArg "DNFARGS")+ where+ modeOpt =+ AddCopr . replace "/" ":" <$> strOptionWith 'c' "add-copr" "COPR" "Create repo file for copr repo" <|>+ AddKoji <$> strOptionWith 'k' "add-koji" "REPO" "Create repo file for koji repo" <|>+ DisableRepo <$> strOptionWith 'd' "disable" "REPOPAT" "Disable repos" <|>+ EnableRepo <$> strOptionWith 'e' "enable" "REPOPAT" "Enable repos" <|>+ ExpireRepo <$> strOptionWith 'x' "expire" "REPOPAT" "Expire repo cache" <|>+ DeleteRepo <$> strOptionWith 'E' "delete-repofile" "REPOPAT" "Remove unwanted .repo file" <|>+ pure Default++ testingOpt =+ flagWith' EnableTesting 't' "enable-testing" "Enable testing repos" <|>+ flagWith' DisableTesting 'T' "disable-testing" "Disable testing repos"++ modularOpt =+ flagWith' EnableModular 'm' "enable-modular" "Enable modular repos" <|>+ flagWith' DisableModular 'M' "disable-modular" "Disable modular repos"++coprRepoTemplate :: FilePath+coprRepoTemplate = "copr.fedorainfracloud.orgCOLONOWNERCOLONREPO.repo"++kojiRepoTemplate :: FilePath+kojiRepoTemplate = "koji-REPO.repo"++-- FIXME both enabling and disabled at the same time+-- FIXME --enable-all-coprs (for updating etc)+-- FIXME confirm repos if many+-- FIXME --disable-non-cores (modular,testing,cisco, etc)+runMain :: Bool -> Bool -> Bool -> Mode -> Maybe Testing -> Maybe Modular+ -> [String] -> IO ()+runMain dryrun debug save mode mtesting mmodular args = do+ hSetBuffering stdout NoBuffering+ withCurrentDirectory "/etc/yum.repos.d" $ do+ case mode of+ AddCopr copr -> addCoprRepo copr+ AddKoji repo -> addKojiRepo repo+ _ -> return ()+ repofiles <- filesWithExtension "." "repo"+-- when debug $ print repofiles+ nameStates <- sort <$> mapM readRepo repofiles+ let repoActs = mapMaybe (selectRepo debug mode mtesting mmodular) nameStates+ unless (null repoActs) $ do+ mapM_ print repoActs+ putStrLn ""+ case mode of+ ExpireRepo _ -> do+ putStrLn ""+ expireRepos dryrun $ mapMaybe expiring repoActs+ DeleteRepo _ ->+ mapM_ deleteRepos $ mapMaybe deleting repoActs+ _ -> return ()+ when save $+ if null repoActs+ then putStrLn "no changes to save\n"+ else do+ prompt_ "Press Enter to save repo enabled state"+ doSudo dryrun "dnf" $+ "config-manager" :+ concatMap saveRepo repoActs+ if null args+ then do+ listRepos $ map (updateState repoActs) nameStates+ else do+ sleep 1+ putStrLn ""+ let repoargs = concatMap changeRepo repoActs+ in doSudo dryrun "dnf" $ repoargs ++ args+ where+ -- FIXME pull non-fedora copr repo file+ -- FIXME delete created copr repo file if repo doesn't exist+ addCoprRepo :: String -> IO ()+ addCoprRepo repo = do+ case stripInfix ":" repo of+ Nothing -> error' $ "invalid copr: " ++ repo+ Just (copr_owner,copr_repo) -> do+ template <- getDataFileName coprRepoTemplate+ repodef <- cmd "sed" ["-e", "s/@COPR_OWNER@/" ++ copr_owner ++ "/g", "-e", "s/@COPR_REPO@/" ++ copr_repo ++ "/g", template]+ let repofile = ("_copr:" ++) $+ replace "COLON" ":" $+ replace "OWNER" copr_owner $+ replace "REPO" copr_repo coprRepoTemplate+ exists <- doesFileExist repofile+ if exists+ then error' $ "repo already defined: " ++ repofile+ else putStrLn $ "Setting up copr repo " ++ repo+ withTempDir $ \ tmpdir -> do+ let tmpfile = tmpdir </> repofile+ unless dryrun $ writeFile tmpfile repodef+ doSudo dryrun "cp" [tmpfile, repofile]++ addKojiRepo :: String -> IO ()+ addKojiRepo repo = do+ template <- getDataFileName kojiRepoTemplate+ repodef <- cmd "sed" ["-e", "s/@REPO@/" ++ repo ++ "/g", template]+ let repofile = replace "REPO" repo kojiRepoTemplate+ exists <- doesFileExist repofile+ if exists+ then error' $ "repo already defined: " ++ repofile+ else putStrLn $ "Setting up koji repo " ++ repo+ withTempDir $ \ tmpdir -> do+ let tmpfile = tmpdir </> repofile+ unless dryrun $ writeFile tmpfile repodef+ doSudo dryrun "cp" [tmpfile, repofile]++ listRepos :: [RepoState] -> IO ()+ listRepos repoStates = do+ let (on,off) =+ -- can't this be simplified?+ bimap (map fst3) (map fst3) $ partition snd3 repoStates+ putStrLn "Enabled:"+ mapM_ putStrLn on+ putStrLn ""+ putStrLn "Disabled:"+ mapM_ putStrLn off++ deleteRepos :: FilePath -> IO ()+ deleteRepos repofile = do+ mowned <- cmdMaybe "rpm" ["-qf", "/etc/yum.repos.d" </> repofile]+ case mowned of+ Just owner -> error' $ repofile +-+ "owned by" +-+ owner+ Nothing -> do+ ok <- yesno $ "Remove " ++ takeFileName repofile+ when ok $ doSudo dryrun "rm" [repofile]++#if !MIN_VERSION_simple_cmd(0,2,4)+filesWithExtension :: FilePath -> String -> IO [FilePath]+filesWithExtension dir ext =+ filter (ext `isExtensionOf`) <$> listDirectory dir+#endif++prompt :: String -> IO String+prompt desc = do+ putStr $ desc ++ ": "+ getLine++prompt_ :: String -> IO ()+prompt_ desc = do+ void $ prompt desc++yesno :: String -> IO Bool+yesno desc = do+ inp <- prompt $ desc ++ "? [y/n]"+ case lower inp of+ "y" -> return True+ "yes" -> return True+ "n" -> return False+ "no" -> return False+ _ -> yesno desc
+ src/Sudo.hs view
@@ -0,0 +1,9 @@+module Sudo (doSudo) where++import SimpleCmd (cmdN, sudo_)++-- FIXME make this silent (simple-cmd-0.2.7) unless debug+doSudo :: Bool -> String -> [String] -> IO ()+doSudo dryrun c args = do+ (if dryrun then cmdN else sudo_) c args+ putStrLn ""
+ src/YumRepoFile.hs view
@@ -0,0 +1,147 @@+{-# LANGUAGE OverloadedStrings #-}++module YumRepoFile (+ Mode(..),+ readRepo,+ RepoState,+ selectRepo,+ Modular(..),+ Testing(..),+ changeRepo,+ saveRepo,+ updateState,+ expiring,+ deleting+ )+where++import Data.List.Extra (isPrefixOf, isInfixOf, isSuffixOf, trim)+import SimpleCmd (error')++type RepoState = (String,Bool,FilePath)++data Mode = AddCopr String | AddKoji String+ | EnableRepo String | DisableRepo String+ | ExpireRepo String | DeleteRepo String | Default+ deriving Eq++data ChangeEnable = Disable String | Enable String | Expire String+ | Delete FilePath+ deriving (Eq,Ord,Show)++changeRepo :: ChangeEnable -> [String]+changeRepo (Disable r) = ["--disablerepo", r]+changeRepo (Enable r) = ["--enablerepo", r]+changeRepo _ = []++saveRepo :: ChangeEnable -> [String]+saveRepo (Disable r) = ["--disable", r]+saveRepo (Enable r) = ["--enable", r]+saveRepo _ = []++expiring :: ChangeEnable -> Maybe String+expiring (Expire r) = Just r+expiring _ = Nothing++deleting :: ChangeEnable -> Maybe String+deleting (Delete r) = Just r+deleting _ = Nothing++updateState :: [ChangeEnable] -> RepoState -> RepoState+updateState [] rs = rs+updateState (ce:ces) re@(repo,enabled,file) =+ case ce of+ Disable r | r == repo && enabled -> (repo,False,file)+ Enable r | r == repo && not enabled -> (repo,True,file)+ _ -> updateState ces re++data Modular = EnableModular | DisableModular+ deriving Eq++data Testing = EnableTesting | DisableTesting+ deriving Eq++selectRepo :: Bool -> Mode -> Maybe Testing -> Maybe Modular+ -> RepoState -> Maybe ChangeEnable+selectRepo _debug mode mtesting mmodular (name,enabled,file) =+ case mode of+ AddCopr repo -> if repo `isSuffixOf` name && not enabled+ then Just (Enable name)+ else selectOther+ AddKoji repo -> if repo `isSuffixOf` name && not enabled+ then Just (Enable name)+ else selectOther+ EnableRepo pat -> if pat `isInfixOf` name && not enabled+ then Just (Enable name)+ else selectOther+ DisableRepo pat -> if pat `isInfixOf` name && enabled+ then Just (Disable name)+ else selectOther+ ExpireRepo pat -> if pat `isInfixOf` name+ then Just (Expire name)+ else Nothing+ DeleteRepo pat -> if pat `isInfixOf` name+ then if enabled+ then error' $ "disable repo before deleting: " ++ name+ else Just (Delete file)+ else Nothing+ _ -> selectOther+ where+ selectOther :: Maybe ChangeEnable+ selectOther+ | "modular" `isSuffixOf` name =+ case mmodular of+ Nothing -> Nothing+ Just include ->+ case include of+ EnableModular | not enabled ->+ if "testing" `isInfixOf` name+ then if mtesting == Just EnableTesting+ then Just (Enable name)+ else Nothing+ else Just (Enable name)+ DisableModular | enabled -> Just (Disable name)+ _ -> Nothing+ | "testing" `isInfixOf` name =+ case mtesting of+ Nothing -> Nothing+ Just include ->+ case include of+ EnableTesting | not enabled -> Just (Enable name)+ DisableTesting | enabled -> Just (Disable name)+ _ -> Nothing+ | otherwise = Nothing++readRepo :: FilePath -> IO RepoState+readRepo file =+ parseRepo file . lines <$> readFile file++-- was called parseIni+parseRepo :: FilePath -> [String] -> RepoState+parseRepo file [] = error' $ "empty ini file: " ++ file+parseRepo file (l:ls) =+ case trim l of+ "" -> parseRepo file ls+ ('#':_) -> parseRepo file ls+ sec ->+ let section = secName sec+ enabled =+ case dropWhile (not . ("enabled=" `isPrefixOf`)) ls of+ [] -> error' $ "no enabled field for " ++ section+ (e:_) ->+ case trim e of+ "enabled=1" -> True+ "enabled=0" -> False+ _ -> error' $ "unknown enabled state " ++ e ++ " for " ++ section+ in (section,enabled,file)+ where+ secName :: String -> String+ secName sec =+ if ' ' `elem` sec+ then error' $ "section contains space: " ++ sec+ else case sec of+ ('[' : rest) ->+ if last rest == ']'+ then init rest+ else error' $ "bad section " ++ sec ++ " in " ++ file+ _ -> error' $ "section not found in " ++ file