require "tempfile" require "pathname" require "core/transports" require "open-uri" require "uri/open-sftp" class Hash class << self # :call-seq: # Hash.from_java_properties(string) # # Returns a hash from a string in the Java properties file format. For example: # str = "foo=bar\nbaz=fab" # Hash.from_properties(str) # => { "foo"=>"bar", "baz"=>"fab" }.to_properties def from_java_properties(string) string.gsub(/\\\n/, "").split("\n").select { |line| line =~ /^[^#].*=.*/ }. map { |line| line.gsub(/\\[trnf\\]/) { |escaped| {?t=>"\t", ?r=>"\r", ?n=>"\n", ?f=>"\f", ?\\=>"\\"}[escaped[1]] } }. inject({}) { |hash, line| name, value = line.split("=") ; hash[name] = value ; hash } end end # :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 # :call-seq: # to_java_properties() => string # # Convert hash to string format used for Java properties file. For example: # { "foo"=>"bar", "baz"=>"fab" }.to_properties # => foo=bar # baz=fab def to_java_properties() keys.sort.map { |key| value = self[key].gsub(/[\t\r\n\f\\]/) { |escape| "\\" + {"\t"=>"t", "\r"=>"r", "\n"=>"n", "\f"=>"f", "\\"=>"\\"}[escape] } "#{key}=#{value}" }.join("\n") 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 # We use this to present environment variable as arrays. class EnvArray < Array #:nodoc: def initialize(name) @name = name.upcase replace((ENV[@name] || ENV[@name.downcase] || "").split(/\s*,\s*/).reject(&:empty?)) end (Array.instance_methods - Object.instance_methods - Enumerable.instance_methods).sort.each do |method| class_eval %{def #{method}(*args, &block) ; result = super ; write ; result ; end} end private def write() ENV[@name.downcase] = nil ENV[@name] = map(&:to_s).join(",") end end # Wraps around the proxy environment variables: # * :http -- HTTP_PROXY # * :exclude -- NO_PROXY class Proxies # Returns the HTTP_PROXY URL. def http() ENV["HTTP_PROXY"] || ENV["http_proxy"] end # Sets the HTTP_PROXY URL. def http=(url) ENV["http_proxy"] = nil ENV["HTTP_PROXY"] = url end # Returns list of hosts to exclude from proxying (NO_PROXY). def exclude() @exclude ||= EnvArray.new("NO_PROXY") end # Sets list of hosts to exclude from proxy (NO_PROXY). Accepts host name, array of names, # or nil to clear the list. def exclude=(url) exclude.clear exclude.concat [url].flatten if url exclude end end # :call-seq: # proxy() => options # # Returns the proxy options. Currently supported options are: # * :http -- HTTP proxy for use when downloading. # * :exclude -- Do not use proxy for these hosts/domains. # # For example: # options.proxy.http = "http://proxy.acme.com:8080" # You can also set it using the environment variable HTTP_PROXY. # # You can exclude individual hosts from being proxied, or entire domains, for example: # options.proxy.exclude = "optimus" # options.proxy.exclude = ["optimus", "prime"] # options.proxy.exclude << "*.internal" def proxy() @proxy ||= Proxies.new 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. # # A filter has one target directory, but you can specify any number of source directories, # either when creating the filter or calling #from. Include/exclude patterns are specified # relative to the source directories, so: # filter.include "*.png" # will only include PNG files from any of the source directories. # # See Buildr#filter. class Filter def initialize() #:nodoc: @include = [] @exclude = [] @sources = [] end # Returns the list of source directories (each being a file task). attr_reader :sources # *Deprecated* Use #sources instead. def source() warn_deprecated "Please use sources instead." @sources.first end # *Deprecated* Use #from instead. def source=(dir) warn_deprecated "Please use from instead." from(dir) end # :call-seq: # from(*sources) => self # # Adds additional directories from which to copy resources. # # For example: # filter.from("src").into("target").using("build"=>Time.now) def from(*sources) @sources |= sources.flatten.map { |dir| 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 sources.empty? sources.each { |source| 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? copy_map = sources.flatten.map(&:to_s).inject({}) do |map, source| base = Pathname.new(source) files = FileList[File.join(source, "**/*")].reject { |file| File.directory?(file) }. map { |file| Pathname.new(file).relative_path_from(base).to_s }. select { |file| @include.empty? || @include.any? { |pattern| File.fnmatch(pattern, file) } }. reject { |file| @exclude.any? { |pattern| File.fnmatch(pattern, file) } } files.each do |file| src, dest = File.expand_path(file, source), File.expand_path(file, target.to_s) map[file] = src if !File.exist?(dest) || File.stat(src).mtime > File.stat(dest).mtime end map end return false if copy_map.empty? verbose(Rake.application.options.trace || false) do mkpath target.to_s copy_map.each do |path, source| dest = File.expand_path(path, target.to_s) mkpath File.dirname(dest) rescue nil case mapping when Proc, Method # Call on input, accept output. mapped = mapping.call(path, File.open(source, "rb") { |file| file.read }) File.open(dest, "wb") { |file| file.write mapped } when Hash # Map ${key} to value mapped = File.open(source, "rb") { |file| file.read }. gsub(/\$\{[^}]*\}/) { |str| mapping[str[2..-2]] || str } File.open(dest, "wb") { |file| file.write mapped } when nil # No mapping. cp source, 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(ies) 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(*sources) Filter.new.from(*sources) 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