bee 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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