packages feed

debian-1.0: Linspire/Debian/SourceTree.hs

-- |Type to represent an unpacked debian source tree.  In order to
-- support Debian's .orig.tar.gz mechanism, the source tree root
-- directory is contained inside another parent directory.
--
-- Author: David Fox
module Linspire.Debian.SourceTree 
    (SourceTree(SourceTreeWithParent, SourceTreeNoParent),
     -- * Accessor
     parent,			-- SourceTree -> FilePath
     path,			-- SourceTree -> FilePath
     -- * Constructors
     -- makeTree,		-- FilePath -> SourceTree
     findTrees,			-- FilePath -> IO [SourceTree]
     findTree,			-- FilePath -> IO SourceTree
     copy,			-- [Style] -> SourceTree -> FilePath -> IO SourceTree
     chroot,			-- FilePath -> FilePath -> SourceTree -> SourceTree
     -- * Inquiry
     controlFile,		-- SourceTree -> IO Control
     latestChange,		-- SourceTree -> IO ChangeLogEntry
     findChanges,		-- SourceTree -> IO Paths.EnvPath
     -- * Modify
     addLogEntry,		-- SourceTree -> ChangeLogEntry -> IO ()
     -- * Helper
     findOrigTar,		-- Paragraph -> Maybe FilePath
     packageName		-- Control -> String
    ) where

import Control.Exception
import Control.Monad
import Data.List
import Data.Maybe
import System.Directory
import System.IO
import Text.Regex
import Linspire.Unix.Progress as Progress
import Linspire.Debian.ChangeLog as ChangeLog
import Linspire.Debian.Control
import Linspire.Debian.Version
import Linspire.Unix.FilePath

data SourceTree
    = SourceTreeWithParent FilePath String |
      SourceTreeNoParent FilePath
    deriving Show
{-
instance Show SourceTree where
    show (SourceTree (Just parent) name) = "SourceTree (Just \"" ++ parent ++ "\") \"" ++ name ++ "\""
    show (SourceTree Nothing path)
    show (No reason) = "No - " ++ reason
-}

-- |Return the location of the source tree.  If the parent
-- is Nothing the path is assumed to be an absolute pathname,
-- otherwise it should be a directory name.
path :: SourceTree -> FilePath
path (SourceTreeWithParent parent subdir) = parent ++ "/" ++ subdir
path (SourceTreeNoParent dir) = dir

-- |Return the location of the source tree's parent directory
-- if that directory is controlled by the source tree.
parent :: SourceTree -> FilePath
parent (SourceTreeWithParent parent _) = parent
parent (SourceTreeNoParent _) = error "Source tree doesn't own its parent"

{-
makeTree :: FilePath -> SourceTree
makeTree path = SourceTree Nothing path
-}

-- |Examine a directory and return any SourceTree objects that can be
-- found there.  The directory should either contain a debian
-- directory with the proper control files, or it should contain a
-- .orig.tar.gz file and a directory containing such a debian
-- directory.
findTrees :: FilePath -> IO [SourceTree]
findTrees dir =
    do
      names <- doesDirectoryExist dir >>= bool (return []) (getDirectoryContents dir)
      let top = SourceTreeNoParent dir
      topValid <- valid top
      case topValid of
        True -> return [top]
        False -> filterM valid (map (SourceTreeWithParent dir) names)

bool :: a -> a -> Bool -> a
bool x _ False = x
bool _ x True = x

findTree :: FilePath -> IO SourceTree
findTree dir =
    do
      trees <- findTrees dir
      case trees of
        [tree] -> return tree
        [] -> error ("No SourceTree found in " ++ dir)
        _ -> error ("Multiple SourceTrees found in " ++ dir)
{-
      case trees of
        [] -> error ("No Debian source tree found in " ++ dir)
        [tree] -> return tree
      case elem "debian" names of
        True ->
            do
      trees <- filterM isSourceTree names
      case trees of
        [name] -> return $ SourceTree (Just dir) name
        [] -> error ("No Debian source tree found in " ++ dir)
        names -> error ("Multiple Debian source trees found in " ++ dir ++ ": " ++ show names)
    where
      isSourceTree dir = doesFileExist (dir ++ "/debian/changelog")
-}

valid :: SourceTree -> IO Bool
valid tree =
    mapM doesFileExist
             [path tree ++ "/debian/changelog",	-- A debian source tree
              path tree ++ "/series"] >>=		-- A quilt patches directory
    return . any id

chroot :: FilePath -> FilePath -> SourceTree -> SourceTree
chroot oldRoot newRoot (SourceTreeWithParent parent name) =
    case changePrefix oldRoot newRoot parent of
      Nothing -> error ("Can't change prefix of parent " ++ parent ++ " from " ++ oldRoot ++ " to " ++ newRoot)
      Just result -> SourceTreeWithParent result name
chroot oldRoot newRoot (SourceTreeNoParent path) =
    case changePrefix oldRoot newRoot path of
      Nothing -> error ("Can't change prefix of path " ++ path ++ " from " ++ oldRoot ++ " to " ++ newRoot)
      Just result -> SourceTreeNoParent result

-- examples
-- copy (SourceTree (Just "/tmp/skipjack/apt/haskell-hsql") "haskell-hsql-1.6") "/tmp/marlin/clean-lax/work/build/haskell-hsql" ->
-- rsync -aHxSpDt --delete '/tmp/skipjack/apt/haskell-hsql/' '/tmp/marlin/clean-lax/work/build/haskell-hsql'

