cabal-rpm-0.9: src/Commands/Spec.hs
{-# LANGUAGE CPP #-}
-- |
-- Module : Commands.Spec
-- Copyright : (C) 2007-2008 Bryan O'Sullivan
-- (C) 2012-2014 Jens Petersen
--
-- Maintainer : Jens Petersen <petersen@fedoraproject.org>
-- Stability : alpha
-- Portability : portable
--
-- Explanation: Generates an RPM spec file from a .cabal file.
-- 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 Commands.Spec (
createSpecFile, createSpecFile_
) where
import Dependencies (packageDependencies, showDep, testsuiteDependencies)
import PackageUtils (getPkgName, isScmDir, notInstalled, PackageData (..),
packageName, packageVersion)
import Setup (RpmFlags (..))
import SysCmd ((+-+))
import Control.Monad (filterM, unless, void, when)
import Data.Char (toLower, toUpper)
import Data.List (groupBy, isPrefixOf, isSuffixOf, sort, (\\))
import Data.Maybe (fromMaybe, maybeToList)
import Data.Time.Clock (UTCTime, getCurrentTime)
import Data.Time.Format (formatTime)
import Data.Version (showVersion)
import Distribution.License (License (..))
import Distribution.Simple.Utils (notice, warn)
import Distribution.PackageDescription (PackageDescription (..), BuildInfo (..),
Executable (..), Library (..),
exeName, hasExes, hasLibs, withExe)
--import Distribution.Version (VersionRange, foldVersionRange')
import System.Directory (doesFileExist, getDirectoryContents)
import System.IO (IOMode (..), hClose, hPutStrLn, openFile)
import System.Locale (defaultTimeLocale)
import System.FilePath (dropFileName, takeDirectory, (</>))
import qualified Paths_cabal_rpm (version)
defaultRelease :: FilePath -> UTCTime -> IO String
defaultRelease cabalPath now = do
let pkgDir = takeDirectory cabalPath
scmRepo <- isScmDir pkgDir
return $ if scmRepo
then formatTime defaultTimeLocale "0.%Y%m%d" now
else "1"
rstrip :: (Char -> Bool) -> String -> String
rstrip p = reverse . dropWhile p . reverse
-- packageName :: PackageIdentifier -> String
-- packageName pkg = name
-- where PackageName name = pkgName pkg
-- packageVersion :: PackageIdentifier -> String
-- packageVersion pkg = (showVersion . pkgVersion) pkg
createSpecFile :: PackageData -> RpmFlags ->
Maybe FilePath -> IO FilePath
createSpecFile pkgdata flags mdest = do
let mspec = specFilename pkgdata
cabalPath = cabalFilename pkgdata
pkgDesc = packageDesc pkgdata
pkg = package pkgDesc
name = packageName pkg
verbose = rpmVerbosity flags
hasExec = hasExes pkgDesc
hasLib = hasLibs pkgDesc
now <- getCurrentTime
defRelease <- defaultRelease cabalPath now
(pkgname, binlib) <- getPkgName mspec pkgDesc (rpmBinary flags)
putStrLn pkgname
let pkg_name = if pkgname == name then "%{name}" else "%{pkg_name}"
basename | binlib = "%{pkg_name}"
| hasExecPkg = name
| otherwise = "ghc-%{pkg_name}"
version = packageVersion pkg
release = fromMaybe defRelease (rpmRelease flags)
specFile = fromMaybe "" mdest </> pkgname ++ ".spec"
hasExecPkg = binlib || (hasExec && not hasLib)
-- run commands before opening file to prevent empty file on error
-- maybe shell commands should be in a monad or something
(deps, tools, clibs, pkgcfgs, selfdep) <- packageDependencies pkgDesc name
let testsuiteDeps = testsuiteDependencies pkgDesc name
missTestDeps <- filterM notInstalled testsuiteDeps
specAlreadyExists <- doesFileExist specFile
let specFile' = specFile ++ if not (rpmForce flags) && specAlreadyExists then ".cblrpm" else ""
when specAlreadyExists $
notice verbose $ specFile +-+ "exists:" +-+ if rpmForce flags then "forcing overwrite" else "creating" +-+ specFile'
h <- openFile specFile' WriteMode
let putHdr hdr val = hPutStrLn h (hdr ++ ":" ++ padding hdr ++ val)
padding hdr = replicate (14 - length hdr) ' ' ++ " "
putNewline = hPutStrLn h ""
put = hPutStrLn h
putDef v s = put $ "%global" +-+ v +-+ s
ghcPkg = if binlib then "-n ghc-%{name}" else ""
ghcPkgDevel = if binlib then "-n ghc-%{name}-devel" else "devel"
distro <- detectDistro
if distro /= SUSE
then put "# https://fedoraproject.org/wiki/Packaging:Haskell"
else do
let year = formatTime defaultTimeLocale "%Y" now
put "#"
put $ "# spec file for package " ++ pkgname
put "#"
put $ "# Copyright (c) " ++ year ++ " SUSE LINUX Products GmbH, Nuernberg, Germany."
put "#"
put "# All modifications and additions to the file contributed by third parties"
put "# remain the property of their copyright owners, unless otherwise agreed"
put "# upon. The license for this file, and modifications and additions to the"
put "# file, is the same license as for the pristine package itself (unless the"
put "# license for the pristine package is not an Open Source License, in which"
put "# case the license is the MIT License). An \"Open Source License\" is a"
put "# license that conforms to the Open Source Definition (Version 1.9)"
put "# published by the Open Source Initiative."
putNewline
put "# Please submit bugfixes or comments via http://bugs.opensuse.org/"
put "#"
putNewline
-- Some packages conflate the synopsis and description fields. Ugh.
let syn = synopsis pkgDesc
when (null syn) $
warn verbose "this package has no synopsis."
let initialCapital (c:cs) = toUpper c:cs
initialCapital [] = []
let syn' = if null syn
then "Haskell" +-+ name +-+ "package"
else (unwords . lines . initialCapital) syn
let summary = rstrip (== '.') syn'
when (length ("Summary : " ++ syn') > 79) $
warn verbose "this package has a long synopsis."
let descr = description pkgDesc
when (null descr) $
warn verbose "this package has no description."
let descLines = (formatParagraphs . initialCapital . filterSymbols . finalPeriod) $ if null descr then syn' else descr
finalPeriod cs = if last cs == '.' then cs else cs ++ "."
filterSymbols (c:cs) =
if c `notElem` "@\\" then c: filterSymbols cs
else case c of
'@' -> '\'': filterSymbols cs
'\\' -> head cs: filterSymbols (tail cs)
_ -> c: filterSymbols cs
filterSymbols [] = []
when hasLib $ do
putDef "pkg_name" name
putNewline
unless (null testsuiteDeps) $ do
put $ "%bcond_" ++ (if null missTestDeps then "without" else "with") +-+ "tests"
putNewline
let eCsources = concatMap (cSources . buildInfo) $ executables pkgDesc
let lCsources = concatMap (cSources . libBuildInfo) $ maybeToList $ library pkgDesc
when (null $ eCsources ++ lCsources) $ do
put "# no useful debuginfo for Haskell packages without C sources"
putDef "debug_package" "%{nil}"
putNewline
putHdr "Name" (if binlib then "%{pkg_name}" else basename)
putHdr "Version" version
putHdr "Release" $ release ++ (if distro == SUSE then [] else "%{?dist}")
putHdr "Summary" summary
case distro of
SUSE -> putHdr "Group" (if binlib then "Development/Languages/Other"
else "System/Libraries")
RHEL5 -> putHdr "Group" (if binlib then "Development/Languages"
else "System Environment/Libraries")
_ -> return ()
putNewline
putHdr "License" $ (showLicense distro . license) pkgDesc
putHdr "Url" $ "http://hackage.haskell.org/package/" ++ pkg_name
putHdr "Source0" $ "http://hackage.haskell.org/package/" ++ pkg_name ++ "-%{version}/" ++ pkg_name ++ "-%{version}.tar.gz"
case distro of
Fedora -> return ()
_ -> putHdr "BuildRoot" "%{_tmppath}/%{name}-%{version}-build"
putNewline
putHdr "BuildRequires" "ghc-Cabal-devel"
putHdr "BuildRequires" "ghc-rpm-macros"
let isa = if distro == SUSE then "" else "%{?_isa}"
let alldeps = sort $ deps ++ tools ++ map (++ isa) clibs ++ pkgcfgs
let extraTestDeps = sort $ testsuiteDeps \\ deps
unless (null $ alldeps ++ extraTestDeps) $ do
put "# Begin cabal-rpm deps:"
mapM_ (putHdr "BuildRequires") alldeps
when (any (\ d -> d `elem` map showDep ["template-haskell", "hamlet"]) deps) $
putHdr "ExclusiveArch" "%{ghc_arches_with_ghci}"
unless (null extraTestDeps) $ do
put "%if %{with tests}"
mapM_ (putHdr "BuildRequires") extraTestDeps
put "%endif"
put "# End cabal-rpm deps"
putNewline
put "%description"
mapM_ put descLines
putNewline
let wrapGenDesc = wordwrap (79 - max 0 (length pkgname - length pkg_name))
when hasLib $ do
when binlib $ do
put $ "%package" +-+ ghcPkg
putHdr "Summary" $ "Haskell" +-+ pkg_name +-+ "library"
case distro of
Fedora -> return ()
SUSE -> putHdr "Group" "System/Libraries"
RHEL5 -> putHdr "Group" "System Environment/Libraries"
putNewline
put $ "%description" +-+ ghcPkg
put $ wrapGenDesc $ "This package provides the Haskell" +-+ pkg_name +-+ "shared library."
putNewline
put $ "%package" +-+ ghcPkgDevel
putHdr "Summary" $ "Haskell" +-+ pkg_name +-+ "library development files"
case distro of
Fedora -> return ()
RHEL5 -> putHdr "Group" "Development/Libraries"
SUSE -> putHdr "Group" "Development/Libraries/Other"
unless (distro == SUSE) $
putHdr "Provides" $ (if binlib then "ghc-%{name}" else "%{name}") ++ "-static = %{version}-%{release}"
putHdr "Requires" "ghc-compiler = %{ghc_version}"
putHdr "Requires(post)" "ghc-compiler = %{ghc_version}"
putHdr "Requires(postun)" "ghc-compiler = %{ghc_version}"
putHdr "Requires" $ (if binlib then "ghc-%{name}" else "%{name}") ++ isa +-+ "= %{version}-%{release}"
unless (null $ clibs ++ pkgcfgs) $ do
put "# Begin cabal-rpm deps:"
mapM_ (putHdr "Requires") $ sort $ map (++ isa) clibs ++ pkgcfgs
put "# End cabal-rpm deps"
putNewline
put $ "%description" +-+ ghcPkgDevel
put $ wrapGenDesc $ "This package provides the Haskell" +-+ pkg_name +-+ "library development files."
putNewline
put "%prep"
put $ "%setup -q" ++ (if pkgname /= name then " -n %{pkg_name}-%{version}" else "")
putNewline
putNewline
put "%build"
let pkgType = if hasLib then "lib" else "bin"
put $ "%ghc_" ++ pkgType ++ "_build"
putNewline
putNewline
put "%install"
put $ "%ghc_" ++ pkgType ++ "_install"
when selfdep $ do
putNewline
put "%ghc_fix_dynamic_rpath %{name}"
putNewline
putNewline
unless (null testsuiteDeps) $ do
put "%check"
put "%if %{with tests}"
put "%cabal test"
put "%endif"
putNewline
putNewline
when hasLib $ do
let putInstallScript = do
put "%ghc_pkg_recache"
putNewline
putNewline
put $ "%post" +-+ ghcPkgDevel
putInstallScript
put $ "%postun" +-+ ghcPkgDevel
putInstallScript
let licensefiles =
#if defined(MIN_VERSION_Cabal) && MIN_VERSION_Cabal(1,20,0)
licenseFiles pkgDesc
#else
if null (licenseFile pkgDesc) then [] else [licenseFile pkgDesc]
#endif
docs <- findDocs cabalPath licensefiles
when hasExecPkg $ do
put "%files"
when (distro /= Fedora) $ put "%defattr(-,root,root,-)"
-- Add the license file to the main package only if it wouldn't
-- otherwise be empty.
mapM_ (\ l -> put $ "%doc" +-+ l) licensefiles
unless (null docs) $
put $ "%doc" +-+ unwords docs
withExe pkgDesc $ \exe ->
let program = exeName exe in
put $ "%{_bindir}/" ++ (if program == name then "%{name}" else program)
unless (null (dataFiles pkgDesc)) $
put "%{_datadir}/%{name}-%{version}"
putNewline
putNewline
when hasLib $ do
let baseFiles = if binlib then "-f ghc-%{name}.files" else "-f %{name}.files"
develFiles = if binlib then "-f ghc-%{name}-devel.files" else "-f %{name}-devel.files"
put $ "%files" +-+ ghcPkg +-+ baseFiles
when (distro /= Fedora) $ put "%defattr(-,root,root,-)"
mapM_ (\ l -> put $ "%doc" +-+ l) licensefiles
-- be strict for now
-- unless (null (dataFiles pkgDesc) || binlib) $
-- put "%{_datadir}/%{pkg_name}-%{version}"
putNewline
putNewline
put $ "%files" +-+ ghcPkgDevel +-+ develFiles
when (distro /= Fedora) $ put "%defattr(-,root,root,-)"
unless (null docs) $
put $ "%doc" +-+ unwords docs
when (not binlib && hasExec) $
withExe pkgDesc $ \exe ->
let program = exeName exe in
put $ "%{_bindir}/" ++ (if program == name then "%{name}" else program)
putNewline
putNewline
put "%changelog"
unless (distro == SUSE) $ do
let date = formatTime defaultTimeLocale "%a %b %e %Y" now
put $ "*" +-+ date +-+ "Fedora Haskell SIG <haskell@lists.fedoraproject.org> - " ++ version ++ "-" ++ release
put $ "- spec file generated by cabal-rpm-" ++ showVersion Paths_cabal_rpm.version
hClose h
return specFile'
createSpecFile_ :: PackageData -> RpmFlags ->
Maybe FilePath -> IO ()
createSpecFile_ pkgFiles flags mdest =
void (createSpecFile pkgFiles flags mdest)
findDocs :: FilePath -> [FilePath] -> IO [FilePath]
findDocs cabalPath licensefiles = do
contents <- getDirectoryContents $ dropFileName cabalPath
let docs = filter likely contents
return $ if null licensefiles
then docs
else filter unlikely $ filter (`notElem` licensefiles) docs
where names = ["author", "copying", "doc", "example", "licence", "license",
"readme", "todo"]
likely name = let lowerName = map toLower name
in any (`isPrefixOf` lowerName) names
unlikely name = not $ any (`isSuffixOf` name) ["~"]
showLicense :: Distro -> License -> String
showLicense SUSE (GPL Nothing) = "GPL-1.0+"
showLicense _ (GPL Nothing) = "GPL+"
showLicense SUSE (GPL (Just ver)) = "GPL-" ++ showVersion ver ++ "+"
showLicense _ (GPL (Just ver)) = "GPLv" ++ showVersion ver ++ "+"
showLicense SUSE (LGPL Nothing) = "LGPL-2.0+"
showLicense _ (LGPL Nothing) = "LGPLv2+"
showLicense SUSE (LGPL (Just ver)) = "LGPL-" ++ [head $ showVersion ver] ++ "+"
showLicense _ (LGPL (Just ver)) = "LGPLv" ++ [head $ showVersion ver] ++ "+"
showLicense SUSE BSD3 = "BSD-3-Clause"
showLicense _ BSD3 = "BSD"
showLicense SUSE BSD4 = "BSD-4-Clause"
showLicense _ BSD4 = "BSD"
showLicense _ MIT = "MIT"
showLicense SUSE PublicDomain = "SUSE-Public-Domain"
showLicense _ PublicDomain = "Public Domain"
showLicense SUSE AllRightsReserved = "SUSE-NonFree"
showLicense _ AllRightsReserved = "Proprietary"
showLicense _ OtherLicense = "Unknown"
showLicense _ (UnknownLicense l) = "Unknown" +-+ l
#if defined(MIN_VERSION_Cabal) && MIN_VERSION_Cabal(1,16,0)
showLicense SUSE (Apache Nothing) = "Apache-2.0"
showLicense _ (Apache Nothing) = "ASL ?"
showLicense SUSE (Apache (Just ver)) = "Apache-" +-+ showVersion ver
showLicense _ (Apache (Just ver)) = "ASL" +-+ showVersion ver
#endif
#if defined(MIN_VERSION_Cabal) && MIN_VERSION_Cabal(1,18,0)
showLicense _ (AGPL Nothing) = "AGPLv?"
showLicense _ (AGPL (Just ver)) = "AGPLv" ++ showVersion ver
#endif
-- from http://stackoverflow.com/questions/930675/functional-paragraphs
-- using split would be: map unlines . (Data.List.Split.splitWhen null)
paragraphs :: [String] -> [String]
paragraphs = map (unlines . filter (not . null)) . groupBy (const $ not . null)
-- http://rosettacode.org/wiki/Word_wrap#Haskell
wordwrap :: Int -> String -> String
wordwrap maxlen = wrap_ 0 False . words
where
wrap_ _ _ [] = "\n"
wrap_ pos eos (w:ws)
-- at line start: put down the word no matter what
| pos == 0 = w ++ wrap_ (pos + lw) endp ws
| pos + lw + 1 > maxlen - 9 && eos = '\n':wrap_ 0 endp (w:ws)
| pos + lw + 1 > maxlen = '\n':wrap_ 0 endp (w:ws)
| otherwise = " " ++ w ++ wrap_ (pos + lw + 1) endp ws
where
lw = length w
endp = last w == '.'
formatParagraphs :: String -> [String]
formatParagraphs = map (wordwrap 79) . paragraphs . lines
data Distro = Fedora | RHEL5 | SUSE deriving (Eq)
-- for now assume Fedora if no /etc/SuSE-release
detectDistro :: IO Distro
detectDistro = do
suse <- doesFileExist "/etc/SuSE-release"
if suse then return SUSE
else do
-- RHEL5 does not have macros.dist
dist <- doesFileExist "/etc/rpm/macros.dist"
return $ if dist then Fedora else RHEL5