buildr 1.1.3 → 1.2.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.
@@ -0,0 +1,65 @@
1
+ module Buildr
2
+ module Generate #:nodoc:
3
+
4
+ task "generate" do
5
+ script = Generate.from_directory(true).join("\n")
6
+ buildfile = File.expand_path("buildfile")
7
+ File.open(buildfile, "w") { |file| file.write script }
8
+ puts "Created #{buildfile}"
9
+ end
10
+
11
+ class << self
12
+
13
+ def from_directory(root = false)
14
+ name = File.basename(Dir.pwd)
15
+ if root
16
+ header = <<-EOF
17
+ # Generated by Buildr #{Buildr::VERSION}, change to your liking
18
+
19
+ # Version number for this release
20
+ VERSION_NUMBER = "1.0.0"
21
+ # Version number for the next release
22
+ NEXT_VERSION = "1.0.1"
23
+ # Group identifier for your projects
24
+ GROUP = "#{name}"
25
+ COPYRIGHT = ""
26
+
27
+ # Specify Maven 2.0 remote repositories here, like this:
28
+ repositories.remote << "http://www.ibiblio.org/maven2/"
29
+
30
+ desc "The #{name.capitalize} project"
31
+ define "#{name}" do
32
+
33
+ project.version = VERSION_NUMBER
34
+ project.group = GROUP
35
+ manifest["Implementation-Vendor"] = COPYRIGHT
36
+ EOF
37
+ script = header.split("\n")
38
+ else
39
+ script = [ %{define "#{name}" do} ]
40
+ end
41
+ script << " compile.with # Add classpath dependencies" if File.exist?("src/main/java")
42
+ script << " resources" if File.exist?("src/main/resources")
43
+ script << " test.compile.with # Add classpath dependencies" if File.exist?("src/test/java")
44
+ script << " test.resources" if File.exist?("src/test/resources")
45
+ if File.exist?("src/main/webapp")
46
+ script << " package(:war)"
47
+ elsif File.exist?("src/main/java")
48
+ script << " package(:jar)"
49
+ end
50
+ dirs = FileList["*"].exclude("src", "target", "report").
51
+ select { |file| File.directory?(file) && File.exist?(File.join(file, "src")) }
52
+ unless dirs.empty?
53
+ script << ""
54
+ dirs.sort.each do |dir|
55
+ Dir.chdir(dir) { script << from_directory.flatten.map { |line| " " + line } << "" }
56
+ end
57
+ end
58
+ script << "end"
59
+ script.flatten
60
+ end
61
+
62
+ end
63
+
64
+ end
65
+ end
@@ -0,0 +1,72 @@
1
+ require "core/common"
2
+ require "core/project"
3
+
4
+
5
+ task "help" do
6
+ # Greeater.
7
+ Rake.application.usage
8
+ puts
9
+
10
+ # Show only the top-level projects.
11
+ projects.reject(&:parent).tap do |top_level|
12
+ unless top_level.empty?
13
+ puts "Top-level projects (buildr help:projects for full list):"
14
+ width = [top_level.map(&:name).map(&:size), 20].flatten.max
15
+ top_level.each do |project|
16
+ puts project.comment.blank? ? project.name : (" %-#{width}s # %s" % [project.name, project.comment])
17
+ end
18
+ puts
19
+ end
20
+ end
21
+
22
+ # Show all the top-level tasks, excluding projects.
23
+ puts "Common tasks:"
24
+ task("help:tasks").invoke
25
+ puts
26
+ puts "For help on command line options:"
27
+ puts " buildr --help"
28
+ end
29
+
30
+
31
+ module Buildr
32
+
33
+ # :call-seq:
34
+ # help() { ... }
35
+ #
36
+ # Use this to enhance the help task, e.g. to print some important information about your build,
37
+ # configuration options, build instructions, etc.
38
+ def help(&block)
39
+ Rake.application["help"].enhance &block
40
+ end
41
+
42
+ end
43
+
44
+
45
+ namespace "help" do
46
+
47
+ desc "List all projects defined by this buildfile"
48
+ task "projects" do
49
+ width = projects.map(&:name).map(&:size).max
50
+ projects.each do |project|
51
+ puts project.comment.blank? ? " #{project.name}" : (" %-#{width}s # %s" % [project.name, project.comment])
52
+ end
53
+ end
54
+
55
+ desc "List all tasks available from this buildfile"
56
+ task "tasks" do
57
+ Rake.application.tasks.select(&:comment).reject { |task| Project === task }.tap do |tasks|
58
+ width = [tasks.map(&:name).map(&:size), 20].flatten.max
59
+ tasks.each do |task|
60
+ printf " %-#{width}s # %s\n", task.name, task.comment
61
+ end
62
+ puts
63
+ end
64
+ end
65
+
66
+ end
67
+
68
+
69
+ task "projects" do
70
+ warn_deprecated "Please run help:projects instead."
71
+ task("help:projects").invoke
72
+ end
@@ -1,4 +1,5 @@
1
1
  require "core/rake_ext"
2
+ require "core/common"
2
3
 
3
4
  module Buildr
4
5
 
@@ -65,14 +66,14 @@ module Buildr
65
66
  # recursive, compiling foo will also compile foo:bar.
66
67
  #
67
68
  # If you run:
68
- # rake compile
69
+ # buildr compile
69
70
  # from the command line, it will execute the compile task of the current project.
70
71
  #
71
- # Projects and sub-projects follow a directory heirarchy. The Rakefile is assumed to
72
+ # Projects and sub-projects follow a directory heirarchy. The Buildfile is assumed to
72
73
  # reside in the same directory as the top-level project, and each sub-project is
73
74
  # contained in a sub-directory in the same name. For example:
74
75
  # /home/foo
75
- # |__ Rakefile
76
+ # |__ Buildfile
76
77
  # |__ src/main/java
77
78
  # |__ foo
78
79
  # |__ src/main/java
@@ -185,12 +186,13 @@ module Buildr
185
186
  options = names.pop if Hash === names.last
186
187
  rake_check_options options, :scope if options
187
188
  @projects ||= {}
189
+ names = names.flatten
188
190
  if options && options[:scope]
189
191
  # We assume parent project is evaluated.
190
192
  if names.empty?
