bee 0.1.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.
@@ -0,0 +1,108 @@
1
+ <!--
2
+ Copyright 2006 Michel Casabianca <casa@sweetohm.net>
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ -->
16
+
17
+ <h2>Scripts</h2>
18
+
19
+ <p>Let's start with traditional <i>Hello World!</i> example:</p>
20
+
21
+ <pre class="code"><code>- target: hello
22
+ script:
23
+ - rb: "name = ENV['USER'].capitalize"
24
+ - "echo \"Hello #{name}!\""</code></pre>
25
+
26
+ <p>A target script can be made of shell and/or Ruby scripts. Variables
27
+ defined in Ruby scripts can be accessed in shell using Ruby syntax
28
+ <code>#{variable}</code>. Each shell script is executed in its own
29
+ context while Ruby variables all live in a global one that persist
30
+ in the whole build. This script will output:</p>
31
+
32
+ <pre class="code"><code>----------------------------------------------------------------------- hello --
33
+ Hello Casa!
34
+ OK</code></pre>
35
+
36
+ <h2>Targets</h2>
37
+
38
+ <p>You can set default target and dependencies as follows:</p>
39
+
40
+ <pre class="code"><code>- build: hello
41
+ default: hello
42
+
43
+ - target: name
44
+ script:
45
+ - rb: "name = ENV['USER'].capitalize"
46
+
47
+ - target: hello
48
+ depends: name
49
+ script:
50
+ - "echo \"Hello #{name}!\""
51
+ </code></pre>
52
+
53
+ <p>When running without indicating any target, bee will run target
54
+ <i>hello</i> (default one, as stated in <code>default</code> key).
55
+ Nevertheless, this target depends on <i>name</i>, thus bee will first
56
+ run target <i>name</i>, then <i>hello</i>. This will write on console:</p>
57
+
58
+ <pre class="code"><code>------------------------------------------------------------------------ name --
59
+ ----------------------------------------------------------------------- hello --
60
+ Hello Casa!
61
+ OK</code></pre>
62
+
63
+ <h2>Properties</h2>
64
+
65
+ <p>You can define build properies as follows:</p>
66
+
67
+ <pre class="code"><code>- properties:
68
+ - name: "casa"
69
+ - capitalized: "#{name.capitalize}"
70
+
71
+ - target: hello
72
+ script:
73
+ - "echo \"Hello #{capitalized}!\""</code></pre>
74
+
75
+ <p>First, we set property <i>name</i> to <i>casa</i>, a simple string.
76
+ Then, we set <i>capitalized</i> to <i>Casa</i>, using a Ruby expression.
77
+ Properties can use values defined in other properties. Furthermore, we
78
+ can use these properties in shell or Ruby scripts.</p>
79
+
80
+ <h2>Context</h2>
81
+
82
+ <p>Ruby scripts run in a global context that persist for the whole build.
83
+ This context is similar to the place you write your code in IRB. In
84
+ this context, you can define variables, functions or classes. You can
85
+ define your own context in a file loaded at startup. This file is the
86
+ place to define utility functions you would use in your embedded Ruby
87
+ scripts.</p>
88
+
89
+ <p>For instance, loading script <i>hello.rb</i>:</p>
90
+
91
+ <pre class="code"><code>def say_hello(who)
92
+ puts "Hello #{who}!"
93
+ end</code></pre>
94
+
95
+ <p>In following build file using <code>context</code> as follows:</p>
96
+
97
+ <pre class="code"><code>- build: hello
98
+ context: hello.rb
99
+
100
+ - properties:
101
+ - me: "Casa"
102
+
103
+ - target: hello
104
+ script:
105
+ - rb: "say_hello(me)"</code></pre>
106
+
107
+ <p>After this quick introduction, you should know enough to start writing
108
+ your first build file. Enjoy!</p>
Binary file
@@ -0,0 +1,36 @@
1
+ # Copyright 2006 Michel Casabianca <casa@sweetohm.net>
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ - description: >
16
+ Here is the list of Bee release with new features and links to download.
17
+ download: http://rubyforge.org/frs/?group_id=2479
18
+
19
+ - release: alpha
20
+ description: A simple night hack.
21
+
22
+ - release: beta-1
23
+ description: First public release.
24
+ new:
25
+ - Run Ruby scripts.
26
+ - Load context on startup.
27
+ - Added checks on build files syntax.
28
+ - Documentation system.
29
+
30
+ - release: 0.1.0
31
+ description: First Gem package.
32
+ new:
33
+ - Distribution as a Gem package.
34
+ - Template generation (-t option).
35
+ fixed:
36
+ - Directory in distribution archive postfixed with version.
@@ -0,0 +1,49 @@
1
+ # Copyright 2006 Michel Casabianca <casa@sweetohm.net>
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ - menu: bee
16
+
17
+ - title: Quick Start
18
+ file: ../html/quickstart.html
19
+ dest: index.html
20
+ type: html
21
+
22
+ - title: Downloads
23
+ dest: http://rubyforge.org/frs/?group_id=2479
24
+ type: link
25
+
26
+ - title: Documentation
27
+ file: ../html/documentation.html
28
+ dest: documentation.html
29
+ type: html
30
+
31
+ - title: Source API
32
+ dir: ../../build/api/
33
+ dest: api
34
+ type: dir
35
+
36
+ - title: License
37
+ file: ../../LICENSE
38
+ dest: license.html
39
+ type: text
40
+
41
+ - title: Syntax check
42
+ file: ../../build/syntax.txt
43
+ dest: syntax.html
44
+ type: text
45
+
46
+ - title: Unit tests
47
+ file: ../../build/tests.txt
48
+ dest: tests.html
49
+ type: text
@@ -0,0 +1,36 @@
1
+ # Copyright 2006 Michel Casabianca <casa@sweetohm.net>
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ - description: >
16
+ Here is the list of Bee release with new features and links to download.
17
+ download: http://rubyforge.org/frs/?group_id=2479
18
+
19
+ - release: alpha
20
+ description: A simple night hack.
21
+
22
+ - release: beta-1
23
+ description: First public release.
24
+ new:
25
+ - Run Ruby scripts.
26
+ - Load context on startup.
27
+ - Added checks on build files syntax.
28
+ - Documentation system.
29
+
30
+ - release: 0.1.0
31
+ description: Distribute bee as a gem.
32
+ new:
33
+ - Distribution as a Gem package.
34
+ - Template generation (-t option).
35
+ fixed:
36
+ - Directory in distribution archive postfixed with version.
@@ -0,0 +1,17 @@
1
+ # Copyright 2006 Michel Casabianca <casa@sweetohm.net>
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ - Write documentation in RDoc format.
16
+ - Freeze and document extension mechanism.
17
+ - Write extension tasks to build bee on any platform.
@@ -0,0 +1,916 @@
1
+ # Copyright 2006 Michel Casabianca <casa@sweetohm.net>
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ # Module for Bee stuff.
16
+ module Bee
17
+
18
+ require 'yaml'
19
+ require 'getoptlong'
20
+
21
+ # Copyright notice.
22
+ COPYRIGHT = 'Bee version 0.1.0 (C) Michel Casabianca - 2006'
23
+ # Command line help.
24
+ HELP = 'Usage: bee [options] [targets]
25
+ -h Print help about usage and exit.
26
+ -b Print help about build and exit.
27
+ -t Write template build file on disk.
28
+ -v Enable verbose mode.
29
+ -s style Define style for output (see documentation).
30
+ -f file Build file to run (defaults to "build.yml").
31
+ targets Targets to run (default target if omitted).'
32
+ # Name for default build file.
33
+ DEFAULT_BUILD_FILE = 'build.yml'
34
+ # Exit value on error parsing command line
35
+ EXIT_PARSING_CMDLINE = 1
36
+ # Exit value on build error
37
+ EXIT_BUILD_ERROR = 2
38
+ # Exit value on unknown error
39
+ EXIT_UNKNOWN_ERROR = 3
40
+
41
+ # Parse command line and return parsed arguments.
42
+ def self.parse_command_line
43
+ help = false
44
+ help_build = false
45
+ template = false
46
+ verbose = false
47
+ style = nil
48
+ file = DEFAULT_BUILD_FILE
49
+ targets = []
50
+ # read options in BEEOPT environment variable
51
+ options = ENV['BEEOPT']
52
+ options.split(' ').reverse.each { |option| ARGV.unshift(option) } if options
53
+ # parse command line arguments
54
+ opts = GetoptLong.new(['--help', '-h', GetoptLong::NO_ARGUMENT ],
55
+ ['--help-build', '-b', GetoptLong::NO_ARGUMENT ],
56
+ ['--template', '-t', GetoptLong::NO_ARGUMENT ],
57
+ ['--verbose', '-v', GetoptLong::NO_ARGUMENT],
58
+ ['--style', '-s', GetoptLong::REQUIRED_ARGUMENT],
59
+ ['--file', '-f', GetoptLong::REQUIRED_ARGUMENT])
60
+ opts.each do |opt, arg|
61
+ case opt
62
+ when '--help'
63
+ help = true
64
+ when '--help-build'
65
+ help_build = true
66
+ when '--template'
67
+ template = true
68
+ when '--verbose'
69
+ verbose = true
70
+ when '--style'
71
+ style = arg
72
+ when '--file'
73
+ file = arg
74
+ end
75
+ end
76
+ targets = ARGV
77
+ return [help, help_build, template, verbose, style, file, targets]
78
+ end
79
+
80
+ # Start build from command line.
81
+ def self.start_command_line
82
+ STDOUT.sync = true
83
+ begin
84
+ help, help_build, template, verbose, style, file, targets =
85
+ parse_command_line
86
+ rescue
87
+ puts "ERROR: parsing command line (type 'bee -h' for help)"
88
+ exit(EXIT_PARSING_CMDLINE)
89
+ end
90
+ formatter = ConsoleFormatter.new(style)
91
+ begin
92
+ if help
93
+ puts COPYRIGHT
94
+ puts HELP
95
+ elsif help_build
96
+ build = Build.new(file)
97
+ puts formatter.help_build(build)
98
+ elsif template
99
+ puts "Writing build template in file '#{file}'..."
100
+ raise BuildError.new("Build file '#{file}' already exists") if
101
+ File.exists?(file)
102
+ File.open(file, 'w') { |file| file.write(BUILD_TEMPLATE) }
103
+ puts formatter.format_success("OK")
104
+ else
105
+ listener = ConsoleListener.new(formatter, verbose)
106
+ build = Build.new(file)
107
+ build.run(targets, listener)
108
+ end
109
+ rescue BuildError
110
+ puts "#{formatter.format_error('ERROR')}: #{$!}"
111
+ exit(EXIT_BUILD_ERROR)
112
+ rescue SystemExit
113
+ # do nothing, exit in code
114
+ rescue Exception => e
115
+ puts "#{formatter.format_error('ERROR')}: #{$!}"
116
+ puts e.backtrace.join("\n")
117
+ exit(EXIT_UNKNOWN_ERROR)
118
+ end
119
+ end
120
+
121
+ #########################################################################
122
+ # BUILD ERROR #
123
+ #########################################################################
124
+
125
+ class BuildError < RuntimeError; end
126
+
127
+ module BuildErrorMixin
128
+
129
+ # Convenient method to raise a BuildError.
130
+ # - message: error message.
131
+ def error(message)
132
+ raise BuildError.new(message)
133
+ end
134
+
135
+ end
136
+
137
+ #########################################################################
138
+ # CONSOLE FORMATTER CLASS #
139
+ #########################################################################
140
+
141
+ # Class to format build output on console.
142
+ class ConsoleFormatter
143
+
144
+ include BuildErrorMixin
145
+
146
+ ###################### ANSI COLORS AND STYLES #########################
147
+
148
+ # List of colors.
149
+ COLORS = [:black, :red, :green, :yellow, :blue, :magenta, :cyan, :white]
150
+ # Foreground color codes.
151
+ FOREGROUND_COLOR_CODES = {
152
+ :black => 30,
153
+ :red => 31,
154
+ :green => 32,
155
+ :yellow => 33,
156
+ :blue => 34,
157
+ :magenta => 35,
158
+ :cyan => 36,
159
+ :white => 37
160
+ }
161
+ # Background color codes.
162
+ BACKGROUND_COLOR_CODES = {
163
+ :black => 40,
164
+ :red => 41,
165
+ :green => 42,
166
+ :yellow => 43,
167
+ :blue => 44,
168
+ :magenta => 45,
169
+ :cyan => 46,
170
+ :white => 47
171
+ }
172
+ # List of styles.
173
+ STYLES = [:reset, :bright, :dim, :underscore, :blink, :reverse, :hidden]
174
+ # Style codes.
175
+ STYLE_CODES = {
176
+ :reset => 0,
177
+ :bright => 1,
178
+ :dim => 2,
179
+ :underscore => 4,
180
+ :blink => 5,
181
+ :reverse => 7,
182
+ :hidden => 8
183
+ }
184
+
185
+ ############################ DEFAULT STYLE ############################
186
+
187
+ # Default style (supposed to work on any configuration).
188
+ DEFAULT_STYLE = {
189
+ :line_character => '-'
190
+ }
191
+ # Default line length.
192
+ DEFAULT_LINE_LENGTH = 80
193
+ # Short style keys for command line
194
+ SHORT_STYLE_KEYS = {
195
+ 'lc' => 'line_character',
196
+ 'll' => 'line_length',
197
+ 'ts' => 'target_style',
198
+ 'tf' => 'target_foreground',
199
+ 'tb' => 'target_background',
200
+ 'ks' => 'task_style',
201
+ 'kf' => 'task_foreground',
202
+ 'kb' => 'task_background',
203
+ 'ss' => 'success_style',
204
+ 'sf' => 'success_foreground',
205
+ 'sb' => 'success_background',
206
+ 'es' => 'error_style',
207
+ 'ef' => 'error_foreground',
208
+ 'eb' => 'error_background'
209
+ }
210
+
211
+ ############################## METHODS ################################
212
+
213
+ # Constructor.
214
+ # - style: style as a Hash or a String.
215
+ def initialize(style)
216
+ # if style is a String, this is command line argument
217
+ style = parse_style_from_command_line(style) if style.kind_of?(String)
218
+ # if style is nil, set default style
219
+ @style = style || DEFAULT_STYLE
220
+ # set default values for keys if nil
221
+ for key in DEFAULT_STYLE.keys
222
+ @style[key] = @style[key] || DEFAULT_STYLE[key]
223
+ end
224
+ end
225
+
226
+ # Format a target.
227
+ # - target: target to format.
228
+ def format_target(target)
229
+ name = target.name
230
+ # generate title line
231
+ length = @style[:line_length] || line_length || DEFAULT_LINE_LENGTH
232
+ right = ' ' + @style[:line_character]*2
233
+ left = @style[:line_character]*(length - (name.length + 4)) + ' '
234
+ line = left + name + right
235
+ # apply style
236
+ formatted = style(line,
237
+ @style[:target_style],
238
+ @style[:target_foreground],
239
+ @style[:target_background])
240
+ return formatted
241
+ end
242
+
243
+ # Format a task.
244
+ # - task: task to format.
245
+ def format_task(task)
246
+ if task.kind_of?(String)
247
+ source = task
248
+ elsif task.kind_of?(Hash)
249
+ if task.key?('rb')
250
+ source = task['rb']
251
+ else
252
+ source = YAML::dump(task)
253
+ source = source.sub(/---/, '')
254
+ end
255
+ end
256
+ formatted = '- ' + source.strip.gsub(/\n/, "\n. ")
257
+ styled = style(formatted,
258
+ @style[:task_style],
259
+ @style[:task_foreground],
260
+ @style[:task_background])
261
+ return styled
262
+ end
263
+
264
+ # Format a success string.
265
+ # - string: string to format.
266
+ def format_success(string)
267
+ string = style(string,
268
+ @style[:success_style],
269
+ @style[:success_foreground],
270
+ @style[:success_background])
271
+ return string
272
+ end
273
+
274
+ # Format an error string.
275
+ # - string: string to format.
276
+ def format_error(string)
277
+ string = style(string,
278
+ @style[:error_style],
279
+ @style[:error_foreground],
280
+ @style[:error_background])
281
+ return string
282
+ end
283
+
284
+ # Return help about build.
285
+ def help_build(build)
286
+ help = ''
287
+ if build.name
288
+ help << "- Build: #{build.name.inspect}\n"
289
+ end
290
+ if build.description
291
+ help << format_description('Description', build.description, 2, false)
292
+ end
293
+ if build.properties.keys.length > 0
294
+ help << "- Properties:\n"
295
+ for property in build.properties.keys.sort
296
+ help << " - #{property}: #{build.properties[property].inspect}\n"
297
+ end
298
+ end
299
+ if build.targets.length > 0
300
+ help << "- Targets:\n"
301
+ for target in build.targets.values.sort { |a, b| a.name <=> b.name }
302
+ help << format_description(target.name, target.description, 2)
303
+ end
304
+ end
305
+ help << "- Default: #{build.default}"
306
+ return help.strip
307
+ end
308
+
309
+ private
310
+
311
+ # Apply style to a string:
312
+ # - string: the string to apply style to.
313
+ # - style: style to apply on string.
314
+ # - foreground: foreground color for string.
315
+ # - background: background color for string.
316
+ def style(string, style, foreground, background)
317
+ # check style, foreground and background colors
318
+ error "Unknown style '#{style}'" unless
319
+ STYLES.member?(style) or not style
320
+ error "Unknown color '#{foreground}'" unless
321
+ COLORS.member?(foreground) or not foreground
322
+ error "Unknown color '#{background}'" unless
323
+ COLORS.member?(background) or not background
324
+ # if no style nor colors, return raw string
325
+ return string if not foreground and not background and not style
326
+ # insert style and colors in string
327
+ colorized = "\e["
328
+ colorized << "#{STYLE_CODES[style]};" if style
329
+ colorized << "#{FOREGROUND_COLOR_CODES[foreground]};" if foreground
330
+ colorized << "#{BACKGROUND_COLOR_CODES[background]};" if background
331
+ colorized = colorized[0..-2]
332
+ colorized << "m#{string}\e[#{STYLE_CODES[:reset]}m"
333
+ return colorized
334
+ end
335
+
336
+ # Get line length calling IOCTL. Return nil if call failed.
337
+ def line_length
338
+ begin
339
+ tiocgwinsz = 0x5413
340
+ string = [0, 0, 0, 0].pack('SSSS')
341
+ if $stdin.ioctl(tiocgwinsz, string) >= 0 then
342
+ rows, cols, xpixels, ypixels = string.unpack('SSSS')
343
+ return cols
344
+ end
345
+ rescue
346
+ return nil
347
+ end
348
+ end
349
+
350
+ # Parse style from command line. If error occurs parsing style, return
351
+ # nil (which means default style).
352
+ # - string: style to parse.
353
+ def parse_style_from_command_line(string)
354
+ return if not string
355
+ style = {}
356
+ begin
357
+ for pair in string.split(',')
358
+ key, value = pair.split(':')
359
+ key = SHORT_STYLE_KEYS[key] || key
360
+ key = key.to_sym
361
+ if key == :line_length
362
+ value = value.to_i
363
+ elsif key == :line_character
364
+ value = ' ' if not value or value.length == 0
365
+ else
366
+ value = value.to_sym
367
+ error "Unkown color or style '#{value}'" if
368
+ not COLORS.member?(value) and not STYLES.member?(value)
369
+ end
370
+ style[key] = value
371
+ end
372
+ return style
373
+ rescue
374
+ # if parsing fails, return default style (nil)
375
+ return nil
376
+ end
377
+ end
378
+
379
+ # Format a description.
380
+ # - title: description title (project, property or target name).
381
+ # - text: description text.
382
+ # - indent: indentation width.
383
+ # - bullet: tells if we must put a bullet.
384
+ def format_description(title, text=nil, indent=0, bullet=true)
385
+ string = ' '*indent
386
+ string << '- ' if bullet
387
+ string << title
388
+ if text
389
+ string << ": "
390
+ if text.split("\n").length > 1
391
+ string << "\n"
392
+ text.split("\n").each do |line|
393
+ string << ' '*(indent+2) + line.strip + "\n"
394
+ end
395
+ else
396
+ string << text.strip + "\n"
397
+ end
398
+ end
399
+ return string
400
+ end
401
+
402
+ end
403
+
404
+ #########################################################################
405
+ # CONSOLE LISTENER #
406
+ #########################################################################
407
+
408
+ # Listener when running in a console. Prints messages on the console using
409
+ # a given formatter.
410
+ class ConsoleListener
411
+
412
+ # Formatter used by listener.
413
+ attr_reader :formatter
414
+ # Verbosity flag.
415
+ attr_reader :verbose
416
+ # Build start time.
417
+ attr_reader :start_time
418
+ # Build end time.
419
+ attr_reader :end_time
420
+ # Build duration.
421
+ attr_reader :duration
422
+ # Build success.
423
+ attr_reader :success
424
+ # Last target met.
425
+ attr_reader :last_target
426
+ # Last task met.
427
+ attr_reader :last_task
428
+
429
+ # Constructor.
430
+ # - formatter: the formatter to use to output on console.
431
+ # - verbose: tells if we run in verbose mode.
432
+ def initialize(formatter, verbose)
433
+ @formatter = formatter
434
+ @verbose = verbose
435
+ end
436
+
437
+ # Called when build is started.
438
+ # - build: the build object.
439
+ def build_started(build)
440
+ @start_time = Time.now
441
+ @end_time = nil
442
+ @duration = nil
443
+ @success = nil
444
+ @last_target = nil
445
+ @last_task = nil
446
+ puts "Starting build '#{build.file}'..." if @verbose
447
+ end
448
+
449
+ # Called when build is finished.
450
+ # - build: the build object.
451
+ def build_finished(build)
452
+ @end_time = Time.now
453
+ @duration = @end_time - @start_time
454
+ @success = true
455
+ puts "Built in #{@duration} s" if @verbose
456
+ puts @formatter.format_success('OK')
457
+ end
458
+
459
+ # Called when a target is met.
460
+ # - target: the target object.
461
+ def target(target)
462
+ @last_target = target
463
+ @last_task = nil
464
+ puts @formatter.format_target(target)
465
+ end
466
+
467
+ # Called when a task is met.
468
+ # - task: task source (shell, Ruby or task).
469
+ def task(task)
470
+ @last_task = task
471
+ puts @formatter.format_task(task) if @verbose
472
+ end
473
+
474
+ # Called when an error was raised.
475
+ # - exception: raised exception.
476
+ def error(exception)
477
+ @end_time = Time.now
478
+ @duration = @end_time - @start_time
479
+ @success = false
480
+ puts "Built in #{@duration} s" if @verbose
481
+ message = ''
482
+ message << "In target '#{@last_target.name}'" if @last_target
483
+ message << ", in task:\n#{@formatter.format_task(@last_task)}\n" if
484
+ @last_task
485
+ message << ': ' if @last_target and not @last_task
486
+ message << exception.to_s
487
+ puts "#{@formatter.format_error('ERROR')}: #{message}"
488
+ end
489
+
490
+ end
491
+
492
+ #########################################################################
493
+ # BUILD CLASS #
494
+ #########################################################################
495
+
496
+ # Class for a given build.
497
+ class Build
498
+
499
+ include BuildErrorMixin
500
+
501
+ # Build file.
502
+ attr_reader :file
503
+ # Base directory.
504
+ attr_reader :base
505
+ # Build name.
506
+ attr_reader :name
507
+ # Default target.
508
+ attr_reader :default
509
+ # Build description.
510
+ attr_reader :description
511
+ # Properties hash (used for project help).
512
+ attr_reader :properties
513
+ # Hash for targets, indexed by target name.
514
+ attr_reader :targets
515
+ # Context for Ruby scripts and properties.
516
+ attr_reader :context
517
+ # Loaded extensions.
518
+ attr_reader :extensions
519
+ # Build listener.
520
+ attr_reader :listener
521
+
522
+ # Constructor:
523
+ # - file: build file to load.
524
+ def initialize(file)
525
+ @file = file
526
+ @properties = {}
527
+ @targets = {}
528
+ @context = Context.new
529
+ @extensions = {}
530
+ @base = File.expand_path(File.dirname(@file))
531
+ @context.set('base', @base)
532
+ @properties['base'] = @base
533
+ load_context(DEFAULT_CONTEXT)
534
+ # load build file
535
+ begin
536
+ source = File.read(@file)
537
+ build = YAML::load(source)
538
+ # parse object entries
539
+ for entry in build
540
+ if entry.key?('build')
541
+ # build info entry
542
+ error "Duplicate build info" if @name
543
+ @name = entry['build']
544
+ @default = entry['default']
545
+ @description = entry['description']
546
+ # load context files if any
547
+ if entry['context']
548
+ for script in entry['context']
549
+ begin
550
+ based_script = File.join(@base, script)
551
+ evaluated_script = @context.evaluate_string(based_script)
552
+ source = File.read(evaluated_script)
553
+ load_context(source)
554
+ rescue
555
+ error "Error loading context '#{script}': #{$!}"
556
+ end
557
+ end
558
+ end
559
+ elsif entry.key?('properties')
560
+ # properties entry
561
+ for property in entry['properties']
562
+ begin
563
+ name = property.keys[0]
564
+ value = @context.evaluate_string(property[name])
565
+ error "Duplicate property '#{name}' definition" if
566
+ @properties[name]
567
+ @properties[name] = value
568
+ @context.set(name, value)
569
+ rescue
570
+ error "Error evaluating property '#{name}': #{$!}"
571
+ end
572
+ end
573
+ elsif entry.key?('target')
574
+ # target entry
575
+ target = Target.new(entry, self)
576
+ @default = target.name if not @default and @targets.keys.length == 0
577
+ error "Duplicate target definition: '#{target.name}'" if
578
+ @targets[target.name]
579
+ @targets[target.name] = target
580
+ else
581
+ # unknown entry
582
+ error "Unknown entry:\n#{YAML::dump(entry)}"
583
+ end
584
+ end
585
+ rescue
586
+ error "Error loading build file '#{@file}': #{$!}"
587
+ end
588
+ end
589
+
590
+ # Run build:
591
+ # - targets: list of targets to run.
592
+ # - listener: listener for the build.
593
+ def run(targets, listener=nil)
594
+ @listener = listener
595
+ working_directory = Dir.getwd
596
+ begin
597
+ @listener.build_started(self) if @listener
598
+ Dir.chdir(base)
599
+ error "No default target given" if (!@default and targets.length == 0)
600
+ targets = [@default] if targets.length == 0
601
+ for target in targets
602
+ run_target(target)
603
+ end
604
+ @listener.build_finished(self)
605
+ rescue BuildError => e
606
+ if listener
607
+ @listener.error(e)
608
+ else
609
+ raise e
610
+ end
611
+ ensure
612
+ @listener = nil
613
+ Dir.chdir(working_directory)
614
+ end
615
+ end
616
+
617
+ # Run a given target.
618
+ # - target: the target to run.
619
+ def run_target(target)
620
+ error "Target '#{target}' not found" if not @targets[target]
621
+ @targets[target].run
622
+ end
623
+
624
+ private
625
+
626
+ # Load a given context.
627
+ # - source: the context source.
628
+ def load_context(source)
629
+ @context.evaluate(source)
630
+ @extensions = @context.get('TASKS')
631
+ end
632
+
633
+ end
634
+
635
+ #########################################################################
636
+ # TARGET CLASS #
637
+ #########################################################################
638
+
639
+ # Class for a given target.
640
+ class Target
641
+
642
+ include BuildErrorMixin
643
+
644
+ # Build that encapsulates target.
645
+ attr_reader :build
646
+ # Name of the target.
647
+ attr_reader :name
648
+ # Target dependencies.
649
+ attr_reader :depends
650
+ # Target description.
651
+ attr_reader :description
652
+ # Script that run in the target.
653
+ attr_reader :script
654
+
655
+ # Constructor.
656
+ # - target: target for target, resulting from YAML parsing.
657
+ # - build: build that encapsulate this target.
658
+ def initialize(target, build)
659
+ @build = build
660
+ @name = target['target']
661
+ @depends = target['depends']||[]
662
+ @description = target['description']
663
+ @script = target['script']
664
+ end
665
+
666
+ # Run target.
667
+ def run
668
+ if not @evaluated
669
+ @evaluated = true
670
+ for depend in @depends
671
+ @build.run_target(depend)
672
+ end
673
+ @build.listener.target(self) if @build.listener
674
+ if @script
675
+ case @script
676
+ when String
677
+ run_task(@script)
678
+ when Array
679
+ for task in @script
680
+ run_task(task)
681
+ end
682
+ end
683
+ end
684
+ end
685
+ end
686
+
687
+ private
688
+
689
+ # Run a task.
690
+ # - task: the task to run.
691
+ def run_task(task)
692
+ @build.listener.task(task) if @build.listener
693
+ case task
694
+ when String
695
+ # shell script
696
+ run_shell(task)
697
+ when Hash
698
+ error "A task entry must be a Hash with a single key" if
699
+ task.keys.length != 1
700
+ if task.key?('rb')
701
+ # ruby script
702
+ script = task['rb']
703
+ run_ruby(script)
704
+ else
705
+ # must be a task
706
+ run_extension(task)
707
+ end
708
+ end
709
+ end
710
+
711
+ # Run a given shell script.
712
+ # - script: the scrip to run.
713
+ def run_shell(script)
714
+ @listener.task(script) if @listener
715
+ evaluated_script = @build.context.evaluate_string(script)
716
+ system(evaluated_script)
717
+ error "Script exited with value #{$?}" if $? != 0
718
+ end
719
+
720
+ # Run a given shell script.
721
+ # - script: the scrip to run.
722
+ def run_ruby(script)
723
+ @listener.task(script) if @listener
724
+ @build.context.evaluate(script)
725
+ end
726
+
727
+ # Run a given task.
728
+ # - task: task to run as a Hash.
729
+ def run_extension(task)
730
+ @listener.task(task) if @listener
731
+ name = task.keys[0]
732
+ error "Unknown task '#{name}'" if
733
+ not @build.extensions.key?(name)
734
+ parameters = task[name]
735
+ script = "#{name}(#{parameters.inspect})"
736
+ @build.context.evaluate(script)
737
+ end
738
+
739
+ end
740
+
741
+ #########################################################################
742
+ # CONTEXT CLASS #
743
+ #########################################################################
744
+
745
+ # Class for Ruby scripts context.
746
+ class Context
747
+
748
+ include BuildErrorMixin
749
+
750
+ # Constructor.
751
+ def initialize
752
+ @binding = get_binding
753
+ end
754
+
755
+ # Set a given value in context.
756
+ # - name: the variable name to set.
757
+ # - value: the variable value.
758
+ def set(name, value)
759
+ begin
760
+ eval("#{name} = #{value.inspect}", @binding)
761
+ rescue
762
+ error "Error setting property '#{name} = #{value.inspect}': #{$!}"
763
+ end
764
+ end
765
+
766
+ # Get a given value in context.
767
+ # - name: the variable name.
768
+ def get(name)
769
+ begin
770
+ eval("#{name}", @binding)
771
+ rescue
772
+ error "Error getting property '#{name}': #{$!}"
773
+ end
774
+ end
775
+
776
+ # Evaluate a script in context.
777
+ # - script: script to evaluate.
778
+ def evaluate(script)
779
+ begin
780
+ eval(script, @binding)
781
+ rescue
782
+ error "Error running script: #{$!}"
783
+ end
784
+ end
785
+
786
+ # Process a given string, replacing properties references with their
787
+ # value. Property references have same form than variable references
788
+ # in ruby strings: '#{variable}' will be replaced with variable value.
789
+ # - string: string to process.
790
+ def evaluate_string(string)
791
+ return nil if string == nil
792
+ return string if not string.kind_of?(String)
793
+ string = string.gsub(/#\{.+?\}/) do |match|
794
+ property = match[2..-2]
795
+ value = get(property)
796
+ error ("Property '#{property}' was not defined") unless value
797
+ value
798
+ end
799
+ return string
800
+ end
801
+
802
+ private
803
+
804
+ # Get a binding as script context.
805
+ def get_binding
806
+ return binding
807
+ end
808
+
809
+ end
810
+
811
+ #########################################################################
812
+ # DEFAULT CONTEXT #
813
+ #########################################################################
814
+
815
+ DEFAULT_CONTEXT = '
816
+ # Default context script loaded at startup.
817
+
818
+ # Hash of tasks help indexed by their name.
819
+ TASKS = {}
820
+
821
+ ######################### TASKS UTILITY FUNCTIONS #########################
822
+
823
+ # Bind tasks. This method must be called by context scripts defining tasks.
824
+ # - tasks: Hash indexing task help with task names.
825
+ def bind_tasks(tasks)
826
+ for name, help in tasks
827
+ raise "Task \'#{name}\' already bound" if TASKS.key?(name)
828
+ TASKS[name] = help
829
+ end
830
+ end
831
+
832
+ # Check a task parameters. Raise a RuntimeError with explanation message if
833
+ # a mandatory parameter is missing or an unknown parameter was found.
834
+ # - params: task parameters as a Hash.
835
+ # - description: parameters description as a Hash associating a parameter
836
+ # name with symbol :mandatory or :optional.
837
+ def check_parameters(params, description)
838
+ raise "Parameters must be a Hash" unless params.kind_of?(Hash)
839
+ for param in description.keys
840
+ raise "Missing mandatory parameter \'#{param}\'" unless
841
+ params[param] or description[param] == :optional
842
+ end
843
+ for param in params.keys
844
+ raise "Unknown parameter \'#{param}\'" if not description.key?(param)
845
+ end
846
+ end
847
+
848
+ ############################ TASK DEFINITIONS #############################
849
+
850
+ # bind tasks defined hereafter
851
+ bind_tasks({
852
+ "mkdir" =>
853
+ "Create a directory and all parent directories if necessary. Do nothing if
854
+ directory already exists. Parameter is a String for directory to create."
855
+ })
856
+
857
+ # Make a directory and parent directories if necessary.
858
+ def self.mkdir(dir)
859
+ raise "Parameter must be a String" unless dir.kind_of?(String)
860
+ require "fileutils"
861
+ FileUtils.makedirs(dir)
862
+ end
863
+
864
+ ############################ UTILITY FUNCTIONS ############################
865
+
866
+ # Run a shell script and raise an exception if script returned a value
867
+ # different of 0.
868
+ # - script: script to run.
869
+ def sh(script)
870
+ system(script) or raise "Script \'#{script}\' exited with value \'#{$?}\'"
871
+ end
872
+
873
+ # Find files that match a given pattern.
874
+ # - pattern: pattern to select files.
875
+ # - base: base directory for search.
876
+ # - dir: tells if we must include directories.
877
+ # Return: a file list.
878
+ def find(pattern=//, base=\'.\', dir=true)
879
+ require \'find\'
880
+ files = []
881
+ Find.find(base) do |path|
882
+ if path =~ pattern and (File.file?(path) or (File.directory?(path) and dir))
883
+ files << path
884
+ end
885
+ end
886
+ return files
887
+ end
888
+ '
889
+
890
+ #########################################################################
891
+ # BUILD TEMPLATE #
892
+ #########################################################################
893
+
894
+ BUILD_TEMPLATE =
895
+ '# Template build file
896
+ - build: template
897
+ description: Template build file.
898
+ default: hello
899
+
900
+ # Build properties
901
+ - properties:
902
+ - user: "#{ENV[\'USER\']}"
903
+
904
+ # Build targets
905
+ - target: capitalize
906
+ description: Capitalize user name
907
+ script:
908
+ - rb: "who = user.capitalize"
909
+
910
+ - target: hello
911
+ depends: capitalize
912
+ description: Print greatings.
913
+ script:
914
+ - "echo \"Hello #{who}!\""
915
+ '
916
+ end