packages feed

cabal-rpm-0.6.2: src/Distribution/Package/Rpm.hs

-- |
-- Module      :  Distribution.Package.Rpm
-- Copyright   :  Bryan O'Sullivan 2007, 2008
--
-- Maintainer  :  Jens Petersen <petersen@fedoraproject.org>
-- 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 <haskell@lists.fedoraproject.org>"
    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" ++ showVersion ver ++ "+"
showLicense (LGPL Nothing) = "LGPLv2+"
showLicense (LGPL (Just ver)) = "LGPLv" ++ showVersion 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