191
- parent = @projects[options[:scope]] or raise "No such project #{options[:scope]}"
192
- @projects.values.select { |project| project.parent == parent }.
193
- each { |project| project.invoke }.sort_by(&:name)
193
+ parent = @projects[options[:scope].to_s] or raise "No such project #{options[:scope]}"
194
+ @projects.values.select { |project| project.parent == parent }.each { |project| project.invoke }.
195
+ map { |project| [project] + projects(:scope=>project) }.flatten.sort_by(&:name)
194
196
  else
195
197
  names.uniq.map { |name| project(name, :scope=>options[:scope]) }
196
198
  end
@@ -224,27 +226,22 @@ module Buildr
224
226
  # current directory.
225
227
  #
226
228
  # Complicated? Try this:
227
- # rake build
229
+ # buildr build
228
230
  # is the same as:
229
- # rake foo:build
231
+ # buildr foo:build
230
232
  # But:
231
233
  # cd bar
232
- # rake build
234
+ # buildr build
233
235
  # is the same as:
234
- # rake foo:bar:build
236
+ # buildr foo:bar:build
235
237
  #
236
238
  # The optional block is called with the project name when the task executes
237
239
  # and returns a message that, for example "Building project #{name}".
238
240
  def local_task(args, &block)
239
241
  task args do |task|
240
- projects = local_projects
241
- if projects.empty?
242
- warn "No projects defined for directory #{Rake.application.original_dir}" if verbose
243
- else
244
- projects.each do |project|
245
- puts block.call(project.name) if block && verbose
246
- task("#{project.name}:#{task.name}").invoke
247
- end
242
+ local_projects do |project|
243
+ puts block.call(project.name) if block && verbose
244
+ task("#{project.name}:#{task.name}").invoke
248
245
  end
249
246
  end
250
247
  end
@@ -271,11 +268,17 @@ module Buildr
271
268
  task_name
272
269
  end
273
270
 
274
- def local_projects(dir = Rake.application.original_dir) #:nodoc:
275
- dir = File.expand_path(dir)
271
+ def local_projects(dir = nil, &block) #:nodoc:
272
+ dir = File.expand_path(dir || Rake.application.original_dir)
276
273
  projects = Project.projects.select { |project| project.base_dir == dir }
277
274
  if projects.empty? && dir != Dir.pwd && File.dirname(dir) != dir
278
- local_projects(File.dirname(dir))
275
+ local_projects(File.dirname(dir), &block)
276
+ elsif block
277
+ if projects.empty?
278
+ warn "No projects defined for directory #{Rake.application.original_dir}" if verbose
279
+ else
280
+ projects.each { |project| block[project] }
281
+ end
279
282
  else
280
283
  projects
281
284
  end
@@ -312,13 +315,13 @@ module Buildr
312
315
  #
313
316
  # Returns the project's base directory.
314
317
  #
315
- # The Rakefile defines top-level project, so it's logical that the top-level project's
316
- # base directory is the one in which we find the Rakefile. And each sub-project has
318
+ # The Buildfile defines top-level project, so it's logical that the top-level project's
319
+ # base directory is the one in which we find the Buildfile. And each sub-project has
317
320
  # a base directory that is one level down, with the same name as the sub-project.
318
321
  #
319
322
  # For example:
320
323
  # /home/foo/ <-- base_directory of project "foo"
321
- # /home/foo/Rakefile <-- builds "foo"
324
+ # /home/foo/Buildfile <-- builds "foo"
322
325
  # /home/foo/bar <-- sub-project "foo:bar"
323
326
  def base_dir()
324
327
  if @base_dir.nil?
@@ -327,7 +330,7 @@ module Buildr
327
330
  # using the same name as the project.
328
331
  @base_dir = File.join(@parent.base_dir, name.split(":").last)
329
332
  else
330
- # For top-level project, a good default is the directory where we found the Rakefile.
333
+ # For top-level project, a good default is the directory where we found the Buildfile.
331
334
  @base_dir = Dir.pwd
332
335
  end
333
336
  end
@@ -357,7 +360,7 @@ module Buildr
357
360
  # Essentially, joins all the supplied names and expands the path relative to #base_dir.
358
361
  # Symbol arguments are converted to paths by calling the attribute accessor on the project.
359
362
  #
360
- # Keep in mind that all tasks are defined and executed relative to the Rakefile directory,
363
+ # Keep in mind that all tasks are defined and executed relative to the Buildfile directory,
361
364
  # so you want to use #path_to to get the actual path within the project as a matter of practice.
362
365
  #
363
366
  # For example:
@@ -499,7 +502,7 @@ module Buildr
499
502
  task_name, deps = Rake.application.resolve_args(args)
500
503
  deps = [deps] unless deps.respond_to?(:to_ary)
501
504
  task = Buildr.options.parallel ? multitask(task_name) : task(task_name)
502
- Rake.application.lookup(task_name, parent.name.split(":")).enhance [task] if parent
505
+ parent.task(task_name).enhance [task] if parent
503
506
  task.enhance deps, &block
504
507
  end
505
508
 
@@ -531,7 +534,7 @@ module Buildr
531
534
  # You pass a block that is executed in the context of the project definition.
532
535
  # This block is used to define the project and tasks that are part of the project.
533
536
  # Do not perform any work inside the project itself, as it will execute each time
534
- # the Rakefile is loaded. Instead, use it to create and extend tasks that are
537
+ # the Buildfile is loaded. Instead, use it to create and extend tasks that are
535
538
  # related to the project.
536
539
  #
537
540
  # For example:
@@ -546,7 +549,7 @@ module Buildr
546
549
  # => "1.0"
547
550
  # puts project("foo:bar").compile.classpath.map(&:to_spec)
548
551
  # => "org.apache.axis2:axis2:jar:1.1"
549
- # % rake build
552
+ # % buildr build
550
553
  # => Compiling 14 source files in foo:bar
551
554
  def define(name, properties = nil, &block) #:yields:project
552
555
  Project.define(name, properties, &block)
@@ -614,14 +617,6 @@ module Buildr
614
617
  Project.projects *args
615
618
  end
616
619
 
617
- desc "List all projects defined by this Rakefile"
618
- task "projects" do
619
- wide = projects.map(&:name).map(&:size).max
620
- projects.each do |project|
621
- puts project.comment.blank? ? project.name : ("%-#{wide}s #%s" % [project.name, project.comment])
622
- end
623
- end
624
-
625
620
  # Forces all the projects to be evaluated before executing any other task.
626
621
  # If we don't do that, we don't get to have tasks available when running Rake.
627
622
  task("buildr:projects") { projects }
