bahuvrihi-tap 0.10.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (86) hide show
  1. data/History +69 -0
  2. data/MIT-LICENSE +21 -0
  3. data/README +119 -0
  4. data/bin/tap +114 -0
  5. data/cmd/console.rb +42 -0
  6. data/cmd/destroy.rb +16 -0
  7. data/cmd/generate.rb +16 -0
  8. data/cmd/run.rb +126 -0
  9. data/doc/Class Reference +362 -0
  10. data/doc/Command Reference +153 -0
  11. data/doc/Tutorial +237 -0
  12. data/lib/tap.rb +32 -0
  13. data/lib/tap/app.rb +720 -0
  14. data/lib/tap/constants.rb +8 -0
  15. data/lib/tap/env.rb +640 -0
  16. data/lib/tap/file_task.rb +547 -0
  17. data/lib/tap/generator/base.rb +109 -0
  18. data/lib/tap/generator/destroy.rb +37 -0
  19. data/lib/tap/generator/generate.rb +61 -0
  20. data/lib/tap/generator/generators/command/command_generator.rb +21 -0
  21. data/lib/tap/generator/generators/command/templates/command.erb +32 -0
  22. data/lib/tap/generator/generators/config/config_generator.rb +26 -0
  23. data/lib/tap/generator/generators/config/templates/doc.erb +12 -0
  24. data/lib/tap/generator/generators/config/templates/nodoc.erb +8 -0
  25. data/lib/tap/generator/generators/file_task/file_task_generator.rb +27 -0
  26. data/lib/tap/generator/generators/file_task/templates/file.txt +11 -0
  27. data/lib/tap/generator/generators/file_task/templates/result.yml +6 -0
  28. data/lib/tap/generator/generators/file_task/templates/task.erb +33 -0
  29. data/lib/tap/generator/generators/file_task/templates/test.erb +29 -0
  30. data/lib/tap/generator/generators/root/root_generator.rb +55 -0
  31. data/lib/tap/generator/generators/root/templates/Rakefile +86 -0
  32. data/lib/tap/generator/generators/root/templates/gemspec +27 -0
  33. data/lib/tap/generator/generators/root/templates/tapfile +8 -0
  34. data/lib/tap/generator/generators/root/templates/test/tap_test_helper.rb +3 -0
  35. data/lib/tap/generator/generators/root/templates/test/tap_test_suite.rb +5 -0
  36. data/lib/tap/generator/generators/root/templates/test/tapfile_test.rb +15 -0
  37. data/lib/tap/generator/generators/task/task_generator.rb +27 -0
  38. data/lib/tap/generator/generators/task/templates/task.erb +14 -0
  39. data/lib/tap/generator/generators/task/templates/test.erb +21 -0
  40. data/lib/tap/generator/manifest.rb +14 -0
  41. data/lib/tap/patches/rake/rake_test_loader.rb +8 -0
  42. data/lib/tap/patches/rake/testtask.rb +55 -0
  43. data/lib/tap/patches/ruby19/backtrace_filter.rb +51 -0
  44. data/lib/tap/patches/ruby19/parsedate.rb +16 -0
  45. data/lib/tap/root.rb +581 -0
  46. data/lib/tap/support/aggregator.rb +55 -0
  47. data/lib/tap/support/assignments.rb +172 -0
  48. data/lib/tap/support/audit.rb +418 -0
  49. data/lib/tap/support/batchable.rb +47 -0
  50. data/lib/tap/support/batchable_class.rb +107 -0
  51. data/lib/tap/support/class_configuration.rb +194 -0
  52. data/lib/tap/support/command_line.rb +98 -0
  53. data/lib/tap/support/comment.rb +270 -0
  54. data/lib/tap/support/configurable.rb +114 -0
  55. data/lib/tap/support/configurable_class.rb +296 -0
  56. data/lib/tap/support/configuration.rb +122 -0
  57. data/lib/tap/support/constant.rb +70 -0
  58. data/lib/tap/support/constant_utils.rb +127 -0
  59. data/lib/tap/support/declarations.rb +111 -0
  60. data/lib/tap/support/executable.rb +111 -0
  61. data/lib/tap/support/executable_queue.rb +82 -0
  62. data/lib/tap/support/framework.rb +71 -0
  63. data/lib/tap/support/framework_class.rb +199 -0
  64. data/lib/tap/support/instance_configuration.rb +147 -0
  65. data/lib/tap/support/lazydoc.rb +428 -0
  66. data/lib/tap/support/manifest.rb +89 -0
  67. data/lib/tap/support/run_error.rb +39 -0
  68. data/lib/tap/support/shell_utils.rb +71 -0
  69. data/lib/tap/support/summary.rb +30 -0
  70. data/lib/tap/support/tdoc.rb +404 -0
  71. data/lib/tap/support/tdoc/tdoc_html_generator.rb +38 -0
  72. data/lib/tap/support/tdoc/tdoc_html_template.rb +42 -0
  73. data/lib/tap/support/templater.rb +180 -0
  74. data/lib/tap/support/validation.rb +410 -0
  75. data/lib/tap/support/versions.rb +97 -0
  76. data/lib/tap/task.rb +259 -0
  77. data/lib/tap/tasks/dump.rb +56 -0
  78. data/lib/tap/tasks/rake.rb +93 -0
  79. data/lib/tap/test.rb +37 -0
  80. data/lib/tap/test/env_vars.rb +29 -0
  81. data/lib/tap/test/file_methods.rb +377 -0
  82. data/lib/tap/test/script_methods.rb +144 -0
  83. data/lib/tap/test/subset_methods.rb +420 -0
  84. data/lib/tap/test/tap_methods.rb +237 -0
  85. data/lib/tap/workflow.rb +187 -0
  86. metadata +145 -0
