module-management-0.20.2: testdata/debian/Debian/Repo/SourceTree.hs
{-# LANGUAGE CPP, FlexibleInstances, MultiParamTypeClasses, OverloadedStrings, PackageImports, ScopedTypeVariables #-}
module Debian.Repo.SourceTree
( addLogEntry
, buildDebs
, HasTopDir(topdir)
, HasSubDir(subdir)
, HasDebDir(debdir)
, HasChangeLog(entry)
, HasSourceTree(findSourceTree, copySourceTree)
, HasBuildTree(findBuildTree)
, explainSourcePackageStatus
, findChanges
, findDebianBuildTrees
, findDebianSourceTrees
, findOneDebianBuildTree
, findOrigTarball
, origTarballPath
, BuildDecision(..)
, SourcePackageStatus (Indep, All, None)
, DebianBuildTree (debTree', topdir')
, DebianSourceTree(tree', control')
, SourceTree(dir')
) where
import Control.Applicative ((<$>), (<*>), pure)
import Control.Exception (evaluate, SomeException, try, throw)
import Control.Monad (foldM)
import Control.Monad.Trans (MonadIO(..))
import Data.List (intercalate, nubBy, sortBy)
import Data.Time (NominalDiffTime)
import Debian.Changes (ChangeLogEntry(..), ChangesFile(..), parseEntries)
import Debian.Control.Policy (HasDebianControl(debianControl), DebianControl, parseDebianControlFromFile)
import Debian.Pretty (ppDisplay)
import Debian.Relation (BinPkgName(..))
import Debian.Repo.Changes (findChangesFiles)
import Debian.Repo.EnvPath (EnvRoot(rootPath))
import Debian.Repo.MonadOS (MonadOS(getOS))
import Debian.Repo.OSImage (osRoot)
import Debian.Repo.Prelude (rsync, getSubDirectories, replaceFile, dropPrefix)
import Debian.Repo.Prelude.Verbosity (readProcFailing, timeTask, noisier, modifyProcessEnv)
import qualified Debian.Version as V (version)
import System.Directory (createDirectoryIfMissing, doesDirectoryExist, doesFileExist)
import System.Environment (getEnv, getEnvironment)
import System.Exit (ExitCode(ExitFailure), ExitCode(ExitSuccess))
import System.FilePath ((</>))
import System.IO (hGetContents, IOMode(ReadMode), withFile)
import System.Process (CmdSpec(..), CreateProcess(cwd, env, cmdspec), proc, readProcessWithExitCode, showCommandForUser)
import System.Process.Chunks (collectProcessTriple)
import System.Unix.Chroot (useEnv)
class HasTopDir t where
topdir :: t -> FilePath
class HasSubDir t where
subdir :: t -> FilePath
class HasDebDir t where
debdir :: t -> FilePath
class HasChangeLog t where
entry :: t -> ChangeLogEntry
class HasSourceTree t where
findSourceTree :: FilePath -> IO t
-- ^ This just determines whether path is a directory and if so
-- wraps SourceTree around it.
copySourceTree :: t -> FilePath -> IO t
class HasBuildTree t where
findBuildTree :: FilePath -> String -> IO t
-- ^ This applies findSourceTree to path </> name.
-- |Find the .changes file which is generated by a successful run of
-- dpkg-buildpackage.
findChanges :: DebianBuildTree -> IO ChangesFile
findChanges tree =
do let dir = topdir tree
result <- findChangesFiles dir
case result of
[cf] -> return cf
[] -> fail ("Couldn't find .changes file in " ++ dir)
lst -> fail ("Multiple .changes files in " ++ dir ++ ": " ++ intercalate ", " (map ppDisplay lst))
-- |Rewrite the changelog with an added entry.
addLogEntry :: (HasChangeLog t, HasDebDir t) => ChangeLogEntry -> t -> IO ()
addLogEntry entry'' debtree =
-- readFile changelogPath >>= replaceFile changelogPath . ((show (pretty entry'')) ++)
withFile changelogPath ReadMode (\ handle -> hGetContents handle >>= replaceFile changelogPath . ((ppDisplay entry'' ++ "\n\n") ++))
where
changelogPath = (debdir debtree) ++ "/debian/changelog"
-- |There are three possible results of a build: an upload consisting
-- of only the architecture independent debs (Indep), one including
-- both indep and binary debs (All), or with a failed build (None).
data SourcePackageStatus = All | Indep [BinPkgName] | None deriving (Show, Eq)
explainSourcePackageStatus :: SourcePackageStatus -> String
explainSourcePackageStatus All = "All architecture dependent files for the current build architecture are present."
explainSourcePackageStatus (Indep missing) = "Some or all architecture-dependent files for the current build architecture are missing: " ++ show missing
explainSourcePackageStatus None = "This version of the package is not present."
-- |Represents a decision whether to build a package, with a text juststification.
data BuildDecision
= Yes String
| No String
| Arch String -- Needs a -B build, architecture dependent files only
| Auto String -- Needs a 'automated' rebuild, with a generated version number and log entry
| Error String -- A fatal condition was encountered - e.g. a build dependency became older since last build
instance Show BuildDecision where
show (Yes reason) = "Yes - " ++ reason
show (No reason) = "No - " ++ reason
show (Arch reason) = "Yes - " ++ reason
show (Auto reason) = "Yes - " ++ reason
show (Error reason) = "Error - " ++ reason
-- | Run dpkg-buildpackage in a build tree.
buildDebs :: (MonadOS m, MonadIO m) => Bool -> Bool -> [(String, Maybe String)] -> DebianBuildTree -> BuildDecision -> m NominalDiffTime
buildDebs noClean _twice setEnv buildTree decision =
do
root <- rootPath . osRoot <$> getOS
noSecretKey <- liftIO $ getEnv "HOME" >>= return . (++ "/.gnupg") >>= doesDirectoryExist >>= return . not
env0 <- liftIO getEnvironment
-- Set LOGNAME so dpkg-buildpackage doesn't die when it fails to
-- get the original user's login information
let run cmd =
liftIO $ do
cmd' <- modifyProcessEnv (("LOGNAME", Just "root") : setEnv) cmd
let cmd'' = cmd' {cwd = dropPrefix root path}
timeTask $ useEnv root forceList $ readProcFailing cmd'' ""
_ <- liftIO $ run (proc "chmod" ["ugo+x", "debian/rules"])
let buildCmd = proc "dpkg-buildpackage" (concat [["-sa"],
case decision of Arch _ -> ["-B"]; _ -> [],
if noSecretKey then ["-us", "-uc"] else [],
if noClean then ["-nc"] else []])
(result, elapsed) <- liftIO . noisier 4 $ run buildCmd
case collectProcessTriple result of
(ExitFailure n, _, _) -> fail $ "*** FAILURE: " ++ showCmd (cmdspec buildCmd) ++ " -> " ++ show n
_ -> return elapsed
where
path = debdir buildTree
showCmd (RawCommand cmd args) = showCommandForUser cmd args
showCmd (ShellCommand cmd) = cmd
forceList :: [a] -> IO [a]
forceList output = evaluate (length output) >> return output
findOrigTarball :: DebianBuildTree -> IO (Maybe FilePath)
findOrigTarball tree =
do exists <- doesFileExist (origTarballPath tree)
return $ if exists then Just (origTarballPath tree) else Nothing
origTarballPath :: DebianBuildTree -> FilePath
origTarballPath tree =
topdir tree ++ "/" ++ orig
where
orig = name ++ "_" ++ version ++ ".orig.tar.gz"
name = logPackage . entry $ tree
version = V.version . logVersion . entry $ tree
-- deprecated
{-
copyDebianSourceTree :: (DebianSourceTreeC t) => t -> FilePath -> IO t
copyDebianSourceTree = copySourceTree
copyDebianBuildTree :: (DebianBuildTreeC t) => t -> FilePath -> IO t
copyDebianBuildTree = copySourceTree
-}
-- |Find all the debian source trees in a directory.
findDebianSourceTrees :: FilePath -> IO [(FilePath, DebianSourceTree)]
findDebianSourceTrees path =
getSubDirectories path >>= \ (subdirs :: [FilePath]) ->
foldM (\ pairs subdir'' ->
try (findSourceTree (path </> subdir'')) >>=
either (\ (_ :: SomeException) -> return pairs) (\ tree -> return ((subdir'', tree) : pairs))) [] subdirs
-- |Find all the debian source trees in a directory.
findDebianBuildTrees :: FilePath -> IO [DebianBuildTree]
findDebianBuildTrees path =
getSubDirectories path >>=
foldM (\ trees subdir'' ->
try (findBuildTree path subdir'') >>=
either (\ (_ :: SomeException) -> return trees) (\ tree -> return $ tree : trees)) []
-- |Find a DebianBuildTree inside a directory. It finds all the
-- DebianSourceTrees, and if they all have the same package name it
-- returns the newest one according to the version numbers. If there
-- are none, or there are trees with different package names, Nothing
-- is returned.
findOneDebianBuildTree :: FilePath -> IO (Maybe DebianBuildTree)
findOneDebianBuildTree path =
do trees <- findDebianBuildTrees path
-- Do all the trees have the same package name?
case nubBy eqNames trees of
-- Yes, return the newest one
[_] -> return . Just . head . sortBy cmpVers $ trees
-- No trees found
[] -> return Nothing
-- No, throw an exception
names -> error $ "findOneDebianBuildTree: more than one source package name found in " ++ path ++ ": " ++ show (map (logPackage . entry) names)
where
eqNames tree1 tree2 = (logPackage . entry $ tree1) == (logPackage . entry $ tree2)
cmpVers tree1 tree2 = compare (logVersion . entry $ tree1) (logVersion . entry $ tree2)
-- |Any directory containing source code.
data SourceTree =
SourceTree {dir' :: FilePath} deriving Show
-- |A Debian source tree, which has a debian subdirectory containing
-- at least a control file and a changelog.
data DebianSourceTree =
DebianSourceTree {tree' :: SourceTree,
control' :: DebianControl,
entry' :: ChangeLogEntry}
deriving Show
instance HasDebianControl DebianSourceTree where
debianControl = control'
-- |A Debian source tree plus a parent directory, which is where the
-- binary and source deb packages appear after a build. Note that
-- topdir' </> subdir' == dir' . tree' . debTree'
data DebianBuildTree =
DebianBuildTree {topdir' :: FilePath,
subdir' :: String,
debTree' :: DebianSourceTree}
deriving Show
instance HasTopDir DebianSourceTree where
topdir = dir' . tree'
instance HasDebDir DebianSourceTree where
debdir = dir' . tree'
instance HasChangeLog DebianSourceTree where
entry = entry'
instance HasDebDir DebianBuildTree where
debdir t = topdir' t </> subdir' t
instance HasTopDir DebianBuildTree where
topdir = topdir'
instance HasSubDir DebianBuildTree where
subdir = subdir'
instance HasTopDir SourceTree where
topdir = dir'
instance HasSourceTree SourceTree where
findSourceTree path =
doesDirectoryExist path >>= \ exists ->
case exists of
False -> fail $ "No such directory: " ++ path
True -> return $ SourceTree path
copySourceTree tree dest =
createDirectoryIfMissing True dest >>
rsync [] (topdir tree) dest >>
return (SourceTree dest)
instance HasSourceTree DebianSourceTree where
findSourceTree path0 =
findSourceTree path0 >>= \ (tree :: SourceTree) ->
parseDebianControlFromFile (path0 ++ "/debian/control") >>= either throw return >>= \ c ->
-- We only read part of the changelog, so be careful that the file
-- descriptor gets closed.
withFile (path0 ++ "/debian/changelog") ReadMode $ \ handle ->
hGetContents handle >>= \ l ->
case parseEntries l of
(Right e : _) ->
-- ePutStrLn ("findDebianSourceTree " ++ show path0 ++ " -> " ++ topdir tree) >>
return (DebianSourceTree tree c e)
(Left msgs : _) -> error $ "Bad changelog entry in " ++ show (path0 ++ "/debian/changelog") ++ ": " ++ intercalate ", " msgs
[] -> return $ error $ "Empty changelog file: " ++ show (path0 ++ "/debian/changelog")
copySourceTree tree dest =
DebianSourceTree <$> copySourceTree (tree' tree) dest
<*> pure (control' tree)
<*> pure (entry' tree)
instance HasSourceTree DebianBuildTree where
findSourceTree path =
do trees <- findDebianBuildTrees path
case nubBy eqNames trees of
[_] -> return . head . sortBy cmpVers $ trees
[] -> error $ "No source trees found in subdirectorys of " ++ path
names -> error $ "Mutiple source package names found in " ++ path ++ ": " ++ show (map (logPackage . entry) names)
where
eqNames tree1 tree2 = (logPackage . entry $ tree1) == (logPackage . entry $ tree2)
cmpVers tree1 tree2 = compare (logVersion . entry $ tree1) (logVersion . entry $ tree2)
copySourceTree build dest =
copySource >>= copyTarball >>= return . moveBuild
where
copySource = createDirectoryIfMissing True dest >> rsync [] (topdir' build) dest
-- copySource = DebianBuildTree <$> pure dest <*> pure (subdir' tree) <*> copySourceTree (debTree' tree) (dest </> subdir' tree)
copyTarball (ExitFailure _, _, _) = error $ "Failed to copy source tree: " ++ topdir' build ++ " -> " ++ dest
copyTarball (ExitSuccess, _, _) =
do exists <- liftIO $ doesFileExist origPath
case exists of
False -> return (ExitSuccess, "", "")
True -> liftIO $ readProcessWithExitCode "cp" ["-p", origPath, dest ++ "/"] ""
moveBuild (ExitFailure _, _, _) = error $ "Failed to copy Tarball: " ++ origPath ++ " -> " ++ dest ++ "/"
moveBuild (ExitSuccess, _, _) = build {topdir' = dest, debTree' = moveSource (debTree' build)}
moveSource source = source {tree' = SourceTree {dir' = dest </> subdir build}}
origPath = topdir build </> orig
orig = name ++ "_" ++ version ++ ".orig.tar.gz"
name = logPackage . entry $ build
version = V.version . logVersion . entry $ build
instance HasBuildTree DebianBuildTree where
findBuildTree path d = findSourceTree (path </> d) >>= return . DebianBuildTree path d
instance HasChangeLog DebianBuildTree where
entry = entry' . debTree'