rant 0.4.8 → 0.5.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 (49) hide show
  1. data/NEWS +31 -0
  2. data/README +3 -1
  3. data/Rantfile +53 -2
  4. data/doc/advanced.rdoc +86 -1
  5. data/doc/c.rdoc +8 -0
  6. data/doc/homepage/index.html +2 -0
  7. data/doc/rant.1 +4 -0
  8. data/doc/rant.rdoc +38 -0
  9. data/doc/rant_vs_rake.rdoc +13 -0
  10. data/doc/rantfile.rdoc +93 -63
  11. data/doc/sys.rdoc +568 -0
  12. data/lib/rant/coregen.rb +43 -16
  13. data/lib/rant/import/command.rb +7 -4
  14. data/lib/rant/import/filelist/more.rb +57 -0
  15. data/lib/rant/import/metadata.rb +5 -1
  16. data/lib/rant/import/nodes/default.rb +3 -24
  17. data/lib/rant/import/signedfile.rb +1 -8
  18. data/lib/rant/import/sys/more.rb +2 -1
  19. data/lib/rant/import/var/booleans.rb +65 -0
  20. data/lib/rant/import/var/lists.rb +34 -0
  21. data/lib/rant/import/var/numbers.rb +116 -0
  22. data/lib/rant/import/var/strings.rb +43 -0
  23. data/lib/rant/import.rb +19 -3
  24. data/lib/rant/node.rb +39 -6
  25. data/lib/rant/rantlib.rb +44 -8
  26. data/lib/rant/rantsys.rb +22 -54
  27. data/lib/rant/rantvar.rb +89 -256
  28. data/misc/TODO +18 -0
  29. data/misc/devel-notes +26 -1
  30. data/test/action.rant +24 -0
  31. data/test/deprecated/test_0_5_4.rb +53 -0
  32. data/test/deprecated/test_0_6_0.rb +1 -1
  33. data/test/dryrun/Rantfile +10 -0
  34. data/test/dryrun/foo.c +8 -0
  35. data/test/dryrun/test_dryrun.rb +31 -0
  36. data/test/import/c/dependencies/Rantfile +1 -1
  37. data/test/import/command/Rantfile +1 -1
  38. data/test/import/sys/test_tgz.rb +22 -0
  39. data/test/subdirs2/root.rant +11 -1
  40. data/test/subdirs2/sub1/sub.rant +3 -0
  41. data/test/subdirs2/test_subdirs2.rb +19 -0
  42. data/test/test_action.rb +75 -0
  43. data/test/test_filelist.rb +13 -10
  44. data/test/test_rant_interface.rb +2 -2
  45. data/test/test_rule.rb +121 -3
  46. data/test/test_sys_methods.rb +558 -0
  47. data/test/test_var.rb +10 -0
  48. data/test/tutil.rb +81 -8
  49. metadata +19 -2
data/lib/rant/rantlib.rb CHANGED
@@ -40,6 +40,7 @@ unless Regexp.respond_to? :union # new in 1.8.1
40
40
  end
41
41
  if RUBY_VERSION < "1.8.2"
42
42
  class Array
43
+ undef_method :flatten, :flatten!
43
44
  def flatten
44
45
  cp = self.dup
45
46
  cp.flatten!
@@ -63,11 +64,12 @@ if RUBY_VERSION < "1.8.2"
63
64
  end
64
65
  end
65
66
  end
66
- end
67
- if RUBY_VERSION < "1.8.1"
68
- module FileUtils
69
- def fu_list(arg)
70
- arg.respond_to?(:to_ary) ? arg.to_ary : [arg]
67
+ if RUBY_VERSION < "1.8.1"
68
+ module FileUtils
69
+ undef_method :fu_list
70
+ def fu_list(arg)
71
+ arg.respond_to?(:to_ary) ? arg.to_ary : [arg]
72
+ end
71
73
  end
72
74
  end
73
75
  end
@@ -85,7 +87,7 @@ class Array
85
87
  def shell_pathes
86
88
  warn caller[0]
87
89
  warn "[WARNING] Array#shell_pathes is highly deprecated " +
88
- "and will not come with future (0.5.0 and later) Rant releases."
90
+ "and will not come with future (0.5.2 and later) Rant releases."
89
91
  warn "Use `ary.map { |path| sys.sp path }' in Rantfiles."