@@ -0,0 +1,547 @@
1
+ require 'tap/support/shell_utils'
2
+ autoload(:FileUtils, "fileutils")
3
+
4
+ module Tap
5
+
6
+ # FileTask provides methods for creating/modifying files such that you can
7
+ # rollback changes if an error occurs.
8
+ #
9
+ # === Creating Files/Rolling Back Changes
10
+ #
11
+ # FileTask tracks which files to roll back using the added_files array
12
+ # and the backed_up_files hash. On an execute error, all added files are
13
+ # removed and then all backed up files (backed_up_files.keys) are restored
14
+ # using the corresponding backup files (backed_up_files.values).
15
+ #
16
+ # For consistency, all filepaths in added_files and backed_up_files should
17
+ # be expanded using File.expand_path. The easiest way to ensure files are
18
+ # properly set up for rollback is to use prepare before working with files
19
+ # and to create directories with mkdir.
20
+ #
21
+ # # this file will be backed up and restored
22
+ # File.open("file.txt", "w") {|f| f << "original content"}
23
+ #
24
+ # t = FileTask.new do |task|
25
+ # task.mkdir("some/dir") # marked for rollback
26
+ # task.prepare("file.txt", "path/to/file.txt") # marked for rollback
27
+ #
28
+ # File.open("file.txt", "w") {|f| f << "new content"}
29
+ # File.touch("path/to/file.txt")
30
+ #
31
+ # # raise an error to start rollback
32
+ # raise "error!"
33
+ # end
34
+ #
35
+ # begin
36
+ # File.exists?("some/dir") # => false
37
+ # File.exists?("path/to/file.txt") # => false
38
+ # t.execute(nil)
39
+ # rescue
40
+ # $!.message # => "error!"
41
+ # File.exists?("some/dir") # => false
42
+ # File.exists?("path/to/file.txt") # => false
43
+ # File.read("file.txt") # => "original content"
44
+ # end
45
+ #
46
+ class FileTask < Task
47
+ include Tap::Support::ShellUtils
48
+
49
+ # A hash of backup (source, target) pairs, such that the
50
+ # backed-up files are backed_up_files.keys and the actual
51
+ # backup files are backed_up_files.values. All filepaths
52
+ # in backed_up_files should be expanded.
53
+ attr_reader :backed_up_files
54
+
55
+ # An array of files added during task execution.
56
+ attr_reader :added_files
57
+
58
+ # The backup directory, defaults to the class backup_dir
59
+ config_attr :backup_dir, 'backup' # the backup directory
60
+
61
+ # A timestamp format used to mark backup files, defaults
62
+ # to the class backup_timestamp
63
+ config :timestamp, "%Y%m%d_%H%M%S" # the backup timestamp format
64
+
65
+ # A flag indicating whether or not to rollback changes on
66
+ # error, defaults to the class rollback_on_error
67
+ config :rollback_on_error, true, &c.switch # rollback changes on error
68
+
69
+ def initialize(config={}, name=nil, app=App.instance, &task_block)
70
+ super
71
+
72
+ @backed_up_files = {}
73
+ @added_files = []
74
+ end
75
+
76
+ def initialize_copy(orig)
77
+ super
78
+ @backed_up_files = {}
79
+ @added_files = []
80
+ end
81
+
82
+ # A batch File.open method. If a block is given, each file in the list will be
83
+ # opened the open files passed to the block. Files are automatically closed when
84
+ # the block returns. If no block is given, the open files are returned.
85
+ #
86
+ # t = FileTask.new
87
+ # t.open(["one.txt", "two.txt"], "w") do |one, two|
88
+ # one << "one"
89
+ # two << "two"
90
+ # end
91
+ #
92
+ # File.read("one.txt") # => "one"
93
+ # File.read("two.txt") # => "two"
94
+ #
95
+ # Note that open normally takes and passes a list (ie an Array). If you provide
96
+ # a single argument, it will be translated into an Array, and passed AS AN ARRAY
97
+ # to the block.
98
+ #
99
+ # t.open("file.txt", "w") do |array|
100
+ # array.first << "content"
101
+ # end
102
+ #
103
+ # File.read("file.txt") # => "content"
104
+ #
105
+ def open(list, mode="rb")
106
+ open_files = []
107
+ begin
108
+ [list].flatten.map {|path| path.to_str }.each do |filepath|
109
+ open_files << File.open(filepath, mode)
110
+ end
111
+
112
+ block_given? ? yield(open_files) : open_files
113
+ ensure
114
+ open_files.each {|file| file.close } if block_given?
115
+ end
116
+ end
117
+
118
+ # Returns the basename of path, exchanging the extension
119
+ # with extname, if provided.
120
+ #
121
+ # t = FileTask.new
122
+ # t.basename('path/to/file.txt') # => 'file.txt'
123
+ # t.basename('path/to/file.txt', '.html') # => 'file.html'
124
+ #
125
+ def basename(path, extname=nil)
126
+ basename = File.basename(path)
127
+ unless extname == nil
128
+ extname = $1 if extname =~ /^\.?(.*)/
129
+ basename = "#{basename.chomp(File.extname(basename))}.#{extname}"
130
+ end
131
+ basename
132
+ end
133
+
134
+ # Constructs a filepath using the dir, name, and the specified paths.
135
+ #
136
+ # t = FileTask.new
137
+ # t.app[:data, true] = "/data"
138
+ # t.name # => "tap/file_task"
139
+ # t.filepath(:data, "result.txt") # => "/data/tap/file_task/result.txt"
140
+ #
141
+ def filepath(dir, *paths)
142
+ app.filepath(dir, name, *paths)
143
+ end
144
+
145
+ # Makes a backup filepath relative to backup_dir by using self.name, the
146
+ # basename of filepath plus a timestamp.
147
+ #
148
+ # t = FileTask.new({:timestamp => "%Y%m%d"}, 'name')
149
+ # t.app['backup', true] = "/backup"
150
+ # time = Time.utc(2008,8,8)
151
+ #
152
+ # t.backup_filepath("path/to/file.txt", time) # => "/backup/name/file_20080808.txt"
153
+ #
154
+ def backup_filepath(filepath, time=Time.now)
155
+ extname = File.extname(filepath)
156
+ backup_path = "#{File.basename(filepath).chomp(extname)}_#{time.strftime(timestamp)}#{extname}"
157
+ filepath(backup_dir, backup_path)
158
+ end
159
+
160
+ # Returns true if all of the targets are up to date relative to all of the listed
161
+ # sources AND date_reference. Single values or arrays can be provided for both
162
+ # targets and sources.
163
+ #
164
+ #---
165
+ # Returns false (ie 'not up to date') if +force?+ is true.
166
+ def uptodate?(targets, sources=[])
167
+ if app.force
168
+ log_basename(:force, *targets)
169
+ false
170
+ else
171
+ targets = [targets] unless targets.kind_of?(Array)
172
+ sources = [sources] unless sources.kind_of?(Array)
173
+
174
+ # should be able to specify this somehow, externally set
175
+ # sources << config_file unless config_file == nil
176
+
177
+ targets.each do |target|
178
+ return false unless FileUtils.uptodate?(target, sources)
179
+ end
180
+ true
181
+ end
182
+ end
183
+
184
+ # Makes a backup of each file in list to backup_filepath(file) and registers
185
+ # the files so that they can be restored using restore. If backup_using_copy
186
+ # is true, the files will be copied to backup_filepath, otherwise the file is
187
+ # moved to backup_filepath. Raises an error if the file is already listed
188
+ # in backed_up_files.
189
+ #
190
+ # Returns a list of the backup_filepaths.
191
+ #
192
+ # file = "file.txt"
193
+ # File.open(file, "w") {|f| f << "file content"}
194
+ #
195
+ # t = FileTask.new
196
+ # backed_up_file = t.backup(file).first
197
+ #
198
+ # File.exists?(file) # => false
199
+ # File.exists?(backed_up_file) # => true
200
+ # File.read(backed_up_file) # => "file content"
201
+ #
202
+ # File.open(file, "w") {|f| f << "new content"}
203
+ # t.restore(file)
204
+ #
205
+ # File.exists?(file) # => true
206
+ # File.exists?(backed_up_file) # => false
207
+ # File.read(file) # => "file content"
208
+ #
209
+ def backup(list, backup_using_copy=false)
210
+ fu_list(list).collect do |filepath|
211
+ next unless File.exists?(filepath)
212
+
213
+ filepath = File.expand_path(filepath)
214
+ if backed_up_files.include?(filepath)
215
+ raise "Backup for #{filepath} already exists."
216
+ end
217
+
218
+ target = File.expand_path(backup_filepath(filepath))
219
+ dir = File.dirname(target)
220
+ mkdir(dir)
221
+
222
+ if backup_using_copy
223
+ log :cp, "#{filepath} to #{target}", Logger::DEBUG
224
+ FileUtils.cp(filepath, target)
225
+ else
226
+ log :mv, "#{filepath} to #{target}", Logger::DEBUG
227
+ FileUtils.mv(filepath, target)
228
+ end
229
+
230
+ # track the target for restores
231
+ backed_up_files[filepath] = target
232
+ target
233
+ end
234
+ end
235
+
236
+ # Restores each file in the input list using the backup file from
237
+ # backed_up_files. The backup directory is removed if it is empty.
238
+ #
239
+ # Returns a list of the restored files.
240
+ #
241
+ # file = "file.txt"
242
+ # File.open(file, "w") {|f| f << "file content"}
243
+ #
244
+ # t = FileTask.new
245
+ # backed_up_file = t.backup(file).first
246
+ #
247
+ # File.exists?(file) # => true
248
+ # File.exists?(backed_up_file) # => true
249
+ # File.read(backed_up_file) # => "file content"
250
+ #
251
+ # File.open(file, "w") {|f| f << "new content"}
252
+ # t.restore(file)
253
+ #
254
+ # File.exists?(file) # => true
255
+ # File.exists?(backed_up_file) # => false
256
+ # File.read(file) # => "file content"
257
+ #
258
+ def restore(list)
259
+ fu_list(list).collect do |filepath|
260
+ filepath = File.expand_path(filepath)
261
+ next unless backed_up_files.has_key?(filepath)
262
+
263
+ target = backed_up_files.delete(filepath)
264
+
265
+ dir = File.dirname(filepath)
266
+ mkdir(dir)
267
+
268
+ log :restore, "#{target} to #{filepath}", Logger::DEBUG
269
+ FileUtils.mv(target, filepath, :force => true)
270
+
271
+ dir = File.dirname(target)
272
+ rmdir(dir)
273
+
274
+ filepath
275
+ end.compact
276
+ end
277
+
278
+ # Creates the directories in list if they do not exist and adds
279
+ # them to added_files so they can be removed using rmdir. Creating
280
+ # directories in this way causes them to be rolled back upon an
281
+ # execution error.
282
+ #
283
+ # Returns the made directories.
284
+ #
285
+ # t = FileTask.new do |task, inputs|
286
+ # File.exists?("path") # => false
287
+ #
288
+ # task.mkdir("path/to/dir") # will be rolled back
289
+ # File.exists?("path/to/dir") # => true
290
+ #
291
+ # FileUtils.mkdir("path/to/another") # will not be rolled back
292
+ # File.exists?("path/to/another") # => true
293
+ #
294
+ # raise "error!"
295
+ # end
296
+ #
297
+ # begin
298
+ # t.execute(nil)
299
+ # rescue
300
+ # $!.message # => "error!"
301
+ # File.exists?("path/to/dir") # => false
302
+ # File.exists?("path/to/another") # => true
303
+ # end
304
+ #
305
+ def mkdir(list)
306
+ fu_list(list).each do |dir|
307
+ dir = File.expand_path(dir)
308
+
309
+ make_paths = []
310
+ while !File.exists?(dir)
311
+ make_paths << dir
312
+ dir = File.dirname(dir)
313
+ end
314
+
315
+ make_paths.reverse_each do |path|
316
+ log :mkdir, path, Logger::DEBUG
317
+ FileUtils.mkdir(path)
318
+ added_files << path
319
+ end
320
+ end
321
+ end
322
+
323
+ # Removes each directory in the input list, provided the directory is in
324
+ # added_files and the directory is empty. When checking if the directory
325
+ # is empty, rmdir checks for regular files and hidden files. Removed
326
+ # directories are removed from added_files.
327
+ #
328
+ # Returns a list of the removed directories.
329
+ #
330
+ # t = FileTask.new
331
+ # File.exists?("path") # => false
332
+ # FileUtils.mkdir("path") # will not be removed
333
+ #
334
+ # t.mkdir("path/to/dir")
335
+ # File.exists?("path/to/dir") # => true
336
+ #
337
+ # t.rmdir("path/to/dir")
338
+ # File.exists?("path") # => true
339
+ # File.exists?("path/to") # => false
340
+ def rmdir(list)
341
+ removed = []
342
+ fu_list(list).each do |dir|
343
+ dir = File.expand_path(dir)
344
+
345
+ # remove directories and parents until the
346
+ # directory was not made by the task
347
+ while added_files.include?(dir)
348
+ break unless dir_empty?(dir)
349
+
350
+ if File.exists?(dir)
351
+ log :rmdir, dir, Logger::DEBUG
352
+ FileUtils.rmdir(dir)
353
+ end
354
+
355
+ removed << added_files.delete(dir)
356
+ dir = File.dirname(dir)
357
+ end
358
+ end
359
+ removed
360
+ end
361
+
362
+ def dir_empty?(dir)
363
+ Dir.entries(dir).delete_if {|d| d == "." || d == ".."}.empty?
364
+ end
365
+
366
+ # Prepares the input list of files by backing them up (if they exist),
367
+ # ensuring that the parent directory for the file exists, and adding
368
+ # each file to added_files. As a result the files can be removed
369
+ # using rm, restored using restore, and will be rolled back upon an
370
+ # execution error.
371
+ #
372
+ # Returns the prepared files.
373
+ #
374
+ # File.open("file.txt", "w") {|f| f << "original content"}
375
+ #
376
+ # t = FileTask.new do |task, inputs|
377
+ # File.exists?("path") # => false
378
+ #
379
+ # # backup... make parent dirs... prepare for restore
380
+ # task.prepare(["file.txt", "path/to/file.txt"])
381
+ #
382
+ # File.open("file.txt", "w") {|f| f << "new content"}
383
+ # File.touch("path/to/file.txt")
384
+ #
385
+ # raise "error!"
386
+ # end
387
+ #
388
+ # begin
389
+ # t.execute(nil)
390
+ # rescue
391
+ # $!.message # => "error!"
392
+ # File.exists?("file.txt") # => true
393
+ # File.read("file.txt") # => "original content"
394
+ # File.exists?("path") # => false
395
+ # end
396
+ #
397
+ def prepare(list, backup_using_copy=false)
398
+ list = fu_list(list)
399
+ existing_files, non_existant_files = list.partition do |filepath|
400
+ File.exists?(filepath)
401
+ end
402
+
403
+ # backup existing files
404
+ existing_files.each do |filepath|
405
+ backup(filepath, backup_using_copy)
406
+ end
407
+
408
+ # ensure the parent directory exists
409
+ # for non-existant files
410
+ non_existant_files.each do |filepath|
411
+ dir = File.dirname(filepath)
412
+ mkdir(dir)
413
+ end
414
+
415
+ list.each do |filepath|
416
+ added_files << File.expand_path(filepath)
417
+ end
418
+
419
+ list
420
+ end
421
+
422
+ # Removes each file in the input list, provided the file is in added_files.
423
+ # The parent directory of each file is removed using rmdir. Removed files
424
+ # are removed from added_files.
425
+ #
426
+ # Returns the removed files and directories.
427
+ #
428
+ # t = FileTask.new
429
+ # File.exists?("path") # => false
430
+ # FileUtils.mkdir("path") # will not be removed
431
+ #
432
+ # t.prepare("path/to/file.txt")
433
+ # FileUtils.touch("path/to/file.txt")
434
+ # File.exists?("path/to/file.txt") # => true
435
+ #
436
+ # t.rm("path/to/file.txt")
437
+ # File.exists?("path") # => true
438
+ # File.exists?("path/to") # => false
439
+ def rm(list)
440
+ removed = []
441
+ fu_list(list).each do |filepath|
442
+ filepath = File.expand_path(filepath)
443
+ next unless added_files.include?(filepath)
444
+
445
+ # if the file exists, remove it
446
+ if File.exists?(filepath)
447
+ log :rm, filepath, Logger::DEBUG
448
+ FileUtils.rm(filepath, :force => true)
449
+ end
450
+
451
+ removed << added_files.delete(filepath)
452
+ removed.concat rmdir(File.dirname(filepath))
453
+ end
454
+ removed
455
+ end
456
+
457
+ # Rolls back changes by removing added_files and restoring backed_up_files.
458
+ # Rollback is performed on an execute error if rollback_on_error == true,
459
+ # but is provided as a separate method for flexibility when needed.
460
+ # Yields errors to the block, which must be provided.
461
+ def rollback # :yields: error
462
+ added_files.dup.each do |filepath|
463
+ begin
464
+ case
465
+ when File.file?(filepath)
466
+ rm(filepath)
467
+ when File.directory?(filepath)
468
+ rmdir(filepath)
469
+ else
470
+ # assures non-existant files are cleared from added_files
471
+ # this is automatically done by rm and rmdir for existing files
472
+ added_files.delete(filepath)
473
+ end
474
+ rescue
475
+ yield $!
476
+ end
477
+ end
478
+
479
+ backed_up_files.keys.each do |filepath|
480
+ begin
481
+ restore(filepath)
482
+ rescue
483
+ yield $!
484
+ end
485
+ end
486
+ end
487
+
488
+ # Removes backed-up files matching the pattern.
489
+ def cleanup(pattern=/.*/)
490
+ backed_up_files.each do |filepath, target|
491
+ next unless target =~ pattern
492
+
493
+ # the filepath needs to be added to added_files
494
+ # before it can be removed by rm
495
+ added_files << target
496
+ rm(target)
497
+ backed_up_files.delete(filepath)
498
+ end
499
+ end
500
+
501
+ # Logs the given action, with the basenames of the input filepaths.
502
+ def log_basename(action, filepaths, level=Logger::INFO)
503
+ msg = case filepaths
504
+ when Array then filepaths.collect {|filepath| File.basename(filepath) }.join(',')
505
+ else
506
+ File.basename(filepaths)
507
+ end
508
+
509
+ log(action, msg, level)
510
+ end
511
+
512
+ protected
513
+
514
+ attr_writer :backed_up_files, :added_files
515
+
516
+ # Clears added_files and backed_up_files so that
517
+ # a failure will not affect previous executions
518
+ def before_execute
519
+ added_files.clear
520
+ backed_up_files.clear
521
+ end
522
+
523
+ # Removes made files/dirs and restores backed-up files upon
524
+ # an execute error. Collects any errors raised along the way
525
+ # and raises them in a Tap::Support::RunError.
526
+ def on_execute_error(original_error)
527
+ rollback_errors = []
528
+ if rollback_on_error
529
+ rollback {|error| rollback_errors << error}
530
+ end
531
+
532
+ # Re-raise the error if no rollback errors occured,
533
+ # otherwise, raise a RunError tracking the errors.
534
+ if rollback_errors.empty?
535
+ raise original_error
536
+ else
537
+ rollback_errors.unshift(original_error)
538
+ raise Support::RunError.new(rollback_errors)
539
+ end
540
+ end
541
+
542
+ # Lifted from FileUtils
543
+ def fu_list(arg)
544
+ [arg].flatten.map {|path| path.to_str }
545
+ end
546
+ end
547
+ end