packages feed

debian-1.0: Linspire/Debian/AptImage.hs

-- |The AptImage object represents a partial OS image which is capable
-- of running apt-get, and thus obtaining repository info and source
-- code packages.
module Linspire.Debian.AptImage 
    (AptImage,
     prepareAptEnv,	-- [Style] -> FilePath -> Maybe Repository -> SourcesList -> IO AptImage
     updateAptEnv,	-- [Style] -> AptImage -> IO AptImage
     aptGetSource,	-- [Style] -> FilePath -> AptImage -> PkgName -> Maybe DebianVersion -> IO SourceTree
     getAvailable)	-- [Style] -> OSImage -> [String] -> IO [Paragraph]
    where

import Data.List
import Data.Maybe
import System.Directory
import System.IO

import Linspire.Unix.FilePath
import Linspire.Unix.Directory
import Linspire.Unix.Misc
import Linspire.Unix.Progress as Progress
import qualified Linspire.Debian.ChangeLog as ChangeLog
import Linspire.Debian.Control
import Linspire.Debian.Version
import Linspire.Debian.Relation
import Linspire.Debian.SourcesList as SourcesList
import Linspire.Debian.SourceTree as SourceTree

data AptImage = Apt FilePath [DebSource]

-- |Create a skeletal enviroment sufficient to run apt-get.
prepareAptEnv ::  [Style] -> FilePath -> [DebSource] -> IO AptImage
prepareAptEnv style root sources =
    do
      let os = Apt root sources
      createDirectoryIfMissing True (root ++ "/var/lib/apt/lists/partial")
      createDirectoryIfMissing True (root ++ "/var/lib/apt/lists/partial")
      createDirectoryIfMissing True (root ++ "/var/cache/apt/archives/partial")
      createDirectoryIfMissing True (root ++ "/var/lib/dpkg")
      createDirectoryIfMissing True (root ++ "/etc/apt")
      writeFileIfMissing True (root ++ "/var/lib/dpkg/status") ""
      -- We need to create the local pool before updating so the
      -- sources.list will be valid.
      let sourceListText = consperse "\n" (map show sources) ++ "\n"
      -- ePut ("writeFile " ++ (root ++ "/etc/apt/sources.list") ++ "\n" ++ sourceListText)
      writeFile (root ++ "/etc/apt/sources.list") sourceListText
      updateAptEnv style os

updateAptEnv :: [Style] -> AptImage -> IO AptImage
updateAptEnv style os@(Apt root _) =
    do
      systemTask updateStyle ("apt-get" ++ aptOpts os ++ " update")
      return os
    where
      updateStyle = addStyles [Start ("Updating apt-get environment (" ++ root ++ ")"),
                               Error "Failure preparing apt-get environment"] style