90
92
  map { |path| Rant::Sys.sp(path) }
91
93
  =begin
@@ -138,8 +140,8 @@ module Rant::Lib
138
140
  # Note: This method splits on the pattern <tt>:(\d+)(:|$)</tt>,
139
141
  # assuming anything before is the filename.
140
142
  def parse_caller_elem(elem)
141
- return { :file => "", :ln => 0 } if elem.nil?
142
- if elem =~ /^(.+):(\d+)(:|$)/
143
+ return { :file => "", :ln => 0 } unless elem
144
+ if elem =~ /^(.+):(\d+)(?::|$)/
143
145
  { :file => $1, :ln => $2.to_i }
144
146
  else
145
147
  # should never occur
@@ -337,6 +339,8 @@ class Rant::RantApp
337
339
  "Multiple files may be specified with this option." ],
338
340
  [ "--force-run","-a", GetoptLong::REQUIRED_ARGUMENT,
339
341
  "Force rebuild of TARGET and all dependencies." ],
342
+ [ "--dry-run", "-n", GetoptLong::NO_ARGUMENT,
343
+ "Print info instead of actually executing actions." ],
340
344
  [ "--tasks", "-T", GetoptLong::NO_ARGUMENT,
341
345
  "Show a list of all described tasks and exit." ],
342
346
 
@@ -449,6 +453,10 @@ class Rant::RantApp
449
453
  end
450
454
  end
451
455
  end
456
+ def resolve_root_ref(path)
457
+ return File.join(@rootdir, path[1..-1]) if path =~ /^@/
458
+ path.sub(/^\\(?=@)/, '')
459
+ end
452
460
  # Returns an absolute path. If path resolves to a directory this
453
461
  # method ensures that the returned absolute path doesn't end in a
454
462
  # slash.
@@ -860,6 +868,12 @@ class Rant::RantApp
860
868
  @rootdir : @current_subdir})"
861
869
  @last_build_subdir = @current_subdir
862
870
  end
871
+ # TODO: model feels sick... this functionality should
872
+ # be implemented in Node
873
+ if @opts[:dry_run]
874
+ task.dry_run
875
+ true
876
+ end
863
877
  end
864
878
 
865
879
  private
@@ -958,6 +972,9 @@ class Rant::RantApp
958
972
  # of tasks invoked.
959
973
  def build(target, opt = {})
960
974
  opt[:force] = true if @force_targets.delete(target)
975
+ # Currently either the whole application has to by run in
976
+ # dry-run mode or nothing.
977
+ opt[:dry_run] = @opts[:dry_run]
961
978
  matching_tasks = 0
962
979
  old_subdir = @current_subdir
963
980
  old_pwd = Dir.pwd
@@ -981,6 +998,8 @@ class Rant::RantApp
981
998
  # Currently always returns an array (which might actually be an
982
999
  # empty array, but never nil).
983
1000
  def resolve(task_name, rel_project_dir = @current_subdir)
1001
+ # alternative implementation:
1002
+ # rec_save_resolve(task_name, nil, rel_project_dir)
984
1003
  s = @tasks[expand_path(rel_project_dir, task_name)]
985
1004
  case s
986
1005
  when nil
@@ -1004,6 +1023,23 @@ class Rant::RantApp
1004
1023
  end
1005
1024
  public :resolve
1006
1025
 
1026
+ def rec_save_resolve(task_name, excl_hook, rel_project_dir = @current_subdir)
1027
+ s = @tasks[expand_path(rel_project_dir, task_name)]
1028
+ case s
1029
+ when nil
1030
+ @resolve_hooks.each { |s|
1031
+ next if s == excl_hook
1032
+ s = s[task_name, rel_project_dir]
1033
+ return s if s
1034
+ }
1035
+ []
1036
+ when Rant::Node: [s]
1037
+ else
1038
+ s
1039
+ end
1040
+ end
1041
+ public :rec_save_resolve
1042
+
1007
1043
  # This hook will be invoked when no matching task is found for a
1008
1044
  # target. It may create one or more tasks for the target, which is
1009
1045
  # given as argument, on the fly and return an array of the created
data/lib/rant/rantsys.rb CHANGED
@@ -125,6 +125,12 @@ module Rant
125
125
  @files.size
126
126
  end
127
127
 
