debian (empty) → 1.0
raw patch · 37 files changed
+4850/−0 lines, 37 filesdep +Unixutilsdep +basedep +mtlbuild-type:Customsetup-changed
Dependencies added: Unixutils, base, mtl, network, parsec, regex-compat, unix
Files
- Linspire/Debian/AptImage.hs +191/−0
- Linspire/Debian/ChangeLog.hs +105/−0
- Linspire/Debian/Config.hs +324/−0
- Linspire/Debian/Control.hs +29/−0
- Linspire/Debian/Control/ByteString.hs +340/−0
- Linspire/Debian/Control/Common.hs +88/−0
- Linspire/Debian/Control/PrettyPrint.hs +27/−0
- Linspire/Debian/Control/String.hs +110/−0
- Linspire/Debian/Dependencies.hs +270/−0
- Linspire/Debian/DistroCache.hs +125/−0
- Linspire/Debian/OSImage.hs +488/−0
- Linspire/Debian/Package.hs +54/−0
- Linspire/Debian/PackageDeprecated.hs +131/−0
- Linspire/Debian/Relation.hs +25/−0
- Linspire/Debian/Relation/ByteString.hs +41/−0
- Linspire/Debian/Relation/Common.hs +86/−0
- Linspire/Debian/Relation/String.hs +111/−0
- Linspire/Debian/Repository.hs +879/−0
- Linspire/Debian/SourceTree.hs +260/−0
- Linspire/Debian/SourcesList.hs +201/−0
- Linspire/Debian/Version.hs +17/−0
- Linspire/Debian/Version/ByteString.hs +17/−0
- Linspire/Debian/Version/Common.hs +168/−0
- Linspire/Debian/Version/Internal.hs +33/−0
- Linspire/Debian/Version/String.hs +14/−0
- Setup.hs +21/−0
- debian.cabal +45/−0
- debian/changelog +139/−0
- debian/compat +1/−0
- debian/control +43/−0
- debian/copyright +26/−0
- debian/haskell-debian-doc.docs +1/−0
- debian/rules +91/−0
- tests/Dependencies.hs +180/−0
- tests/Main.hs +14/−0
- tests/SourcesList.hs +44/−0
- tests/Versions.hs +111/−0
+ Linspire/Debian/AptImage.hs view
@@ -0,0 +1,191 @@+-- |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
+ Linspire/Debian/ChangeLog.hs view
@@ -0,0 +1,105 @@+-- |Module for handling the Debian changelog file format+-- <http://www.debian.org/doc/debian-policy/ch-source.html#s-dpkgchangelog>+--+-- Author: David Fox+{-+ |package (version) distribution(s); urgency=urgency+ | [optional blank line(s), stripped]+ | * change details+ | more change details+ | [blank line(s), included in output of dpkg-parsechangelog]+ | * even more change details+ | [optional blank line(s), stripped]+ | -- maintainer name <email address>[two spaces] date+-}+module Linspire.Debian.ChangeLog+ (ChangeLogEntry(Entry),+ -- * Accessors+ package,+ Linspire.Debian.ChangeLog.version,+ dists,+ urgency,+ comments,+ who,+ date,+ -- * read, show+ parse, -- String -> [ChangeLogEntry]+ showHeader, -- ChangeLogEntry -> String+ -- * Helper+ getRFC822Date -- IO String+ ) where++import Data.List+import Data.Maybe+import Linspire.Debian.Version+import System.IO+import System.Process+import Text.Regex++-- |A changelog is a series of ChangeLogEntries+data ChangeLogEntry = Entry {package :: String, -- Package name+ version :: DebianVersion, -- Version number+ dists :: [String], -- distributions+ urgency :: String, -- urgency+ comments :: String, -- comments+ who :: String, -- who+ date :: String} -- date++instance Show ChangeLogEntry where+ show (Entry package version dists urgency details who date) =+ package ++ " (" ++ show version ++ ") " ++ consperse " " dists ++ "; urgency=" ++ urgency ++ "\n\n" +++ details ++ "\n\n -- " ++ who ++ " " ++ date ++ "\n\n"++-- |Show just the top line of a changelog entry (for debugging output.)+showHeader :: ChangeLogEntry -> String+showHeader (Entry package version dists urgency _ _ _) =+ package ++ " (" ++ show version ++ ") " ++ consperse " " dists ++ "; urgency=" ++ urgency ++ "..."++-- |Parse a Debian Changelog and return a list of entries+parse :: String -> [ChangeLogEntry]+parse text =+ case matchRegexAll entryRE text of+ Nothing -> []+ Just (_, _, remaining, submatches) ->+ case submatches of+ [name, version, dists, urgency, _, _, details, _, _, _, _, who, date, _] ->+ let entry =+ Entry name + (parseDebianVersion version)+ (words dists)+ urgency+ details+ who+ date in+ entry : parse remaining+ _ -> error "Parse error in changelog"+ where+ entryRE = mkRegex (header ++ blankLines ++ details ++ blankLines ++ signature)++ header = package ++ version ++ dists ++ urgency++ package = "^([^ \t(]*)" ++ optWhite+ version = "\\(([^)]*)\\)" ++ optWhite+ dists = "([^;]*);" ++ optWhite+ urgency = "urgency=([^\n]*)\n" ++ blankLines+ details = "(" ++ prefixedLine ++ "(" ++ blankLines ++ prefixedLine ++ ")*)"+ signature = "( -- ([^\n]*) (...............................))[ \t]*\n" ++ blankLines++ prefixedLine = " [^\n]*\n"+ blankLines = "(" ++ optWhite ++ "\n)*"+ optWhite = "[ \t]*"++-- FIXME: The %z formatting directive (rfc-822 conformant timezone)+-- isn't working:+-- date = getClockTime >>= toCalendarTime >>=+-- return . formatCalendarTime defaultTimeLocale "%a, %d %b %Y %T %z"+getRFC822Date :: IO String+getRFC822Date =+ do (_, stdout, _, handle) <- runInteractiveCommand "date -R"+ date <- hGetContents stdout >>= return . lines >>= return . listToMaybe+ waitForProcess handle+ return $ maybe (error "Error running 'date -R'") id date++-- |The mighty consperse function+consperse :: [a] -> [[a]] -> [a]+consperse sep items = concat (intersperse sep items)
+ Linspire/Debian/Config.hs view
@@ -0,0 +1,324 @@+-- |The 'Config' module implements both command line and configuration+-- file option handling. It converts the command line parameters into+-- Flag objects, and then expands any Include, Use, or Config flags.+-- Every command line parameter has an equivalent format that can+-- appear in the configuration file. Using Debian conventions, a+-- command line option such as+--+-- --some-option=value+--+-- could appear in a configuration using this syntax:+--+-- Some-Option: value+--+-- This is the format used in Debian Control files, and is similar to+-- the format described in RFC-922. Note that a value in a control+-- file can be continued onto multiple lines by prefixing the extra+-- lines with whitespace, as described here:+-- <http://www.debian.org/doc/debian-policy/ch-controlfields.html>+--+-- See the documentation of the Flag datatype below for a description+-- of the features this module supports.+--+-- Author: David Fox <david.fox@linspire.com>+module Linspire.Debian.Config+ (Flag(Use, Name, Include, Param),+ computeConfig, -- String -> [OptDescr Flag] -> Maybe String -> IO [[Flag]]+ findParams, -- [Flag] -> String -> [String]+ findParam, -- [Flag] -> String -> Maybe String+ findBool) -- [Flag] -> String -> Bool+ where++import Control.Exception+import Control.Monad+import Data.Char(isDigit)+import Data.List+import Data.Maybe+import System.Directory+import System.Environment as Environment+import System.Console.GetOpt+import Text.Regex+import Linspire.Debian.Control++-- |The command line arguments are converted to a list of 'Flag'+-- objects, and this list is then expanded by the 'computeConfig'+-- function and the result (due to the operation of 'Use') is a list+-- of Flag lists.+data Flag+ = Include FilePath |+ -- ^ Any --include or --config flag is converted to an Include+ -- object, where the value is a filename containing+ -- configuration information. If no --config option is given,+ -- default locations will be used as described in the+ -- 'computeConfig' function.+ Name String |+ -- ^ This option has no command line equivalent, it is used to+ -- assign a name to a paragraph which can be referred to by a+ -- Use flag.+ Use [String] |+ -- ^ The --use flag is used to refer to one or more named paragraphs.+ -- Referring to a single named paragraph causes the parameters in that+ -- paragraph to be added to the set of parameters we are computing.+ -- If several paragraphs are named, a copy the current set of parameters+ -- is created for each named paragraph, and the result is several sets+ -- of parameters. This is what causes the result of the computeConfig+ -- function to be a list of Flag lists.+ Param String String+ -- ^ Any other named parameter (No command line equivalent)+ deriving Eq++-- |Display a flag in (pseudo) RFC-922 format.+instance Show Flag where+ show (Include x) = "Include: " ++ x+ show (Name x) = "Name: " ++ x+ show (Use xs) = "Use: " ++ consperse " " xs+ show (Param name value) = name ++ ": " ++ value++instance Read Flag where+ readsPrec _ =+ let re = mkRegex "^([^ \t:]+):([^\n]*(\n[ \t].*)*)($|\n)" in+ (\ s ->+ case matchRegexAll re s of+ Just (_, _, after, [name, value, _, _]) ->+ case name of+ "Include" -> [(Include (stripWS value), after)]+ "Config" -> [(Include (stripWS value), after)]+ "Name" -> [(Name (stripWS value), after)]+ "Use" -> [(Use (words value), after)]+ _ -> [(Param (stripWS name) (stripWS value), after)]+ _ -> error ("Parse error reading flag: '" ++ s ++ "'"))++-- |Find the configuration file or directory we will use, if any.+-- This is the first that exists among those that are mentioned on the+-- command line, the one in $HOME, and the one in \/etc.+configPath :: String -> [Flag] -> IO [FilePath]+configPath appName flags =+ do+ home <- try (getEnv "HOME") >>=+ return . either+ (\ _ -> [])+ (\ path -> [path ++ "/." ++ appName ++ ".d",+ path ++ "/." ++ appName ++ ".conf"])+ let etc = ["/etc/" ++ appName ++ ".d", "/etc/" ++ appName ++ ".conf"]+ -- FIXME: If $HOME/.appname is a directory, look inside for config+ -- FIXME: If $HOME/.appname.d exists, read all the files starting with+ -- digits that don't look like backup files. Same for /etc/appname+ -- and /etc/appname.d.+ let candidates = (findConfigs flags ++ home ++ etc)+ existant <- filterM exists candidates+ return $ maybeToList (listToMaybe existant)+ where+ exists path = do+ fileExists <- doesFileExist path+ dirExists <- doesDirectoryExist path+ return (fileExists || dirExists)++-- |Find the value of the --include and (synonymous) --config flags+findConfigs :: [Flag] -> [FilePath]+findConfigs [] = []+findConfigs (Include path : etc) = path : findConfigs etc+findConfigs (_ : etc) = findConfigs etc++-- |Return the configuration information computed from the command+-- line and the configuration files. Each list of flags represents a+-- paragraph in the control file, which may be named using the Name:+-- flag and referred to using the Use: flag.+computeConfig :: String -> [OptDescr Flag] -> Maybe String -> IO [[Flag]]+computeConfig appName options global =+ do+ args <- Environment.getArgs+ -- Convert the command line arguments to flags. Any arguments+ -- not recognized by getOpt is treated as implicit "--use"+ -- parameter.+ commandLineFlags <-+ case getOpt Permute customizedOptions args of+ (o, [], []) -> return o+ (o, extra, []) -> return (o ++ [Use extra])+ (_, _, errs) -> ioError (userError (concat errs ++ usageInfo header customizedOptions))+ -- Compute the configuration file path and then load and expand it.+ configFlags <- configPath appName commandLineFlags >>= tryPaths >>= expandIncludes+ -- Compute the set of global flags.+ let globalFlags =+ case (configFlags, global) of+ -- No configuration info was found+ ([], _) -> []+ -- Use the first section of the configuration file+ (section : _, Nothing) -> section+ -- Use a named section of the configuration file+ (sections, Just name) -> maybe (head sections) id (find (elem (Name name)) sections)+ -- ePut ("configFlags: " ++ show configFlags)+ -- ePut ("commandLineFlags: " ++ show commandLineFlags)+ sequence <- expandSections [commandLineFlags ++ globalFlags] configFlags+ return sequence+ where+ customizedOptions = optSpecs appName options+ header = "Usage: " ++ appName ++ " [OPTION...]"++-- Load a list of configuration files.+tryPaths :: [FilePath] -> IO [[Flag]]+tryPaths paths = do+ -- My.ePut ("tryPaths " ++ show paths ++ "\n")+ flags <- mapM tryPath paths >>= return . mergeControls >>= return . flagsOfControl+ return flags+ where+ -- Each paragraph of the control file becomes a list of flags+ flagsOfControl (Control paragraphs) = map (\ (Paragraph fields) -> (map flagOfField fields)) paragraphs+ flagOfField (Field (name, value)) = read (name ++ ": " ++ value)+ tryPath path =+ do+ isDir <- doesDirectoryExist path+ case isDir of+ False -> do+ try (parseControlFromFile path) >>=+ either (\ _ -> return (Right (Control []))) return >>=+ either (\ _ -> return (Control [])) return+ True -> do+ getDirectoryContents path >>=+ return . map ((path ++ "/") ++) . sort . filter isConfigPart >>=+ mapM tryPath >>=+ return . mergeControls+ isConfigPart "" = False+ isConfigPart s | isDigit (head s) && head (reverse s) /= '~' = True+ isConfigPart _ = False+ ++-- Load a list of configuration files.+expandIncludes :: [[Flag]] -> IO [[Flag]]+expandIncludes flags =+ do+ -- ePut ("flags: " ++ show flags)+ let paths = concat (map includePaths flags)+ -- ePut (concat (map (("Include: " ++) . (++ "\n")) paths))+ case paths of+ [] -> return flags+ _ ->+ do+ newflags <- tryPaths paths+ return (flags ++ newflags)+ where+ includePaths (Include path : etc) = path : includePaths etc+ includePaths (_ : etc) = includePaths etc+ includePaths [] = []++-- |Expand occurrences of --use in a list of flag lists.+expandSections :: [[Flag]] -> [[Flag]] -> IO [[Flag]]+expandSections toExpand expansions =+ do+ expanded <- mapM (expandSection [] expansions) toExpand+ return (concat expanded)+ where+ expandSection :: [String] -> [[Flag]] -> [Flag] -> IO [[Flag]]+ expandSection stack expansions toExpand =+ do+ -- ePut ("stack: " ++ show stack)+ -- ePut ("toExpand: " ++ show toExpand)+ -- ePut ("expansions: " ++ show expansions)+ let (useFlags, otherFlags) = partition isUse toExpand+ -- ePut ("useFlags: " ++ show useFlags)+ let sequences = map getNames useFlags+ -- ePut ("sequences: " ++ show sequences)+ -- A sequence of name lists+ let (sequence :: [[String]]) = cartesianProduct sequences+ -- ePut ("sequence: " ++ show sequence)+ -- map (elem stack) (concat sequence)+ let (newstack :: [String]) = nub $ stack ++ concat sequence+ case filter (flip elem $ stack) (concat sequence) of+ [] -> return ()+ l -> error ("Use loop: " ++ show (nub l))+ -- ePut ("newstack: " ++ show newstack)+ -- A sequence of flag lists+ let (sequence' :: [[[Flag]]]) = map (expandNames expansions stack) sequence+ -- ePut ("sequence': " ++ show sequence')+ case sequence' of+ [] ->+ return [otherFlags]+ _ ->+ do+ let sequence'' = map (++ otherFlags) (map concat sequence')+ -- ePut ("sequence'': " ++ show sequence'')+ result <- mapM (expandSection newstack expansions) sequence''+ -- ePut ("result: " ++ show result)+ return (concat result)++ isUse (Use _) = True+ isUse _ = False+ getNames (Use names) = names+ getNames _ = []++ -- FIXME: use the stack to prevent infinite recursion+ expandNames expansions stack names =+ map (expandName expansions stack) names+ expandName expansions stack name =+ maybe (error ("Section '" ++ name ++ "' not found.")) id (find (elem (Name name)) expansions)++-- |Command line option specifications. The caller passes in a list of+-- options, and three additional standard options are added here:+-- --config <path> - specify the path to a configuration file+-- --include <path> - pull in options from a file+-- --use 'name1 name2 ...' - pull in some named sections+optSpecs :: String -> [OptDescr Flag] -> [OptDescr Flag]+optSpecs appName specs =+ specs +++ [Option ['c'] ["config","include"] (ReqArg Include "PATH")+ (consperse "\n"+ ["Location of additional configuration files or directories.",+ "This option may be given multiple times, but only the first",+ "one that exists is use. In addition, if none exist or none",+ "are supplied, four additional paths are tried, in this order:",+ "'/etc/" ++ appName ++ ".d', '/etc/" ++ appName ++ ".conf', '$HOME/." ++ appName ++ ".d',",+ "and '$HOME/." ++ appName ++ ".conf'. If the configuration path",+ "specifies a directory all the files in the directory that begin",+ "with digits are read in lexical order and merged. If it is a",+ "regular file, it is read and the result is used."]),+ Option [] ["use"] (ReqArg (Use . words) "NAME")+ (consperse "\n"+ ["Used to refer to a sequence of named section of the configuration",+ "file. The parameters defined in the section with the line",+ "'Name: NAME' will be substituted for this argument. Each of the",+ "elements will be expanded and executed in sequence."])]++-- |Return all values of a string paramter in a flag list.+findParams :: [Flag] -> String -> [String]+findParams (Param name value : etc) sought | name == sought = value : (findParams etc sought)+findParams (_ : etc) sought = findParams etc sought+findParams [] _ = []++-- |Return the value of a string paramter in a flag list.+findParam :: [Flag] -> String -> Maybe String+findParam (Param name value : _) sought | name == sought = Just value+findParam (_ : etc) sought = findParam etc sought+findParam [] _ = Nothing++-- |Return the value of a boolean paramter in a flag list.+findBool :: [Flag] -> String -> Bool+findBool flags sought = maybe False (\ _ -> True) (findParam flags sought)++-- |The mighty consperse function+consperse :: [a] -> [[a]] -> [a]+consperse sep items = concat (intersperse sep items)++-- |cartesianProduct [[1,2,3], [4,5],[6]] -> [[1,4,6],[1,5,6],[2,4,6],[2,5,6],[3,4,6],[3,5,6]]+cartesianProduct [] = []+cartesianProduct [xs] = map (: []) xs+cartesianProduct (xs : yss) =+ distribute xs (cartesianProduct yss)+ where distribute xs yss = concat (map (\ x -> map (x :) yss) xs)++-- Example:+--+-- let (optSpecs :: [OptDescr Flag]) =+-- [Option [] ["verbose"] (ReqArg (Param "Verbose") "NUMBER")+-- "Specify the amount of debugging output",+-- Option ['n'] ["dry-run"] (NoArg (Param "Dry-Run" "yes"))+-- "Test run, don't modify the repository."]+-- flags <- computeConfig "myapp" optSpecs Nothing >>= return . head+-- let dryRun = findBool flags "Dry-Run"+-- verbose = maybe 0 read (findParam flags "Verbose")+--++-- When this is executed, it will load either the configuration file+-- ~/.myapp.conf, /etc/myapp.conf, or some configuration file+-- specified using the --config command line app. The top section of+-- the configuration file will be merged with the command line flags.+-- Then those flags are expanded using the rules described in the+-- definition of the Flag datatype, and the result is returned.
+ Linspire/Debian/Control.hs view
@@ -0,0 +1,29 @@+-- |A module for working with Debian control files <http://www.debian.org/doc/debian-policy/ch-controlfields.html>+module Linspire.Debian.Control + ( -- * Types+ Control'(..)+ , Paragraph'(..)+ , Field'(..)+ , Control+ , Paragraph+ , Field+ , ControlParser+ , ControlFunctions(..)+ -- * Control File Parser+ , pControl+ -- * Helper Functions+ , mergeControls+ , fieldValue+ , removeField+ , prependFields+ , appendFields+ , renameField+ , modifyField+ , raiseFields+ ) where++import Control.Monad+import Data.List+import Text.ParserCombinators.Parsec+import System.IO+import Linspire.Debian.Control.String
+ Linspire/Debian/Control/ByteString.hs view
@@ -0,0 +1,340 @@+module Linspire.Debian.Control.ByteString+ ( Control'(..)+ , Paragraph'(..)+ , Field'(..)+ , Control+ , Paragraph+ , Field+ , ControlFunctions(..)+ -- * Helper Functions+ , mergeControls+ , fieldValue+ , removeField+ , prependFields+ , appendFields+ , renameField+ , modifyField+ , raiseFields+ ) where++-- Standard GHC modules++import Control.Monad.State++import Data.Char(chr,ord)+import Data.List+import Data.Maybe+import Data.Word++import Foreign.C.String (CString, CStringLen)+import Foreign.C.Types (CSize)+import Foreign.ForeignPtr+import Foreign.Marshal.Array+import Foreign.Ptr+import Foreign.Storable (Storable(..))++import System.IO.Unsafe+import System.Environment++import Text.ParserCombinators.Parsec.Error+import Text.ParserCombinators.Parsec.Pos++-- Third Party Modules++import qualified Data.ByteString as B+import qualified Data.ByteString.Base as BB+import qualified Data.ByteString.Char8 as C++import Linspire.Debian.Control.Common++-- Local Modules++-- import ByteStreamParser++-- * Types+{-+newtype Control = Control [Paragraph]+newtype Paragraph = Paragraph [Field]+newtype Field = Field (C.ByteString, C.ByteString)+-}++type Control = Control' C.ByteString+type Paragraph = Paragraph' C.ByteString+type Field = Field' C.ByteString+-- * Control Parser++type ControlParser a = Parser C.ByteString a++pKey :: ControlParser C.ByteString+pKey = notEmpty $ pTakeWhile (\c -> (c /= ':') && (c /= '\n'))++pValue :: ControlParser C.ByteString+pValue = pTakeWhile2 (\a b -> not (endOfValue a b))+ where+ endOfValue :: Char -> Maybe Char -> Bool+ endOfValue '\n' Nothing = True+ endOfValue '\n' (Just ' ') = False+ endOfValue '\n' (Just '\t') = False+ endOfValue '\n' (Just '#') = False+ endOfValue '\n' _ = True+ endOfValue _ _ = False++pField :: ControlParser Field+pField =+ do k <- pKey+ pChar ':'+ v <- pValue+-- pChar '\n'+ (pChar '\n' >> return ()) <|> pEOF+ return (Field (k,v))++pComment :: ControlParser Field+pComment =+ do c1 <- pChar '#'+ text <- pTakeWhile2 (\ a b -> not (endOfComment a b))+ return . Comment $ (B.append (B.singleton . c2w $ c1) text)+ where+ endOfComment '\n' Nothing = True+ endOfComment '\n' (Just '#') = False+ endOfComment '\n' _ = True+ endOfComment _ _ = False++pParagraph :: ControlParser Paragraph+pParagraph = + do f <- pMany1 (pComment <|> pField)+ pSkipMany (pChar '\n')+ return (Paragraph f)++pControl :: ControlParser Control+pControl = + do pSkipMany (pChar '\n')+ c <- pMany pParagraph+ return (Control c)+++-- parseControlFromFile :: FilePath -> IO (Either String Control)++instance ControlFunctions C.ByteString where+ parseControlFromFile fp = + do c <- C.readFile fp+ case parse pControl c of+ Nothing -> return (Left (newErrorMessage (Message ("Failed to parse " ++ fp)) (newPos fp 0 0)))+ (Just (cntl,_)) -> return (Right cntl)+ parseControlFromHandle sourceName handle = + do c <- C.hGetContents handle+ case parse pControl c of+ Nothing -> return (Left (newErrorMessage (Message ("Failed to parse " ++ sourceName)) (newPos sourceName 0 0)))+ (Just (cntl,_)) -> return (Right cntl)+ lookupP fieldName (Paragraph fields) =+ let pFieldName = C.pack fieldName in+ find (\ (Field (fieldName',_)) -> fieldName' == pFieldName) fields+ -- NOTE: probably inefficient+ stripWS = C.reverse . strip . C.reverse . strip+ where strip = C.dropWhile (flip elem " \t")++{-+main = + do [fp] <- getArgs+ C.readFile fp >>= \c -> maybe (putStrLn "failed.") (print . length . fst) (parse pControl c)+-}+-- * Helper Functions++-- | 'takeWhile', applied to a predicate @p@ and a ByteString @xs@,+-- returns the longest prefix (possibly empty) of @xs@ of elements that+-- satisfy @p@.+takeWhile2 :: (Word8 -> Maybe Word8 -> Bool) -> B.ByteString -> B.ByteString+takeWhile2 f ps = BB.unsafeTake (findIndex2OrEnd (\w1 w2 -> not (f w1 w2)) ps) ps+{-# INLINE takeWhile2 #-}++break2 :: (Word8 -> Maybe Word8 -> Bool) -> B.ByteString -> Maybe (B.ByteString, B.ByteString)+break2 p ps = case findIndex2OrEnd p ps of n -> Just (BB.unsafeTake n ps, BB.unsafeDrop n ps)++span2 :: (Word8 -> Maybe Word8 -> Bool) -> B.ByteString -> Maybe (B.ByteString, B.ByteString)+span2 p ps = break2 (\a b -> not (p a b)) ps+++-- | 'findIndexOrEnd' is a variant of findIndex, that returns the length+-- of the string if no element is found, rather than Nothing.++findIndex2OrEnd :: (Word8 -> Maybe Word8 -> Bool) -> B.ByteString -> Int+findIndex2OrEnd k (BB.PS x s l) = unsafePerformIO $ withForeignPtr x $ \f -> go (f `plusPtr` s) 0+ where+ go a b | a `seq` b `seq` False = undefined+ go ptr n | n >= l = return l+ | otherwise = do w1 <- peek ptr+ w2 <- if (n + 1 < l) then (peek (ptr `plusPtr` 1) >>= return . Just) else return Nothing+ if k w1 w2+ then return n+ else go (ptr `plusPtr` 1) (n+1)+++{-+findIndex2OrEnd :: (Word8 -> Maybe Word8 -> Bool) -> B.ByteString -> Int+findIndex2OrEnd k (B.PS x s l) = unsafePerformIO $ withForeignPtr x $ \f -> go (f `plusPtr` s) 0+ where+ go a b | a `seq` b `seq` False = undefined+ go ptr n | n >= l = return l+ | otherwise = do w1 <- peek ptr+ case (w2c w1) of+ '\n' ->+ if (n + 1 < l)+ then do w2 <- peek (ptr `plusPtr` 1)+ case (w2c w2) of+ ' ' -> go (ptr `plusPtr` 2) (n + 2)+ _ -> return n+ else return l -- go (ptr `plusPtr` 1) (n + 1)+ _ -> go (ptr `plusPtr` 1) (n + 1)+-}+{-+ w2 <- if (n + 1 < l) then (peek (ptr `plusPtr` 1) >>= return . Just) else return Nothing+ if k w1 w2+ then return n+ else go (ptr `plusPtr` 1) (n+1)+-}+{-# INLINE findIndex2OrEnd #-}++-- | The 'findIndex' function takes a predicate and a 'ByteString' and+-- returns the index of the first element in the ByteString+-- satisfying the predicate.+findIndex2 :: (Word8 -> Maybe Word8 -> Bool) -> B.ByteString -> Maybe Int+findIndex2 k (BB.PS x s l) = unsafePerformIO $ withForeignPtr x $ \f -> go (f `plusPtr` s) 0+ where+ go a b | a `seq` b `seq` False = undefined+ go ptr n | n >= l = return Nothing+ | otherwise = do w1 <- peek ptr+ w2 <- if (n + 1 < l) then (peek (ptr `plusPtr` 1) >>= return . Just) else return Nothing+ if k w1 w2+ then return (Just n)+ else go (ptr `plusPtr` 1) (n+1)+{-# INLINE findIndex2 #-}++-- Copied from ByteStream because they are not exported++w2c :: Word8 -> Char+w2c = chr . fromIntegral++c2w :: Char -> Word8+c2w = fromIntegral . ord++-- * Parser++data Result a+ = Ok a+ | Fail+ | Empty+ deriving Show++m2r :: Maybe a -> Result a+m2r (Just a) = Ok a+m2r Nothing = Empty ++r2m :: Result a -> Maybe a+r2m (Ok a) = Just a+r2m _ = Nothing++newtype Parser state a = Parser { unParser :: (state -> Result (a, state)) }++instance Monad (Parser state) where+ return a = Parser (\s -> Ok (a,s))+ m >>= f =+ Parser $ \state ->+ let r = (unParser m) state in+ case r of+ Ok (a,state') -> + case unParser (f a) $ state' of+ Empty -> Fail+ o -> o+ Empty -> Empty+ Fail -> Fail++instance MonadPlus (Parser state) where+ mzero = Parser (const Empty)+ mplus (Parser p1) (Parser p2) =+ Parser (\s -> case p1 s of+ Empty -> p2 s+ o -> o+ )+ +-- Parser (\s -> maybe (p2 s) (Just) (p1 s))+++pSucceed :: a -> Parser state a+pSucceed = return++pFail :: Parser state a+pFail = Parser (const Empty)+++(<|>) :: Parser state a -> Parser state a -> Parser state a+(<|>) = mplus+++satisfy :: (Char -> Bool) -> Parser C.ByteString Char+satisfy f =+ Parser $ \bs ->+ if C.null bs+ then Empty+ else let (s,ss) = (C.head bs, C.tail bs) in+ if (f s)+ then Ok (s,ss)+ else Empty++pChar :: Char -> Parser C.ByteString Char+pChar c = satisfy ((==) c)+++try :: Parser state a -> Parser state a+try (Parser p) =+ Parser $ \bs -> case (p bs) of+ Fail -> Empty+ o -> o++pEOF :: Parser C.ByteString ()+pEOF =+ Parser $ \bs -> if C.null bs then Ok ((),bs) else Empty++pTakeWhile2 :: (Char -> Maybe Char -> Bool) -> Parser C.ByteString C.ByteString+pTakeWhile2 f =+ Parser $ \bs -> m2r (span2 (\w1 w2 -> f (w2c w1) (fmap w2c w2)) bs)++pTakeWhile :: (Char -> Bool) -> Parser C.ByteString C.ByteString+pTakeWhile f =+ Parser $ \bs -> Ok (B.span (\w -> f (w2c w)) bs)++pSkipWhile :: (Char -> Bool) -> Parser C.ByteString ()+pSkipWhile p =+ Parser $ \bs -> Ok ((), C.dropWhile p bs)++pMany :: Parser st a -> Parser st [a]+pMany p + = scan id+ where+ scan f = do x <- p+ scan (\tail -> f (x:tail))+ <|> return (f [])++notEmpty :: Parser st C.ByteString -> Parser st C.ByteString +notEmpty (Parser p) =+ Parser $ \s -> case p s of+ o@(Ok (a, s)) ->+ if C.null a+ then Empty+ else o+ x -> x++pMany1 :: Parser st a -> Parser st [a]+pMany1 p =+ do x <- p+ xs <- pMany p+ return (x:xs)++pSkipMany :: Parser st a -> Parser st ()+pSkipMany p = scan+ where+ scan = (p >> scan) <|> return ()+ +pSkipMany1 :: Parser st a -> Parser st ()+pSkipMany1 p = p >> pSkipMany p++parse :: Parser state a -> state -> Maybe (a, state)+parse p s = r2m ((unParser p) s)
+ Linspire/Debian/Control/Common.hs view
@@ -0,0 +1,88 @@+module Linspire.Debian.Control.Common+ ( -- * Types+ Control'(..)+ , Paragraph'(..)+ , Field'(..)+ , ControlFunctions(..)+ , mergeControls+ , fieldValue+ , removeField+ , prependFields+ , appendFields+ , renameField+ , modifyField+ , raiseFields,+ )+ where++import Text.ParserCombinators.Parsec+import System.IO+import Data.List++newtype Control' a+ = Control { unControl :: [Paragraph' a] }++newtype Paragraph' a+ = Paragraph [Field' a]++-- |NOTE: we do not strip the leading or trailing whitespace in the+-- name or value+data Field' a+ = Field (a, a)+ | Comment a+ deriving Eq++class ControlFunctions a where+ -- |'parseControlFromFile' @filepath@ is a simple wrapper function+ -- that parses @filepath@ using 'pControl'+ parseControlFromFile :: FilePath -> IO (Either ParseError (Control' a))+ -- |'parseControlFromHandle' @sourceName@ @handle@ - @sourceName@ is only used for error reporting+ parseControlFromHandle :: String -> Handle -> IO (Either ParseError (Control' a))+ -- | 'lookupP' @fieldName paragraph@ looks up a 'Field' in a 'Paragraph'.+ -- @N.B.@ trailing and leading whitespace is /not/ stripped.+ lookupP :: String -> (Paragraph' a) -> Maybe (Field' a)+ -- |Strip the trailing and leading space and tab characters from a+ -- string. Folded whitespace is /not/ unfolded. This should probably+ -- be moved to someplace more general purpose.+ stripWS :: a -> a++mergeControls :: [Control' a] -> Control' a+mergeControls controls =+ Control (concatMap unControl controls)++fieldValue :: (ControlFunctions a) => String -> Paragraph' a -> Maybe a+fieldValue fieldName paragraph =+ fmap value (lookupP fieldName paragraph)+ where value (Field (_, val)) = stripWS val++removeField :: (Eq a) => a -> Paragraph' a -> Paragraph' a+removeField toRemove (Paragraph fields) =+ Paragraph (filter remove fields)+ where+ remove (Field (name,_)) = name == toRemove++prependFields :: [Field' a] -> Paragraph' a -> Paragraph' a+prependFields newfields (Paragraph fields) = Paragraph (newfields ++ fields)++appendFields :: [Field' a] -> Paragraph' a -> Paragraph' a+appendFields newfields (Paragraph fields) = Paragraph (fields ++ newfields)++renameField :: (Eq a) => a -> a -> Paragraph' a -> Paragraph' a+renameField oldname newname (Paragraph fields) =+ Paragraph (map rename fields)+ where+ rename (Field (name, value)) | name == oldname = Field (newname, value)+ rename field = field++modifyField :: (Eq a) => a -> (a -> a) -> Paragraph' a -> Paragraph' a+modifyField name f (Paragraph fields) =+ Paragraph (map modify fields)+ where+ modify (Field (name', value)) | name' == name = Field (name, f value)+ modify field = field++raiseFields :: (Eq a) => (a -> Bool) -> Paragraph' a -> Paragraph' a+-- ^ Move selected fields to the beginning of a paragraph.+raiseFields f (Paragraph fields) =+ let (a, b) = partition f' fields in Paragraph (a ++ b)+ where f' (Field (name, _)) = f name
+ Linspire/Debian/Control/PrettyPrint.hs view
@@ -0,0 +1,27 @@+module Linspire.Debian.Control.PrettyPrint where++import qualified Data.ByteString.Char8 as C+import Text.PrettyPrint.HughesPJ++import Linspire.Debian.Control.Common++ppControl :: (ToText a) => Control' a -> Doc+ppControl (Control paragraph) =+ fsep (map ppParagraph paragraph)++ppParagraph :: (ToText a) => Paragraph' a -> Doc+ppParagraph (Paragraph fields) =+ fsep (map ppField fields)++ppField :: (ToText a) => Field' a -> Doc+ppField (Field (n,v)) = totext n <> text ":" <> totext v+++class ToText a where+ totext :: a -> Doc++instance ToText String where+ totext = text++instance ToText C.ByteString where+ totext = text . C.unpack
+ Linspire/Debian/Control/String.hs view
@@ -0,0 +1,110 @@+module Linspire.Debian.Control.String+ ( -- * Types+ Control'(..)+ , Paragraph'(..)+ , Field'(..)+ , Control+ , Paragraph+ , Field+ , ControlParser+ , ControlFunctions(..)+ -- * Control File Parser+ , pControl+ -- * Helper Functions+ , mergeControls+ , fieldValue+ , removeField+ , prependFields+ , appendFields+ , renameField+ , modifyField+ , raiseFields+ ) where++import Control.Monad+import Data.List+import Text.ParserCombinators.Parsec+import System.IO+import Linspire.Debian.Control.Common++-- |This may have bad performance issues +instance Show (Control' String) where+ show (Control paragraph) = concat (intersperse "\n" (map show paragraph))++instance Show (Paragraph' String) where+ show (Paragraph fields) = unlines (map show fields)++instance Show (Field' String) where+ show (Field (name,value)) = name ++":"++ value+ show (Comment text) = text++type Field = Field' String+type Control = Control' String+type Paragraph = Paragraph' String++-- * ControlFunctions++instance ControlFunctions String where+ parseControlFromFile filepath = + parseFromFile pControl filepath + parseControlFromHandle sourceName handle = + hGetContents handle >>= return . parse pControl sourceName+ lookupP fieldName (Paragraph paragraph) = + find hasFieldName paragraph+ where hasFieldName (Field (fieldName',_)) = fieldName == fieldName'+ hasFieldName _ = False+ stripWS = reverse . strip . reverse . strip+ where strip = dropWhile (flip elem " \t")++-- * Control File Parser++type ControlParser a = CharParser () a++-- |A parser for debian control file. This parser handles control files+-- that end without a newline as well as ones that have several blank+-- lines at the end. It is very liberal and does not attempt validate+-- the fields in any way. All trailing, leading, and folded whitespace+-- is preserved in the field values. See 'stripWS'.+pControl :: ControlParser Control+pControl =+ do many $ char '\n'+ sepEndBy pParagraph pBlanks >>= return . Control++pParagraph :: ControlParser Paragraph+pParagraph = many1 (pComment <|> pField) >>= return . Paragraph++-- |We are liberal in that we allow *any* field to have folded white+-- space, even though the specific restricts that to a few fields.+pField :: ControlParser Field+pField =+ do c1 <- noneOf "#\n"+ fieldName <- many1 $ noneOf ":\n"+ char ':'+ fieldValue <- many fcharfws+ (char '\n' >> return ()) <|> eof+ return $ Field (c1 : fieldName, fieldValue)++pComment :: ControlParser Field+pComment =+ do char '#'+ text <- many (satisfy (not . ((==) '\n')))+ char '\n'+ return $ Comment ("#" ++ text ++ "\n")++fcharfws :: ControlParser Char+fcharfws = fchar <|> (try $ lookAhead (string "\n ") >> char '\n') <|> (try $ lookAhead (string "\n\t") >> char '\n') <|> (try $ lookAhead (string "\n#") >> char '\n')++fchar :: ControlParser Char+fchar = satisfy (/='\n')++fws :: ControlParser String+fws =+ try $ do char '\n'+ ws <- many1 (char ' ')+ c <- many1 (satisfy (not . ((==) '\n')))+ return $ '\n' : (ws ++ c)++-- |We go with the assumption that 'blank lines' mean lines that+-- consist of entirely of zero or more whitespace characters.+pBlanks :: ControlParser String+pBlanks = many1 (oneOf " \n")
+ Linspire/Debian/Dependencies.hs view
@@ -0,0 +1,270 @@+module Linspire.Debian.Dependencies+{-+ ( solve+ , State + , binaryDepends+ , search+ , bj'+ , bt+ , CSP(..)+ ) -} where++import Data.List+import Data.Maybe+import Data.Tree+import qualified Data.Map as Map++import qualified Data.ByteString.Char8 as BS+import qualified Data.ByteString.Char8 as C++import Linspire.Debian.Control.ByteString+import Linspire.Debian.Control.PrettyPrint+-- import qualified Linspire.Debian.Control as SC+import Linspire.Debian.Relation.ByteString+import Linspire.Debian.Package+import Linspire.Debian.Version++-- * Basic CSP Types and Functions++data Status + = Remaining AndRelation+ | MissingDep Relation+ | Complete+ deriving (Show, Eq)++type State a = (Status, [a])++complete :: State a -> Bool+complete (Complete, _) = True+complete _ = False++data CSP a+ = CSP { pnm :: PackageNameMap a+ , relations :: Relations+ , depFunction :: (a -> Relations)+ , conflicts :: a -> Relations+ , packageVersion :: a -> (String, DebianVersion)+ }++-- * Test CSP++-- |TODO addProvides -- see DQL.Exec+controlCSP :: Control -> Relations -> (Paragraph -> Relations) -> CSP Paragraph+controlCSP (Control paragraphs) rels depF =+ CSP { pnm = packageNameMap getName paragraphs+ , relations = rels+ , depFunction = depF+ , conflicts = conflicts'+ , packageVersion = packageVersionParagraph+ }+ where+ getName :: Paragraph -> String+ getName p = case lookupP "Package" p of Nothing -> error "Missing Package field" ; (Just (Field (_,n))) -> C.unpack (stripWS n)+ conflicts' :: Paragraph -> Relations+ conflicts' p = + case lookupP "Conflicts" p of+ Nothing -> []+ Just (Field (_, c)) -> either (error . show) id (parseRelations c)++testCSP :: FilePath -> (Paragraph -> Relations) -> String -> (CSP Paragraph -> IO a) -> IO a+testCSP controlFile depf relationStr cspf =+ do c' <- parseControlFromFile controlFile+ case c' of+ Left e -> error (show e)+ Right control@(Control paragraphs) ->+ case parseRelations relationStr of+ Left e -> error (show e)+ Right r ->+ cspf (controlCSP control r depf)++depF :: Paragraph -> Relations+depF p =+ let preDepends =+ case lookupP "Pre-Depends" p of+ Nothing -> []+ Just (Field (_,pd)) -> + either (error . show) id (parseRelations pd)+ depends =+ case lookupP "Depends" p of+ Nothing -> []+ Just (Field (_,pd)) -> + either (error . show) id (parseRelations pd)+ in+ preDepends ++ depends++sidPackages = "/var/lib/apt/lists/ftp.debian.org_debian_dists_unstable_main_binary-i386_Packages"++test rel labeler =+ testCSP sidPackages depF rel (mapM_ (\ (_,p) -> mapM_ (print . packageVersionParagraph) p ) . take 1 . search labeler)++-- TODO: add better errors+packageVersionParagraph :: Paragraph -> (String, DebianVersion)+packageVersionParagraph p =+ case lookupP "Package" p of+ Nothing -> error $ "Paragraph missing Package field"+ (Just (Field (_, name))) ->+ case lookupP "Version" p of+ Nothing -> error $ "Paragraph missing Version field"+ (Just (Field (_, version))) -> (C.unpack (stripWS name), parseDebianVersion (C.unpack version))++++conflict :: CSP p -> p -> p -> Bool+conflict csp p1 p2 =+ let (name1, version1) = (packageVersion csp) p1+ (name2, version2) = (packageVersion csp) p2+ in+ if name1 == name2+ then version1 /= version2+ else+ any (conflict' (name1, version1)) (concat $ (conflicts csp) p2) || + any (conflict' (name2, version2)) (concat $ (conflicts csp) p1)+ +-- |JAS: deal with 'Provides' (can a package provide more than one package?)+conflict' :: (String, DebianVersion) -> Relation -> Bool+conflict' (pName, pVersion) rel@(Rel pkgName mVersionReq _) =+ (pName == pkgName) && (checkVersionReq mVersionReq (Just pVersion))++++-- * Tree Helper Functions++mkTree :: a -> [Tree a] -> Tree a+mkTree = Node++label :: Tree a -> a+label = rootLabel++initTree :: (a -> [a]) -> a -> Tree a+initTree f a = Node a (map (initTree f) (f a))++mapTree :: (a -> b) -> Tree a -> Tree b+mapTree = fmap++foldTree :: (a -> [b] -> b) -> Tree a -> b+foldTree f (Node a ts) = f a (map (foldTree f) ts)++zipTreesWith :: (a -> b -> c) -> Tree a -> Tree b -> Tree c+zipTreesWith f (Node a ts) (Node b us) =+ Node (f a b) (zipWith (zipTreesWith f) ts us)++prune :: (a -> Bool) -> Tree a -> Tree a+prune p = foldTree f+ where f a ts = Node a (filter (not . p . label) ts)++leaves :: Tree a -> [a]+leaves = foldTree f+ where f leaf [] = [leaf]+ f _ ts = concat ts++inhTree :: (b -> a -> b) -> b -> Tree a -> Tree b+inhTree f b (Node a ts) = Node b' (map (inhTree f b') ts)+ where b' = f b a++distrTree :: (a -> [b]) -> b -> Tree a -> Tree b+distrTree f b (Node a ts) = Node b (zipWith (distrTree f) (f a) ts)++-- * mkSearchTree++-- TODO: might want to leave markers about what relation we are satisfying?+mkSearchTree :: forall a. CSP a -> Tree (State a)+mkSearchTree csp =+ Node (Remaining (relations csp),[]) (andRelation ([],[]) (relations csp))+ where+ andRelation :: ([a],AndRelation) -> AndRelation -> [Tree (State a)]+ andRelation (candidates,[]) [] = [Node (Complete, candidates) []]+ andRelation (candidates,remaining) [] = andRelation (candidates, []) remaining+ andRelation (candidates, remaining) (or:ors) =+ orRelation (candidates, ors ++ remaining) or+ orRelation :: ([a],AndRelation) -> OrRelation -> [Tree (State a)]+ orRelation acc or =+ concat (fmap (relation acc) or)+ relation :: ([a],AndRelation) -> Relation -> [Tree (State a)]+ relation acc@(candidates,_) rel =+ let packages = lookupPackageByRel (pnm csp) (packageVersion csp) rel in+ case packages of+ [] -> [Node (MissingDep rel, candidates) []]+ _ -> map (package acc) packages+ package :: ([a],AndRelation) -> a -> Tree (State a)+ package (candidates, remaining) package =+ if ((packageVersion csp) package) `elem` (map (packageVersion csp) candidates)+ then if null remaining+ then Node (Complete, candidates) []+ else Node (Remaining remaining, candidates) (andRelation (candidates, []) remaining)+ else Node (Remaining remaining, (package : candidates)) (andRelation ((package : candidates), remaining) ((depFunction csp) package))+++-- |earliestInconsistency does what it sounds like+-- the 'reverse as' is because the vars are order high to low, but we+-- want to find the lowest numbered (aka, eariest) inconsistency ??+-- +earliestInconsistency :: CSP a -> State a -> Maybe ((String, DebianVersion), (String, DebianVersion))+earliestInconsistency _ (_,[]) = Nothing+earliestInconsistency _ (_,[p]) = Nothing+earliestInconsistency csp (_,(p:ps)) =+ case find ((conflict csp) p) (reverse ps) of+ Nothing -> Nothing+ (Just conflictingPackage) -> Just ((packageVersion csp) p, (packageVersion csp) conflictingPackage)++-- * Conflict Set++type ConflictSet = ([(String, DebianVersion)],[Relation]) -- ^ conflicting packages and relations that require non-existant packages++isConflict :: ConflictSet -> Bool+isConflict ([],[]) = False+isConflict _ = True++solutions :: Tree (State a, ConflictSet) -> [State a]+solutions = filter complete . map fst . leaves . prune (isConflict . snd)++type Labeler a = CSP a -> Tree (State a) -> Tree (State a, ConflictSet)++search :: Labeler a -> CSP a -> [State a]+search labeler csp = (solutions . (labeler csp) . mkSearchTree) csp++-- * Backtracking Labeler++bt :: Labeler a+bt csp = mapTree f+ where+ f s@(status,_) =+ case status of+ (MissingDep rel) -> (s, ([], [rel]))+ _ -> + (s,+ case (earliestInconsistency csp) s of+ Nothing -> ([],[])+ Just (a,b) -> ([a,b], []))++-- * BackJumping Solver++{-|bj - backjumping labeler++If the node already has a conflict set, then leave it alone.++Otherwise, the conflictset for the node is the combination of the+conflict sets of its direct children.+-}+bj :: CSP p -> Tree (State p, ConflictSet) -> Tree (State p, ConflictSet)+bj csp = foldTree f+ where f (s, cs) ts+ | isConflict cs = mkTree (s, cs) ts+-- | isConflict cs' = mkTree (s, cs') [] -- prevent space leak+ | otherwise = mkTree (s, cs') ts+ where cs' = + let set = combine csp (map label ts) [] in+ set `seq` set -- prevent space leak++unionCS :: [ConflictSet] -> ConflictSet+unionCS css = foldr (\(c1, m1) (c2, m2) -> ((c1 `union` c2), (m1 `union` m2))) ([],[]) css++combine :: CSP p -> [(State p, ConflictSet)] -> [ConflictSet] -> ConflictSet+combine _ [] acc = unionCS acc+combine csp ((s,cs@(c,m)):ns) acc+ | (not (lastvar `elem` c)) && null m = cs+ | null c && null m = ([],[]) -- is this case ever used?+ | otherwise = combine csp ns ((c, m):acc)+ where lastvar = + let (_,(p:_)) = s in (packageVersion csp) p++
+ Linspire/Debian/DistroCache.hs view
@@ -0,0 +1,125 @@+-- |A DistroCache is a distribution (a list of DebSource) with a name+-- and a directory which it can use to download and store files. It+-- is the basis for several of the other Debian modules.+--+-- Author: David Fox <david.fox@linspire.com>+module Linspire.Debian.DistroCache+ (DistroCache,+ -- * Constructors+ distroFromConfig, -- FilePath -> String -> IO DistroCache+ distroFromConfig', -- FilePath -> String -> DistroCache+ makeDistro, -- FilePath -> String -> [DebSource] -> DistroCache+ -- * Accessors+ dist, -- DistroCache -> String+ aptDir, -- DistroCache -> String -> FilePath+ aptRootDir, -- DistroCache -> FilePath+ sources, -- DistroCache -> [DebSource],+ sourcesPath, -- DistroCache -> FilePath+ poolRoot, -- DistroCache -> FilePath+ localPool, -- DistroCache -> FilePath -> [DebSource]+ cleanRoot, -- DistroCache -> String -> FilePath+ dirtyRoot) -- DistroCache -> String -> FilePath+ where++import Control.Monad+import Data.List+import Data.Maybe+import System.Directory+import System.IO+import Text.Regex+import Linspire.Unix.FilePath+import Linspire.Debian.SourcesList++-- FIXME: we should have a flag to indicate whether we have+-- verified the files that distroFromConfig creates.+data DistroCache =+ Distro {top :: FilePath, -- ^ The autobuilder cache directory+ dist :: String, -- ^ Name of the distro.+ sources :: [DebSource]} -- ^ The contents of the sources.list file.++instance Eq DistroCache where+ Distro a b _ == Distro c d _ = a == c && b == d++distDir :: DistroCache -> FilePath+distDir (Distro top dist _) = top ++ "/dists/" ++ dist++-- |The directory where package's source deb is downloaded to.+aptDir :: DistroCache -> String -> FilePath+aptDir distro package = distDir distro ++ "/apt/" ++ package++-- |The partial environment for performing apt-gets+aptRootDir :: DistroCache -> FilePath+aptRootDir distro = distDir distro ++ "/aptEnv"++sourcesPath :: DistroCache -> FilePath+sourcesPath distro = distDir distro ++ "/sources"++localPool :: DistroCache -> FilePath -> [DebSource]+localPool distro dir = + parseSourcesList ("\ndeb file://" ++ dir ++ " " ++ dist distro ++ " main" +++ "\ndeb-src file://" ++ dir ++ " " ++ dist distro ++ " main\n")++poolRoot :: DistroCache -> FilePath+poolRoot distro = distDir distro ++ "/upload-env"++dirtyRoot :: DistroCache -> String -> FilePath+dirtyRoot distro strictness = distDir distro ++ "/build-" ++ strictness++cleanRoot :: DistroCache -> String -> FilePath+cleanRoot distro strictness = distDir distro ++ "/clean-" ++ strictness++-- |Given the text for a source list in the config file, return+-- a Distro object. This is an IO operation because we may+-- need to create the sources.list files.+distroFromConfig :: FilePath -> String -> IO DistroCache+distroFromConfig top text =+ do+ let distro@(Distro _ dist _) = distroFromConfig' top text+ let dir = distDir distro+ distExists <- doesFileExist (sourcesPath distro)+ case distExists of+ True ->+ do+ fileSources <- readFile (sourcesPath distro) >>= return . parseSourcesList+ case fileSources == (sources distro) of+ True -> return distro+ False ->+ do+ ePut ("Sources for '" ++ dist ++ "' don't match sources from the configuration file" +++ ":\n\n" ++ sourcesPath distro ++ ":\n\n" +++ consperse "\n" (map show fileSources) +++ "\n\nConfiguration file:\n\n" +++ consperse "\n" (map show (sources distro)) ++ "\n\n" +++ "This means it is possible that everything in\n" +++ dir ++ " is invalid.")+ -- removeRecursiveSafely dir+ error ("Please remove " ++ dir ++ " and restart.")+ False ->+ do+ createDirectoryIfMissing True dir+ writeFile (sourcesPath distro) (consperse "\n" (map show (sources distro)) ++ "\n")+ return distro++-- |Create Distro info from an entry in the config file, which+-- includes a dist name and the lines of the sources.list file.+-- This also creates the basic +distroFromConfig' :: FilePath -> String -> DistroCache+distroFromConfig' top text =+ -- FIXME: This regexp is too permissive - it will match almost anything+ let re = mkRegex "^[ \t\n]*([^ \t\n]+)[ \t\n]+(.*)$" in+ case matchRegex re text of+ Just [dist, sources] -> Distro top dist (parseSourcesList sources)+ _ -> error "Syntax error in sources text"++-- |This is unsafe in the sense that we don't know that the+-- directories have been created and the sources.list files+-- have been written.+makeDistro :: FilePath -> String -> [DebSource] -> DistroCache+makeDistro top dist sources = Distro top dist sources++ePut :: String -> IO ()+ePut s = hPutStrLn stderr s++-- |The mighty consperse function+consperse :: [a] -> [[a]] -> [a]+consperse sep items = concat (intersperse sep items)
+ Linspire/Debian/OSImage.hs view
@@ -0,0 +1,488 @@+-- |Module to handle Debian OS images, AKA build environments or change roots.+--+-- Author: David Fox <david.fox@linspire.com>+module Linspire.Debian.OSImage+ (OSImage, -- (export abstract to assure consistency)+ EnvRoot(EnvRoot),+ -- * Creation+ prepareEnv, -- [Style] -> EnvRoot -> DistroCache -> Maybe Repository -> Bool -> Bool -> IO OSImage+ syncEnv, -- [Style] -> OSImage -> OSImage -> IO TimeDiff+ chrootEnv, -- OSImage -> EnvRoot -> OSImage+ -- * Modification+ updateEnv, -- [Style] -> OSImage -> IO OSImage+ neuterEnv, -- [Style] -> OSImage -> IO OSImage+ restoreEnv, -- OSImage -> IO OSImage+ -- * Queries+ osRoot, -- OSImage -> EnvRoot+ osDistro,+ osSources, -- OSImage -> [DebSource]+ osArch, -- OSImage -> String+ osLocalRepository, -- OSImage -> Maybe Repository+ getSourceInfo, -- [Style] -> OSImage -> [PkgName] -> IO Control+ getBinaryInfo, -- [Style] -> OSImage -> [PkgName] -> IO Control+ getAvailable, -- [Style] -> OSImage -> [String] -> IO [Paragraph]+ buildEssential, -- OSImage -> Bool -> IO Relations+ buildArch, -- EnvRoot -> IO String+ -- * Removal+ removeEnv, -- OSImage -> IO ()+ -- * Helper+ chrootPool -- DistroCache -> [DebSource]+ ) where++import Control.Exception+import Data.List+import qualified Data.Map as Map+import System.Cmd+import System.Directory+import System.Exit+import System.IO+import System.Posix.Files+import System.Process+import Text.Regex++import Linspire.Unix.Directory as Unix+import Linspire.Unix.FilePath+import Linspire.Debian.Control+import Linspire.Debian.DistroCache as DistroCache+import Linspire.Debian.Relation+import Linspire.Debian.Repository as Repository+import Linspire.Debian.SourcesList+import Linspire.Debian.Version+import Linspire.Unix.Misc+import Linspire.Unix.Progress as Progress++-- |This type represents an OS image located at root built from a+-- particular distro using a particular architecture. If a Repository+-- 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 EnvRoot DistroCache Architecture (Maybe Repository)+type Architecture = String++-- |The root directory of an OS image.+data EnvRoot = EnvRoot FilePath++instance Show EnvRoot where+ show (EnvRoot path) = path++-- |A directory inside of an OS image.+data EnvPath = EnvPath EnvRoot FilePath++instance Show EnvPath where+ show (EnvPath root path) = show root ++ path++-- |Create or update an image. FIXME: Make sure there is no+-- ".incomplete" flag next to the directory+prepareEnv :: [Style] -> EnvRoot -> DistroCache -> Maybe Repository -> Bool -> Bool -> IO OSImage+prepareEnv style root distro repo flush pgp =+ do+ exists <- doesFileExist (show root ++ "/etc/apt/sources.list")+ case flush || not exists of+ True -> + do+ Unix.removeRecursiveSafely (show root)+ buildEnv failStyle root distro repo pgp >>=+ neuterEnv failStyle >>=+ syncPool style+ False ->+ do+ let os = OS root distro "i386" repo+ try (updateEnv style os) >>=+ -- At one point I removed and re-created the+ -- environment if anything went wrong trying to update+ -- it. This is often the wrong thing to do, since it+ -- might be caused by a glitch in networking.+ either (\ e -> error (updateError root e)) return+ where+ failStyle = Progress.removeStyle (Start "") style+ updateError root e = ("OSImage.updateEnv failed: " ++ show e +++ "\n (You may need to remove " ++ show root ++ ")")++prepareDevs :: FilePath -> IO ()+prepareDevs root = do+ mapM_ prepareDev + ([(root ++ "/dev/null", "c", 1, 3),+ (root ++ "/dev/zero", "c", 1, 5),+ (root ++ "/dev/full", "c", 1, 7),+ (root ++ "/dev/console", "c", 5, 1),+ (root ++ "/dev/random", "c", 1, 8),+ (root ++ "/dev/urandom", "c", 1, 9)] +++ (map (\ n -> (root ++ "/dev/loop" ++ show n, "b", 7, n)) [0..7]) +++ (map (\ n -> (root ++ "/dev/loop/" ++ show n, "b", 7, n)) [0..7]))+ where+ prepareDev (path, typ, major, minor) = do+ createDirectoryIfMissing True (fst (splitFileName path))+ let cmd = "mknod " ++ path ++ " " ++ typ ++ " " ++ show major ++ " " ++ show minor+ exists <- doesFileExist path+ if not exists then+ system cmd else+ return ExitSuccess++-- Create a new clean build environment in root.clean+-- FIXME: create an ".incomplete" flag and remove it when build-env succeeds+buildEnv :: [Style] -> EnvRoot -> DistroCache -> Maybe Repository -> Bool -> IO OSImage+buildEnv style root distro repo pgp =+ do+ let extra = ["makedev", "build-essential"] ++ (if pgp then ["pgp"] else [])+ -- 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.+ systemTask buildStyle ("build-env -o " ++ show root +++ " -s " ++ DistroCache.sourcesPath distro +++ " with '" ++ consperse " " extra ++ "'")+ let os = OS root distro "i386" repo+ let sourceListText = osSources os+ -- ePut ("writeFile2 " ++ (show root ++ "/etc/apt/sources.list") ++ "\n" ++ (formatSources sourceListText))+ writeFile (show root ++ "/etc/apt/sources.list") (consperse "\n" (map show sourceListText) ++ "\n")+ updateEnv style os+ where+ buildStyle = setStyles [Start ("Creating clean build environment (" ++ DistroCache.dist distro ++ ")"),+ Error "Could not create build environment."] style++-- |Update an existing build environment - run apt-get update+-- and dist-upgrade.+updateEnv :: [Style] -> OSImage -> IO OSImage+updateEnv style os@(OS root _ _ _) =+ do+ verifySources os+ prepareDevs (show root)+ syncPool style os+ syncSSH+ return os+ where+ verifySources os =+ do+ let correct = osSources os+ let sourcesPath = show root ++ "/etc/apt/sources.list"+ installed <- readFile sourcesPath >>= return . parseSourcesList+ if correct /= installed then do+ ePut ("Sources in " ++ sourcesPath ++ " don't match configuration for " ++ (show . dist . osDistro) os ++ ":\n\n" +++ consperse "\n" (map show correct))+ error ("Sources.list mismatch for " ++ (show . dist . osDistro) os) else+ return () + syncSSH = systemTask syncSSHStyle ("rsync -aHxSpDt --delete ~/.ssh/ " ++ show root ++ "/root/.ssh && " +++ "chown -R root.root " ++ show root ++ "/root/.ssh")+ syncSSHStyle = addStyles [Start "Copying SSH parameters",+ Error "Could copy SSH parameters", Output Dots] style++-- These are the sources.list lines for the local pool when viewed+-- from inside the changeroot.+chrootPool :: DistroCache -> [DebSource]+chrootPool distro = DistroCache.localPool distro "/work/localpool"++chrootEnv :: OSImage -> EnvRoot -> OSImage+chrootEnv (OS _ a b c) dst = (OS dst a b c)++-- Sync the environment from the clean copy. All this does besides+-- performing the proper rsync command is to make sure the destination+-- directory exists, otherwise rsync will fail. Not sure why the 'work'+-- subdir is appended. There must have been a reason at one point.+syncEnv :: [Style] -> OSImage -> OSImage -> IO OSImage+syncEnv style src dst =+ do+ createDirectoryIfMissing True (show (osRoot dst) ++ "/work")+ systemTask (setStyles [Start "Copying clean build environment",+ Error "Could not sync with clean build environment"] style)+ ("rsync -aHxSpDt --delete '" ++ show (osRoot src) ++ "/' '" ++ show (osRoot dst) ++ "'")+ return dst++-- |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 :: [Style] -> OSImage -> IO OSImage+neuterEnv style os@(OS root _ _ _) =+ do+ msg style ("Neutering OS image (" ++ stripDist (show root) ++ ")...")+ result <- try $ mapM_ (neuterFile os) neuterFiles+ either (\ e -> error $ "Failed to neuter environment " ++ show root ++ ": " ++ show e)+ (\ _ -> return os)+ result++neuterFiles :: [(FilePath, Bool)]+neuterFiles = [("/sbin/start-stop-daemon", True),+ ("/usr/sbin/invoke-rc.d", True),+ ("/sbin/init",True),+ ("/usr/sbin/policy-rc.d", False)]++-- neuter_file from build-env.ml+neuterFile :: OSImage -> (FilePath, Bool) -> IO ()+neuterFile (OS root _ _ _) (file, mustExist) =+ do+ -- putStrLn ("Neutering file " ++ file)+ exists <- doesFileExist (show fullPath)+ if exists then+ neuterExistantFile else+ if mustExist then+ error ("Can't neuter nonexistant file: " ++ show fullPath) else+ return () -- putStrLn "File doesn't exist, nothing to do"++ where+ neuterExistantFile =+ do+ sameFile <- sameInode (show fullPath) (show binTrue)+ if sameFile then+ return () else -- putStrLn "File already neutered"+ neuterUnneuteredFile+ neuterUnneuteredFile =+ do+ hasReal <- doesFileExist (show fullPath ++ ".real")+ if hasReal then+ neuterFileWithRealVersion else+ neuterFileWithoutRealVersion+ createLink (show binTrue) (show fullPath)+ neuterFileWithRealVersion =+ do+ sameCksum <- sameMd5sum (show fullPath) (show fullPath ++ ".real")+ if sameCksum then+ removeFile (show fullPath) else+ error (file ++ " and " ++ file ++ ".real differ (in " ++ show root ++ ")")+ + neuterFileWithoutRealVersion = renameFile (show fullPath) (show fullPath ++ ".real")++ fullPath = EnvPath root file+ binTrue = EnvPath root "/bin/true"++-- |Reverse the neuterEnv operation.+restoreEnv :: OSImage -> IO OSImage+restoreEnv os@(OS root _ _ _) =+ do+ hPutStr stderr "De-neutering OS image..."+ result <- try $ mapM_ (restoreFile os) neuterFiles+ either (\ e -> error $ "damaged environment " ++ show root ++ ": " ++ show e ++ "\n please remove it.")+ (\ _ -> return os) result++-- check_and_restore from build-env.ml+restoreFile :: OSImage -> (FilePath, Bool) -> IO ()+restoreFile (OS root _ _ _) (file, mustExist) =+ do+ exists <- doesFileExist (show fullPath)+ if exists then+ restoreExistantFile else+ if mustExist then+ error ("Can't restore nonexistant file: " ++ show fullPath) else+ return ()+ where+ restoreExistantFile =+ do+ isTrue <- sameInode (show fullPath) (show binTrue)+ hasReal <- doesFileExist (show fullPath ++ ".real")+ case (isTrue, hasReal) of+ (True, True) ->+ do+ removeFile (show fullPath)+ renameFile (show fullPath ++ ".real") (show 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"++-----------------------------------++osRoot :: OSImage -> EnvRoot+osRoot (OS root _ _ _) = root++osDistro :: OSImage -> DistroCache+osDistro (OS _ distro _ _) = distro++osSources :: OSImage -> [DebSource]+osSources (OS _ distro _ repo) =+ DistroCache.sources distro +++ maybe [] (\ _ -> chrootPool distro) repo++osDist :: OSImage -> String+osDist (OS _ distro _ _) = DistroCache.dist distro++-- FIXME: Compute this with dpkg-architecture and save this in the OSImage+osArch :: OSImage -> String+osArch (OS _ _ arch _) = arch++osLocalRepository :: OSImage -> Maybe Repository+osLocalRepository (OS _ _ _ repo) = repo++-- |Debian Status file (not currently used)+{-+type StatusMap = Map.Map String (Maybe Paragraph)++getStatusFile :: OSImage -> IO StatusMap+getStatusFile (OS root _ _ _ _) =+ parseControlFromFile (show root ++ "/var/lib/dpkg/status") >>=+ return . either (error . show) id >>=+ return . statusMake++statusFind :: String -> StatusMap -> Maybe Paragraph+statusFind name map = Map.findWithDefault Nothing name map++statusMake :: Control -> StatusMap+statusMake (Control control) =+ Map.fromList (map makepair control)+ where+ makepair paragraph =+ case lookupP "Package" paragraph of+ Just (Field (_, name)) -> (stripWS name, Just paragraph)+ _ -> error "A status file entry has no Package field"+-}++-- |Function to get information about source packages using apt-cache showsrc.+getSourceInfo :: [Style] -> OSImage -> [PkgName] -> IO Control+getSourceInfo style root packages =+ runAptCache (addStyles [Start ("getSourceInfo " ++ consperse " " packages ++ " in " ++ osDist root)] style) root "showsrc" packages++-- |Function to get information about binary packages using apt-cache show.+getBinaryInfo :: [Style] -> OSImage -> [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] -> OSImage -> String -> [PkgName] -> IO Control+-- |apt-cache will fail if called with no package names+runAptCache _ _ _ [] = return (Control [])+runAptCache style os 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 " ++ osDist os)] style++getAvailable :: [Style] -> OSImage -> [String] -> IO [Paragraph]+getAvailable style os names =+ do+ Control available <- getSourceInfo style os 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)++aptOpts :: OSImage -> String+aptOpts (OS root _ _ _) =+ (" -o=Dir::State::status=" ++ show root ++ "/var/lib/dpkg/status" +++ " -o=Dir::State::Lists=" ++ show root ++ "/var/lib/apt/lists" +++ " -o=Dir::Cache::Archives=" ++ show root ++ "/var/cache/apt/archives" +++ " -o=Dir::Etc::SourceList=" ++ show root ++ "/etc/apt/sources.list")++-- |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 -> Bool -> IO Relations+buildEssential _ True = return []+buildEssential (OS root _ _ _) False =+ do+ essential <-+ readFile (show root ++ "/usr/share/build-essential/essential-packages-list") >>=+ return . lines >>= return . dropWhile (/= "") >>= return . tail >>=+ return . parseRelations . (consperse ", ") >>=+ return . (either (error "parse error in /usr/share/build-essential/essential-packages-list") id)+ let re = mkRegex "^[^ \t]"+ relationText <-+ readFile (show 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: " ++ consperse ", " relationText)+ let buildEssential = parseRelations (consperse ", " relationText)+ let buildEssential' = either (\ l -> error ("parse error in /usr/share/build-essential/list:\n" ++ show l)) id buildEssential+ return (essential ++ buildEssential')++buildArch :: OSImage -> IO String+buildArch (OS root _ _ _) =+ processOutput ("export LOGNAME=root; chroot " ++ show root ++ " dpkg-architecture") >>=+ return . lines . fst >>=+ return . map (mapSnd (drop 1) . break (== '=')) >>=+ return . Map.fromList >>=+ Map.lookup "DEB_BUILD_ARCH"++processOutput :: String -> IO (String, ExitCode)+processOutput command =+ do+ (_, outh, _, handle) <- runInteractiveCommand command+ output <- hGetContents outh+ exitCode <- waitForProcess handle+ return (output, exitCode)++-- |Remove an image. The removeRecursiveSafely function is used to+-- ensure that any file systems mounted inside the image are unmounted+-- instead of destroyed.+removeEnv :: OSImage -> IO ()+removeEnv (OS root _ _ _) =+ do+ hPutStr stderr "Removing build environment..."+ Unix.removeRecursiveSafely (show root)+ hPutStrLn stderr "done."++{-+ -- mapM_ (createDirectoryIfMissing True . (++ "/dists") . show) copies+ -- syncCopies style repo+-}++syncPool :: [Style] -> OSImage -> IO OSImage+syncPool _ os@(OS _ _ _ Nothing) = return os+syncPool style os@(OS root _ _ (Just repo)) =+ do+ createDirectoryIfMissing True (show root ++ "/work")+ systemTask syncStyle + ("rsync -aHxSpDt --delete '" +++ topDir repo ++ "/' '" +++ show root ++ "/work/localpool'")+ updateLists style os+ return os+ where+ syncStyle = setStyles [Start ("Syncing local pool from " ++ topDir repo ++ " -> " ++ show root),+ Error "Failure syncing local pool"] style++updateLists :: [Style] -> OSImage -> IO TimeDiff+updateLists style (OS root _ _ _) =+ systemTask updateStyle+ ("chroot " ++ show root ++ " bash -c 'unset LANG; apt-get update && apt-get -y dist-upgrade'")+ where+ updateStyle = addStyles [Start ("Updating environment (" ++ stripDist (show root) ++ ")"),+ Error "Could not update environment", Output Dots] style++sameInode :: FilePath -> FilePath -> IO Bool+sameInode a b =+ do+ aStatus <- getFileStatus a+ bStatus <- getFileStatus b+ return (deviceID aStatus == deviceID bStatus && fileID aStatus == fileID bStatus)++sameMd5sum :: FilePath -> FilePath -> IO Bool+sameMd5sum a b =+ do+ asum <- md5sum a+ bsum <- md5sum b+ return (asum == bsum)++-- |The mighty consperse function+consperse :: [a] -> [[a]] -> [a]+consperse sep items = concat (intersperse sep items)++mapSnd :: (b -> c) -> (a, b) -> (a, c)+mapSnd f (a, b) = (a, f b)++ePut :: String -> IO ()+ePut s = hPutStrLn stderr s
+ Linspire/Debian/Package.hs view
@@ -0,0 +1,54 @@+-- |Functions for dealing with source and binary packages in an abstract-way+module Linspire.Debian.Package where++-- Standard GHC Modules++import qualified Data.Map as Map++-- Local Modules++import Linspire.Debian.Version+import Linspire.Debian.Control+import Linspire.Debian.Relation++type PackageNameMap a = Map.Map String [a]++-- |'packageNameMap' creates a map from a package name to all the versions of that package+-- NOTE: Provides are not included in the map+-- NOTE: the sort order is random -- this is perhaps a bug+-- see also: 'addProvides'+packageNameMap :: (a -> String) -> [a] -> PackageNameMap a+packageNameMap getName packages = foldl (\m p -> Map.insertWith (++) (getName p) [p] m) Map.empty packages++-- |'addProvides' finds packages that Provide other packages and adds+-- them to the PackageNameMap. They will be adde to the end of the+-- list, so that real packages have 'higher priority' than virtual+-- packages.+-- NOTE: Does not check for duplication or multiple use+addProvides :: (p -> [PkgName]) -> [p] -> PackageNameMap p -> PackageNameMap p+addProvides providesf ps pnm =+ let provides = findProvides providesf ps in+ foldl (\m (packageName, package) -> Map.insertWith (flip (++)) packageName [package] m) pnm provides++-- |'findProvides'+findProvides :: forall p. (p -> [PkgName]) -> [p] -> [(PkgName, p)]+findProvides providesf packages = foldl addProvides [] packages+ where addProvides :: [(PkgName, p)] -> p -> [(PkgName, p)]+ addProvides providesList package =+ foldl (\pl pkgName -> (pkgName, package): pl) providesList (providesf package)++-- |'lookupPackageByRel' returns all the packages that satisfy the specified relation+-- TODO: Add architecture check+lookupPackageByRel :: PackageNameMap a -> (a -> (String, DebianVersion)) -> Relation -> [a]+lookupPackageByRel pm packageVersionF (Rel pkgName mVerReq mArch) =+ case Map.lookup pkgName pm of+ Nothing -> []+ Just packages -> filter filterVer packages+ where filterVer p =+ case mVerReq of+ Nothing -> True+ Just verReq ->+ let (pName, pVersion) = packageVersionF p+ in if pName /= pkgName+ then False -- package is a virtual package, hence we can not do a version req+ else checkVersionReq mVerReq (Just pVersion)
+ Linspire/Debian/PackageDeprecated.hs view
@@ -0,0 +1,131 @@+-- |Experimental module for representing a single package , highly likely to change in incompatible ways.+module Linspire.Debian.PackageDeprecated where++-- Standard GHC Modules++import qualified Data.Map as Map++-- Local Modules++import Linspire.Debian.Version+import Linspire.Debian.Control+import Linspire.Debian.Relation+import Linspire.Debian.Package++-- Simple Package Representation++-- |A package with a name and list of dependencies+data Package + = Package { pName :: String+ , pVersion :: Maybe DebianVersion+ , pDepends :: Relations+ , pPreDepends :: Relations+ , pConflicts ::Relations+ , pReplaces :: Relations+ , pProvides :: Relations+-- , pParagraph :: Paragraph+ }+ deriving Show++instance Eq Package where+ p1 == p2 = (pName p1 == pName p2) && (pVersion p1 == pVersion p2)++-- |FIXME: we do not deal with Provides\/virtual packages yet+paragraphToPackages :: Paragraph -> Package+paragraphToPackages p =+ Package { pName = + case lookupP "Package" p of+ Nothing -> error $ "Paragraph does not have Package field: " ++ (show p)+ Just (Field (_,a)) -> stripWS a+ , pVersion = + case lookupP "Version" p of+ Nothing -> error $ "Version not found for " ++ show p+ Just (Field (_,a)) -> Just (parseDebianVersion (stripWS a))+ , pDepends = tryParseRel $ lookupP "Depends" p+ , pPreDepends = tryParseRel $ lookupP "Pre-Depends" p+ , pConflicts = tryParseRel $ lookupP "Conflicts" p+ , pReplaces = tryParseRel $ lookupP "Replaces" p+ , pProvides = tryParseRel $ lookupP "Provides" p+-- , pParagraph = p+ }++type ProvidesMap = Map.Map String [Package]++{-+findProvides :: [Package] -> ProvidesMap+findProvides packages = foldl addProvides Map.empty packages+ where addProvides :: ProvidesMap -> Package -> ProvidesMap + addProvides pm package =+ foldl (\m [(Rel pkgName Nothing Nothing)] -> Map.insertWith (++) pkgName [package] m) pm (pProvides package)+-}++findProvides :: [Package] -> [(PkgName, Package)]+findProvides packages = foldl addProvides [] packages+ where addProvides :: [(PkgName, Package)] -> Package -> [(PkgName, Package)]+ addProvides providesList package =+ foldl (\pl [(Rel pkgName Nothing Nothing)] ->(pkgName, package): pl) providesList (pProvides package)++-- |Architecture ?+makeVirtualPackages :: ProvidesMap -> [Package]+makeVirtualPackages pm =+ Map.foldWithKey mVP [] pm+ where mVP vpName packages virtualPackages+ = (Package { pName = vpName+ , pVersion = Nothing+ , pDepends = [map mkDepend packages] -- or depends on things that provide it+ , pPreDepends = []+ , pConflicts = []+ , pReplaces = []+ , pProvides = []+-- , pParagraph = Paragraph []+ } ) : virtualPackages+ mkDepend package = + let name = pName package+ rel = fmap EEQ (pVersion package)+ in Rel name rel Nothing+{-+-- |Does not check for duplication or multiple use+addProvides :: [Package] -> PackageNameMap -> PackageNameMap+addProvides ps pnm =+ let provides = findProvides ps in+ foldl (\m (packageName, package) -> Map.insertWith (flip (++)) packageName [package] m) pnm provides+-- ++ (makeVirtualPackages (findProvides ps))+ ++tryParseRel :: Maybe Field -> Relations+tryParseRel Nothing = []+tryParseRel (Just (Field (_, relStr))) = either (error . show) id (parseRelations relStr)+-}++controlToPackageNameMap :: Control -> (Paragraph -> Package) -> PackageNameMap Package+controlToPackageNameMap (Control p) p2p = + let packages = (map p2p p) in+ addProvides (map (\ (Rel pkgName _ _) -> pkgName) . concat . pProvides) packages (packageNameMap pName packages)++packagesToPackageNameMap :: [Package] -> PackageNameMap Package+packagesToPackageNameMap packages =+ addProvides (map (\ (Rel pkgName _ _) -> pkgName) . concat . pProvides) packages (packageNameMap pName packages)++-- |TODO: Add architecture check+lookupPackageByRel :: PackageNameMap Package -> Relation -> [Package]+lookupPackageByRel pm (Rel pkgName mVerReq mArch) =+ case Map.lookup pkgName pm of+ Nothing -> []+ Just packages -> filter filterVer packages+ where filterVer p =+ case mVerReq of+ Nothing -> True+ Just verReq ->+ if (pName p) /= pkgName+ then False -- package is a virtual package, hence we can not do a version req+ else checkVersionReq mVerReq (pVersion p)+++{-+ Nothing -> Nothing+ Just packages -> Just $ filter (\p -> checkVersionReq mVerReq (pVersion p)) packages+-}++tryParseRel :: Maybe Field -> Relations+tryParseRel Nothing = []+tryParseRel (Just (Field (_, relStr))) = either (error . show) id (parseRelations relStr)
+ Linspire/Debian/Relation.hs view
@@ -0,0 +1,25 @@+-- |A module for working with debian relationships <http://www.debian.org/doc/debian-policy/ch-relationships.html>+module Linspire.Debian.Relation+ ( -- * Types+ PkgName+ , AndRelation+ , OrRelation+ , Relations+ , Relation(..)+ , ArchitectureReq(..)+ , VersionReq(..)+ -- * Helper Functions+ , checkVersionReq+ -- * Relation Parser+ , RelParser+ , ParseRelations(..)+ ) where ++-- Standard GHC Modules++import Data.List+import Text.ParserCombinators.Parsec++-- Local Modules++import Linspire.Debian.Relation.String
+ Linspire/Debian/Relation/ByteString.hs view
@@ -0,0 +1,41 @@+-- |A module for working with debian relationships <http://www.debian.org/doc/debian-policy/ch-relationships.html>+module Linspire.Debian.Relation.ByteString+ ( -- * Types+ PkgName+ , AndRelation+ , OrRelation+ , Relations+ , Relation(..)+ , ArchitectureReq(..)+ , VersionReq(..)+ -- * Helper Functions+ , checkVersionReq+ -- * Relation Parser+ , RelParser+ , ParseRelations(..)+ ) where ++-- Standard GHC Modules++import Data.List+import Text.ParserCombinators.Parsec++-- 3rd Party Modules++import qualified Data.ByteString.Char8 as C++-- Local Modules++import Linspire.Debian.Relation.Common+import Linspire.Debian.Relation.String+import Linspire.Debian.Version++-- * ParseRelations++-- For now we just wrap the string version+instance ParseRelations C.ByteString where+ parseRelations byteStr = + let str = C.unpack byteStr in+ case parse pRelations str str of+ Right relations -> Right (filter (/= []) relations)+ x -> x
+ Linspire/Debian/Relation/Common.hs view
@@ -0,0 +1,86 @@+module Linspire.Debian.Relation.Common where++-- Standard GHC Modules++import Data.List+import Text.ParserCombinators.Parsec++-- Local Modules++import Linspire.Debian.Version++-- Datatype for relations++type PkgName = String++type Relations = AndRelation+type AndRelation = [OrRelation]+type OrRelation = [Relation]++data Relation = Rel PkgName (Maybe VersionReq) (Maybe ArchitectureReq)+ deriving Eq+++class ParseRelations a where+ -- |'parseRelations' parse a debian relation (i.e. the value of a+ -- Depends field). Return a parsec error or a value of type+ -- 'Relations'+ parseRelations :: a -> Either ParseError Relations+++instance Show Relation where+ show (Rel name ver arch) =+ name ++ maybe "" show ver ++ maybe "" show arch++instance Ord Relation where+ compare (Rel pkgName1 mVerReq1 mArch1) (Rel pkgName2 mVerReq2 mArch2) =+ case compare pkgName1 pkgName2 of+ LT -> LT+ GT -> GT+ EQ -> compare mVerReq1 mVerReq2++data ArchitectureReq+ = ArchOnly [String]+ | ArchExcept [String]+ deriving Eq++instance Show ArchitectureReq where+ show (ArchOnly arch) = " [" ++ concat (intersperse " " arch) ++ "]"+ show (ArchExcept arch) = " [!" ++ concat (intersperse " !" arch) ++ "]"++data VersionReq+ = SLT DebianVersion+ | LTE DebianVersion+ | EEQ DebianVersion+ | GRE DebianVersion+ | SGR DebianVersion+ deriving Eq++instance Show VersionReq where+ show (SLT v) = " (<< " ++ show v ++ ")"+ show (LTE v) = " (<= " ++ show v ++ ")"+ show (EEQ v) = " (= " ++ show v ++ ")"+ show (GRE v) = " (>= " ++ show v ++ ")"+ show (SGR v) = " (>> " ++ show v ++ ")"++-- |@FIXME:@ This instance is currently incomplete and only handles the case+-- where two version requirements are equal.+instance Ord VersionReq where+ compare r1 r2 =+ if r1 == r2 + then EQ + else+ case (r1, r2) of+ (EEQ v1, EEQ v2) -> compare v1 v2+ (a,b) -> error $ "Ord VersionReq does not handle (" ++ show a ++", "++ show b++")"+++-- |Check if a version number satisfies a version requirement.+checkVersionReq :: Maybe VersionReq -> Maybe DebianVersion -> Bool+checkVersionReq Nothing _ = True+checkVersionReq _ Nothing = False+checkVersionReq (Just (SLT v1)) (Just v2) = v2 < v1+checkVersionReq (Just (LTE v1)) (Just v2) = v2 <= v1+checkVersionReq (Just (EEQ v1)) (Just v2) = v2 == v1+checkVersionReq (Just (GRE v1)) (Just v2) = v2 >= v1+checkVersionReq (Just (SGR v1)) (Just v2) = v2 > v1
+ Linspire/Debian/Relation/String.hs view
@@ -0,0 +1,111 @@+-- |A module for working with debian relationships <http://www.debian.org/doc/debian-policy/ch-relationships.html>+module Linspire.Debian.Relation.String+ ( -- * Types+ PkgName+ , AndRelation+ , OrRelation+ , Relations+ , Relation(..)+ , ArchitectureReq(..)+ , VersionReq(..)+ -- * Helper Functions+ , checkVersionReq+ -- * Relation Parser+ , RelParser+ , ParseRelations(..)+ , pRelations+ ) where ++-- Standard GHC Modules++import Data.List+import Text.ParserCombinators.Parsec++-- Local Modules++import Linspire.Debian.Relation.Common+import Linspire.Debian.Version++-- * ParseRelations++instance ParseRelations String where+ parseRelations str = + case parse pRelations str str of+ Right relations -> Right (filter (/= []) relations)+ x -> x++-- * Relation Parser++type RelParser a = CharParser () a++pRelations :: RelParser Relations+pRelations = sepBy pOrRelation (char ',')++pOrRelation :: RelParser OrRelation+pOrRelation = sepBy pRelation (char '|')++whiteChar = oneOf [' ','\t','\n']++pRelation :: RelParser Relation+pRelation =+ do skipMany whiteChar+ pkgName <- many1 (noneOf [' ',',','|','\t','\n'])+ skipMany whiteChar+ mVerReq <- pMaybeVerReq+ skipMany whiteChar+ mArch <- pMaybeArch+ return $ Rel pkgName mVerReq mArch++pMaybeVerReq :: RelParser (Maybe VersionReq)+pMaybeVerReq =+ do char '('+ skipMany whiteChar+ op <- pVerReq+ skipMany whiteChar+ version <- many1 (noneOf [' ',')','\t','\n'])+ skipMany whiteChar+ char ')'+ return $ Just (op (parseDebianVersion version))+ <|>+ do return $ Nothing++pVerReq =+ do char '<'+ (do char '<' <|> char ' ' <|> char '\t'+ return $ SLT+ <|>+ do char '='+ return $ LTE)+ <|>+ do string "="+ return $ EEQ+ <|>+ do char '>'+ (do char '='+ return $ GRE+ <|>+ do char '>' <|> char ' ' <|> char '\t'+ return $ SGR)++pMaybeArch :: RelParser (Maybe ArchitectureReq)+pMaybeArch =+ do char '['+ (do archs <- pArchExcept+ char ']'+ return (Just (ArchExcept archs))+ <|>+ do archs <- pArchOnly+ char ']'+ return (Just (ArchOnly archs))+ )+ <|>+ return Nothing++-- Some packages (e.g. coreutils) have architecture specs like [!i386+-- !hppa], even though this doesn't really make sense: once you have+-- one !, anything else you include must also be (implicitly) a !.+pArchExcept :: RelParser [String]+pArchExcept = sepBy (char '!' >> many1 (noneOf [']',' '])) (skipMany1 whiteChar)++pArchOnly :: RelParser [String]+pArchOnly = sepBy (many1 (noneOf [']',' '])) (skipMany1 whiteChar)
+ Linspire/Debian/Repository.hs view
@@ -0,0 +1,879 @@+-- |A repository is a the directory heirarchy that is used by the apt+-- packages to retrieve packages. It is set of source packages. It+-- also implies the set of binary packages which were produced along+-- with the source packages. It could also be derived from the deb+-- and deb-src lines contained in a sources.list file, or from the+-- path to a sources.list file.+--+-- Author: David Fox <david.fox@linspire.com>+module Linspire.Debian.Repository+ (Repository(Repo),+ fromPath, -- FilePath -> Repository+ topDir, -- Repository -> FilePath+ prepare, -- [Style] -> Bool -> Bool -> FilePath -> IO Repository+ -- update, -- [Style] -> Bool -> Repository -> IO Repository+ flush, -- Repository -> IO Repository+ findLive, -- Repository -> IO [FilePath]+ uploadLocal, -- [Style] -> Repository -> FilePath -> IO Repository+ uploadAll, -- [Style] -> Bool -> Repository -> Maybe String -> IO Repository+ Layout(Pool, Flat),+ layout, -- Repository -> IO Layout,+ PackageID, -- (Name, Dist, Arch, Version)+ Arch(Arch),+ parsePackage, -- String -> PackageID+ createSkeleton, -- Bool -> FilePath -> Layout -> IO Repository+ createDists, -- [String] -> Repository -> IO Repository+ scanIncoming, -- [Style] -> Bool -> Repository -> IO [Bool]+ removePackage, -- Bool -> Repository -> PackageID -> IO Repository+ removeTrumped, -- [Style] -> Bool -> Repository -> IO Repository+ removeGarbage, -- [Style] -> Bool -> Repository -> IO Repository+ parseChangesFilename, -- String -> (String, String, String, String)+ bool) -- bool :: a -> a -> Bool -> a+ where++import Control.Exception+import Control.Monad+import Data.List+import Data.Maybe+import qualified Data.Set as Set+import System.Cmd+import System.Directory+import System.Exit+import System.IO+import qualified System.Posix.Files as Posix.Files+import System.Process+import Text.ParserCombinators.Parsec.Error+import Text.Regex++import Linspire.Unix.FilePath+import Linspire.Debian.Control+import Linspire.Debian.Version+import qualified Linspire.Unix.Directory as Unix+import qualified Linspire.Unix.Misc as Unix+import Linspire.Unix.Progress as Progress++data Repository =+ Repo+ FilePath -- the location of the repository files+ Layout -- Flat or Pool, how the repository is laid out+ Bool -- Is the repository in a consistent state?++fromPath :: FilePath -> Layout -> Repository+fromPath dir layout = Repo dir layout False++topDir :: Repository -> FilePath+topDir (Repo dir _ _) = dir++prepare :: [Style] -> Bool -> Bool -> FilePath -> Layout -> [String] -> IO Repository+prepare _ dryRun reset dir layout dists = do+ repo <- createSkeleton dryRun dir layout >>= createDists dryRun dists+ if reset then flush dryRun repo else return repo++flush :: Bool -> Repository -> IO Repository+flush dryRun repo@(Repo dir layout _) =+ do+ dists <- getDists repo+ Unix.removeRecursiveSafely dir -?- (dryRun, "")+ createSkeleton dryRun dir layout >>= createDists dryRun dists++-- |Update a repository. This means making sure the Sources.gz and+-- Packages.gz files are up-to-date with respect to the packages in+-- the pool. This only makes sense for a repository where we assume+-- that all the packages are supposed to be in a single dist, i.e. the+-- local repository used to hold the results of the autobuilder.+{-+update :: [Style] -> Bool -> Repository -> IO Repository+update style True repo = flush repo >>= update style False+update _ False repo@(Repo _ _ True) = return repo+update style False (Repo _ Pool False) = error "Unimplemented: update for Pool layout"+update style False (Repo dir Flat False) =+ do+ -- FIXME: We should see what architectures are present and create+ -- a directory for each. Unfortunately, apt-get update fails if+ -- the Packages file for its architecture doesn't exist. Maybe we+ -- need to change the sources.list when packages show up in the+ -- local pool?+ createDirectoryIfMissing True (dir ++ "/dists/source")+ createDirectoryIfMissing True (dir ++ "/dists/binary-i386")+ let scanCmd = ("cd " ++ dir ++ " && " +++ "dpkg-scanpackages dists /dev/null | gzip > dists/binary-i386/Packages.gz && " ++ + "dpkg-scansources dists /dev/null | gzip > dists/source/Sources.gz")+ systemTask updateStyle scanCmd+ return $ Repo dir Flat True+ where+ updateStyle = addStyles [Start ("updating repository at " ++ dir)] style+-}++uploadAll :: [Style] -> Bool -> Repository -> Maybe String -> IO Repository+uploadAll style dryRun repo host =+ do+ let dir = topDir repo+ files <- getDirectoryContents dir+ let changes = map base . concat . groupByNames . catMaybes . map parseChangesFilename . filter (isSuffixOf ".changes") $ files+ let upload = map base . catMaybes $ map parseChangesFilename (filter (isSuffixOf ".upload") files)+ -- Eliminate any packages already uploaded+ let changes' = Set.toList (Set.difference (Set.fromList changes) (Set.fromList upload))+ -- Reconstruct the full pathname of the .changes files+ let changes'' = map (\ base -> dir ++ "/" ++ base ++ ".changes") changes'+ case changes'' of+ [] -> ePut "Nothing to upload."+ _ -> mapM_ (dupload style dryRun host) changes''+ return repo+ where+ -- newest = map (head . reverse . (sortBy compareVersions))+ -- compareVersions (_, a, _, _) (_, b, _, _) = compare (parseDebianVersion a) (parseDebianVersion b)+ groupByNames = groupBy equalNames . sortBy compareNames+ compareNames (a, _, _, _) (b, _, _, _) = compare a b+ equalNames a b = compareNames a b == EQ+ -- Reconstruct the .changes or .upload filename without the extension+ base :: (String, String, Arch, String) -> String+ base (n, v, (Arch a), _) = n ++ "_" ++ v ++ "_" ++ a++-- |Run dupload on a changes file with an optional host (--to)+-- argument.+dupload :: [Style] -> Bool -> Maybe String -> FilePath -> IO TimeDiff+dupload style dryRun host changesFile =+ systemTask style' (if dryRun then ("echo " ++ cmd) else cmd)+ where+ cmd = ("dupload" ++ maybe "" (" --to " ++) host ++ " " ++ changesFile)+ style' = setStyles [Start "Uploading", Error "dupload failed", Output Indented] style++-- |Install a package into a local repository.+uploadLocal :: [Style] -> Repository -> FilePath -> IO Repository+uploadLocal style repo changesFile =+ do+ -- My.ePut ("uploadLocal " ++ show changesFile)+ let buildDirParent = parentPath changesFile+ -- My.ePut (" dir=" ++ show buildDirParent)+ changes <- parseControlFromFile changesFile+ case changes of+ Right (Control [paragraph]) ->+ case fieldValue "Files" paragraph of+ Just fileList ->+ let paths = map ((buildDirParent ++ "/") ++) (map fifth (parseChangesList fileList)) in+ linkFiles style repo (changesFile : paths)+ _ -> error "couldn't find either Distribution or Files section in .changes file"+ _ -> error "invalid .changes file format"+ where fifth (_, _, _, _, x) = x++-- |Move some files into the local repository incoming directory.+linkFiles :: [Style] -> Repository -> [FilePath] -> IO Repository+linkFiles style repo@(Repo dir _ _) paths =+ do+ mapM install paths+ result <- scanIncoming style False repo+ if all id result then+ return repo else+ error "Local upload failed"+ where+ install path =+ do+ removeIfExists (dest path)+ Posix.Files.createLink path (dest path)+ -- Posix.Files.removeLink path+ dest path = dir ++ "/incoming/" ++ snd (splitFileName path)+ removeIfExists path =+ do+ exists <- doesFileExist path+ if exists then Posix.Files.removeLink path else return ()+ -- updateStyle = addStyles [Start "Update local pool - after upload"] style++parseChangesFile :: FilePath -> IO (Either ParseError Control)+parseChangesFile path =+ do+ ePut ("-> " ++ path)+ parseControlFromFile path++parseChangesList :: String -> [(String, String, String, String, FilePath)]+parseChangesList text =+ -- md5sum size section priority name+ case (text, matchRegexAll re text) of+ ("", _) -> []+ (_, Just (_, _, remaining, [md5sum, size, section, priority, filename])) ->+ (md5sum, size, section, priority, filename) : parseChangesList remaining+ _ -> error ("Parse error in Files section of changes file: '" ++ text)+ where+ re = mkRegex ("^[ \t\n]*" ++ g ++w++ g ++w++ g ++w++ g ++w++ g ++ "[ \t\n]*")+ g = "(" ++ t ++ ")"+ t = "[^ \t\n]+"+ w = "[ \t]+"++------------------------ FROM newdist -------------------------++-- Name, Dist, Arch, Version+data PackageID = PackageID String Dist Arch DebianVersion+data Layout = Flat | Pool+newtype Arch = Arch String+newtype Dist = Dist String++instance Show Layout where+ show Flat = "Flat"+ show Pool = "Pool"++instance Show PackageID where+ show (PackageID name (Dist dist) (Arch arch) version) = name ++ "=" ++ show version ++ ", dist=" ++ dist ++ ", arch=" ++ arch++instance Show Arch where+ show (Arch arch) = arch++instance Show Dist where+ show (Dist arch) = arch++layout :: Repository -> IO Layout+layout repo =+ do+ exists <- doesDirectoryExist (topDir repo ++ "/pool")+ return $ case exists of False -> Flat; True -> Pool++-- |Parse a string in the form <packagename>=<versionnumber>+parsePackage :: String -> Maybe PackageID+parsePackage s =+ case splitRegex (mkRegex "[,=]") s of+ [dist, arch, name, version] -> Just (PackageID name (Dist dist) (Arch arch) (parseDebianVersion version))+ _ -> Nothing++-- |If the repository already exists we must look at it to determine+-- the layout, otherwise use the layout argument.+computeLayout :: FilePath -> Layout -> IO Layout+computeLayout root layout =+ do+ isPool <- doesDirectoryExist (root ++ "/pool")+ isFlat <- doesDirectoryExist root + case (isPool, isFlat) of+ (True, _) -> return Pool+ (False, True) -> return Flat+ (False, False) -> return layout++-- |Create the directories which will hold the repository.+createSkeleton :: Bool -> FilePath -> Layout -> IO Repository+createSkeleton dryRun root layout =+ do+ layout' <- computeLayout root layout+ mapM_ initDir ([("dists", 0o40755),+ ("incoming", 0o41755),+ ("removed", 0o40750),+ ("reject", 0o40750)] +++ case layout' of+ Pool -> [("pool", 0o40755),+ ("installed", 0o40755)]+ Flat -> [])+ return $ Repo root layout' True+ where+ initDir (name, mode) = + do+ let path = root ++ "/" ++ name+ createDirectoryIfMissing True path -?- (dryRun, "")+ Posix.Files.setFileMode path mode -?- (dryRun, "")++createDists :: Bool -> [String] -> Repository -> IO Repository+createDists dryRun dists (Repo root layout False) = do+ repo' <- createSkeleton dryRun root layout+ createDists dryRun dists repo'+createDists dryRun dists repo@(Repo root _ True) = do+ mapM_ initDir ((map (\ dist -> ("dists/" ++ dist ++ "/main/source", 0o040755)) dists) +++ (map (\ dist -> ("dists/" ++ dist ++ "/main/binary-i386", 0o040755)) dists))+ mapM_ initFile (map (\ dist -> root ++ "/dists/" ++ dist ++ "/main/source/Sources") dists +++ map (\ dist -> root ++ "/dists/" ++ dist ++ "/main/binary-i386/Packages") dists)+ return repo+ where+ initDir (name, mode) = + do+ let path = root ++ "/" ++ name+ createDirectoryIfMissing True path -?- (dryRun, "")+ Posix.Files.setFileMode path mode -?- (dryRun, "")+ initFile path =+ do+ exists <- doesFileExist (path ++ ".gz")+ if not (exists || dryRun) then do+ writeFile path ""+ system ("gzip < " ++ path ++ " > " ++ path ++ ".gz") else+ return ExitSuccess++-- |Find the .changes files in the incoming directory and try to+-- process each.+scanIncoming :: [Style] -> Bool -> Repository -> IO [Bool]+scanIncoming style dryRun repo@(Repo root _ _) =+ do+ changesFiles <- getDirectoryContents (root ++ "/incoming") >>=+ return . filter (isSuffixOf ".changes")+ let changesPaths = map ((root ++ "/incoming/") ++) changesFiles+ changes <- mapM parseChangesFile changesPaths+ mapM tryUpload (zip changes changesFiles)+ where+ tryUpload (Right changes, changesFile) =+ try (installPackage style dryRun repo changesFile changes) >>=+ either (\ e -> do ePut (" rejected: " ++ changesFile ++ "\n " ++ show e); return False)+ (\ _ -> do ePut (" accepted: " ++ changesFile); return True)+ tryUpload (Left e, changesFile) =+ do+ renameFileWithBackup + (root ++ "/incoming/" ++ changesFile)+ (root ++ "/reject/" ++ changesFile)+ ePut ("Couldn't parse " ++ changesFile ++ ": " ++ show e)+ return False++{-+data SubDir = SubDir [String] -- path elements++show0 (SubDir l) = concat (intersperse "/" l)+show1 dir = show0 dir ++ "/"+show2 dir = "/" ++ show0 dir+show3 dir = "/" ++ show0 dir ++ "/"+show4 [] = ""+show4 dir = show1 dir+-}++-- |Install a source package into the repository. This means+-- 1. getting the list of files from the .changes file,+--+-- 2. verifying the file checksums,+--+-- 3. removing any existing version and perhaps other versions which+-- were listed in the remove list,+--+-- 4. updating the Packages and Sources files, and+--+-- 5. moving the files from the incoming directory to the proper+-- place in the package pool.+installPackage :: [Style] -> Bool -> Repository -> FilePath -> Control -> IO Repository+installPackage style dryRun repo@(Repo root layout True) changesFile (Control [changes]) =+ do+ -- FIXME - if the upload seems incomplete (missing files, short+ -- files) we should leave it here, but if it seems incorrect+ -- (bad checksums for files that seem long enough) we should+ -- move it to reject.+ mapM_ (verifyFile root) uploadFiles+ verifyInstall repo root poolDir uploadFiles+ -- Build the control file entries for the package+ (sourceInfo, binaryInfo) <- buildControl root source poolDir uploadFiles+ -- Add our "Build-Info" field+ sourceInfo' <-+ case sourceInfo of+ Control [Paragraph paragraph] ->+ return $ Control [Paragraph (paragraph ++ + maybe [] (\ s -> [Field ("Build-Info", s)])+ (fieldValue "Build-Info" changes))]+ _ -> error "Invalid sourceInfo"+ -- already exist.+ -- Write out the modified control files+ writeControl dryRun root source poolDir sourceInfo' binaryInfo+ -- Move the files out of incoming+ mapM_ (installFile dryRun root poolDir) uploadFiles+ let changesDir = case layout of Pool -> root ++ "/installed"; Flat -> root+ renameFileWithBackup changesPath (changesDir ++ "/" ++ changesFile) -?- (dryRun, " installing " ++ changesFile)+ updatePackageLists style dryRun root dist buildArch sourceInfo' binaryInfo+ return repo+ where+ poolDir = case layout of+ Flat -> ""+ Pool -> "pool/main/" ++ prefixDir ++ "/" ++ source+ prefixDir = if isPrefixOf "lib" source then+ take (min 4 (length source)) source else+ take (min 1 (length source)) source+ uploadFiles = parseChangesList (getField changes "Files")+ -- version = parseDebianVersion $ getField changes "Version"+ source = getField changes "Source"+ dist = Dist $ getField changes "Distribution"+ changesPath = root ++ "/incoming/" ++ changesFile+ buildArch = case parseChangesFilename changesFile of+ Nothing -> error ("Invalid changes filename: " ++ changesFile)+ Just (_, _, x, _) -> x+ getField para name = maybe (error ("Missing field: " ++ name)) id (fieldValue name para)+installPackage _ _ _ _ (Control _) = error "Invalid changes file"++-- |Make sure that none of the files we are about to install are+-- already in the repository (other than uncollected garbage.)+verifyInstall :: Repository -> FilePath -> FilePath -> [(String, String, String, String, FilePath)] -> IO ()+verifyInstall repo root poolDir uploadFiles =+ do+ let moves = map (installPaths root poolDir) uploadFiles+ -- If none of the files exist we have nothing to worry about.+ exists <- mapM doesFileExist (map snd moves)+ case exists of+ [] -> return ()+ _ -> do+ -- Now we have to look at all the dists to see if+ -- any of these files are present. This is the same+ -- operation removeGarbage does.+ live <- findLive repo+ case Set.toList (Set.intersection (Set.fromList live) (Set.fromList (map snd moves))) of+ [] -> return ()+ files -> error ("Uploaded files already exist in repository:\n " ++ consperse "\n " files)++-- filename name version arch ext+parseChangesFilename :: String -> Maybe (String, String, Arch, String)+parseChangesFilename name =+ case matchRegex (mkRegex "^([^_]*)_(.*)_([^.]*)\\.(changes|upload)$") name of+ Just [name, version, arch, ext] -> Just (name, version, Arch arch, ext)+ _ -> error ("Invalid .changes file name: " ++ name)++-- text md5sum size name+parseSourcesFileList :: String -> [(String, Int, String)]+parseSourcesFileList text =+ catMaybes $ map parseSourcesFiles (lines text)+ where+ parseSourcesFiles line =+ case words line of+ [md5sum, size, name] -> Just (md5sum, read size, name)+ [] -> Nothing+ _ -> error ("Invalid line in Files list: '" ++ line ++ "'")++-- |Remove the version of the package from a dist's package lists.+-- Note that the package's files are not removed, they may still be+-- referenced in other dists.+removePackage :: Bool -> Repository -> PackageID -> IO Repository+removePackage dryRun repo package@(PackageID name dist arch version) =+ do+ ePut (" Repository.removePackage (" ++ consperse " " [name, show dist, show version] ++ ")")+ Control sources <- getSources repo dist+ Control packages <- getPackages repo dist arch+ case partition testSource sources of+ ([], _) ->+ -- We didn't find a source package, maybe it is a binary package+ case partition testBinary packages of+ ([], _) -> do+ ePut (" Package not found: " ++ show package)+ return repo+ ([remove], _) ->+ case fieldValue "Source" remove of+ Nothing -> do+ ePut " Binary package has no 'Source' field"+ return repo+ Just name -> do+ ePut (" Removing binary package version: " ++ show package)+ removePackage dryRun repo (PackageID name dist arch version)+ (remove, _) -> do+ ePut (" Multiple packages found: " ++ consperse "\n\n" (map show remove))+ return repo+ (remove, keep) -> do+ -- We found one or more source packages to remove, rewrite Sources with+ -- only the keep packages.+ case remove of (_ : _ : _) -> ePut ("WARNING: multiple " ++ show package ++ " found"); _ -> return ()+ ePut (" Removing source package version: " ++ show package)+ putSources repo dist (Control keep) -?- (dryRun, (""))+ case partition testBinaryBySource packages of+ ([], _) -> do+ ePut (" No binary packages found for " ++ show package)+ return repo+ (remove, keep) -> do+ ePut (" Removing binary package versions: " ++ show (catMaybes (map binaryVersion remove)))+ putPackages repo dist arch (Control keep) -?- (dryRun, (""))+ return repo+ where+ -- Is this the source package we are looking for?+ testSource paragraph =+ case (fieldValue "Package" paragraph, fieldValue "Version" paragraph) of+ (Just a, Just b) -> a == name && b == show version+ _ -> False+ -- Is this binary packages part of the source package we are looking for?+ testBinaryBySource paragraph =+ case (fieldValue "Source" paragraph, fieldValue "Version" paragraph) of+ (Just a, Just b) -> a == name && b == show version+ _ -> False+ -- Is this the binary package we are looking for?+ testBinary paragraph =+ case (fieldValue "Package" paragraph, fieldValue "Version" paragraph) of+ (Just a, Just b) -> a == name && b == show version+ _ -> False+{-+ removeBinaryP paragraph =+ case (fieldValue "Source" paragraph, fieldValue "Version" paragraph) of+ (Just a, Just b) -> a == name && b == version+ _ -> False+ sourceVersion paragraph =+ case (fieldValue "Package" paragraph, fieldValue "Version" paragraph) of+ (Just a, Just b) -> Just (a, b)+ _ -> Nothing+-}+ binaryVersion paragraph =+ case (fieldValue "Binary" paragraph, fieldValue "Version" paragraph) of+ (Just a, Just b) -> Just (a, b)+ _ -> Nothing+ -- For debugging+ nameversion paragraph = (fieldValue "Package" paragraph, fieldValue "Version" paragraph)++-- |Get the contents of the Sources file for a dist+getSources :: Repository -> Dist -> IO Control+getSources repo dist =+ do+ exists <- doesFileExist (sourcesPath repo dist)+ case exists of+ False -> return (Control [])+ True -> do+ let path = sourcesPath repo dist+ Control paragraphs <- parseControlFromFile path >>= + either (error ("Error reading " ++ sourcesPath repo dist)) return+ return (Control paragraphs)++putSources :: Repository -> Dist -> Control -> IO ()+putSources repo dist control = rewriteFile (sourcesPath repo dist) (show control)++-- |Return the name of the Sources file for a dist+sourcesPath :: Repository -> Dist -> FilePath+sourcesPath (Repo root _ _) dist = root ++ "/dists/" ++ show dist ++ "/main/source/Sources"++getPackages :: Repository -> Dist -> Arch -> IO Control+getPackages repo dist arch =+ do+ exists <- doesFileExist path+ case exists of+ False -> return (Control [])+ True -> parseControlFromFile path >>=+ either (error ("Error reading " ++ path)) return+ where+ path = (packagesPath repo dist arch)++putPackages :: Repository -> Dist -> Arch -> Control -> IO ()+putPackages repo dist arch control =+ do+ rewriteFile (packagesPath repo dist arch) (show control)++packagesPath :: Repository -> Dist -> Arch -> FilePath+packagesPath (Repo root _ _) dist arch = root ++ "/dists/" ++ show dist ++ "/main/binary-" ++ show arch ++ "/Packages"++-- |Add a package to the source and binary package lists.+updatePackageLists :: [Style] -> Bool -> FilePath -> Dist -> Arch -> Control -> Control -> IO ()+updatePackageLists style dryRun root dist buildArch sourceInfo binaryInfo =+ do+ append' sourceStyle (sourcesDir dist) "Sources" (show sourceInfo)+ -- Add the packages to the package list for the build architecture+ append' binaryStyle (archDir dist buildArch) "Packages" (show binaryInfo)+ where+ sourcesDir dist = root ++ "/dists/" ++ show dist ++ "/main/source"+ archDir dist arch = root ++ "/dists/" ++ show dist ++ "/main/binary-" ++ show arch+ -- Append text to a file with a separating newline+ -- (This version of append is dying on gzip, claiming that the+ -- Sources file is locked, presumably by appendFile. Odd.)+ append _ dir name text =+ do+ createDirectoryIfMissing True dir -?- (dryRun, "") -- ("create directory " ++ dir)+ let path = dir ++ "/" ++ name+ exists <- doesFileExist path+ if exists then+ appendFile path ("\n" ++ text) -?- (dryRun, " append package info " ++ path) else+ writeFile path text -?- (dryRun, " write package info " ++ path)+ Unix.gzip path -?- (dryRun, " gzip package info " ++ path)+ -- system ("ls -l " ++ path ++ " " ++ path ++ ".gz 1>&2")+ return ()+ append' style dir name text = do+ createDirectoryIfMissing True dir -?- (dryRun, "") -- ("create directory " ++ dir)+ let path = dir ++ "/" ++ name+ newtext <- empty path >>= return . bool "\n" "" >>= return . (++ text)+ writeFile (path ++ ".new") newtext+ let cmd = ("cat " ++ path ++ ".new >> " ++ path +++ " && gzip < " ++ path ++ " > " ++ path ++ ".gz")+ systemTask style cmd+ removeFile (path ++ ".new")+ return ()+ empty path = doesFileExist path >>= bool (return True) (readFile path >>= return . (== ""))+ sourceStyle = setStyles [Start ("Updating repository " ++ show dist ++ " source indices"),+ Error ("Failure updating repository " ++ show dist ++ " source indices")] style+ binaryStyle = setStyles [Start ("Updating repository " ++ show dist ++ " binary indices"),+ Error ("Failure updating repository " ++ show dist ++ " binary indices")] style++-- |Verify the checksum of a file. (We could verify the size too at+-- some point.)+verifyFile :: FilePath -> (String, String, String, String, FilePath) -> IO ()+verifyFile root (md5sum, _ {- size -}, _, _, name) =+ do+ let path = root ++ "/incoming/" ++ name+ exists <- doesFileExist path+ case exists of+ False -> error ("Missing file: " ++ path) + True ->+ do+ sum <- Unix.md5sum path+ case sum == md5sum of+ True -> return ()+ False -> error ("checksum mismatch on " ++ path ++ ":\n expected: " ++ md5sum ++ "\n actual: " ++ sum)++-- |Return the path where a file is to be installed.+installPaths :: FilePath -> FilePath -> (String, String, String, String, FilePath) -> (FilePath, FilePath)+installPaths root dir (_, _, _, _, name) =+ (root +/+ "incoming" +/+ name, root +/+ dir +/+ name)++-- |Install one of the files listed in the .changes file into the+-- pool.+installFile :: Bool -> FilePath -> FilePath -> (String, String, String, String, FilePath) -> IO ()+installFile dryRun root dir (_, _, _, _, name) =+ do+ -- This is used as the Directory attribute+ let src = root +/+ "incoming" +/+ name+ let dst = root +/+ dir +/+ name+ createDirectoryIfMissing True (root +/+ dir) -?- (dryRun, "" {- "create directory " ++ show dir -})+ removeFileIf dst -?- (dryRun, "" {- "remove existing file " ++ dst -})+ Posix.Files.createLink src dst -?- (dryRun, (" installing " ++ dst) {- "link new name " ++ dst -})+ Posix.Files.removeLink src -?- (dryRun, "" {- "unlink old name " ++ src -})+ where+ removeFileIf path = + doesFileExist path >>=+ (\ exists -> if exists then removeFile path else return ())++writeControl :: Bool -> FilePath -> String -> FilePath -> Control -> Control -> IO ()+writeControl dryRun root source dir sourceInfo binaryInfo = do+ createDirectoryIfMissing True (root +/+ dir) -?- (dryRun, "") -- "create directory " ++ dir+ writeFile (root +/+ dir +/+ source ++ ".package") (show binaryInfo) -?- (dryRun, "") -- "write .package file for " ++ source+ writeFile (root +/+ dir +/+ source ++ ".source") (show sourceInfo) -?- (dryRun, "") -- "write .source file for " ++ source++-- |Write the info which belongs in the Packages and Sources files.+buildControl :: FilePath -> String -> FilePath -> [(String, String, String, String, FilePath)] -> IO (Control, Control)+buildControl root source dir files =+ do+ let debs = filter (isSuffixOf ".deb" . name) files+ let dsc = filter (isSuffixOf ".dsc" . name) files+ case (dsc, debs) of+ ([dscTuple], (_ : _)) ->+ do+ binaryInfo <- mapM doPackage debs >>= return . mergeControls+ sourceInfo <- mapM (doSource dscTuple) dsc >>= return . mergeControls+ return (sourceInfo, binaryInfo)+ ([_], _) ->+ error "Package contains no debs"+ (_, _) ->+ error "Package must contain exactly 1 .dsc file"+ where+ name (_, _, _, _, x) = x+ doPackage (md5sum, size, _, _, name) =+ do+ let debpath = root ++ "/incoming/" ++ name+ info <- getControl debpath+ case info of+ Control [info] ->+ do+ let newfields = [Field ("Source", " " ++ source),+ Field ("Filename", " " ++ dir +/+ name),+ Field ("Size", " " ++ size),+ Field ("MD5sum", " " ++ md5sum)]+ return $ Control [appendFields newfields info]+ _ -> error ("Invalid control file in " ++ debpath)+ doSource (md5sum, size, _, _, dscName) (_, _, section, priority, name) =+ do+ let dscpath = root ++ "/incoming/" ++ name+ dsc <- parseControlFromFile dscpath >>= either (error ("couldn't parse " ++ dscpath)) return+ case dsc of+ Control [info] ->+ do+ let info' = renameField "Source" "Package" info+ let info'' = modifyField "Files" (++ "\n " ++ md5sum ++ " " ++ size ++ " " ++ dscName) info'+ let info''' = raiseFields (== "Package") info''+ let newfields = [Field ("Priority", " " ++ priority),+ Field ("Section", " " ++ section),+ Field ("Directory", " " ++ dir)]+ return $ Control [appendFields newfields info''']+ _ -> error ("Invalid .dsc file: " ++ dscpath)++-- |Remove any packages from a dist which are trumped by newer packages.+-- These packages are not garbage because they can still be installed by+-- explicitly giving their version number to apt.+removeTrumped :: [Style] -> Bool -> Repository -> IO Repository+removeTrumped style dryRun repo =+ do+ dists <- getDists repo >>= return . map Dist+ victims <- mapM (findTrumped style repo) dists >>= return . concat+ case victims of+ [] -> do+ ePut "removeTrumped: nothing to remove"+ return repo+ _ -> do+ ePut "removeTrumped:"+ foldM (removePackage dryRun) repo victims++-- |Return a list of packages in a dist which are trumped by some+-- newer version.+findTrumped :: [Style] -> Repository -> Dist -> IO [PackageID]+findTrumped _ (Repo _ _ False) _ = error "Invalid repository"+findTrumped _ (Repo dir _ True) dist =+ do+ ePut ("findTrumped " ++ show dist)+ packages <- return (dir ++ "/dists/" ++ show dist ++ "/main/source/Sources") >>=+ parseControlFromFile >>=+ return . either (error "control file parse error") id >>=+ return . (\ (Control p) -> p) >>=+ return . map makePackage+ let (groups :: [[PackageID]]) = groupByName packages+ mapM_ ePut (catMaybes (map formatGroup groups))+ return . concat . (map older) $ groups+ where+ makePackage p = case (fieldValue "Package" p, fieldValue "Version" p) of+ (Just n, Just v) -> (PackageID n dist arch (parseDebianVersion v))+ _ -> error ("Invalid Sources paragraph: " ++ show p)+ groupByName = groupBy equalNames . sortBy compareNames+ equalNames a b = compareNames a b == EQ+ compareNames (PackageID a _ _ _) (PackageID b _ _ _) = compare a b+ older :: [PackageID] -> [PackageID]+ older = tail . reverse . (sortBy compareVersions)+ compareVersions (PackageID _ _ _ a) (PackageID _ _ _ b) = compare a b+ arch = Arch "i386"+ formatGroup [] = Nothing+ formatGroup [_] = Nothing+ formatGroup (newest@(PackageID _ dist _ _) : other) =+ Just ("Trumped by " ++ formatPackage newest ++ " in " ++ show dist ++ ":\n " ++ consperse "\n " (map formatPackage other))+ formatPackage (PackageID name _ arch version) = name ++ "=" ++ show version ++ "[" ++ show arch ++ "]"++-- |Collect files that no longer appear in any package index and move+-- them to the removed directory. The .changes files are treated+-- specially: they don't appear in any index files, but the package+-- they belong to can be constructed from their name.+removeGarbage :: [Style] -> Bool -> Repository -> IO Repository+removeGarbage style dryRun repo@(Repo dir layout True) =+ do+ ePut ("removeGarbage in " ++ dir ++ " (layout=" ++ show layout ++ ", dryRun=" ++ show dryRun ++ ")")+ allFiles1 <- poolFiles repo+ allFiles2 <- changesFilePaths repo+ let allFiles = allFiles1 ++ allFiles2+ -- ePut ("allFiles:\n " ++ consperse "\n " (sort allFiles) ++ "\n")+ liveFiles <- findLive repo+ -- ePut ("liveFiles:\n " ++ consperse "\n " (sort liveFiles) ++ "\n")+ let deadFiles = Set.toList (Set.difference (Set.fromList allFiles) (Set.fromList liveFiles))+ ePut ("Removing:\n " ++ consperse "\n " (sort deadFiles) ++ "\n")+ mapM_ (moveToRemoved dryRun) deadFiles+ return repo+ where+ poolFiles (Repo dir Flat _) = getDirectoryContents dir >>= filterM (doesFileExist . ((dir ++ "/") ++))+ poolFiles (Repo dir Pool _) = + getSubPaths (dir ++ "/pool") >>=+ mapM getSubPaths >>= return . concat >>=+ mapM getSubPaths >>= return . concat >>=+ mapM getSubPaths >>= return . concat+ changesFilePaths (Repo dir Pool _) = getDirectoryPaths (dir ++ "/installed")+ -- In this case we already got the .changes files from the top directory+ changesFilePaths (Repo _ Flat _) = return []+ getSubPaths path = + do+ isDir <- doesDirectoryExist path+ case isDir of+ False -> return [path]+ True -> getDirectoryPaths path+ getDirectoryPaths dir = getDirectoryContents dir >>= return . filter filterDots >>= return . map ((dir ++ "/") ++)+ filterDots "." = False+ filterDots ".." = False+ filterDots _ = True+ -- upload files only appear when we dupload from a flat repository to another.+ moveToRemoved True file = do+ ePut ("renameFile " ++ file ++ " " ++ dir ++ "/removed/" ++ snd (splitFileName file))+ return ()+ moveToRemoved False file = do+ ePut ("renameFile " ++ file ++ " " ++ dir ++ "/removed/" ++ snd (splitFileName file))+ renameFile file (dir ++ "/removed/" ++ snd (splitFileName file))+removeGarbage style dryRun (Repo dir layout False) =+ createSkeleton dryRun dir layout >>= removeGarbage style dryRun++findLive :: Repository -> IO [FilePath]+findLive (Repo _ _ False) = error "Invalid operation on unverified repository"+findLive repo@(Repo dir layout True) =+ do+ dists <- getDists repo >>= return . map Dist+ sourcePackages <- mapM packagesOfIndex (map sourceIndexPath dists) >>= return . concat+ binaryPackages <- mapM packagesOfIndex (map binaryIndexPath dists) >>= return . concat+ let sourceFiles = map ((dir ++ "/") ++) . map (\ (_, _, name) -> name) . concat . map filesOfSourcePackage $ sourcePackages+ let binaryFiles = map ((dir ++ "/") ++) . catMaybes $ map (fieldValue "Filename") binaryPackages+ let changesFiles = map (changesFilePath layout) $ sourcePackages+ let uploadFiles = map ((dir ++ "/") ++) . map uploadFilePath $ sourcePackages+ return $ sourceFiles ++ binaryFiles ++ changesFiles ++ uploadFiles+ where+ sourceIndexPath dist = dir ++ "/dists/" ++ show dist ++ "/main/source/Sources"+ binaryIndexPath dist = dir ++ "/dists/" ++ show dist ++ "/main/binary-" ++ show arch ++ "/Packages"+ packagesOfIndex :: FilePath -> IO [Paragraph]+ packagesOfIndex path = + parseControlFromFile path >>=+ return . either (error ("Bad index file: " ++ path)) id >>=+ return . (\ (Control p) -> p)+ filesOfSourcePackage :: Paragraph -> [(String, Int, String)]+ filesOfSourcePackage package =+ let dir = sourcePackageDirectory package in+ map (\ (a, b, name) -> (a, b, dir ++ name)) (maybe [] parseSourcesFileList (fieldValue "Files" package))+ sourcePackageDirectory package =+ case fieldValue "Directory" package of+ Nothing -> ""+ Just "" -> ""+ Just s -> s ++ "/"+ changesFilePath Flat package = ((dir ++ "/") ++) . changesFileName $ package+ changesFilePath Pool package = ((dir ++ "/installed/") ++) . changesFileName $ package+ changesFileName package = + consperse "_" [fromJust (fieldValue "Package" package),+ fromJust (fieldValue "Version" package),+ show arch] ++ ".changes"+ uploadFilePath package = ((dir ++ "/") ++) . uploadFileName $ package+ uploadFileName package =+ consperse "_" [fromJust (fieldValue "Package" package),+ fromJust (fieldValue "Version" package),+ show arch] ++ ".upload"+ arch = Arch "i386"++getDists :: Repository -> IO [String]+getDists (Repo dir _ _) =+ getDirectoryContents (dir ++ "/dists") >>= filterM isDist+ where+ isDist "." = return False+ isDist ".." = return False+ isDist name = doesFileExist $ dir ++ "/dists/" ++ name ++ "/main/source/Sources.gz"++getControl :: FilePath -> IO Control+getControl path =+ do+ let cmd = "ar p " ++ path ++ " control.tar.gz | tar xzO ./control"+ -- let cmd = "dpkg-deb --info " ++ path ++ " control"+ (_, outh, _, handle) <- runInteractiveCommand cmd+ control <- parseControlFromHandle cmd outh+ exitcode <- waitForProcess handle+ case exitcode of+ ExitSuccess -> return $ either (error ("Failure reading contol info from " ++ path)) id control+ ExitFailure _ -> error ("Failure running " ++ cmd)++-- |Back-up and rewrite a file+rewriteFile :: FilePath -> String -> IO ()+rewriteFile path text =+ do+ backupFile path+ writeFile path text+ Unix.gzip path++type DryRunFn = IO () -> (Bool, String) -> IO ()++-- |If this is a dry run (dryRun params is True) do *not* evaluate f,+-- but instead print a message.+(-?-) :: DryRunFn+(-?-) f (dryRun, "") = if dryRun then return () else f+(-?-) f (dryRun, msg) =+ do+ hPutStrLn stderr msg+ if dryRun then return () else f+infixr 9 -?-++renameFileWithBackup :: FilePath -> FilePath -> IO ()+renameFileWithBackup src dst =+ do+ removeIfExists (dst ++ "~")+ renameIfExists dst (dst ++ "~")+ System.Directory.renameFile src dst+ where+ removeIfExists path =+ do exists <- doesFileExist path+ if exists then removeFile path else return ()+ renameIfExists src dst =+ do exists <- doesFileExist src+ if exists then System.Directory.renameFile src dst else return ()++backupFile :: FilePath -> IO ()+backupFile path =+ do+ removeIfExists (path ++ "~")+ System.Directory.renameFile path (path ++ "~")+ where+ removeIfExists path =+ do exists <- doesFileExist path+ if exists then removeFile path else return ()++parentPath :: FilePath -> FilePath+parentPath path = fst (splitFileName path)++ePut :: String -> IO ()+ePut s = hPutStrLn stderr s++bool :: a -> a -> Bool -> a+bool _ t True = t+bool f _ False = f++-- |The mighty consperse function+consperse :: [a] -> [[a]] -> [a]+consperse sep items = concat (intersperse sep items)
+ Linspire/Debian/SourceTree.hs view
@@ -0,0 +1,260 @@+-- |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
+ Linspire/Debian/SourcesList.hs view
@@ -0,0 +1,201 @@+module Linspire.Debian.SourcesList+ (DebSource(DebSource),+ SourceType(Deb, DebSrc),+ parseSourceLine, -- String -> DebSource+ parseSourcesList, -- String -> [DebSource]+ quoteWords, -- String -> [String]+ archFiles) -- FilePath -> Maybe String -> DebSource -> [FilePath]+ where++import Control.Exception+import Data.List+import Network.URI++{-++deb uri distribution [component1] [componenent2] [...]++The URI for the deb type must specify the base of the Debian+distribution, from which APT will find the information it needs.++distribution can specify an exact path, in which case the components+must be omitted and distribution must end with a slash (/).++If distribution does not specify an exact path, at least one component+must be present.++Distribution may also contain a variable, $(ARCH), which expands to+the Debian architecture (i386, m68k, powerpc, ...) used on the+system.++The rest of the line can be marked as a comment by using a #.++Additional Notes:++ + Lines can begin with leading white space.++ + If the dist ends with slash (/), then it must be an absolute path+ and it is an error to specify components after it.++-}++data SourceType+ = Deb | DebSrc+ deriving Eq+ +instance Show SourceType where+ show Deb = "deb"+ show DebSrc = "deb-src"++data DebSource + = DebSource + { sourceType :: SourceType+ , sourceUri :: URI+ , sourceDist :: Either String (String, [String]) -- ^ Either (ExactPath) (Distribution, [Section])+ }+ deriving Eq++instance Show DebSource where+ show (DebSource thetype theuri thedist) =+ (show thetype) ++" "++ (uriToString id theuri " ") +++ case thedist of+ Left exactPath -> escape exactPath+ Right (dist, sections) ->+ (escape dist) ++ " " ++ concat (intersperse " " (map escape sections))+ where escape = escapeURIString isAllowedInURI++-- |quoteWords - similar to words, but with special handling of+-- double-quotes and brackets.+--+-- The handling double quotes and [] is supposed to match:+-- apt-0.6.44.2\/apt-pkg\/contrib\/strutl.cc:ParseQuoteWord()+--+-- The behaviour can be defined as:+--+-- Break the string into space seperated words ignoring spaces that+-- appear between \"\" or []. Strip trailing and leading white space+-- around words. Strip out double quotes, but leave the square+-- brackets intact.+quoteWords :: String -> [String]+quoteWords [] = []+quoteWords s = quoteWords' Nothing (dropWhile (==' ') s)+ where+ quoteWords' :: Maybe Char -> String -> [String]+ quoteWords' Nothing [] = []+ quoteWords' Nothing str =+ case break (flip elem " [\"") str of+ ([],[]) -> []+ (w, []) -> [w]+ (w, (' ':rest)) -> w : (quoteWords' Nothing (dropWhile (==' ') rest))+ (w, ('"':rest)) ->+ case break (== '"') rest of+ (w',('"':rest)) ->+ case quoteWords' Nothing rest of+ [] -> [w ++ w']+ (w'':ws) -> ((w ++ w' ++ w''): ws)+ (w',[]) -> error ("quoteWords: missing \" in the string: " ++ s)+ (w, ('[':rest)) ->+ case break (== ']') rest of+ (w',(']':rest)) ->+ case quoteWords' Nothing rest of+ [] -> [w ++ "[" ++ w' ++ "]"]+ (w'':ws) -> ((w ++ "[" ++ w' ++ "]" ++ w''): ws)+ (w',[]) -> error ("quoteWords: missing ] in the string: " ++ s)++stripLine :: String -> String+stripLine = takeWhile (/= '#') . dropWhile (== ' ')++sourceLines :: String -> [String]+sourceLines = filter (not . null) . map stripLine . lines++-- |parseSourceLine -- parses a source line+-- the argument must be a non-empty, valid source line with comments stripped+-- see: 'sourceLines'+parseSourceLine :: String -> DebSource+parseSourceLine str =+ case quoteWords str of+ (theTypeStr : theUriStr : theDistStr : sectionStrs) ->+ let theType = case unEscapeString theTypeStr of+ "deb" -> Deb+ "deb-src" -> DebSrc+ o -> error ("parseSourceLine: invalid type " ++ o ++ " in line:\n" ++ str)+ theUri = case parseURI theUriStr of+ Nothing -> error ("parseSourceLine: invalid uri " ++ theUriStr ++ " in the line:\n" ++ str)+ Just u -> u+ theDist = unEscapeString theDistStr+ in+ case last theDist of+ '/' -> if null sectionStrs+ then DebSource { sourceType = theType, sourceUri = theUri, sourceDist = Left theDist }+ else error ("parseSourceLine: Dist is an exact path, so sections are not allowed on the line:\n" ++ str)+ _ -> if null sectionStrs+ then error ("parseSourceLine: Dist is not an exact path, so at least one section is required on the line:\n" ++ str)+ else DebSource { sourceType = theType, sourceUri = theUri, sourceDist = Right (theDist, map unEscapeString sectionStrs) }+ _ -> error ("parseSourceLine: invalid line in sources.list:\n" ++ str)+ +parseSourcesList :: String -> [DebSource]+parseSourcesList = map parseSourceLine . sourceLines++-- |Return the list of files that apt-get update would write into+-- \/var\/lib\/apt\/lists when it processed the given list of DebSource.+-- FIXME: remove the root argument from this and just return the names.+archFiles :: FilePath -> Maybe String -> DebSource -> [FilePath]+archFiles root (Just arch) deb@(DebSource Deb _ _) =+ map (++ ("_binary-" ++ arch ++ "_Packages")) (archFiles' root deb)+archFiles root Nothing deb@(DebSource DebSrc _ _) =+ map (++ "_source_Sources") (archFiles' root deb)+archFiles _ _ _ = [] -- error?++archFiles' :: FilePath -> DebSource -> [FilePath]+archFiles' root deb =+ let uri = sourceUri deb+ distro = sourceDist deb in+ let scheme = uriScheme uri+ auth = uriAuthority uri+ path = uriPath uri in+ let userpass = maybe "" uriUserInfo auth+ reg = maybeOfString $ maybe "" uriRegName auth+ port = maybe "" uriPort auth in+ let (user, pass) = break (== ':') userpass in+ let user' = maybeOfString user+ pass' = maybeOfString pass in+ let uriText = prefix scheme user' pass' reg port path in+ -- what about dist?+ either (\ exact -> [root ++ (escapeURIString (/= '@') ("/var/lib/apt/lists/" ++ uriText ++ escape exact))])+ (\ (dist, sections) ->+ map (\ section -> root ++ (escapeURIString (/= '@')+ ("/var/lib/apt/lists/" +++ uriText +++ "_dists_" ++ escape dist ++ "_" +++ escape section)))+ sections)+ distro+ where+ -- If user is given and password is not, the user name is+ -- added to the file name. Otherwise it is not. Really.+ prefix "http:" (Just user) Nothing (Just host) port path =+ user ++ host ++ port ++ escape path+ prefix "http:" _ _ (Just host) port path =+ host ++ port ++ escape path+ prefix "ftp:" _ _ (Just host) _ path =+ host ++ escape path+ prefix "file:" Nothing Nothing Nothing "" path =+ escape path+ prefix "ssh:" (Just user) Nothing (Just host) port path =+ user ++ host ++ port ++ escape path+ prefix "ssh" _ _ (Just host) port path =+ host ++ port ++ escape path+ prefix _ _ _ _ _ _ = error ("invalid DebSource: " ++ show deb)+ maybeOfString "" = Nothing+ maybeOfString s = Just s+ escape s = consperse "_" (wordsBy (== '/') s)++-- |The mighty consperse function+consperse :: [a] -> [[a]] -> [a]+consperse sep items = concat (intersperse sep items)++wordsBy :: Eq a => (a -> Bool) -> [a] -> [[a]]+wordsBy p s = + case (break p s) of+ (s, []) -> [s]+ (h, t) -> h : wordsBy p (drop 1 t)
+ Linspire/Debian/Version.hs view
@@ -0,0 +1,17 @@+-- |A module for parsing, comparing, and (eventually) modifying debian version+-- numbers. <http://www.debian.org/doc/debian-policy/ch-controlfields.html#s-f-Version>+module Linspire.Debian.Version + (DebianVersion -- |Exported abstract because the internal representation is likely to change + , parseDebianVersion+ , epoch+ , version+ , revision+ , buildDebianVersion+ , evr+ ) where ++import Data.Char+import Text.ParserCombinators.Parsec++import Linspire.Debian.Version.Common+import Linspire.Debian.Version.String
+ Linspire/Debian/Version/ByteString.hs view
@@ -0,0 +1,17 @@+module Linspire.Debian.Version.ByteString+ ( ParseDebianVersion(..)+ ) where++import Text.ParserCombinators.Parsec++import qualified Data.ByteString.Char8 as C++import Linspire.Debian.Version.Common+import Linspire.Debian.Version.Internal+ +instance ParseDebianVersion C.ByteString where+ parseDebianVersion byteStr =+ let str = C.unpack byteStr in+ case parse parseDV str str of+ Left e -> error (show e)+ Right dv -> DebianVersion str dv
+ Linspire/Debian/Version/Common.hs view
@@ -0,0 +1,168 @@+-- |A module for parsing, comparing, and (eventually) modifying debian version+-- numbers. <http://www.debian.org/doc/debian-policy/ch-controlfields.html#s-f-Version>+module Linspire.Debian.Version.Common+ (DebianVersion -- |Exported abstract because the internal representation is likely to change + , ParseDebianVersion(..)+ , evr -- DebianVersion -> (Maybe Int, String, Maybe String)+ , epoch+ , version+ , revision+ , buildDebianVersion+ , parseDV+ ) where ++import qualified Control.Exception(try)+import Data.Char+import Text.ParserCombinators.Parsec+import Text.Regex++import Linspire.Debian.Version.Internal++instance Show DebianVersion where+ show (DebianVersion s _) = s++instance Eq DebianVersion where+ (DebianVersion _ v1) == (DebianVersion _ v2) = v1 == v2++instance Ord DebianVersion where+ compare (DebianVersion _ v1) (DebianVersion _ v2) = compare v1 v2++-- * Ord instance++-- make ~ less than everything, and everything else higher that letters+order :: Char -> Int+order c+ | isDigit c = 0+ | isAlpha c = ord c+ | c == '~' = -1+ | otherwise = (ord c) + 256++-- |We have to do this wackiness because ~ is less than the empty string+compareNonNumeric :: [Char] -> [Char] -> Ordering+compareNonNumeric "" "" = EQ+compareNonNumeric "" ('~':cs) = GT+compareNonNumeric ('~':cs) "" = LT+compareNonNumeric "" _ = LT+compareNonNumeric _ "" = GT+compareNonNumeric (c1:cs1) (c2:cs2) =+ if (order c1) == (order c2)+ then compareNonNumeric cs1 cs2+ else compare (order c1) (order c2)++instance Eq NonNumeric where+ (NonNumeric s1 n1) == (NonNumeric s2 n2) =+ case compareNonNumeric s1 s2 of+ EQ -> n1 == n2+ o -> False++instance Ord NonNumeric where+ compare (NonNumeric s1 n1) (NonNumeric s2 n2) =+ case compareNonNumeric s1 s2 of+ EQ -> compare n1 n2+ o -> o++instance Eq Numeric where+ (Numeric n1 mnn1) == (Numeric n2 mnn2) =+ case compare n1 n2 of+ EQ -> case compareMaybeNonNumeric mnn1 mnn2 of+ EQ -> True+ _ -> False+ _ -> False++compareMaybeNonNumeric :: Maybe NonNumeric -> Maybe NonNumeric -> Ordering+compareMaybeNonNumeric mnn1 mnn2 =+ case (mnn1, mnn2) of+ (Nothing, Nothing) -> EQ+ (Just (NonNumeric nn _), Nothing) -> compareNonNumeric nn ""+ (Nothing, Just (NonNumeric nn _)) -> compareNonNumeric "" nn+ (Just nn1, Just nn2) -> compare nn1 nn2++instance Ord Numeric where+ compare (Numeric n1 mnn1) (Numeric n2 mnn2) =+ case compare n1 n2 of+ EQ -> compareMaybeNonNumeric mnn1 mnn2+ o -> o++-- * Parser++class ParseDebianVersion a where+ parseDebianVersion :: a-> DebianVersion+-- |Convert a string to a debian version number. May throw an+-- exception if the string is unparsable -- but I am not sure if that+-- can currently happen. Are there any invalid version strings?+-- Perhaps ones with underscore, or something?++showNN :: NonNumeric -> String+showNN (NonNumeric s n) = s ++ showN n++showN :: Found Numeric -> String+showN (Found (Numeric n nn)) = show n ++ maybe "" showNN nn+showN (Simulated _) = "" ++parseDV :: CharParser () (Found Int, NonNumeric, Found NonNumeric)+parseDV =+ do skipMany $ oneOf " \t"+ e <- parseEpoch + upstreamVersion <- parseNonNumeric True True+ debianRevision <- option (Simulated (NonNumeric "" (Simulated (Numeric 0 Nothing)))) (char '-' >> parseNonNumeric True False >>= return . Found)+ return (e, upstreamVersion, debianRevision)++parseEpoch :: CharParser () (Found Int)+parseEpoch =+ option (Simulated 0) (try (many1 digit >>= \d -> char ':' >> return (Found (read d))))+ ++parseNonNumeric :: Bool -> Bool -> CharParser () NonNumeric+parseNonNumeric zeroOk upstream =+ do nn <- (if zeroOk then many else many1) ((noneOf "-0123456789") <|> (if upstream then upstreamDash else pzero))+ n <- parseNumeric upstream+ return $ NonNumeric nn n+ where+ upstreamDash :: CharParser () Char+ upstreamDash = try $ do char '-'+ lookAhead $ (many (noneOf "- \n\t") >> char '-')+ return '-'++parseNumeric :: Bool -> CharParser () (Found Numeric)+parseNumeric upstream =+ do n <- many1 (satisfy isDigit)+ nn <- option Nothing (parseNonNumeric False upstream >>= return . Just)+ return $ Found (Numeric (read n) nn)+ <|>+ return (Simulated (Numeric 0 Nothing))++compareTest :: String -> String -> Ordering+compareTest str1 str2 =+ let v1 = either (error . show) id $ parse parseDV str1 str1+ v2 = either (error . show) id $ parse parseDV str2 str2+ in + compare v1 v2++-- |Split a DebianVersion into its three components: epoch, version,+-- revision. It is not safe to use the parsed version number for+-- this because you will lose information, such as leading zeros.+evr :: DebianVersion -> (Maybe Int, String, Maybe String)+evr (DebianVersion s _) =+ let re = mkRegex "^(([0-9]+):)?(([^-]*)|((.*)-([^-]*)))$" in+ -- ( ) ( ( ))+ -- ( e ) ( v ) (v2) ( r )+ case matchRegex re s of+ Just ["", _, _, v, "", _, _] -> (Nothing, v, Nothing)+ Just ["", _, _, _, _, v, r] -> (Nothing, v, Just r)+ Just [_, e, _, v, "", _, _] -> (Just (read e), v, Nothing)+ Just [_, e, _, _, _, v, r] -> (Just (read e), v, Just r)+ -- I really don't think this can happen.+ _ -> error ("Invalid Debian Version String: " ++ s)++epoch v = case evr v of (x, _, _) -> x+version v = case evr v of (_, x, _) -> x+revision v = case evr v of (_, _, x) -> x++-- Build a Debian version number from epoch, version, revision+buildDebianVersion :: Maybe Int -> String -> Maybe String -> DebianVersion+buildDebianVersion epoch version revision =+ either (error . show) (DebianVersion str) $ parse parseDV str str+ where+ str = (maybe "" (\ n -> show n ++ ":") epoch +++ version +++ maybe "" (\ s -> "-" ++ s) revision)
+ Linspire/Debian/Version/Internal.hs view
@@ -0,0 +1,33 @@+module Linspire.Debian.Version.Internal+ ( DebianVersion(..)+ , Numeric(..)+ , NonNumeric(..)+ , Found(..)+ )+ where+-- Currently we store the original version string in the data-type so+-- that we can faithfully reproduce it quickly. Currently we do not+-- have any way to modify a version number -- so this works fine. May+-- have to change later.+data DebianVersion+ = DebianVersion String (Found Int, NonNumeric, Found NonNumeric)++data NonNumeric+ = NonNumeric String (Found Numeric)+ deriving Show++data Numeric+ = Numeric Int (Maybe NonNumeric)+ deriving Show+++data Found a+ = Found { unFound :: a }+ | Simulated { unFound :: a }+ deriving Show++instance (Eq a) => Eq (Found a) where+ f1 == f2 = (unFound f1) == (unFound f2)++instance (Ord a) => Ord (Found a) where+ compare f1 f2 = compare (unFound f1) (unFound f2)
+ Linspire/Debian/Version/String.hs view
@@ -0,0 +1,14 @@+module Linspire.Debian.Version.String+ ( ParseDebianVersion(..)+ ) where++import Text.ParserCombinators.Parsec++import Linspire.Debian.Version.Common+import Linspire.Debian.Version.Internal+ +instance ParseDebianVersion String where+ parseDebianVersion str =+ case parse parseDV str str of+ Left e -> error (show e)+ Right dv -> DebianVersion str dv
+ Setup.hs view
@@ -0,0 +1,21 @@+#!/usr/bin/runhaskell++import Distribution.Simple+import Distribution.PackageDescription+import Distribution.Simple.LocalBuildInfo+import System.Exit+import System.Cmd+import System.Directory+import Control.Exception++withCurrentDirectory :: FilePath -> IO a -> IO a+withCurrentDirectory path f = do+ cur <- getCurrentDirectory+ setCurrentDirectory path+ finally f (setCurrentDirectory cur)++runTestScript :: Args -> Bool -> PackageDescription -> LocalBuildInfo -> IO ExitCode+runTestScript args flag pd lbi = withCurrentDirectory "tests" (system "runhaskell Main.hs")++main :: IO ()+main = defaultMainWithHooks defaultUserHooks{runTests = runTestScript}
+ debian.cabal view
@@ -0,0 +1,45 @@+name: debian+version: 1.0+license: BSD3+license-file: debian/copyright+author: Jeremy Shaw+maintainer: Jeremy Shaw <jeremy@n-heptane.com>+stability: volitile+tested-with: GHC+build-depends: base, parsec, mtl, network, unix, Unixutils, regex-compat+synopsis: A set of modules for working with debian control files and packages+description: Modules for parsing debian control files, resolving+ depedencies, comparing version numbers, and other useful stuff.+category: System+extensions: FlexibleInstances+ghc-options: -O2 -funbox-strict-fields+exposed-modules: + Linspire.Debian.AptImage,+ Linspire.Debian.ChangeLog,+ Linspire.Debian.Config,+ Linspire.Debian.Control,+ Linspire.Debian.Control.Common,+ Linspire.Debian.Control.String,+ Linspire.Debian.Control.ByteString,+ Linspire.Debian.Control.PrettyPrint,+ Linspire.Debian.Dependencies,+ Linspire.Debian.DistroCache,+ Linspire.Debian.OSImage,+ Linspire.Debian.Package,+ Linspire.Debian.PackageDeprecated,+ Linspire.Debian.Relation,+ Linspire.Debian.Relation.Common,+ Linspire.Debian.Relation.String,+ Linspire.Debian.Relation.ByteString,+ Linspire.Debian.Repository,+ Linspire.Debian.SourceTree,+ Linspire.Debian.SourcesList,+ Linspire.Debian.Version,+ Linspire.Debian.Version.Common,+ Linspire.Debian.Version.String,+ Linspire.Debian.Version.ByteString+other-modules:+ Linspire.Debian.Version.Internal+extra-source-files:+ tests/Dependencies.hs tests/Main.hs tests/SourcesList.hs tests/Versions.hs debian/changelog + debian/compat debian/control debian/copyright debian/haskell-debian-doc.docs debian/rules
+ debian/changelog view
@@ -0,0 +1,139 @@+haskell-debian (1.2) unstable; urgency=low++ * Cleanup in preperation for upload to hackageDB++ -- Jeremy Shaw <jeremy.shaw@linspire.com> Tue, 3 Apr 2007 11:13:06 -0700++haskell-debian (1.1.1) unstable; urgency=low++ * Handle '#' comments+ * fix quoteWords in SourcesList to handle case where last+ character in string is a quote character+ * Remove dependency on missingh+ * Dependency changes for GHC 6.6++ -- David Fox <david.fox@linspire.com> Tue, 27 Feb 2007 05:59:05 -0800++haskell-debian (1.1) unstable; urgency=low++ * Deprecated Package data-type and updated a bunch of code+ to remove usage of Package type+ + Add Linspire.Debian.Control.PrettyPrinter + + Major update to Linsipre.Debian.Dependencies+ - No longer cares what data-type is used to store the package+ information.+ + Major update to Linsipre.Debian.Package+ - moved a bunch of stuff to PackageDeprecated+ - remaining functions now package data-type independent+ * Added Linspire.Debian and Linspire.Debian.DistroSpec+ * Add raiseFields to Control+ * Remove Tag management code from Version module (it will stay+ in the autobuilder source code.)+ * Fix Control parser so it can handle blank lines at the+ beginning of a file.+ + -- David Fox <david.fox@linspire.com> Sat, 21 Oct 2006 18:48:24 -0400++haskell-debian (1.0.14) unstable; urgency=low++ * Create multiple dists in the local repository, manage it+ using scanIncoming et. al.++ -- David Fox <david.fox@linspire.com> Thu, 17 Aug 2006 21:41:09 -0700++haskell-debian (1.0.13) unstable; urgency=low++ * Repository API cleanup+ * Allow a quilt 'patches' directory to be a SourceTree+ * Remove DistrCache.localPoolDir function, all the distros+ should share a localpool to avoid version number conflicts.++ -- David Fox <david.fox@linspire.com> Thu, 17 Aug 2006 12:40:06 -0700++haskell-debian (1.0.12) unstable; urgency=low++ * Add Config module, fix bug in ssh url formatting.++ -- David Fox <david.fox@linspire.com> Wed, 16 Aug 2006 09:34:00 -0700++haskell-debian (1.0.11) unstable; urgency=low++ * Add DistroCache, Repository, AptImage, and OSImage modules.++ -- David Fox <david.fox@linspire.com> Sat, 12 Aug 2006 08:21:28 -0700++haskell-debian (1.0.10) unstable; urgency=low++ * Add SourceTree, and ChangeLog modules.++ -- David Fox <david.fox@linspire.com> Tue, 8 Aug 2006 15:49:43 -0700++haskell-debian (1.0.9) unstable; urgency=low++ * Add Linspire.Debian.Control.modifyField++ -- David Fox <david.fox@linspire.com> Tue, 8 Aug 2006 10:29:38 -0700++haskell-debian (1.0.8) unstable; urgency=low++ * Add setTag, changeTag, getTag, dropTag, splitTag.++ -- David Fox <david.fox@linspire.com> Sat, 5 Aug 2006 07:02:17 -0700++haskell-debian (1.0.7) unstable; urgency=low++ * Add ByteString based versions of Control, Relation, Version (JS)+ * Fix reading of zero length Control file fields (DSF)+ * add a show method for Relations (DSF)+ * Support revisions containing "-" (JS)+ * Support (nonstandard) multi-line build dependencies (DSF)+ * Fix pretty printing of ArchExpect type (JS)+ * Fix compare for version numbers (JS)+ * Accept tab as the beginning of continuation line+ * Version number unit tests++ -- David Fox <david.fox@linspire.com> Fri, 9 Jun 2006 14:45:24 -0700++haskell-debian (1.0.6) unstable; urgency=low++ * cleaned up to match our emerging standard for debianized haskell libraries (JS)++ -- David Fox <david.fox@linspire.com> Wed, 17 May 2006 10:59:08 -0700++haskell-debian (1.0.5) unstable; urgency=low++ * Fix bug introduced in 1.0.4 where version 1.0.4 < 1.0.4-.++ -- David Fox <david.fox@linspire.com> Fri, 12 May 2006 06:11:56 -0700++haskell-debian (1.0.4) unstable; urgency=low++ * Add accessors for the version components, modify data structure so+ we get revision Nothing when there is no dash in the version string.++ -- David Fox <david.fox@linspire.com> Wed, 10 May 2006 06:37:58 -0700++haskell-debian (1.0.3) unstable; urgency=low++ * Version bump to help with autobuilder development++ -- David Fox <david.fox@linspire.com> Tue, 9 May 2006 16:06:49 -0700++haskell-debian (1.0.2) unstable; urgency=low++ * Export parseControlFromHandle++ -- David Fox <david.fox@linspire.com> Tue, 9 May 2006 14:11:42 -0700++haskell-debian (1.0.1) unstable; urgency=low++ * Jeremy added parseControlFromHandle++ -- David Fox <david.fox@linspire.com> Tue, 9 May 2006 13:40:48 -0700++haskell-debian (1.0.0) unstable; urgency=low++ * Initial Release.++ -- Jeremy Shaw <jeremy.shaw@linspireinc.com> Tue, 4 Apr 2006 16:15:35 -0700+
+ debian/compat view
@@ -0,0 +1,1 @@+4
+ debian/control view
@@ -0,0 +1,43 @@+Source: haskell-debian+Priority: optional+Maintainer: Jeremy Shaw <jeremy.shaw@linspireinc.com>+Build-Depends: debhelper (>= 4.0.0), ghc6 (>= 6.4.1), haskell-devscripts (>= 0.5.11+DARCS), libghc6-cabal-dev (>=1.1.3), libghc6-unixutils-dev (>= 1.2), libghc6-unixutils-prof (>= 1.2), ghc6-prof, libghc6-network-dev, libghc6-hunit-dev, libghc6-mtl-dev+Build-Depends-Indep: haddock, hugs (>= 98.200503.08)+Standards-Version: 3.7.2.1+Section: devel++Package: libghc6-debian-dev+Section: devel+Architecture: any+Depends: ${haskell:Depends}, libghc6-unixutils-dev (>= 1.2), libghc6-network-dev, libghc6-hunit-dev, libghc6-mtl-dev+Suggests: haskell-debian-doc+Description: GHC library for dealing with debian control files and packages+ Modules for parsing debian control files, resolving+ depedencies, comparing version numbers, and other useful stuff.++Package: libghc6-debian-prof+Section: devel+Architecture: any+Depends: ${haskell:Depends}, libghc6-unixutils-prof (>= 1.2), libghc6-network-dev, libghc6-hunit-dev, libghc6-mtl-dev+Suggests: haskell-debian-doc+Description: GHC library for dealing with debian control files and packages+ Modules for parsing debian control files, resolving+ depedencies, comparing version numbers, and other useful stuff.+ .+ This package contains the profiling enabled libraries.++Package: libhugs-debian+Section: devel+Architecture: all+Depends: ${haskell:Depends}+Suggests: haskell-debian-doc+Description: GHC library for dealing with debian control files and packages+ Modules for parsing debian control files, resolving+ depedencies, comparing version numbers, and other useful stuff.++Package: haskell-debian-doc+Section: doc+Architecture: all+Description: Documentation for Haskell Debian library+ Modules for parsing debian control files, resolving+ depedencies, comparing version numbers, and other useful stuff.
+ debian/copyright view
@@ -0,0 +1,26 @@+Copyright (c) 2006, Jeremy Shaw <jeremy@n-heptane.com>+All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions+are met:+1. Redistributions of source code must retain the above copyright+ notice, this list of conditions and the following disclaimer.+2. Redistributions in binary form must reproduce the above copyright+ notice, this list of conditions and the following disclaimer in the+ documentation and/or other materials provided with the distribution.+3. The names of the author may not be used to endorse or promote+ products derived from this software without specific prior written+ permission.++THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE+ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS+OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)+HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT+LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY+OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF+SUCH DAMAGE.
+ debian/haskell-debian-doc.docs view
@@ -0,0 +1,1 @@+dist/doc/html
+ debian/rules view
@@ -0,0 +1,91 @@+#!/usr/bin/make -f+# -*- makefile -*-+# Sample debian/rules that uses debhelper.+# This file was originally written by Joey Hess and Craig Small.+# As a special exception, when this file is copied by dh-make into a+# dh-make output file, you may use that output file without restriction.+# This special exception was added by Craig Small in version 0.37 of dh-make.++# Uncomment this to turn on verbose mode.+#export DH_VERBOSE=1++build: build-stamp+build-stamp:+ dh_testdir++ # Add here commands to compile the package.+ touch build-stamp++clean:+ dh_testdir+ dh_testroot+ rm -f build-stamp++ # Add here commands to clean up after the build process.+ if [ -x setup ] && [ -e .setup-config ] ; then ./setup clean ; fi+ rm -rf setup Setup.hi Setup.ho Setup.o .*config* dist html+ make clean++ dh_clean ++install: build+ dh_testdir+ dh_testroot+ dh_clean -k + dh_installdirs -a ++ # Add here commands to install the package into debian/tmp+ dh_haskell -a++build-indep: build-indep-stamp+build-indep-stamp:+ dh_testdir++install-indep: build-indep+ dh_testdir+ dh_testroot+ dh_clean -k+ dh_installdirs -i++ dh_haskell -i+ ./Setup.hs test+ ./Setup.hs haddock++# Build architecture-independent files here.+binary-indep: build-indep install-indep+ dh_testdir+ dh_testroot+ dh_installchangelogs -i+ dh_installdocs -i+ dh_installexamples -i+ dh_installman -i+ dh_link -i+ dh_strip -i+ dh_compress -i+ dh_fixperms -i+ dh_installdeb -i+ dh_shlibdeps -i+ dh_gencontrol -i+ dh_md5sums -i+ dh_builddeb -i++# Build architecture-dependent files here.+binary-arch: build install+ dh_testdir+ dh_testroot+ dh_installchangelogs -a+ dh_installdocs -a+ dh_installexamples -a+ dh_installman -a+ dh_link -a+ dh_strip -a+ dh_compress -a+ dh_fixperms -a+ dh_installdeb -a+ dh_shlibdeps -a+ dh_gencontrol -a+ dh_md5sums -a+ dh_builddeb -a++binary: binary-indep binary-arch+.PHONY: build clean binary-indep binary-arch binary install build-indep install-indep
+ tests/Dependencies.hs view
@@ -0,0 +1,180 @@+module Dependencies where++import Control.Arrow+import Test.HUnit++import Linspire.Debian.Control.String+import Linspire.Debian.Dependencies hiding (packageVersionParagraph)+import Linspire.Debian.Relation+import Linspire.Debian.Version+import Linspire.Debian.Package+++packageA =+ [ ("Package", " a")+ , ("Version", " 1.0")+ , ("Depends", " b")+ ]++packageB =+ [ ("Package", " b")+ , ("Version", " 1.0")+ ]++packageC =+ [ ("Package", " c")+ , ("Version", " 1.0")+ , ("Depends", " doesNotExist")+ ]++packageD =+ [ ("Package", " d")+ , ("Version", " 1.0")+ , ("Depends", " e | f, g | h")+ ]++packageE =+ [ ("Package", " e")+ , ("Version", " 1.0")+ ]++packageF =+ [ ("Package", " f")+ , ("Version", " 1.0")+ ]++packageG =+ [ ("Package", " g")+ , ("Version", " 1.0")+ ]++packageH =+ [ ("Package", " h")+ , ("Version", " 1.0")+ ]++packageI =+ [ ("Package", " i")+ , ("Version", " 1.0")+ , ("Depends", " k")+ ]++packageJ =+ [ ("Package", " j")+ , ("Version", " 1.0")+ , ("Provides", " k")+ ]++packageK =+ [ ("Package", " k")+ , ("Version", " 1.0")+ ]++++control + = [ packageA+ , packageB+ , packageC+ , packageD+ , packageE+ , packageF+ , packageG+ , packageH+ , packageI+ , packageJ+ , packageK+ ]++depends p =+ case lookup "Depends" p of+ Nothing -> []+ (Just v) -> either (error . show) id (parseRelations v)++mkCSP :: [[(String, String)]] -> String -> ([(String, String)] -> Relations) -> CSP [(String, String)]+mkCSP paragraphs relStr depF =+ CSP { pnm = addProvides providesF paragraphs $ packageNameMap getName paragraphs+ , relations = either (error . show) id (parseRelations relStr)+ , depFunction = depF+ , conflicts = conflicts'+ , packageVersion = packageVersionParagraph+ }+ where+ getName :: [(String, String)] -> String+ getName p = case lookup "Package" p of Nothing -> error "Missing Package field" ; (Just n) -> stripWS n+ conflicts' :: [(String, String)] -> Relations+ conflicts' p = + case lookup "Conflicts" p of+ Nothing -> []+ (Just c) -> either (error . show) id (parseRelations c)++ providesF :: [(String, String)] -> [String]+ providesF p =+ case lookup "Provides" p of+ Nothing -> []+ (Just v) ->+ parseCommaList v++parseCommaList :: String -> [String]+parseCommaList str =+ words $ map (\c -> if c == ',' then ' ' else c) str++packageVersionParagraph :: [(String, String)] -> (String, DebianVersion) +packageVersionParagraph p =+ case lookup "Package" p of+ Nothing -> error $ "Could not find Package in " ++ show p+ (Just n) -> + case lookup "Version" p of+ Nothing -> error $ "Could not find Package in " ++ show p+ (Just v) -> + (stripWS n, parseDebianVersion v)++mapSnd :: (b -> c) -> [(a,b)] -> [(a,c)]+mapSnd f = map (second f)++test1 =+ let csp = mkCSP control "a" depends+ expected = [ (Complete, [packageB, packageA])]+ in+ TestCase (assertEqual "test1" expected (search bt csp))++missing1 =+ let csp = mkCSP control "c" depends+ expected = []+ in+ TestCase (assertEqual "missing1" expected (search bt csp))++ors1 =+ let csp = mkCSP control "d" depends+ expected = [ (Complete, [packageG, packageE, packageD])+ , (Complete, [packageH, packageE, packageD])+ , (Complete, [packageG, packageF, packageD])+ , (Complete, [packageH, packageF, packageD])+ ]+ in+ TestCase (assertEqual "ors1" expected (search bt csp))++provides1 =+ let csp = mkCSP control "i" depends+ expected = [ (Complete, [packageK, packageI])+ , (Complete, [packageJ, packageI])+ ]+ in+ TestCase (assertEqual "provides1" expected (search bt csp))++provides2 =+ let csp = mkCSP control "k" depends+ expected = [ (Complete, [packageK])+ , (Complete, [packageJ])+ ]+ in+ TestCase (assertEqual "provides2" expected (search bt csp))++dependencyTests =+ [ test1+ , missing1+ , ors1+ , provides1+ , provides2+ ]+-- runTestText putTextToShowS test1 >>= \(c,st) -> putStrLn (st "")
+ tests/Main.hs view
@@ -0,0 +1,14 @@+module Main where++import Test.HUnit+import System.Exit+import Versions+import SourcesList++main =+ do (c,st) <- runTestText putTextToShowS (TestList (versionTests ++ sourcesListTests))+ putStrLn (st "")+ case (failures c) + (errors c) of+ 0 -> return ()+ n -> exitFailure+
+ tests/SourcesList.hs view
@@ -0,0 +1,44 @@+module SourcesList where++import Test.HUnit++import Linspire.Debian.SourcesList++-- * Unit Tests++-- TODO: add test cases that test for unterminated double-quote or bracket+testQuoteWords :: Test+testQuoteWords =+ test [ assertEqual "Space seperate words, no quoting" ["hello", "world","!"] (quoteWords " hello world ! ")+ , assertEqual "Space seperate words, double quotes" ["hello world","!"] (quoteWords " hel\"lo world\" ! ")+ , assertEqual "Space seperate words, square brackets" ["hel[lo worl]d","!"] (quoteWords " hel[lo worl]d ! ")+ , assertEqual "Space seperate words, square-bracket at end" ["hel[lo world]"] (quoteWords " hel[lo world]")+ , assertEqual "Space seperate words, double quote at end" ["hello world"] (quoteWords " hel\"lo world\"")+ ]++testSourcesList :: Test+testSourcesList =+ test [ assertEqual "valid sources.list" validSourcesListExpected (unlines . map show . parseSourcesList $ validSourcesListStr) ]+ where+ validSourcesListStr =+ unlines $ [ " # A comment only line "+ , " deb ftp://ftp.debian.org/debian unstable main contrib non-free # typical deb line"+ , " deb-src ftp://ftp.debian.org/debian unstable main contrib non-free # typical deb-src line"+ , ""+ , "# comment line"+ , "deb http://pkg-kde.alioth.debian.org/kde-3.5.0/ ./ # exact path"+ , "deb http://ftp.debian.org/whee \"space dist\" main"+ , "deb http://ftp.debian.org/whee dist space%20section"+ ]+ validSourcesListExpected =+ unlines $ [ "deb ftp://ftp.debian.org/debian unstable main contrib non-free"+ , "deb-src ftp://ftp.debian.org/debian unstable main contrib non-free"+ , "deb http://pkg-kde.alioth.debian.org/kde-3.5.0/ ./"+ , "deb http://ftp.debian.org/whee space%20dist main"+ , "deb http://ftp.debian.org/whee dist space%20section"+ ]+ invalidSourcesListStr1 = "deb http://pkg-kde.alioth.debian.org/kde-3.5.0/ ./ main contrib non-free # exact path with sections"+ ++sourcesListTests =+ [ testQuoteWords, testSourcesList ]
+ tests/Versions.hs view
@@ -0,0 +1,111 @@+module Versions where++import Test.HUnit++import Linspire.Debian.Version++-- * Implicit Values++implicit1 =+ TestCase (assertEqual "1.0 == 1.0-" EQ (compare (parseDebianVersion "1.0") (parseDebianVersion "1.0-")))++implicit2 =+ TestCase (assertEqual "1.0 == 1.0-0" EQ (compare (parseDebianVersion "1.0") (parseDebianVersion "1.0-0")))++implicit3 = + TestCase (assertEqual "1.0 == 0:1.0-0" EQ (compare (parseDebianVersion "1.0") (parseDebianVersion "0:1.0-0")))++implicit4 = + TestCase (assertEqual "1.0 == 1.0-" EQ (compare (parseDebianVersion "1.0") (parseDebianVersion "1.0-")))++implicit5 = + TestCase (assertEqual "apple = apple0" EQ (compare (parseDebianVersion "apple") (parseDebianVersion "apple0")))++implicit6 = + TestCase (assertEqual "apple = apple0-" EQ (compare (parseDebianVersion "apple") (parseDebianVersion "apple0-")))++implicit7 = + TestCase (assertEqual "apple = apple0-0" EQ (compare (parseDebianVersion "apple") (parseDebianVersion "apple0-0")))++-- * epoch, version, revision++epoch1 =+ TestCase (assertEqual "epoch 0:0" (Just 0) (epoch $ parseDebianVersion "0:0"))++epoch2 =+ TestCase (assertEqual "epoch 0" Nothing(epoch $ parseDebianVersion "0"))++epoch3 =+ TestCase (assertEqual "epoch 1:0" (Just 1) (epoch $ parseDebianVersion "1:0"))++version1 =+ TestCase (assertEqual "version apple" "apple" (version $ parseDebianVersion "apple"))++version2 =+ TestCase (assertEqual "version apple0" "apple0" (version $ parseDebianVersion "apple0"))++version3 =+ TestCase (assertEqual "version apple1" "apple1" (version $ parseDebianVersion "apple1"))++revision1 =+ TestCase (assertEqual "revision 1.0" Nothing (revision $ parseDebianVersion "1.0"))++revision2 =+ TestCase (assertEqual "revision 1.0-" (Just "") (revision $ parseDebianVersion "1.0-"))++revision3 =+ TestCase (assertEqual "revision 1.0-0" (Just "0") (revision $ parseDebianVersion "1.0-0"))++revision4 =+ TestCase (assertEqual "revision 1.0-apple" (Just "apple") (revision $ parseDebianVersion "1.0-apple"))+++-- * Ordering++compareV str1 str2 = compare (parseDebianVersion str1) (parseDebianVersion str2)++order1 =+ TestCase (assertEqual "1:1-1 > 0:1-1" GT (compareV "1:1-1" "0:1-1"))++order2 =+ TestCase (assertEqual "1-1-1 > 1-1" GT (compareV "1-1-1" "1-1"))++-- * Dashes in upstream version++dash1 =+ TestCase (assertEqual "version of upstream-version-revision" "upstream-version" (version (parseDebianVersion "upstream-version-revision")))++dash2 =+ TestCase (assertEqual "revision of upstream-version-revision" (Just "revision") (revision (parseDebianVersion "upstream-version-revision")))++-- * Insignificant Zero's++zero1 =+ TestCase (assertEqual "0.09 = 0.9" EQ (compareV "0.09" "0.9"))++-- * Tests++versionTests =+ [ TestLabel "implicit1" implicit1+ , TestLabel "implicit2" implicit2+ , TestLabel "implicit3" implicit3+ , TestLabel "implicit4" implicit4+ , TestLabel "implicit5" implicit5+ , TestLabel "implicit5" implicit6+ , TestLabel "implicit5" implicit7+ , TestLabel "epoch1" epoch1+ , TestLabel "epoch2" epoch2+ , TestLabel "epoch3" epoch3+ , TestLabel "version1" version1+ , TestLabel "version2" version2+ , TestLabel "version3" version3+ , TestLabel "revision1" revision1+ , TestLabel "revision2" revision2+ , TestLabel "revision3" revision3+ , TestLabel "revision4" revision4+ , TestLabel "order1" order1+ , TestLabel "order2" order2+ , dash1+ , dash2+ , zero1+ ]