rant 0.3.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.
Files changed (64) hide show
  1. data/COPYING +504 -0
  2. data/README +203 -0
  3. data/Rantfile +104 -0
  4. data/TODO +19 -0
  5. data/bin/rant +12 -0
  6. data/bin/rant-import +12 -0
  7. data/devel-notes +50 -0
  8. data/doc/configure.rdoc +40 -0
  9. data/doc/csharp.rdoc +74 -0
  10. data/doc/rant-import.rdoc +32 -0
  11. data/doc/rant.rdoc +24 -0
  12. data/doc/rantfile.rdoc +227 -0
  13. data/doc/rubyproject.rdoc +210 -0
  14. data/lib/rant.rb +9 -0
  15. data/lib/rant/cs_compiler.rb +334 -0
  16. data/lib/rant/import.rb +291 -0
  17. data/lib/rant/import/rubydoc.rb +125 -0
  18. data/lib/rant/import/rubypackage.rb +417 -0
  19. data/lib/rant/import/rubytest.rb +97 -0
  20. data/lib/rant/plugin/README +50 -0
  21. data/lib/rant/plugin/configure.rb +345 -0
  22. data/lib/rant/plugin/csharp.rb +275 -0
  23. data/lib/rant/plugin_methods.rb +41 -0
  24. data/lib/rant/rantenv.rb +217 -0
  25. data/lib/rant/rantfile.rb +664 -0
  26. data/lib/rant/rantlib.rb +1118 -0
  27. data/lib/rant/rantsys.rb +258 -0
  28. data/lib/rant/rantvar.rb +82 -0
  29. data/rantmethods.rb +79 -0
  30. data/run_import +7 -0
  31. data/run_rant +7 -0
  32. data/setup.rb +1360 -0
  33. data/test/Rantfile +2 -0
  34. data/test/plugin/configure/Rantfile +47 -0
  35. data/test/plugin/configure/test_configure.rb +58 -0
  36. data/test/plugin/csharp/Hello.cs +10 -0
  37. data/test/plugin/csharp/Rantfile +30 -0
  38. data/test/plugin/csharp/src/A.cs +8 -0
  39. data/test/plugin/csharp/src/B.cs +8 -0
  40. data/test/plugin/csharp/test_csharp.rb +99 -0
  41. data/test/project1/Rantfile +127 -0
  42. data/test/project1/test_project.rb +203 -0
  43. data/test/project2/buildfile +14 -0
  44. data/test/project2/rantfile.rb +20 -0
  45. data/test/project2/sub1/Rantfile +12 -0
  46. data/test/project2/test_project.rb +87 -0
  47. data/test/project_rb1/README +14 -0
  48. data/test/project_rb1/bin/wgrep +5 -0
  49. data/test/project_rb1/lib/wgrep.rb +56 -0
  50. data/test/project_rb1/rantfile.rb +30 -0
  51. data/test/project_rb1/test/tc_wgrep.rb +21 -0
  52. data/test/project_rb1/test/text +3 -0
  53. data/test/project_rb1/test_project_rb1.rb +153 -0
  54. data/test/test_env.rb +47 -0
  55. data/test/test_filetask.rb +57 -0
  56. data/test/test_lighttask.rb +49 -0
  57. data/test/test_metatask.rb +29 -0
  58. data/test/test_rant_interface.rb +65 -0
  59. data/test/test_sys.rb +61 -0
  60. data/test/test_task.rb +115 -0
  61. data/test/toplevel.rf +11 -0
  62. data/test/ts_all.rb +4 -0
  63. data/test/tutil.rb +95 -0
  64. metadata +133 -0
