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,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
+