128
+ alias _object_respond_to? respond_to?
129
+ private :_object_respond_to?
130
+ def respond_to?(msg)
131
+ _object_respond_to?(msg) or @files.respond_to?(msg)
132
+ end
133
+
128
134
  def method_missing(sym, *args, &block)
129
135
  if @files.respond_to? sym
130
136
  resolve if @pending
@@ -324,55 +330,6 @@ end
324
330
  end
325
331
  private :apply_no_dir
326
332
 
327
- # Remove all files which have the given name.
328
- def no_file(name)
329
- @actions << [:apply_ary_method, :reject!,
330
- lambda { |entry|
331
- entry == name && !@keep[entry] && test(?f, entry)
332
- }]
333
- @pending = true
334
- self
335
- end
336
-
337
- # Remove all entries which contain an element
338
- # with the given suffix.
339
- def no_suffix(suffix)
340
- @actions << [:no_suffix, suffix]
341
- @pending = true
342
- self
343
- end
344
-
345
- def apply_no_suffix(suffix)
346
- elems = nil
347
- elem = nil
348
- @files.reject! { |entry|
349
- elems = Sys.split_all(entry)
350
- elems.any? { |elem|
351
- elem =~ /#{suffix}$/ && !@keep[entry]
352
- }
353
- }
354
- end
355
- private :apply_no_suffix
356
-
357
- # Remove all entries which contain an element
358
- # with the given prefix.
359
- def no_prefix(prefix)
360
- @actions << [:no_prefix, prefix]
361
- @pending = true
362
- self
363
- end
364
-
365
- def apply_no_prefix(prefix)
366
- elems = elem = nil
367
- @files.reject! { |entry|
368
- elems = Sys.split_all(entry)
369
- elems.any? { |elem|
370
- elem =~ /^#{prefix}/ && !@keep[entry]
371
- }
372
- }
373
- end
374
- private :apply_no_prefix
375
-
376
333
  # Get a string with all entries. This is very usefull
377
334
  # if you invoke a shell:
378
335
  # files # => ["foo/bar", "with space"]
@@ -417,6 +374,11 @@ end
417
374
  def apply_ary_method_1(meth, arg1, block=nil)
418
375
  @files.send meth, arg1, &block
419
376
  end
377
+ =begin
378
+ def apply_lazy_operation(meth, args, block)
379
+ @files.send(meth, *args, &block)
380
+ end
381
+ =end
420
382
  end # class FileList
421
383
 
422
384
  class RacFileList < FileList
@@ -645,19 +607,25 @@ end
645
607
 
646
608
  # If supported, make a hardlink, otherwise
647
609
  # fall back to copying.
648
- def safe_ln(*args)
610
+ def safe_ln(src, dest)
611
+ dest = dest.to_str
612
+ src = src.respond_to?(:to_ary) ? src.to_ary : src.to_str
649
613
  unless Sys.symlink_supported
650
- cp(*args)
614
+ cp(src, dest)
651
615
  else
652
616
  begin
653
- ln(*args)
654
- rescue Exception #Errno::EOPNOTSUPP
617
+ ln(src, dest)
618
+ rescue Exception # SystemCallError # Errno::EOPNOTSUPP
655
619
  Sys.symlink_supported = false
656
- cp(*args)
620
+ cp(src, dest)
657
621
  end
658
622
  end
659
623
  end
660
624
 
625
+ def ln_f(src, dest)
626
+ ln(src, dest, :force => true)
627
+ end
628
+
661
629
  # Split a path in all elements.
662
630
  def split_all(path)
663
631
  base, last = File.split(path)
data/lib/rant/rantvar.rb CHANGED
@@ -15,8 +15,9 @@
15
15
  #
16
16
  # If you're looking for general info about Rant, read the
17
17
  # README[link:files/README.html].
18
+
18
19
  module Rant
19
- VERSION = '0.4.8'
20
+ VERSION = '0.5.0'
20
21
 
21
22
  # Those are the filenames for rantfiles.
22
23
  # Case matters!
@@ -115,7 +116,7 @@ module Rant
115
116
  # holds constraints for values in @store
116
117
  @constraints = {}
117
118
  # set by default query
118
- @current_var = nil
119
+ @current_var = nil # this line will go in 0.5.4
119
120
  end
120
121
 
121
122
  def query(*args, &block)
@@ -128,7 +129,7 @@ module Rant
128
129
  if Hash === arg
129
130
  if arg.size == 1