@@ -0,0 +1,1118 @@
1
+
2
+ # rantlib.rb - The core of Rant.
3
+ #
4
+ # Copyright (C) 2005 Stefan Lang <langstefan@gmx.at>
5
+ #
6
+ # This program is free software.
7
+ # You can distribute/modify this program under the terms of
8
+ # the GNU LGPL, Lesser General Public License version 2.1.
9
+
10
+ require 'getoptlong'
11
+ require 'rant/rantvar'
12
+ require 'rant/rantenv'
13
+ require 'rant/rantfile'
14
+ require 'rant/rantsys'
15
+
16
+ module Rant
17
+ VERSION = '0.3.0'
18
+
19
+ # Those are the filenames for rantfiles.
20
+ # Case matters!
21
+ RANTFILES = [ "Rantfile",
22
+ "rantfile",
23
+ "Rantfile.rb",
24
+ "rantfile.rb",
25
+ ]
26
+
27
+ # Names of plugins and imports for which code was loaded.
28
+ # Files that where loaded with the `import' commant are directly
29
+ # added; files loaded with the `plugin' command are prefixed with
30
+ # "plugin/".
31
+ CODE_IMPORTS = []
32
+
33
+ class RantAbortException < StandardError
34
+ end
35
+
36
+ class RantDoneException < StandardError
37
+ end
38
+
39
+ class RantfileException < StandardError
40
+ end
41
+
42
+ # This module is a namespace for generator classes.
43
+ module Generators
44
+ end
45
+
46
+ end
47
+
48
+ # There is one problem with executing Rantfiles in a special context:
49
+ # In the top-level execution environment, there are some methods
50
+ # available which are not available to all objects. One example is the
51
+ # +include+ method.
52
+ #
53
+ # To (at least partially) solve this problem, we capture the `main'
54
+ # object here and delegate methods from RantContext#method_missing to
55
+ # this object.
56
+ Rant::MAIN_OBJECT = self
57
+
58
+ class Array
59
+ def arglist
60
+ self.shell_pathes.join(' ')
61
+ end
62
+
63
+ def shell_pathes
64
+ if ::Rant::Env.on_windows?
65
+ self.collect { |entry|
66
+ entry = entry.tr("/", "\\")
67
+ if entry.include? ' '
68
+ '"' + entry + '"'
69
+ else
70
+ entry
71
+ end
72
+ }
73
+ else
74
+ self.collect { |entry|
75
+ if entry.include? ' '
76
+ "'" + entry + "'"
77
+ else
78
+ entry
79
+ end
80
+ }
81
+ end
82
+ end
83
+ end
84
+
85
+ module Rant::Lib
86
+
87
+ # Parses one string (elem) as it occurs in the array
88
+ # which is returned by caller.
89
+ # E.g.:
90
+ # p parse_caller_elem "/usr/local/lib/ruby/1.8/irb/workspace.rb:52:in `irb_binding'"
91
+ # prints:
92
+ # {:method=>"irb_binding", :ln=>52, :file=>"/usr/local/lib/ruby/1.8/irb/workspace.rb"}
93
+ def parse_caller_elem elem
94
+ parts = elem.split(":")
95
+ rh = { :file => parts[0],
96
+ :ln => parts[1].to_i
97
+ }
98
+ =begin
99
+ # commented for better performance
100
+ meth = parts[2]
101
+ if meth && meth =~ /\`(\w+)'/
102
+ meth = $1
103
+ end
104
+ rh[:method] = meth
105
+ =end
106
+ rh
107
+ end
108
+
109
+ module_function :parse_caller_elem
110
+
111
+ # currently unused
112
+ class Caller
113
+ def self.[](i)
114
+ new(caller[i+1])
115
+ end
116
+ def initialize(clr)
117
+ @clr = clr
118
+ @file = @ln = nil
119
+ end
120
+ def file
121
+ unless @file
122
+ ca = Lib.parse_caller_elem(clr)
123
+ @file = ca[:file]
124
+ @ln = ca[:ln]
125
+ end
126
+ @file
127
+ end
128
+ def ln
129
+ unless @ln
130
+ ca = Lib.parse_caller_elem(clr)
131
+ @file = ca[:file]
132
+ @ln = ca[:ln]
133
+ end
134
+ @ln
135
+ end
136
+ end
137
+ end
138
+
139
+ # The methods in this module are the public interface to Rant that can
140
+ # be used in Rantfiles.
141
+ module RantContext
142
+ include Rant::Generators
143
+
144
+ # Define a basic task.
145
+ def task targ, &block
146
+ rantapp.task(targ, &block)
147
+ end
148
+
149
+ # Define a file task.
150
+ def file targ, &block
151
+ rantapp.file(targ, &block)
152
+ end
153
+
154
+ # Add code and/or prerequisites to existing task.
155
+ def enhance targ, &block
156
+ rantapp.enhance(targ, &block)
157
+ end
158
+
159
+ def desc(*args)
160
+ rantapp.desc(*args)
161
+ end
162
+
163
+ def gen(*args, &block)
164
+ rantapp.gen(*args, &block)
165
+ end
166
+
167
+ def import(*args, &block)
168
+ rantapp.import(*args, &block)
169
+ end
170
+
171
+ def plugin(*args, &block)
172
+ rantapp.plugin(*args, &block)
173
+ end
174
+
175
+ # Look in the subdirectories, given by args,
176
+ # for rantfiles.
177
+ def subdirs *args
178
+ rantapp.subdirs(*args)
179
+ end
180
+
181
+ def source rantfile
182
+ rantapp.source(rantfile)
183
+ end
184
+
185
+ def sys *args
186
+ rantapp.sys(*args)
187
+ end
188
+ end # module RantContext
189
+
190
+ class RantAppContext
191
+ include Rant
192
+ include RantContext
193
+
194
+ def initialize(app)
195
+ @rantapp = app
196
+ end
197
+
198
+ def rantapp
199
+ @rantapp
200
+ end
201
+
202
+ def method_missing(sym, *args)
203
+ # See the documentation for Rant::MAIN_OBJECT why we're doing
204
+ # this...
205
+ # Note also that the +send+ method also invokes private
206
+ # methods, this is very important for our intent.
207
+ Rant::MAIN_OBJECT.send(sym, *args)
208
+ rescue NoMethodError
209
+ raise NameError, "NameError: undefined local " +
210
+ "variable or method `#{sym}' for main:Object", caller
211
+ end
212
+ end
213
+
214
+ module Rant
215
+ include RantContext
216
+
217
+ # In the class definition of Rant::RantApp, this will be set to a
218
+ # new application object.
219
+ @@rantapp = nil
220
+
221
+ class << self
222
+
223
+ # Run a new rant application in the current working directory.
224
+ # This has the same effect as running +rant+ from the
225
+ # commandline. You can give arguments as you would give them
226
+ # on the commandline. If no argument is given, ARGV will be
227
+ # used.
228
+ #
229
+ # This method returns 0 if the rant application was
230
+ # successfull and 1 on failure. So if you need your own rant
231
+ # startscript, it could look like:
232
+ #
233
+ # exit Rant.run
234
+ #
235
+ # This runs rant in the current directory, using the arguments
236
+ # given to your script and the exit code as suggested by the
237
+ # rant application.
238
+ #
239
+ # Or if you want rant to always be quiet with this script,
240
+ # use:
241
+ #
242
+ # exit Rant.run("--quiet", ARGV)
243
+ #
244
+ # Of course, you can invoke rant directly at the bottom of
245
+ # your rantfile, so you can run it directly with ruby.
246
+ def run(first_arg=nil, *other_args)
247
+ other_args = other_args.flatten
248
+ args = first_arg.nil? ? ARGV.dup : ([first_arg] + other_args)
249
+ if @@rantapp && !@@rantapp.ran?
250
+ @@rantapp.args.replace(args.flatten)
251
+ @@rantapp.run
252
+ else
253
+ @@rantapp = Rant::RantApp.new(args)
254
+ @@rantapp.run
255
+ end
256
+ end
257
+
258
+ def rantapp
259
+ @@rantapp
260
+ end
261
+
262
+ def rantapp=(app)
263
+ @@rantapp = app
264
+ end
265
+
266
+ # "Clear" the current Rant application. After this call,
267
+ # Rant has the same state as immediately after startup.
268
+ def reset
269
+ @@rantapp = nil
270
+ end
271
+ end
272
+
273
+ def rantapp
274
+ @@rantapp
275
+ end
276
+
277
+ # Pre 0.2.7: Manually making necessary methods module
278
+ # functions. Note that it caused problems with caller
279
+ # parsing when the Rantfile did a `require "rant"' (irb!).
280
+ #module_function :task, :file, :desc, :subdirs,
281
+ # :gen, :source, :enhance, :sys, :plugin
282
+
283
+ extend self
284
+
285
+ end # module Rant
286
+
287
+ class Rant::RantApp
288
+ include Rant::Console
289
+
290
+ # Important: We try to synchronize all tasks referenced indirectly
291
+ # by @rantfiles with the task hash @tasks. The task hash is
292
+ # intended for fast task lookup per task name.
293
+
294
+ # The RantApp class has no own state.
295
+
296
+ OPTIONS = [
297
+ [ "--help", "-h", GetoptLong::NO_ARGUMENT,
298
+ "Print this help and exit." ],
299
+ [ "--version", "-V", GetoptLong::NO_ARGUMENT,
300
+ "Print version of Rant and exit." ],
301
+ [ "--verbose", "-v", GetoptLong::NO_ARGUMENT,
302
+ "Print more messages to stderr." ],
303
+ [ "--quiet", "-q", GetoptLong::NO_ARGUMENT,
304
+ "Don't print commands." ],
305
+ [ "--err-commands", GetoptLong::NO_ARGUMENT,
306
+ "Print failed commands and their exit status." ],
307
+ [ "--directory","-C", GetoptLong::REQUIRED_ARGUMENT,
308
+ "Run rant in DIRECTORY." ],
309
+ [ "--rantfile", "-f", GetoptLong::REQUIRED_ARGUMENT,
310
+ "Process RANTFILE instead of standard rantfiles.\n" +
311
+ "Multiple files may be specified with this option" ],
312
+ [ "--force-run","-a", GetoptLong::REQUIRED_ARGUMENT,
313
+ "Force TARGET to be run, even if it isn't required.\n"],
314
+ [ "--tasks", "-T", GetoptLong::NO_ARGUMENT,
315
+ "Show a list of all described tasks and exit." ],
316
+
317
+ # "private" options intended for debugging, testing and
318
+ # internal use. A private option is distuingished from others
319
+ # by having +nil+ as description!
320
+ [ "--stop-after-load", GetoptLong::NO_ARGUMENT, nil ],
321
+ ]
322
+
323
+ # Arguments, usually those given on commandline.
324
+ attr_reader :args
325
+ # A list of all Rantfiles used by this app.
326
+ attr_reader :rantfiles
327
+ # A list of target names to be forced (run even
328
+ # if not required). Each of these targets will be removed
329
+ # from this list after the first run.
330
+ #
331
+ # Forced targets will be run before other targets.
332
+ attr_reader :force_targets
333
+ # A list of all registered plugins.
334
+ attr_reader :plugins
335
+ # The context in which Rantfiles are loaded. RantContext methods
336
+ # may be called through an instance_eval on this object (e.g. from
337
+ # plugins).
338
+ attr_reader :context
339
+ # The [] and []= operators may be used to set/get values from this
340
+ # object (like a hash). It is intended to let the different
341
+ # modules, plugins and tasks to communicate to each other.
342
+ attr_reader :var
343
+ # A hash with all tasks. For fast task lookup use this hash with
344
+ # the taskname as key.
345
+ attr_reader :tasks
346
+ # A list with of all imports (code loaded with +import+).
347
+ attr_reader :imports
348
+
349
+ def initialize *args
350
+ @args = args.flatten
351
+ # Rantfiles will be loaded in the context of this object.
352
+ @context = RantAppContext.new(self)
353
+ @sys = ::Rant::SysObject.new(self)
354
+ Rant.rantapp ||= self
355
+ @rantfiles = []
356
+ @tasks = {}
357
+ @opts = {
358
+ :verbose => 0,
359
+ :quiet => false,
360
+ }
361
+ @arg_rantfiles = [] # rantfiles given in args
362
+ @arg_targets = [] # targets given in args
363
+ @force_targets = []
364
+ @ran = false
365
+ @done = false
366
+ @plugins = []
367
+ @var = Rant::RantVar::Space.new
368
+ @imports = []
369
+
370
+ @task_show = nil
371
+ @task_desc = nil
372
+
373
+ @orig_pwd = nil
374
+
375
+ end
376
+
377
+ # Just ensure that Rant.rantapp holds an RantApp after loading
378
+ # this file. The code in initialize will register the new app with
379
+ # Rant.rantapp= if necessary.
380
+ self.new
381
+
382
+ def [](opt)
383
+ @opts[opt]
384
+ end
385
+
386
+ def []=(opt, val)
387
+ case opt
388
+ when :directory
389
+ self.rootdir = val
390
+ else
391
+ @opts[opt] = val
392
+ end
393
+ end
394
+
395
+ def rootdir
396
+ @opts[:directory].dup
397
+ end
398
+
399
+ def rootdir=(newdir)
400
+ if @ran
401
+ raise "rootdir of rant application can't " +
402
+ "be changed after calling `run'"
403
+ end
404
+ @opts[:directory] = newdir.dup
405
+ rootdir # return a dup of the new rootdir
406
+ end
407
+
408
+ def ran?
409
+ @ran
410
+ end
411
+
412
+ def done?
413
+ @done
414
+ end
415
+
416
+ # Returns 0 on success and 1 on failure.
417
+ def run
418
+ @ran = true
419
+ # remind pwd
420
+ @orig_pwd = Dir.pwd
421
+ # Process commandline.
422
+ process_args
423
+ # Set pwd.
424
+ opts_dir = @opts[:directory]
425
+ if opts_dir
426
+ unless test(?d, opts_dir)
427
+ abort("No such directory - #{opts_dir}")
428
+ end
429
+ opts_dir != @orig_pwd && Dir.chdir(opts_dir)
430
+ else
431
+ @opts[:directory] = @orig_pwd
432
+ end
433
+ # read rantfiles
434
+ load_rantfiles
435
+
436
+ raise Rant::RantDoneException if @opts[:stop_after_load]
437
+
438
+ # Notify plugins before running tasks
439
+ @plugins.each { |plugin| plugin.rant_start }
440
+ if @opts[:targets]
441
+ show_descriptions
442
+ raise Rant::RantDoneException
443
+ end
444
+ # run tasks
445
+ run_tasks
446
+ raise Rant::RantDoneException
447
+ rescue Rant::RantDoneException
448
+ @done = true
449
+ # Notify plugins
450
+ @plugins.each { |plugin| plugin.rant_done }
451
+ return 0
452
+ rescue Rant::RantfileException
453
+ err_msg "Invalid Rantfile: " + $!.message
454
+ $stderr.puts "rant aborted!"
455
+ return 1
456
+ rescue Rant::RantAbortException
457
+ $stderr.puts "rant aborted!"
458
+ return 1
459
+ rescue
460
+ err_msg $!.message, $!.backtrace
461
+ $stderr.puts "rant aborted!"
462
+ return 1
463
+ ensure
464
+ # TODO: exception handling!
465
+ @plugins.each { |plugin| plugin.rant_plugin_stop }
466
+ @plugins.each { |plugin| plugin.rant_quit }
467
+ # restore pwd
468
+ Dir.pwd != @orig_pwd && Dir.chdir(@orig_pwd)
469
+ Rant.rantapp = self.class.new
470
+ end
471
+
472
+ ###### methods accessible through RantContext ####################
473
+ def show *args
474
+ @task_show = *args.join("\n")
475
+ end
476
+
477
+ def desc *args
478
+ if args.empty? || (args.size == 1 && args.first.nil?)
479
+ @task_desc = nil
480
+ else
481
+ @task_desc = args.join("\n")
482
+ end
483
+ end
484
+
485
+ def task targ, &block
486
+ prepare_task(targ, block) { |name,pre,blk|
487
+ Rant::Task.new(self, name, pre, &blk)
488
+ }
489
+ end
490
+
491
+ def file targ, &block
492
+ prepare_task(targ, block) { |name,pre,blk|
493
+ Rant::FileTask.new(self, name, pre, &blk)
494
+ }
495
+ end
496
+
497
+ def gen(*args, &block)
498
+ # retrieve caller info
499
+ clr = caller[1]
500
+ ch = Rant::Lib::parse_caller_elem(clr)
501
+ name = nil
502
+ pre = []
503
+ ln = ch[:ln] || 0
504
+ file = ch[:file]
505
+ # validate args
506
+ generator = args.shift
507
+ # Let modules/classes from the Generator namespace override
508
+ # other generators.
509
+ begin
510
+ if generator.is_a? Module
511
+ generator = ::Rant::Generators.const_get(generator.to_s)
512
+ end
513
+ rescue NameError, ArgumentError
514
+ end
515
+ unless generator.respond_to? :rant_generate
516
+ abort(pos_text(file, ln),
517
+ "First argument to `gen' has to be a task-generator.")
518
+ end
519
+ # ask generator to produce a task for this application
520
+ generator.rant_generate(self, ch, args, &block)
521
+ end
522
+
523
+ # Currently ignores block.
524
+ def import(*args, &block)
525
+ if block
526
+ warn_msg "import: currently ignoring block"
527
+ end
528
+ args.flatten.each { |arg|
529
+ unless String === arg
530
+ abort("import: currently " +
531
+ "only strings are allowed as arguments")
532
+ end
533
+ unless @imports.include? arg
534
+ unless Rant::CODE_IMPORTS.include? arg
535
+ begin
536
+ require "rant/import/#{arg}"
537
+ rescue LoadError => e
538
+ abort("No such import - #{arg}")
539
+ end
540
+ Rant::CODE_IMPORTS << arg.dup
541
+ end
542
+ @imports << arg.dup
543
+ end
544
+ }
545
+ end
546
+
547
+ def plugin(*args, &block)
548
+ # retrieve caller info
549
+ clr = caller[1]
550
+ ch = Rant::Lib::parse_caller_elem(clr)
551
+ name = nil
552
+ pre = []
553
+ ln = ch[:ln] || 0
554
+ file = ch[:file]
555
+
556
+ pl_name = args.shift
557
+ pl_name = pl_name.to_str if pl_name.respond_to? :to_str
558
+ pl_name = pl_name.to_s if pl_name.is_a? Symbol
559
+ unless pl_name.is_a? String
560
+ abort(pos_text(file, ln),
561
+ "Plugin name has to be a string or symbol.")
562
+ end
563
+ lc_pl_name = pl_name.downcase
564
+ import_name = "plugin/#{lc_pl_name}"
565
+ unless Rant::CODE_IMPORTS.include? import_name
566
+ begin
567
+ require "rant/plugin/#{lc_pl_name}"
568
+ Rant::CODE_IMPORTS << import_name
569
+ rescue LoadError
570
+ abort(pos_text(file, ln),
571
+ "no such plugin library - `#{lc_pl_name}'")
572
+ end
573
+ end
574
+ pl_class = nil
575
+ begin
576
+ pl_class = ::Rant::Plugin.const_get(pl_name)
577
+ rescue NameError, ArgumentError
578
+ abort(pos_text(file, ln),
579
+ "`#{pl_name}': no such plugin")
580
+ end
581
+
582
+ plugin = pl_class.rant_plugin_new(self, ch, *args, &block)
583
+ # TODO: check for rant_plugin?
584
+ @plugins << plugin
585
+ msg 2, "Plugin `#{plugin.rant_plugin_name}' registered."
586
+ plugin.rant_plugin_init
587
+ # return plugin instance
588
+ plugin
589
+ end
590
+
591
+ # Add block and prerequisites to the task specified by the
592
+ # name given as only key in targ.
593
+ # If there is no task with the given name, generate a warning
594
+ # and a new file task.
595
+ def enhance targ, &block
596
+ prepare_task(targ, block) { |name,pre,blk|
597
+ t = select_task { |t| t.name == name }
598
+ if t
599
+ t.enhance(pre, &blk)
600
+ return t
601
+ end
602
+ warn_msg "enhance \"#{name}\": no such task",
603
+ "Generating a new file task with the given name."
604
+ Rant::FileTask.new(self, name, pre, &blk)
605
+ }
606
+ end
607
+
608
+ def source rantfile
609
+ rf, is_new = rantfile_for_path(rantfile)
610
+ return false unless is_new
611
+ unless rf.exist?
612
+ abort("source: No such file to load - #{rantfile}")
613
+ end
614
+ load_file rf
615
+ true
616
+ end
617
+
618
+ # Search the given directories for Rantfiles.
619
+ def subdirs *args
620
+ args.flatten!
621
+ cinf = Rant::Lib::parse_caller_elem(caller[1])
622
+ ln = cinf[:ln] || 0
623
+ file = cinf[:file]
624
+ args.each { |arg|
625
+ if arg.is_a? Symbol
626
+ arg = arg.to_s
627
+ elsif arg.respond_to? :to_str
628
+ arg = arg.to_str
629
+ end
630
+ unless arg.is_a? String
631
+ abort(pos_text(file, ln),
632
+ "in `subdirs' command: arguments must be strings")
633
+ end
634
+ loaded = false
635
+ rantfiles_in_dir(arg).each { |f|
636
+ loaded = true
637
+ rf, is_new = rantfile_for_path(f)
638
+ if is_new
639
+ load_file rf
640
+ end
641
+ }
642
+ unless loaded || quiet?
643
+ warn_msg(pos_text(file, ln) + "; in `subdirs' command:",
644
+ "No Rantfile in subdir `#{arg}'.")
645
+ end
646
+ }
647
+ rescue SystemCallError => e
648
+ abort(pos_text(file, ln),
649
+ "in `subdirs' command: " + e.message)
650
+ end
651
+
652
+ def sys *args
653
+ if args.empty?
654
+ @sys
655
+ else
656
+ @sys.sh(*args)
657
+ end
658
+ end
659
+ ##################################################################
660
+
661
+ # Pop (remove and return) current pending task description.
662
+ def pop_desc
663
+ td = @task_desc
664
+ @task_desc = nil
665
+ td
666
+ end
667
+
668
+ # Prints msg as error message and throws a RantAbortException.
669
+ def abort *msg
670
+ err_msg(msg) unless msg.empty?
671
+ raise Rant::RantAbortException
672
+ end
673
+
674
+ def help
675
+ puts "rant [-f RANTFILE] [OPTIONS] tasks..."
676
+ puts
677
+ puts "Options are:"
678
+ print option_listing(OPTIONS)
679
+ raise Rant::RantDoneException
680
+ end
681
+
682
+ def show_descriptions
683
+ tlist = select_tasks { |t| t.description }
684
+ if tlist.empty?
685
+ msg "No described targets."
686
+ return
687
+ end
688
+ prefix = "rant "
689
+ infix = " # "
690
+ name_length = 0
691
+ tlist.each { |t|
692
+ if t.name.length > name_length
693
+ name_length = t.name.length
694
+ end
695
+ }
696
+ name_length < 7 && name_length = 7
697
+ cmd_length = prefix.length + name_length
698
+ tlist.each { |t|
699
+ print(prefix + t.name.ljust(name_length) + infix)
700
+ dt = t.description.sub(/\s+$/, "")
701
+ puts dt.sub("\n", "\n" + ' ' * cmd_length + infix + " ")
702
+ }
703
+ true
704
+ end
705
+
706
+ # Increase verbosity.
707
+ def more_verbose
708
+ @opts[:verbose] += 1
709
+ @opts[:quiet] = false
710
+ end
711
+
712
+ def verbose
713
+ @opts[:verbose]
714
+ end
715
+
716
+ def quiet?
717
+ @opts[:quiet]
718
+ end
719
+
720
+ def pos_text file, ln
721
+ t = "in file `#{file}'"
722
+ if ln && ln > 0
723
+ t << ", line #{ln}"
724
+ end
725
+ t + ": "
726
+ end
727
+
728
+ def msg *args
729
+ verbose_level = args[0]
730
+ if verbose_level.is_a? Integer
731
+ super(args[1..-1]) if verbose_level <= verbose
732
+ else
733
+ super
734
+ end
735
+ end
736
+
737
+ # Print a command message as would be done from a call to a
738
+ # Sys method.
739
+ def cmd_msg cmd
740
+ $stdout.puts cmd unless quiet?
741
+ end
742
+
743
+ ###### public methods regarding plugins ##########################
744
+ # The preferred way for a plugin to report a warning.
745
+ def plugin_warn(*args)
746
+ warn_msg(*args)
747
+ end
748
+ # The preferred way for a plugin to report an error.
749
+ def plugin_err(*args)
750
+ err_msg(*args)
751
+ end
752
+
753
+ # Get the plugin with the given name or nil. Yields the plugin
754
+ # object if block given.
755
+ def plugin_named(name)
756
+ @plugins.each { |plugin|
757
+ if plugin.rant_plugin_name == name
758
+ yield plugin if block_given?
759
+ return plugin
760
+ end
761
+ }
762
+ nil
763
+ end
764
+ ##################################################################
765
+
766
+ # All targets given on commandline, including those given
767
+ # with the -a option. The list will be in processing order.
768
+ def cmd_targets
769
+ @force_targets + @arg_targets
770
+ end
771
+
772
+ private
773
+ def have_any_task?
774
+ not @rantfiles.all? { |f| f.tasks.empty? }
775
+ end
776
+
777
+ def run_tasks
778
+ unless have_any_task?
779
+ abort("No tasks defined for this rant application!")
780
+ end
781
+
782
+ # Target selection strategy:
783
+ # Run tasks specified on commandline, if not given:
784
+ # run default task, if not given:
785
+ # run first defined task.
786
+ target_list = @force_targets + @arg_targets
787
+ # The target list is a list of strings, not Task objects!
788
+ if target_list.empty?
789
+ have_default = @rantfiles.any? { |f|
790
+ f.tasks.any? { |t| t.name == "default" }
791
+ }
792
+ if have_default
793
+ target_list << "default"
794
+ else
795
+ first = nil
796
+ @rantfiles.each { |f|
797
+ unless f.tasks.empty?
798
+ first = f.tasks.first.name
799
+ break
800
+ end
801
+ }
802
+ target_list << first
803
+ end
804
+ end
805
+ # Now, run all specified tasks in all rantfiles,
806
+ # rantfiles in reverse order.
807
+ opt = {}
808
+ matching_tasks = 0
809
+ target_list.each do |target|
810
+ matching_tasks = 0
811
+ if @force_targets.include?(target)
812
+ opt[:force] = true
813
+ @force_targets.delete(target)
814
+ end
815
+ (select_tasks { |t| t.name == target }).each { |t|
816
+ matching_tasks += 1
817
+ begin
818
+ t.invoke(opt)
819
+ rescue Rant::TaskFail => e
820
+ # TODO: Report failed dependancy.
821
+ abort("Task `#{e.tname}' fail.")
822
+ end
823
+ }
824
+ if matching_tasks == 0
825
+ abort("Don't know how to build `#{target}'.")
826
+ end
827
+ end
828
+ end
829
+
830
+ # Returns a list with all tasks for which yield
831
+ # returns true.
832
+ def select_tasks
833
+ selection = []
834
+ ### pre 0.2.10 ##################
835
+ # @rantfile.reverse.each { |rf|
836
+ #################################
837
+ @rantfiles.each { |rf|
838
+ rf.tasks.each { |t|
839
+ selection << t if yield t
840
+ }
841
+ }
842
+ selection
843
+ end
844
+ public :select_tasks
845
+
846
+ # Returns an array (might be a MetaTask) with all tasks that have
847
+ # the given name.
848
+ def select_tasks_by_name name
849
+ s = @tasks[name]
850
+ case s
851
+ when nil: []
852
+ when Rant::Worker: [s]
853
+ else # assuming MetaTask
854
+ s
855
+ end
856
+ end
857
+ public :select_tasks_by_name
858
+
859
+ # Get the first task for which yield returns true. Returns nil if
860
+ # yield never returned true.
861
+ def select_task
862
+ @rantfiles.reverse.each { |rf|
863
+ rf.tasks.each { |t|
864
+ return t if yield t
865
+ }
866
+ }
867
+ nil
868
+ end
869
+
870
+ def load_rantfiles
871
+ # Take care: When rant isn't invoked from commandline,
872
+ # some "rant code" could already have run!
873
+ # We run the default Rantfiles only if no tasks where
874
+ # already defined and no Rantfile was given in args.
875
+ new_rf = []
876
+ @arg_rantfiles.each { |rf|
877
+ if test(?f, rf)
878
+ new_rf << rf
879
+ else
880
+ abort("No such file: " + rf)
881
+ end
882
+ }
883
+ if new_rf.empty? && !have_any_task?
884
+ # no Rantfiles given in args, no tasks defined,
885
+ # so let's look for the default files
886
+ new_rf = rantfiles_in_dir
887
+ end
888
+ new_rf.map! { |path|
889
+ rf, is_new = rantfile_for_path(path)
890
+ if is_new
891
+ load_file rf
892
+ end
893
+ rf
894
+ }
895
+ if @rantfiles.empty?
896
+ abort("No Rantfile in current directory (" + Dir.pwd + ")",
897
+ "looking for " + Rant::RANTFILES.join(", ") +
898
+ "; case matters!")
899
+ end
900
+ end
901
+
902
+ def load_file rantfile
903
+ msg 1, "source #{rantfile.path}"
904
+ begin
905
+ path = rantfile.absolute_path
906
+ @context.instance_eval(File.read(path), path)
907
+ rescue NameError => e
908
+ abort("Name error when loading `#{rantfile.path}':",
909
+ e.message, e.backtrace)
910
+ rescue LoadError => e
911
+ abort("Load error when loading `#{rantfile.path}':",
912
+ e.message, e.backtrace)
913
+ rescue ScriptError => e
914
+ abort("Script error when loading `#{rantfile.path}':",
915
+ e.message, e.backtrace)
916
+ end
917
+ unless @rantfiles.include?(rantfile)
918
+ @rantfiles << rantfile
919
+ end
920
+ end
921
+ private :load_file
922
+
923
+ # Get all rantfiles in dir.
924
+ # If dir is nil, look in current directory.
925
+ # Returns always an array with the pathes (not only the filenames)
926
+ # to the rantfiles.
927
+ def rantfiles_in_dir dir=nil
928
+ files = []
929
+ ::Rant::RANTFILES.each { |rfn|
930
+ path = dir ? File.join(dir, rfn) : rfn
931
+ # We load don't accept rantfiles with pathes that differ
932
+ # only in case. This protects from loading the same file
933
+ # twice on case insensitive file systems.
934
+ unless files.find { |f| f.downcase == path.downcase }
935
+ files << path if test(?f, path)
936
+ end
937
+ }
938
+ files
939
+ end
940
+
941
+ def process_args
942
+ # WARNING: we currently have to fool getoptlong,
943
+ # by temporory changing ARGV!
944
+ # This could cause problems.
945
+ old_argv = ARGV.dup
946
+ ARGV.replace(@args.dup)
947
+ cmd_opts = GetoptLong.new(*OPTIONS.collect { |lst| lst[0..-2] })
948
+ cmd_opts.quiet = true
949
+ cmd_opts.each { |opt, value|
950
+ case opt
951
+ when "--verbose": more_verbose
952
+ when "--quiet"
953
+ @opts[:quiet] = true
954
+ @opts[:verbose] = -1
955
+ when "--err-commands"
956
+ @opts[:err_commands] = true
957
+ when "--version"
958
+ $stdout.puts "rant #{Rant::VERSION}"
959
+ raise Rant::RantDoneException
960
+ when "--help"
961
+ help
962
+ when "--directory"
963
+ @opts[:directory] = value
964
+ when "--rantfile"
965
+ @arg_rantfiles << value
966
+ when "--force-run"
967
+ @force_targets << value
968
+ when "--tasks"
969
+ @opts[:targets] = true
970
+ when "--stop-after-load"
971
+ @opts[:stop_after_load] = true
972
+ end
973
+ }
974
+ rescue GetoptLong::Error => e
975
+ abort(e.message)
976
+ ensure
977
+ rem_args = ARGV.dup
978
+ ARGV.replace(old_argv)
979
+ rem_args.each { |ra|
980
+ if ra =~ /(^[^=]+)=([^=]+)$/
981
+ msg 2, "Environment: #$1=#$2"
982
+ ENV[$1] = $2
983
+ else
984
+ @arg_targets << ra
985
+ end
986
+ }
987
+ end
988
+
989
+ def prepare_task(targ, block, clr = caller[2])
990
+
991
+ # Allow override of caller, usefull for plugins and libraries
992
+ # that define tasks.
993
+ if targ.is_a? Hash
994
+ targ.reject! { |k, v|
995
+ case k
996
+ when :__caller__
997
+ clr = v
998
+ true
999
+ else
1000
+ false
1001
+ end
1002
+ }
1003
+ end
1004
+ cinf = Hash === clr ? clr : Rant::Lib::parse_caller_elem(clr)
1005
+
1006
+ name, pre, file, ln = normalize_task_arg(targ, cinf)
1007
+
1008
+ file, is_new = rantfile_for_path(file)
1009
+ nt = yield(name, pre, block)
1010
+ nt.rantfile = file
1011
+ nt.line_number = ln
1012
+ nt.description = @task_desc
1013
+ @task_desc = nil
1014
+ file.tasks << nt
1015
+ hash_task nt
1016
+ nt
1017
+ end
1018
+ public :prepare_task
1019
+
1020
+ def hash_task task
1021
+ n = task.name
1022
+ et = @tasks[n]
1023
+ case et
1024
+ when nil
1025
+ @tasks[n] = task
1026
+ when Rant::Worker
1027
+ mt = Rant::MetaTask.new n
1028
+ mt << et << task
1029
+ @tasks[n] = mt
1030
+ else # assuming Rant::MetaTask
1031
+ et << task
1032
+ end
1033
+ end
1034
+
1035
+ # Tries to extract task name and prerequisites from the typical
1036
+ # argument to the +task+ command. +targ+ should be one of String,
1037
+ # Symbol or Hash. clr is the caller and is used for error
1038
+ # reporting and debugging.
1039
+ #
1040
+ # Returns four values, the first is a string which is the task name
1041
+ # and the second is an array with the prerequisites.
1042
+ # The third is the file name of +clr+, the fourth is the line number
1043
+ # of +clr+.
1044
+ def normalize_task_arg(targ, clr)
1045
+ # TODO: check the code calling this method so that we can
1046
+ # assume clr is already a hash
1047
+ ch = Hash === clr ? clr : Rant::Lib::parse_caller_elem(clr)
1048
+ name = nil
1049
+ pre = []
1050
+ ln = ch[:ln] || 0
1051
+ file = ch[:file]
1052
+
1053
+ # process and validate targ
1054
+ if targ.is_a? Hash
1055
+ if targ.empty?
1056
+ abort(pos_text(file, ln),
1057
+ "Empty hash as task argument, " +
1058
+ "task name required.")
1059
+ end
1060
+ if targ.size > 1
1061
+ abort(pos_text(file, ln),
1062
+ "Too many hash elements, " +
1063
+ "should only be one.")
1064
+ end
1065
+ targ.each_pair { |k,v|
1066
+ name = normalize_task_name(k, file, ln)
1067
+ pre = v
1068
+ }
1069
+ if pre.respond_to? :to_ary
1070
+ pre = pre.to_ary.dup
1071
+ pre.map! { |elem|
1072
+ normalize_task_name(elem, file, ln)
1073
+ }
1074
+ else
1075
+ pre = [normalize_task_name(pre, file, ln)]
1076
+ end
1077
+ else
1078
+ name = normalize_task_name(targ, file, ln)
1079
+ end
1080
+
1081
+ [name, pre, file, ln]
1082
+ end
1083
+ public :normalize_task_arg
1084
+
1085
+ # Tries to make a task name out of arg and returns
1086
+ # the valid task name. If not possible, calls abort
1087
+ # with an appropriate error message using file and ln.
1088
+ def normalize_task_name(arg, file, ln)
1089
+ return arg if arg.is_a? String
1090
+ if Symbol === arg
1091
+ arg.to_s
1092
+ elsif arg.respond_to? :to_str
1093
+ arg.to_str
1094
+ else
1095
+ abort(pos_text(file, ln),
1096
+ "Task name has to be a string or symbol.")
1097
+ end
1098
+ end
1099
+
1100
+ # Returns a Rant::Rantfile object as first value
1101
+ # and a boolean value as second. If the second is true,
1102
+ # the rantfile was created and added, otherwise the rantfile
1103
+ # already existed.
1104
+ def rantfile_for_path path
1105
+ # TODO: optimization: File.expand_path is called very often
1106
+ # (don't forget the calls from Rant::Path#absolute_path)
1107
+ abs_path = File.expand_path(path)
1108
+ if @rantfiles.any? { |rf| rf.absolute_path == abs_path }
1109
+ file = @rantfiles.find { |rf| rf.absolute_path == abs_path }
1110
+ [file, false]
1111
+ else
1112
+ file = Rant::Rantfile.new(abs_path, abs_path)
1113
+ @rantfiles << file
1114
+ [file, true]
1115
+ end
1116
+ end
1117
+
1118
+ end # class Rant::RantApp