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
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
--------------------------------------------------------------------------------
--- This module implements the local package cache. The local package cache is
--- located in the .cpm/package_cache of the current package. It contains
--- symlinks to all dependencies used by the current package. Package files are
--- copied from the local cache to the runtime cache when they need to be used.
--- The package manager usually creates symlinks to the global package cache.
--- Symlinks to other locations can be used to include modified versions of
--- packages that are not yet published to the package repository or installed
--- in the global cache.
--------------------------------------------------------------------------------

module CPM.PackageCache.Local
  ( cacheDir
  , createLinkToGlobalCache
  , linkPackages
  , clearCache
  , createLink
  , doesLinkPointToGlobalCache
  , packageDir
  , isPackageInCache
  , allPackages
  ) where

import Debug.Trace
import System.Directory ( createDirectoryIfMissing, copyFile, getAbsolutePath
                        , getDirectoryContents, doesDirectoryExist
                        , doesFileExist )
import System.FilePath  ( (</>) )
import Data.Either      ( rights )
import Data.List        ( isPrefixOf )
import Control.Monad
import System.IOExts    ( readCompleteFile )

import CPM.Config       ( Config, packageInstallDir )
import CPM.ErrorLogger
import CPM.FileUtil     ( isSymlink, removeSymlink, createSymlink, linkTarget )
import CPM.Package      ( Package, packageId, readPackageSpec )
import CPM.PackageCache.Global ( installedPackageDir )

--- The cache directory of the local package cache.
---
--- @param dir the package directory
cacheDir :: String -> String
cacheDir pkgDir = pkgDir </> ".cpm" </> "package_cache"

--- Reads all packages specifications from the local package cache.
---
--- @param dir the package directory
allPackages :: String -> ErrorLogger [Package]
allPackages pkgDir = do
  cacheExists <- liftIOEL $ doesDirectoryExist cdir
  if cacheExists
    then do
      logDebug $ "Reading local package cache from '" ++ cdir ++ "'..."
      cdircont <- liftIOEL $ getDirectoryContents cdir
      let pkgDirs = filter (not . isPrefixOf ".") cdircont
      pkgPaths <- liftIOEL $ mapM removeIfIllegalSymLink $ map (cdir </>) pkgDirs
      let specPaths = map (</> "package.json") $ concat pkgPaths
      specs <- liftIOEL $ mapM (readPackageSpecIO . readCompleteFile) specPaths
      return $ rights specs
    else return []
 where
  readPackageSpecIO = fmap readPackageSpec
  cdir = cacheDir pkgDir

  removeIfIllegalSymLink target = do
    dirExists  <- doesDirectoryExist target
    fileExists <- doesFileExist target
    isLink     <- isSymlink target
    if isLink && (dirExists || fileExists)
      then return [target]
      else when isLink (removeSymlink target >> return ()) >> return []

--- Creates a link to a package from the global cache in the local cache. Does
--- not overwrite existing links.
---
--- @param cfg the current configuration
--- @param dir the package directory
--- @param gc the global package cache
--- @param pkg the package to copy
createLinkToGlobalCache :: Config -> String -> Package -> ErrorLogger ()
createLinkToGlobalCache cfg pkgDir pkg =
  createLink pkgDir (installedPackageDir cfg pkg) (packageId pkg) False

--- Links a list of packages from the global cache into the local cache. Does
--- not overwrite existing links.
---
--- @param cfg the current configuration
--- @param dir the package directory
--- @param gc the global package cache
--- @param pkgs the list of packages
linkPackages :: Config -> String -> [Package]
             -> ErrorLogger ()
linkPackages cfg pkgDir pkgs =
  mapM (createLinkToGlobalCache cfg pkgDir) pkgs >> return ()

--- Tests whether a link in the local package cache points to a package in the
--- global package cache.
---
--- @param cfg the current configuration
--- @param gc the global package cache
--- @param dir the package directory
--- @param name the name of the link
doesLinkPointToGlobalCache :: Config -> String -> String -> IO Bool
doesLinkPointToGlobalCache cfg pkgDir name = do
  target <- linkTarget link
  return $ isPrefixOf (packageInstallDir cfg) target
 where
  link = (cacheDir pkgDir) </> name

--- Calculates the local package path of the given package
---
--- @param dir the package directory
--- @param pkg the package
packageDir :: String -> Package -> String
packageDir pkgDir pkg = (cacheDir pkgDir) </> (packageId pkg)

--- Checks whether a package is in the local cache.
---
--- @param dir the package directory
--- @param pkg the package
isPackageInCache :: String -> Package -> IO Bool
isPackageInCache pkgDir pkg = do
  dirExists <- doesDirectoryExist packageDir'
  fileExists <- doesFileExist packageDir'
  return $ dirExists || fileExists
 where
  packageDir' = packageDir pkgDir pkg

--- Clear the local package cache.
---
--- @param dir the package directory
clearCache :: String -> ErrorLogger ()
clearCache pkgDir = do
  cacheExists <- liftIOEL $ doesDirectoryExist cdir
  if cacheExists
    then do
      pkgDirs <- liftIOEL $ getDirectoryContents cdir
      mapM deleteIfLink (map (cdir </>) $ filter (not . isDotOrDotDot) pkgDirs)
      return ()
    else return ()
 where
  cdir = cacheDir pkgDir

ensureCacheDir :: String -> IO String
ensureCacheDir pkgDir = do
  createDirectoryIfMissing True (cacheDir pkgDir)
  return (cacheDir pkgDir)

deleteIfLink :: String -> ErrorLogger ()
deleteIfLink target = do
  dirExists  <- liftIOEL $ doesDirectoryExist target
  fileExists <- liftIOEL $ doesFileExist target
  isLink     <- liftIOEL $ isSymlink target
  if dirExists || fileExists
    then
      if isLink
        then liftIOEL (removeSymlink target) >> return ()
        else fail $ "deleteIfLink can only delete links!\n" ++
                      "Unexpected target: " ++ target
    else
      if isLink -- maybe it is a link to some non-existing target
        then liftIOEL (removeSymlink target) >> return ()
        else return ()

linkExists :: String -> IO Bool
linkExists target = do
  dirExists <- doesDirectoryExist target
  fileExists <- doesFileExist target
  if dirExists || fileExists
    then isSymlink target
    else return False

isDotOrDotDot :: String -> Bool
isDotOrDotDot s = case s of
  "."  -> True
  ".." -> True
  _    -> False

--- Create a link from a directory into the local package cache.
---
--- @param pkgDir the package directory
--- @param from the source directory to be linked into the local cache
--- @param name the name of the link in the package directory (should be a
---        package id)
--- @param replace replace existing link?
createLink :: String -> String -> String -> Bool -> ErrorLogger ()
createLink pkgDir from name replace = do
  liftIOEL $ ensureCacheDir pkgDir
  exists <- liftIOEL $ linkExists target
  if exists && not replace
    then return ()
    else do
      deleteIfLink target
      fromabs <- liftIOEL $ getAbsolutePath from
      rc <- liftIOEL $ createSymlink fromabs target
      if rc == 0
        then return ()
        else fail $ "Failed to create symlink from '" ++ from ++ "' to '" ++
                      target ++ "', return code " ++ show rc
 where
  target = cacheDir pkgDir </> name