rant 0.3.0

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