buildr 1.1.3 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,358 @@
1
+ require "core/project"
2
+ require "tasks/zip"
3
+ require "spec"
4
+
5
+ module Buildr
6
+
7
+ module BuildChecks #:nodoc:
8
+
9
+ module Matchers
10
+
11
+ class << self
12
+
13
+ # Define matchers that operate by calling a method on the tested object.
14
+ # For example:
15
+ # foo.should contain(bar)
16
+ # calls:
17
+ # foo.contain(bar)
18
+ def match_using(*names)
19
+ names.each do |name|
20
+ matcher = Class.new do
21
+ # Initialize with expected arguments (i.e. contain(bar) initializes with bar).
22
+ define_method(:initialize) { |*args| @expects = args }
23
+ # Matches against actual value (i.e. foo.should exist called with foo).
24
+ define_method(:matches?) do |actual|
25
+ @actual = actual
26
+ return actual.send("#{name}?", *@expects) if actual.respond_to?("#{name}?")
27
+ return actual.send(name, *@expects) if actual.respond_to?(name)
28
+ raise "You can't check #{actual}, it doesn't respond to #{name}."
29
+ end
30
+ # Some matchers have arguments, others don't, treat appropriately.
31
+ define_method :failure_message do
32
+ args = " " + @expects.map{ |arg| "'#{arg}'" }.join(", ") unless @expects.empty?
33
+ "Expected #{@actual} to #{name}#{args}"
34
+ end
35
+ define_method :negative_failure_message do
36
+ args = " " + @expects.map{ |arg| "'#{arg}'" }.join(", ") unless @expects.empty?
37
+ "Expected #{@actual} to not #{name}#{args}"
38
+ end
39
+ end
40
+ # Define method to create matcher.
41
+ define_method(name) { |*args| matcher.new(*args) }
42
+ end
43
+ end
44
+
45
+ end
46
+
47
+ # Define delegate matchers for exist and contain methods.
48
+ match_using :exist, :contain
49
+
50
+ end
51
+
52
+
53
+ # An expectation has subject, description and block. The expectation is validated by running the block,
54
+ # and can access the subject from the method #it. The description is used for reporting.
55
+ #
56
+ # The expectation is run by calling #run_against. You can share expectations by running them against
57
+ # different projects (or any other context for that matter).
58
+ #
59
+ # If the subject is missing, it is set to the argument of #run_against, typically the project itself.
60
+ # If the description is missing, it is set from the project. If the block is missing, the default behavior
61
+ # prints "Pending" followed by the description. You can use this to write place holders and fill them later.
62
+ class Expectation
63
+
64
+ attr_reader :description, :subject, :block
65
+
66
+ # :call-seq:
67
+ # initialize(subject, description?) { .... }
68
+ # initialize(description?) { .... }
69
+ #
70
+ # First argument is subject (returned from it method), second argument is description. If you omit the
71
+ # description, it will be set from the subject. If you omit the subject, it will be set from the object
72
+ # passed to run_against.
73
+ def initialize(*args, &block)
74
+ @description = args.pop if String === args.last
75
+ @subject = args.shift
76
+ raise ArgumentError, "Expecting subject followed by description, and either one is optional. Not quite sure what to do with this list of arguments." unless args.empty?
77
+ @block = block || lambda { puts "Pending: #{description}" if verbose }
78
+ end
79
+
80
+ # :call-seq:
81
+ # run_against(context)
82
+ #
83
+ # Runs this expectation against the context object. The context object is different from the subject,
84
+ # but used as the subject if no subject specified (i.e. returned from the it method).
85
+ #
86
+ # This method creates a new context object modeled after the context argument, but a separate object
87
+ # used strictly for running this expectation, and used only once. The context object will pass methods
88
+ # to the context argument, so you can call any method, e.g. package(:jar).
89
+ #
90
+ # It also adds all matchers defined in Buildr and RSpec, and two additional methods:
91
+ # * it() -- Returns the subject.
92
+ # * description() -- Returns the description.
93
+ def run_against(context)
94
+ subject = @subject || context
95
+ description = @description ? "#{subject} #{@description}" : subject.to_s
96
+ # Define anonymous class and load it with:
97
+ # - All instance methods defined in context, so we can pass method calls to the context.
98
+ # - it() method to return subject, description() method to return description.
99
+ # - All matchers defined by Buildr and RSpec.
100
+ klass = Class.new
101
+ klass.instance_eval do
102
+ context.class.instance_methods(false).each do |method|
103
+ define_method(method) { |args| context.send(method, args) }
104
+ end
105
+ define_method(:it) { subject }
106
+ define_method(:description) { description }
107
+ include Spec::Matchers
108
+ include Matchers
109
+ end
110
+
111
+ # Run the expectation. We only print the expectation name when tracing (to know they all ran),
112
+ # or when we get a failure.
113
+ begin
114
+ puts description if Rake.application.options.trace
115
+ klass.new.instance_eval &@block
116
+ rescue Exception=>error
117
+ raise error.exception("#{description}\n#{error}").tap { |wrapped| wrapped.set_backtrace(error.backtrace) }
118
+ end
119
+ end
120
+
121
+ end
122
+
123
+ end
124
+
125
+
126
+ class Project
127
+
128
+ # :call-seq:
129
+ # check(description) { ... }
130
+ # check(subject, description) { ... }
131
+ #
132
+ # Adds an expectation. The expectation is run against the project by the check task, executed after packaging.
133
+ # You can access any package created by the project.
134
+ #
135
+ # An expectation is written using a subject, description and block to validate the expectation. For example:
136
+ #
137
+ # For example:
138
+ # check package(:jar), "should exist" do
139
+ # it.should exist
140
+ # end
141
+ # check package(:jar), "should contain a manifest" do
142
+ # it.should contain("META-INF/MANIFEST.MF")
143
+ # end
144
+ # check package(:jar).path("com/acme"), "should contain classes" do
145
+ # it.should_not be_empty
146
+ # end
147
+ # check package(:jar).entry("META-INF/MANIFEST"), "should be a recent license" do
148
+ # it.should contain(/Copyright (C) 2007/)
149
+ # end
150
+ #
151
+ # If you omit the subject, the project is used as the subject. If you omit the description, the subject is
152
+ # used as description.
153
+ #
154
+ # During development you can write placeholder expectations by omitting the block. This will simply report
155
+ # the expectation as pending.
156
+ def check(*args, &block)
157
+ expectations << BuildChecks::Expectation.new(*args, &block)
158
+ end
159
+
160
+ # :call-seq:
161
+ # expectations() => Expectation*
162
+ #
163
+ # Returns a list of expectations (see #check).
164
+ def expectations()
165
+ @expectations ||= []
166
+ end
167
+
168
+ end
169
+
170
+ Project.on_define do |project|
171
+ # The check task can do any sort of interesting things, but the most important is running expectations.
172
+ project.task("check") do |task|
173
+ project.expectations.inject(true) do |passed, expect|
174
+ begin
175
+ expect.run_against project
176
+ passed
177
+ rescue Exception=>error
178
+ if verbose
179
+ puts error.backtrace.detect { |line| line =~ /#{Rake.application.rakefile}/ } || ""
180
+ puts error
181
+ end
182
+ false
183
+ end
184
+ end or fail "Checks failed for project #{project.name} (see errors above)."
185
+ end
186
+ project.task("package").enhance do |task|
187
+ # Run all actions before checks.
188
+ task.enhance { project.task("check").invoke }
189
+ end
190
+ end
191
+
192
+ end
193
+
194
+
195
+ module Rake #:nodoc:
196
+ class FileTask
197
+
198
+ # :call-seq:
199
+ # exist?() => boolean
200
+ #
201
+ # Returns true if this file exists.
202
+ def exist?()
203
+ File.exist?(name)
204
+ end
205
+
206
+ # :call-seq:
207
+ # empty?() => boolean
208
+ #
209
+ # Returns true if file/directory is empty.
210
+ def empty?()
211
+ File.directory?(name) ? Dir.glob("#{name}/*").empty? : File.read(name).empty?
212
+ end
213
+
214
+ # :call-seq:
215
+ # contain(pattern*) => boolean
216
+ # contain(file*) => boolean
217
+ #
218
+ # For a file, returns true if the file content matches against all the arguments. An argument may be
219
+ # a string or regular expression.
220
+ #
221
+ # For a directory, return true if the directory contains the specified files. You can use relative
222
+ # file names and glob patterns (using *, **, etc).
223
+ def contain?(*patterns)
224
+ if File.directory?(name)
225
+ patterns.map { |pattern| "#{name}/#{pattern}" }.all? { |pattern| !Dir[pattern].empty? }
226
+ else
227
+ contents = File.read(name)
228
+ patterns.map { |pattern| Regexp === pattern ? pattern : Regexp.new(Regexp.escape(pattern.to_s)) }.
229
+ all? { |pattern| contents =~ pattern }
230
+ end
231
+ end
232
+
233
+ end
234
+ end
235
+
236
+
237
+ module Zip #:nodoc:
238
+ class ZipEntry
239
+
240
+ # :call-seq:
241
+ # exist() => boolean
242
+ #
243
+ # Returns true if this entry exists.
244
+ def exist?()
245
+ Zip::ZipFile.open(zipfile) { |zip| zip.file.exist?(@name) }
246
+ end
247
+
248
+ # :call-seq:
249
+ # empty?() => boolean
250
+ #
251
+ # Returns true if this entry is empty.
252
+ def empty?()
253
+ Zip::ZipFile.open(zipfile) { |zip| zip.file.read(@name) }.empty?
254
+ end
255
+
256
+ # :call-seq:
257
+ # contain(patterns*) => boolean
258
+ #
259
+ # Returns true if this ZIP file entry matches against all the arguments. An argument may be
260
+ # a string or regular expression.
261
+ def contain?(*patterns)
262
+ content = Zip::ZipFile.open(zipfile) { |zip| zip.file.read(@name) }
263
+ patterns.map { |pattern| Regexp === pattern ? pattern : Regexp.new(Regexp.escape(pattern.to_s)) }.
264
+ all? { |pattern| content =~ pattern }
265
+ end
266
+
267
+ end
268
+ end
269
+
270
+
271
+ module Buildr
272
+ class ZipTask
273
+
274
+ class Path
275
+
276
+ # :call-seq:
277
+ # exist() => boolean
278
+ #
279
+ # Returns true if this path exists. This only works if the path has any entries in it,
280
+ # so exist on path happens to be the opposite of empty.
281
+ def exist?()
282
+ !empty?
283
+ end
284
+
285
+ # :call-seq:
286
+ # empty?() => boolean
287
+ #
288
+ # Returns true if this path is empty (has no other entries inside).
289
+ def empty?()
290
+ check { |entries| entries.empty? }
291
+ end
292
+
293
+ # :call-seq:
294
+ # contain(file*) => boolean
295
+ #
296
+ # Returns true if this ZIP file path contains all the specified files. You can use relative
297
+ # file names and glob patterns (using *, **, etc).
298
+ def contain?(*files)
299
+ check do |entries|
300
+ files.all? { |file| entries.detect { |entry| File.fnmatch(file, entry.to_s) } }
301
+ end
302
+ end
303
+
304
+ # :call-seq:
305
+ # entry(name) => ZipEntry
306
+ #
307
+ # Returns a ZIP file entry. You can use this to check if the entry exists and its contents,
308
+ # for example:
309
+ # package(:jar).path("META-INF").entry("LICENSE").should contain(/Apache Software License/)
310
+ def entry(name)
311
+ ::Zip::ZipEntry.new(root.name, "#{@path}#{name}")
312
+ end
313
+
314
+ protected
315
+
316
+ def check() #:nodoc:
317
+ unless @cached_entries
318
+ if @path
319
+ base = Regexp.new("^" + Regexp.escape(@path || ""))
320
+ @cached_entries = root.path("").check.map { |name| name.to_s.sub!(base, "") }.reject(&:nil?)
321
+ else
322
+ @cached_entries = Zip::ZipFile.open(root.name) { |zip| zip.entries }
323
+ end
324
+ end
325
+ block_given? ? yield(@cached_entries) : @cached_entries
326
+ end
327
+
328
+ end
329
+
330
+ # :call-seq:
331
+ # empty?() => boolean
332
+ #
333
+ # Returns true if this ZIP file is empty (has no other entries inside).
334
+ def empty?()
335
+ path("").empty
336
+ end
337
+
338
+ # :call-seq:
339
+ # contain(file*) => boolean
340
+ #
341
+ # Returns true if this ZIP file contains all the specified files. You can use absolute
342
+ # file names and glob patterns (using *, **, etc).
343
+ def contain?(*files)
344
+ path("").contain?(*files)
345
+ end
346
+
347
+ # :call-seq:
348
+ # entry(name) => Entry
349
+ #
350
+ # Returns a ZIP file entry. You can use this to check if the entry exists and its contents,
351
+ # for example:
352
+ # package(:jar).entry("META-INF/LICENSE").should contain(/Apache Software License/)
353
+ def entry(name)
354
+ path("").entry(name)
355
+ end
356
+
357
+ end
358
+ end
@@ -1,9 +1,85 @@
1
1
  require "tempfile"
2
2
  require "pathname"
3
3
  require "core/transports"
4
+ require "open-uri"
5
+ require "uri/open-sftp"
6
+
7
+
8
+ class Hash
9
+
10
+ # :call-seq:
11
+ # only(keys*) => hash
12
+ #
13
+ # Returns a new hash with only the specified keys.
14
+ #
15
+ # For example:
16
+ # { :a=>1, :b=>2, :c=>3, :d=>4 }.only(:a, :c)
17
+ # => { :b=>2, :d=>4 }
18
+ def only(*keys)
19
+ self.inject({}) { |hash, pair| hash[pair[0]] = pair[1] if keys.include?(pair[0]) ; hash }
20
+ end
21
+
22
+
23
+ # :call-seq:
24
+ # except(keys*) => hash
25
+ #
26
+ # Returns a new hash without the specified keys.
27
+ #
28
+ # For example:
29
+ # { :a=>1, :b=>2, :c=>3, :d=>4 }.except(:a, :c)
30
+ # => { :a=>1, :c=>3 }
31
+ def except(*keys)
32
+ self.inject({}) { |hash, pair| hash[pair[0]] = pair[1] unless keys.include?(pair[0]) ; hash }
33
+ end
34
+
35
+ end
36
+
4
37
 
5
38
  module Buildr
6
39
 
40
+ # Collection of options for controlling Buildr. For example for running builds without running
41
+ # test cases, using a proxy server, JVM arguments, etc. You access this object by calling options,
42
+ # for example:
43
+ # options.proxy.http = "http://proxy.acme.com:8080"
44
+ # options.java_args = "-Xmx512M"
45
+ class Options
46
+
47
+ # :call-seq:
48
+ # proxy() => options
49
+ #
50
+ # Returns the proxy options. Currently supported options are:
51
+ # * :http -- HTTP proxy for use when downloading.
52
+ #
53
+ # For example:
54
+ # options.proxy.http = "http://proxy.acme.com:8080"
55
+ # You can also set it using the environment variable HTTP_PROXY.
56
+ def proxy()
57
+ @proxy ||= Struct.new(:http).new(ENV['HTTP_PROXY'] || ENV['http_proxy'])
58
+ end
59
+
60
+ end
61
+
62
+ class << self
63
+
64
+ # :call-seq:
65
+ # options() => Options
66
+ #
67
+ # Returns the Buildr options. See Options.
68
+ def options()
69
+ @options ||= Options.new
70
+ end
71
+
72
+ end
73
+
74
+ # :call-seq:
75
+ # options() => Options
76
+ #
77
+ # Returns the Buildr options. See Options.
78
+ def options()
79
+ Buildr.options
80
+ end
81
+
82
+
7
83
  # :call-seq:
8
84
  # struct(hash) => Struct
9
85
  #
@@ -76,27 +152,30 @@ module Buildr
76
152
  # For example:
77
153
  # download "image.jpg"=>"http://example.com/theme/image.jpg"
78
154
  def download(args)
79
- if String === args || URI === args
155
+ args = URI.parse(args) if String === args
156
+ if URI === args
80
157
  # Given only a download URL, download into a temporary file.
81
158
  # You can infer the file from task name.
82
- temp = Tempfile.new(File.basename(args.to_s))
83
- task = file_create(temp.path) do |task|
84
- Transports.download task.source, task.name
159
+ temp = Tempfile.open(File.basename(args.to_s))
160
+ file(temp.path).tap do |task|
161
+ # Since temporary file exists, force a download.
162
+ class << task ; def needed?() ; true ; end ; end
163
+ task.sources << args
164
+ task.enhance { args.download temp, :proxy=>Buildr.options.proxy }
85
165
  end
86
- task.sources << args
87
166
  else
88
167
  # Download to a file created by the task.
89
168
  fail unless args.keys.size == 1
90
- url = args.values.first
91
- task = file_create(args.keys.first) do |task|
92
- mkpath File.dirname(task.name), :verbose=>false
93
- Transports.download task.source, task.name
169
+ uri = URI.parse(args.values.first.to_s)
170
+ file_create(args.keys.first).tap do |task|
171
+ task.sources << uri
172
+ task.enhance { uri.download task.name, :proxy=>Buildr.options.proxy }
94
173
  end
95
- task.sources << url
96
174
  end
97
- task
175
+
98
176
  end
99
177
 
178
+
100
179
  # A filter knows how to copy files from one directory to another, applying mappings to the
101
180
  # contents of these files.
102
181
  #
@@ -188,37 +267,52 @@ module Buildr
188
267
  # For example:
189
268
  # filter.using "version"=>"1.2"
190
269
  # will replace all occurrences of "${version}" with "1.2".
191
- def using(mapping, &block)
270
+ def using(mapping = nil, &block)
192
271
  self.mapping = mapping || block
193
272
  self
194
273
  end
195
274
 
275
+ # :call-seq:
276
+ # run() => boolean
277
+ #
196
278
  # Runs the filter.
197
279
  def run()
198
- if needed?
199
- unless copy_map.empty?
200
- verbose(Rake.application.options.trace || false) do
201
- mkpath target.to_s
202
- copy_map do |dest, src|
203
- mkpath File.dirname(dest) rescue nil
204
- case mapping
205
- when Proc, Method # Call on input, accept output.
206
- mapped = mapping.call(src, File.open(src, "rb") { |file| file.read })
207
- File.open(dest, "wb") { |file| file.write mapped }
208
- when Hash # Map ${key} to value
209
- mapped = File.open(src, "rb") { |file| file.read }.
210
- gsub(/\$\{.*\}/) { |str| mapping[str[2..-2]] || str }
211
- File.open(dest, "wb") { |file| file.write mapped }
212
- when nil # No mapping.
213
- cp src, dest
214
- else
215
- fail "Filter can be a hash (key=>value), or a proc/method; I don't understand #{mapping}"
216
- end
217
- end
218
- touch target.to_s
280
+ raise "No source directory specified, where am I going to find the files to filter?" if source.nil?
281
+ raise "Source directory #{source} doesn't exist" unless File.exist?(source.to_s)
282
+ raise "No target directory specified, where am I going to copy the files to?" if target.nil?
283
+
284
+ includes = @include.empty? ? ["*"] : @include
285
+ src_base = Pathname.new(source.to_s)
286
+ copy_map = Dir[File.join(source.to_s, "**/*")].reject { |file| File.directory?(file) }.
287
+ map { |src| Pathname.new(src).relative_path_from(src_base).to_s }.
288
+ select { |file| includes.any? { |pattern| File.fnmatch(pattern, file) } }.
289
+ reject { |file| @exclude.any? { |pattern| File.fnmatch(pattern, file) } }.
290
+ map { |file| [File.expand_path(file, target.to_s), File.expand_path(file, source.to_s)] }.
291
+ select { |dest, src| !File.exist?(dest) || File.stat(src).mtime > File.stat(dest).mtime }
292
+ return false if copy_map.empty?
293
+
294
+ verbose(Rake.application.options.trace || false) do
295
+ mkpath target.to_s
296
+ copy_map.each do |dest, src|
297
+ mkpath File.dirname(dest) rescue nil
298
+ case mapping
299
+ when Proc, Method # Call on input, accept output.
300
+ relative = Pathname.new(src).relative_path_from(src_base).to_s
301
+ mapped = mapping.call(relative, File.open(src, "rb") { |file| file.read })
302
+ File.open(dest, "wb") { |file| file.write mapped }
303
+ when Hash # Map ${key} to value
304
+ mapped = File.open(src, "rb") { |file| file.read }.
305
+ gsub(/\$\{[^}]*\}/) { |str| mapping[str[2..-2]] || str }
306
+ File.open(dest, "wb") { |file| file.write mapped }
307
+ when nil # No mapping.
308
+ cp src, dest
309
+ else
310
+ fail "Filter can be a hash (key=>value), or a proc/method; I don't understand #{mapping}"
219
311
  end
