-
Notifications
You must be signed in to change notification settings - Fork 0
/
StackCache.hs
executable file
·241 lines (204 loc) · 8.46 KB
/
StackCache.hs
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
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE DerivingStrategies #-}
#!/usr/bin/env stack
-- stack --resolver lts-16.3 script --package yaml --package aeson --package universum --package shelly --package optparse-applicative --package text
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE ExtendedDefaultRules #-}
{-# OPTIONS_GHC -fno-warn-type-defaults #-}
module Main where
import Universum
import Shelly hiding (whenM, unlessM, cd, rm_rf, test_e, test_d, mv, mkdir_p, lsT, test_s)
import qualified Shelly as S
import Options.Applicative hiding (command)
import Options.Applicative.Help ((.$.), Doc)
import qualified Options.Applicative as O
import qualified Data.Yaml as Y
import qualified Data.Text as T
import System.Environment (getEnv)
import GHC.IO (unsafeDupablePerformIO)
import qualified System.Posix.Files as Posix
import qualified System.Posix.Directory as Posix
import qualified GHC.IO.Exception as IOE
default (Text)
main :: IO ()
main =
execParser (info (opts <**> helper) (
fullDesc
<> header "stack-cache – manage stack-work cache during SCM branch switches"
<> footerDoc (Just doc)
))
>>= shelly
doc :: Doc
doc = "SETUP: "
.$. "1. Add stack-cache to $PATH"
.$. "2. Perform stack-cache init from the directory with stack.yaml"
.$. "3. Add post-checkout git hook to call `stack-cache relink` on branch change"
.$. ""
.$. "4. Make sure to ignore stack-cache directories:"
.$. "$ git config --global core.excludesfile ~/.gitignore"
.$. "$ echo .stack-work-cache >> ~/.gitignore"
.$. "$ echo .stack-work >> ~/.gitignore"
.$. ""
.$. "5. Periodically run stack-cache clean-all to free some disk space"
opts :: Parser (Sh ())
opts = subparser
( O.command "init" (info (pure initCache) (progDesc "Create initial directory structure"))
<> O.command "relink" (info (pure $ check >> relink) (progDesc "Relink stack-work from current branch cache"))
<> O.command "check" (info (pure check) (progDesc "Check if stack-cache is properly inited (do before switching branches)"))
<> O.command "clone" (info ((clone . Branch) <$> argument str (metavar "BRANCH")) (progDesc "Clone stack-work from other branch cache"))
<> O.command "clean-branch" (info ((cleanBranch . Branch) <$> argument str (metavar "BRANCH")) (progDesc "Delete sepecified branch cache"))
<> O.command "clean-current" (info (pure $ withCurrentBranch cleanBranch) (progDesc "Remove current stack-work, use relink to copy from master"))
<> O.command "clean-all" (info (pure $ cleanAll) (progDesc "Deletes cache of all branches except current branch and master"))
)
check :: Sh ()
check = do
withCurrentBranch $ \currentBranch -> do
results <- forAllPackages $ \package -> sub $ do
cd package
exists <- test_e stackWork
isSymlink <- test_s stackWork
case (exists, isSymlink) of
(True, False) -> pure True
_ -> pure False
let failed = any snd results
unless failed $ putTextLn "Symlinks OK"
when failed $ do
putTextLn ".stack-work for some packages is not a symlink, aborting relink – please do stack-cache init, affected packages:"
forM_ (fmap fst . filter snd $ results) (putTextLn . (" - "<>))
errorExit "stack-cache is not properly initialized"
relink :: Sh ()
relink = do
withCurrentBranch $ \currentBranch -> do
forAllPackages_ $ \package -> sub $ do
cd package
exists <- test_e stackWork
isSymlink <- test_s stackWork
case (exists, isSymlink) of
(False, _) -> pass
(True, True) -> cmd "rm" stackWork
(True, False) -> errorExit $ ".stack-work for " <> package <> " is not a symlink"
cacheExists <- test_d_not_l (cacheDir currentBranch)
masterExists <- test_d_not_l $ cacheDir master
unless cacheExists $ do
if masterExists
then do
putText "C"
copyCoW (cacheDir master) (cacheDir currentBranch)
else do
mkdir_p $ cacheDir currentBranch
ln_s (cacheDir currentBranch) stackWork
clone :: Branch -> Sh ()
clone fromBranch = do
withCurrentBranch $ \currentBranch -> do
cleanBranch currentBranch
forAllPackages_ $ \package -> sub $ do
cd package
copyCoW (cacheDir fromBranch) (cacheDir currentBranch)
initCache :: Sh ()
initCache = do
withCurrentBranch $ \currentBranch -> do
forAllPackages_ $ \package -> sub $ do
cd package
mkdir_p cacheRoot
whenM (test_d_not_l stackWork) $
unlessM (test_d_not_l $ cacheDir currentBranch) $
mv stackWork $ cacheDir currentBranch
rm_rf stackWork
relink
forAllPackages_ $ \package -> sub $ do
cd package
ignoreBackups ".stack-work-cache"
ignoreBackups stackWork
cleanBranch :: Branch -> Sh ()
cleanBranch branch = do
forAllPackages_ $ \package -> sub $ do
cd package
rm_rf (cacheDir branch)
cleanAll :: Sh ()
cleanAll = do
currentBranch <- getCurrentBranch
cleanAllExcept $ catMaybes [currentBranch, Just master]
cleanAllExcept :: [Branch] -> Sh ()
cleanAllExcept branches = do
let ignoreDirs = cacheDir <$> branches
forAllPackages_ $ \package -> sub $ do
cd package
allDirs <- lsT cacheRoot
let dirsToRemove = filter (\dir -> not (dir `elem` ignoreDirs)) allDirs
forM_ dirsToRemove $ \dir -> do
rm_rf dir
newtype Branch = Branch { unBranch :: Text }
deriving newtype (Show, Eq, IsString)
master = Branch "master"
sanitizeBranch :: Branch -> Text
sanitizeBranch (Branch branch) = T.strip $ T.replace "/" "_" branch
cacheDir :: Branch -> Text
cacheDir branch = cacheRoot <> "/" <> sanitizeBranch branch
cacheRoot :: Text
cacheRoot = "./.stack-work-cache/branch"
stackWork :: Text
stackWork = "./.stack-work"
data StackYAML = StackYAML { packages :: [Text] } deriving (Generic, Y.FromJSON)
allPackages :: MonadIO m => m [Text]
allPackages = packages <$> liftIO (Y.decodeFileThrow "stack.yaml")
forAllPackages_ :: MonadIO m => (Text -> m ()) -> m ()
forAllPackages_ = void . forAllPackages
forAllPackages :: MonadIO m => (Text -> m a) -> m [(Text, a)]
forAllPackages action = do
packages <- allPackages
results <- forM packages $ \package -> do
result <- action package
putText "."
pure (package, result)
putText "\n"
pure results
git = cmd "/usr/bin/env" "git"
ln_s = cmd "ln" "-s" :: Text -> Text -> Sh ()
getCurrentBranch :: Sh (Maybe Branch)
getCurrentBranch = do
refName <- silently $ Branch <$> git "rev-parse" "--abbrev-ref" "HEAD" -- git "symbolic-ref" "HEAD"
pure $ if refName /= "HEAD"
then Just refName
else Nothing
withCurrentBranch :: (Branch -> Sh ()) -> Sh ()
withCurrentBranch action = do
branchNameMb <- getCurrentBranch
case branchNameMb of
Just branchName -> action branchName
Nothing -> putTextLn "Not on branch, doing nothing."
ignoreBackups :: Text -> Sh ()
ignoreBackups = when isMacOS . cmd "tmutil" "addexclusion"
copyCoW :: Text -> Text -> Sh ()
copyCoW from to = do
when isMacOS $ cmd "cp" "-cpr" from to
when isLinux $ cmd "cp" "--reflink=always" "-pr" from to
osType :: Text
osType = toText $ unsafeDupablePerformIO $ shelly $ silently $ cmd "uname"
isMacOS, isLinux :: Bool
isMacOS = T.isPrefixOf "Darwin" osType
isLinux = T.isPrefixOf "Linux" osType
-- Shelly API is a mess wrt. Text vs. String issue and FilePath aka String is not well supported by `cmd` in last version
cd = S.cd . toString
lsT = S.lsT . toString
rm_rf = S.rm_rf . toString
mkdir_p = S.mkdir_p . toString
mv f t = S.mv (toString f) (toString t)
test_e = fmap isJust . getFileStatus
test_d = fmap (maybe False Posix.isDirectory) . getFileStatus
test_s = fmap (maybe False Posix.isSymbolicLink) . getFileStatus
getFileStatus :: Text -> Sh (Maybe Posix.FileStatus)
getFileStatus = fmap toText . absPath . toString >=> \path -> liftIO $ do
result <- try $ Posix.getSymbolicLinkStatus (toString path)
case result of
Left err | IOE.ioe_type err == IOE.NoSuchThing -> pure Nothing
Left err -> IOE.ioError err
Right status -> pure $ Just status
test_d_not_l :: Text -> Sh Bool
test_d_not_l path = do
status <- getFileStatus path
pure $ case status of
Just status -> Posix.isDirectory status && not (Posix.isSymbolicLink status)
Nothing -> False