-
Notifications
You must be signed in to change notification settings - Fork 21
/
Antigen.hs
289 lines (240 loc) · 9.12 KB
/
Antigen.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
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
{-# LANGUAGE CPP #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
module Antigen
( antigen
, AntigenConfig(..)
, defaultConfig
, ZshPlugin(..)
, RepoStorage(..)
, SourcingStrategy
, defaultZshPlugin
, bundle
, local
, strictSourcingStrategy
, antigenSourcingStrategy
, filePathsSourcingStrategy
) where
#if __GLASGOW_HASKELL__ < 709
import Control.Applicative
#endif
import Control.Exception (bracket_)
import Control.Monad
import Data.List (isSuffixOf)
import Data.Maybe (fromMaybe)
import Data.Monoid ((<>))
import Data.Text (Text)
import System.Directory (createDirectoryIfMissing, doesDirectoryExist,
getCurrentDirectory, getDirectoryContents,
getHomeDirectory, setCurrentDirectory)
import System.Environment (lookupEnv)
import System.Exit (exitFailure)
import System.FilePath (isRelative, normalise, (</>))
import qualified Data.Text as Text
import qualified Data.Text.IO as Text
import qualified System.Process as Proc
-- | Configuration of Antigen.
data AntigenConfig = AntigenConfig
{ plugins :: ![ZshPlugin]
-- ^ List of plugins to install.
, outputDirectory :: FilePath
-- ^ Directory to which all generated files will be written.
--
-- This may be overridden with the @ANTIGEN_HS_OUT@ environment variable.
}
-- | Get a default AntigenConfig.
defaultConfig :: AntigenConfig
defaultConfig = AntigenConfig
{ plugins = []
, outputDirectory = ".antigen-hs"
}
-- | Data type representing a zsh plugin
data ZshPlugin = ZshPlugin
{ storage :: RepoStorage
-- ^ Where can this plugin be found?
--
-- If this is a git repository, it will automatically be checked out in a
-- system-determined location.
, sourcingStrategy :: SourcingStrategy
-- ^ How to determine which scripts to source and in what order?
, sourcingLocations :: [FilePath]
-- ^ List of paths relative to the plugin root in which @sourcingStrategy@
-- will be executed.
, fpathLocations :: [FilePath]
-- ^ Paths relative to plugin root which will be added to @fpath@.
}
-- | The SourcingStrategy is executed inside the plugin's @sourcingLocations@,
-- and the paths returned by it are sourced in-order.
type SourcingStrategy = IO [FilePath]
-- | A location where the plugin may be found.
data RepoStorage
= -- | The plugin is available in a GitHub repository.
--
-- A local copy will be cloned.
GitRepository
{ url :: !Text
-- ^ Example: https://github.com/Tarrasch/zsh-functional
}
| -- | The plugin is available locally.
Local
{ filePath :: !FilePath
-- ^ See 'local'
}
deriving (Show, Eq)
-- | Directory where the repositories will be stored.
reposDirectory :: AntigenConfig -> FilePath
reposDirectory config = outputDirectory config </> "repos"
-- | The final output script.
--
-- This is automatically sourced by init.zsh.
outputFileToSource :: AntigenConfig -> FilePath
outputFileToSource config = outputDirectory config </> "antigen-hs.zsh"
-- | A default ZshPlugin
--
-- The default plugin uses the 'strictSourcingStrategy' inside the plugin
-- root, and adds the plugin root to @fpaths@.
defaultZshPlugin :: ZshPlugin
defaultZshPlugin = ZshPlugin
{ storage = error "Please specify a plugin storage."
, sourcingStrategy = strictSourcingStrategy
, sourcingLocations = ["."]
, fpathLocations = [""]
}
-- | Like @antigen bundle@ from antigen. It assumes you want a GitHub
-- repository.
bundle :: Text -> ZshPlugin
bundle repo = defaultZshPlugin
{ storage = GitRepository $ "https://github.com/" <> repo
}
-- | A local repository, useful when testing plugins
--
-- > local "/home/arash/repos/zsh-snakebite-completion"
local :: FilePath -> ZshPlugin
local filePath = defaultZshPlugin
{ storage = Local filePath
} -- TODO should resolve path to absolute
-- | Get the folder in which the storage will be stored on disk
storagePath :: AntigenConfig -> RepoStorage -> FilePath
storagePath config storage = case storage of
GitRepository repo ->
reposDirectory config </> Text.unpack (santitize repo)
Local path -> path
where
santitize = Text.concatMap aux
aux ':' = "-COLON-"
aux '/' = "-SLASH-"
aux c = Text.singleton c
-- | Clone the repository if it already doesn't exist.
ensurePlugin :: AntigenConfig -> RepoStorage -> IO ()
ensurePlugin _ (Local path) = do
exists <- doesDirectoryExist path
unless exists $
die $ "Local plugin " ++ show path ++ " does not exist. " ++
"Make sure that the path is absolute."
ensurePlugin config storage@(GitRepository url) = do
exists <- doesDirectoryExist path
unless exists $
gitClone url path
where
path = storagePath config storage
-- TODO URL should be ByteString?
gitClone :: Text -> FilePath -> IO ()
gitClone url path =
Proc.callProcess "git"
["clone", "--recursive", "--", Text.unpack url, path]
-- | Convert a relative path to absolute by prepending the current directory.
--
-- This is available in directory >= 1.2.3, but GHC 7.8 uses 1.2.1.0.
makeAbsolute :: FilePath -> IO FilePath
makeAbsolute = (normalise <$>) . absolutize
where
absolutize path
| isRelative path = (</> path) <$> getCurrentDirectory
| otherwise = return path
withDirectory :: FilePath -> IO a -> IO a
withDirectory p io = do
old <- getCurrentDirectory
bracket_ (setCurrentDirectory p)
(setCurrentDirectory old)
io
-- | Gather scripts to source for each sourcing location of the plugin.
findPluginZshs :: AntigenConfig -> ZshPlugin -> IO [FilePath]
findPluginZshs config plugin =
withDirectory (storagePath config (storage plugin)) $
fmap concat . forM (sourcingLocations plugin) $ \loc ->
withDirectory loc $
sourcingStrategy plugin
-- | Get full paths to all files in the given directory.
listFiles :: FilePath -> IO [FilePath]
listFiles p
= map (p </>)
. filter (`notElem` [".", ".."])
<$> getDirectoryContents p
die :: String -> IO a
die msg = putStrLn msg >> exitFailure
-- | Match for one single *.plugin.zsh file
strictSourcingStrategy :: SourcingStrategy
strictSourcingStrategy = do
directory <- getCurrentDirectory
files <- listFiles directory
let matches = filter (".plugin.zsh" `isSuffixOf`) files
case matches of
[file] -> return [file]
[] -> die $
"No *.plugin.zsh file in " ++
directory ++ "! " ++
"See antigenSourcingStrategy example in README " ++
"on how to configure this."
_ -> die ("Too many *.plugin.zsh files in " ++ directory ++ "!")
-- | Find what to source, using the strategy described here:
--
-- https://github.com/zsh-users/antigen#notes-on-writing-plugins
antigenSourcingStrategy :: SourcingStrategy
antigenSourcingStrategy = do
files <- getCurrentDirectory >>= listFiles
let candidatePatterns = [".plugin.zsh", "init.zsh", ".zsh", ".sh"]
filesMatching pat = filter (pat `isSuffixOf`) files
filteredResults = map filesMatching candidatePatterns
results = filter (not . null) filteredResults
case results of
(matchedFiles:_) -> return matchedFiles
[] -> die $ "No files to source among " ++ show files
-- | Source all files in the given order. Currently does no file existence
-- check or anything.
filePathsSourcingStrategy :: [FilePath] -> SourcingStrategy
filePathsSourcingStrategy paths = do
cwd <- getCurrentDirectory
return $ map (cwd </>) paths
getAbsoluteFpaths :: AntigenConfig -> ZshPlugin -> IO [FilePath]
getAbsoluteFpaths config plugin = do
let path = storagePath config (storage plugin)
mapM (makeAbsolute . (path </>)) (fpathLocations plugin)
-- | Get the content that will be put in the file to source.
fileToSourceContent
:: AntigenConfig
-> [FilePath] -- ^ List of all the *.plugin.zsh files
-> IO Text -- ^ What the file should contain
fileToSourceContent config@AntigenConfig{plugins} pluginZshs = do
pluginZshsAbs <- mapM makeAbsolute pluginZshs
fpaths <- concat <$> mapM (getAbsoluteFpaths config) plugins
return $ Text.unlines
$ "# THIS FILE IS GENERATED BY antigen-hs!!!!\n"
: map (("source " <>) . Text.pack) pluginZshsAbs
++ map (("fpath+=" <>) . Text.pack) fpaths
-- | Do an action inside the home directory
inHomeDir :: IO a -> IO a
inHomeDir io = do
home <- getHomeDirectory
withDirectory home io
-- | The main function that will clone all the repositories and create the
-- file to be sourced by the user
antigen :: AntigenConfig -> IO ()
antigen config'@AntigenConfig{plugins} = inHomeDir $ do
hsOut <- lookupEnv "ANTIGEN_HS_OUT"
let config = config'
{ outputDirectory = fromMaybe (outputDirectory config') hsOut }
createDirectoryIfMissing True (reposDirectory config)
mapM_ (ensurePlugin config . storage) plugins
pluginZshs <- concat <$> mapM (findPluginZshs config) plugins
contents <- fileToSourceContent config pluginZshs
Text.writeFile (outputFileToSource config) contents