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