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