-- | -- Module : PackageUtils -- Copyright : (C) 2013-2019 Jens Petersen -- -- Maintainer : Jens Petersen -- Stability : alpha -- Portability : portable -- -- Explanation: functions related to Cabal dependency generation. -- This program is free software: you can redistribute it and/or modify -- it under the terms of the GNU General Public License as published by -- the Free Software Foundation, either version 3 of the License, or -- (at your option) any later version. module PackageUtils ( bringTarball, cabal_, editSpecField, getRevisedCabal, getPkgName, getSpecField, latestPackage, nameVersion, PackageData (..), packageManager, packageName, packageVersion, patchSpec, prepare, prettyShow, readVersion, removePrefix, removeSuffix, repoquery, rpmbuild, rpmInstall, RpmStage (..), stripPkgDevel ) where import FileUtils (filesWithExtension, fileWithExtension, getDirectoryContents_, mktempdir, withTempDirectory) import Options (RpmFlags (..)) import SimpleCmd (cmd, cmd_, cmdIgnoreErr, cmdLines, grep_, sudo, (+-+)) import SimpleCmd.Git (isGitDir, grepGitConfig) import SysCmd (die, optionalProgram, requireProgram, rpmEval) import Stackage (latestStackage) #if (defined(MIN_VERSION_base) && MIN_VERSION_base(4,8,0)) #else import Control.Applicative ((<$>)) #endif import Control.Monad (filterM, unless, when) import Data.Char (isDigit, toLower) import Data.List (groupBy, isPrefixOf, isSuffixOf, nub, sort, stripPrefix) import Data.Maybe (isNothing, fromJust, fromMaybe) import Data.Time.Clock (diffUTCTime, getCurrentTime) import Data.Version ( #if (defined(MIN_VERSION_base) && MIN_VERSION_base(4,8,0)) Version, makeVersion, #else Version(..), #endif ) #if defined(MIN_VERSION_Cabal) && MIN_VERSION_Cabal(2,0,0) #if defined(MIN_VERSION_Cabal) && MIN_VERSION_Cabal(2,2,0) import Distribution.Pretty (prettyShow) #else import qualified Distribution.Version (showVersion, Version) #endif #else import qualified Data.Version (showVersion) #endif import Distribution.Compiler import Distribution.Package (PackageIdentifier (..), #if defined(MIN_VERSION_Cabal) && MIN_VERSION_Cabal(1,22,0) unPackageName #else PackageName (..) #endif ) import Distribution.PackageDescription (PackageDescription (..), hasExes, hasLibs #if defined(MIN_VERSION_Cabal) && MIN_VERSION_Cabal(2,2,0) , mkFlagAssignment #endif ) #if defined(MIN_VERSION_Cabal) && MIN_VERSION_Cabal(2,0,0) import Distribution.PackageDescription.Configuration (finalizePD) import Distribution.Types.ComponentRequestedSpec (defaultComponentRequestedSpec) #else import Distribution.PackageDescription.Configuration (finalizePackageDescription) #endif #if defined(MIN_VERSION_Cabal) && MIN_VERSION_Cabal(2,2,0) import Distribution.PackageDescription.Parsec (readGenericPackageDescription) #elif defined(MIN_VERSION_Cabal) && MIN_VERSION_Cabal(2,0,0) import Distribution.PackageDescription.Parse (readGenericPackageDescription) #else import Distribution.PackageDescription.Parse (readPackageDescription) #endif import Distribution.Simple.Compiler ( #if defined(MIN_VERSION_Cabal) && MIN_VERSION_Cabal(1,22,0) compilerInfo #else Compiler (..) #endif ) import Distribution.Simple.Configure ( #if defined(MIN_VERSION_Cabal) && MIN_VERSION_Cabal(1,18,0) configCompilerEx #else configCompiler #endif ) #if defined(MIN_VERSION_Cabal) && MIN_VERSION_Cabal(2,0,0) import Distribution.Simple.Program (defaultProgramDb) #else import Distribution.Simple.Program (defaultProgramConfiguration) #endif import Distribution.Simple.Utils ( #if defined(MIN_VERSION_Cabal) && MIN_VERSION_Cabal(1,20,0) tryFindPackageDesc #else findPackageDesc #endif ) import Distribution.System (Platform (..), buildArch, buildOS) import System.Directory (copyFile, createDirectoryIfMissing, doesDirectoryExist, doesFileExist, getCurrentDirectory, getDirectoryContents, getModificationTime, removeDirectoryRecursive, renameFile, setCurrentDirectory) import System.Environment (getEnv) import System.FilePath ((), (<.>), dropFileName, takeBaseName, takeExtensions, takeFileName) import System.IO (hIsTerminalDevice, stdout) import System.Posix.Files (accessTime, fileMode, getFileStatus, modificationTime, setFileMode) #if defined(MIN_VERSION_Cabal) && MIN_VERSION_Cabal(2,2,0) #else #if defined(MIN_VERSION_Cabal) && MIN_VERSION_Cabal(2,0,0) prettyShow :: Distribution.Version.Version -> String prettyShow = Distribution.Version.showVersion #else prettyShow :: Version -> String prettyShow = Data.Version.showVersion #endif #endif stripVersion :: String -> String stripVersion n | '-' `notElem` n = n stripVersion nv = if hasVer then reverse emaN else nv where (reV, '-':emaN) = break (== '-') $ reverse nv hasVer = all (\c -> isDigit c || c == '.') reV simplePackageDescription :: FilePath -> RpmFlags -> IO (PackageDescription, [FilePath], [FilePath]) simplePackageDescription cabalfile opts = do final <- finalPackageDescription cabalfile opts (docs, licensefiles) <- findDocsLicenses (dropFileName cabalfile) final return (final, docs, licensefiles) finalPackageDescription :: FilePath -> RpmFlags -> IO PackageDescription finalPackageDescription cabalfile opts = do let verbose = rpmVerbosity opts #if defined(MIN_VERSION_Cabal) && MIN_VERSION_Cabal(2,0,0) #else let readGenericPackageDescription = readPackageDescription defaultProgramDb = defaultProgramConfiguration #endif genPkgDesc <- readGenericPackageDescription verbose cabalfile compiler <- case rpmCompilerId opts of Just cid -> return #if defined(MIN_VERSION_Cabal) && MIN_VERSION_Cabal(1,22,0) (unknownCompilerInfo cid NoAbiTag) #else cid #endif Nothing -> do #if defined(MIN_VERSION_Cabal) && MIN_VERSION_Cabal(1,18,0) (compiler, _, _) <- configCompilerEx #else (compiler, _) <- configCompiler #endif (Just GHC) Nothing Nothing defaultProgramDb verbose #if defined(MIN_VERSION_Cabal) && MIN_VERSION_Cabal(1,22,0) return (compilerInfo compiler) #else return (compilerId compiler) #endif #if defined(MIN_VERSION_Cabal) && MIN_VERSION_Cabal(2,2,0) #else let mkFlagAssignment = id #endif #if defined(MIN_VERSION_Cabal) && MIN_VERSION_Cabal(2,0,0) let finalizePackageDescription flags = finalizePD flags defaultComponentRequestedSpec #endif let final = finalizePackageDescription (mkFlagAssignment $ rpmConfigurationsFlags opts) (const True) (Platform buildArch buildOS) compiler [] genPkgDesc case final of Left e -> die $ "finalize failed: " ++ show e Right res -> return $ fst res findDocsLicenses :: FilePath -> PackageDescription -> IO ([FilePath], [FilePath]) findDocsLicenses dir pkgDesc = do contents <- getDirectoryContents dir let docs = sort $ filter unlikely $ filter (likely docNames) contents let licenses = sort $ nub $ #if defined(MIN_VERSION_Cabal) && MIN_VERSION_Cabal(1,20,0) licenseFiles pkgDesc #else [licenseFile pkgDesc | licenseFile pkgDesc /= ""] #endif ++ filter (likely licenseNames) contents docfiles = if null licenses then docs else filter (`notElem` licenses) docs return (docfiles, licenses) where docNames = ["announce", "author", "bugs", "changelog", "changes", "contribut", "example", "news", "readme", "todo"] licenseNames = ["copying", "licence", "license"] likely names name = let lowerName = map toLower name in any (`isPrefixOf` lowerName) names unlikely name = not $ any (`isSuffixOf` name) ["~", ".cabal"] #if defined(MIN_VERSION_Cabal) && MIN_VERSION_Cabal(1,20,0) #else tryFindPackageDesc :: FilePath -> IO FilePath tryFindPackageDesc = findPackageDesc #endif cabalFromSpec :: FilePath -> Bool -> IO FilePath cabalFromSpec specFile revise = do -- no rpmspec command in RHEL 5 and 6 namever <- removePrefix "ghc-" . head <$> cmdLines "rpm" ["-q", "--qf", "%{name}-%{version}\n", "--specfile", specFile] bringTarball namever revise specFile dExists <- doesDirectoryExist namever if dExists then do specTime <- modificationTime <$> getFileStatus specFile dirTime <- accessTime <$> getFileStatus namever when (specTime > dirTime) $ rpmbuild Prep specFile else rpmbuild Prep specFile tryFindPackageDesc namever bringTarball :: FilePath -> Bool -> FilePath -> IO () bringTarball nv revise spec = do havespec <- doesFileExist spec sources <- if havespec then map sourceFieldFile <$> cmdLines "spectool" ["-S", spec] else return [tarfile] srcdir <- getSourceDir allExist <- and <$> mapM (doesFileExist . (srcdir )) sources unless allExist $ do pkggit <- grepGitConfig "\\(pkgs\\|src\\)." when pkggit $ do srcnv <- grep_ tarfile "sources" when srcnv $ cmd_ "fedpkg" ["sources"] when havespec $ createDirectoryIfMissing True srcdir mapM_ (copyTarball False srcdir) sources haveLocalCabal <- doesFileExist $ srcdir nv <.> "cabal" when (not haveLocalCabal && revise) $ getRevisedCabal nv allExist' <- and <$> mapM (doesFileExist . (srcdir )) sources unless allExist' $ cmd_ "spectool" ["-g", "-S", "-C", srcdir, spec] where tarfile = nv <.> "tar.gz" sourceFieldFile :: String -> FilePath sourceFieldFile field = if null field then -- should be impossible error "Empty source field!" else (takeFileName . last . words) field copyTarball :: Bool -> FilePath -> FilePath -> IO () copyTarball ranFetch dir file = when (takeExtensions file == ".tar.gz") $ do let dest = dir file already <- doesFileExist dest unless already $ do home <- getEnv "HOME" let cacheparent = home ".cabal" "packages" havecache <- doesDirectoryExist cacheparent unless havecache cabalUpdate remotes <- getDirectoryContents_ cacheparent let (n,v) = nameVersion nv tarpath = n v tarfile paths = map (\ repo -> cacheparent repo tarpath) remotes -- if more than one tarball, should maybe warn if they are different tarballs <- filterM doesFileExist paths if null tarballs then if ranFetch then error $ "No" +-+ tarfile +-+ "found" else do cabal_ "fetch" ["-v0", "--no-dependencies", nv] copyTarball True dir file else do createDirectoryIfMissing True dir copyFile (head tarballs) dest -- cabal-1.18 fetch creates tarballs with mode 0600 stat <- getFileStatus dest when (fileMode stat /= 0o100644) $ setFileMode dest 0o0644 getSourceDir :: IO FilePath getSourceDir = do git <- isGitDir "." if git then getCurrentDirectory else fromJust <$> rpmEval "%{_sourcedir}" getRevisedCabal :: String -> IO () getRevisedCabal nv = do let (n,_) = nameVersion nv file = n <.> "cabal" dir <- getSourceDir withTempDirectory $ \ _ -> do cmd_ "wget" ["--quiet", "https://hackage.haskell.org/package" nv file] revised <- grep_ "x-revision" file when revised $ do cmd_ "dos2unix" ["--keepdate", file] renameFile file $ dir nv <.> "cabal" nameVersion :: String -> (String, String) nameVersion nv = if '-' `notElem` nv then error $ "nameVersion: malformed NAME-VER string" +-+ nv else (reverse eman, reverse rev) where (rev, '-':eman) = break (== '-') $ reverse nv data RpmStage = Binary | Source | Prep | BuildDep deriving Eq rpmbuild :: RpmStage -> FilePath -> IO () rpmbuild mode spec = do let rpmCmd = case mode of Binary -> "a" Source -> "s" Prep -> "p" BuildDep -> "_" cwd <- getCurrentDirectory gitDir <- isGitDir "." let rpmdirs_override = [ "--define="++ mcr +-+ cwd | mcr <- ["_builddir", "_rpmdir", "_srcrpmdir", "_sourcedir"], gitDir] cmd_ "rpmbuild" $ ["-b" ++ rpmCmd] ++ ["--nodeps" | mode == Prep] ++ rpmdirs_override ++ [spec] removePrefix :: String -> String-> String removePrefix pref str = fromMaybe str (stripPrefix pref str) removeSuffix :: String -> String -> String removeSuffix suffix orig = fromMaybe orig $ stripSuffix suffix orig where stripSuffix sf str = reverse <$> stripPrefix (reverse sf) (reverse str) stripPkgDevel :: String -> String stripPkgDevel = removeSuffix "-devel" . removePrefix "ghc-" cabalUpdate :: IO () cabalUpdate = do home <- getEnv "HOME" let dir = home ".cabal/packages/hackage.haskell.org" done <- checkTimestamp $ dir "01-index.timestamp" unless done $ do done' <- checkTimestamp $ dir "00-index.cache" unless done' $ cmd_ "cabal" ["update"] where checkTimestamp tsfile = do haveFile <- doesFileExist tsfile if haveFile then do ts <- getModificationTime tsfile t <- getCurrentTime when (diffUTCTime t ts > 600) $ cmd_ "cabal" ["update"] return True else return False cabal :: String -> [String] -> IO [String] cabal c args = do cabalUpdate cmdLines "cabal" (c:args) cabal_ :: String -> [String] -> IO () cabal_ c args = do cabalUpdate cmd_ "cabal" (c:args) tryUnpack :: String -> Bool -> IO (FilePath, Maybe FilePath) tryUnpack pkgver revise = do isdir <- doesDirectoryExist pkgver if isdir then do pth <- tryFindPackageDesc pkgver return (pth, Nothing) else do cwd <- getCurrentDirectory tmpdir <- mktempdir setCurrentDirectory tmpdir cabal_ "unpack" $ ["-v0"] ++ ["--pristine" | not revise] ++ [pkgver] pth <- tryFindPackageDesc pkgver setCurrentDirectory cwd return (tmpdir pth, Just tmpdir) latestPackage :: String -> String -> IO String latestPackage "hackage" pkg = latestHackage pkg latestPackage str pkg = do stk <- latestStackage str pkg case stk of Just pv -> return pv Nothing -> latestHackage pkg latestHackage :: String -> IO String latestHackage pkg = do contains_pkg <- cabal "list" ["-v0", pkg] let top = dropWhile (/= "*" +-+ pkg) contains_pkg if null top then error $ pkg +-+ "hackage not found" else do let field = " Default available version: " let avails = map (removePrefix field) $ filter (isPrefixOf field) top if null avails then error $ pkg +-+ "latest available version not found" else do let res = pkg ++ "-" ++ head avails putStrLn $ res +-+ "in Hackage" return res #if defined(MIN_VERSION_Cabal) && MIN_VERSION_Cabal(1,22,0) #else unPackageName :: PackageName -> String unPackageName (PackageName n) = n #endif packageName :: PackageIdentifier -> String packageName = unPackageName . pkgName packageVersion :: PackageIdentifier -> String packageVersion = prettyShow . pkgVersion getPkgName :: Maybe FilePath -> PackageDescription -> Bool -> IO (String, Bool) getPkgName (Just spec) pkgDesc binary = do let name = packageName $ package pkgDesc pkgname = takeBaseName spec hasLib = hasLibs pkgDesc return $ if name == pkgname || binary then (name, hasLib) else (pkgname, False) getPkgName Nothing pkgDesc binary = do let name = packageName $ package pkgDesc hasExec = hasExes pkgDesc hasLib = hasLibs pkgDesc return $ if binary || hasExec && not hasLib then (name, hasLib) else ("ghc-" ++ name, False) checkForSpecFile :: Maybe String -> IO (Maybe FilePath) checkForSpecFile mpkg = do allSpecs <- allSpecfiles -- emacs makes ".#*.spec" tmp files let predicate = maybe ((/= '.') . head) (\ pkg -> (`elem` [pkg <.> "spec", "ghc-" ++ pkg <.> "spec"])) mpkg specs = filter predicate allSpecs when (specs /= allSpecs && isNothing mpkg) $ putStrLn "Warning: dir contains a hidden spec file" case specs of [one] -> return $ Just one [] -> return Nothing _ -> error "More than one spec file found!" allSpecfiles :: IO [FilePath] allSpecfiles = filesWithExtension "." ".spec" checkForCabalFile :: String -> IO (Maybe FilePath) checkForCabalFile pkgmver = do let pkg = stripVersion pkgmver cabalfile = pkg <.> "cabal" pkgcabal <- doesFileExist cabalfile if pkgcabal then return $ Just cabalfile else do exists <- doesDirectoryExist pkgmver if exists then fileWithExtension pkgmver ".cabal" else return Nothing -- findSpecFile :: PackageDescription -> RpmFlags -> IO (FilePath, Bool) -- findSpecFile pkgDesc flags = do -- pkgname <- findPkgName pkgDesc flags -- let specfile = pkgname <.> "spec" -- exists <- doesFileExist specfile -- return (specfile, exists) data PackageData = PackageData { specFilename :: Maybe FilePath , docFilenames :: [FilePath] , licenseFilenames :: [FilePath] , packageDesc :: PackageDescription } -- Nothing implies existing packaging in cwd -- Something implies either new packaging or some existing spec file in dir prepare :: RpmFlags -> Maybe String -> Bool -> IO PackageData prepare flags mpkgver revise = do let mpkg = stripVersion <$> mpkgver mspec <- checkForSpecFile mpkg case mspec of Just spec -> do cabalfile <- cabalFromSpec spec revise (pkgDesc, docs, licenses) <- simplePackageDescription cabalfile flags return $ PackageData mspec docs licenses pkgDesc Nothing -> case mpkgver of Nothing -> do cwd <- getCurrentDirectory prepare flags (Just $ takeFileName cwd) revise Just pkgmver -> do mcabal <- checkForCabalFile pkgmver case mcabal of Just cabalfile -> do (pkgDesc, docs, licenses) <- simplePackageDescription cabalfile flags return $ PackageData Nothing docs licenses pkgDesc Nothing -> do pkgver <- if stripVersion pkgmver == pkgmver then latestPackage (rpmStream flags) pkgmver else return pkgmver (cabalfile, mtmp) <- tryUnpack pkgver revise (pkgDesc, docs, licenses) <- simplePackageDescription cabalfile flags maybe (return ()) removeDirectoryRecursive mtmp return $ PackageData Nothing docs licenses pkgDesc patchSpec :: Maybe FilePath -> FilePath -> FilePath -> IO () patchSpec mdir oldspec newspec = do diff <- cmdIgnoreErr "diff" ["-u2", "-I", "- spec file generated by cabal-rpm", "-I", "Fedora Haskell SIG ", oldspec, newspec] "" >>= cmdIgnoreErr "sed" ["-e", "s/.cblrpm//"] putStrLn diff out <- cmdIgnoreErr "patch" opts diff putStrLn out where opts = ["--fuzz=1"] ++ ["-p1" | '/' `elem` newspec] ++ maybe [] (\ d -> ["-d", d]) mdir packageManager :: IO String packageManager = do havednf <- optionalProgram "dnf" if havednf then return "dnf" else requireProgram "yum" >> return "yum" repoquery :: [String] -> String -> IO String repoquery args key = do havednf <- optionalProgram "dnf" let (prog, subcmd) = if havednf then ("dnf", ["repoquery", "-q"]) else ("repoquery", []) cmd prog (subcmd ++ args ++ [key]) rpmInstall :: [String] -> IO () rpmInstall [] = return () rpmInstall rpms = do pkginstaller <- packageManager let (inst, arg) = if pkginstaller == "dnf" then ("dnf", "install") else ("yum", "localinstall") tty <- hIsTerminalDevice stdout sudo inst $ ["-y" | not tty] ++ [arg] ++ rpms editSpecField :: String -> String -> FilePath -> IO () editSpecField field new spec = cmd_ "sed" ["-i", "-e s/^\\(" ++ field ++ ":\\s\\+\\).*/\\1" ++ new ++ "/", spec] getSpecField :: String -> FilePath -> IO String getSpecField field spec = cmd "sed" ["-n", "-e s/^" ++ field ++ ":\\s\\+\\(\\S\\+\\)/\\1/p", spec] readVersion :: String -> Version readVersion = makeVersion . parseVer where parseVer :: String -> [Int] parseVer cs = let vs = filter (/= ".") $ groupBy (\ c c' -> c /= '.' && c' /= '.') cs in map read vs #if (defined(MIN_VERSION_base) && MIN_VERSION_base(4,8,0)) #else -- from Data.Version makeVersion :: [Int] -> Version makeVersion b = Version b [] #endif