-- |Retrieve a source package via apt-get.
aptGetSource :: [Style] -> FilePath -> AptImage -> PkgName -> Maybe DebianVersion -> IO SourceTree
aptGetSource style dir root package version =
    do
      ready <- SourceTree.findTrees dir >>= mapM latestChange >>= return . map ChangeLog.version
      newest <- getAvailable availableStyle root [package] >>=
                return . listToMaybe . map parseDebianVersion . catMaybes . map (fieldValue "Version")
      let version' = maybe newest Just version
      case (version', ready) of
        (Nothing, _) -> error ("No available versions of " ++ package)
        (Just requested, [available]) | requested == available -> SourceTree.findTree dir
        (Just requested, []) ->
            do
              runAptGet aptGetStyle root dir "source" [(package, Just requested)]
              trees <- SourceTree.findTrees dir
              case trees of
                [tree] -> return tree
                _ -> error "apt-get source failed"
        (Just requested, _) ->
            do
              -- One or more incorrect versions are available, remove them
              removeRecursiveSafely dir
              runAptGet aptGetStyle root dir "source" [(package, Just requested)]
              trees <- SourceTree.findTrees dir
              case trees of
                [tree] -> return tree
                _ -> error "apt-get source failed"
    where
      availableStyle = setStyles [Start ("Finding available versions of " ++ package ++ " in APT pool")] style
      aptGetStyle = setStyles [Start ("Retrieving APT source for " ++ package)] style


runAptGet :: [Style] -> AptImage -> FilePath -> String -> [(PkgName, Maybe DebianVersion)] -> IO TimeDiff
runAptGet style root dir command packages =
    do
      createDirectoryIfMissing True dir
      systemTask aptGetStyle (consperse " " ("cd" : dir : "&&" : "apt-get" : aptOpts root : command : map formatPackage packages))
    where
      formatPackage (name, Nothing) = name
      formatPackage (name, Just version) = name ++ "=" ++ show version
      aptGetStyle = setStyles [Error "apt-get failed"] style

getAvailable :: [Style] -> AptImage -> [String] -> IO [Paragraph]
getAvailable style root names =
    do
      Control available <- getSourceInfo style root names
      return $ sortBy cmp available
    where
      cmp p1 p2 =
          compare v2 v1		-- Flip args to get newest first
          where
            v1 = maybe Nothing (Just . parseDebianVersion) (fieldValue "Version" p1)
            v2 = maybe Nothing (Just . parseDebianVersion) (fieldValue "Version" p2)

-- |Function to get information about source packages using apt-cache showsrc.
getSourceInfo :: [Style] -> AptImage -> [PkgName] -> IO Control
getSourceInfo style os@(Apt root _) packages =
    runAptCache (addStyles [Start ("getSourceInfo " ++ consperse " " packages ++ " in " ++ root)] style) os "showsrc" packages

-- |Function to get information about binary packages using apt-cache show.
getBinaryInfo :: [Style] -> AptImage -> [PkgName] -> IO Control
getBinaryInfo style root packages =
    runAptCache (addStyles [Start "getBinaryInfo"] style) root "show" packages

-- We get different files in \/var\/lib\/apt\/lists depending on whether we run
-- apt-get update in or out of the changeroot.  Depending on that we need to
-- use a different sources.list to query the cache.
-- FIXME: it would be nice to not use temporary files here, but
-- waitForProcess hangs below.
-- FIXME: Uniquify the list of packages
runAptCache :: [Style] -> AptImage -> String -> [PkgName] -> IO Control
-- |apt-cache will fail if called with no package names
runAptCache _ _ _ [] = return (Control [])
runAptCache style os@(Apt root _) command packages =
    do
      let opts = aptOpts os
      let cmd = consperse " " ("apt-cache" : opts : command : packages)
      -- FIXME: don't use a temporary file here, but waitForProcess is hanging
{-    (_, outh, _, handle) <- runInteractiveCommand cmd
      control <- parseControlFromHandle ("getPackageInfo " ++ command) outh
      exitcode <- waitForProcess handle
      return $ either (error "Parse error in apt-cache output") id control -}
      let tmp = "/tmp/output"
      systemTask runStyle (cmd ++ " > " ++ tmp)
      control <- parseControlFromFile tmp
      -- If this file is not removed here, its contents may be
      -- replaced by a subsequent call before the parseControlFromFile
      -- is executed.  Seriously!
      removeFile tmp
      return $ either (error "Parse error in apt-cache output") id control
    where
      runStyle = addStyles [Start ("Running apt-cache " ++ command ++ {- " " ++ show packages ++ -}
                                   " in " ++ root)] style

{-
aptSourcesList :: AptImage -> [DebSource]
aptSourcesList (Apt _ sources) = sources
    -- DistroCache.sources distro ++ maybe [] (DistroCache.localPool distro . topDir) repo
-}

aptOpts :: AptImage -> String
aptOpts (Apt root _) =
    (" -o=Dir::State::status=" ++ root ++ "/var/lib/dpkg/status" ++
     " -o=Dir::State::Lists=" ++ root ++ "/var/lib/apt/lists" ++
     " -o=Dir::Cache::Archives=" ++ root ++ "/var/cache/apt/archives" ++
     " -o=Dir::Etc::SourceList=" ++ root ++ "/etc/apt/sources.list")

{-
aptDist :: AptImage -> String
aptDist (Apt _ sources) = DistroCache.dist distro
-}

writeFileIfMissing :: Bool -> FilePath -> String -> IO ()
writeFileIfMissing mkdirs path text =
    do
      exists <- doesFileExist path
      case exists of
        False ->
            do
              if mkdirs then
                  createDirectoryIfMissing True (parentPath path) else
                  return ()
              writeFile path text
        True ->
            return ()

parentPath :: FilePath -> FilePath
parentPath path = fst (splitFileName path)

-- |The mighty consperse function
consperse :: [a] -> [[a]] -> [a]
consperse sep items = concat (intersperse sep items)

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