220
312
  end
313
+ touch target.to_s
221
314
  end
315
+ true
222
316
  end
223
317
 
224
318
  # Returns the target directory.
@@ -226,34 +320,6 @@ module Buildr
226
320
  @target.to_s
227
321
  end
228
322
 
229
- private
230
-
231
- def needed?()
232
- return false if target.nil? || source.nil? || !File.exist?(source.to_s)
233
- return true unless File.exist?(target.to_s)
234
- !copy_map.empty?
235
- end
236
-
237
- # Return a copy map of all the files that need copying: the key is the file to copy to,
238
- # the value is the source file. If called with a block, yields with each dest/source pair.
239
- def copy_map(&block)
240
- unless @copy_map
241
- @include = ["*"] if @include.empty?
242
- base = Pathname.new(source.to_s)
243
- @copy_map = Dir[File.join(source.to_s, "**/*")].reject { |file| File.directory?(file) }.
244
- map { |src| Pathname.new(src).relative_path_from(base).to_s }.
245
- select { |file| @include.any? { |pattern| File.fnmatch(pattern, file) } }.
246
- reject { |file| @exclude.any? { |pattern| File.fnmatch(pattern, file) } }.
247
- map { |file| [File.expand_path(file, target.to_s), File.expand_path(file, source.to_s)] }.
248
- select { |dest, src| !File.exist?(dest) || File.stat(src).mtime > File.stat(dest).mtime }
249
- end
250
- if block_given?
251
- @copy_map.each(&block)
252
- else
253
- @copy_map
254
- end
255
- end
256
-
257
323
  end
