scout-gear 2.0.0 → 5.1.1

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 (91) hide show
  1. checksums.yaml +4 -4
  2. data/.vimproject +59 -2
  3. data/VERSION +1 -1
  4. data/bin/scout +231 -24
  5. data/lib/scout/cmd.rb +344 -0
  6. data/lib/scout/concurrent_stream.rb +259 -0
  7. data/lib/scout/exceptions.rb +15 -8
  8. data/lib/scout/indiferent_hash/options.rb +8 -26
  9. data/lib/scout/log/color.rb +2 -2
  10. data/lib/scout/log/fingerprint.rb +11 -1
  11. data/lib/scout/log/progress/report.rb +0 -1
  12. data/lib/scout/log/progress/util.rb +1 -1
  13. data/lib/scout/log/progress.rb +4 -4
  14. data/lib/scout/log.rb +10 -2
  15. data/lib/scout/meta_extension.rb +15 -1
  16. data/lib/scout/misc/digest.rb +56 -0
  17. data/lib/scout/misc/filesystem.rb +26 -0
  18. data/lib/scout/misc/format.rb +1 -2
  19. data/lib/scout/misc/insist.rb +56 -0
  20. data/lib/scout/misc.rb +4 -11
  21. data/lib/scout/open/lock.rb +61 -0
  22. data/lib/scout/open/remote.rb +120 -0
  23. data/lib/scout/open/stream.rb +372 -0
  24. data/lib/scout/open/util.rb +225 -0
  25. data/lib/scout/open.rb +169 -0
  26. data/lib/scout/path/find.rb +67 -21
  27. data/lib/scout/path/tmpfile.rb +8 -0
  28. data/lib/scout/path/util.rb +14 -1
  29. data/lib/scout/path.rb +6 -30
  30. data/lib/scout/persist/open.rb +17 -0
  31. data/lib/scout/persist/path.rb +15 -0
  32. data/lib/scout/persist/serialize.rb +140 -0
  33. data/lib/scout/persist.rb +54 -0
  34. data/lib/scout/resource/path.rb +15 -0
  35. data/lib/scout/resource/produce/rake.rb +69 -0
  36. data/lib/scout/resource/produce.rb +246 -0
  37. data/lib/scout/resource/scout.rb +3 -0
  38. data/lib/scout/resource.rb +37 -0
  39. data/lib/scout/simple_opt/accessor.rb +1 -1
  40. data/lib/scout/simple_opt/doc.rb +4 -22
  41. data/lib/scout/simple_opt/parse.rb +4 -3
  42. data/lib/scout/tmpfile.rb +39 -1
  43. data/lib/scout/workflow/definition.rb +72 -0
  44. data/lib/scout/workflow/documentation.rb +77 -0
  45. data/lib/scout/workflow/step/info.rb +77 -0
  46. data/lib/scout/workflow/step.rb +96 -0
  47. data/lib/scout/workflow/task/inputs.rb +112 -0
  48. data/lib/scout/workflow/task.rb +141 -0
  49. data/lib/scout/workflow/usage.rb +294 -0
  50. data/lib/scout/workflow/util.rb +11 -0
  51. data/lib/scout/workflow.rb +39 -0
  52. data/lib/scout-gear.rb +4 -0
  53. data/lib/scout.rb +1 -0
  54. data/lib/workflow-scout.rb +2 -0
  55. data/scout-gear.gemspec +66 -5
  56. data/scout_commands/alias +48 -0
  57. data/scout_commands/find +83 -0
  58. data/scout_commands/glob +0 -0
  59. data/scout_commands/rbbt +23 -0
  60. data/scout_commands/workflow/task +707 -0
  61. data/test/scout/indiferent_hash/test_options.rb +11 -1
  62. data/test/scout/misc/test_digest.rb +30 -0
  63. data/test/scout/misc/test_filesystem.rb +30 -0
  64. data/test/scout/misc/test_insist.rb +13 -0
  65. data/test/scout/open/test_lock.rb +52 -0
  66. data/test/scout/open/test_remote.rb +25 -0
  67. data/test/scout/open/test_stream.rb +515 -0
  68. data/test/scout/open/test_util.rb +73 -0
  69. data/test/scout/path/test_find.rb +28 -0
  70. data/test/scout/persist/test_open.rb +37 -0
  71. data/test/scout/persist/test_path.rb +37 -0
  72. data/test/scout/persist/test_serialize.rb +114 -0
  73. data/test/scout/resource/test_path.rb +40 -0
  74. data/test/scout/resource/test_produce.rb +62 -0
  75. data/test/scout/test_cmd.rb +85 -0
  76. data/test/scout/test_concurrent_stream.rb +29 -0
  77. data/test/scout/test_misc.rb +0 -7
  78. data/test/scout/test_open.rb +146 -0
  79. data/test/scout/test_path.rb +3 -1
  80. data/test/scout/test_persist.rb +83 -0
  81. data/test/scout/test_resource.rb +26 -0
  82. data/test/scout/test_workflow.rb +87 -0
  83. data/test/scout/workflow/step/test_info.rb +28 -0
  84. data/test/scout/workflow/task/test_inputs.rb +182 -0
  85. data/test/scout/workflow/test_step.rb +36 -0
  86. data/test/scout/workflow/test_task.rb +178 -0
  87. data/test/scout/workflow/test_usage.rb +26 -0
  88. data/test/scout/workflow/test_util.rb +17 -0
  89. data/test/test_helper.rb +17 -0
  90. data/test/test_scout-gear.rb +0 -0
  91. metadata +64 -3