@@ -1,14 +1,10 @@
1
- module Rake #:nodoc:
2
- class Task
1
+ module Rake #:nodoc
2
+ class Task #:nodoc:
3
3
 
4
- def invoke #:nodoc:
5
- if stack.include?(name)
6
- fail "Circular dependency " + (stack + [name]).join("=>")
7
- end
4
+ def invoke()
5
+ fail "Circular dependency " + (stack + [name]).join("=>") if stack.include?(name)
8
6
  @lock.synchronize do
9
- if application.options.trace
10
- puts "** Invoke #{name} #{format_trace_flags}"
11
- end
7
+ puts "** Invoke #{name} #{format_trace_flags}" if application.options.trace
12
8
  return if @already_invoked
13
9
  begin
14
10
  stack.push name
@@ -21,10 +17,14 @@ module Rake #:nodoc:
21
17
  end
22
18
  end
23
19
 
24
- def invoke_prerequisites() #:nodoc:
20
+ def invoke_prerequisites()
25
21
  prerequisites.each { |n| application[n, @scope].invoke }
26
22
  end
27
23
 
24
+ def inspect()
25
+ "#{self.class}: #{name}"
26
+ end
27
+
28
28
  protected
29
29
 
30
30
  def stack()
@@ -33,8 +33,8 @@ module Rake #:nodoc:
33
33
 
34
34
  end
35
35
 
36
- class MultiTask
37
- def invoke_prerequisites
36
+ class MultiTask #:nodoc:
37
+ def invoke_prerequisites()
38
38
  threads = @prerequisites.collect do |p|
39
39
  copy = stack.dup
40
40
  Thread.new(p) { |r| stack.replace copy ; application[r].invoke }
@@ -61,58 +61,4 @@ module Rake #:nodoc:
61
61
 
62
62
  end
63
63
 
64
- class CheckTask < Rake::Task
65
-
66
- def execute()
67
- @warnings = []
68
- super
69
- report if verbose
70
- end
71
-
72
- def note(*msg)
73
- @warnings += msg
74
- end
75
-
76
- def report()
77
- if @warnings.empty?
78
- puts HighLine.new.color("No warnings", :green)
79
- else
80
- warn "These are possible problems with your Rakefile"
81
- @warnings.each { |msg| warn " #{msg}" }
82
- end
83
- end
84
-
85
- end
86
-
87
-
88
- desc "Check your Rakefile for common errors"
89
- CheckTask.define_task "check"
90
-
91
- # Check for circular dependencies
92
- task "check" do
93
- # Keep track of tasks we already checked, to avoid death circles.
94
- checked = {}
95
- # The stack keeps track of all the tasks we visit, so we can display the tasks
96
- # involved in the circular dependency.
97
- expand = lambda do |stack, task|
98
- # Already been here, no need to check again, but make sure we're not seeing
99
- # the same task twice due to a circular dependency.
100
- fail "Circular " + (stack + [task]).join("=>") if stack.include?(task)
101
- unless checked[task]
102
- checked[task] = true
103
- # Variable task may be a Task, but may also be a task name. In the later
104
- # case, we need to resolve it into a Task. But it may also be a filename,
105
- # pointing to a file that may exist, just not during the check, so we need
106
- # this check to avoid dying on "Don't know how to build ..."
107
- if real_task = Rake.application.lookup(task, [])
108
- one_deeper = stack + [task.to_s]
109
- real_task.prerequisites.each { |prereq| expand[one_deeper, prereq.to_s] }
110
- end
111
- end
112
- end
113
- Rake.application.tasks.each do |task|
114
- expand[ [], task.to_s ]
115
- end
116
- end
117
-
118
64
  end
@@ -8,6 +8,8 @@ require "digest/md5"
8
8
  require "digest/sha1"
9
9
  require "facet/progressbar"
10
10
  require "highline"
11
+ require "tempfile"
12
+ require "uri/sftp"
11
13
 
12
14
 
13
15
  # Monkeypatching: SFTP never defines the mkdir method on its session or the underlying
@@ -27,294 +29,264 @@ module Net #:nodoc:all
27
29
  end
28
30
  end
29
31
 
30
- # Monkeypatching Net::HTTP to solve keep_alive bug, see http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-core/10818
31
- module Net
32
- class HTTP
33
- def keep_alive?(req, res)
34
- return false if /close/i =~ req['connection'].to_s
35
- return false if @seems_1_0_server
36
- return false if /close/i =~ res['connection'].to_s
37
- return true if /keep-alive/i =~ res['connection'].to_s
38
- return false if /close/i =~ res['proxy-connection'].to_s
39
- return true if /keep-alive/i =~ res['proxy-connection'].to_s
40
- (@curr_http_version == '1.1')
41
- end
32
+
33
+ # Not quite open-uri, but similar. Provides read and write methods for the resource represented by the URI.
34
+ # Currently supports reads for URI::HTTP and writes for URI::SFTP. Also provides convenience methods for
35
+ # downloads and uploads.
36
+ module URI
37
+
38
+ # Raised when trying to read/download a resource that doesn't exist.
39
+ class NotFoundError < RuntimeError
42
40
  end
43
- end
44
41
 
42
+ class << self
43
+
44
+ # :call-seq:
45
+ # read(uri, options?) => content
46
+ # read(uri, options?) { |chunk| ... }
47
+ #
48
+ # Reads from the resource behind this URI. The first form returns the content of the resource,
49
+ # the second form yields to the block with each chunk of content (usually more than one).
50
+ #
51
+ # For example:
52
+ # File.open "image.jpg", "w" do |file|
53
+ # URI.read("http://example.com/image.jpg") { |chunk| file.write chunk }
54
+ # end
55
+ # Shorter version:
56
+ # File.open("image.jpg", "w") { |file| file.write URI.read("http://example.com/image.jpg") }
57
+ #
58
+ # Supported options:
59
+ # * :proxy -- Collection of proxy settings, accessed by scheme.
60
+ # * :modified -- Only download if file modified since this timestamp. Returns nil if not modified.
61
+ # * :progress -- Show the progress bar while reading.
62
+ def read(uri, options = nil, &block)
63
+ uri = URI.parse(uri.to_s) unless URI === uri
64
+ uri.read options, &block
65
+ end
45
66
 
