bee 0.1.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,539 @@
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
+ module Console
19
+
20
+ require 'bee'
21
+ require 'getoptlong'
22
+
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
+ -k [task] Print help about all tasks or given one.
28
+ -t Write template build file on disk.
29
+ -v Enable verbose mode.
30
+ -s style Define style for output (see documentation).
31
+ -f file Build file to run (defaults to "build.yml").
32
+ targets Targets to run (default target if omitted).'
33
+ # Name for default build file.
34
+ DEFAULT_BUILD_FILE = 'build.yml'
35
+ # Exit value on error parsing command line
36
+ EXIT_PARSING_CMDLINE = 1
37
+ # Exit value on build error
38
+ EXIT_BUILD_ERROR = 2
39
+ # Exit value on unknown error
40
+ EXIT_UNKNOWN_ERROR = 3
41
+
42
+ # Parse command line and return parsed arguments.
43
+ def self.parse_command_line
44
+ help = false
45
+ help_build = false
46
+ help_task = false
47
+ task = nil
48
+ template = false
49
+ verbose = false
50
+ style = nil
51
+ file = DEFAULT_BUILD_FILE
52
+ targets = []
53
+ # read options in BEEOPT environment variable
54
+ options = ENV['BEEOPT']
55
+ options.split(' ').reverse.each { |option| ARGV.unshift(option) } if
56
+ options
57
+ # parse command line arguments
58
+ opts = GetoptLong.new(['--help', '-h', GetoptLong::NO_ARGUMENT],
59
+ ['--help-build', '-b', GetoptLong::NO_ARGUMENT],
60
+ ['--help-task','-k', GetoptLong::OPTIONAL_ARGUMENT],
61
+ ['--template', '-t', GetoptLong::NO_ARGUMENT ],
62
+ ['--verbose', '-v', GetoptLong::NO_ARGUMENT],
63
+ ['--style', '-s', GetoptLong::REQUIRED_ARGUMENT],
64
+ ['--file', '-f', GetoptLong::REQUIRED_ARGUMENT])
65
+ opts.each do |opt, arg|
66
+ case opt
67
+ when '--help'
68
+ help = true
69
+ when '--help-build'
70
+ help_build = true
71
+ when '--help-task'
72
+ help_task = true
73
+ task = arg
74
+ when '--template'
75
+ template = true
76
+ when '--verbose'
77
+ verbose = true
78
+ when '--style'
79
+ style = arg
80
+ when '--file'
81
+ file = arg
82
+ end
83
+ end
84
+ targets = ARGV
85
+ return [help, help_build, help_task, task, template, verbose, style,
86
+ file, targets]
87
+ end
88
+
89
+ # Start build from command line.
90
+ def self.start_command_line
91
+ STDOUT.sync = true
92
+ begin
93
+ help, help_build, help_task, task, template, verbose, style, file,
94
+ targets = parse_command_line
95
+ rescue
96
+ puts "ERROR: parsing command line (type 'bee -h' for help)"
97
+ exit(EXIT_PARSING_CMDLINE)
98
+ end
99
+ formatter = Formatter.new(style)
100
+ begin
101
+ if help
102
+ readme = File.join(File.dirname(__FILE__), '..', 'README')
103
+ if File.exists?(readme)
104
+ copyright = File.read(readme).strip!.split("\n")[-1]
105
+ puts copyright
106
+ end
107
+ puts HELP
108
+ elsif help_build
109
+ build = Build.new(file)
110
+ puts formatter.help_build(build)
111
+ elsif help_task
112
+ build = Build.new(file)
113
+ puts formatter.help_task(build, task)
114
+ elsif template
115
+ puts "Writing build template in file '#{file}'..."
116
+ raise BuildError.new("Build file '#{file}' already exists") if
117
+ File.exists?(file)
118
+ File.open(file, 'w') { |file| file.write(BUILD_TEMPLATE) }
119
+ puts formatter.format_success("OK")
120
+ else
121
+ listener = ConsoleListener.new(formatter, verbose)
122
+ build = Build.new(file)
123
+ build.run(targets, listener)
124
+ end
125
+ rescue BuildError
126
+ puts "#{formatter.format_error('ERROR')}: #{$!}"
127
+ exit(EXIT_BUILD_ERROR)
128
+ rescue SystemExit
129
+ # do nothing, exit in code
130
+ rescue Exception => e
131
+ puts "#{formatter.format_error('ERROR')}: #{$!}"
132
+ puts e.backtrace.join("\n")
133
+ exit(EXIT_UNKNOWN_ERROR)
134
+ end
135
+ end
136
+
137
+ #########################################################################
138
+ # FORMATTER CLASS #
139
+ #########################################################################
140
+
141
+ # Class to format build output on console.
142
+ class Formatter
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
+ # - build: running build.
286
+ def help_build(build)
287
+ help = ''
288
+ if build.name
289
+ help << "- Build: #{build.name.inspect}\n"
290
+ end
291
+ if build.description
292
+ help << format_description('Description', build.description, 2, false)
293
+ end
294
+ if build.properties.keys.length > 0
295
+ help << "- Properties:\n"
296
+ for property in build.properties.keys.sort
297
+ help << " - #{property}: #{build.properties[property].inspect}\n"
298
+ end
299
+ end
300
+ if build.targets.length > 0
301
+ help << "- Targets:\n"
302
+ for target in build.targets.values.sort { |a, b| a.name <=> b.name }
303
+ help << format_description(target.name, target.description, 2)
304
+ end
305
+ end
306
+ help << "- Default: #{build.default}"
307
+ return help.strip
308
+ end
309
+
310
+ # Return help about task(s).
311
+ # - build: running build.
312
+ # - task: task to print help about (all tasks if nil).
313
+ def help_task(build, task)
314
+ task = nil if task.length == 0
315
+ help = ''
316
+ if task
317
+ error "Task '#{task}' not loaded" if not build.tasks[task]
318
+ help << build.tasks[task]
319
+ else
320
+ for task in build.tasks.keys.sort
321
+ help << format_description(task, build.tasks[task], 0, true)
322
+ end
323
+ end
324
+ return help
325
+ end
326
+
327
+ private
328
+
329
+ # Apply style to a string:
330
+ # - string: the string to apply style to.
331
+ # - style: style to apply on string.
332
+ # - foreground: foreground color for string.
333
+ # - background: background color for string.
334
+ def style(string, style, foreground, background)
335
+ # check style, foreground and background colors
336
+ error "Unknown style '#{style}'" unless
337
+ STYLES.member?(style) or not style
338
+ error "Unknown color '#{foreground}'" unless
339
+ COLORS.member?(foreground) or not foreground
340
+ error "Unknown color '#{background}'" unless
341
+ COLORS.member?(background) or not background
342
+ # if no style nor colors, return raw string
343
+ return string if not foreground and not background and not style
344
+ # insert style and colors in string
345
+ colorized = "\e["
346
+ colorized << "#{STYLE_CODES[style]};" if style
347
+ colorized << "#{FOREGROUND_COLOR_CODES[foreground]};" if foreground
348
+ colorized << "#{BACKGROUND_COLOR_CODES[background]};" if background
349
+ colorized = colorized[0..-2]
350
+ colorized << "m#{string}\e[#{STYLE_CODES[:reset]}m"
351
+ return colorized
352
+ end
353
+
354
+ # Get line length calling IOCTL. Return nil if call failed.
355
+ def line_length
356
+ begin
357
+ tiocgwinsz = 0x5413
358
+ string = [0, 0, 0, 0].pack('SSSS')
359
+ if $stdin.ioctl(tiocgwinsz, string) >= 0 then
360
+ rows, cols, xpixels, ypixels = string.unpack('SSSS')
361
+ return cols
362
+ end
363
+ rescue
364
+ return nil
365
+ end
366
+ end
367
+
368
+ # Parse style from command line. If error occurs parsing style, return
369
+ # nil (which means default style).
370
+ # - string: style to parse.
371
+ def parse_style_from_command_line(string)
372
+ return if not string
373
+ style = {}
374
+ begin
375
+ for pair in string.split(',')
376
+ key, value = pair.split(':')
377
+ key = SHORT_STYLE_KEYS[key] || key
378
+ key = key.to_sym
379
+ if key == :line_length
380
+ value = value.to_i
381
+ elsif key == :line_character
382
+ value = ' ' if not value or value.length == 0
383
+ else
384
+ value = value.to_sym
385
+ error "Unkown color or style '#{value}'" if
386
+ not COLORS.member?(value) and not STYLES.member?(value)
387
+ end
388
+ style[key] = value
389
+ end
390
+ return style
391
+ rescue
392
+ # if parsing fails, return default style (nil)
393
+ return nil
394
+ end
395
+ end
396
+
397
+ # Format a description.
398
+ # - title: description title (project, property or target name).
399
+ # - text: description text.
400
+ # - indent: indentation width.
401
+ # - bullet: tells if we must put a bullet.
402
+ def format_description(title, text=nil, indent=0, bullet=true)
403
+ string = ' '*indent
404
+ string << '- ' if bullet
405
+ string << title
406
+ if text
407
+ string << ": "
408
+ if text.split("\n").length > 1
409
+ string << "\n"
410
+ text.split("\n").each do |line|
411
+ string << ' '*(indent+2) + line.strip + "\n"
412
+ end
413
+ else
414
+ string << text.strip + "\n"
415
+ end
416
+ end
417
+ return string
418
+ end
419
+
420
+ end
421
+
422
+ #########################################################################
423
+ # CONSOLE LISTENER #
424
+ #########################################################################
425
+
426
+ # Listener when running in a console. Prints messages on the console using
427
+ # a given formatter.
428
+ class ConsoleListener
429
+
430
+ # Formatter used by listener.
431
+ attr_reader :formatter
432
+ # Verbosity flag.
433
+ attr_reader :verbose
434
+ # Build start time.
435
+ attr_reader :start_time
436
+ # Build end time.
437
+ attr_reader :end_time
438
+ # Build duration.
439
+ attr_reader :duration
440
+ # Build success.
441
+ attr_reader :success
442
+ # Last target met.
443
+ attr_reader :last_target
444
+ # Last task met.
445
+ attr_reader :last_task
446
+
447
+ # Constructor.
448
+ # - formatter: the formatter to use to output on console.
449
+ # - verbose: tells if we run in verbose mode.
450
+ def initialize(formatter, verbose)
451
+ @formatter = formatter
452
+ @verbose = verbose
453
+ end
454
+
455
+ # Called when build is started.
456
+ # - build: the build object.
457
+ def build_started(build)
458
+ @start_time = Time.now
459
+ @end_time = nil
460
+ @duration = nil
461
+ @success = nil
462
+ @last_target = nil
463
+ @last_task = nil
464
+ puts "Starting build '#{build.file}'..." if @verbose
465
+ end
466
+
467
+ # Called when build is finished.
468
+ # - build: the build object.
469
+ def build_finished(build)
470
+ @end_time = Time.now
471
+ @duration = @end_time - @start_time
472
+ @success = true
473
+ puts "Built in #{@duration} s" if @verbose
474
+ puts @formatter.format_success('OK')
475
+ end
476
+
477
+ # Called when a target is met.
478
+ # - target: the target object.
479
+ def target(target)
480
+ @last_target = target
481
+ @last_task = nil
482
+ puts @formatter.format_target(target)
483
+ end
484
+
485
+ # Called when a task is met.
486
+ # - task: task source (shell, Ruby or task).
487
+ def task(task)
488
+ @last_task = task
489
+ puts @formatter.format_task(task) if @verbose
490
+ end
491
+
492
+ # Called when an error was raised.
493
+ # - exception: raised exception.
494
+ def error(exception)
495
+ @end_time = Time.now
496
+ @duration = @end_time - @start_time
497
+ @success = false
498
+ puts "Built in #{@duration} s" if @verbose
499
+ message = ''
500
+ message << "In target '#{@last_target.name}'" if @last_target
501
+ message << ", in task:\n#{@formatter.format_task(@last_task)}\n" if
502
+ @last_task
503
+ message << ': ' if @last_target and not @last_task
504
+ message << exception.to_s
505
+ puts "#{@formatter.format_error('ERROR')}: #{message}"
506
+ end
507
+
508
+ end
509
+
510
+ #########################################################################
511
+ # BUILD TEMPLATE #
512
+ #########################################################################
513
+
514
+ BUILD_TEMPLATE =
515
+ '# Template build file
516
+ - build: template
517
+ description: Template build file.
518
+ default: hello
519
+
520
+ # Build properties
521
+ - properties:
522
+ - user: "#{ENV[\'USER\']}"
523
+
524
+ # Build targets
525
+ - target: capitalize
526
+ description: Capitalize user name
527
+ script:
528
+ - rb: "who = user.capitalize"
529
+
530
+ - target: hello
531
+ depends: capitalize
532
+ description: Print greatings.
533
+ script:
534
+ - "echo \"Hello #{who}!\""
535
+ '
536
+
537
+ end
538
+
539
+ end