fire 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.index +53 -0
- data/.yardopts +10 -0
- data/HISTORY.md +33 -0
- data/LICENSE.txt +29 -0
- data/README.md +217 -0
- data/bin/autofire +5 -0
- data/bin/fire +5 -0
- data/demo/03_runner/01_applying_rules.md +51 -0
- data/demo/03_runner/02_resolve_prerequisites.md +95 -0
- data/demo/applique/ae.rb +1 -0
- data/demo/applique/fire.rb +1 -0
- data/demo/applique/rules.rb +5 -0
- data/demo/overview.md +14 -0
- data/lib/fire.rb +3 -0
- data/lib/fire.yml +53 -0
- data/lib/fire/cli.rb +64 -0
- data/lib/fire/core_ext.rb +4 -0
- data/lib/fire/core_ext/boolean.rb +10 -0
- data/lib/fire/core_ext/cli.rb +56 -0
- data/lib/fire/core_ext/true_class.rb +58 -0
- data/lib/fire/digest.rb +112 -0
- data/lib/fire/dsl.rb +34 -0
- data/lib/fire/match.rb +26 -0
- data/lib/fire/rule.rb +103 -0
- data/lib/fire/rulefile.rb +12 -0
- data/lib/fire/runner.rb +76 -0
- data/lib/fire/session.rb +268 -0
- data/lib/fire/shellutils.rb +77 -0
- data/lib/fire/state.rb +91 -0
- data/lib/fire/system.rb +244 -0
- data/lib/fire/task.rb +71 -0
- data/man/fire.1 +47 -0
- data/man/fire.1.html +130 -0
- data/man/fire.1.ronn +52 -0
- metadata +135 -0
data/lib/fire/rule.rb
ADDED
@@ -0,0 +1,103 @@
|
|
1
|
+
module Fire
|
2
|
+
|
3
|
+
# Rule class encapsulates a *rule* definition.
|
4
|
+
#
|
5
|
+
class Rule
|
6
|
+
# Initialize new instanance of Rule.
|
7
|
+
#
|
8
|
+
# logic - Logic condition. [Logic]
|
9
|
+
# procedure - Procedure to run if logic condition is met. [Proc]
|
10
|
+
#
|
11
|
+
# Options
|
12
|
+
# :todo - Names of prerequisite tasks. [Array<Symbol>]
|
13
|
+
#
|
14
|
+
def initialize(logic, options={}, &procedure)
|
15
|
+
@logic = logic
|
16
|
+
@requisite = options[:todo]
|
17
|
+
@procedure = procedure
|
18
|
+
end
|
19
|
+
|
20
|
+
# Access logic condition.
|
21
|
+
#
|
22
|
+
# Returns [Logic]
|
23
|
+
attr :logic
|
24
|
+
|
25
|
+
# Returns [Proc]
|
26
|
+
attr :procedure
|
27
|
+
|
28
|
+
# Names of requisite tasks.
|
29
|
+
def requisite
|
30
|
+
@requisite
|
31
|
+
end
|
32
|
+
|
33
|
+
# More convenient alias for `#requisite`.
|
34
|
+
alias :todo :requisite
|
35
|
+
|
36
|
+
# Rules don't generally have names, but task rules do.
|
37
|
+
def name
|
38
|
+
nil
|
39
|
+
end
|
40
|
+
|
41
|
+
# Apply logic, running the rule's prcedure if the logic
|
42
|
+
# condition is satisfied.
|
43
|
+
#
|
44
|
+
# Returns nothing.
|
45
|
+
def apply(&prepare)
|
46
|
+
case logic
|
47
|
+
when true
|
48
|
+
call
|
49
|
+
when false, nil
|
50
|
+
else
|
51
|
+
result_set = logic.call
|
52
|
+
if result_set && !result_set.empty?
|
53
|
+
prepare.call
|
54
|
+
call(*result_set)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# Alias for #apply.
|
60
|
+
alias :invoke :apply
|
61
|
+
|
62
|
+
protected
|
63
|
+
|
64
|
+
# Run rule procedure.
|
65
|
+
#
|
66
|
+
# result_set - The result set returned by the logic condition.
|
67
|
+
#
|
68
|
+
# Returns whatever the procedure returns. [Object]
|
69
|
+
def call(*result_set)
|
70
|
+
if @procedure.arity == 0
|
71
|
+
@procedure.call
|
72
|
+
else
|
73
|
+
#@procedure.call(session, *args)
|
74
|
+
@procedure.call(*result_set)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
=begin
|
79
|
+
private
|
80
|
+
|
81
|
+
# Reduce todo list to the set of tasks to be run.
|
82
|
+
#
|
83
|
+
# Returns [Array<Task>]
|
84
|
+
def reduce
|
85
|
+
return [] if @_reducing
|
86
|
+
list = []
|
87
|
+
begin
|
88
|
+
@_reducing = true
|
89
|
+
@requisite.each do |r|
|
90
|
+
next if @system.post.include?(r.to_sym)
|
91
|
+
list << @system.tasks[r.to_sym].reduce
|
92
|
+
end
|
93
|
+
list << self
|
94
|
+
ensure
|
95
|
+
@_reducing = false
|
96
|
+
end
|
97
|
+
list.flatten.uniq
|
98
|
+
end
|
99
|
+
=end
|
100
|
+
|
101
|
+
end
|
102
|
+
|
103
|
+
end
|
data/lib/fire/runner.rb
ADDED
@@ -0,0 +1,76 @@
|
|
1
|
+
module Fire
|
2
|
+
|
3
|
+
# Time to DCI this shit!
|
4
|
+
|
5
|
+
# Runner class takes a rule system and runs it.
|
6
|
+
#
|
7
|
+
class Runner
|
8
|
+
|
9
|
+
def initialize(system)
|
10
|
+
@system = system
|
11
|
+
@_post = []
|
12
|
+
end
|
13
|
+
|
14
|
+
#
|
15
|
+
attr :system
|
16
|
+
|
17
|
+
#
|
18
|
+
def run_rules
|
19
|
+
system.rules.each do |rule|
|
20
|
+
rule.apply{ prepare(rule) }
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
#
|
25
|
+
def run_task(trigger)
|
26
|
+
task = system.tasks[trigger.to_sym]
|
27
|
+
task.apply{ prepare(task) }
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
# Execute rule by first running any outstanding prerequistes
|
33
|
+
# then then the rul procedure itself.
|
34
|
+
def prepare(rule)
|
35
|
+
pre = resolve(rule)
|
36
|
+
pre = pre - post
|
37
|
+
pre = pre - [rule.name.to_sym] if rule.name
|
38
|
+
pre.each do |r|
|
39
|
+
r.call
|
40
|
+
end
|
41
|
+
post(pre)
|
42
|
+
end
|
43
|
+
|
44
|
+
# TODO: It would be nice #resolve could detect infinite recursions
|
45
|
+
# and raise an error.
|
46
|
+
#
|
47
|
+
|
48
|
+
# Resolve prerequistes.
|
49
|
+
#
|
50
|
+
# Returns [Array<Symbol>]
|
51
|
+
def resolve(rule, todo=[])
|
52
|
+
return [] if (rule.todo - todo).empty?
|
53
|
+
left = rule.todo - todo
|
54
|
+
list = left
|
55
|
+
todo.concat(left)
|
56
|
+
left.each do |r|
|
57
|
+
t = system.tasks[r.to_sym]
|
58
|
+
x = resolve(t, todo)
|
59
|
+
list.concat(x)
|
60
|
+
end
|
61
|
+
list.uniq
|
62
|
+
end
|
63
|
+
|
64
|
+
# List of prerequistes that have already been run.
|
65
|
+
# Keeping this list prevents the same prequistes
|
66
|
+
# from ever being run twice in the same session.
|
67
|
+
#
|
68
|
+
# Returns [Array<Symbol>]
|
69
|
+
def post(pre=nil)
|
70
|
+
@_post.concat(pre) if pre
|
71
|
+
@_post
|
72
|
+
end
|
73
|
+
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|
data/lib/fire/session.rb
ADDED
@@ -0,0 +1,268 @@
|
|
1
|
+
require 'fire/system'
|
2
|
+
require 'fire/runner'
|
3
|
+
require 'fileutils'
|
4
|
+
|
5
|
+
module Fire
|
6
|
+
|
7
|
+
# Markers to look for to identify a project's root directory.
|
8
|
+
ROOT_INDICATORS = %w{.fire task/fire.rb .git .hg _darcs .gemspec *.gemspec}
|
9
|
+
|
10
|
+
# This file can be used as an alternative to using the #ignore method
|
11
|
+
# to define what paths to ignore.
|
12
|
+
IGNORE_FILE = '.fire/ignore'
|
13
|
+
|
14
|
+
# Session is the main Fire class which controls execution.
|
15
|
+
#
|
16
|
+
# TODO: Maybee call this Runner, and have a special Session class that limits interface.
|
17
|
+
#
|
18
|
+
class Session
|
19
|
+
|
20
|
+
#
|
21
|
+
# Initialize new Session instance.
|
22
|
+
#
|
23
|
+
# Returns nothing.
|
24
|
+
#
|
25
|
+
def initialize(options={})
|
26
|
+
self.watch = options[:watch]
|
27
|
+
self.trial = options[:trial]
|
28
|
+
|
29
|
+
load_system
|
30
|
+
end
|
31
|
+
|
32
|
+
#
|
33
|
+
# Watch period, default is every 5 minutes.
|
34
|
+
#
|
35
|
+
def watch
|
36
|
+
@watch || 300
|
37
|
+
end
|
38
|
+
|
39
|
+
#
|
40
|
+
# Set watch seconds. Minimum watch time is 1 second.
|
41
|
+
# Setting watch before calling #run creates a simple loop.
|
42
|
+
# It can eat up CPU cycles so use it wisely. A watch time
|
43
|
+
# of 4 seconds is a good time period. If you are patient
|
44
|
+
# go for 15 seconds or more.
|
45
|
+
#
|
46
|
+
def watch=(seconds)
|
47
|
+
if seconds
|
48
|
+
seconds = seconds.to_i
|
49
|
+
seconds = 1 if seconds < 1
|
50
|
+
@watch = seconds
|
51
|
+
else
|
52
|
+
@watch = nil
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# Is this trial-run only?
|
57
|
+
def trial?
|
58
|
+
@trial
|
59
|
+
end
|
60
|
+
|
61
|
+
# Set trial run mode.
|
62
|
+
#
|
63
|
+
# Arguments
|
64
|
+
# bool - Flag for trial mode. [Boolean]
|
65
|
+
#
|
66
|
+
# Returns `bool` flag. [Boolean]
|
67
|
+
def trial=(bool)
|
68
|
+
@trial = !!bool
|
69
|
+
end
|
70
|
+
|
71
|
+
# Instance of {Fire::System}.
|
72
|
+
def system
|
73
|
+
#@system ||= System.new(:ignore=>ignore, :files=>scripts)
|
74
|
+
@system ||= Fire.system
|
75
|
+
end
|
76
|
+
|
77
|
+
# TODO: load configuration
|
78
|
+
#
|
79
|
+
#def rc?
|
80
|
+
# Dir.glob('{.c,c,C}onfig{.rb,}').first
|
81
|
+
#end
|
82
|
+
|
83
|
+
#
|
84
|
+
# Default rules script is the first file matching `.fire/rules.rb`
|
85
|
+
# or `rules.rb` from the the project's root directory. The easiest
|
86
|
+
# way to customize this is to use `.fire/rules.rb` and use `import`
|
87
|
+
# to load which ever files you prefer, e.g. `import "task/*.fire"`.
|
88
|
+
#
|
89
|
+
# @return [Array] List of file paths.
|
90
|
+
#
|
91
|
+
def script
|
92
|
+
@script ||= (
|
93
|
+
glob = File.join(root, '{.fire/rules.rb,rules.rb}')
|
94
|
+
Dir.glob(glob, File::FNM_CASEFOLD).first
|
95
|
+
)
|
96
|
+
end
|
97
|
+
alias :scripts :script
|
98
|
+
|
99
|
+
#
|
100
|
+
# Change this rules script(s) that are loaded.
|
101
|
+
#
|
102
|
+
def script=(files)
|
103
|
+
@script = Array(files)
|
104
|
+
end
|
105
|
+
alias :scripts= :script=
|
106
|
+
|
107
|
+
#
|
108
|
+
# File globs to ignore.
|
109
|
+
#
|
110
|
+
# @return [Array] List of file globs.
|
111
|
+
#
|
112
|
+
def ignore
|
113
|
+
@ignore ||= (
|
114
|
+
f = File.join(root, IGNORE_FILE)
|
115
|
+
i = []
|
116
|
+
if File.exist?(f)
|
117
|
+
File.read(f).lines.each do |line|
|
118
|
+
glob = line.strip
|
119
|
+
i << glob unless glob.empty?
|
120
|
+
end
|
121
|
+
end
|
122
|
+
i
|
123
|
+
)
|
124
|
+
end
|
125
|
+
|
126
|
+
# Run once.
|
127
|
+
def run(argv)
|
128
|
+
Dir.chdir(root) do
|
129
|
+
if argv.size > 0
|
130
|
+
run_task(*argv)
|
131
|
+
else
|
132
|
+
run_rules
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
# Run periodically.
|
138
|
+
def autorun(argv)
|
139
|
+
Dir.chdir(root) do
|
140
|
+
trap("INT") { puts "\nPutting out the fire!"; exit }
|
141
|
+
puts "Fire started! (pid #{Process.pid})"
|
142
|
+
loop do
|
143
|
+
run_rules
|
144
|
+
sleep(watch)
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
private
|
150
|
+
|
151
|
+
#
|
152
|
+
def load_system
|
153
|
+
system.ignore(*ignore)
|
154
|
+
system.import(*script)
|
155
|
+
end
|
156
|
+
|
157
|
+
# Run the rules.
|
158
|
+
def run_rules
|
159
|
+
runner.run_rules
|
160
|
+
save_digest
|
161
|
+
end
|
162
|
+
|
163
|
+
# Run the rules.
|
164
|
+
def run_task(*argv)
|
165
|
+
runner.run_task(*argv)
|
166
|
+
end
|
167
|
+
|
168
|
+
#
|
169
|
+
#def save_pid
|
170
|
+
# File.open('.fire/pid', 'w') do |f|
|
171
|
+
# f << Process.pid.to_s
|
172
|
+
# end
|
173
|
+
#end
|
174
|
+
|
175
|
+
#
|
176
|
+
def save_digest
|
177
|
+
digest = Digest.new(:ignore=>ignore)
|
178
|
+
digest.save
|
179
|
+
end
|
180
|
+
|
181
|
+
#
|
182
|
+
def runner
|
183
|
+
Runner.new(system)
|
184
|
+
end
|
185
|
+
|
186
|
+
#def run
|
187
|
+
# @states.each do |state|
|
188
|
+
# session = OpenStruct.new
|
189
|
+
# next unless state.active?(info)
|
190
|
+
# @rules.each do |rule|
|
191
|
+
# if md = rule.match?(state)
|
192
|
+
# if rule.arity == 0 or md == true
|
193
|
+
# rule.call(info)
|
194
|
+
# else
|
195
|
+
# rule.call(info,*md[1..-1])
|
196
|
+
# end
|
197
|
+
# end
|
198
|
+
# end
|
199
|
+
# end
|
200
|
+
#end
|
201
|
+
|
202
|
+
# List of rules from the system.
|
203
|
+
def rules
|
204
|
+
system.rules
|
205
|
+
end
|
206
|
+
|
207
|
+
# Mapping of tasks from the system.
|
208
|
+
def tasks
|
209
|
+
system.tasks
|
210
|
+
end
|
211
|
+
|
212
|
+
# Produce a printable list of tasks that can run from the command line.
|
213
|
+
#
|
214
|
+
# Returns [String]
|
215
|
+
def task_sheet
|
216
|
+
max = tasks.map{|n,t| n.to_s.size}.max.to_i
|
217
|
+
layout = "ou %-#{max}s # %s"
|
218
|
+
text = []
|
219
|
+
tasks.each do |name, task|
|
220
|
+
text << layout % [name, task.description] if task.description
|
221
|
+
end
|
222
|
+
text.join("\n")
|
223
|
+
end
|
224
|
+
|
225
|
+
# Locate project root. This method ascends up the file system starting
|
226
|
+
# as the current working directory looking for `ROOT_INDICATORS`. When
|
227
|
+
# a match is found, the directory in which it is found is returned as
|
228
|
+
# the root. It is also memoized, so repeated calls to this method will
|
229
|
+
# not repeat the search.
|
230
|
+
#
|
231
|
+
# Returns [String]
|
232
|
+
def root
|
233
|
+
@root ||= (
|
234
|
+
r = nil
|
235
|
+
d = Dir.pwd
|
236
|
+
while d != home && d != '/'
|
237
|
+
if ROOT_INDICATORS.any?{ |g| Dir.glob(File.join(d, g), File::FNM_CASEFOLD).first }
|
238
|
+
break r = d
|
239
|
+
end
|
240
|
+
d = File.dirname(d)
|
241
|
+
end
|
242
|
+
abort "Can't locate project root." unless r
|
243
|
+
r
|
244
|
+
)
|
245
|
+
end
|
246
|
+
|
247
|
+
# Home directory.
|
248
|
+
#
|
249
|
+
# Returns [String]
|
250
|
+
def home
|
251
|
+
@home ||= File.expand_path('~')
|
252
|
+
end
|
253
|
+
|
254
|
+
|
255
|
+
# TODO: support rc profiles
|
256
|
+
#if config = Fire.rc_config
|
257
|
+
# config.each do |c|
|
258
|
+
# if c.arity == 0
|
259
|
+
# system.instance_eval(&c)
|
260
|
+
# else
|
261
|
+
# c.call(system)
|
262
|
+
# end
|
263
|
+
# end
|
264
|
+
#end
|
265
|
+
|
266
|
+
end
|
267
|
+
|
268
|
+
end
|