46
- module Buildr
47
-
48
- # Transports are used for downloading artifacts from remote repositories, uploading
49
- # artifacts to deployment repositories, and anything else you need to move around.
50
- #
51
- # The HTTP transport is used for all URLs with the scheme http or https. You can only
52
- # use the HTTP transport to download artifacts.
53
- #
54
- # The HTTP transport supports the following options:
55
- # * :proxy -- Proxy server to use. A hash with the values host, port, user and password.
56
- # You can also pass a URL (string or URI object).
57
- #
58
- # The SFTP transport is used for all URLs with the schema sftp. You can only use the
59
- # SFTP transport to upload artifacts.
60
- #
61
- # The SFTP transport supports the following options:
62
- # * :username -- The username.
63
- # * :password -- A password. If unspecified, you will be prompted to enter a password.
64
- # * :permissions -- Permissions to set on the uploaded file.
65
- # You can also pass the username/password in the URL.
66
- #
67
- # The SFTP transport will automatically create MD5 and SHA1 digest files for each file
68
- # it uploads.
69
- module Transports
70
-
71
- # Indicates the requested resource was not found.
72
- class NotFound < Exception
67
+ # :call-seq:
68
+ # download(uri, target, options?)
69
+ #
70
+ # Downloads the resource to the target.
71
+ #
72
+ # The target may be a file name (string or task), in which case the file is created from the resource.
73
+ # The target may also be any object that responds to +write+, e.g. File, StringIO, Pipe.
74
+ #
75
+ # Use the progress bar when running in verbose mode.
76
+ def download(uri, target, options = nil)
77
+ uri = URI.parse(uri.to_s) unless URI === uri
78
+ uri.download target, options
79
+ end
80
+
81
+ # :call-seq:
82
+ # write(uri, content, options?)
83
+ # write(uri, options?) { |bytes| .. }
84
+ #
85
+ # Writes to the resource behind the URI. The first form writes the content from a string or an object
86
+ # that responds to +read+ and optionally +size+. The second form writes the content by yielding to the
87
+ # block. Each yield should return up to the specified number of bytes, the last yield returns nil.
88
+ #
89
+ # For example:
90
+ # File.open "killer-app.jar", "rb" do |file|
91
+ # write("sftp://localhost/jars/killer-app.jar") { |chunk| file.read(chunk) }
92
+ # end
93
+ # Or:
94
+ # write "sftp://localhost/jars/killer-app.jar", File.read("killer-app.jar")
95
+ #
96
+ # Supported options:
97
+ # * :proxy -- Collection of proxy settings, accessed by scheme.
98
+ # * :progress -- Show the progress bar while reading.
99
+ def write(uri, *args, &block)
100
+ uri = URI.parse(uri.to_s) unless URI === uri
101
+ uri.write *args, &block
102
+ end
103
+
104
+ # :call-seq:
105
+ # upload(uri, source, options?)
106
+ #
107
+ # Uploads from source to the resource.
108
+ #
109
+ # The source may be a file name (string or task), in which case the file is uploaded to the resource.
110
+ # The source may also be any object that responds to +read+ (and optionally +size+), e.g. File, StringIO, Pipe.
111
+ #
112
+ # Use the progress bar when running in verbose mode.
113
+ def upload(uri, source, options = nil)
114
+ uri = URI.parse(uri.to_s) unless URI === uri
115
+ uri.upload source, options
73
116
  end
74
117
 
75
- class << self
118
+ end
76
119
 
77
- # :call-seq:
78
- # perform(url, options?) { |transport| ... }
79
- #
80
- # Perform one or more operations using an open connection to the
81
- # specified URL. For examples, see Transport#download and Transport#upload.
82
- def perform(url, options = nil, &block)
83
- uri = URI.parse(url.to_s)
84
- const_get(uri.scheme.upcase).perform(uri, options, &block)
85
- end
120
+ class Generic
121
+
122
+ # :call-seq:
123
+ # read(options?) => content
124
+ # read(options?) { |chunk| ... }
125
+ #
126
+ # Reads from the resource behind this URI. The first form returns the content of the resource,
127
+ # the second form yields to the block with each chunk of content (usually more than one).
128
+ #
129
+ # For options, see URI::read.
130
+ def read(options = nil, &block)
131
+ fail "This protocol doesn't support reading (yet, how about helping by implementing it?)"
132
+ end
86
133
 
87
- # :call-seq:
88
- # download(url, target, options?)
89
- #
90
- # Convenience method for downloading a single file from the specified
91
- # URL to the target file.
92
- def download(url, target, options = nil, &block)
93
- uri = URI.parse(url.to_s)
94
- path, uri.path = uri.path, ""
95
- const_get(uri.scheme.upcase).perform(uri, options) do |transport|
96
- transport.download(path, target, &block)
134
+ # :call-seq:
135
+ # download(target, options?)
136
+ #
137
+ # Downloads the resource to the target.
138
+ #
139
+ # The target may be a file name (string or task), in which case the file is created from the resource.
140
+ # The target may also be any object that responds to +write+, e.g. File, StringIO, Pipe.
141
+ #
142
+ # Use the progress bar when running in verbose mode.
143
+ def download(target, options = nil)
144
+ case target
145
+ when Rake::Task
146
+ download target.name, options
147
+ when String
148
+ # If download breaks we end up with a partial file which is
149
+ # worse than not having a file at all, so download to temporary
150
+ # file and then move over.
151
+ modified = File.stat(target).mtime if File.exist?(target)
152
+ temp = nil
153
+ Tempfile.open File.basename(target) do |temp|
154
+ temp.binmode
155
+ read({:progress=>verbose}.merge(options || {}).merge(:modified=>modified)) { |chunk| temp.write chunk }
97
156
  end
157
+ mkpath File.dirname(target)
158
+ File.move temp.path, target
159
+ when File
160
+ read({:progress=>verbose}.merge(options || {}).merge(:modified=>target.mtime)) { |chunk| target.write chunk }
161
+ target.flush
162
+ else
163
+ raise ArgumentError, "Expecting a target that is either a file name (string, task) or object that responds to write (file, pipe)." unless target.respond_to?(:write)
164
+ read({:progress=>verbose}.merge(options || {})) { |chunk| target.write chunk }
165
+ target.flush
98
166
  end
