-- | -- Module : Distribution.Package.Rpm -- Copyright : Bryan O'Sullivan 2007, 2008 -- -- Maintainer : Jens Petersen -- Stability : alpha -- Portability : portable -- -- Explanation: Support for building RPM packages. Can also generate -- an RPM spec file if you need a basic one to hand-customize. -- This software may be used and distributed according to the terms of -- the GNU General Public License, incorporated herein by reference. module Distribution.Package.Rpm ( createSpecFile -- , rpm -- , rpmBuild ) where --import Control.Exception (bracket) import Control.Monad (when, unless) import Data.Char (toLower) import Data.List (intercalate, isPrefixOf, isSuffixOf, nub) import Data.Maybe (fromMaybe) import Data.Time.Clock (UTCTime, getCurrentTime) import Data.Time.Format (formatTime) import Data.Version (showVersion) --import System.Cmd (system) import System.Directory (doesDirectoryExist, doesFileExist, getDirectoryContents) --import System.Exit (ExitCode(..)) import System.IO (IOMode(..), hClose, hPutStrLn, openFile) import System.Locale (defaultTimeLocale) --import System.Process (runInteractiveCommand, waitForProcess) import System.FilePath (dropFileName) --() import Distribution.Compiler (CompilerFlavor(..)) import Distribution.Simple.Compiler (Compiler(..)) import Distribution.System (Platform(..), buildOS, buildArch) import Distribution.License (License(..)) import Distribution.Package (Dependency(..), PackageIdentifier(..), PackageName(..)) --import Distribution.Simple.PreProcess (knownSuffixHandlers) import Distribution.Simple.Program (defaultProgramConfiguration) import Distribution.Simple.Configure (configCompiler) --import Distribution.Simple.LocalBuildInfo (LocalBuildInfo) --import Distribution.Simple.SrcDist (createArchive, prepareTree) import Distribution.Simple.Utils (die, warn) import Distribution.PackageDescription (-- BuildInfo(..), GenericPackageDescription(..), PackageDescription(..), exeName, hasExes, hasLibs, withExe, -- withLib ) import Distribution.PackageDescription.Configuration (finalizePackageDescription) import Distribution.PackageDescription.Parse (readPackageDescription) --import Distribution.Verbosity (Verbosity) import Distribution.Version (VersionRange, foldVersionRange') --import Distribution.Simple.Setup (configConfigurationsFlags, emptyConfigFlags) import Distribution.Package.Rpm.Setup (RpmFlags(..)) --import System.Posix.Files (setFileCreationMask) import qualified Paths_cabal_rpm (version) (+-+) :: String -> String -> String "" +-+ s = s s +-+ "" = s s +-+ t = s ++ " " ++ t simplePackageDescription :: GenericPackageDescription -> RpmFlags -> IO PackageDescription simplePackageDescription genPkgDesc flags = do (compiler, _) <- configCompiler (Just GHC) Nothing Nothing defaultProgramConfiguration (rpmVerbosity flags) case finalizePackageDescription (rpmConfigurationsFlags flags) (const True) (Platform buildArch buildOS) (compilerId compiler) [] genPkgDesc of Left e -> die $ "finalize failed:" +-+ show e Right (pd, _) -> return pd -- | Copy a file or directory (recursively, in the latter case) to the -- same name in the target directory. Arguments flipped from the -- conventional order. -- copyTo :: Verbosity -> FilePath -> FilePath -> IO () -- copyTo verbose dest src = do -- isFile <- doesFileExist src -- let destDir = dest src -- if isFile -- then copyFileVerbose verbose src destDir -- else copyDirectoryRecursiveVerbose verbose src destDir -- autoreconf :: Verbosity -> PackageDescription -> IO () -- autoreconf verbose pkgDesc = do -- ac <- doesFileExist "configure.ac" -- when ac $ do -- c <- doesFileExist "configure" -- when (not c) $ do -- setupMessage verbose "Running autoreconf" pkgDesc -- ret <- system "autoreconf" -- case ret of -- ExitSuccess -> return () -- ExitFailure n -> die ("autoreconf failed with status" +-+ show n) -- localBuildInfo :: PackageDescription -> RpmFlags -> IO LocalBuildInfo -- localBuildInfo pkgDesc flags = do -- mb_lbi <- maybeGetPersistBuildConfig -- case mb_lbi of -- Just lbi -> return lbi -- Nothing -> configure (Right pkgDesc, emptyHookedBuildInfo) -- ((emptyConfigFlags defaultProgramConfiguration) -- { configConfigurationsFlags = rpmConfigurationsFlags flags }) -- rpmBuild :: GenericPackageDescription -> RpmFlags -> IO () -- rpmBuild genPkgDesc flags = do -- tgtPfx <- canonicalizePath (rpmTopDir flags) -- (compiler, pkgDesc) <- simplePackageDescription genPkgDesc flags -- let verbose = rpmVerbosity flags -- tmpDir = tgtPfx "src" -- flip mapM_ ["BUILD", "RPMS", "SOURCES", "SPECS", "SRPMS"] $ \ subDir -> do -- createDirectoryIfMissing True (tgtPfx subDir) -- let specsDir = tgtPfx "SPECS" -- lbi <- localBuildInfo pkgDesc flags -- bracket (setFileCreationMask 0o022) setFileCreationMask $ \ _ -> do -- -- autoreconf verbose pkgDesc -- (specFile, extraDocs) <- createSpecFile pkgDesc flags compiler -- specsDir -- tree <- prepareTree pkgDesc verbose (Just lbi) False tmpDir -- knownSuffixHandlers 0 -- mapM_ (copyTo verbose tree) extraDocs -- createArchive pkgDesc verbose (Just lbi) tmpDir (tgtPfx "SOURCES") -- ret <- system ("rpmbuild -ba --define \"_topdir" +-+ tgtPfx ++ "\"" +-+ -- specFile) -- case ret of -- ExitSuccess -> return () -- ExitFailure n -> die ("rpmbuild failed with status" +-+ show n) defaultRelease :: UTCTime -> IO String defaultRelease now = do darcsRepo <- doesDirectoryExist "_darcs" return $ if darcsRepo then formatTime defaultTimeLocale "0.%Y%m%d" now else "1" rstrip :: (Char -> Bool) -> String -> String rstrip p = reverse . dropWhile p . reverse createSpecFile :: FilePath -- ^pkg src dir -> RpmFlags -- ^rpm flags -> IO FilePath createSpecFile cabalPath flags = do let verbose = rpmVerbosity flags genPkgDesc <- readPackageDescription verbose cabalPath pkgDesc <- simplePackageDescription genPkgDesc flags now <- getCurrentTime defRelease <- defaultRelease now let pkg = package pkgDesc PackageName packageName = pkgName pkg name = fromMaybe (if isExec then packageName else "ghc-" ++ packageName) (rpmName flags) pkg_name = if isExec then "%{name}" else "%{pkg_name}" version = fromMaybe ((showVersion . pkgVersion) pkg) (rpmVersion flags) release = fromMaybe defRelease (rpmRelease flags) specPath = name ++ ".spec" isExec = hasExes pkgDesc isLib = hasLibs pkgDesc specAlreadyExists <- doesFileExist specPath h <- openFile (specPath ++ if specAlreadyExists then ".cabal-rpm" else "") WriteMode let putHdr hdr val = hPutStrLn h (hdr ++ ":" ++ padding hdr ++ val) padding hdr = replicate (15 - length hdr) ' ' putHdr_ hdr val = unless (null val) $ putHdr hdr val putHdrD hdr val dfl = putHdr hdr (if null val then dfl else val) putNewline = hPutStrLn h "" put = hPutStrLn h putDef v s = put $ "%global" +-+ v +-+ s date = formatTime defaultTimeLocale "%a %b %e %Y" now put "# https://fedoraproject.org/wiki/PackagingDrafts/Haskell" putNewline -- Some packages conflate the synopsis and description fields. Ugh. let syn = synopsis pkgDesc (syn', synTooLong) <- case lines syn of (x:_) -> return (x, x /= syn) _ -> do warn verbose "This package has no synopsis." return ("Haskell" +-+ packageName +-+ "package", False) let common_summary = if synTooLong then syn' +-+ "[...]" else rstrip (== '.') syn' when synTooLong $ warn verbose "The synopsis for this package spans multiple lines." let common_description = intercalate "\\\n" $ lines $ if (null . description) pkgDesc then if synTooLong then syn else "This package does not have a description." else description pkgDesc when isLib $ do putDef "pkg_name" packageName putNewline putDef "common_summary" common_summary putNewline putDef "common_description" common_description putNewline putHdr "Name" (if isExec then (if isLib then "%{pkg_name}" else name) else "ghc-%{pkg_name}") putHdr "Version" version putHdr "Release" $ release ++ "%{?dist}" if isLib then putHdr "Summary" "%{common_summary}" else putHdrD "Summary" common_summary "This package has no summary" putNewline putHdr "License" $ (showLicense . license) pkgDesc putHdr_ "URL" $ "http://hackage.haskell.org/package/" ++ pkg_name putHdr "Source0" $ "http://hackage.haskell.org/packages/archive/" ++ pkg_name ++ "/%{version}/" ++ pkg_name ++ "-%{version}.tar.gz" putNewline putHdr "BuildRequires" "ghc-Cabal-devel" putHdr "BuildRequires" $ "ghc-rpm-macros" ++ (if isLib then " %{!?without_hscolour:hscolour}" else "") put "# Begin cabal-rpm deps:" let extDeps = map (nub . showDep) (buildDepends pkgDesc) mapM_ (putHdr "BuildRequires" . intercalate ", ") extDeps put "# End cabal-rpm deps" putNewline put "%description" put $ if isLib then "%{common_description}" else common_description putNewline putNewline put "%prep" put $ "%setup -q" ++ (if name /= packageName then " -n %{pkg_name}-%{version}" else "") putNewline putNewline put "%build" let pkgType = if isLib then "lib" else "bin" put $ "%ghc_" ++ pkgType ++ "_build" putNewline putNewline put "%install" put $ "%ghc_" ++ pkgType ++ "_install" putNewline putNewline when (isExec && isLib) $ do put "%ghc_package" putNewline put "%ghc_description" putNewline putNewline when isLib $ do put "%ghc_devel_package" putNewline put "%ghc_devel_description" putNewline putNewline put "%ghc_devel_post_postun" putNewline putNewline docs <- findDocs cabalPath pkgDesc when isExec $ do put "%files" -- Add the license file to the main package only if it wouldn't -- otherwise be empty. unless (null $ licenseFile pkgDesc) $ put $ "%doc" +-+ licenseFile pkgDesc unless (null docs) $ put $ "%doc" +-+ unwords docs withExe pkgDesc $ \exe -> let program = exeName exe in put $ "%{_bindir}/" ++ (if program == packageName then "%{name}" else program) unless (null (dataFiles pkgDesc) && isExec) $ put "%{_datadir}/%{name}-%{version}" putNewline putNewline when isLib $ do put $ "%ghc_files" +-+ licenseFile pkgDesc unless (null docs) $ put $ "%doc" +-+ unwords docs putNewline putNewline put "%changelog" put $ "*" +-+ date +-+ "Fedora Haskell SIG " put $ "- spec file generated by cabal-rpm-" ++ showVersion Paths_cabal_rpm.version hClose h return specPath findDocs :: FilePath -> PackageDescription -> IO [FilePath] findDocs cabalPath pkgDesc = do contents <- getDirectoryContents $ dropFileName cabalPath let docs = filter likely contents return $ if null lf then docs else filter unlikely $ filter (/= lf) docs where names = ["author", "copying", "doc", "example", "licence", "license", "readme", "todo"] likely name = let lowerName = map toLower name in any (`isPrefixOf` lowerName) names lf = licenseFile pkgDesc unlikely name = not $ any (`isSuffixOf` name) ["~"] showLicense :: License -> String showLicense (GPL Nothing) = "GPL+" showLicense (GPL (Just ver)) = "GPLv" ++ show ver ++ "+" showLicense (LGPL Nothing) = "LGPLv2+" showLicense (LGPL (Just ver)) = "LGPLv" ++ show ver ++ "+" showLicense BSD3 = "BSD" showLicense BSD4 = "BSD" showLicense MIT = "MIT" showLicense PublicDomain = "Public Domain" showLicense AllRightsReserved = "Proprietary" showLicense OtherLicense = "Unknown" showLicense (UnknownLicense l) = "Unknown" +-+ l -- | Generate a string expressing runtime dependencies, but only -- on package/version pairs not already "built into" a compiler -- distribution. -- showRuntimeReq :: Verbosity -> PackageDescription -> IO String -- showRuntimeReq verbose pkgDesc = do -- let externalDeps = (buildDepends pkgDesc) -- clauses <- mapM (showRpmReq verbose) externalDeps -- return $ (commaSep . concat) clauses -- | Represent a dependency in a form suitable for an RPM spec file. showDep :: Dependency -> [String] showDep (Dependency (PackageName pkg) range) = map (ghc_devel +-+) (renderVersion range) where renderVersion :: VersionRange -> [String] renderVersion = foldVersionRange' [""] -- any (\ v -> ["=" +-+ showVersion v]) (\ v -> [">" +-+ showVersion v]) (\ v -> ["<" +-+ showVersion v]) (\ v -> [">=" +-+ showVersion v]) (\ v -> ["<=" +-+ showVersion v]) (\ x y -> [">=" +-+ showVersion x , "<" +-+ showVersion y]) (\ _ _ -> [""]) -- rpm can't handle || (++) id ghc_devel = "ghc-" ++ pkg ++ "-devel" -- -- | Find the paths to all "extra" libraries specified in the package -- -- config. Prefer shared libraries, since that's what gcc prefers. -- findLibPaths :: BuildInfo -> IO [FilePath] -- findLibPaths buildInfo = mapM findLib (extraLibs buildInfo) -- where findLib :: String -> IO FilePath -- findLib lib = do -- so <- findLibPath ("lib" ++ lib ++ ".so") -- if isJust so -- then return (fromJust so) -- else findLibPath ("lib" ++ lib ++ ".a") >>= -- maybe (die $ "could not find library: lib" ++ lib) -- return -- findLibPath extraLib = do -- loc <- findInExtraLibs (extraLibDirs buildInfo) -- if isJust loc -- then return loc -- else findWithGcc extraLib -- where findInExtraLibs (d:ds) = do -- let path = d extraLib -- exists <- doesFileExist path -- if exists -- then return (Just path) -- else findInExtraLibs ds -- findInExtraLibs [] = return Nothing -- | Return the full path to a file (usually an object file) that gcc -- knows about. -- findWithGcc :: FilePath -> IO (Maybe FilePath) -- findWithGcc lib = do -- (i,o,e,p) <- runInteractiveCommand $ "gcc -print-file-name=" ++ lib -- loc <- hGetLine o -- mapM_ hClose [i,o,e] -- waitForProcess p -- return $ if loc == lib then Nothing else Just loc -- | Return the RPM that owns a particular file or directory. Die if -- not owned. -- findRpmOwner :: FilePath -> IO String -- findRpmOwner path = do -- (i,o,e,p) <- runInteractiveCommand (rpmQuery ++ path) -- pkg <- hGetLine o -- mapM_ hClose [i,o,e] -- ret <- waitForProcess p -- case ret of -- ExitSuccess -> return pkg -- _ -> die $ "not owned by any package:" +-+ path -- where rpmQuery = "rpm --queryformat='%{NAME}' -qf " -- | Find all RPMs on which the build of this package depends. Die if -- a dependency is not present, or not owned by an RPM. --findLibDeps :: BuildInfo -> IO [String] --findLibDeps buildInfo = findLibPaths buildInfo >>= mapM findRpmOwner