require "tempfile"
require "pathname"
require "core/transports"
module Buildr
# :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)
content = yield if block_given?
File.open(name.to_s, "w") { |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)
if block_given?
yield File.read(name.to_s)
else
File.read(name.to_s)
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)
if String === args || URI === args
# Given only a download URL, download into a temporary file.
# You can infer the file from task name.
temp = Tempfile.new(File.basename(args.to_s))
task = file_create(temp.path) do |task|
Transports.download task.source, task.name
end
task.sources << args
else
# Download to a file created by the task.
fail unless args.keys.size == 1
url = args.values.first
task = file_create(args.keys.first) do |task|
mkpath File.dirname(task.name), :verbose=>false
Transports.download task.source, task.name
end
task.sources << url
end
task
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, &block)
self.mapping = mapping || block
self
end
# Runs the filter.
def run()
if needed?
unless copy_map.empty?
verbose(Rake.application.options.trace || false) do
mkpath target.to_s
copy_map do |dest, src|
mkpath File.dirname(dest) rescue nil
case mapping
when Proc, Method # Call on input, accept output.
mapped = mapping.call(src, 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
end
end
end
# Returns the target directory.
def to_s()
@target.to_s
end
private
def needed?()
return false if target.nil? || source.nil? || !File.exist?(source.to_s)
return true unless File.exist?(target.to_s)
!copy_map.empty?
end
# Return a copy map of all the files that need copying: the key is the file to copy to,
# the value is the source file. If called with a block, yields with each dest/source pair.
def copy_map(&block)
unless @copy_map
@include = ["*"] if @include.empty?
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(base).to_s }.
select { |file| @include.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 }
end
if block_given?
@copy_map.each(&block)
else
@copy_map
end
end
end
# :call-seq:
# filter(target=>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("target/classes"=>"src/files")
# To include only the text files, and replace each instance of ${build} with the current
# date/time:
# filter("target/classes"=>"src/files").include("*.txt").using("build"=>Time.now)
def filter(args)
target, source = Rake.application.resolve_args(args)
Filter.new.into(target).tap { |filter| filter.from(source) if source }
end
end