99
-
167
+ end
168
+
169
+ # :call-seq:
170
+ # write(content, options?)
171
+ # write(options?) { |bytes| .. }
172
+ #
173
+ # Writes to the resource behind the URI. The first form writes the content from a string or an object
174
+ # that responds to +read+ and optionally +size+. The second form writes the content by yielding to the
175
+ # block. Each yield should return up to the specified number of bytes, the last yield returns nil.
176
+ #
177
+ # For options, see URI::write.
178
+ def write(*args, &block)
179
+ fail "This protocol doesn't support writing (yet, how about helping by implementing it?)"
100
180
  end
101
181
 
102
- # Extend this class if you are implementing a new transport.
103
- class Transport
104
-
105
- class << self
106
-
107
- # :call-seq:
108
- # perform(url, options?) { |transport| ... }
109
- #
110
- # Perform one or more operations using an open connection to the
111
- # specified URL. For examples, see #download and #upload.
112
- def perform(url, options = nil)
113
- instance = new(url, options)
114
- begin
115
- yield instance
116
- ensure
117
- instance.close
182
+ # :call-seq:
183
+ # upload(source, options?)
184
+ #
185
+ # Uploads from source to the resource.
186
+ #
187
+ # The source may be a file name (string or task), in which case the file is uploaded to the resource.
188
+ # If the source is a directory, uploads all files inside the directory (including nested directories).
189
+ # The source may also be any object that responds to +read+ (and optionally +size+), e.g. File, StringIO, Pipe.
190
+ #
191
+ # Use the progress bar when running in verbose mode.
192
+ def upload(source, options = nil)
193
+ source = source.name if Rake::Task === source
194
+ options ||= {}
195
+ if String === source
196
+ raise NotFoundError, "No source file/directory to upload." unless File.exist?(source)
197
+ if File.directory?(source)
198
+ Dir.glob("#{source}/**/*").reject { |file| File.directory?(file) }.each do |file|
199
+ path = self.path + file.sub(source, "")
200
+ (self + path).upload file, {:digests=>[]}.merge(options)
118
201
  end
202
+ else
203
+ File.open(source, "rb") { |input| upload input, options }
119
204
  end
120
- end
121
-
122
- # The server URI.
123
- attr_reader :uri
124
- # The base path on the server, always ending with a slash.
125
- attr_reader :base_path
126
- # Options passed during construction.
127
- attr_reader :options
128
-
129
- # Initialize the transport with the specified URL and options.
130
- def initialize(url, options)
131
- @uri = URI.parse(url.to_s)
132
- @base_path = @uri.path || "/"
133
- @base_path += "/" unless @base_path[-1] == ?/
134
- @options = options || {}
135
- end
136
-
137
- # Downloads a file from the specified path, relative to the server URI.
138
- # Downloads to either the target file, or by calling the block with each
139
- # chunk of the file. Returns the file's modified timestamp, or now.
140
- #
141
- # For example:
142
- # Transports.perform("http://server/libs") do |http|
143
- # http.download("my_project/test.jar", "test.jar")
144
- # http.download("my_project/readme") { |text| $stdout.write text }
145
- # end
146
- def download(path, target, &block)
147
- fail "Upload not implemented for this transport"
148
- end
149
-
150
- # Uploads a file (source) to the server at the specified path,
151
- # relative to the server URI.
152
- #
153
- # For example:
154
- # Transports.perform("sftp://server/libs") do |sftp|
155
- # sftp.mkpath "my_project"
156
- # sftp.upload("target/test.jar", "my_project/test.jar")
157
- # end
158
- def upload(source, path)
159
- fail "Upload not implemented for this transport"
160
- end
161
-
162
- # Creates a path on the server relative to the server URI.
163
- # See #upload for example.
164
- def mkpath(path)
165
- fail "Upload not implemented for this transport"
166
- end
167
-
168
- protected
169
-
170
- # :call-seq:
171
- # with_progress_bar(file_name, length) { |progress| ... }
172
- #
173
- # Displays a progress bar while executing the block. The first
174
- # argument provides a filename to display, the second argument
175
- # its size in bytes.
176
- #
177
- # The block is yielded with a progress object that implements
178
- # a single method. Call << for each block of bytes down/uploaded.
179
- def with_progress_bar(file_name, length)
180
- if verbose && $stdout.isatty
181
- progress_bar = Console::ProgressBar.new(file_name, length)
182
- # Extend the progress bar so we can display count/total.
183
- class << progress_bar
184
- def total()
185
- convert_bytes(@total)
186
- end
187
- end
188
- # Squeeze the filename into 30 characters.
189
- if file_name.size > 30
190
- base, ext = file_name.split(".")
191
- ext ||= ""
192
- truncated = "#{base[0..26-ext.size]}...#{ext}"
193
- else
194
- truncated = file_name
205
+ elsif source.respond_to?(:read)
206
+ digests = (options[:digests] || [:md5, :sha1]).
207
+ inject({}) { |hash, name| hash[name] = Digest.const_get(name.to_s.upcase).new ; hash }
208
+ size = source.size rescue nil
209
+ write (options).merge(:progress=>verbose && size, :size=>size) do |bytes|
210
+ source.read(bytes).tap do |chunk|
211
+ digests.values.each { |digest| digest << chunk } if chunk
195
212
  end
196
- progress_bar.format = "#{truncated}: %3d%% %s %s/%s %s"
197
- progress_bar.format = "%3d%% %s %s/%s %s"
198
- progress_bar.format_arguments = [:percentage, :bar, :bytes, :total, :stat]
199
- progress_bar.bar_mark = "."
200
-
213
+ end
214
+ digests.each do |key, digest|
215
+ self.merge("#{self.path}.#{key}").write "#{digest.hexdigest} #{File.basename(path)}",
216
+ (options).merge(:progress=>false)
217
+ end
218
+ else
219
+ raise ArgumentError, "Expecting source to be a file name (string, task) or any object that responds to read (file, pipe)."
220
+ end
221
+ end
201
222
 
202
- begin
203
- class << progress_bar
204
- def <<(bytes)
205
- inc bytes.respond_to?(:size) ? bytes.size : bytes
206
- end
207
- end
208
- yield progress_bar
209
- ensure
210
- progress_bar.finish
223
+ protected
224
+
225
+ # :call-seq:
226
+ # with_progress_bar(enable, file_name, size) { |progress| ... }
227
+ #
228
+ # Displays a progress bar while executing the block. The first argument must be true for the
229
+ # progress bar to show (TTY output also required), as a convenient for selectively using the
230
+ # progress bar from a single block.
231
+ #
232
+ # The second argument provides a filename to display, the third its size in bytes.
233
+ #
234
+ # The block is yielded with a progress object that implements a single method.
235
+ # Call << for each block of bytes down/uploaded.
236
+ def with_progress_bar(enable, file_name, size) #:nodoc:
237
+ if enable && $stdout.isatty
238
+ progress_bar = Console::ProgressBar.new(file_name, size)
239
+ # Extend the progress bar so we can display count/total.
240
+ class << progress_bar
241
+ def total()
242
+ convert_bytes(@total)
211
243
  end
