lib/buildr/packaging/zip.rb in vic-buildr-1.3.3 vs lib/buildr/packaging/zip.rb in vic-buildr-1.3.4
- old
+ new
@@ -12,711 +12,53 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under
# the License.
-$LOADED_FEATURES.unshift 'ftools' if RUBY_VERSION >= '1.9.0'
+$LOADED_FEATURES.unshift 'ftools' if RUBY_VERSION >= '1.9.0' # Required to properly load RubyZip under Ruby 1.9
require 'zip/zip'
require 'zip/zipfilesystem'
-module Buildr
+module Zip #: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}"
- trace "Adding #{dest}"
- file_map[dest] = file
- end
- else
- 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 = args.pop if Hash === args.last
- files = args.flatten
-
- 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 canont use the :from option with file names' unless files.empty?
- [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 = files.flatten.map(&:to_s)
- @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 = args.flatten
- 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 { |file_map| expander.expand(file_map, path) }
- 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) #:nodoc:
- @actions.each { |action| action.call(file_map) }
- end
-
- def to_s()
- @path
- end
-
- protected
-
- def include_as(source, as)
- @sources << proc { source }
- @actions << proc do |file_map|
- file = source.to_s
- 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('/')}"
- trace "Adding #{dest}"
- file_map[dest] = file
- end
- else
- trace "Adding #{@path}#{as}"
- 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, File::FNM_PATHNAME) }
- end
-
+ class ZipCentralDirectory #:nodoc:
+ # Patch to add entries in alphabetical order.
+ def write_to_stream(io)
+ offset = io.tell
+ @entrySet.sort { |a,b| a.name <=> b.name }.each { |entry| entry.write_c_dir_entry(io) }
+ write_e_o_c_d(io, offset)
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
- end
-
-
- # Extend one Zip file into another.
- class ZipExpander #:nodoc:
-
- def initialize(zip_file)
- @zip_file = zip_file.to_s
- @includes = []
- @excludes = []
- end
-
- def include(*files)
- @includes |= files
- self
- end
- alias :<< :include
-
- def exclude(*files)
- @excludes |= files
- self
- end
-
- def expand(file_map, path)
- @includes = ['**/*'] if @includes.empty?
- Zip::ZipFile.open(@zip_file) do |source|
- source.entries.reject { |entry| entry.directory? }.each do |entry|
- if @includes.any? { |pattern| File.fnmatch(pattern, entry.name, File::FNM_PATHNAME) } &&
- !@excludes.any? { |pattern| File.fnmatch(pattern, entry.name, File::FNM_PATHNAME) }
- dest = path =~ /^\/?$/ ? entry.name : Util.relative_path(path + "/" + entry.name)
- trace "Adding #{dest}"
- file_map[dest] = lambda { |output| output.write source.read(entry) }
- end
- end
- end
- 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 = {}
- 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, :verbose=>false rescue nil
- mkpath File.dirname(name), :verbose=>false
- begin
- @paths.each do |name, object|
- @file_map[name] = nil unless name.empty?
- object.add_files(@file_map)
- end
- create_from @file_map
- rescue
- rm name, :verbose=>false 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 = { '' => 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)
- @paths[''].include *files
- 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
- @prerequisites |= @paths.collect { |name, path| path.sources }.flatten
- 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.
- each { |src| File.directory?(src) ? Util.recursive_with_dot_files(src) | [src] : src }.flatten.
- select { |file| File.exist?(file) }.collect { |file| File.stat(file).mtime }.max
- File.stat(name).mtime < (most_recent || Rake::EARLY) || super
- 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
- # The ZipTask creates a new Zip file. You can include any number of files and and directories,
- # use exclusion patterns, and include files into specific directories.
- #
- # For example:
- # zip('test.zip').tap do |task|
- # task.include 'srcs'
- # task.include 'README', 'LICENSE'
- # end
- #
- # See Buildr#zip and ArchiveTask.
- class ZipTask < ArchiveTask
- # Compression leve for this Zip.
- attr_accessor :compression_level
+ class ZipEntry
- def initialize(*args) #:nodoc:
- self.compression_level = Zlib::NO_COMPRESSION
- super
- end
-
- private
-
- def create_from(file_map)
- Zip::ZipOutputStream.open name do |zip|
- seen = {}
- mkpath = lambda do |dir|
- unless dir == '.' || seen[dir]
- mkpath.call File.dirname(dir)
- zip.put_next_entry(dir + '/', compression_level)
- seen[dir] = true
- end
- end
-
- file_map.each do |path, content|
- mkpath.call File.dirname(path)
- if content.respond_to?(:call)
- zip.put_next_entry(path, compression_level)
- content.call zip
- elsif content.nil? || File.directory?(content.to_s)
- mkpath.call path
- else
- zip.put_next_entry(path, compression_level)
- File.open content.to_s, 'rb' do |is|
- while data = is.read(4096)
- zip << data
- end
- end
- end
- end
- end
- end
-
- end
-
-
- # :call-seq:
- # zip(file) => ZipTask
- #
- # The ZipTask creates a new Zip file. You can include any number of files and
- # and directories, use exclusion patterns, and include files into specific
- # directories.
- #
- # For example:
- # zip('test.zip').tap do |task|
- # task.include 'srcs'
- # task.include 'README', 'LICENSE'
- # end
- def zip(file)
- ZipTask.define_task(file)
- end
-
-
- # An object for unzipping a file into a target directory. You can tell it to include
- # or exclude only specific files and directories, and also to map files from particular
- # paths inside the zip file into the target directory. Once ready, call #extract.
- #
- # Usually it is more convenient to create a file task for extracting the zip file
- # (see #unzip) and pass this object as a prerequisite to other tasks.
- #
- # See Buildr#unzip.
- class Unzip
-
- # The zip file to extract.
- attr_accessor :zip_file
- # The target directory to extract to.
- attr_accessor :target
-
- # Initialize with hash argument of the form target=>zip_file.
- def initialize(args)
- @target, arg_names, @zip_file = Buildr.application.resolve_args([args])
- @paths = {}
- end
-
# :call-seq:
- # extract()
+ # exist() => boolean
#
- # Extract the zip file into the target directory.
- #
- # You can call this method directly. However, if you are using the #unzip method,
- # it creates a file task for the target directory: use that task instead as a
- # prerequisite. For example:
- # build unzip(dir=>zip_file)
- # Or:
- # unzip(dir=>zip_file).target.invoke
- def extract()
- # If no paths specified, then no include/exclude patterns
- # specified. Nothing will happen unless we include all files.
- if @paths.empty?
- @paths[nil] = FromPath.new(self, nil)
- end
-
- # Otherwise, empty unzip creates target as a file when touching.
- mkpath target.to_s, :verbose=>false
- Zip::ZipFile.open(zip_file.to_s) do |zip|
- entries = zip.collect
- @paths.each do |path, patterns|
- patterns.map(entries).each do |dest, entry|
- next if entry.directory?
- dest = File.expand_path(dest, target.to_s)
- trace "Extracting #{dest}"
- mkpath File.dirname(dest), :verbose=>false rescue nil
- entry.extract(dest) { true }
- end
- end
- end
- # Let other tasks know we updated the target directory.
- touch target.to_s, :verbose=>false
+ # Returns true if this entry exists.
+ def exist?()
+ Zip::ZipFile.open(zipfile) { |zip| zip.file.exist?(@name) }
end
# :call-seq:
- # include(*files) => self
- # include(*files, :path=>name) => self
+ # empty?() => boolean
#
- # Include all files that match the patterns and returns self.
- #
- # Use include if you only want to unzip some of the files, by specifying
- # them instead of using exclusion. You can use #include in combination
- # with #exclude.
- def include(*files)
- if Hash === files.last
- from_path(files.pop[:path]).include *files
- else
- from_path(nil).include *files
- end
- self
+ # Returns true if this entry is empty.
+ def empty?()
+ Zip::ZipFile.open(zipfile) { |zip| zip.file.read(@name) }.empty?
end
- alias :add :include
# :call-seq:
- # exclude(*files) => self
+ # contain(patterns*) => boolean
#
- # Exclude all files that match the patterns and return self.
- #
- # Use exclude to unzip all files except those that match the pattern.
- # You can use #exclude in combination with #include.
- def exclude(*files)
- if Hash === files.last
- from_path(files.pop[:path]).exclude *files
- else
- from_path(nil).exclude *files
- end
- self
+ # Returns true if this ZIP file entry matches against all the arguments. An argument may be
+ # a string or regular expression.
+ def contain?(*patterns)
+ content = Zip::ZipFile.open(zipfile) { |zip| zip.file.read(@name) }
+ patterns.map { |pattern| Regexp === pattern ? pattern : Regexp.new(Regexp.escape(pattern.to_s)) }.
+ all? { |pattern| content =~ pattern }
end
- # :call-seq:
- # from_path(name) => Path
- #
- # Allows you to unzip from a path. Returns an object you can use to
- # specify which files to include/exclude relative to that path.
- # Expands the file relative to that path.
- #
- # For example:
- # unzip(Dir.pwd=>'test.jar').from_path('etc').include('LICENSE')
- # will unzip etc/LICENSE into ./LICENSE.
- #
- # This is different from:
- # unzip(Dir.pwd=>'test.jar').include('etc/LICENSE')
- # which unzips etc/LICENSE into ./etc/LICENSE.
- def from_path(name)
- @paths[name] ||= FromPath.new(self, name)
- end
- alias :path :from_path
-
- # :call-seq:
- # root() => Unzip
- #
- # Returns the root path, essentially the Unzip object itself. In case you are wondering
- # down paths and want to go back.
- def root()
- self
- end
-
- # Returns the path to the target directory.
- def to_s()
- target.to_s
- end
-
- class FromPath #:nodoc:
-
- def initialize(unzip, path)
- @unzip = unzip
- if path
- @path = path[-1] == ?/ ? path : path + '/'
- else
- @path = ''
- end
- end
-
- # See UnzipTask#include
- def include(*files) #:doc:
- @include ||= []
- @include |= files
- self
- end
-
- # See UnzipTask#exclude
- def exclude(*files) #:doc:
- @exclude ||= []
- @exclude |= files
- self
- end
-
- def map(entries)
- includes = @include || ['**/*']
- excludes = @exclude || []
- entries.inject({}) do |map, entry|
- if entry.name =~ /^#{@path}/
- short = entry.name.sub(@path, '')
- if includes.any? { |pat| File.fnmatch(pat, short, File::FNM_PATHNAME) } &&
- !excludes.any? { |pat| File.fnmatch(pat, short, File::FNM_PATHNAME) }
- map[short] = entry
- end
- end
- map
- end
- end
-
- # Documented in Unzip.
- def root()
- @unzip
- end
-
- # The target directory to extract to.
- def target()
- @unzip.target
- end
-
- end
-
end
-
- # :call-seq:
- # unzip(to_dir=>zip_file) => Zip
- #
- # Creates a task that will unzip a file into the target directory. The task name
- # is the target directory, the prerequisite is the file to unzip.
- #
- # This method creates a file task to expand the zip file. It returns an Unzip object
- # that specifies how the file will be extracted. You can include or exclude specific
- # files from within the zip, and map to different paths.
- #
- # The Unzip object's to_s method return the path to the target directory, so you can
- # use it as a prerequisite. By keeping the Unzip object separate from the file task,
- # you overlay additional work on top of the file task.
- #
- # For example:
- # unzip('all'=>'test.zip')
- # unzip('src'=>'test.zip').include('README', 'LICENSE')
- # unzip('libs'=>'test.zip').from_path('libs')
- def unzip(args)
- target, arg_names, zip_file = Buildr.application.resolve_args([args])
- task = file(File.expand_path(target.to_s)=>zip_file)
- Unzip.new(task=>zip_file).tap do |setup|
- task.enhance { setup.extract }
- end
- end
-
end
-
-
-module Zip #:nodoc:
-
- class ZipCentralDirectory #:nodoc:
- # Patch to add entries in alphabetical order.
- def write_to_stream(io)
- offset = io.tell
- @entrySet.sort { |a,b| a.name <=> b.name }.each { |entry| entry.write_c_dir_entry(io) }
- write_e_o_c_d(io, offset)
- end
- end
-
-end