-
Notifications
You must be signed in to change notification settings - Fork 12
/
Copy pathnakelib.nim
305 lines (265 loc) · 10.2 KB
/
nakelib.nim
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
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
## Reusable module for the `nake module <https://github.com/fowlmouth/nake>`_.
##
## This ``nakelib`` module implements all of `nake <nake.html>`_'s
## functionality except some *magic* code at the end of the `nake <nake.html>`_
## module which `registers a quit procedure
## <http://nim-lang.org/system.html#addQuitProc>`_ when imported to turn the
## program into a runnable nakefile.
##
## Import this module instead of `nake <nake.html>`_ if you want to use any of
## its procs without affecting your program execution.
import std/[os, osproc, rdstdin, strutils, tables, times]
type
NakeTask* = object ## Defines a task with a description and action.
desc*: string
action*: NakeAction
NakeAction* = proc() ## \
## Type for the actions associated with a task name.
##
## Used in `NakeTask <#NakeTask>`_ objects.
NakeTaskLister* = proc() ## \
## Type for the ``proc`` which prints out the list of available tasks.
##
## Assigned to the `listTasks <#listTasks>`_ global.
var
tasks* = initOrderedTable[string, NakeTask](32) ## \
## Holds the list of defined tasks.
##
## Use the `task() <#task>`_ template to add elements to this variable.
validateShellCommands* = false ## \
## Set this global to ``true`` if you want the `shell() <#shell>`_ and
## `direShell() <#direShell>`_ procs to ask the user for confirmation before
## executing a command.
nimExe*: string ## \
## Full path to the Nim compiler binary.
##
## You can use this in your code to avoid having to hardcode the path to the
## compiler. The path is obtained at runtime. First the ``nim`` binary is
## probed, and if that fails, the older ``nimrod`` is searched for backwards
## compatibility. Example:
##
## .. code-block:: nimrod
## if "nake.html".needsRefresh("nake.nim"):
## echo "nake.nim -> nake.html"
## direShell nimExe, "doc2", "--verbosity:0", "--index:on", "nake.nim"
const
defaultTask* = "default" ## \
## String with the name of the default task nake will run `if you define it
## and the user doesn't specify any task <#listTasks>`_.
nimExe = findExe("nim").quoteShell
proc askShellCmd (cmd: string): bool {.raises: [ValueError, IOError].} =
if validateShellCommands:
let ans = readLineFromStdin("Run? `$#` [N/y]\L" % cmd)
if ans[0] in {'y', 'Y'}:
result = execShellCmd(cmd) == 0
else:
return false
else:
result = execShellCmd(cmd) == 0
proc askSilentShellCmd(cmd: string):
tuple[output: string, exitCode: int]
{.raises: [ValueError, OSError, Exception].} =
## Variant of askShellCMD which returns the output and exit code.
##
## In case of the user rejecting the command the proc will return the empty
## string for `output` and a negative value for `exitCode`.
assert(cmd.len != 0)
if validateShellCommands:
let ans = readLineFromStdin("Run? `$#` [N/y]\L" % cmd)
if ans[0] in {'y','Y'}:
result = execCmdEx(cmd)
else:
result.output = ""
result.exitCode = -1
else:
result = execCmdEx(cmd)
proc shell*(cmd: varargs[string, `$`]): bool {.discardable,
raises: [ValueError, IOError] .} =
## Invokes an external command.
##
## The proc will return ``false`` if the command exits with a non zero code,
## ``true`` otherwise.
##
## This proc respects the value of the `validateShellCommands
## <#validateShellCommands>`_ global.
result = askShellCmd(cmd.join(" "))
proc direShell*(cmd: varargs[string, `$`]): bool {.discardable,
raises:[ValueError, IOError].} =
## Wrapper around the `shell() <#shell>`_ proc.
##
## Instead of returning on a non zero value like `shell() <#shell>`_,
## ``direShell()`` `quits <http://nim-lang.org/system.html#quit>`_ if the
## process does not return 0.
result = shell(cmd)
if not result: quit 1
proc silentShell*(info: string, cmd: varargs[string, `$`]): bool {.discardable,
raises: [ValueError, OSError, Exception].} =
## Invokes an external command silently, informing the user about it.
##
## The proc will return ``false`` if the command exits with a non zero code,
## ``true`` otherwise. This is very similar to the `shell() <#shell>`_ proc,
## but the output of the command will be displayed only if the proc returns
## ``false``, successful commands won't output anything. Since this means
## that the output is buffered until the command returns, for long running
## commands you should maybe tell the user work is being done. This can be
## done through the `info` parameter. When not ``nil``, the `info` parameter
## will be echoed to standard output before executing `cmd`.
##
## This proc respects the value of the `validateShellCommands
## <#validateShellCommands>`_ global. Example:
##
## .. code-block:: nimrod
## if not silentShell("Checking for A", "cmdA", "--version"):
## let another = silentShell("Checking for B", "cmdB", "-v")
## if not another:
## quit("Sorry, neither A nor B were found, please install one")
if info.len != 0:
echo info
let (output, exitCode) = askSilentShellCmd(cmd.join(" "))
result = (0 == exitCode)
if not result:
echo output
proc direSilentShell*(info: string, cmd: varargs[string, `$`]): bool
{.discardable, raises: [ValueError, OSError, Exception].} =
## Wrapper around the `silentShell() <#silentShell>`_ proc.
##
## Instead of returning on a non zero value like `silentShell()
## <#silentShell>`_, ``direSilentShell()`` `quits
## <http://nim-lang.org/system.html#quit>`_ if the process does not return
## zero. Example:
##
## .. code-block:: nimrod
## direSilentShell(nil, "git", "--version")
## direSilentShell("Building stuff", nimExe, "c", "-r", "stuff.nim")
result = silentShell(info, cmd)
if not result: quit 1
proc cd*(dir: string) {.inline, raises: [OSError].} =
## Changes the current directory.
##
## The change is permanent for the rest of the execution, since this is just
## a shortcut for `os.setCurrentDir()
## <http://nim-lang.org/os.html#setCurrentDir,string>`_ . Use the `withDir()
## <#withDir>`_ template if you want to perform a temporary change only.
setCurrentDir(dir)
template withDir*(dir: string; body: untyped): untyped =
## Changes the current directory temporarily.
##
## If you need a permanent change, use the `cd() <#cd>`_ proc. Usage example:
##
## .. code-block:: nimrod
## withDir "foo":
## # inside foo
## #back to last dir
let curDir = getCurrentDir()
cd(dir)
body
cd(curDir)
proc needsRefresh*(target: string, src: varargs[string]): bool {.
raises: [OSError].} =
## Returns true if target is missing or src has newer modification date.
##
## This is a convenience proc you can use in your tasks to verify if
## compilation for a binary should happen. The proc will return true if
## ``target`` doesn't exists or any of the file paths in ``src`` have a more
## recent last modification timestamp. All paths in ``src`` must be reachable
## or else the proc will raise an exception. Example:
##
## .. code-block:: nimrod
## import nake, os
##
## let
## src = "prog.nim"
## exe = src.changeFileExt(exeExt)
## if exe.needsRefresh(src):
## direShell nimExe, "c", src
## else:
## echo "All done!"
assert len(src) > 0, "Pass some parameters to check for"
var targetTime: float
try:
targetTime = toUnixFloat(getLastModificationTime(target))
except OSError:
return true
for s in src:
let srcTime = toUnixFloat(getLastModificationTime(s))
if srcTime > targetTime:
return true
proc runTask*(name: string) {.inline.} = ## \
## Runs the specified task.
##
## You can call this proc to *chain* other tasks for the current task and
## avoid repeating code. Example:
##
## .. code-block:: nimrod
## import nake, os
##
## ...
##
## task "docs", "generates docs for module":
## echo "Generating " & moduleHtml
## direShell nimExe, "doc", moduleNim
##
## task "install_docs", "copies docs to " & docInstallDir:
## runTask("docs")
## echo "Copying documentation to " & docInstallDir
## copyFile(moduleHtml, docInstallDir / moduleHtml)
tasks[name].action()
template task*(name, description: string; body: untyped): untyped =
## Defines a task for nake.
##
## Pass the name of the task, the description that will be displayed to the
## user when `nake` is invoked, and the body of the task. Example:
##
## .. code-block:: nimrod
## import nake
##
## task "bin", "compiles all binaries":
## for binName in binaries:
## echo "Generating " & binName
## direShell nimExe, "c", binName
bind tasks
tasks[name] = NakeTask(desc: description, action: proc() {.closure.} =
body)
proc listTasksImpl*() =
## Default implementation for listing tasks to stdout.
##
## This implementation will print out each task and it's description to the
## command line. You can change the value of the `listTasks <#listTasks>`_
## global if you don't like it.
assert tasks.len > 0
echo "Available tasks:"
for name, task in pairs(tasks):
echo name, " - ", task.desc
var listTasks*: NakeTaskLister = listTasksImpl ## \
## Holds the proc that is used by default to list available tasks to the user.
##
## You can call the proc held here inside your `defaultTask <#defaultTask>`_
## task to tell the user about available options if your default task doesn't
## have anything to do. You can assign to this var to provide another
## implementation, the default is `listTasksImpl() <#listTasksImpl>`_.
## Example:
##
## .. code-block:: nimrod
## import nake, sequtils
##
## nake.listTasks = proc() =
## ## only lists the task names, no descriptions
## echo "Available tasks: ", toSeq(nake.tasks.keys).join(", ")
##
## task defaultTask, "lists all tasks":
## listTasks()
##
## Here is an alternative version which blacklists tasks to end users.
## They may not be interested or capable of running some of them due to extra
## development dependencies:
##
## .. code-block:: nimrod
## const privateTasks = ["dist", defaultTask, "testRemote", "upload"]
##
## nake.listTasks = proc() =
## echo "Available tasks:"
## for taskKey in nake.tasks.keys:
## # Show only public tasks.
## if taskKey in privateTasks:
## continue
## echo "\t", taskKey