rant 0.3.0 → 0.3.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. data/NEWS +21 -0
  2. data/README +8 -0
  3. data/Rantfile +30 -16
  4. data/TODO +3 -4
  5. data/devel-notes +16 -0
  6. data/doc/csharp.rdoc +2 -3
  7. data/doc/examples/myprog/README +2 -0
  8. data/doc/examples/myprog/Rantfile +13 -0
  9. data/doc/examples/myprog/src/Rantfile +15 -0
  10. data/doc/examples/myprog/src/lib.c +6 -0
  11. data/doc/examples/myprog/src/lib.h +4 -0
  12. data/doc/examples/myprog/src/main.c +7 -0
  13. data/doc/rantfile.rdoc +108 -3
  14. data/lib/rant/cs_compiler.rb +6 -4
  15. data/lib/rant/import.rb +13 -0
  16. data/lib/rant/import/rubypackage.rb +43 -20
  17. data/lib/rant/plugin/configure.rb +21 -7
  18. data/lib/rant/plugin/csharp.rb +8 -9
  19. data/lib/rant/rantenv.rb +3 -0
  20. data/lib/rant/rantfile.rb +66 -33
  21. data/lib/rant/rantlib.rb +133 -27
  22. data/lib/rant/rantsys.rb +197 -42
  23. data/rantmethods.rb +46 -0
  24. data/setup.rb +18 -4
  25. data/test/plugin/configure/Rantfile +0 -6
  26. data/test/plugin/configure/test_configure.rb +2 -2
  27. data/test/plugin/csharp/test_csharp.rb +7 -2
  28. data/test/plugin/rantfile +45 -0
  29. data/test/plugin/test_conf_csharp.rb +53 -0
  30. data/test/project1/Rantfile +4 -0
  31. data/test/project1/test_project.rb +33 -8
  32. data/test/project2/buildfile +3 -3
  33. data/test/project2/sub1/Rantfile +3 -3
  34. data/test/project2/test_project.rb +6 -4
  35. data/test/project_rb1/test_project_rb1.rb +2 -2
  36. data/test/standalone.rf +10 -0
  37. data/test/subdirs/Rantfile +26 -0
  38. data/test/subdirs/sub1/Rantfile +15 -0
  39. data/test/subdirs/sub2/rantfile.rb +14 -0
  40. data/test/subdirs/sub2/sub/rantfile +15 -0
  41. data/test/subdirs/test_subdirs.rb +96 -0
  42. data/test/test_env.rb +3 -3
  43. data/test/test_filelist.rb +143 -0
  44. data/test/test_lighttask.rb +21 -0
  45. data/test/test_metatask.rb +1 -1
  46. data/test/test_rant_interface.rb +5 -5
  47. data/test/test_sys.rb +7 -1
  48. data/test/test_task.rb +25 -0
  49. data/test/toplevel.rf +0 -1
  50. data/test/tutil.rb +26 -1
  51. metadata +39 -14
@@ -14,7 +14,7 @@ require 'rant/rantfile'
14
14
  require 'rant/rantsys'
15
15
 
16
16
  module Rant
17
- VERSION = '0.3.0'
17
+ VERSION = '0.3.2'
18
18
 
19
19
  # Those are the filenames for rantfiles.
20
20
  # Case matters!
@@ -83,7 +83,7 @@ class Array
83
83
  end
84
84
 
85
85
  module Rant::Lib
86
-
86
+
87
87
  # Parses one string (elem) as it occurs in the array
88
88
  # which is returned by caller.
89
89
  # E.g.:
@@ -317,9 +317,15 @@ class Rant::RantApp
317
317
  # "private" options intended for debugging, testing and
318
318
  # internal use. A private option is distuingished from others
319
319
  # by having +nil+ as description!
320
+
320
321
  [ "--stop-after-load", GetoptLong::NO_ARGUMENT, nil ],
322
+ # Print caller to $stderr on abort.
323
+ [ "--trace-abort", GetoptLong::NO_ARGUMENT, nil ],
321
324
  ]
322
325
 
326
+ ROOT_DIR_ID = "#"
327
+ ESCAPE_ID = "\\"
328
+
323
329
  # Arguments, usually those given on commandline.
324
330
  attr_reader :args