244
+ end
245
+ # Squeeze the filename into 30 characters.
246
+ if file_name.size > 30
247
+ base, ext = file_name.split(".")
248
+ truncated = "#{base[0..26-ext.to_s.size]}...#{ext}"
212
249
  else
213
- progress_bar = Object.new
250
+ truncated = file_name
251
+ end
252
+ progress_bar.format = "#{truncated}: %3d%% %s %s/%s %s"
253
+ progress_bar.format = "%3d%% %s %s/%s %s"
254
+ progress_bar.format_arguments = [:percentage, :bar, :bytes, :total, :stat]
255
+ progress_bar.bar_mark = "."
256
+
257
+ begin
214
258
  class << progress_bar
215
259
  def <<(bytes)
260
+ inc bytes.respond_to?(:size) ? bytes.size : bytes
216
261
  end
217
262
  end
218
263
  yield progress_bar
264
+ ensure
265
+ progress_bar.finish
219
266
  end
220
- end
221
-
222
- # :call-seq:
223
- # with_digests(types?) { |digester| ... } => hash
224
- #
225
- # Use the Digester to create digests for files you are downloading or
226
- # uploading, and either verify their signatures (download) or create
227
- # signatures (upload).
228
- #
229
- # The method takes one argument with the list of digest algorithms to
230
- # support. Leave if empty and it will default to MD5 and SHA1.
231
- #
232
- # The method then yields the block passing it a Digester. The Digester
233
- # supports two methods. Use << to pass data that you are down/uploading.
234
- # Once all data is transmitted, use each to iterate over the digests.
235
- # The each method calls the block with the digest type (e.g. "md5")
236
- # and the hexadecimal digest value.
237
- #
238
- # For example:
239
- # with_digests do |digester|
240
- # download url do |block|
241
- # digester << block
242
- # end
243
- # digester.each do |type, hexdigest|
244
- # signature = download "#{url}.#{type}"
245
- # fail "Mismatch" unless signature == hexdigest
246
- # end
247
- # end
248
- def with_digests(types = nil)
249
- digester = Digester.new(types)
250
- yield digester
251
- digester.to_hash
252
- end
253
-
254
- class Digester #:nodoc:
255
-
256
- def initialize(types)
257
- # Digests disabled for now, we have a keep-alive problem with Net::HTTP.
258
- #types ||= [ "md5", "sha1" ]
259
- types ||= []
260
- @digests = types.inject({}) do |hash, type|
261
- hash[type.to_s.downcase] = Digest.const_get(type.to_s.upcase).new
262
- hash
263
- end
264
- end
265
-
266
- # Add bytes for digestion.
267
- def <<(bytes)
268
- @digests.each { |type, digest| digest << bytes }
269
- end
270
-
271
- # Iterate over all the digests calling the block with two arguments:
272
- # the digest type (e.g. "md5") and the hexadecimal digest value.
273
- def each()
274
- @digests.each { |type, digest| yield type, digest.hexdigest }
275
- end
276
-
277
- # Returns a hash that maps each digest type to its hexadecimal digest value.
278
- def to_hash()
279
- @digests.keys.inject({}) do |hash, type|
280
- hash[type] = @digests[type].hexdigest
281
- hash
267
+ else
268
+ progress_bar = Object.new
269
+ class << progress_bar
270
+ def <<(bytes)
282
271
  end
283
272
  end
284
-
273
+ yield progress_bar
285
274
  end
286
-
287
275
  end
288
276
 
277
+ end
289
278
 
290
- class HTTP < Transport #:nodoc:
279
+ class HTTP #:nodoc:
291
280
 
292
- def initialize(url, options)
293
- super
294
- if options
295
- rake_check_options options, :proxy, :digests
296
- proxy = options[:proxy]
297
- end
281
+ # See URI::Generic#read
282
+ def read(options = nil, &block)
283
+ options ||= {}
298
284
 
299
- case proxy
300
- when Hash
301
- @http = Net::HTTP.start(@uri.host, @uri.port, proxy[:host], proxy[:port], proxy[:user], proxy[:password])
302
- when URI, String
303
- proxy = URI.parse(proxy.to_s)
304
- @http = Net::HTTP.start(@uri.host, @uri.port, proxy.host, proxy.port, proxy.user, proxy.password)
305
- else
306
- @http = Net::HTTP.start(@uri.host, @uri.port)
307
- end
308
- end
309
-
310
- def download(path, target = nil, &block)
311
- puts "Requesting #{@uri}/#{path} " if Rake.application.options.trace
312
- if target && File.exist?(target)
313
- last_modified = File.stat(target).mtime.utc
314
- headers = { "If-Modified-Since" => CGI.rfc1123_date(last_modified) }
315
- end
316
- path = path[1..-1] if path[0..0] == '/'
317
- @http.request_get(@base_path + path, headers) do |response|
285
+ headers = { "If-Modified-Since" => CGI.rfc1123_date(options[:modified].utc) } if options[:modified]
286
+ result = nil
287
+ request = lambda do |http|
288
+ puts "Requesting #{self}" if Rake.application.options.trace
289
+ http.request_get(path, headers) do |response|
318
290
  case response
319
291
  when Net::HTTPNotModified
320
292
  # No modification, nothing to do.
@@ -323,155 +295,208 @@ module Buildr
323
295
  when Net::HTTPRedirection
324
296
  # Try to download from the new URI, handle relative redirects.
325
297
  puts "Redirected to #{response['Location']}" if Rake.application.options.trace
326
- last_modified = Transports.download(@uri + URI.parse(response["location"]), target, @options, &block)
298
+ result = (self + URI.parse(response["location"])).read(options, &block)
327
299
 
328
300
  when Net::HTTPOK
