module-management-0.20.2: testdata/debian/Debian/Repo/OSImage.hs
{-# LANGUAGE DeriveDataTypeable, FlexibleContexts, FlexibleInstances, OverloadedStrings, PackageImports, ScopedTypeVariables, StandaloneDeriving, TemplateHaskell, TypeSynonymInstances #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}
module Debian.Repo.OSImage
(
-- * OSImage type
OSImage(osRoot, osBaseDistro, osArch, osLocalMaster, osLocalCopy, osSourcePackageCache, osBinaryPackageCache)
, createOSImage
, cloneOSImage
-- * OSImage Creation
, pbuilder
, debootstrap
, syncOS'
-- * OSImage Queries
, osFullDistro
, buildEssential
-- * OSImage Manipulation
, neuterEnv
, restoreEnv
, localeGen
, removeEnv
) where
import Control.Exception (evaluate, SomeException)
import Control.Monad.Catch (try)
import Control.Monad.State (MonadIO(..))
import Data.Data (Data)
import Data.List (intercalate)
import Data.Typeable (Typeable)
import Debian.Arch (Arch)
import Debian.Pretty (ppDisplay)
import Debian.Relation (ParseRelations(parseRelations), Relations)
import Debian.Release (parseReleaseName, parseSection', ReleaseName(relName))
import Debian.Repo.EnvPath (EnvPath(EnvPath, envPath, envRoot), EnvRoot(rootPath), outsidePath)
import Debian.Repo.Internal.IO (buildArchOfRoot)
import Debian.Repo.LocalRepository (copyLocalRepo, LocalRepository)
import Debian.Repo.PackageIndex (BinaryPackage, SourcePackage)
import Debian.Repo.Prelude (isSublistOf, replaceFile, rsync, sameInode, sameMd5sum)
import Debian.Repo.Prelude.Verbosity (qPutStr, qPutStrLn, ePutStr, ePutStrLn, readProcFailing)
import Debian.Repo.Repo (repoKey, repoURI)
import Debian.Repo.Slice (NamedSliceList(sliceList), NamedSliceList(sliceListName), Slice(Slice, sliceRepoKey, sliceSource), SliceList(..))
import Debian.Sources (DebSource(..), DebSource(sourceDist, sourceUri), SourceType(..), SourceType(..))
import Debian.URI (uriToString')
import System.Directory (createDirectoryIfMissing, doesFileExist, removeFile, renameFile)
import System.Exit (ExitCode(ExitSuccess))
import System.FilePath ((</>))
import System.Posix.Files (createLink, deviceID, fileID, FileStatus, modificationTime)
import System.Process (shell)
import System.Process.Chunks (collectProcessTriple, collectProcessResult)
import System.Unix.Chroot (useEnv)
import System.Unix.Directory (removeRecursiveSafely)
import System.Unix.Mount (umountBelow)
import Text.Regex (matchRegex, mkRegex)
instance Ord FileStatus where
compare a b = compare (deviceID a, fileID a, modificationTime a) (deviceID b, fileID b, modificationTime b)
instance Eq FileStatus where
a == b = compare a b == EQ
-- |This type represents an OS image located at osRoot built from a
-- particular osBaseDistro using a particular osArch. If an
-- osLocalRepo argument is given, that repository will be copied into
-- the environment and kept in sync, and lines will be added to
-- sources.list to point to it.
data OSImage
= OS { osRoot :: EnvRoot
, osBaseDistro :: NamedSliceList
, osArch :: Arch
, osLocalMaster :: LocalRepository
-- ^ The associated local repository, where packages we build
-- inside this image are first uploaded to.
, osLocalCopy :: LocalRepository
-- ^ A copy of osLocalMaster located inside the os root environment.
, osSourcePackageCache :: Maybe [SourcePackage]
, osBinaryPackageCache :: Maybe [BinaryPackage]
}
instance Ord OSImage where
compare a b = case compare (osRoot a) (osRoot b) of
EQ -> case compare (osBaseDistro a) (osBaseDistro b) of
EQ -> compare (osArch a) (osArch b)
x -> x
x -> x
instance Eq OSImage where
a == b = compare a b == EQ
-- |Create an OS image record
createOSImage ::
EnvRoot -- ^ The location where image is to be built
-> NamedSliceList -- ^ The sources.list of the base distribution
-> LocalRepository -- ^ The location of the local upload repository
-> IO OSImage
createOSImage root distro repo =
do copy <- copyLocalRepo (EnvPath {envRoot = root, envPath = "/work/localpool"}) repo
-- At this point we can only support the build architecture of
-- the underlying system. We can support multiple
-- distributions, but if the hardware is an amd64 the packages
-- produced will be amd64.
arch <- liftIO buildArchOfRoot
let os = OS { osRoot = root
, osBaseDistro = distro
, osArch = arch
, osLocalMaster = repo
, osLocalCopy = copy
, osSourcePackageCache = Nothing
, osBinaryPackageCache = Nothing }
return os
-- | Create the OSImage record for a copy of an existing OSImage at a
-- different location.
cloneOSImage :: OSImage -> EnvRoot -> IO OSImage
cloneOSImage src dst = do
copy <- copyLocalRepo (EnvPath {envRoot = dst, envPath = "/work/localpool"}) (osLocalMaster src)
return $ src {osRoot = dst, osLocalCopy = copy}
-- | Set the location of the OSImage's root directory - where you
-- would cd to before running chroot.
-- chrootEnv :: OSImage -> EnvRoot -> OSImage
-- chrootEnv os dst = os {osRoot = dst}
-- The following are path functions which can be used while
-- constructing instances of AptCache. Each is followed by a
-- corresponding function that gives the same result when applied to
-- an AptCache instance.
data SourcesChangedAction =
SourcesChangedError |
UpdateSources |
RemoveRelease
deriving (Eq, Show, Data, Typeable)
instance Show OSImage where
show os = intercalate " " ["OS {",
rootPath (osRoot os),
relName (sliceListName (osBaseDistro os)),
show (osArch os),
show (osLocalCopy os)]
-- |The sources.list is the list associated with the distro name, plus
-- the local sources where we deposit newly built packages.
osFullDistro :: OSImage -> SliceList
osFullDistro os =
let base = osBaseDistro os
repo' = osLocalCopy os
name = relName (sliceListName base)
localSources :: SliceList
localSources = SliceList {slices = [Slice {sliceRepoKey = repoKey repo', sliceSource = src},
Slice {sliceRepoKey = repoKey repo', sliceSource = bin}]}
src = DebSource Deb (repoURI repo') (Right (parseReleaseName name, [parseSection' "main"]))
bin = DebSource DebSrc (repoURI repo') (Right (parseReleaseName name, [parseSection' "main"])) in
SliceList { slices = slices (sliceList base) ++ slices localSources }
data UpdateError
= Changed ReleaseName FilePath SliceList SliceList
| Missing ReleaseName FilePath
| Flushed
instance Show UpdateError where
show (Changed r p l1 l2) = unwords ["Changed", show r, show p, ppDisplay l1, ppDisplay l2]
show (Missing r p) = unwords ["Missing", show r, show p]
show Flushed = "Flushed"
syncOS' :: OSImage -> EnvRoot -> IO OSImage
syncOS' src dst = do
mkdir
umount
(_result, _, _) <- rsync ["--exclude=/work/build/*"] (rootPath (osRoot src)) (rootPath dst)
cloneOSImage src dst
where
mkdir = createDirectoryIfMissing True (rootPath dst ++ "/work")
umount =
do srcResult <- umountBelow False (rootPath (osRoot src))
dstResult <- umountBelow False (rootPath dst)
case filter (\ (_, (code, _, _)) -> code /= ExitSuccess) (srcResult ++ dstResult) of
[] -> return ()
failed -> fail $ "umount failure(s): " ++ show failed
-- | FIXME - we should notice the locale problem and run this.
localeGen :: OSImage -> String -> IO ()
localeGen os locale =
do let root = osRoot os
qPutStr ("Generating locale " ++ locale ++ " (" ++ stripDist (rootPath root) ++ ")...")
result <- try $ useEnv (rootPath root) forceList (readProcFailing (shell cmd) "")
case result of
Left (e :: SomeException) -> error $ "Failed to generate locale " ++ rootPath root ++ ": " ++ show e
Right output ->
case collectProcessTriple output of
(ExitSuccess, _, _) -> qPutStrLn "done"
(code, _, _) -> error $ "Failed to generate locale " ++ rootPath root ++ ": " ++ cmd ++ " -> " ++ show code
where
cmd = "locale-gen " ++ locale
-- |To "neuter" an executable is to replace it with a hard link to
-- \/bin\/true in such a way that the operation can be reversed. This
-- is done in order to make it safe to install files into it when it
-- isn't "live". If this operation fails it is assumed that the
-- image is damaged, so it is removed.
neuterEnv :: OSImage -> IO ()
neuterEnv os =
do qPutStr ("Neutering OS image (" ++ stripDist root ++ ")...")
result <- try $ mapM_ (neuterFile os) neuterFiles
either (\ (e :: SomeException) -> error $ "Failed to neuter environment " ++ root ++ ": " ++ show e)
(\ _ -> qPutStrLn "done.")
result
where
root = rootPath (osRoot os)
neuterFiles :: [(FilePath, Bool)]
neuterFiles = [("/sbin/start-stop-daemon", True),
("/usr/sbin/invoke-rc.d", True),
("/sbin/init",False),
("/usr/sbin/policy-rc.d", False)]
-- neuter_file from build-env.ml
neuterFile :: OSImage -> (FilePath, Bool) -> IO ()
neuterFile os (file, mustExist) =
do
-- putStrBl ("Neutering file " ++ file)
exists <- doesFileExist (outsidePath fullPath)
if exists then
neuterExistantFile else
if mustExist then
error ("Can't neuter nonexistant file: " ++ outsidePath fullPath) else
return () -- putStrBl "File doesn't exist, nothing to do"
where
neuterExistantFile =
do
sameFile <- sameInode (outsidePath fullPath) (outsidePath binTrue)
if sameFile then
return () else -- putStrBl "File already neutered"
neuterUnneuteredFile
neuterUnneuteredFile =
do
hasReal <- doesFileExist (outsidePath fullPath ++ ".real")
if hasReal then
neuterFileWithRealVersion else
neuterFileWithoutRealVersion
createLink (outsidePath binTrue) (outsidePath fullPath)
neuterFileWithRealVersion =
do
sameCksum <- sameMd5sum (outsidePath fullPath) (outsidePath fullPath ++ ".real")
if sameCksum then
removeFile (outsidePath fullPath) else
error (file ++ " and " ++ file ++ ".real differ (in " ++ rootPath root ++ ")")
neuterFileWithoutRealVersion = renameFile (outsidePath fullPath) (outsidePath fullPath ++ ".real")
fullPath = EnvPath root file
binTrue = EnvPath root "/bin/true"
root = osRoot os
-- |Reverse the neuterEnv operation.
restoreEnv :: OSImage -> IO OSImage
restoreEnv os =
do
qPutStr "De-neutering OS image..."
result <- try $ mapM_ (restoreFile os) neuterFiles
either (\ (e :: SomeException) -> error $ "damaged environment " ++ rootPath root ++ ": " ++ show e ++ "\n please remove it.")
(\ _ -> return os) result
where
root = osRoot os
-- check_and_restore from build-env.ml
restoreFile :: OSImage -> (FilePath, Bool) -> IO ()
restoreFile os (file, mustExist) =
do
exists <- doesFileExist (outsidePath fullPath)
if exists then
restoreExistantFile else
if mustExist then
error ("Can't restore nonexistant file: " ++ outsidePath fullPath) else
return ()
where
restoreExistantFile =
do
isTrue <- sameInode (outsidePath fullPath) (outsidePath binTrue)
hasReal <- doesFileExist (outsidePath fullPath ++ ".real")
case (isTrue, hasReal) of
(True, True) ->
do
removeFile (outsidePath fullPath)
renameFile (outsidePath fullPath ++ ".real") (outsidePath fullPath)
(False, _) -> error "Can't restore file not linked to /bin/true"
(_, False) -> error "Can't restore file with no .real version"
fullPath = EnvPath root file
binTrue = EnvPath root "/bin/true"
root = osRoot os
-- | Build the dependency relations for the build essential packages.
-- For this to work the @build-essential@ package must be installed in
-- the OSImage.
buildEssential :: OSImage -> IO Relations
buildEssential os = do
let root = osRoot os
-- qPutStrLn "Computing build essentials"
essential <-
readFile (rootPath root ++ "/usr/share/build-essential/essential-packages-list") >>=
return . lines >>= return . dropWhile (/= "") >>= return . tail >>= return . filter (/= "sysvinit") >>=
return . parseRelations . (intercalate ", ") >>=
return . (either (error "parse error in /usr/share/build-essential/essential-packages-list") id)
let re = mkRegex "^[^ \t]"
relationText <-
readFile (rootPath root ++ "/usr/share/build-essential/list") >>=
return . lines >>=
return . dropWhile (/= "BEGIN LIST OF PACKAGES") >>= return . tail >>=
return . takeWhile (/= "END LIST OF PACKAGES") >>=
return . filter ((/= Nothing) . (matchRegex re))
-- ePut ("buildEssentialText: " ++ intercalate ", " relationText)
let buildEssential'' = parseRelations (intercalate ", " relationText)
let buildEssential' = either (\ l -> error ("parse error in /usr/share/build-essential/list:\n" ++ show l)) id buildEssential''
return (essential ++ buildEssential')
-- |Remove an image. The removeRecursiveSafely function is used to
-- ensure that any file systems mounted inside the image are unmounted
-- and not destroyed.
removeEnv :: OSImage -> IO ()
removeEnv os =
do
ePutStr "Removing build environment..."
removeRecursiveSafely (rootPath root)
ePutStrLn "done."
where
root = osRoot os
-- prefixes :: Maybe (L.ByteString, L.ByteString)
-- prefixes = Just (" 1> ", " 2> ")
stripDist :: FilePath -> FilePath
stripDist path = maybe path (\ n -> drop (n + 7) path) (isSublistOf "/dists/" path)
-- | This is a deepseq thing
forceList :: [a] -> IO [a]
forceList output = evaluate (length output) >> return output
pbuilder :: FilePath
-> EnvRoot
-> NamedSliceList
-> LocalRepository
-> [String]
-> [String]
-> [String]
-> IO OSImage
pbuilder top root distro repo _extraEssential _omitEssential _extra =
-- We can't create the environment if the sources.list has any
-- file:// URIs because they can't yet be visible inside the
-- environment. So we grep them out, create the environment, and
-- then add them back in.
do ePutStrLn ("Creating clean build environment (" ++ relName (sliceListName distro) ++ ")")
ePutStrLn ("# " ++ cmd top)
let codefn ExitSuccess = return ()
codefn failure = error ("Could not create build environment:\n " ++ cmd top ++ " -> " ++ show failure)
liftIO (readProcFailing (shell (cmd top)) "") >>= return . collectProcessTriple >>= \ (result, _, _) -> codefn result
ePutStrLn "done."
os <- createOSImage root distro repo -- arch? copy?
let sourcesPath' = rootPath root ++ "/etc/apt/sources.list"
-- Rewrite the sources.list with the local pool added.
sources = ppDisplay $ osFullDistro os
replaceFile sourcesPath' sources
return os
where
cmd x =
intercalate " " $ [ "pbuilder"
, "--create"
, "--distribution", (relName . sliceListName $ distro)
, "--basetgz", x </> "pbuilderBase"
, "--buildplace", rootPath root
, "--preserve-buildplace"
]
-- Create a new clean build environment in root.clean
-- FIXME: create an ".incomplete" flag and remove it when build-env succeeds
debootstrap
:: EnvRoot
-> NamedSliceList
-> LocalRepository
-> [String]
-> [String]
-> [String]
-> IO OSImage
debootstrap root distro repo include exclude components =
do
ePutStr (unlines [ "Creating clean build environment (" ++ relName (sliceListName distro) ++ ")"
, " root: " ++ show root
, " baseDist: " ++ show baseDist
, " mirror: " ++ show mirror ])
-- We can't create the environment if the sources.list has any
-- file:// URIs because they can't yet be visible inside the
-- environment. So we grep them out, create the environment, and
-- then add them back in.
readProcFailing (shell cmd) "" >>= codefn . fst . collectProcessResult
ePutStrLn "done."
os <- createOSImage root distro repo -- arch? copy?
let sourcesPath' = rootPath root ++ "/etc/apt/sources.list"
-- Rewrite the sources.list with the local pool added.
sources = ppDisplay $ osFullDistro os
liftIO $ replaceFile sourcesPath' sources
return os
where
codefn ExitSuccess = return ()
codefn failure = error ("Could not create build environment:\n " ++ cmd ++ " -> " ++ show failure)
woot = rootPath root
wootNew = woot ++ ".new"
baseDist = either id (relName . fst) . sourceDist . sliceSource . head . slices . sliceList $ distro
mirror = uriToString' . sourceUri . sliceSource . head . slices . sliceList $ distro
cmd = intercalate " && "
["set -x",
"rm -rf " ++ wootNew,
("debootstrap " ++
(if include /= [] then "--include=" ++ intercalate "," include ++ " " else "") ++
(if exclude /= [] then "--exclude=" ++ intercalate "," exclude ++ " " else "") ++
"--variant=buildd " ++
"--components=" ++ intercalate "," components ++ " " ++
baseDist ++ " " ++
wootNew ++ " " ++
mirror),
"cat " ++ wootNew ++ "/etc/apt/sources.list | sed -e 's/^deb /deb-src /' >>" ++ wootNew ++ "/etc/apt/sources.list",
"mkdir -p " ++ woot,
"rm -rf " ++ woot,
"mv " ++ wootNew ++ " " ++ woot]