@@ -0,0 +1,372 @@
1
+ module Open
2
+ BLOCK_SIZE = 1024
3
+
4
+ class << self
5
+ attr_accessor :sensible_write_lock_dir
6
+
7
+ def sensible_write_lock_dir
8
+ @sensible_write_lock_dir ||= Path.setup("tmp/sensible_write_locks").find
9
+ end
10
+ end
11
+
12
+ class << self
13
+ attr_accessor :sensible_write_dir
14
+ def sensible_write_dir
15
+ @sensible_write_dir ||= Path.setup("tmp/sensible_write").find
16
+ end
17
+ end
18
+
19
+ def self.consume_stream(io, in_thread = false, into = nil, into_close = true, &block)
20
+ return if Path === io
21
+ return unless io.respond_to? :read
22
+
23
+ if io.respond_to? :closed? and io.closed?
24
+ io.join if io.respond_to? :join
25
+ return
26
+ end
27
+
28
+ if in_thread
29
+ consumer_thread = Thread.new(Thread.current) do |parent|
30
+ Thread.current["name"] = "Consumer #{Log.fingerprint io}"
31
+ Thread.current.report_on_exception = false
32
+ consume_stream(io, false, into, into_close)
33
+ end
34
+ io.threads.push(consumer_thread) if io.respond_to?(:threads)
35
+ consumer_thread
36
+ else
37
+ if into
38
+ Log.medium "Consuming stream #{Log.fingerprint io} -> #{Log.fingerprint into}"
39
+ else
40
+ Log.medium "Consuming stream #{Log.fingerprint io}"
41
+ end
42
+
43
+ begin
44
+ into = into.find if Path === into
45
+
46
+ if String === into
47
+ dir = File.dirname(into)
48
+ Open.mkdir dir unless File.exist?(dir)
49
+ into_path, into = into, File.open(into, 'w')
50
+ end
51
+
52
+ into.sync = true if IO === into
53
+ into_close = false unless into.respond_to? :close
54
+ io.sync = true
55
+
56
+ Log.high "started consuming stream #{Log.fingerprint io}"
57
+ begin
58
+ while c = io.readpartial(BLOCK_SIZE)
59
+ into << c if into
60
+ end
61
+ rescue EOFError
62
+ end
63
+
64
+ io.join if io.respond_to? :join
65
+ io.close unless io.closed?
66
+ into.join if into and into_close and into.respond_to?(:joined?) and not into.joined?
67
+ into.close if into and into_close and not into.closed?
68
+ block.call if block_given?
69
+
70
+ Log.high "Done consuming stream #{Log.fingerprint io} into #{into_path || into}"
71
+ rescue Aborted
72
+ Log.high "Consume stream Aborted #{Log.fingerprint io} into #{into_path || into}"
73
+ io.abort $! if io.respond_to? :abort
74
+ into.close if into.respond_to?(:closed?) && ! into.closed?
75
+ FileUtils.rm into_path if into_path and File.exist?(into_path)
76
+ rescue Exception
77
+ Log.high "Consume stream Exception reading #{Log.fingerprint io} into #{into_path || into} - #{$!.message}"
78
+ exception = io.stream_exception || $!
79
+ io.abort exception if io.respond_to? :abort
80
+ into.close if into.respond_to?(:closed?) && ! into.closed?
81
+ into_path = into if into_path.nil? && String === into
82
+ if into_path and File.exist?(into_path)
83
+ FileUtils.rm into_path
84
+ end
85
+ raise exception
86
+ end
87
+ end
88
+ end
89
+
90
+ def self.sensible_write(path, content = nil, options = {}, &block)
91
+ force = IndiferentHash.process_options options, :force
92
+
93
+ if File.exist?(path) and not force
94
+ Open.consume_stream content
95
+ return
96
+ end
97
+
98
+ lock_options = IndiferentHash.pull_keys options.dup, :lock
99
+ lock_options = lock_options[:lock] if Hash === lock_options[:lock]
100
+ tmp_path = TmpFile.tmp_for_file(path, {:dir => Open.sensible_write_dir})
101
+ tmp_path_lock = TmpFile.tmp_for_file(path, {:dir => Open.sensible_write_lock_dir})
102
+
103
+ tmp_path_lock = nil if FalseClass === options[:lock]
104
+
105
+ Open.lock tmp_path_lock, lock_options do
106
+
107
+ if File.exist? path and not force
108
+ Log.warn "Path exists in sensible_write, not forcing update: #{ path }"
109
+ Open.consume_stream content
110
+ else
111
+ FileUtils.mkdir_p File.dirname(tmp_path) unless File.directory? File.dirname(tmp_path)
112
+ FileUtils.rm_f tmp_path if File.exist? tmp_path
113
+ begin
114
+
115
+ case
116
+ when block_given?
117
+ File.open(tmp_path, 'wb', &block)
118
+ when String === content
119
+ File.open(tmp_path, 'wb') do |f| f.write content end
120
+ when (IO === content or StringIO === content or File === content)
121
+ Open.write(tmp_path) do |f|
122
+ f.sync = true
123
+ begin
124
+ while block = content.readpartial(BLOCK_SIZE)
125
+ f.write block
126
+ end
127
+ rescue EOFError
128
+ end
129
+ end
130
+ else
131
+ File.open(tmp_path, 'wb') do |f| end
132
+ end
133
+
134
+ begin
135
+ Misc.insist do
136
+ Open.mv tmp_path, path, lock_options
137
+ end
138
+ rescue Exception
139
+ raise $! unless File.exist? path
140
+ end
141
+
142
+ Open.touch path if File.exist? path
143
+ content.join if content.respond_to?(:join) and not Path === content and not (content.respond_to?(:joined?) && content.joined?)
144
+
145
+ Open.notify_write(path)
146
+ rescue Aborted
147
+ Log.medium "Aborted sensible_write -- #{ Log.reset << Log.color(:blue, path) }"
148
+ content.abort if content.respond_to? :abort
149
+ Open.rm path if File.exist? path
150
+ rescue Exception
151
+ exception = (AbortedStream === content and content.exception) ? content.exception : $!
152
+ Log.medium "Exception in sensible_write: [#{Process.pid}] #{exception.message} -- #{ Log.color :blue, path }"
153
+ content.abort if content.respond_to? :abort
154
+ Open.rm path if File.exist? path
155
+ raise exception
156
+ rescue
157
+ raise $!
158
+ ensure
159
+ FileUtils.rm_f tmp_path if File.exist? tmp_path
160
+ if Lockfile === lock_options[:lock] and lock_options[:lock].locked?
161
+ lock_options[:lock].unlock
162
+ end
163
+ end
164
+ end
165
+ end
166
+ end
167
+
168
+ PIPE_MUTEX = Mutex.new
169
+
170
+ OPEN_PIPE_IN = []
171
+ def self.pipe
172
+ OPEN_PIPE_IN.delete_if{|pipe| pipe.closed? }
173
+ res = PIPE_MUTEX.synchronize do
174
+ sout, sin = IO.pipe
175
+ OPEN_PIPE_IN << sin
176
+
177
+ [sout, sin]
178
+ end
179
+ Log.debug{"Creating pipe #{[Log.fingerprint(res.last), Log.fingerprint(res.first)] * " => "}"}
180
+ res
181
+ end
182
+
183
+ def self.with_fifo(path = nil, clean = true, &block)
184
+ begin
185
+ erase = path.nil?
186
+ path = TmpFile.tmp_file if path.nil?
187
+ File.rm path if clean && File.exist?(path)
188
+ File.mkfifo path
189
+ yield path
190
+ ensure
191
+ FileUtils.rm path if erase && File.exist?(path)
192
+ end
193
+ end
194
+
195
+ def self.release_pipes(*pipes)
196
+ PIPE_MUTEX.synchronize do
197
+ pipes.flatten.each do |pipe|
198
+ pipe.close unless pipe.closed?
199
+ end
200
+ end
201
+ end
202
+
203
+ def self.purge_pipes(*save)
204
+ PIPE_MUTEX.synchronize do
205
+ OPEN_PIPE_IN.each do |pipe|
206
+ next if save.include? pipe
207
+ pipe.close unless pipe.closed?
208
+ end
209
+ end
210
+ end
211
+
212
+ def self.open_pipe(do_fork = false, close = true)
213
+ raise "No block given" unless block_given?
214
+
215
+ sout, sin = Open.pipe
216
+
217
+ if do_fork
218
+
219
+ #parent_pid = Process.pid
220
+ pid = Process.fork {
221
+ purge_pipes(sin)
222
+ sout.close
223
+ begin
224
+
225
+ yield sin
226
+ sin.close if close and not sin.closed?
227
+
228
+ rescue Exception
229
+ Log.exception $!
230
+ #Process.kill :INT, parent_pid
231
+ Kernel.exit!(-1)
232
+ end
233
+ Kernel.exit! 0
234
+ }
235
+ sin.close
236
+
237
+ ConcurrentStream.setup sout, :pids => [pid]
238
+ else
239
+
240
+ ConcurrentStream.setup sin, :pair => sout
241
+ ConcurrentStream.setup sout, :pair => sin
242
+
243
+ thread = Thread.new do
244
+ Thread.current["name"] = "Pipe input #{Log.fingerprint sin} => #{Log.fingerprint sout}"
245
+ Thread.current.report_on_exception = false
246
+ begin
247
+
248
+ yield sin
249
+
250
+ sin.close if close and not sin.closed? and not sin.aborted?
251
+ rescue Aborted
252
+ Log.medium "Aborted open_pipe: #{$!.message}"
253
+ raise $!
254
+ rescue Exception
255
+ Log.medium "Exception in open_pipe: #{$!.message}"
256
+ begin
257
+ sout.threads.delete(Thread.current)
258
+ sout.pair = []
259
+ sout.abort($!) if sout.respond_to?(:abort)
260
+ sin.threads.delete(Thread.current)
261
+ sin.pair = []
262
+ sin.abort($!) if sin.respond_to?(:abort)
263
+ ensure
264
+ raise $!
265
+ end
266
+ end
267
+ end
268
+
269
+ sin.threads = [thread]
270
+ sout.threads = [thread]
271
+ end
272
+
273
+ sout
274
+ end
275
+
276
+ def self.tee_stream_thread_multiple(stream, num = 2)
277
+ in_pipes = []
278
+ out_pipes = []
279
+ num.times do
280
+ sout, sin = Open.pipe
281
+ in_pipes << sin
282
+ out_pipes << sout
283
+ end
284
+
285
+ filename = stream.filename if stream.respond_to? :filename
286
+
287
+ splitter_thread = Thread.new(Thread.current) do |parent|
288
+ begin
289
+ Thread.current["name"] = "Splitter #{Log.fingerprint stream}"
290
+ Thread.current.report_on_exception = false
291
+
292
+ skip = [false] * num
293
+ begin
294
+ while block = stream.readpartial(BLOCK_SIZE)
295
+
296
+ in_pipes.each_with_index do |sin,i|
297
+ begin
298
+ sin.write block
299
+ rescue IOError
300
+ Log.warn("Tee stream #{i} #{Log.fingerprint stream} IOError: #{$!.message} (#{Log.fingerprint sin})");
301
+ skip[i] = true
302
+ rescue
303
+ Log.warn("Tee stream #{i} #{Log.fingerprint stream} Exception: #{$!.message} (#{Log.fingerprint sin})");
304
+ raise $!
305
+ end unless skip[i]
306
+ end
307
+ end
308
+ rescue IOError
309
+ end
310
+
311
+ stream.join if stream.respond_to? :join
312
+ stream.close unless stream.closed?
313
+ in_pipes.first.close unless in_pipes.first.closed?
314
+ rescue Aborted, Interrupt
315
+ stream.abort if stream.respond_to? :abort
316
+ out_pipes.each do |sout|
317
+ sout.abort if sout.respond_to? :abort
318
+ end
319
+ Log.medium "Tee aborting #{Log.fingerprint stream}"
320
+ raise $!
321
+ rescue Exception
322
+ begin
323
+ stream.abort($!) if stream.respond_to?(:abort) && ! stream.aborted?
324
+ out_pipes.reverse.each do |sout|
325
+ sout.threads.delete(Thread.current)
326
+ begin
327
+ sout.abort($!) if sout.respond_to?(:abort) && ! sout.aborted?
328
+ rescue
329
+ end
330
+ end
331
+ in_pipes.each do |sin|
332
+ sin.close unless sin.closed?
333
+ end
334
+ Log.medium "Tee exception #{Log.fingerprint stream}"
335
+ rescue
336
+ Log.exception $!
337
+ ensure
338
+ in_pipes.each do |sin|
339
+ sin.close unless sin.closed?
340
+ end
341
+ raise $!
342
+ end
343
+ end
344
+ end
345
+
346
+
347
+ out_pipes.each do |sout|
348
+ ConcurrentStream.setup sout, :threads => splitter_thread, :filename => filename, :pair => stream
349
+ end
350
+ splitter_thread.wakeup until splitter_thread["name"]
351
+
352
+ main_pipe = out_pipes.first
353
+ main_pipe.autojoin = true
354
+
355
+ main_pipe.callback = Proc.new do
356
+ stream.join if stream.respond_to? :join
357
+ in_pipes[1..-1].each do |sin|
358
+ sin.close unless sin.closed?
359
+ end
360
+ end
361
+
362
+ out_pipes
363
+ end
364
+
365
+ def self.tee_stream_thread(stream)
366
+ tee_stream_thread_multiple(stream, 2)
367
+ end
368
+
369
+ def self.tee_stream(stream)
370
+ tee_stream_thread(stream)
371
+ end
372
+ end
@@ -0,0 +1,225 @@
1
+ module Open
2
+ GREP_CMD = begin
3
+ if ENV["GREP_CMD"]
4
+ ENV["GREP_CMD"]
5
+ elsif File.exist?('/bin/grep')
6
+ "/bin/grep"
7
+ elsif File.exist?('/usr/bin/grep')
8
+ "/usr/bin/grep"
9
+ else
10
+ "grep"
11
+ end
12
+ end
13
+
14
+ def self.grep(stream, grep, invert = false, fixed = nil)
15
+ case
16
+ when Array === grep
17
+ TmpFile.with_file(grep * "\n", false) do |f|
18
+ if FalseClass === fixed
19
+ CMD.cmd("#{GREP_CMD} #{invert ? '-v' : ''} -", "-f" => f, :in => stream, :pipe => true, :post => proc{FileUtils.rm f})
20
+ else
21
+ CMD.cmd("#{GREP_CMD} #{invert ? '-v' : ''} -", "-w" => true, "-F" => true, "-f" => f, :in => stream, :pipe => true, :post => proc{FileUtils.rm f})
22
+ end
23
+ end
24
+ else
25
+ CMD.cmd("#{GREP_CMD} #{invert ? '-v ' : ''} '#{grep}' -", :in => stream, :pipe => true, :post => proc{begin stream.force_close; rescue Exception; end if stream.respond_to?(:force_close)})
26
+ end
27
+ end
28
+
29
+ def self.gzip_pipe(file)
30
+ Open.gzip?(file) ? "<(gunzip -c '#{file}')" : "'#{file}'"
31
+ end
32
+
33
+ def self.bgunzip(stream)
34
+ Bgzf.setup stream
35
+ end
36
+
37
+ def self.gunzip(stream)
38
+ CMD.cmd('zcat', :in => stream, :pipe => true, :no_fail => true, :no_wait => true)
39
+ end
40
+
41
+ def self.gzip(stream)
42
+ CMD.cmd('gzip', :in => stream, :pipe => true, :no_fail => true, :no_wait => true)
43
+ end
44
+
45
+ def self.bgzip(stream)
46
+ CMD.cmd('bgzip', :in => stream, :pipe => true, :no_fail => true, :no_wait => true)
47
+ end
48
+
49
+ def self.unzip(stream)
50
+ TmpFile.with_file(stream.read) do |filename|
51
+ StringIO.new(CMD.cmd("unzip '{opt}' #{filename}", "-p" => true, :pipe => true).read)
52
+ end
53
+ end
54
+
55
+ # Questions
56
+ def self.gzip?(file)
57
+ file = file.find if Path === file
58
+ !! (file =~ /\.gz$/)
59
+ end
60
+
61
+ def self.bgzip?(file)
62
+ file = file.find if Path === file
63
+ !! (file =~ /\.bgz$/)
64
+ end
65
+
66
+ def self.zip?(file)
67
+ file = file.find if Path === file
68
+ !! (file =~ /\.zip$/)
69
+ end
70
+
71
+ def self.notify_write(file)
72
+ begin
73
+ notification_file = file + '.notify'
74
+ if Open.exists? notification_file
75
+ key = Open.read(notification_file).strip
76
+ key = nil if key.empty?
77
+ if key && key.include?("@")
78
+ to = from = key
79
+ subject = "Wrote " << file
80
+ message = "Content attached"
81
+ Misc.send_email(from, to, subject, message, :files => [file])
82
+ else
83
+ Misc.notify("Wrote " << file, nil, key)
84
+ end
85
+ Open.rm notification_file
86
+ end
87
+ rescue
88
+ Log.exception $!
89
+ Log.warn "Error notifying write of #{ file }"
90
+ end
91
+ end
92
+
93
+ def self.broken_link?(path)
94
+ File.symlink?(path) && ! File.exist?(File.readlink(path))
95
+ end
96
+
97
+ def self.exists?(file)
98
+ file = file.find if Path === file
99
+ File.exist?(file)
100
+ end
101
+ class << self; alias exist? exists? end
102
+
103
+ def self.mv(source, target, options = {})
104
+ target = target.find if Path === target
105
+ source = source.find if Path === source
106
+ FileUtils.mkdir_p File.dirname(target) unless File.exist?(File.dirname(target))
107
+ tmp_target = File.join(File.dirname(target), '.tmp_mv.' + File.basename(target))
108
+ FileUtils.mv source, tmp_target
109
+ FileUtils.mv tmp_target, target
110
+ return nil
111
+ end
112
+
113
+ def self.rm(file)
114
+ FileUtils.rm(file) if File.exist?(file) or Open.broken_link?(file)
115
+ end
116
+
117
+ def self.rm_rf(file)
118
+ FileUtils.rm_rf(file)
119
+ end
120
+
121
+ def self.touch(file)
122
+ FileUtils.touch(file)
123
+ end
124
+
125
+ def self.mkdir(target)
126
+ target = target.find if Path === target
127
+ if ! File.exist?(target)
128
+ FileUtils.mkdir_p target
129
+ end
130
+ end
131
+
132
+ def self.writable?(path)
133
+ path = path.find if Path === path
134
+ if File.symlink?(path)
135
+ File.writable?(File.dirname(path))
136
+ elsif File.exist?(path)
137
+ File.writable?(path)
138
+ else
139
+ File.writable?(File.dirname(File.expand_path(path)))
140
+ end
141
+ end
142
+
143
+ def self.ctime(file)
144
+ file = file.find if Path === file
145
+ File.ctime(file)
146
+ end
147
+
148
+ def self.realpath(file)
149
+ file = file.find if Path === file
150
+ Pathname.new(File.expand_path(file)).realpath.to_s
151
+ end
152
+
153
+ def self.mtime(file)
154
+ file = file.find if Path === file
155
+ begin
156
+ if File.symlink?(file) || File.stat(file).nlink > 1
157
+ if File.exist?(file + '.info') && defined?(Step)
158
+ done = Step::INFO_SERIALIZER.load(Open.open(file + '.info'))[:done]
159
+ return done if done
160
+ end
161
+
162
+ file = Pathname.new(file).realpath.to_s
163
+ end
164
+ return nil unless File.exist?(file)
165
+ File.mtime(file)
166
+ rescue
167
+ nil
168
+ end
169
+ end
170
+
171
+ def self.cp(source, target, options = {})
172
+ source = source.find if Path === source
173
+ target = target.find if Path === target
174
+
175
+ FileUtils.mkdir_p File.dirname(target) unless File.exist?(File.dirname(target))
176
+ FileUtils.rm target if File.exist?(target)
177
+ FileUtils.cp_r source, target
178
+ end
179
+
180
+
181
+ def self.ln_s(source, target, options = {})
182
+ source = source.find if Path === source
183
+ target = target.find if Path === target
184
+
185
+ target = File.join(target, File.basename(source)) if File.directory? target
186
+ FileUtils.mkdir_p File.dirname(target) unless File.exist?(File.dirname(target))
187
+ FileUtils.rm target if File.exist?(target)
188
+ FileUtils.rm target if File.symlink?(target)
189
+ FileUtils.ln_s source, target
190
+ end
191
+
192
+ def self.ln(source, target, options = {})
193
+ source = source.find if Path === source
194
+ target = target.find if Path === target
195
+ source = File.realpath(source) if File.symlink?(source)
196
+
197
+ FileUtils.mkdir_p File.dirname(target) unless File.exist?(File.dirname(target))
198
+ FileUtils.rm target if File.exist?(target)
199
+ FileUtils.rm target if File.symlink?(target)
200
+ FileUtils.ln source, target
201
+ end
202
+
203
+ def self.ln_h(source, target, options = {})
204
+ source = source.find if Path === source
205
+ target = target.find if Path === target
206
+
207
+ FileUtils.mkdir_p File.dirname(target) unless File.exist?(File.dirname(target))
208
+ FileUtils.rm target if File.exist?(target)
209
+ begin
210
+ CMD.cmd("ln -L '#{ source }' '#{ target }'")
211
+ rescue ProcessFailed
212
+ Log.debug "Could not hard link #{source} and #{target}: #{$!.message.gsub("\n", '. ')}"
213
+ CMD.cmd("cp -L '#{ source }' '#{ target }'")
214
+ end
215
+ end
216
+
217
+ def self.link(source, target, options = {})
218
+ begin
219
+ Open.ln(source, target, options)
220
+ rescue
221
+ Open.ln_s(source, target, options)
222
+ end
223
+ nil
224
+ end
225
+ end