329
- puts "Downloading #{@uri}/#{path}" if verbose
330
- last_modified = Time.parse(response["Last-Modified"] || "")
331
- with_progress_bar path.split("/").last, response.content_length do |progress|
332
- with_digests(@options[:digests]) do |digester|
333
-
334
- download = proc do |write|
335
- # Read the body of the page and write it out.
336
- response.read_body do |chunk|
337
- write[chunk]
338
- digester << chunk
339
- progress << chunk
340
- end
341
- # Check server digests before approving the download.
342
- digester.each do |type, hexdigest|
343
- @http.request_get("#{@base_path}#{path}.#{type.to_s.downcase}") do |response|
344
- if Net::HTTPOK === response
345
- puts "Checking signature from #{@uri}/#{path}" if Rake.application.options.trace
346
- fail "Checksum failure for #{@uri}/#{path}: #{type.to_s.upcase} digest on server did not match downloaded file" unless
347
- response.read_body.split.first == hexdigest
348
- end
349
- end
350
- end
301
+ puts "Downloading #{self}" if verbose
302
+ with_progress_bar options[:progress], path.split("/").last, response.content_length do |progress|
303
+ if block
304
+ response.read_body do |chunk|
305
+ block.call chunk
306
+ progress << chunk
351
307
  end
352
-
353
- if target
354
- # If download breaks we end up with a partial file which is
355
- # worse than not having a file at all, so download to temporary
356
- # file and then move over.
357
- temp = Tempfile.open(File.basename(target))
358
- temp.binmode
359
- download[ proc { |chunk| temp.write chunk } ]
360
- temp.close
361
- File.move temp.path, target
362
- else
363
- download[ block ]
308
+ else
309
+ result = ""
310
+ response.read_body do |chunk|
311
+ result << chunk
312
+ progress << chunk
364
313
  end
365
-
366
314
  end
367
315
  end
316
+
368
317
  when Net::HTTPNotFound
369
- raise NotFound, "Looking for #{@uri}/#{path} and all I got was a 404!"
318
+ raise NotFoundError, "Looking for #{self} and all I got was a 404!"
370
319
  else
371
- fail "Failed to download #{@uri}/#{path}: #{response.message}"
320
+ raise RuntimeError, "Failed to download #{self}: #{response.message}"
372
321
  end
373
322
  end
374
- last_modified
375
323
  end
376
324
 
377
- def close()
378
- @http.finish
379
- @http = nil
325
+ proxy = options[:proxy] && options[:proxy].http
326
+ if proxy
327
+ proxy = URI.parse(proxy) if String === proxy
328
+ Net::HTTP.start(host, port, proxy.host, proxy.port, proxy.user, proxy.password) { |http| request[http] }
329
+ else
330
+ Net::HTTP.start(host, port) { |http| request[http] }
380
331
  end
381
-
332
+ result
382
333
  end
383
334
 
384
- # Use the HTTP transport for HTTPS connections.
385
- HTTPS = HTTP #:nodoc:
386
-
335
+ end
387
336
 
388
- class SFTP < Transport #:nodoc:
337
+ class SFTP #:nodoc:
389
338
 
390
- class << self
391
- def passwords()
392
- @passwords ||= {}
393
- end
339
+ class << self
340
+ # Caching of passwords, so we only need to ask once.
341
+ def passwords()
342
+ @passwords ||= {}
394
343
  end
344
+ end
395
345
 
396
- attr_reader :sftp
346
+ # See URI::Generic#write
347
+ def write(*args, &block)
348
+ options = args.pop if Hash === args.last
349
+ options ||= {}
350
+ if String === args.first
351
+ ios = StringIO.new(args.first, "r")
352
+ write(options.merge(:size=>args.first.size)) { |bytes| ios.read(bytes) }
353
+ elsif args.first.respond_to?(:read)
354
+ size = args.first.size rescue nil
355
+ write({:size=>size}.merge(options)) { |bytes| args.first.read(bytes) }
356
+ elsif args.empty? && block
397
357
 
398
- def initialize(url, options)
399
- super
400
- rake_check_options options, :digests, :permissions, :port, :uri, :username, :password
401
- @permissions = options.delete :permissions
402
358
  # SSH options are based on the username/password from the URI.
403
- ssh_options = { :port=>@uri.port, :username=>@uri.user }.merge(options || {})
404
- ssh_options[:password] ||= SFTP.passwords[@uri.host]
359
+ ssh_options = { :port=>port, :username=>user }.merge(options[:ssh_options] || {})
360
+ ssh_options[:password] ||= SFTP.passwords[host]
405
361
  begin
406
- puts "Connecting to #{@uri.host}" if Rake.application.options.trace
407
- session = Net::SSH.start(@uri.host, ssh_options)
408
- SFTP.passwords[@uri.host] = ssh_options[:password]
362
+ puts "Connecting to #{host}" if Rake.application.options.trace
363
+ session = Net::SSH.start(host, ssh_options)
364
+ SFTP.passwords[host] = ssh_options[:password]
409
365
  rescue Net::SSH::AuthenticationFailed=>ex
410
366
  # Only if running with console, prompt for password.
411
367
  if !ssh_options[:password] && $stdout.isatty
412
- password = HighLine.new.ask("Password for #{@uri.host}:") { |q| q.echo = "*" }
368
+ password = HighLine.new.ask("Password for #{host}:") { |q| q.echo = "*" }
413
369
  ssh_options[:password] = password
414
370
  retry
415
371
  end
416
372
  raise
417
373
  end
418
- @sftp = session.sftp.connect
419
- puts "connected" if Rake.application.options.trace
420
- end
421
374
 
