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