require "tempfile" require "pathname" require "core/transports" require "open-uri" require "uri/open-sftp" class Hash # :call-seq: # only(keys*) => hash # # Returns a new hash with only the specified keys. # # For example: # { :a=>1, :b=>2, :c=>3, :d=>4 }.only(:a, :c) # => { :b=>2, :d=>4 } def only(*keys) self.inject({}) { |hash, pair| hash[pair[0]] = pair[1] if keys.include?(pair[0]) ; hash } end # :call-seq: # except(keys*) => hash # # Returns a new hash without the specified keys. # # For example: # { :a=>1, :b=>2, :c=>3, :d=>4 }.except(:a, :c) # => { :a=>1, :c=>3 } def except(*keys) self.inject({}) { |hash, pair| hash[pair[0]] = pair[1] unless keys.include?(pair[0]) ; hash } end end module Buildr # Collection of options for controlling Buildr. For example for running builds without running # test cases, using a proxy server, JVM arguments, etc. You access this object by calling options, # for example: # options.proxy.http = "http://proxy.acme.com:8080" # options.java_args = "-Xmx512M" class Options # :call-seq: # proxy() => options # # Returns the proxy options. Currently supported options are: # * :http -- HTTP proxy for use when downloading. # # For example: # options.proxy.http = "http://proxy.acme.com:8080" # You can also set it using the environment variable HTTP_PROXY. def proxy() @proxy ||= Struct.new(:http).new(ENV['HTTP_PROXY'] || ENV['http_proxy']) end end class << self # :call-seq: # options() => Options # # Returns the Buildr options. See Options. def options() @options ||= Options.new end end # :call-seq: # options() => Options # # Returns the Buildr options. See Options. def options() Buildr.options end # :call-seq: # struct(hash) => Struct # # Convenience method for creating an anonymous Struct. # # For example: # COMMONS = struct( # :collections =>"commons-collections:commons-collections:jar:3.1", # :lang =>"commons-lang:commons-lang:jar:2.1", # :logging =>"commons-logging:commons-logging:jar:1.0.3", # ) # # compile.with COMMONS.logging def struct(hash) Struct.new(nil, *hash.keys).new(*hash.values) end # :call-seq: # write(name, content) # write(name) { ... } # # Write the contents into a file. The second form calls the block and writes the result. # # For example: # write "TIMESTAMP", Time.now # write("TIMESTAMP") { Time.now } # # Yields to the block before writing the file, so you can chain read and write together. # For example: # write("README") { read("README").sub("${build}", Time.now) } def write(name, content = nil) mkpath File.dirname(name), :verbose=>false content = yield if block_given? File.open(name.to_s, "wb") { |file| file.write content.to_s } content.to_s end # :call-seq: # read(name) => string # read(name) { |string| ... } => result # # Reads and returns the contents of a file. The second form yields to the block and returns # the result of the block. # # For example: # puts read("README") # read("README") { |text| puts text } def read(name) contents = File.open(name.to_s) { |f| f.read } if block_given? yield contents else contents end end # :call-seq: # download(url_or_uri) => task # download(path=>url_or_uri) =>task # # Create a task that will download a file from a URL. # # Takes a single argument, a hash with one pair. The key is the file being # created, the value if the URL to download. The task executes only if the # file does not exist; the URL is not checked for updates. # # The task will show download progress on the console; if there are MD5/SHA1 # checksums on the server it will verify the download before saving it. # # For example: # download "image.jpg"=>"http://example.com/theme/image.jpg" def download(args) args = URI.parse(args) if String === args if URI === args # Given only a download URL, download into a temporary file. # You can infer the file from task name. temp = Tempfile.open(File.basename(args.to_s)) file(temp.path).tap do |task| # Since temporary file exists, force a download. class << task ; def needed?() ; true ; end ; end task.sources << args task.enhance { args.download temp, :proxy=>Buildr.options.proxy } end else # Download to a file created by the task. fail unless args.keys.size == 1 uri = URI.parse(args.values.first.to_s) file_create(args.keys.first).tap do |task| task.sources << uri task.enhance { uri.download task.name, :proxy=>Buildr.options.proxy } end end end # A filter knows how to copy files from one directory to another, applying mappings to the # contents of these files. # # You can specify the mapping using a Hash, and it will map ${key} fields found in each source # file into the appropriate value in the target file. For example: # filter.using "version"=>"1.2", "build"=>Time.now # will replace all occurrences of ${version} with 1.2, and ${build} # with the current date/time. # # You can also specify the mapping by passing a proc or a method, that will be called for # each source file, with the file name and content, returning the modified content. # # Without any mapping, the filter simply copies files from the source directory into the target # directory. # # See Buildr#filter. class Filter def initialize() #:nodoc: @include = [] @exclude = [] end # The source directory as a file task. attr_accessor :source # :call-seq: # from(dir) => self # # Sets the source directory from which files are copied and returns self. # # For example: # filter.from("src").into("target").using("build"=>Time.now) def from(dir) @source = file(File.expand_path(dir.to_s)) self end # The target directory as a file task. attr_reader :target # :call-seq: # into(dir) => self # # Sets the target directory into which files are copied and returns self. # # For example: # filter.from("src").into("target").using("build"=>Time.now) def into(dir) @target = file(File.expand_path(dir.to_s)) self end # :call-seq: # include(*files) => self # # Specifies files to include and returns self. See FileList#include. # # By default all files are included. You can use this method to only include specific # files form the source directory. def include(*files) @include += files self end alias :add :include # :call-seq: # exclude(*files) => self # # Specifies files to exclude and returns self. See FileList#exclude. def exclude(*files) @exclude += files self end # The mapping. See #using. attr_accessor :mapping # :call-seq: # using(mapping) => self # using() { |file_name, contents| ... } => self # # Specifies the mapping to use and returns self. # # The mapping can be a proc or a method called with the file name and content, returning # the modified content. Or the mapping can be a Hash for mapping each ${key} into a value. # Without any mapping, all files are copied as is. # # For example: # filter.using "version"=>"1.2" # will replace all occurrences of "${version}" with "1.2". def using(mapping = nil, &block) self.mapping = mapping || block self end # :call-seq: # run() => boolean # # Runs the filter. def run() raise "No source directory specified, where am I going to find the files to filter?" if source.nil? raise "Source directory #{source} doesn't exist" unless File.exist?(source.to_s) raise "No target directory specified, where am I going to copy the files to?" if target.nil? includes = @include.empty? ? ["*"] : @include src_base = Pathname.new(source.to_s) copy_map = Dir[File.join(source.to_s, "**/*")].reject { |file| File.directory?(file) }. map { |src| Pathname.new(src).relative_path_from(src_base).to_s }. select { |file| includes.any? { |pattern| File.fnmatch(pattern, file) } }. reject { |file| @exclude.any? { |pattern| File.fnmatch(pattern, file) } }. map { |file| [File.expand_path(file, target.to_s), File.expand_path(file, source.to_s)] }. select { |dest, src| !File.exist?(dest) || File.stat(src).mtime > File.stat(dest).mtime } return false if copy_map.empty? verbose(Rake.application.options.trace || false) do mkpath target.to_s copy_map.each do |dest, src| mkpath File.dirname(dest) rescue nil case mapping when Proc, Method # Call on input, accept output. relative = Pathname.new(src).relative_path_from(src_base).to_s mapped = mapping.call(relative, File.open(src, "rb") { |file| file.read }) File.open(dest, "wb") { |file| file.write mapped } when Hash # Map ${key} to value mapped = File.open(src, "rb") { |file| file.read }. gsub(/\$\{[^}]*\}/) { |str| mapping[str[2..-2]] || str } File.open(dest, "wb") { |file| file.write mapped } when nil # No mapping. cp src, dest else fail "Filter can be a hash (key=>value), or a proc/method; I don't understand #{mapping}" end end touch target.to_s end true end # Returns the target directory. def to_s() @target.to_s end end # :call-seq: # filter(source) => Filter # # Creates a filter that will copy files from the source directory into the target directory. # You can extend the filter to modify files by mapping ${key} into values in each # of the copied files, and by including or excluding specific files. # # A filter is not a task, you must call the Filter#run method to execute it. # # For example, to copy all files from one directory to another: # filter("src/files").into("target/classes").run # To include only the text files, and replace each instance of ${build} with the current # date/time: # filter("src/files").into("target/classes").include("*.txt").using("build"=>Time.now).run def filter(source) Filter.new.from(source) end end # Add a touch of colors (red) to warnings. HighLine.use_color = PLATFORM !~ /win32/ module Kernel #:nodoc: def warn_with_color(message) warn_without_color $terminal.color(message.to_s, :red) end alias_method_chain :warn, :color # :call-seq: # warn_deprecated(message) # # Use with deprecated methods and classes. This method automatically adds the file name and line number, # and the text "Deprecated" before the message, and eliminated duplicate warnings. It only warns when # running in verbose mode. # # For example: # warn_deprecated "Please use new_foo instead of foo." def warn_deprecated(message) #:nodoc: return unless verbose "#{caller[1]}: Deprecated: #{message}".tap do |message| @deprecated ||= {} unless @deprecated[message] @deprecated[message] = true warn message end end end end