130
131
  arg.each { |k,v|
131
- @current_var = k
132
+ @current_var = k # this line will go in 0.5.4
132
133
  self[k] = v if self[k].nil?
133
134
  }
134
135
  self
@@ -138,16 +139,21 @@ module Rant
138
139
  else
139
140
  self[arg]
140
141
  end
141
- when 2..3
142
- @current_var, cf, val = *args
143
- self.is cf
144
- self[@current_var] = val if val
142
+ when 2, 3
143
+ vid, cf, val = *args
144
+ @current_var = vid # this line will go in 0.5.4
145
+ constrain vid,
146
+ get_factory(cf).rant_constraint
147
+ self[vid] = val if val
145
148
  else
146
- raise QueryError, "to many arguments"
149
+ raise QueryError, "too many arguments"
147
150
  end
148
151
  end
149
152
 
150
153
  def is ct, *ct_args
154
+ warn caller[0]
155
+ warn "method `var.is' is deprecated and will not be " +
156
+ "in Rant 0.5.4 and later"
151
157
  constrain @current_var,
152
158
  get_factory(ct).rant_constraint(*ct_args)
153
159
  self
@@ -166,13 +172,53 @@ module Rant
166
172
  def get_factory id
167
173
  if String === id || Symbol === id
168
174
  begin
175
+ ### temporary solution ###
176
+ raise unless Constraints.const_defined? id
177
+ ##########################
169
178
  id = Constraints.const_get(id)
170
179
  rescue
171
- raise NotAConstraintFactoryError.new(id), caller
180
+ ### temporary solution ###
181
+ id_sym = id.to_sym
182
+ rl =
183
+ if [:Integer, :IntegerInRange, :Float,
184
+ :FloatInRange].include? id_sym
185
+ 'rant/import/var/numbers'
186
+ elsif [:Bool, :BoolTrue].include? id_sym
187
+ 'rant/import/var/booleans'
188
+ elsif :List == id_sym
189
+ 'rant/import/var/lists'
190
+ elsif [:String, :ToString].include? id_sym
191
+ 'rant/import/var/strings'
192
+ else
193
+ ##########################
194
+ raise NotAConstraintFactoryError.new(id), caller
195
+ end
196
+ warn "Explicitely <code>import " +
197
+ "'#{rl.sub(/^rant\/import\//, '')}'" +
198
+ "</code> to use the #{id_sym.inspect} " +
199
+ "constraint."
200
+ require rl #rant-import:remove
201
+ retry
172
202
  end
173
203
  end
174
204
  unless id.respond_to? :rant_constraint
175
- raise NotAConstraintFactoryError.new(id), caller
205
+ ### temporary solution ###
206
+ rl =
207
+ if id == true || id == false
208
+ 'rant/import/var/booleans'
209
+ elsif id == String
210
+ 'rant/import/var/strings'
211
+ elsif id.kind_of? Range
212
+ 'rant/import/var/numbers'
213
+ else
214
+ ##########################
215
+ raise NotAConstraintFactoryError.new(id), caller
216
+ end
217
+ warn "Explicitely <code>import " +
218
+ "'#{rl.sub(/^rant\/import\//, '')}'" +
219
+ "</code> to use the #{id.inspect} " +
220
+ "constraint."
221
+ require rl #rant-import:remove
176
222
  end
177
223
  id
178
224
  end
@@ -278,237 +324,6 @@ module Rant
278
324
  end
279
325
  end
280
326
 
