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
|
# Copyright (C) 2008 Zed A. Shaw. Licensed under the terms of the GPLv3.
import os
import sys
from . import DieError, TargetNotFoundError
from .parser import Reference
### @export "class Scribe"
class Scribe(object):
"""
Turns a build spec into something that can actually run. Scribe
is responsible for taking the results from Script and loading
the extra commands out of ~/.vellum/modules so that you can run
it.
This is the equiv. of the component in an interpreter that
processes a cleaned and structured AST to execute it.
"""
def __init__(self, script):
self.script = script
self.options = self.script.options
self.target = None
self.line = 1
self.source = os.path.expanduser("~/.vellum/modules")
self.stack = []
self.commands = self.script.commands
### @export "support methods"
def option(self, name):
"""Tells if there's an option of this type."""
return self.options.get(name,None)
def log(self, msg):
"""
Logs a message to the screen, but only if "verbose"
option (not quiet).
"""
if self.option("verbose"):
print msg
sys.stdout.flush()
def die(self, cmd, msg=""):
"""
Dies with an error message for the given command listing
the target and line number in that target.
"""
if not self.option("keep_going"):
raise DieError(self.target, self.line, cmd, msg)
### @export "handling targets"
def body_of_target(self, name):
"""Just gets the target out of the script, returning the
body."""
return self.script.targets[name]
def parse_target(self, cmds):
"""
Takes the body of a target and figures out how to split it
up so that you can execute it.
"""
if isinstance(cmds, list):
return cmds # lists of stuff are just fine
elif isinstance(cmds, Reference):
return [cmds] # gotta put single references into a list
elif isinstance(cmds, basestring):
return cmds.split("\n") # convert big strings into lists of command shells
else:
self.die("parse_target", "Definition of target isn't a list, command, or string.")
return [] # needed in case -k is given
def is_target(self, target):
"""
Determines if this is a real target we can transition to,
not a virtual one only found in the dependency graph.
"""
return (target in self.script.targets
and self.script.targets[target])
### @export "handling commands"
def is_command(self, name):
"""Tells the scribe if this name is an actual command."""
return callable(self.commands.get(name, None))
def command(self, name, expr):
"""
Runs the command for the given name. Pulls it out of the
self.commands. Normally it just passes expr to the
command, but if expr is a dict then it will call it
with **expr so you can do simpler kword commands. If
you don't want this then you just define your command as
taking **args.
"""
try:
to_call = self.commands[name]
except KeyError, err:
self.die(name, "Invalid command name %s, use -C to find out what's available.")
if isinstance(expr, dict):
return to_call(self, **expr)
else:
return to_call(self, expr)
### @export "execute target body"
def execute(self, body):
"""
Executes the body which can be anything parse_target() can
handle. It properly handles the difference between a plain
string (shell command(s)) or a Reference (do some command),
or a list of those two. Assumes you call self.start_target()
"""
for cmd in self.parse_target(body):
if "__builtins__" in self.options:
self.die(cmd, "Your command leaked __builtins__."
"Use scribe.push_scope and "
"scribe.pop_scope.")
self.line += 1
if isinstance(cmd, Reference):
# Reference objects are indications to run some
# Python command rather than a shell.
if self.is_command(cmd.name):
# discontinue on True
if self.command(cmd.name, cmd.expr):
self.log("<-- %s" % cmd)
return
else:
self.die(cmd,
"Invalid command reference, available "
"commands are:\n%r." %
sorted(self.commands.keys()))
else:
# it's just shell
cmd = cmd.strip()
if cmd: self.command("sh", cmd)
### @export "transition to target"
def transition(self, target):
"""
The main engine of the whole thing, it will transition to
the given target and then process it's commands listed. It
properly figures out if this is a command reference or a
plain string to run as a shell.
"""
if self.is_target(target):
self.line = 0
self.target = target
body = self.body_of_target(target)
self.execute(body)
elif target in self.script.depends:
return
else:
raise TargetNotFoundError()
### @export "running all targets"
def build(self, to_build):
"""
Main entry point that resolves the main targets for
those listed in to_build and then runs the results in order.
"""
building = self.script.resolve_targets(to_build)
self.log("BUILDING: %s" % building)
for target in building:
self.log("-->: %s" % target)
try:
self.transition(target)
except TargetNotFoundError, e:
if self.option('ignore_missing'):
self.log('Warning, target not found: %s' % target)
else:
self.die(target, 'Target not found: %s' % target)
### @export "string handling"
def interpolate(self, cmd_name, expr):
"""
Takes a string expression and interpolates it
using the self.options dict as the % paramter.
It prints more useful errors than you'd normally
get from Python.
"""
err_name = "%s %r" % (cmd_name, expr)
try:
return expr % self.options
except ValueError, err:
self.die(err_name, "Expression has invalid format: %s" % err)
except KeyError, err:
self.die(err_name, "No key %s for format, available keys are: %r" % (err, sorted(self.options.keys())))
### @export "scope management"
def push_scope(self, vars={}):
"""
Takes the current set of options and pushes it onto
an internal stack but with the new vars in the
options for the next command to use. This
effectively emulates function call semantics for
targets.
"""
self.stack.append(self.options)
self.options = self.options.copy()
self.options.update(vars)
def pop_scope(self):
"""
Does the inverse of push_scope() recovering
the previous scope.
"""
self.options = self.stack.pop()
|