fire 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
@@ -0,0 +1,77 @@
|
|
1
|
+
module Fire
|
2
|
+
|
3
|
+
# TODO: Borrow code from Detroit for ShellUtils and beef her up!
|
4
|
+
|
5
|
+
# File system utility methods.
|
6
|
+
#
|
7
|
+
module ShellUtils
|
8
|
+
# Shell out via system call.
|
9
|
+
#
|
10
|
+
# Arguments
|
11
|
+
# args - Argument vector. [Array]
|
12
|
+
#
|
13
|
+
# Returns success of shell invocation.
|
14
|
+
def sh(*args)
|
15
|
+
puts args.join(' ')
|
16
|
+
system(*args)
|
17
|
+
end
|
18
|
+
|
19
|
+
def directory?(path)
|
20
|
+
File.directory?(path)
|
21
|
+
end
|
22
|
+
|
23
|
+
#
|
24
|
+
# Synchronize a destination directory with a source directory.
|
25
|
+
#
|
26
|
+
# TODO: Augment FileUtils instead.
|
27
|
+
# TODO: Not every action needs to be verbose.
|
28
|
+
#
|
29
|
+
def sync(src, dst, options={})
|
30
|
+
src_files = Dir[File.join(src, '**', '*')].map{ |f| f.sub(src+'/', '') }
|
31
|
+
dst_files = Dir[File.join(dst, '**', '*')].map{ |f| f.sub(dst+'/', '') }
|
32
|
+
|
33
|
+
removal = dst_files - src_files
|
34
|
+
|
35
|
+
rm_dirs, rm_files = [], []
|
36
|
+
removal.each do |f|
|
37
|
+
path = File.join(dst, f)
|
38
|
+
if File.directory?(path)
|
39
|
+
rm_dirs << path
|
40
|
+
else
|
41
|
+
rm_files << path
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
rm_files.each { |f| rm(f) }
|
46
|
+
rm_dirs.each { |d| rmdir(d) }
|
47
|
+
|
48
|
+
src_files.each do |f|
|
49
|
+
src_path = File.join(src, f)
|
50
|
+
dst_path = File.join(dst, f)
|
51
|
+
if File.directory?(src_path)
|
52
|
+
mkdir_p(dst_path)
|
53
|
+
else
|
54
|
+
parent = File.dirname(dst_path)
|
55
|
+
mkdir_p(parent) unless File.directory?(parent)
|
56
|
+
install(src_path, dst_path)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
#
|
62
|
+
# If FileUtils responds to a missing method, then call it.
|
63
|
+
#
|
64
|
+
def method_missing(s, *a, &b)
|
65
|
+
if FileUtils.respond_to?(s)
|
66
|
+
if $DRYRUN
|
67
|
+
FileUtils::DryRun.__send__(s, *a, &b)
|
68
|
+
else
|
69
|
+
FileUtils::Verbose.__send__(s, *a, &b)
|
70
|
+
end
|
71
|
+
else
|
72
|
+
super(s, *a, &b)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
end
|
data/lib/fire/state.rb
ADDED
@@ -0,0 +1,91 @@
|
|
1
|
+
module Fire
|
2
|
+
require 'fire/match'
|
3
|
+
|
4
|
+
# Fire's logic system is a *set logic* system. That means an empty set, `[]`
|
5
|
+
# is treated as `false` and a non-empty set is `true`.
|
6
|
+
#
|
7
|
+
# Fire handles complex logic by building-up lazy logic constructs. It's logical
|
8
|
+
# operators are defined using single charcter symbols, e.g. `&` and `|`.
|
9
|
+
#
|
10
|
+
class State
|
11
|
+
def initialize(&procedure)
|
12
|
+
@procedure = procedure
|
13
|
+
end
|
14
|
+
|
15
|
+
def call
|
16
|
+
set @procedure.call
|
17
|
+
end
|
18
|
+
|
19
|
+
# set or
|
20
|
+
def |(other)
|
21
|
+
State.new{ set(self.call) | set(other.call) }
|
22
|
+
end
|
23
|
+
|
24
|
+
# set and
|
25
|
+
def &(other)
|
26
|
+
State.new{ set(self.call) & set(other.call) }
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
#
|
32
|
+
def set(value)
|
33
|
+
case value
|
34
|
+
when Array
|
35
|
+
value.compact
|
36
|
+
when Boolean
|
37
|
+
value ? true : []
|
38
|
+
else
|
39
|
+
[value]
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
|
45
|
+
# File state.
|
46
|
+
#
|
47
|
+
class FileState < State
|
48
|
+
# Initialize new instance of Autologic.
|
49
|
+
#
|
50
|
+
# pattern - File glob or regular expression. [String,Regexp]
|
51
|
+
# digest -
|
52
|
+
# ignore -
|
53
|
+
#
|
54
|
+
def initialize(pattern, digest, ignore)
|
55
|
+
@pattern = pattern
|
56
|
+
@digest = digest
|
57
|
+
@ignore = ignore
|
58
|
+
end
|
59
|
+
|
60
|
+
# File glob or regular expression.
|
61
|
+
attr :pattern
|
62
|
+
|
63
|
+
# TODO: it would be nice if we could pass the regexp match too the procedure too
|
64
|
+
|
65
|
+
# Process logic.
|
66
|
+
def call
|
67
|
+
result = []
|
68
|
+
case pattern
|
69
|
+
when Regexp
|
70
|
+
@digest.current.keys.each do |fname|
|
71
|
+
if md = pattern.match(fname)
|
72
|
+
if @digest.current[fname] != @digest.saved[fname]
|
73
|
+
result << Match.new(fname, md)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
else
|
78
|
+
# TODO: if fnmatch? worked like glob then we'd follow the same code as for regexp
|
79
|
+
list = Dir[pattern].reject{ |path| @ignore.any?{ |ig| /^#{ig}/ =~ path } }
|
80
|
+
list.each do |fname|
|
81
|
+
if @digest.current[fname] != @digest.saved[fname]
|
82
|
+
result << fname
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
result
|
87
|
+
end
|
88
|
+
|
89
|
+
end
|
90
|
+
|
91
|
+
end
|
data/lib/fire/system.rb
ADDED
@@ -0,0 +1,244 @@
|
|
1
|
+
require 'ostruct'
|
2
|
+
require 'notify'
|
3
|
+
|
4
|
+
require 'fire/shellutils'
|
5
|
+
require 'fire/rule'
|
6
|
+
require 'fire/state'
|
7
|
+
require 'fire/task'
|
8
|
+
require 'fire/digest'
|
9
|
+
#require 'fire/rulefile'
|
10
|
+
|
11
|
+
module Fire
|
12
|
+
|
13
|
+
#
|
14
|
+
# Master system instance.
|
15
|
+
#
|
16
|
+
def self.system
|
17
|
+
@system ||= System.new
|
18
|
+
end
|
19
|
+
|
20
|
+
# System stores states and rules.
|
21
|
+
class System < Module
|
22
|
+
|
23
|
+
# TODO: there are some namespace issues to deal with here.
|
24
|
+
# we don't necessarily want a rule block to be able to call #rule.
|
25
|
+
|
26
|
+
# Instantiate new system.
|
27
|
+
#
|
28
|
+
def initialize(options={})
|
29
|
+
extend self
|
30
|
+
extend ShellUtils
|
31
|
+
|
32
|
+
@ignore = Array(options[:ignore] || [])
|
33
|
+
@files = Array(options[:files] || [])
|
34
|
+
|
35
|
+
@rules = []
|
36
|
+
@states = {}
|
37
|
+
@tasks = {}
|
38
|
+
|
39
|
+
@digest = Digest.new
|
40
|
+
@session = OpenStruct.new
|
41
|
+
|
42
|
+
@files.each do |file|
|
43
|
+
module_eval(File.read(file), file)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# Current session.
|
48
|
+
attr :session
|
49
|
+
|
50
|
+
# Array of defined states.
|
51
|
+
attr :states
|
52
|
+
|
53
|
+
# Array of defined rules.
|
54
|
+
attr :rules
|
55
|
+
|
56
|
+
# Mapping of defined tasks.
|
57
|
+
attr :tasks
|
58
|
+
|
59
|
+
# File digest.
|
60
|
+
attr :digest
|
61
|
+
|
62
|
+
# Import from another file, or glob of files, relative to project root.
|
63
|
+
#
|
64
|
+
# @todo Should importing be relative the importing file?
|
65
|
+
# @return nothing
|
66
|
+
def import(*globs)
|
67
|
+
globs.each do |glob|
|
68
|
+
#if File.relative?(glob)
|
69
|
+
# dir = Dir.pwd #session.root #File.dirname(caller[0])
|
70
|
+
# glob = File.join(dir, glob)
|
71
|
+
#end
|
72
|
+
Dir[glob].each do |file|
|
73
|
+
next unless File.file?(file)
|
74
|
+
#instance_eval(File.read(file), file)
|
75
|
+
module_eval(File.read(file), file)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# Add paths to be ignored in file rules.
|
81
|
+
def ignore(*globs)
|
82
|
+
@ignore.concat(globs.flatten)
|
83
|
+
@ignore
|
84
|
+
end
|
85
|
+
|
86
|
+
# Define a named state. States define conditions that are used to trigger
|
87
|
+
# rules. Named states are kept in a hash table to ensure that only one state
|
88
|
+
# is ever defined for a given name. Calling state again with the same name
|
89
|
+
# as a previously defined state will redefine the condition of that state.
|
90
|
+
#
|
91
|
+
# @example
|
92
|
+
# state :no_rdocs? do
|
93
|
+
# files = Dir.glob('lib/**/*.rb')
|
94
|
+
# FileUtils.uptodate?('doc', files) ? files : false
|
95
|
+
# end
|
96
|
+
#
|
97
|
+
# Returns nil if state name is given. [nil]
|
98
|
+
# Returns State in no name is given. [State]
|
99
|
+
def state(name=nil, &condition)
|
100
|
+
if name
|
101
|
+
if condition
|
102
|
+
@states[name.to_sym] = condition
|
103
|
+
define_method(name) do |*args|
|
104
|
+
state = @states[name.to_sym]
|
105
|
+
State.new{ states[name.to_sym].call(*args) }
|
106
|
+
end
|
107
|
+
else
|
108
|
+
raise ArgumentError
|
109
|
+
end
|
110
|
+
else
|
111
|
+
State.new{ condition.call(*args) }
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
# Define a file state.
|
116
|
+
#
|
117
|
+
# Returns [FileState]
|
118
|
+
def file(pattern)
|
119
|
+
FileState.new(pattern, digest, ignore)
|
120
|
+
end
|
121
|
+
|
122
|
+
# Define an environment state.
|
123
|
+
#
|
124
|
+
# Examples
|
125
|
+
# env('PATH'=>/foo/)
|
126
|
+
#
|
127
|
+
# Returns [State]
|
128
|
+
def env(name_to_pattern)
|
129
|
+
State.new do
|
130
|
+
name_to_pattern.any? do |name, re|
|
131
|
+
re === ENV[name.to_s] # or `all?` instead?
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
# Define a rule. Rules are procedures that are tiggered
|
137
|
+
# by logical states.
|
138
|
+
#
|
139
|
+
# Examples
|
140
|
+
# rule no_rdocs do |files|
|
141
|
+
# sh "rdoc --output doc/rdoc " + files.join(" ")
|
142
|
+
# end
|
143
|
+
#
|
144
|
+
def rule(state, &procedure)
|
145
|
+
state, todo = parse_arrow(state)
|
146
|
+
|
147
|
+
case state
|
148
|
+
when String, Regexp
|
149
|
+
file_rule(state, :todo=>todo, &procedure)
|
150
|
+
when Symbol
|
151
|
+
# TODO: Is this really the best idea?
|
152
|
+
#@states[state.to_sym]
|
153
|
+
else
|
154
|
+
@rules << Rule.new(state, :todo=>todo, &procedure)
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
#
|
159
|
+
# Check a name state.
|
160
|
+
#
|
161
|
+
def state?(name, *args)
|
162
|
+
@states[name.to_sym].call(*args)
|
163
|
+
end
|
164
|
+
|
165
|
+
#
|
166
|
+
# Run a task.
|
167
|
+
#
|
168
|
+
def run(task_name) #, *args)
|
169
|
+
tasks[task_name.to_sym].invoke #call(*args)
|
170
|
+
end
|
171
|
+
|
172
|
+
# Set task description. The next task defined will get the most
|
173
|
+
# recently defined description attached to it.
|
174
|
+
def desc(description)
|
175
|
+
@_desc = description
|
176
|
+
end
|
177
|
+
|
178
|
+
# Define a command line task. A task is special type of rule that
|
179
|
+
# is triggered when the command line tool is invoked with
|
180
|
+
# the name of the task.
|
181
|
+
#
|
182
|
+
# Tasks are an isolated set of rules and suppress the activation of
|
183
|
+
# all other rules not specifically given as prerequisites.
|
184
|
+
#
|
185
|
+
# task :rdoc do
|
186
|
+
# trip no_rdocs
|
187
|
+
# end
|
188
|
+
#
|
189
|
+
# Returns [Task]
|
190
|
+
def task(name_and_state, &procedure)
|
191
|
+
name, todo = parse_arrow(name_and_state)
|
192
|
+
task = Task.new(name, :todo=>todo, :desc=>@_desc, &procedure)
|
193
|
+
@tasks[name.to_sym] = task
|
194
|
+
@_desc = nil
|
195
|
+
task
|
196
|
+
end
|
197
|
+
|
198
|
+
#
|
199
|
+
# Issue notification.
|
200
|
+
#
|
201
|
+
def notify(message, options={})
|
202
|
+
title = options.delete(:title) || 'Fire Notification'
|
203
|
+
Notify.notify(title, message.to_s, options)
|
204
|
+
end
|
205
|
+
|
206
|
+
private
|
207
|
+
|
208
|
+
# Split a hash argument into it's key and value pair.
|
209
|
+
# The hash is expected to have only one entry. If the argument
|
210
|
+
# is not a hash then returns the argument and an empty array.
|
211
|
+
#
|
212
|
+
# Raises an [ArgumetError] if the hash has more than one entry.
|
213
|
+
#
|
214
|
+
# Returns key and value. [Array]
|
215
|
+
def parse_arrow(argument)
|
216
|
+
case argument
|
217
|
+
when Hash
|
218
|
+
raise ArgumentError if argument.size > 1
|
219
|
+
head, tail = *argument.to_a.first
|
220
|
+
return head, Array(tail)
|
221
|
+
else
|
222
|
+
return argument, []
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
# TODO: pass `self` to FileState instead of digest and igonre ?
|
227
|
+
|
228
|
+
# Define a file rule. A file rule is a rule with a specific state
|
229
|
+
# based on changes in files.
|
230
|
+
#
|
231
|
+
# @example
|
232
|
+
# file_rule 'test/**/case_*.rb' do |files|
|
233
|
+
# sh "ruby-test " + files.join(" ")
|
234
|
+
# end
|
235
|
+
#
|
236
|
+
# Returns nothing.
|
237
|
+
def file_rule(pattern, options={}, &procedure)
|
238
|
+
state = FileState.new(pattern, digest, ignore)
|
239
|
+
@rules << Rule.new(state, options, &procedure)
|
240
|
+
end
|
241
|
+
|
242
|
+
end
|
243
|
+
|
244
|
+
end
|
data/lib/fire/task.rb
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
module Fire
|
2
|
+
|
3
|
+
# The Task class encapsulates command-line dependent rules.
|
4
|
+
#
|
5
|
+
class Task
|
6
|
+
#
|
7
|
+
def initialize(name, options={}, &procedure)
|
8
|
+
@name = name
|
9
|
+
@description = options[:desc]
|
10
|
+
@requisite = options[:todo] || []
|
11
|
+
@procedure = procedure
|
12
|
+
|
13
|
+
#@_reducing = nil
|
14
|
+
end
|
15
|
+
|
16
|
+
# The tasks name.
|
17
|
+
attr :name
|
18
|
+
|
19
|
+
# Task description. This is need for a task to
|
20
|
+
# available via the command line.
|
21
|
+
attr :description
|
22
|
+
|
23
|
+
#
|
24
|
+
attr :requisite
|
25
|
+
|
26
|
+
#
|
27
|
+
alias :todo :requisite
|
28
|
+
|
29
|
+
# Run the task.
|
30
|
+
def invoke(&prepare)
|
31
|
+
prepare.call
|
32
|
+
call
|
33
|
+
end
|
34
|
+
|
35
|
+
# Alias for #invoke.
|
36
|
+
alias :apply :invoke
|
37
|
+
|
38
|
+
#def to_s
|
39
|
+
# @description.to_s
|
40
|
+
#end
|
41
|
+
|
42
|
+
protected
|
43
|
+
|
44
|
+
#
|
45
|
+
def call
|
46
|
+
@procedure.call
|
47
|
+
end
|
48
|
+
|
49
|
+
=begin
|
50
|
+
# Reduce task list.
|
51
|
+
#
|
52
|
+
# Returns [Array<Task>]
|
53
|
+
def reduce
|
54
|
+
return [] if @_reducing
|
55
|
+
list = []
|
56
|
+
begin
|
57
|
+
@_reducing = true
|
58
|
+
@requisite.each do |r|
|
59
|
+
list << @system.tasks[r.to_sym].reduce
|
60
|
+
end
|
61
|
+
list << self
|
62
|
+
ensure
|
63
|
+
@_reducing = false
|
64
|
+
end
|
65
|
+
list.flatten.uniq
|
66
|
+
end
|
67
|
+
=end
|
68
|
+
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|