-- |Copy a source tree.  The resulting tree will be a sub-directory of
-- the dest filepath.  If the parent directory contains a .orig.tar.gz
-- file that is copied too.
copy :: [Style] -> SourceTree -> FilePath -> IO SourceTree
copy style source dest =
    do
      -- ePut ("copy " ++ show source ++ " " ++ show dest)
      createDirectoryIfMissing True dest
      systemTask copyStyle ("rsync -aHxSpDt --delete " ++ src source ++ " '" ++ dest ++ "'")

      log <- readFile (path source ++ "/debian/changelog") >>= return . ChangeLog.parse
      let origTar =
              case log of
                (entry : _) ->
                    path source ++ "/../" ++
                    ChangeLog.package entry ++ "_" ++
                    Linspire.Debian.Version.version (ChangeLog.version entry) ++ ".orig.tar.gz"
                [] -> error "Couldn't find changelog entry" 
      exists <- doesFileExist origTar
      case exists of
        False -> return noTimeDiff -- ePut ("No " ++ origTar ++ " found.")
        True -> systemTask tarballStyle ("cp -p " ++ origTar ++ " " ++ dest)
      -- ePut (" -> " ++ show (newTree source))
      return $ newTree source
    where
      src (SourceTreeWithParent parent _) = "'" ++ parent ++ "/'" 
      src (SourceTreeNoParent path) = "'" ++ path ++ "'" 
      newTree (SourceTreeWithParent _ name) = SourceTreeWithParent dest name
      newTree (SourceTreeNoParent path) = SourceTreeWithParent dest (baseName path)
      copyStyle = setStyles [Start ("Copying clean source (" ++ stripDist dest ++ ")"),
                          Error ("Could not copy source tree from " ++ path source ++ " to " ++ dest)] style
      tarballStyle = setStyles [Start ("Copying original tarball"),
                                Error ("Could not copy original tarball from " ++ path source ++ " to " ++ dest)] style

-- |Return the contents of debian\/control.    
controlFile :: SourceTree -> IO Control
controlFile source =
    do
      let controlPath = (path source ++ "/debian/control")
      result <- parseControlFromFile controlPath
      case result of
        -- A valid control file must have a source paragraph and at
        -- least one binary paragraph.
        Right control@(Control (_ : _ : _)) -> return control
        Right control -> error ("Invalid control file: " ++ show control)
        Left e -> error ("Couldn't read control file: " ++ show e)

latestChange :: SourceTree -> IO ChangeLogEntry
latestChange tree =
    do
      let changelog = path tree ++ "/debian/changelog"
      text <- try $ readFile changelog
      let entries = either (error ("Error reading changelog in " ++ changelog)) ChangeLog.parse text
      case entries of
        [] -> error ("Empty changelog: " ++ changelog)
        (entry : _) -> return entry

-- |Find the .changes file which is generated by a successful run of
-- dpkg-buildpackage.
findChanges :: SourceTree -> IO FilePath
findChanges source =
    do
      buildFiles <- getDirectoryContents (parent source)
      let changesFileList = filter isChangesFile buildFiles
      case changesFileList of
        [] -> error ("No .changes file in " ++ parent source)
        [name] -> return $ parent source ++ "/" ++ name
        names -> error ("Multiple .changes files to upload: " ++ show names)
    where
      isChangesFile name = maybe False (\ _ -> True) (matchRegex changesRE name)
      changesRE = mkRegex "\\.changes$"

-- |Rewrite the changelog with an added entry.
addLogEntry :: SourceTree -> ChangeLogEntry -> IO ()
addLogEntry source entry =
    do
      let changelogPath = path source ++ "/debian/changelog"
      original <- readFile changelogPath
      -- date <- getRFC822Date
      let newtext = show entry
      -- ePut ("Adding changelog entry:\n" ++ show entry)

      -- This is delicate - we unlink the file here while its contents
      -- are still being read into the the lazy list 'text'.  If we
      -- don't remove the file before writing it, text ends up looking
      -- like an empty list.
      removeFile changelogPath
      writeFile changelogPath (newtext ++ original)

-- |Get the name of the .orig.tar.gz file of a source package by
-- examining the Files section of a paragraph from a Sources.gz, or as
-- output by apt-cache showsrc.
findOrigTar :: Paragraph -> Maybe FilePath
findOrigTar control =
    maybe Nothing (\ (Field (_, value)) ->
                       find (isSuffixOf ".orig.tar.gz") (parseFiles (stripWS value)))
              (lookupP "Files" control)
    where
      -- Parse the three-field "Files" entry of section output by apt-cache showsrc:
      -- md5sum size name
      parseFiles text =
          case (text, matchRegexAll re text) of
            ("", _) -> []
            (_, Just (_, _, remaining, [filename])) ->
                filename : parseFiles remaining
            _ -> error ("Parse error in Files section of changes file: '" ++ text)
      re = mkRegex ("^[ \t\n]*" ++ t ++ w ++ t ++ w ++ "(" ++ t ++ ")" ++ "[ \t\n]*")
      t = "[^ \t\n]+"
      w = "[ \t]+"

-- |Get the source package name from the control file.
packageName :: Control -> String
packageName (Control (paragraph : _)) =
    maybe (error "Missing Source field in control file") id (fieldValue "Source" paragraph)
packageName _ = error "Invalid control file"

changePrefix :: (Eq a) => [a] -> [a] -> [a] -> Maybe [a]
changePrefix old new s = maybe Nothing (Just . (new ++)) (Linspire.Debian.SourceTree.dropPrefix old s)

dropPrefix :: (Eq a) => [a] -> [a] -> Maybe [a]
dropPrefix prefix s =
    case isPrefixOf prefix s of
      True -> Just (drop (length prefix) s)
      False -> Nothing

ePut :: String -> IO ()
ePut s = hPutStrLn stderr s