fire 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -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
@@ -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
@@ -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