422
- def upload(source, path)
423
- File.open(source) do |file|
424
- with_progress_bar path.split("/").last, File.size(source) do |progress|
425
- with_digests(@options[:digests]) do |digester|
426
- target_path = "#{@base_path}#{path}"
427
- puts "Uploading to #{target_path}" if Rake.application.options.trace
428
- @sftp.open_handle(target_path, "w") do |handle|
429
- # Writing in chunks gives us the benefit of a progress bar,
430
- # but also require that we maintain a position in the file,
431
- # since write() with two arguments always writes at position 0.
432
- pos = 0
433
- while chunk = file.read(32 * 4096)
434
- @sftp.write(handle, chunk, pos)
435
- pos += chunk.size
436
- digester << chunk
437
- progress << chunk
438
- end
439
- end
440
- @sftp.setstat(target_path, :permissions => @permissions) if @permissions
441
-
442
- # Upload all the digests.
443
- digester.each do |type, hexdigest|
444
- digest_file = "#{@base_path}#{path}.#{type}"
445
- puts "Uploading signature to #{digest_file}" if Rake.application.options.trace
446
- @sftp.open_handle(digest_file, "w") do |handle|
447
- @sftp.write(handle, "#{hexdigest} #{path}")
448
- end
449
- @sftp.setstat(digest_file, :permissions => @permissions) if @permissions
375
+ session.sftp.connect do |sftp|
376
+ puts "connected" if Rake.application.options.trace
377
+
378
+ # To create a path, we need to create all its parent. We use realpath to determine if
379
+ # the path already exists, otherwise mkdir fails.
380
+ puts "Creating path #{@base_path}" if Rake.application.options.trace
381
+ path.split("/").inject("") do |base, part|
382
+ combined = base + part
383
+ sftp.realpath combined rescue sftp.mkdir combined, {}
384
+ "#{combined}/"
385
+ end
386
+
387
+ with_progress_bar options[:progress] && options[:size], path.split("/"), options[:size] || 0 do |progress|
388
+ puts "Uploading to #{path}" if Rake.application.options.trace
389
+ sftp.open_handle(path, "w") do |handle|
390
+ # Writing in chunks gives us the benefit of a progress bar,
391
+ # but also require that we maintain a position in the file,
392
+ # since write() with two arguments always writes at position 0.
393
+ pos = 0
394
+ while chunk = yield(32 * 4096)
395
+ sftp.write(handle, chunk, pos)
396
+ pos += chunk.size
397
+ progress << chunk
450
398
  end
399
+ sftp.setstat(target_path, :permissions => options[:permissions]) if options[:permissions]
451
400
  end
452
-
453
401
  end
454
402
  end
403
+ else
404
+ raise ArgumentError, "Either give me the content, or pass me a block, otherwise what would I upload?"
455
405
  end
406
+ end
407
+
408
+ end
456
409
 
457
- def mkpath(path)
458
- # To create a path, we need to create all its parent.
459
- # We use realpath to determine if the path already exists,
460
- # otherwise mkdir fails.
461
- puts "Creating path #{@base_path}" if Rake.application.options.trace
462
- path.split("/").inject(@base_path) do |base, part|
463
- combined = base + part
464
- @sftp.realpath combined rescue @sftp.mkdir combined, {}
465
- "#{combined}/"
410
+
411
+ # File URL. Keep in mind that file URLs take the form of <code>file://host/path</code>, although the host
412
+ # is not used, so typically all you will see are three backslashes. This methods accept common variants,
413
+ # like <code>file:/path</code> but always returns a valid URL.
414
+ class FILE < Generic
415
+
416
+ COMPONENT = [ :host, :path ].freeze
417
+
418
+ def initialize(*args)
419
+ super
420
+ # file:something (opaque) becomes file:///something
421
+ if path.nil?
422
+ set_path "/#{opaque}"
423
+ unless opaque.nil?
424
+ set_opaque nil
425
+ warn "#{caller[2]}: We'll accept this URL, but just so you know, it needs three slashes, as in: #{to_s}"
466
426
  end
467
427
  end
428
+ # Sadly, file://something really means file://something/ (something being server)
429
+ set_path "/" if path.empty?
430
+
431
+ # On windows, file://c:/something is not a valid URL, but people do it anyway, so if we see a drive-as-host,
432
+ # we'll just be nice enough to fix it. (URI actually strips the colon here)
433
+ if host =~ /^[a-zA-Z]$/
434
+ set_path "/#{host}:#{path}"
435
+ set_host nil
436
+ end
437
+ end
468
438
 
469
- def close()
470
- @sftp.close
471
- @sftp = nil
439
+ # See URI::Generic#read
440
+ def read(options = nil, &block)
441
+ options ||= {}
442
+ raise ArgumentError, "Either you're attempting to read a file from another host (which we don't support), or you used two slashes by mistake, where you should have file:///<path>." unless host.blank?
443
+
444
+ path = real_path
445
+ # TODO: complain about clunky URLs
446
+ raise NotFoundError, "Looking for #{self} and can't find it." unless File.exists?(path)
447
+ raise NotFoundError, "Looking for the file #{self}, and it happens to be a directory." if File.directory?(path)
448
+ File.open path, "rb" do |input|
449
+ with_progress_bar options[:progress], path.split("/").last, input.stat.size do |progress|
450
+ block ? block.call(input.read) : input.read
451
+ end
472
452
  end
453
+ end
454
+
455
+ # See URI::Generic#write
456
+ def write(*args, &block)
457
+ options = args.pop if Hash === args.last
458
+ options ||= {}
459
+ raise ArgumentError, "Either you're attempting to write a file to another host (which we don't support), or you used two slashes by mistake, where you should have file:///<path>." unless host.blank?
460
+
461
+ if String === args.first
462
+ ios = StringIO.new(args.first, "r")
463
+ write(options.merge(:size=>args.first.size)) { |bytes| ios.read(bytes) }
464
+ elsif args.first.respond_to?(:read)
465
+ size = args.first.size rescue nil
466
+ write({:size=>size}.merge(options)) { |bytes| args.first.read(bytes) }
467
+ elsif args.empty? && block
468
+ temp = nil
469
+ Tempfile.open File.basename(path) do |temp|
470
+ temp.binmode
471
+ with_progress_bar options[:progress] && options[:size], path.split("/"), options[:size] || 0 do |progress|
472
+ while chunk = yield(32 * 4096)
473
+ temp.write chunk
474
+ progress << chunk
475
+ end
476
+ end
477
+ end
478
+ real_path.tap do |path|
479
+ mkpath File.dirname(path)
480
+ File.move temp.path, path
481
+ end
482
+ else
483
+ raise ArgumentError, "Either give me the content, or pass me a block, otherwise what would I upload?"
484
+ end
485
+ end
486
+
487
+ def to_s()
488
+ "file://#{host}#{path}"
489
+ end
473
490
 
491
+ # The URL path always starts with a backslash. On most operating systems (Linux, Darwin, BSD) it points
492
+ # to the absolute path on the file system. But on Windows, it comes before the drive letter, creating an
493
+ # unusable path, so real_path fixes that. Ugly but necessary hack.
494
+ def real_path() #:nodoc:
495
+ RUBY_PLATFORM =~ /win32/ && path =~ /^\/[a-zA-Z]:\// ? path[1..-1] : path
474
496
  end
475
497
 
498
+ @@schemes["FILE"] = FILE
499
+
476
500
  end
501
+
477
502
  end