packages feed

cabal-rpm-2.3.2: src/PackageUtils.hs

{-# LANGUAGE CPP #-}

-- |
-- Module      :  PackageUtils
-- Copyright   :  (C) 2013-2020  Jens Petersen
--
-- Maintainer  :  Jens Petersen <petersen@fedoraproject.org>
-- 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_,
  checkForSpecFile,
  dependencySortCabals,
  dropChangelog,
  editSpecField,
  getRevisedCabal,
  getSpecField,
  latestPackage,
  PackageData (..),
  packageManager,
  patchSpec,
  pkgSpecPkgData,
  prepare,
  repoquery,
  rpmbuild,
  rpmInstall,
  RpmStage (..),
  packageMacro,
  readGlobalMacro,
  builtExecs
  ) where

#if !MIN_VERSION_base(4,8,0)
import Control.Applicative ((<$>))
#endif
import Control.Monad    (filterM, forM_, unless, void, when)
import Data.List.Extra
import Data.Maybe
import Data.Ord (comparing, Down(Down))
import Data.Time.Clock (diffUTCTime, getCurrentTime)
import Distribution.PackageDescription (buildable, Executable(buildInfo),
                                        exeName, executables,
                                       )
import Distribution.Text (display, simpleParse)
#if MIN_VERSION_Cabal(2,0,0)
import Distribution.Types.UnqualComponentName (UnqualComponentName(),
                                               mkUnqualComponentName)
#endif
#if MIN_VERSION_Cabal(3,6,0)
import Distribution.Utils.Path (getSymbolicPath)
#endif
import Safe (headMay, tailSafe)
import SimpleCabal (finalPackageDescription, licenseFiles, mkPackageName,
                    PackageDescription, PackageIdentifier(..), PackageName,
                    showPkgId, tryFindPackageDesc)
import SimpleCmd (cmd, cmd_, cmdBool, cmdIgnoreErr, cmdLines,
                  cmdStderrToStdoutIn, error', grep, grep_,
                  removePrefix, sudo_, (+-+))
import SimpleCmd.Git (isGitDir, grepGitConfig)
import SimpleCmd.Rpm (rpmspec)
import System.Directory.Extra (copyFile, createDirectoryIfMissing,
                               doesDirectoryExist, doesFileExist,
                               getCurrentDirectory, getModificationTime,
                               listFilesRecursive, withCurrentDirectory)
import System.Environment (getEnv)
import System.FilePath
import System.IO (hIsTerminalDevice, stdout)
import System.Posix.Files (accessTime, fileMode, getFileStatus,
                           modificationTime, setFileMode)