325
331
  # A list of all Rantfiles used by this app.
@@ -346,6 +352,8 @@ class Rant::RantApp
346
352
  # A list with of all imports (code loaded with +import+).
347
353
  attr_reader :imports
348
354
 
355
+ attr_reader :current_subdir
356
+
349
357
  def initialize *args
350
358
  @args = args.flatten
351
359
  # Rantfiles will be loaded in the context of this object.
@@ -371,14 +379,10 @@ class Rant::RantApp
371
379
  @task_desc = nil
372
380
 
373
381
  @orig_pwd = nil
382
+ @current_subdir = ""
374
383
 
375
384
  end
376
385
 
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
386
  def [](opt)
383
387
  @opts[opt]
384
388
  end
@@ -393,7 +397,8 @@ class Rant::RantApp
393
397
  end
394
398
 
395
399
  def rootdir
396
- @opts[:directory].dup
400
+ od = @opts[:directory]
401
+ od ? od.dup : ""
397
402
  end
398
403
 
399
404
  def rootdir=(newdir)
@@ -402,9 +407,71 @@ class Rant::RantApp
402
407
  "be changed after calling `run'"
403
408
  end
404
409
  @opts[:directory] = newdir.dup
405
- rootdir # return a dup of the new rootdir
406
410
  end
407
411
 
412
+ ### experimental support for subdirectories ######################
413
+ def expand_project_path(path)
414
+ expand_path(@current_subdir, path)
415
+ =begin
416
+ case path
417
+ when nil: @current_subdir.dup
418
+ when "": @current_subdir.dup
419
+ when /^#/: path.sub(/^#/, '')
420
+ when /^\\#/: path.sub(/^\\/, '')
421
+ else
422
+ #puts "epp: current_subdir: #@current_subdir"
423
+ if @current_subdir.empty?
424
+ # we are in project's root directory
425
+ path
426
+ else
427
+ File.join(@current_subdir, path)
428
+ end
429
+ end
430
+ =end
431
+ end
432
+ def expand_path(subdir, path)
433
+ case path
434
+ when nil: subdir.dup
435
+ when "": subdir.dup
436
+ when /^#/: path.sub(/^#/, '')
437
+ when /^\\#/: path.sub(/^\\/, '')
438
+ else
439
+ #puts "epp: current_subdir: #@current_subdir"
440
+ if subdir.empty?
441
+ # we are in project's root directory
442
+ path
443
+ else
444
+ File.join(subdir, path)
445
+ end
446
+ end
447
+ end
448
+ # Returns an absolute path. If path resolves to a directory this
449
+ # method ensures that the returned absolute path doesn't end in a
450
+ # slash.
451
+ def project_to_fs_path(path)
452
+ base = rootdir.empty? ? Dir.pwd : rootdir
453
+ sub = expand_project_path(path)
454
+ sub.empty? ? base : File.join(base, sub)
455
+ end
456
+ def goto(dir)
457
+ # TODO: optimize
458
+ p_dir = expand_project_path(dir)
459
+ abs_path = project_to_fs_path(dir)
460
+ @current_subdir = p_dir
461
+ unless Dir.pwd == abs_path
462
+ #puts "pwd: #{Dir.pwd}; abs_path: #{abs_path}"
463
+ #puts " current subdir: #@current_subdir"
464
+ Dir.chdir abs_path
465
+ msg 1, "in #{abs_path}"
466
+ #STDERR.puts "rant: in #{p_dir}"
467
+ end
468
+ end
469
+ def goto_project_dir(dir)
470
+ # TODO: optimize
471
+ goto "##{dir}"
472
+ end
473
+ ##################################################################
474
+
408
475
  def ran?
409
476
  @ran
410
477
  end
@@ -423,10 +490,12 @@ class Rant::RantApp
423
490
  # Set pwd.
424
491
  opts_dir = @opts[:directory]
425
492
  if opts_dir
493
+ opts_dir = File.expand_path(opts_dir)
426
494
  unless test(?d, opts_dir)
427
495
  abort("No such directory - #{opts_dir}")
428
496
  end
429
- opts_dir != @orig_pwd && Dir.chdir(opts_dir)
497
+ Dir.chdir(opts_dir) if opts_dir != @orig_pwd
498
+ @opts[:directory] = opts_dir
430
499
  else
