File: World.hs

package info (click to toggle)
haskell-cabal-install 1.20.0.3-1
  • links: PTS, VCS
  • area: main
  • in suites: jessie, jessie-kfreebsd
  • size: 1,324 kB
  • ctags: 10
  • sloc: haskell: 18,563; sh: 225; ansic: 36; makefile: 6
file content (174 lines) | stat: -rw-r--r-- 6,446 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
-----------------------------------------------------------------------------
-- |
-- Module      :  Distribution.Client.World
-- Copyright   :  (c) Peter Robinson 2009
-- License     :  BSD-like
--
-- Maintainer  :  thaldyron@gmail.com
-- Stability   :  provisional
-- Portability :  portable
--
-- Interface to the world-file that contains a list of explicitly
-- requested packages. Meant to be imported qualified.
--
-- A world file entry stores the package-name, package-version, and
-- user flags.
-- For example, the entry generated by
-- # cabal install stm-io-hooks --flags="-debug"
-- looks like this:
-- # stm-io-hooks -any --flags="-debug"
-- To rebuild/upgrade the packages in world (e.g. when updating the compiler)
-- use
-- # cabal install world
--
-----------------------------------------------------------------------------
module Distribution.Client.World (
    WorldPkgInfo(..),
    insert,
    delete,
    getContents,
  ) where

import Distribution.Package
         ( Dependency(..) )
import Distribution.PackageDescription
         ( FlagAssignment, FlagName(FlagName) )
import Distribution.Verbosity
         ( Verbosity )
import Distribution.Simple.Utils
         ( die, info, chattyTry, writeFileAtomic )
import Distribution.Text
         ( Text(..), display, simpleParse )
import qualified Distribution.Compat.ReadP as Parse
import Distribution.Compat.Exception ( catchIO )
import qualified Text.PrettyPrint as Disp
import Text.PrettyPrint ( (<>), (<+>) )


import Data.Char as Char

import Data.List
         ( unionBy, deleteFirstsBy, nubBy )
import Data.Maybe
         ( isJust, fromJust )
import System.IO.Error
         ( isDoesNotExistError )
import qualified Data.ByteString.Lazy.Char8 as B
import Prelude hiding (getContents)


data WorldPkgInfo = WorldPkgInfo Dependency FlagAssignment
  deriving (Show,Eq)

-- | Adds packages to the world file; creates the file if it doesn't
-- exist yet. Version constraints and flag assignments for a package are
-- updated if already present. IO errors are non-fatal.
insert :: Verbosity -> FilePath -> [WorldPkgInfo] -> IO ()
insert = modifyWorld $ unionBy equalUDep

-- | Removes packages from the world file.
-- Note: Currently unused as there is no mechanism in Cabal (yet) to
-- handle uninstalls. IO errors are non-fatal.
delete :: Verbosity -> FilePath -> [WorldPkgInfo] -> IO ()
delete = modifyWorld $ flip (deleteFirstsBy equalUDep)

-- | WorldPkgInfo values are considered equal if they refer to
-- the same package, i.e., we don't care about differing versions or flags.
equalUDep :: WorldPkgInfo -> WorldPkgInfo -> Bool
equalUDep (WorldPkgInfo (Dependency pkg1 _) _)
          (WorldPkgInfo (Dependency pkg2 _) _) = pkg1 == pkg2

-- | Modifies the world file by applying an update-function ('unionBy'
-- for 'insert', 'deleteFirstsBy' for 'delete') to the given list of
-- packages. IO errors are considered non-fatal.
modifyWorld :: ([WorldPkgInfo] -> [WorldPkgInfo]
                -> [WorldPkgInfo])
                        -- ^ Function that defines how
                        -- the list of user packages are merged with
                        -- existing world packages.
            -> Verbosity
            -> FilePath               -- ^ Location of the world file
            -> [WorldPkgInfo] -- ^ list of user supplied packages
            -> IO ()
modifyWorld _ _         _     []   = return ()
modifyWorld f verbosity world pkgs =
  chattyTry "Error while updating world-file. " $ do
    pkgsOldWorld <- getContents world
    -- Filter out packages that are not in the world file:
    let pkgsNewWorld = nubBy equalUDep $ f pkgs pkgsOldWorld
    -- 'Dependency' is not an Ord instance, so we need to check for
    -- equivalence the awkward way:
    if not (all (`elem` pkgsOldWorld) pkgsNewWorld &&
            all (`elem` pkgsNewWorld) pkgsOldWorld)
      then do
        info verbosity "Updating world file..."
        writeFileAtomic world . B.pack $ unlines
            [ (display pkg) | pkg <- pkgsNewWorld]
      else
        info verbosity "World file is already up to date."


-- | Returns the content of the world file as a list
getContents :: FilePath -> IO [WorldPkgInfo]
getContents world = do
  content <- safelyReadFile world
  let result = map simpleParse (lines $ B.unpack content)
  if all isJust result
    then return $ map fromJust result
    else die "Could not parse world file."
  where
  safelyReadFile :: FilePath -> IO B.ByteString
  safelyReadFile file = B.readFile file `catchIO` handler
    where
      handler e | isDoesNotExistError e = return B.empty
                | otherwise             = ioError e


instance Text WorldPkgInfo where
  disp (WorldPkgInfo dep flags) = disp dep <+> dispFlags flags
    where
      dispFlags [] = Disp.empty
      dispFlags fs = Disp.text "--flags="
                  <> Disp.doubleQuotes (flagAssToDoc fs)
      flagAssToDoc = foldr (\(FlagName fname,val) flagAssDoc ->
                             (if not val then Disp.char '-'
                                         else Disp.empty)
                             Disp.<> Disp.text fname
                             Disp.<+> flagAssDoc)
                           Disp.empty
  parse = do
      dep <- parse
      Parse.skipSpaces
      flagAss <- Parse.option [] parseFlagAssignment
      return $ WorldPkgInfo dep flagAss
    where
      parseFlagAssignment :: Parse.ReadP r FlagAssignment
      parseFlagAssignment = do
          _ <- Parse.string "--flags"
          Parse.skipSpaces
          _ <- Parse.char '='
          Parse.skipSpaces
          inDoubleQuotes $ Parse.many1 flag
        where
          inDoubleQuotes :: Parse.ReadP r a -> Parse.ReadP r a
          inDoubleQuotes = Parse.between (Parse.char '"') (Parse.char '"')

          flag = do
            Parse.skipSpaces
            val <- negative Parse.+++ positive
            name <- ident
            Parse.skipSpaces
            return (FlagName name,val)
          negative = do
            _ <- Parse.char '-'
            return False
          positive = return True

          ident :: Parse.ReadP r String
          ident = do
            -- First character must be a letter/digit to avoid flags
            -- like "+-debug":
            c  <- Parse.satisfy Char.isAlphaNum
            cs <- Parse.munch (\ch -> Char.isAlphaNum ch || ch == '_'
                                                         || ch == '-')
            return (c:cs)