281
- module Constraints
282
-
283
- class String
284
- include Constraint
285
-
286
- class << self
287
- alias rant_constraint new
288
- end
289
-
290
- def filter(val)
291
- if val.respond_to? :to_str
292
- val.to_str
293
- elsif Symbol === val
294
- val.to_s
295
- else
296
- raise ConstraintError.new(self, val)
297
- end
298
- end
299
- def default
300
- ""
301
- end
302
- def to_s
303
- "string"
304
- end
305
- end
306
-
307
- class ToString < String
308
- class << self
309
- alias rant_constraint new
310
- end
311
- def filter(val)
312
- val.to_s
313
- end
314
- end
315
-
316
- class Integer
317
- include Constraint
318
-
319
- class << self
320
- def rant_constraint(range = nil)
321
- if range
322
- IntegerInRange.new(range)
323
- else
324
- self.new
325
- end
326
- end
327
- end
328
-
329
- def filter(val)
330
- Kernel::Integer(val)
331
- rescue
332
- raise ConstraintError.new(self, val)
333
- end
334
- def default
335
- 0
336
- end
337
- def to_s
338
- "integer"
339
- end
340
- end
341
-
342
- class IntegerInRange < Integer
343
- def initialize(range)
344
- @range = range
345
- end
346
- def filter(val)
347
- i = super
348
- if @range === i
349
- i
350
- else
351
- raise ConstraintError.new(self, val)
352
- end
353
- end
354
- def default
355
- @range.min
356
- end
357
- def to_s
358
- super + " #{@range}"
359
- end
360
- end
361
-
362
- class Float
363
- include Constraint
364
-
365
- class << self
366
- def rant_constraint(range = nil)
367
- if range
368
- FloatInRange.new(range)
369
- else
370
- self.new
371
- end
372
- end
373
- end
374
-
375
- def filter(val)
376
- Kernel::Float(val)
377
- rescue
378
- raise ConstraintError.new(self, val)
379
- end
380
- def default
381
- 0.0
382
- end
383
- def to_s
384
- "float"
385
- end
386
- end
387
-
388
- class FloatInRange < Float
389
- def initialize(range)
390
- @range = range
391
- end
392
- def filter(val)
393
- i = super
394
- if @range === i
395
- i
396
- else
397
- raise ConstraintError.new(self, val)
398
- end
399
- end
400
- def default
401
- @range.first
402
- end
403
- def to_s
404
- super + " #{@range}"
405
- end
406
- end
407
-
408
- class AutoList
409
- include Constraint
410
-
411
- class << self
412
- alias rant_constraint new
413
- end
414
-
415
- def filter(val)
416
- if val.respond_to? :to_ary
417
- val.to_ary
418
- elsif val.nil?
419
- raise ConstraintError.new(self, val)
420
- else
421
- [val]
422
- end
423
- end
424
- def default
425
- []
426
- end
427
- def to_s
428
- "list or single, non-nil value"
429
- end
430
- end
431
-
432
- class List
433
- include Constraint
434
-
435
- class << self
436
- alias rant_constraint new
437
- end
438
-
439
- def filter(val)
440
- if val.respond_to? :to_ary
441
- val.to_ary
442
- else
443
- raise ConstraintError.new(self, val)
444
- end
445
- end
446
- def default
447
- []
448
- end
449
- def to_s
450
- "list (Array)"
451
- end
452
- end
453
-
454
- Array = List
455
-
456
- class Bool
457
- include Constraint
458
- class << self
459
- alias rant_constraint new
460
- end
461
- def filter(val)
462
- if ::Symbol === val or ::Integer === val
463
- val = val.to_s
464
- end
465
- if val == true
466
- true
467
- elsif val == false || val == nil
468
- false
469
- elsif val.respond_to? :to_str
470
- case val.to_str
471
- when /^\s*true\s*$/i: true
472
- when /^\s*false\s*$/i: false
473
- when /^\s*y(es)?\s*$/i: true
474
- when /^\s*n(o)?\s*$/: false
475
- when /^\s*on\s*$/i: true
476
- when /^\s*off\s*$/i: false
477
- when /^\s*1\s*$/: true
478
- when /^\s*0\s*$/: false
479
- else
480
- raise ConstraintError.new(self, val)
481
- end
482
- else
483
- raise ConstraintError.new(self, val)
484
- end
485
- end
486
- def default
487
- false
488
- end
489
- def to_s
490
- "bool"
491
- end
492
- end
493
-
494
- class BoolTrue < Bool
495
- def default
496
- true
497
- end
498
- end
499
-
500
- #--
501
- # perhaps this should stay a secret ;)
502
- #++
503
- def true.rant_constraint
504
- BoolTrue.rant_constraint
505
- end
506
- def false.rant_constraint
507
- Bool.rant_constraint
508
- end
509
-
510
- end # module Constraints
511
-
512
327
  # A +vid+ has to be a String to be valid.
513
328
  def valid_vid(obj)
514
329
  case obj
@@ -537,18 +352,36 @@ module Rant
537
352
  end
538
353
 
539
354
  module_function :valid_constraint?, :valid_vid
