buildr 0.14.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,381 @@
1
+ require "net/ssh"
2
+ require "net/sftp"
3
+ require "uri/sftp"
4
+ require "digest/md5"
5
+ require "digest/sha1"
6
+ require "facet/progressbar"
7
+ require "highline"
8
+
9
+
10
+ # Monkeypatching: SFTP never defines the mkdir method on its session or the underlying
11
+ # driver, it just redirect calls through method_missing. Rake, on the other hand, decides
12
+ # to define mkdir on Object, and so routes our calls to FileUtils.
13
+ class Net::SFTP::Session
14
+ def mkdir(path, attrs = {})
15
+ method_missing :mkdir, path, attrs
16
+ end
17
+ end
18
+
19
+ class Net::SFTP::Protocol::Driver
20
+ def mkdir(first, path, attrs = {})
21
+ method_missing :mkdir, first, path, attrs
22
+ end
23
+ end
24
+
25
+
26
+ module Buildr
27
+ module Transports
28
+
29
+ class NotFound < Exception
30
+ end
31
+
32
+ # Perform one or more operations using an open connection to the
33
+ # specified URL. For examples, see Transport#download and Transport#upload.
34
+ def self.perform(url, options = nil, &block)
35
+ uri = URI.parse(url.to_s)
36
+ const_get(uri.scheme.upcase).perform(uri, options, &block)
37
+ end
38
+
39
+ # Convenience method for downloading a single file from the specified
40
+ # URL to the target file.
41
+ def self.download(url, target, options = nil)
42
+ uri = URI.parse(url.to_s)
43
+ path, uri.path = uri.path, ""
44
+ const_get(uri.scheme.upcase).perform(uri, options) do |transport|
45
+ transport.download(path, target)
46
+ end
47
+ end
48
+
49
+ class Transport
50
+
51
+ class << self
52
+
53
+ # Perform one or more operations using an open connection to the
54
+ # specified URL. For examples, see #download and #upload.
55
+ def perform(url, options = nil)
56
+ instance = new(url, options)
57
+ begin
58
+ yield instance
59
+ ensure
60
+ instance.close
61
+ end
62
+ end
63
+ end
64
+
65
+ # The server URI.
66
+ attr_reader :uri
67
+ # The base path on the server, always ending with a slash.
68
+ attr_reader :base_path
69
+ # Options passed during construction.
70
+ attr_reader :options
71
+
72
+ def initialize(url, options)
73
+ @uri = URI.parse(url.to_s)
74
+ @base_path = @uri.path || "/"
75
+ @base_path += "/" unless @base_path[-1] == ?/
76
+ @options = options || {}
77
+ end
78
+
79
+ # Downloads a file from the specified path, relative to the
80
+ # server URI. Downloads to either the target file, or by
81
+ # calling the block with each chunk of the file.
82
+ #
83
+ # For example:
84
+ # Transports.perform("http://server/libs") do |http|
85
+ # http.download("my_project/test.jar", "test.jar")
86
+ # http.download("my_project/readme") { |text| $stdout.write text }
87
+ # end
88
+ def download(path, target, &block)
89
+ fail "Upload not implemented for this transport"
90
+ end
91
+
92
+ # Uploads a file (source) to the server at the specified path,
93
+ # relative to the server URI.
94
+ #
95
+ # For example:
96
+ # Transports.perform("sftp://server/libs") do |sftp|
97
+ # sftp.mkpath "my_project"
98
+ # sftp.upload("target/test.jar", "my_project/test.jar")
99
+ # end
100
+ def upload(source, path)
101
+ fail "Upload not implemented for this transport"
102
+ end
103
+
104
+ # Creates a path on the server relative to the server URI.
105
+ # See #upload for example.
106
+ def mkpath(path)
107
+ fail "Upload not implemented for this transport"
108
+ end
109
+
110
+ protected
111
+
112
+ # :call-seq:
113
+ # with_progress_bar(file_name, length) { |progress| ... }
114
+ #
115
+ # Displays a progress bar while executing the block. The first
116
+ # argument provides a filename to display, the second argument
117
+ # its size in bytes.
118
+ #
119
+ # The block is yielded with a progress object that implements
120
+ # a single method. Call << for each block of bytes down/uploaded.
121
+ def with_progress_bar(file_name, length)
122
+ if verbose && $stdout.isatty
123
+ progress_bar = Console::ProgressBar.new(file_name, length)
124
+ # Extend the progress bar so we can display count/total.
125
+ class << progress_bar
126
+ def total()
127
+ convert_bytes(@total)
128
+ end
129
+ end
130
+ # Squeeze the filename into 30 characters.
131
+ if file_name.size > 30
132
+ base, ext = file_name.split(".")
133
+ truncated = "#{base[0..26-ext.size]}...#{ext}"
134
+ else
135
+ truncated = file_name
136
+ end
137
+ progress_bar.format = "#{truncated}: %3d%% %s %s/%s %s"
138
+ progress_bar.format = "%3d%% %s %s/%s %s"
139
+ progress_bar.format_arguments = [:percentage, :bar, :bytes, :total, :stat]
140
+ progress_bar.bar_mark = "."
141
+
142
+
143
+ begin
144
+ class << progress_bar
145
+ def <<(bytes)
146
+ inc bytes.respond_to?(:size) ? bytes.size : bytes
147
+ end
148
+ end
149
+ yield progress_bar
150
+ ensure
151
+ progress_bar.finish
152
+ end
153
+ else
154
+ progress_bar = Object.new
155
+ class << progress_bar
156
+ def <<(bytes)
157
+ end
158
+ end
159
+ yield progress_bar
160
+ end
161
+ end
162
+
163
+ # :call-seq:
164
+ # with_digests(types?) { |digester| ... } => hash
165
+ #
166
+ # Use the Digester to create digests for files you are downloading or
167
+ # uploading, and either verify their signatures (download) or create
168
+ # signatures (upload).
169
+ #
170
+ # The method takes one argument with the list of digest algorithms to
171
+ # support. Leave if empty and it will default to MD5 and SHA1.
172
+ #
173
+ # The method then yields the block passing it a Digester. The Digester
174
+ # supports two methods. Use << to pass data that you are down/uploading.
175
+ # Once all data is transmitted, use each to iterate over the digests.
176
+ # The each method calls the block with the digest type (e.g. "md5")
177
+ # and the hexadecimal digest value.
178
+ #
179
+ # For example:
180
+ # with_digests do |digester|
181
+ # download url do |block|
182
+ # digester << block
183
+ # end
184
+ # digester.each do |type, hexdigest|
185
+ # signature = download "#{url}.#{type}"
186
+ # fail "Mismatch" unless signature == hexdigest
187
+ # end
188
+ # end
189
+ def with_digests(types = nil)
190
+ digester = Digester.new(types)
191
+ yield digester
192
+ digester.to_hash
193
+ end
194
+
195
+ class Digester
196
+
197
+ def initialize(types)
198
+ types ||= [ "md5", "sha1" ]
199
+ @digests = types.inject({}) do |hash, type|
200
+ hash[type.to_s.downcase] = Digest.const_get(type.to_s.upcase).new
201
+ hash
202
+ end
203
+ end
204
+
205
+ # Add bytes for digestion.
206
+ def <<(bytes)
207
+ @digests.each { |type, digest| digest << bytes }
208
+ end
209
+
210
+ # Iterate over all the digests calling the block with two arguments:
211
+ # the digest type (e.g. "md5") and the hexadecimal digest value.
212
+ def each()
213
+ @digests.each { |type, digest| yield type, digest.hexdigest }
214
+ end
215
+
216
+ # Returns a hash that maps each digest type to its hexadecimal digest value.
217
+ def to_hash()
218
+ @digests.keys.inject({}) do |hash, type|
219
+ hash[type] = @digests[type].hexdigest
220
+ hash
221
+ end
222
+ end
223
+
224
+ end
225
+
226
+ end
227
+
228
+
229
+ class HTTP < Transport
230
+
231
+ def initialize(url, options)
232
+ super
233
+ @http = Net::HTTP.start(@uri.host, @uri.port)
234
+ end
235
+
236
+ def download(path, target = nil, &block)
237
+ puts "Requesting #{path} from #{@uri}" if Rake.application.options.trace
238
+ @http.request_get(@base_path + path) do |response|
239
+ case response
240
+ when Net::HTTPRedirection
241
+ # Try to download from the new URI, handle relative redirects.
242
+ puts "Redirected" if Rake.application.options.trace
243
+ Transports.download(@uri + URI.parse(response["location"]), target, @options)
244
+
245
+ when Net::HTTPOK
246
+ puts "Downloading #{@uri}/#{path}" if verbose
247
+ with_progress_bar path.split("/").last, response.content_length do |progress|
248
+ with_digests(@options[:digests]) do |digester|
249
+
250
+ download = proc do |write|
251
+ # Read the body of the page and write it out.
252
+ response.read_body do |chunk|
253
+ write[chunk]
254
+ digester << chunk
255
+ progress << chunk
256
+ end
257
+ # Check server digests before approving the download.
258
+ digester.each do |type, hexdigest|
259
+ @http.request_get("#{@base_path}#{path}.#{type.to_s.downcase}") do |response|
260
+ if Net::HTTPOK === response
261
+ puts "Checking signature from #{@uri}/#{path}" if Rake.application.options.trace
262
+ fail "Checksum failure for #{@uri}/#{path}: #{type.to_s.upcase} digest on server did not match downloaded file" unless
263
+ response.read_body.split.first == hexdigest
264
+ end
265
+ end
266
+ end
267
+ end
268
+
269
+ if target
270
+ # If download breaks we end up with a partial file which is
271
+ # worse than not having a file at all, so download to temporary
272
+ # file and then move over.
273
+ temp = Tempfile.new(File.basename(target))
274
+ download[ proc { |chunk| temp.write chunk } ]
275
+ temp.close
276
+ File.move temp.path, target if target
277
+ else
278
+ download[ block ]
279
+ end
280
+
281
+ end
282
+ end
283
+ when Net::HTTPNotFound
284
+ raise NotFound
285
+ else
286
+ fail "Failed to download #{@uri}/#{path}: #{response.message}"
287
+ end
288
+ end
289
+ end
290
+
291
+ def close()
292
+ @http.finish
293
+ @http = nil
294
+ end
295
+
296
+ end
297
+
298
+
299
+ class SFTP < Transport
300
+
301
+ class << self
302
+ def passwords()
303
+ @passwords ||= {}
304
+ end
305
+ end
306
+
307
+ attr_reader :sftp
308
+
309
+ def initialize(url, options)
310
+ super
311
+ # SSH options are based on the username/password from the URI.
312
+ ssh_options = { :port=>@uri.port, :username=>@uri.user }.merge(options || {})
313
+ ssh_options[:password] ||= SFTP.passwords[@uri.host]
314
+ begin
315
+ puts "Connecting to #{@uri.host}" if Rake.application.options.trace
316
+ session = Net::SSH.start(@uri.host, ssh_options)
317
+ SFTP.passwords[@uri.host] = ssh_options[:password]
318
+ rescue Net::SSH::AuthenticationFailed=>ex
319
+ # Only if running with console, prompt for password.
320
+ if !ssh_options[:password] && $stdout.isatty
321
+ password = HighLine.new.ask("Password for #{@uri.host}:") { |q| q.echo = "*" }
322
+ ssh_options[:password] = password
323
+ retry
324
+ end
325
+ raise
326
+ end
327
+ @sftp = session.sftp.connect
328
+ puts "connected" if Rake.application.options.trace
329
+ end
330
+
331
+ def upload(source, path)
332
+ File.open(source) do |file|
333
+ with_progress_bar path.split("/").last, File.size(source) do |progress|
334
+ with_digests(@options[:digests]) do |digester|
335
+ puts "Uploading to #{@base_path}#{path}" if Rake.application.options.trace
336
+ @sftp.open_handle(@base_path + path, "w") do |handle|
337
+ # Writing in chunks gives us the benefit of a progress bar,
338
+ # but also require that we maintain a position in the file,
339
+ # since write() with two arguments always writes at position 0.
340
+ pos = 0
341
+ while chunk = file.read(32 * 4096)
342
+ @sftp.write(handle, chunk, pos)
343
+ pos += chunk.size
344
+ digester << chunk
345
+ progress << chunk
346
+ end
347
+ end
348
+
349
+ # Upload all the digests.
350
+ digester.each do |type, hexdigest|
351
+ puts "Uploading signature to #{@base_path}#{path}.#{type}" if Rake.application.options.trace
352
+ @sftp.open_handle("#{@base_path}#{path}.#{type}", "w") do |handle|
353
+ @sftp.write(handle, "#{hexdigest} #{path}")
354
+ end
355
+ end
356
+ end
357
+
358
+ end
359
+ end
360
+ end
361
+
362
+ def mkpath(path)
363
+ # To create a path, we need to create all its parent.
364
+ # We use realpath to determine if the path already exists,
365
+ # otherwise mkdir fails.
366
+ puts "Creating path #{@base_path}" if Rake.application.options.trace
367
+ path.split("/").inject(@base_path) do |base, part|
368
+ @sftp.realpath(base+part) rescue @sftp.mkdir base + part
369
+ "#{base}#{part}/"
370
+ end
371
+ end
372
+
373
+ def close()
374
+ @sftp.close
375
+ @sftp = nil
376
+ end
377
+
378
+ end
379
+
380
+ end
381
+ end
@@ -0,0 +1,316 @@
1
+ module Buildr
2
+ module Java
3
+
4
+ class CompileTask < Rake::Task
5
+
6
+ # Compiler options, accessible from Compiler#options.
7
+ #
8
+ # Supported options are:
9
+ # - warnings -- Generate warnings if true (opposite of -nowarn).
10
+ # - verbose -- Output messages about what the compiler is doing.
11
+ # - deprecation -- Output source locations where deprecated APIs
12
+ # are used.
13
+ # - source -- Source compatibility with specified release.
14
+ # - target -- Class file compatibility with specified release.
15
+ # - lint -- Value to pass to xlint argument. Use true to enable
16
+ # default lint options, or pass a specific setting as string
17
+ # or array of strings.
18
+ # - debug -- Generate debugging info.
19
+ # - other -- Array of options to pass to the Java compiler as is.
20
+ #
21
+ # For example:
22
+ # options.verbose = true
23
+ # options.source = options.target = "1.6"
24
+ class Options
25
+
26
+ include Attributes
27
+
28
+ OPTIONS = [:warnings, :verbose, :deprecation, :source, :target, :lint, :debug, :other]
29
+
30
+ # Generate warnings (opposite of -nowarn).
31
+ inherited_attr :warnings
32
+ # Output messages about what the compiler is doing.
33
+ inherited_attr :verbose
34
+ # Output source locations where deprecated APIs are used.
35
+ inherited_attr :deprecation
36
+ # Provide source compatibility with specified release.
37
+ inherited_attr :source
38
+ # Generate class files for specific VM version.
39
+ inherited_attr :target
40
+ # Values to pass to Xlint: string or array. Use true to enable
41
+ # Xlint with no values.
42
+ inherited_attr :lint
43
+ # Generate all debugging info.
44
+ inherited_attr :debug
45
+ # Array of arguments passed to the Java compiler as is.
46
+ inherited_attr :other
47
+
48
+ def initialize(parent = nil)
49
+ @parent = parent
50
+ end
51
+
52
+ attr_reader :parent
53
+
54
+ def clear()
55
+ OPTIONS.each { |name| send "#{name}=", nil }
56
+ end
57
+
58
+ def to_s()
59
+ OPTIONS.inject({}){ |hash, name| hash[name] = send(name) ; hash }.reject{ |name,value| value.nil? }.inspect
60
+ end
61
+
62
+ # Returns Javac command line arguments from the set of options.
63
+ def javac_args()
64
+ args = []
65
+ args << "-nowarn" unless warnings && Rake.application.options.trace
66
+ args << "-verbose" if verbose
67
+ args << "-g" if debug
68
+ args << "-deprecation" if deprecation
69
+ args << ["-source", source.to_s] if source
70
+ args << ["-target", target.to_s] if target
71
+ case lint
72
+ when Array
73
+ args << "-Xlint:#{lint.join(',')}"
74
+ when String
75
+ args << "-Xlint:#{lint}"
76
+ when true
77
+ args << "-Xlint"
78
+ end
79
+ args << other if other
80
+ args
81
+ end
82
+
83
+ end
84
+
85
+ # The target directory for the generated class files.
86
+ attr_accessor :target
87
+
88
+ def initialize(*args)
89
+ super
90
+ if name[":"] # Only if in namespace
91
+ parent = Rake::Task["^compile"]
92
+ if parent && parent.respond_to?(:options)
93
+ @options = Options.new(parent.options)
94
+ end
95
+ end
96
+
97
+ enhance do |task|
98
+ # Do we have any files to compile?
99
+ if target && files.empty?
100
+ puts "All source files are up to date" if Rake.application.options.trace && !sources.empty?
101
+ elsif target
102
+ mkpath target, :verbose=>false
103
+ Java.javac files, :sourcepath=>sourcepath, :classpath=>classpath,
104
+ :output=>target, :javac_args=>options.javac_args, :name=>task.name
105
+ # By touching the target we let other tasks know we did something,
106
+ # and also prevent recompiling again for classpath dependencies.
107
+ touch target, :verbose=>false
108
+ end
109
+ end
110
+ end
111
+
112
+ # An array of source directories and files.
113
+ def sources()
114
+ @sources ||= []
115
+ end
116
+
117
+ def sources=(paths)
118
+ @sources = paths.flatten
119
+ end
120
+
121
+ # Array of classpath dependencies: files, file tasks, artifacts specs.
122
+ def classpath()
123
+ @classpath ||= []
124
+ end
125
+
126
+ def classpath=(paths)
127
+ @classpath = paths.flatten
128
+ end
129
+
130
+ # Returns the compiler options.
131
+ def options()
132
+ @options ||= Options.new
133
+ end
134
+
135
+ # Sets the compile options based on a hash of values, or reset to
136
+ # the default by passing nil.
137
+ def options=(arg)
138
+ case arg
139
+ when Options
140
+ @options = arg
141
+ when Hash
142
+ options.clear
143
+ arg.each { |k,v| options.send "#{k}=", v }
144
+ when nil
145
+ options.clear
146
+ else
147
+ raise ArgumentError, "Expecting Options, hash or nil (to reset)"
148
+ end
149
+ @options
150
+ end
151
+
152
+ # Sets the target directory and returns self. For example:
153
+ # compile(src_dir).to(target_dir).with(artifacts)
154
+ def to(dir)
155
+ self.target = dir
156
+ self
157
+ end
158
+
159
+ # Adds files and artifacts to the classpath and returns self.
160
+ # For example:
161
+ # compile(src_dir).to(target_dir).with(artifact, file, task)
162
+ def with(*args)
163
+ self.classpath |= args.flatten
164
+ self
165
+ end
166
+
167
+ # Sets the compiler options and returns self. For example:
168
+ # compile(src_dir).using(:warnings=>true, :verbose=>true)
169
+ def using(hash)
170
+ self.options = hash
171
+ self
172
+ end
173
+
174
+ # Returns true if any classes were compiled.
175
+ def compiled?()
176
+ @files && !@files.empty?
177
+ end
178
+
179
+ protected
180
+
181
+ # Returns the real classpath. Uses the values of #classpath, but resolves
182
+ # artifact specifications, projects and other conveniences, executes tasks
183
+ # (see #sanitize) and returns a compact array of unique file names.
184
+ def real_classpath()
185
+ @real_classpath ||= sanitize(artifacts(classpath))
186
+ end
187
+
188
+ # Return the sourcepath, essentialy compact array of directory and file names,
189
+ # as set by the user, but after invoking dependencies (see #sanitize).
190
+ def sourcepath()
191
+ @real_sources ||= sanitize(sources)
192
+ @real_sources.select { |source| File.directory?(source) }
193
+ end
194
+
195
+ # Returns the files to compile. This list is derived from the list of sources,
196
+ # expanding directories into files, and includes only source files that are
197
+ # newer than the corresponding class file. Includes all files if one or more
198
+ # classpath dependency has been updated.
199
+ def files()
200
+ unless @files
201
+ @real_sources ||= sanitize(sources)
202
+ # Compile all files if we compiled nothing, or one of the classpath
203
+ # dependencies is newer than the compiled classes.
204
+ all = !File.exist?(target) ||
205
+ File.stat(target).mtime < (real_classpath.collect{ |file| File.stat(file).mtime } + [ Rake::EARLY ]).max
206
+
207
+ if all # Do not restrict to changed files
208
+ @files = @real_sources.collect do |source|
209
+ File.directory?(source) ? Dir[File.join(source, "**", "*.java")] : source
210
+ end.flatten
211
+ else
212
+ # Only changed files.
213
+ @files = @real_sources.collect do |source|
214
+ if File.directory?(source)
215
+ Dir[File.join(source, "**", "*.java")].select do |java|
216
+ klass = java.sub(source, target).ext(".class")
217
+ !File.exist?(klass) || File.stat(java).mtime > File.stat(klass).mtime
218
+ end
219
+ else
220
+ source
221
+ end
222
+ end.flatten
223
+ end
224
+ end
225
+ @files
226
+ end
227
+
228
+ def sanitize(paths)
229
+ # Flatten to handle nested arrays often used with dependencies.
230
+ # Invoke all tasks, treated as prerequisites (e.g. download artifacts).
231
+ # Return task name or file name, ignoring nils and duplicates.
232
+ paths.flatten.each { |path| path.invoke if path.respond_to?(:invoke) }.
233
+ collect { |path| path.respond_to?(:name) ? path.name : path.to_s }.
234
+ compact.uniq
235
+ end
236
+
237
+ end
238
+
239
+ end
240
+
241
+
242
+ # Create and return a compiler task. The task is name "compile" in the current
243
+ # namespace. Method arguments are passed as sources to compile, and options are
244
+ # inherited from any compile task in a parent namespace.
245
+ #
246
+ # For example:
247
+ # compile("src").to("classes").with(artifact, file, task)
248
+ def self.compile(*sources)
249
+ returning(Java::CompileTask.define_task("compile")) do |task|
250
+ task.sources |= sources
251
+ end
252
+ end
253
+
254
+ def compile(*sources)
255
+ Buildr.compile(*sources)
256
+ end
257
+
258
+ # Global task compiles all projects.
259
+ desc "Compile all projects"
260
+ LocalDirectoryTask.define_task "compile"
261
+
262
+
263
+ class Project
264
+
265
+ # The source directory. The default is "src".
266
+ inherited_attr :src_dir, "src"
267
+ # The Java source code directory. The default is <src_dir>/main/java.
268
+ inherited_attr :java_src_dir do File.join(src_dir, "main", "java") end
269
+ # The resources source directory. The default is <src_dir>/main/resources.
270
+ inherited_attr :resources_dir do File.join(src_dir, "main", "resources") end
271
+ # The target directory. The default is "target".
272
+ inherited_attr :target_dir, "target"
273
+ # The Java target directory. The default is <target_dir>/classes.
274
+ inherited_attr :java_target_dir do File.join(target_dir, "classes") end
275
+
276
+ def resources(*tasks, &block)
277
+ returning(@resources_task ||= Filter.define_task("resources")) do |task|
278
+ task.enhance tasks, &block
279
+ end
280
+ end
281
+
282
+ def compile(*sources, &block)
283
+ returning(@compile_task ||= Java::CompileTask.define_task("compile")) do |task|
284
+ task.sources |= sources
285
+ task.enhance &block if block
286
+ end
287
+ end
288
+
289
+ def prepare(*tasks, &block)
290
+ returning(@prepare_task ||= task("prepare")) do |task|
291
+ task.enhance tasks, &block
292
+ end
293
+ end
294
+
295
+ end
296
+
297
+
298
+ Project.on_create do |project|
299
+ # Prepare is prerequisite for compile, resources follows compile.
300
+ project.compile.enhance([project.prepare]) { |task| project.resources.invoke }
301
+ project.recursive_task("compile")
302
+ task("build").enhance [ project.compile ]
303
+ task("clean") { rm_rf project.path_to(:target_dir), :verbose=>false }
304
+
305
+ project.enhance do |project|
306
+ # Automagic compilation only if the source directory exists.
307
+ project.compile.sources << project.path_to(:java_src_dir) if File.exist?(project.path_to(:java_src_dir))
308
+ project.compile.target ||= project.path_to(:java_target_dir)
309
+ project.resources.include project.path_to(:resources_dir, "*") if File.exist?(project.path_to(:resources_dir))
310
+ project.resources.target ||= project.compile.target
311
+ file(project.compile.target).enhance([ project.compile, project.resources ])
312
+ end
313
+
314
+ end
315
+
316
+ end