431
500
  @opts[:directory] = @orig_pwd
432
501
  end
@@ -594,8 +663,14 @@ class Rant::RantApp
594
663
  # and a new file task.
595
664
  def enhance targ, &block
596
665
  prepare_task(targ, block) { |name,pre,blk|
597
- t = select_task { |t| t.name == name }
666
+ t = @tasks[name]
667
+ if Rant::MetaTask === t
668
+ t = t.last
669
+ end
598
670
  if t
671
+ unless t.respond_to? :enhance
672
+ abort("Can't enhance task `#{name}'")
673
+ end
599
674
  t.enhance(pre, &blk)
600
675
  return t
601
676
  end
@@ -622,9 +697,9 @@ class Rant::RantApp
622
697
  ln = cinf[:ln] || 0
623
698
  file = cinf[:file]
624
699
  args.each { |arg|
625
- if arg.is_a? Symbol
626
- arg = arg.to_s
627
- elsif arg.respond_to? :to_str
700
+ #if arg.is_a? Symbol
701
+ # arg = arg.to_s
702
+ if arg.respond_to? :to_str
628
703
  arg = arg.to_str
629
704
  end
630
705
  unless arg.is_a? String
@@ -632,13 +707,25 @@ class Rant::RantApp
632
707
  "in `subdirs' command: arguments must be strings")
633
708
  end
634
709
  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
- }
710
+ prev_subdir = @current_subdir
711
+ begin
712
+ #puts "* subdir *",
713
+ # " rootdir: #{rootdir}",
714
+ # " current subdir: #@current_subdir",
715
+ # " pwd: #{Dir.pwd}",
716
+ # " arg: #{arg}"
717
+ goto arg
718
+ rantfiles_in_dir.each { |f|
719
+ loaded = true
720
+ rf, is_new = rantfile_for_path(f)
721
+ if is_new
722
+ load_file rf
723
+ end
724
+ }
725
+ ensure
726
+ #puts " going back to project dir: #{prev_subdir}"
727
+ goto_project_dir prev_subdir
728
+ end
642
729
  unless loaded || quiet?
643
730
  warn_msg(pos_text(file, ln) + "; in `subdirs' command:",
644
731
  "No Rantfile in subdir `#{arg}'.")
@@ -649,7 +736,7 @@ class Rant::RantApp
649
736
  "in `subdirs' command: " + e.message)
650
737
  end
651
738
 
652
- def sys *args
739
+ def sys(*args)
653
740
  if args.empty?
654
741
  @sys
655
742
  else
@@ -665,9 +752,10 @@ class Rant::RantApp
665
752
  td
666
753
  end
667
754
 
668
- # Prints msg as error message and throws a RantAbortException.
755
+ # Prints msg as error message and raises a RantAbortException.
669
756
  def abort *msg
670
757
  err_msg(msg) unless msg.empty?
758
+ $stderr.puts caller if @opts[:trace_abort]
671
759
  raise Rant::RantAbortException
672
760
  end
673
761
 
@@ -709,6 +797,8 @@ class Rant::RantApp
709
797
  @opts[:quiet] = false
710
798
  end
711
799
 
800
+ # This is actually an integer indicating the verbosity level.
801
+ # Usual values range from 0 to 3.
712
802
  def verbose
713
803
  @opts[:verbose]
714
804
  end
@@ -812,7 +902,9 @@ class Rant::RantApp
812
902
  opt[:force] = true
813
903
  @force_targets.delete(target)
814
904
  end
815
- (select_tasks { |t| t.name == target }).each { |t|
905
+ ### pre 0.3.1 ###
906
+ #(select_tasks { |t| t.name == target }).each { |t|
907
+ select_tasks_by_name(target).each { |t|
816
908
  matching_tasks += 1
817
909
  begin
818
910
  t.invoke(opt)
@@ -845,8 +937,10 @@ class Rant::RantApp
845
937
 
846
938
  # Returns an array (might be a MetaTask) with all tasks that have
847
939
  # the given name.
848
- def select_tasks_by_name name
849
- s = @tasks[name]
940
+ def select_tasks_by_name name, project_dir = @current_subdir
941
+ # pre 0.3.1
942
+ #s = @tasks[name]
943
+ s = @tasks[expand_path(project_dir, name)]
850
944
  case s
851
945
  when nil: []
852
946
  when Rant::Worker: [s]
@@ -969,6 +1063,8 @@ class Rant::RantApp
969
1063
  @opts[:targets] = true
970
1064
  when "--stop-after-load"
971
1065
  @opts[:stop_after_load] = true
1066
+ when "--trace-abort"
1067
+ @opts[:trace_abort] = true
972
1068
  end
973
1069
  }
974
1070
  rescue GetoptLong::Error => e
@@ -986,7 +1082,9 @@ class Rant::RantApp
986
1082
  }