540
- end # module RantVar
541
- end # module Rant
542
-
543
- class Range
544
- def rant_constraint
545
- case first
546
- when ::Integer
547
- Rant::RantVar::Constraints::IntegerInRange.new(self)
548
- when ::Float
549
- Rant::RantVar::Constraints::FloatInRange.new(self)
550
- else
551
- raise NotAConstraintFactoryError.new(self)
552
- end
553
- end
554
- end
355
+
356
+ module Constraints
357
+ class AutoList
358
+ include Constraint
359
+ class << self
360
+ alias rant_constraint new
361
+ end
362
+ def filter(val)
363
+ if val.respond_to? :to_ary
364
+ val.to_ary
365
+ elsif val.nil?
366
+ raise ConstraintError.new(self, val)
367
+ else
368
+ [val]
369
+ end
370
+ end
371
+ def default
372
+ []
373
+ end
374
+ def to_s
375
+ "list or single, non-nil value"
376
+ end
377
+ end
378
+ end # module Constraints
379
+ end # module RantVar
380
+ end # module Rant
381
+
382
+ ### temporary solution ###
383
+ #require 'rant/import/var/numbers' #rant-import:uncomment
384
+ #require 'rant/import/var/booleans' #rant-import:uncomment
385
+ #require 'rant/import/var/lists' #rant-import:uncomment
386
+ #require 'rant/import/var/strings' #rant-import:uncomment
387
+ ##########################
data/misc/TODO CHANGED
@@ -1,6 +1,24 @@
1
1
 
2
2
  = TODO
3
3
 
4
+ == Convinient symlink method
5
+
6
+ Implement a Rant::Sys.symlink (or similar name) method that converts
7
+ destination filenames to absolute pathes before actual linking.
8
+
9
+ == More tests for --dry-run
10
+
11
+ == Behaviour of `gen Rule' without block
12
+
13
+ Currently dies with <tt>undefined method `arity' for
14
+ nil:NilClass</tt>.
15
+
16
+ Done (0.4.9), printing an appropriate error message.
17
+
18
+ == Command: Hash interpolation
19
+
20
+ Decide about behaviour of hash interpolation in Command tasks.
21
+
4
22
  == Don't use +invoke+ result of dependency to check if update is
5
23
  necessary
6
24
 
data/misc/devel-notes CHANGED
@@ -1,6 +1,31 @@
1
1
 
2
+ == Rule recursion
3
+
4
+ Since Rant 0.4.9, rules created with <tt>gen Rule</tt> no longer
5
+ recurse. This means, that when looking for the Rule's source(s), this
6
+ specific rule itself is ignored. This prevents infinite recursion.
7
+
8
+ Consider:
9
+
10
+ gen Rule, "o" => ".foo" do something end
11
+
12
+ Say we call <tt>make "bo"</tt>.
13
+
14
+ Rant pre 0.4.9 behaviour:
15
+
16
+ The rule matches anything ending in "o". Since "bo" ends in "o", the
17
+ rule matches and searches for the source "b.foo". No task is defined
18
+ for "b.foo", but again our only rule matches and thus searches for the
19
+ source "b.fo.foo". Now we have our infinite recursion.
20
+
21
+ Rant 0.4.9 and later behaviour:
22
+
23
+ The only rule matches and searches for the source "b.foo". When a rule
24
+ searches for a source, the rule itself is excluded. No infinite
25
+ recursion.
26
+
2
27
  == Rant::Env.find_bin...
3
- ... could honour PATHEXT on windows.
28
+ ... should honour PATHEXT on windows.
4
29
 
5
30
  == Task alias
6
31
  Since +alias+ is a Ruby keyword, we could use +nick+ to create task
data/test/action.rant ADDED
@@ -0,0 +1,24 @@
1
+
2
+ import "sys/more"
3
+
4
+ gen Action, /\.t$/ do
5
+ puts 'executing action: rx /\.t$/'
6
+ source "action.t.rant"
7
+ end
8
+
9
+ file "action.t.rant" do |t|
10
+ sys.write_to_file t.name, <<-EOF
11
+ file "a.t" do |t|
12
+ sys.touch t.name
13
+ end
14
+ file "b.t" do |t|
15
+ sys.touch t.name
16
+ end
17
+ EOF
18
+ end
19
+
20
+ file "b.tt" do |t|
21
+ sys.touch t.name
22
+ end
23
+
24
+ subdirs "sub.t" if test ?d, "sub.t"