import FileUtils (assertFileNonEmpty, filesWithExtension, fileWithExtension,
#if !MIN_VERSION_filepath(1,4,2)
                  isExtensionOf,
#endif
                  listDirectory', withTempDirectory)
import SysCmd (optionalProgram, requireProgram, rpmEval)
import Stackage (defaultLTS, latestStackage)
import Types


simplePackageDescription :: Flags -> FilePath -> Maybe FilePath
                         -> IO PackageData
simplePackageDescription flags cabalfile mspec = do
  final <- finalPackageDescription flags cabalfile
  (docs, licensefiles, manpages) <- findDocsLicenses (dropFileName cabalfile) final
  return $ PackageData mspec final docs licensefiles manpages

builtExecs :: PackageDescription ->
#if MIN_VERSION_Cabal(2,0,0)
              [UnqualComponentName]
#else
              [String]
#endif
builtExecs = sort . map exeName . filter isBuildable . executables
  where
    isBuildable :: Executable -> Bool
    isBuildable = buildable . buildInfo

#if !MIN_VERSION_Cabal(2,0,0)
mkUnqualComponentName :: String -> String
mkUnqualComponentName = id
#endif

-- FIXME only include (doc/man) files listed in the .cabal file
-- eg ChangeLog.md may exist but not dist packaged
-- (edge case for packaging from git repo)
findDocsLicenses :: FilePath -> PackageDescription
                 -> IO ([FilePath], [FilePath], [FilePath])
findDocsLicenses dir pkgDesc = do
  contents <- listDirectory' dir
  let docs = sort $ filter unlikely $ filter (likely docNames) contents
  let licenses = sort $ nub
                 (map getSymbolicPath (licenseFiles pkgDesc)
                  ++ filter (likely licenseNames) contents)
      docfiles = if null licenses then docs else filter (`notElem` licenses) docs
  let execs = builtExecs pkgDesc
  manpages <- filter (\f -> ".1" `isExtensionOf` f &&
                            mkUnqualComponentName (takeBaseName f) `elem` execs) <$>
              withCurrentDirectory dir (listFilesRecursive ".")
  return (docfiles, licenses, manpages)
  where
    docNames = ["announce", "author", "bugs", "changelog", "changes",
                 "contribut", "example", "news", "readme", "todo"]
    licenseNames = ["copying", "licence", "license"]
    likely :: [String] -> String -> Bool
    likely names name = any (`isPrefixOf` lower name) names
    unlikely name = not $ any (`isSuffixOf` name)
                    ["~", ".cabal", ".hs", ".hi", ".o"]

bringTarball :: PackageIdentifier -> Maybe FilePath -> IO ()
bringTarball pkgid mspec = do
  let tarfile = display pkgid <.> "tar.gz"
  havespec <- case mspec of
                Nothing -> return False
                Just spec -> doesFileExist spec
  sources <- if havespec
             then map sourceFieldFile <$> cmdLines "spectool" ["-S", fromJust mspec]
             else return [tarfile]
  srcdir <- getSourceDir
  createDirectoryIfMissing True srcdir
  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"]
    mapM_ (copyTarball False srcdir) sources
    haveLocalCabal <- doesFileExist $ srcdir </> display pkgid <.> "cabal"
    unless haveLocalCabal $
      void $ getRevisedCabal pkgid
    allExist' <- and <$> mapM (doesFileExist . (srcdir </>)) sources
    when (not allExist' && havespec) $
      cmd_ "spectool" ["-g", "-S", "-C", srcdir, fromJust mspec]
 where
  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 <- listDirectory' cacheparent
      let pkgid' =
            fromMaybe (error' $ "Parse failed for:" +-+ dropExtensions file) $
            simpleParse (dropExtensions file)
          tarfile = display pkgid' <.> "tar.gz"
          tarpath =
              display (pkgName pkgid') </> display (pkgVersion pkgid') </> tarfile
          paths = map (\ repo -> cacheparent </> repo </> tarpath) remotes
      -- if more than one tarball, should maybe warn if they are different
      tarballs <- filterM doesFileExist paths
      case tarballs of
        [] ->
          if ranFetch
          then error $ "no" +-+ tarfile +-+ "found"
          else do
            cabal_ "fetch" ["-v0", "--no-dependencies", display pkgid']
            copyTarball True dir file
        (tarball:_) -> do
          createDirectoryIfMissing True dir
          copyFile tarball 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}"

getBuildDir :: Maybe String -> IO FilePath
getBuildDir mnv = do
  builddir <- fromMaybe "" <$> rpmEval "%{_builddir}"
  rpmver <- dropPrefix "RPM version " <$> cmd "rpm" ["--version"]
  return $ builddir </>
    case mnv of
      Just nv ->
        if readVersion rpmver >= readVersion "4.19.91"
        then nv ++ "-build"
        else ""
      Nothing -> ""

getRevisedCabal :: PackageIdentifier -> IO (Maybe Int)
getRevisedCabal pkgid = do
  let file = display (pkgName pkgid) <.> "cabal"
  srcdir <- getSourceDir
  withTempDirectory $ do
    -- FIXME use cabal-file
    dl <- cmdBool "wget" ["--quiet", "https://hackage.haskell.org/package" </> display pkgid </> file]
    if not dl
      then return Nothing
      else do
      revised <- listToMaybe <$> grep "^x-revision:" file
      case fmap word1 revised of
        Nothing -> return Nothing
        Just (_,rev) -> do
          when (isJust revised) $ do
            createDirectoryIfMissing True srcdir
            -- renameFile can fail across fs devices
            copyFile file $ srcdir </> display pkgid <.> "cabal"
          return $ Just (read rev)

data RpmStage = Binary | Source | Prep
  deriving Eq

instance Show RpmStage where
  show Binary = "binary"
  show Source = "source"
  show Prep = "prep"

rpmbuild :: Bool -> RpmStage -> FilePath -> IO ()
rpmbuild quiet mode spec = do
  let rpmCmd = case mode of
        Binary -> 'a'
        Source -> 's'
        Prep -> 'p'
  cwd <- getCurrentDirectory
  gitDir <- isGitDir "."
  let rpmdirs_override =
        [ "--define="++ mcr +-+ cwd | gitDir, mcr <- ["_sourcedir"]]
  let args = ["-b" ++ singleton rpmCmd] ++ ["--nodeps" | mode == Prep] ++
             rpmdirs_override ++ [spec]
  if not quiet
    then cmd_ "rpmbuild" args
    else do
    putStr $ "rpmbuild" +-+ show mode ++ ": "
    -- may hang for build
    (ok, out) <- cmdStderrToStdoutIn "rpmbuild" args ""
    if ok
      then putStrLn "done"
      else error' $ "\n" ++ dropToPrefix "+ /usr/bin/chmod -Rf" out
  where
    dropToPrefix :: String -> String -> String
    dropToPrefix _ "" = ""
    dropToPrefix prefix cs =
      let ls = lines cs
          rest = dropWhile (not . (prefix `isPrefixOf`)) ls
      in unlines $
         case rest of
           [] -> tailSafe ls
           (_:rs) -> rs

#if !MIN_VERSION_base(4,15,0)
    singleton :: Char -> String
    singleton c = [c]
#endif

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 </> "01-index.cache"
    unless done' cabalUpdateCmd
  where
    checkTimestamp tsfile = do
      haveFile <- doesFileExist tsfile
      if haveFile then do
        ts <- getModificationTime tsfile
        t <- getCurrentTime
        -- less than 3 hours
        when (diffUTCTime t ts > 10000) cabalUpdateCmd
        return True
        else
        return False

    cabalUpdateCmd :: IO ()
    cabalUpdateCmd = do
      putStrLn "Running 'cabal update'"
      cmd_ "cabal" ["update", "-v0"]

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 :: Maybe String -> PackageIdentifier -> IO FilePath
tryUnpack mnv pkgid = do
  builddir <- getBuildDir mnv
  let dir = builddir </> display pkgid
  isdir <- doesDirectoryExist dir
  if isdir
    then do
    mcabal <- withCurrentDirectory dir $ checkForCabalFile (Just (pkgName pkgid))
    if isJust mcabal
      then tryFindPackageDesc dir
      else error $ "could not find" +-+ display (pkgName pkgid) <.> "cabal"
    else do
    createDirectoryIfMissing True builddir
    withCurrentDirectory builddir $ do
      cabal_ "unpack" ["-v0", display pkgid]
      tryFindPackageDesc dir

latestPackage :: Maybe Stream -> PackageName -> IO PackageIdentifier
latestPackage (Just Hackage) pkg = latestHackage pkg
latestPackage mstream pkg = do
  stk <- latestStackage mstream pkg
  case stk of
    Just pkgid -> return pkgid
    Nothing -> latestHackage pkg

-- use cabal-file when it parses preferred-versions
latestHackage :: PackageName -> IO PackageIdentifier
latestHackage pkgname = do
  let pkg = display pkgname
  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
    case avails of
      [] -> error $ pkg +-+ "latest available version not found"
      (avail:_) -> do
        let res = pkg ++ "-" ++ avail
        putStrLn $ res +-+ "in Hackage"
        return $ PackageIdentifier (mkPackageName pkg) $ readVersion avail

checkForSpecFile :: Maybe PackageName -> IO (Maybe FilePath)
checkForSpecFile mpkg = do
  allSpecs <- filesWithExtension "." ".spec"
  -- emacs makes ".#*.spec" tmp files
  let predicate = maybe ((/= Just '.') . headMay) ((\ pkg -> (`elem` [pkg <.> "spec", "ghc-" ++ pkg <.> "spec"])) . display) 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!"

checkForCabalFile :: Maybe PackageName -> IO (Maybe FilePath)
checkForCabalFile mpkg = do
  allCabals <- filesWithExtension "." ".cabal"
  let predicate = maybe (const True) ((\ pkg -> (== pkg <.> "cabal")) . display) mpkg
      cabals = filter predicate allCabals
  case cabals of
    [one] -> return $ Just one
    [] -> return Nothing
    _ -> error' "more than one cabal file found!"

checkForPkgCabalFile :: Maybe String -> PackageIdentifier
                     -> IO (Maybe FilePath)
checkForPkgCabalFile mnv pkgid = do
  let pkg = pkgName pkgid
      cabalfile = display pkg <.> "cabal"
  pkgcabal <- doesFileExist cabalfile
  if pkgcabal
    then return $ Just cabalfile
    else do
    builddir <- getBuildDir mnv
    let dir = builddir </> display pkgid
    exists <- doesDirectoryExist dir
    if exists
      then fileWithExtension dir ".cabal"
      else return Nothing

pkgSpecPkgData :: Flags -> Maybe String -> Maybe PackageName
               -> IO PackageData
pkgSpecPkgData flags mnv mpkg = do
  mspec <- checkForSpecFile mpkg
  case mspec of
    Just spec -> specPackageData spec
    Nothing -> do
      mcabal <- checkForCabalFile mpkg
      case mcabal of
        Just cabalfile -> simplePackageDescription flags cabalfile Nothing
        Nothing ->
          case mpkg of
            Just pkg -> prepStreamPkg flags mnv Nothing defaultLTS pkg
            Nothing -> do
              cwd <- getCurrentDirectory
              case simpleParse (takeFileName cwd) of
                Just pdir ->
                  prepare flags mnv (streamPkgToPVS Nothing (Just pdir))
                Nothing -> error' "package not found for directory"
  where
    specPackageData :: FilePath -> IO PackageData
    specPackageData spec = do
      assertFileNonEmpty spec
      cabalfile <- cabalFromSpec spec
      simplePackageDescription flags cabalfile (Just spec)
      where
        cabalFromSpec :: FilePath -> IO FilePath
        cabalFromSpec specFile = do
          -- FIXME handle ghcX.Y
          havePkgname <- grep_ "%{pkg_name}" specFile
          -- FIXME handle bin packages starting with ghc, like "ghc-tags"
          -- FIXME what about mnv above??
          mnv' <- headMay <$>
                 rpmspec ["--srpm"] (Just "%{name}-%{version}") specFile
          case mnv' of
            Nothing -> error' $ "Failed to determine NVR for" +-+ specFile
            Just nv -> do
              let namever =
                    (if havePkgname then removePrefix "ghc-" else id) nv
              case simpleParse namever of
                Nothing -> error' $ "pkgid could not be parsed:" +-+ namever
                Just pkgid -> do
                  bringTarball pkgid (Just specFile)
                  builddir <- getBuildDir $ Just nv
                  let pkgsrcdir = builddir </> namever
                  dExists <- doesDirectoryExist pkgsrcdir
                  if dExists
                    then do
                    specTime <- modificationTime <$> getFileStatus specFile
                    dirTime <- accessTime <$> getFileStatus pkgsrcdir
                    when (specTime > dirTime) $ do
                      rpmbuild True Prep specFile
                      dExists' <- doesDirectoryExist pkgsrcdir
                      when dExists' $ cmd_ "touch" [pkgsrcdir]
                    else
                    rpmbuild True Prep specFile
                  tryFindPackageDesc pkgsrcdir

-- 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
              , packageDesc :: PackageDescription
              , docFilenames :: [FilePath]
              , licenseFilenames :: [FilePath]
              , manpageFiles :: [FilePath]
              }

prepPkgId :: Flags -> Maybe String -> Maybe FilePath
          -> PackageIdentifier -> IO PackageData
prepPkgId flags mnv mspec pkgid = do
  void $ getRevisedCabal pkgid
  cabalfile <- tryUnpack mnv pkgid
  simplePackageDescription flags cabalfile mspec

prepStreamPkg :: Flags -> Maybe String -> Maybe FilePath -> Stream
              -> PackageName -> IO PackageData
prepStreamPkg flags mnv mspec stream pkg = do
  pkgid <- latestPackage (Just stream) pkg
  mcabal <- checkForPkgCabalFile mnv pkgid
  case mcabal of
    Just cabalfile -> simplePackageDescription flags cabalfile mspec
    Nothing -> prepPkgId flags mnv mspec pkgid

prepare :: Flags -> Maybe String
        -- Nothing means package in cwd
        -> Maybe PackageVersionSpecifier
        -> IO PackageData
prepare flags mnv Nothing = pkgSpecPkgData flags mnv Nothing
-- Something implies either new packaging or some existing spec file in dir
prepare flags mpkgid (Just pvs) =
  case pvs of
    PVPackageName pkg -> pkgSpecPkgData flags mpkgid (Just pkg)
    PVPackageId pkgid -> do
      mspec <- checkForSpecFile $ Just (pkgName pkgid)
      mcabal <- checkForPkgCabalFile mpkgid pkgid
      case mcabal of
        Just cabalfile -> simplePackageDescription flags cabalfile mspec
        Nothing -> prepPkgId flags mpkgid mspec pkgid
    PVStreamPackage stream Nothing -> do
      mspec <- checkForSpecFile Nothing
      case mspec of
        Nothing -> do
          cwd <- getCurrentDirectory
          let trydir = simpleParse (takeFileName cwd)
          case trydir of
            Just pdir | pkgVersion pdir == nullVersion ->
                          prepare flags mpkgid (streamPkgToPVS (Just stream) trydir)
            _ -> error' "package not found"
        Just spec -> do
          let pkg = mkPackageName $ removePrefix "ghc-" $ takeBaseName spec
          prepStreamPkg flags mpkgid (Just spec) stream pkg
    PVStreamPackage stream (Just pkg) -> do
      mspec <- checkForSpecFile (Just pkg)
      prepStreamPkg flags mpkgid mspec stream pkg

-- redundant mdir was earlier for update
patchSpec :: Bool -> Maybe FilePath -> FilePath -> FilePath -> IO ()
patchSpec dryrun mdir oldspec newspec = do
  diff <- cmdIgnoreErr "diff" ["-u2", "-I", "- spec file generated by cabal-rpm", "-I", "Fedora Haskell SIG <haskell@lists.fedoraproject.org>", oldspec, newspec] "" >>= cmdIgnoreErr "sed" ["-e", "s%.Cblrpm/%%", "-e", "s/.cblrpm//"] . dropChangelog
  unless (null diff) $ do
    putStrLn diff
    unless dryrun $ do
      putStrLn ""
      out <- cmdIgnoreErr "patch" opts (diff ++ "\n")
      putStrLn out
  where
    opts = ["--fuzz=1"] ++ ["-p" ++ show n | let n = count '/' (removePrefix ".Cblrpm/" newspec)] ++ maybe [] (\ d -> ["-d", d]) mdir

    count :: Eq a => a -> [a] -> Int
    count x =  length . filter (==x)

dropChangelog :: String -> String
dropChangelog cs =
  let ls = lines cs in
  if " %changelog" `elem` ls then
    let rest = (dropWhileEnd ("@@ " `isPrefixOf`) . dropWhileEnd (== " ") . takeWhile (/= " %changelog")) ls in
      if length rest > 2 then unlines rest else ""
    else cs

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"
  -- FIXME use frpq
  -- --quiet needed to silence dnf5 unwanted stdout
  let (prog, subcmd) = if havednf then ("dnf", ["repoquery", "--quiet"]) else ("repoquery", [])
  cmd prog (subcmd ++ args ++ [key])

rpmInstall :: Bool -> [String] -> IO ()
rpmInstall _ [] = return ()
rpmInstall yes rpms = do
  pkginstaller <- packageManager
  let (inst, arg) = if pkginstaller == "dnf" then ("dnf", "install") else ("yum", "localinstall")
  tty <- hIsTerminalDevice stdout
  sudo_ inst $ ["--assumeyes" | yes || 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 "rpmspec" ["-q", "--qf", "%{" ++ field ++ "}", "--srpm", "--undefine", "dist", spec]

#if !MIN_VERSION_Cabal(3,6,0)
getSymbolicPath :: FilePath -> String
getSymbolicPath = id
#endif

-- FIXME for rpm 4.20 %{builddir}
dependencySortCabals :: String -> Maybe FilePath
                     -> [PackageIdentifier] -> IO [PackageIdentifier]
dependencySortCabals _ _ [] = return []
dependencySortCabals nv mspec pkgids = do
  cabalsort <- optionalProgram "cabal-sort"
  if cabalsort
    then do
    forM_ pkgids $ prepare [] (Just nv) . Just . PVPackageId
    builddir <- getBuildDir (Just nv)
    withCurrentDirectory builddir $ do
      -- pre-sort to stabilize sorting
      sorted <- cmdLines "cabal-sort" (map (\pid -> showPkgId pid </> display (pkgName pid) <.> "cabal") $ reverseSort pkgids)
      --print sorted
      return $ mapMaybe (simpleParse . takeDirectory) sorted
    else do
    mapM_ (`bringTarball` mspec) pkgids -- FIXME ?
    return pkgids
  where
    reverseSort :: Ord a => [a] -> [a]
    reverseSort = sortBy (comparing Down)

readGlobalMacro :: String -> FilePath -> IO (Maybe String)
readGlobalMacro macro spec = do
            ps <- grep ("%global" +-+ macro ++ " ") spec
            return $
              case ps of
                [] -> Nothing
                [m] -> Just $ last $ words m
                _ -> error' $
                     "multiple %" ++ macro +-+ "definitions in" +-+ spec

packageMacro :: String -> String
packageMacro = filter (/= '-')