987
1083
  end
988
1084
 
1085
+ # Every task has to be registered with this method.
989
1086
  def prepare_task(targ, block, clr = caller[2])
1087
+ #STDERR.puts "prepare task (#@current_subdir):\n #{targ.inspect}"
990
1088
 
991
1089
  # Allow override of caller, usefull for plugins and libraries
992
1090
  # that define tasks.
@@ -1018,7 +1116,8 @@ class Rant::RantApp
1018
1116
  public :prepare_task
1019
1117
 
1020
1118
  def hash_task task
1021
- n = task.name
1119
+ n = task.full_name
1120
+ #STDERR.puts "hash_task: `#{n}'"
1022
1121
  et = @tasks[n]
1023
1122
  case et
1024
1123
  when nil
@@ -1109,10 +1208,17 @@ class Rant::RantApp
1109
1208
  file = @rantfiles.find { |rf| rf.absolute_path == abs_path }
1110
1209
  [file, false]
1111
1210
  else
1211
+ # create new Rantfile object
1112
1212
  file = Rant::Rantfile.new(abs_path, abs_path)
1213
+ file.project_subdir = @current_subdir
1113
1214
  @rantfiles << file
1114
1215
  [file, true]
1115
1216
  end
1116
1217
  end
1117
1218
 
1219
+ # Just ensure that Rant.rantapp holds an RantApp after loading
1220
+ # this file. The code in initialize will register the new app with
1221
+ # Rant.rantapp= if necessary.
1222
+ self.new
1223
+
1118
1224
  end # class Rant::RantApp
@@ -4,39 +4,170 @@ require 'rant/rantenv'
4
4
 
5
5
  module Rant
6
6
 
7
- class FileList < Array
7
+ class Glob < String
8
+ class << self
9
+ # A synonym for +new+.
10
+ def [](pattern)
11
+ new(pattern)
12
+ end
13
+ end
14
+ end
15
+
16
+ class FileList
17
+ include Enumerable
8
18
 
9
- attr_reader :pattern
19
+ ESC_SEPARATOR = Regexp.escape(File::SEPARATOR)
20
+ ESC_ALT_SEPARATOR = File::ALT_SEPARATOR ?
21
+ Regexp.escape(File::ALT_SEPARATOR) : nil
22
+
23
+ # Flags for the File::fnmatch method.
24
+ # Initialized to 0.
25
+ attr_accessor :glob_flags
10
26
 
11
27
  class << self
12
- def [] pattern
13
- new(pattern).no_suffix("~").no_suffix(".bak")
28
+ def [](*patterns)
29
+ new(*patterns)
14
30
  end
15
31
  end
16
32
 
17
- def initialize(pattern, flags = 0)
18
- super(Dir.glob(pattern, flags))
19
- @actions = []
33
+ def initialize(*patterns)
34
+ @glob_flags = 0
35
+ @files = []
36
+ @actions = patterns.map { |pat| [:apply_include, pat] }
37
+ @pending = true
38
+ yield self if block_given?
39
+ end
40
+
41
+ protected
42
+ attr_reader :actions, :files
43
+ attr_accessor :pending
44
+
45
+ public
46
+ ### Methods having an equivalent in the Array class. #########
47
+
48
+ def each &block
49
+ resolve if @pending
50
+ @files.each(&block)
51
+ end
52
+
53
+ def to_ary
54
+ resolve if @pending
55
+ @files
56
+ end
57
+
58
+ def to_a
59
+ to_ary
20
60
  end
21
61
 
22
62
  def +(other)
