bee 0.1.0

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