258
324
 
259
325
  # :call-seq:
@@ -273,5 +339,38 @@ module Buildr
273
339
  def filter(source)
274
340
  Filter.new.from(source)
275
341
  end
276
-
342
+
277
343
  end
344
+
345
+
346
+ # Add a touch of colors (red) to warnings.
347
+ HighLine.use_color = PLATFORM !~ /win32/
348
+ module Kernel #:nodoc:
349
+
350
+ def warn_with_color(message)
351
+ warn_without_color $terminal.color(message.to_s, :red)
352
+ end
353
+ alias_method_chain :warn, :color
354
+
355
+ # :call-seq:
356
+ # warn_deprecated(message)
357
+ #
358
+ # Use with deprecated methods and classes. This method automatically adds the file name and line number,
359
+ # and the text "Deprecated" before the message, and eliminated duplicate warnings. It only warns when
360
+ # running in verbose mode.
361
+ #
362
+ # For example:
363
+ # warn_deprecated "Please use new_foo instead of foo."
364
+ def warn_deprecated(message) #:nodoc:
365
+ return unless verbose
366
+ "#{caller[1]}: Deprecated: #{message}".tap do |message|
367
+ @deprecated ||= {}
368
+ unless @deprecated[message]
369
+ @deprecated[message] = true
370
+ warn message
371
+ end
372
+ end
373
+ end
374
+
375
+ end
376
+