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