23
- fl = self.dup
24
- fl.concat(other.to_ary)
25
- fl
26
- end
27
-
28
- # Reevaluate pattern. Replay all modifications
29
- # if replay is given.
30
- def update(replay = true)
31
- self.replace(Dir[pattern])
32
- if replay
33
- @actions.each { |action|
34
- self.send(*action)
35
- }
63
+ case other
64
+ when Array
65
+ dup.files.concat(other)
66
+ when self.class
67
+ c = other.dup
68
+ c.actions.concat(@actions)
69
+ c.files.concat(@files)
70
+ c.pending = !c.actions.empty?
71
+ c
72
+ else
73
+ raise "argument has to be an Array or FileList"
74
+ end
75
+ end
76
+
77
+ def <<(file)
78
+ @files << file
79
+ self
80
+ end
81
+
82
+ def size
83
+ resolve if @pending
84
+ @files.size
85
+ end
86
+
87
+ def method_missing(sym, *args, &block)
88
+ if @files.respond_to? sym
89
+ resolve if @pending
90
+ @files.send(sym, *args, &block)
91
+ else
92
+ super
36
93
  end
94
+ end
95
+ ##############################################################
96
+
97
+ def resolve
98
+ @pending = false
99
+ @actions.each { |action|
100
+ self.send(*action)
101
+ }
37
102
  @actions.clear
38
103
  end
39
104
 
