blob: dfe664f8178732c27c07b901d8e3b1ce861d6acf [file] [log] [blame]
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with this
# work for additional information regarding copyright ownership. The ASF
# licenses this file to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under
# the License.
module Buildr #:nodoc:
# Base class for ZipTask, TarTask and other archives.
class ArchiveTask < Rake::FileTask
# Which files go where. All the rules for including, excluding and merging files
# are handled by this object.
class Path #:nodoc:
# Returns the archive from this path.
attr_reader :root
def initialize(root, path)
@root = root
@path = path.empty? ? path : "#{path}/"
@includes = FileList[]
@excludes = []
# Expand source files added to this path.
expand_src = proc { @includes.map{ |file| file.to_s }.uniq }
@sources = [ expand_src ]
# Add files and directories added to this path.
@actions = [] << proc do |file_map|
expand_src.call.each do |path|
unless excluded?(path)
if File.directory?(path)
in_directory path do |file, rel_path|
dest = "#{@path}#{rel_path}"
unless excluded?(dest)
trace "Adding #{dest}"
file_map[dest] = file
end
end
end
unless File.basename(path) == "."
trace "Adding #{@path}#{File.basename(path)}"
file_map["#{@path}#{File.basename(path)}"] = path
end
end
end
end
end
# :call-seq:
# include(*files) => self
# include(*files, :path=>path) => self
# include(file, :as=>name) => self
# include(:from=>path) => self
# include(*files, :merge=>true) => self
def include(*args)
options = Hash === args.last ? args.pop : nil
files = to_artifacts(args)
raise 'AchiveTask.include() values should not include nil' if files.include? nil
if options.nil? || options.empty?
@includes.include *files.flatten
elsif options[:path]
sans_path = options.reject { |k,v| k == :path }
path(options[:path]).include *files + [sans_path]
elsif options[:as]
raise 'You can only use the :as option in combination with the :path option' unless options.size == 1
raise 'You can only use one file with the :as option' unless files.size == 1
include_as files.first.to_s, options[:as]
elsif options[:from]
raise 'You can only use the :from option in combination with the :path option' unless options.size == 1
raise 'You cannot use the :from option with file names' unless files.empty?
fail 'AchiveTask.include() :from value should not be nil' if [options[:from]].flatten.include? nil
[options[:from]].flatten.each { |path| include_as path.to_s, '.' }
elsif options[:merge]
raise 'You can only use the :merge option in combination with the :path option' unless options.size == 1
files.each { |file| merge file }
else
raise "Unrecognized option #{options.keys.join(', ')}"
end
self
end
alias :add :include
alias :<< :include
# :call-seq:
# exclude(*files) => self
def exclude(*files)
files = to_artifacts(files)
@excludes |= files
@excludes |= files.reject { |f| f =~ /\*$/ }.map { |f| "#{f}/*" }
self
end
# :call-seq:
# merge(*files) => Merge
# merge(*files, :path=>name) => Merge
def merge(*args)
options = Hash === args.last ? args.pop : {}
files = to_artifacts(args)
rake_check_options options, :path
raise ArgumentError, "Expected at least one file to merge" if files.empty?
path = options[:path] || @path
expanders = files.collect do |file|
@sources << proc { file.to_s }
expander = ZipExpander.new(file)
@actions << proc do |file_map, transform_map|
file.invoke() if file.is_a?(Rake::Task)
expander.expand(file_map, transform_map, path)
end
expander
end
Merge.new(expanders)
end
# Returns a Path relative to this one.
def path(path)
return self if path.nil?
return root.path(path[1..-1]) if path[0] == ?/
root.path("#{@path}#{path}")
end
# Returns all the source files.
def sources #:nodoc:
@sources.map{ |source| source.call }.flatten
end
def add_files(file_map, transform_map) #:nodoc:
@actions.each { |action| action.call(file_map, transform_map) }
end
# :call-seq:
# exist => boolean
#
# Returns true if this path exists. This only works if the path has any entries in it,
# so exist on path happens to be the opposite of empty.
def exist?
!entries.empty?
end
# :call-seq:
# empty? => boolean
#
# Returns true if this path is empty (has no other entries inside).
def empty?
entries.all? { |entry| entry.empty? }
end
# :call-seq:
# contain(file*) => boolean
#
# Returns true if this ZIP file path contains all the specified files. You can use relative
# file names and glob patterns (using *, **, etc).
def contain?(*files)
files.all? { |file| entries.detect { |entry| File.fnmatch(file, entry.to_s) } }
end
# :call-seq:
# entry(name) => ZipEntry
#
# Returns a ZIP file entry. You can use this to check if the entry exists and its contents,
# for example:
# package(:jar).path("META-INF").entry("LICENSE").should contain(/Apache Software License/)
def entry(name)
root.entry("#{@path}#{name}")
end
def to_s
@path
end
protected
# Convert objects to artifacts, where applicable
def to_artifacts(files)
files.flatten.inject([]) do |set, file|
case file
when ArtifactNamespace
set |= file.artifacts
when Symbol, Hash
set |= [Buildr.artifact(file)]
when /([^:]+:){2,4}/ # A spec as opposed to a file name.
set |= [Buildr.artifact(file)]
when Project
set |= Buildr.artifacts(file.packages)
when Rake::Task
set |= [file]
when Struct
set |= Buildr.artifacts(file.values)
else
# non-artifacts passed as-is; in particular, String paths are
# unmodified since Rake FileTasks don't use absolute paths
set |= [file]
end
end
end
def include_as(source, as)
@sources << proc { source }
@actions << proc do |file_map|
file = source.to_s
file(file).invoke
unless excluded?(file)
if File.directory?(file)
in_directory file do |file, rel_path|
path = rel_path.split('/')[1..-1]
path.unshift as unless as == '.'
dest = "#{@path}#{path.join('/')}"
unless excluded?(dest)
trace "Adding #{dest}"
file_map[dest] = file
end
end
unless as == "."
trace "Adding #{@path}#{as}/"
file_map["#{@path}#{as}/"] = nil # :as is a folder, so the trailing / is required.
end
else
file_map["#{@path}#{as}"] = file
end
end
end
end
def in_directory(dir)
prefix = Regexp.new('^' + Regexp.escape(File.dirname(dir) + File::SEPARATOR))
Util.recursive_with_dot_files(dir).reject { |file| excluded?(file) }.
each { |file| yield file, file.sub(prefix, '') }
end
def excluded?(file)
@excludes.any? { |exclude| File.fnmatch(exclude, file) }
end
def entries #:nodoc:
return root.entries unless @path
@entries ||= root.entries.inject([]) { |selected, entry|
selected << entry.name.sub(@path, "") if entry.name.index(@path) == 0
selected
}
end
end
class Merge
def initialize(expanders)
@expanders = expanders
end
def include(*files)
@expanders.each { |expander| expander.include(*files) }
self
end
alias :<< :include
def exclude(*files)
@expanders.each { |expander| expander.exclude(*files) }
self
end
def concatenate(*files)
@expanders.each { |expander| expander.concatenate(*files) }
self
end
def transform(*files, &block)
@expanders.each { |expander| expander.transform(*files, &block) }
self
end
end
# Extend one Zip file into another.
class ZipExpander #:nodoc:
def initialize(zip_file)
@zip_file = zip_file.to_s
@includes = []
@excludes = []
@concatenates = []
@transforms = {}
end
def include(*files)
@includes |= files
self
end
alias :<< :include
def exclude(*files)
@excludes |= files
self
end
def concatenate(*files)
@concatenates |= files
self
end
def transform(*files, &block)
@transforms[[files].flatten] = block
self
end
def expand(file_map, transform_map, path)
@includes = ['*'] if @includes.empty?
Zip::File.open(@zip_file) do |source|
source.entries.reject { |entry| entry.directory? }.each do |entry|
if @includes.any? { |pattern| File.fnmatch(pattern, entry.name) } &&
!@excludes.any? { |pattern| File.fnmatch(pattern, entry.name) }
dest = path =~ /^\/?$/ ? entry.name : Util.relative_path(path + "/" + entry.name)
trace "Adding #{dest}"
if @concatenates.any? { |pattern| File.fnmatch(pattern, entry.name) }
file_map[dest] << ZipEntryData.new(source, entry)
elsif @transforms.each_pair.detect do |transform, transform_block|\
if transform.any? { |pattern| File.fnmatch(pattern, entry.name) }
file_map[dest] << ZipEntryData.new(source, entry)
transform_map[dest] = transform_block
true
end
end
else
file_map[dest] = ZipEntryData.new(source, entry)
end
end
end
end
end
end
class ZipEntryData
def initialize(zipfile, entry)
@zipfile = zipfile
@entry = entry
end
def call(output)
output.write @zipfile.read(@entry)
end
def mode
@entry.unix_perms
end
end
def initialize(*args) #:nodoc:
super
clean
# Make sure we're the last enhancements, so other enhancements can add content.
enhance do
@file_map = Hash.new {|h,k| h[k]=[]}
@transform_map = {}
enhance do
send 'create' if respond_to?(:create)
# We're here because the archive file does not exist, or one of the files is newer than the archive contents;
# we need to make sure the archive doesn't exist (e.g. opening an existing Zip will add instead of create).
# We also want to protect against partial updates.
rm name rescue nil
mkpath File.dirname(name)
begin
@paths.each do |name, object|
@file_map[name] = nil unless name.empty?
object.add_files(@file_map, @transform_map)
end
create_from @file_map, @transform_map
rescue
rm name rescue nil
raise
end
end
end
end
# :call-seq:
# clean => self
#
# Removes all previously added content from this archive.
# Use this method if you want to remove default content from a package.
# For example, package(:jar) by default includes compiled classes and resources,
# using this method, you can create an empty jar and afterwards add the
# desired content to it.
#
# package(:jar).clean.include path_to('desired/content')
def clean
@paths = {}
@paths[''] = Path.new(self, '')
@prepares = []
self
end
# :call-seq:
# include(*files) => self
# include(*files, :path=>path) => self
# include(file, :as=>name) => self
# include(:from=>path) => self
# include(*files, :merge=>true) => self
#
# Include files in this archive, or when called on a path, within that path. Returns self.
#
# The first form accepts a list of files, directories and glob patterns and adds them to the archive.
# For example, to include the file foo, directory bar (including all files in there) and all files under baz:
# zip(..).include('foo', 'bar', 'baz/*')
#
# The second form is similar but adds files/directories under the specified path. For example,
# to add foo as bar/foo:
# zip(..).include('foo', :path=>'bar')
# The :path option is the same as using the path method:
# zip(..).path('bar').include('foo')
# All other options can be used in combination with the :path option.
#
# The third form adds a file or directory under a different name. For example, to add the file foo under the
# name bar:
# zip(..).include('foo', :as=>'bar')
#
# The fourth form adds the contents of a directory using the directory as a prerequisite:
# zip(..).include(:from=>'foo')
# Unlike <code>include('foo')</code> it includes the contents of the directory, not the directory itself.
# Unlike <code>include('foo/*')</code>, it uses the directory timestamp for dependency management.
#
# The fifth form includes the contents of another archive by expanding it into this archive. For example:
# zip(..).include('foo.zip', :merge=>true).include('bar.zip')
# You can also use the method #merge.
def include(*files)
fail "AchiveTask.include() called with nil values" if files.include? nil
@paths[''].include *files if files.compact.size > 0
self
end
alias :add :include
alias :<< :include
# :call-seq:
# exclude(*files) => self
#
# Excludes files and returns self. Can be used in combination with include to prevent some files from being included.
def exclude(*files)
@paths[''].exclude *files
self
end
# :call-seq:
# merge(*files) => Merge
# merge(*files, :path=>name) => Merge
#
# Merges another archive into this one by including the individual files from the merged archive.
#
# Returns an object that supports two methods: include and exclude. You can use these methods to merge
# only specific files. For example:
# zip(..).merge('src.zip').include('module1/*')
def merge(*files)
@paths[''].merge *files
end
# :call-seq:
# path(name) => Path
#
# Returns a path object. Use the path object to include files under a path, for example, to include
# the file 'foo' as 'bar/foo':
# zip(..).path('bar').include('foo')
#
# Returns a Path object. The Path object implements all the same methods, like include, exclude, merge
# and so forth. It also implements path and root, so that:
# path('foo').path('bar') == path('foo/bar')
# path('foo').root == root
def path(name)
return @paths[''] if name.nil?
normalized = name.split('/').inject([]) do |path, part|
case part
when '.', nil, ''
path
when '..'
path[0...-1]
else
path << part
end
end.join('/')
@paths[normalized] ||= Path.new(self, normalized)
end
# :call-seq:
# root => ArchiveTask
#
# Call this on an archive to return itself, and on a path to return the archive.
def root
self
end
# :call-seq:
# with(options) => self
#
# Passes options to the task and returns self. Some tasks support additional options, for example,
# the WarTask supports options like :manifest, :libs and :classes.
#
# For example:
# package(:jar).with(:manifest=>'MANIFEST_MF')
def with(options)
options.each do |key, value|
begin
send "#{key}=", value
rescue NoMethodError
raise ArgumentError, "#{self.class.name} does not support the option #{key}"
end
end
self
end
def invoke_prerequisites(args, chain) #:nodoc:
@prepares.each { |prepare| prepare.call(self) }
@prepares.clear
file_map = Hash.new {|h,k| h[k]=[]}
transform_map = {}
@paths.each do |name, path|
path.add_files(file_map, transform_map)
end
# filter out Procs (dynamic content), nils and others
@prerequisites |= file_map.values.select { |src| src.is_a?(String) || src.is_a?(Rake::Task) }
super
end
def needed? #:nodoc:
return true unless File.exist?(name)
# You can do something like:
# include('foo', :path=>'foo').exclude('foo/bar', path=>'foo').
# include('foo/bar', :path=>'foo/bar')
# This will play havoc if we handled all the prerequisites together
# under the task, so instead we handle them individually for each path.
#
# We need to check that any file we include is not newer than the
# contents of the Zip. The file itself but also the directory it's
# coming from, since some tasks touch the directory, e.g. when the
# content of target/classes is included into a WAR.
most_recent = @paths.collect { |name, path| path.sources }.flatten.
select { |file| File.exist?(file) }.collect { |file| File.stat(file).mtime }.max
File.stat(name).mtime < (most_recent || Rake::EARLY) || super
end
# :call-seq:
# empty? => boolean
#
# Returns true if this ZIP file is empty (has no other entries inside).
def empty?
path("").empty
end
# :call-seq:
# contain(file*) => boolean
#
# Returns true if this ZIP file contains all the specified files. You can use absolute
# file names and glob patterns (using *, **, etc).
def contain?(*files)
path("").contain?(*files)
end
protected
# Adds a prepare block. These blocks are called early on for adding more content to
# the archive, before invoking prerequsities. Anything you add here will be invoked
# as a prerequisite and used to determine whether or not to generate this archive.
# In contrast, enhance blocks are evaluated after it was decided to create this archive.
def prepare(&block)
@prepares << block
end
def []=(key, value) #:nodoc:
raise ArgumentError, "This task does not support the option #{key}."
end
end
end