105
+ def include(*patterns)
106
+ patterns.each { |pat|
107
+ @actions << [:apply_include, pat]
108
+ }
109
+ @pending = true
110
+ self
111
+ end
112
+ alias glob include
113
+
114
+ def apply_include(pattern)
115
+ @files.concat Dir.glob(pattern, @glob_flags)
116
+ end
117
+ private :apply_include
118
+
119
+ def exclude(*patterns)
120
+ patterns.each { |pat|
121
+ if Regexp === pat
122
+ @actions << [:apply_exclude_rx, pat]
123
+ else
124
+ @actions << [:apply_exclude, pat]
125
+ end
126
+ }
127
+ @pending = true
128
+ self
129
+ end
130
+
131
+ def apply_exclude(pattern)
132
+ @files.reject! { |elem|
133
+ File.fnmatch? pattern, elem, @glob_flags
134
+ }
135
+ end
136
+ private :apply_exclude
137
+
138
+ def apply_exclude_rx(rx)
139
+ @files.reject! { |elem|
140
+ elem =~ rx
141
+ }
142
+ end
143
+ private :apply_exclude_rx
144
+
145
+ def exclude_all(*files)
146
+ files.each { |file|
147
+ @actions << [:apply_exclude_rx, mk_all_rx(file)]
148
+ }
149
+ @pending = true
150
+ self
151
+ end
152
+ alias shun exclude_all
153
+
154
+ if File::ALT_SEPARATOR
155
+ # TODO: check for FS case sensitivity?
156
+ def mk_all_rx(file)
157
+
158
+ /(^|(#{ESC_SEPARATOR}|#{ESC_ALT_SEPARATOR})+)
159
+ #{Regexp.escape(file)}
160
+ ((#{ESC_SEPARATOR}|#{ESC_ALT_SEPARATOR})+|$)/x
161
+ end
162
+ else
163
+ def mk_all_rx(file)
164
+ /(^|#{ESC_SEPARATOR}+)
165
+ #{Regexp.escape(file)}
166
+ (#{ESC_SEPARATOR}+|$)/x
167
+ end
168
+ end
169
+ private :mk_all_rx
170
+
40
171
  # Remove all entries which contain a directory with the
41
172
  # given name.
42
173
  # If no argument or +nil+ given, remove all directories.
@@ -48,16 +179,21 @@ module Rant
48
179
  # src/CVS/lib.c
49
180
  # CVS/foo/bar/
50
181
  def no_dir(name = nil)
51
- @actions << [:no_dir, name]
182
+ @actions << [:apply_no_dir, name]
183
+ @pending = true
184
+ self
185
+ end
186
+
187
+ def apply_no_dir(name)
52
188
  entry = nil
53
189
  unless name
54
- self.reject! { |entry|
190
+ @files.reject! { |entry|
55
191
  test(?d, entry)
56
192
  }
57
- return self
193
+ return
58
194
  end
59
195
  elems = nil
60
- self.reject! { |entry|
196
+ @files.reject! { |entry|
61
197
  elems = Sys.split_path(entry)
62
198
  i = elems.index(name)
63
199
  if i
@@ -67,45 +203,61 @@ module Rant
67
203
  false
68
204
  end
69
205
  }
70
- self
71
206
  end
207
+ private :apply_no_dir
72
208
 
73
209
  # Remove all files which have the given name.
74
210
  def no_file(name)
75
- @actions << [:no_file, name]
76
- self.reject! { |entry|
211
+ @actions << [:apply_no_file, name]
212
+ @pending = true
213
+ self
214
+ end
215
+
216
+ def apply_no_file(name)
217
+ @files.reject! { |entry|
77
218
  entry == name and test(?f, entry)
78
219
  }
79
- self
80
220
  end
221
+ private :apply_no_file
81
222
 
82
223
  # Remove all entries which contain an element
83
224
  # with the given suffix.
84
225
  def no_suffix(suffix)
85
226
  @actions << [:no_suffix, suffix]
86
- elems = elem = nil
87
- self.reject! { |entry|
227
+ @pending = true
228
+ self
229
+ end
230
+
231
+ def apply_no_suffix(suffix)
232
+ elems = nil
233
+ elem = nil
234
+ @files.reject! { |entry|
88
235
  elems = Sys.split_path(entry)
89
236
  elems.any? { |elem|
90
237
  elem =~ /#{suffix}$/
91
238
  }
92
239
  }
93
- self
94
240
  end
241
+ private :apply_no_suffix
95
242
 
96
243
  # Remove all entries which contain an element
97
244
  # with the given prefix.
98
245
  def no_prefix(prefix)
99
246
  @actions << [:no_prefix, prefix]
247
+ @pending = true
248
+ self
249
+ end
250
+
251
+ def apply_no_prefix(prefix)
100
252
  elems = elem = nil
101
- self.reject! { |entry|
253
+ @files.reject! { |entry|
102
254
  elems = Sys.split_path(entry)
103
255
  elems.any? { |elem|
104
256
  elem =~ /^#{prefix}/
105
257
  }
106
258
  }
107
- self
108
259
  end
260
+ private :apply_no_prefix
109
261
 
110
262
  # Get a string with all entries. This is very usefull
111
263
  # if you invoke a shell:
@@ -115,6 +267,9 @@ module Rant
115
267
  # rdoc foo\bar "with space"
116
268
  # on other systems:
117
269
  # rdoc foo/bar 'with space'
270
+ def arglist
271
+ to_ary.arglist
272
+ end
118
273
  =begin
119
274
  def arglist
120
275
  self.list.join(' ')
@@ -141,6 +296,7 @@ module Rant
141
296
  end
142
297
  end
143
298
  =end
299
+
144
300
  end # class FileList
145
301
 
146
302
  class CommandError < StandardError
@@ -182,26 +338,25 @@ module Rant
182
338
  # We override the output method of the FileUtils module to
183
339
  # allow the Rant application to control output.
184
340
  def fu_output_message(msg) #:nodoc:
185
- ::Rant.rantapp.cmd_msg msg
341
+ ::Rant.rantapp.cmd_msg msg if ::Rant.rantapp
186
342
  end
187
343
 
188
344
  def sh(*cmd_args, &block)
189
345
  cmd_args.flatten!
190
346
  cmd = cmd_args.join(" ")
191
- unless block_given?
192
- block = lambda { |succ, status|
193
- succ or raise CommandError.new(cmd, status)
194
- }
195
- end
196
347
  fu_output_message cmd
197
- block.call(system(*cmd_args), $?)
348
+ if block_given?
349
+ block[system(*cmd_args), $?]
350
+ else
351
+ system(*cmd_args) or raise CommandError.new(cmd, $?)
352
+ end
198
353
  end
199
354
 
200
355
  def ruby(*args, &block)
201
356
  if args.size > 1
202
- sh(*([Env::RUBY] + args), &block)
357
+ sh([Env::RUBY] + args, &block)
203
358
  else
204
- sh(Env::RUBY + ' ' + args.join(' '), &block)
359
+ sh("#{Env::RUBY} #{args.first}", &block)
205
360
  end
206
361
  end
207
362