lib/tasks/zip.rb in buildr-1.2.2 vs lib/tasks/zip.rb in buildr-1.2.3

- old
+ new

@@ -2,304 +2,307 @@ require "zip/zipfilesystem" module Buildr - # 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. - class ZipTask < Rake::FileTask + # 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. A zip has at least one path. + # are handled by this object. class Path #:nodoc: - attr_reader :actions + # Returns the archive from this path. + attr_reader :root - def initialize(zip, path) - @zip = zip + def initialize(root, path) + @root = root @path = "#{path}/" if path - expand_src = proc { (@files || []).map(&:to_s).uniq } + @files = FileList[] + # Expand source files added to this path. + expand_src = proc { @files.map{ |file| file.to_s }.uniq } @sources = [ expand_src ] - @actions = [] << proc do |zip| + # Add files and directories added to this path. + @actions = [] << proc do |file_map| + file_map[@path] = nil if @path expand_src.call.each do |path| if File.directory?(path) in_directory(path, @files) do |file, rel_path| - puts "Adding #{@path}#{rel_path}" if Rake.application.options.trace - zip.add("#{@path}#{rel_path}", file) { true } + dest = "#{@path}#{rel_path}" + puts "Adding #{dest}" if Rake.application.options.trace + file_map[dest] = file end else puts "Adding #{@path}#{File.basename(path)}" if Rake.application.options.trace - zip.add("#{@path}#{File.basename(path)}", path) { true } + file_map["#{@path}#{File.basename(path)}"] = path end end end end - # Documented in ZipTask. - def include(*files) - if Hash === files.last - options = files.pop - else - options = {} - end - files = files.flatten + # :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[:path] - path(options[:path]).include *files +[ options.reject { |k,v| k == :path } ] + if options.nil? || options.empty? + @files.include *files.map { |file| file.to_s } + 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.keys.size == 1 + 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]) + 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.keys.size == 1 + 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, ".") } + [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.keys.size == 1 + raise "You can only use the :merge option in combination with the :path option" unless options.size == 1 files.each { |file| merge file } - elsif options.keys.empty? - (@files ||= FileList[]).include files.map(&:to_s) else raise "Unrecognized option #{options.keys.join(", ")}" end self end alias :add :include - # Documented in ZipTask. + # :call-seq: + # exclude(*files) => self def exclude(*files) - (@files ||= FileList[]).exclude *files + @files.exclude *files self end - # Documented in ZipTask. - def merge(*files) - options = files.pop if Hash === files.last + # :call-seq: + # merge(*files) => Merge + # merge(*files, :path=>name) => Merge + def merge(*args) + options = args.pop if Hash === args.last + files = args.flatten - if options && options[:path] - path(options[:path]).merge *files +[ options.reject { |k,v| k == :path } ] - elsif options.nil? || options.keys.empty? + if options.nil? || options.empty? files.collect do |file| @sources << proc { file.to_s } expander = ZipExpander.new(file) - @actions << proc { |zip| expander.expand(zip, @path) } + @actions << proc { |file_map| expander.expand(file_map, @path) } expander end.first + elsif options[:path] + sans_path = options.reject { |k,v| k == :path } + path(options[:path]).merge *files + [sans_path] + self else raise "Unrecognized option #{options.keys.join(", ")}" end end - # Documented in ZipTask. + # Returns a Path relative to this one. def path(path) - path.blank? ? self : @zip.path("#{@path}#{path}") + path.blank? ? self : @root.path("#{@path}#{path}") end - # Documented in ZipTask. - def root() - @zip + # Returns all the source files. + def sources() #:nodoc: + @sources.map{ |source| source.call }.flatten end - # Returns all the source files. - def sources() - @sources.map(&:call).flatten + 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 |zip| + @actions << proc do |file_map| file = source.to_s + file_map[@path] = nil if @path if File.directory?(file) in_directory(file) do |file, rel_path| - if as == "." - dest = (@path || "") + rel_path.split("/")[1..-1].join("/") - else - dest = "#{@path}#{as}#{rel_path}" - end + dest = as == "." ? (@path || "") + rel_path.split("/")[1..-1].join("/") : "#{@path}#{as}#{rel_path}" puts "Adding #{dest}" if Rake.application.options.trace - zip.add(dest, file) { true } + file_map[dest] = file end else puts "Adding #{@path}#{as}" if Rake.application.options.trace - zip.add("#{@path}#{as}", file) { true } + file_map["#{@path}#{as}"] = file end end end def in_directory(dir, excludes = nil) prefix = Regexp.new("^" + Regexp.escape(File.dirname(dir) + File::SEPARATOR)) - Dir["#{dir}/**/*"]. + FileList["#{dir}/**/*"]. reject { |file| File.directory?(file) || (excludes && excludes.exclude?(file)) }. each { |file| yield file, file.sub(prefix, "") } 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 ||= []) @includes |= files self end def exclude(*files) - (@excludes ||= []) @excludes |= files self end - def expand(zip, path) - @includes ||= ["*"] - @excludes ||= [] + 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) } && !@excludes.any? { |pattern| File.fnmatch(pattern, entry.name) } - puts "Adding #{path}#{entry.name}" if Rake.application.options.trace - zip.get_output_stream("#{path}#{entry.name}") { |output| output.write source.read(entry) } + dest = "#{path}#{entry.name}" + puts "Adding #{dest}" if Rake.application.options.trace + file_map[dest] = lambda { |output| output.write source.read(entry) } end end end end end + def initialize(*args) #:nodoc: super @paths = { nil=>Path.new(self, nil) } - enhance do |task| - puts "Creating #{task.name}" if verbose - # We're here because the Zip file does not exist, or one of the files is - # newer than the Zip contents; in the later case, opening the Zip file - # will add to its contents instead of replacing it, so we want the Zip - # gone before we change it. We also don't want to see any partial updates. - rm task.name, :verbose=>false rescue nil - mkpath File.dirname(task.name), :verbose=>false - begin - Zip::ZipFile.open(task.name, Zip::ZipFile::CREATE) do |zip| - zip.restore_permissions = true - create zip + @prepares = [] + + # 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 { |name, object| object.add_files(@file_map) } + create_from @file_map + rescue + rm name, :verbose=>false rescue nil + raise end - rescue - rm task.name, :verbose=>false rescue nil - raise end end end # :call-seq: # include(*files) => self # include(*files, :path=>path) => self # include(file, :as=>name) => self - # include(*zips, :merge=>true) => self + # include(:from=>path) => self + # include(*files, :merge=>true) => self # - # Include files in the ZIP (or current path) and returns self. + # Include files in this archive, or when called on a path, within that path. Returns self. # - # This method accepts three options. You can use :path to include files under - # a specific path, for example: + # 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") - # includes the file bar as bar/foo. See also #path. + # 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. # - # You can use :as to include a file under a different name, for example: + # 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") - # You can use the :as option in combination with the :path option, but only with - # a single file at a time. # - # As a special case, you can include the entire contents of a directory by including - # the directory and using :as=>".". This: - # zip(..).include("srcs", :as=>".") - # will include all the source files, using the directory as a prerequisite. This: - # zip(..).include("srcs/*") - # includes all the same source files, using the source files as a prerequisite. + # 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. # - # You can use :merge option to include the contents of another ZIP file, for example: - # zip(..).include("foo.zip", :merge=>true) - # You can use the :merge option in combination with the :path option. See also #merge. + # 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[nil].include *files self end alias :add :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. + # Excludes files and returns self. Can be used in combination with include to prevent some files from being included. def exclude(*files) @paths[nil].exclude *files self end # :call-seq: # merge(*files) => Merge # merge(*files, :path=>name) => Merge # - # Merges ZIP files and returns a merge object. The contents of the merged ZIP file is - # extracted into this ZIP file (or current path). + # Merges another archive into this one by including the individual files from the merged archive. # - # The returned object supports two methods: include and exclude. You can use these to - # merge only specific files from the ZIP. For example: + # 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/*") - # - # This differs from include with the :merge option, which returns self. def merge(*files) @paths[nil].merge *files end # :call-seq: # path(name) => Path # - # Returns a path object. You can use the path object to include files in a given - # path inside the ZIP file. The path object implements the include, exclude, merge, - # path and root methods. - # - # For example: + # 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") - # Will add the file foo under the name bar/foo. # - # As a shorthand, you can also use the :path option: - # zip(..).include("foo", :path=>"bar") + # 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) - name.blank? ? @paths[nil] : (@paths[name] ||= Path.new(self, name)) + return @paths[nil] if name.blank? + @paths[name] ||= Path.new(self, name) end # :call-seq: - # root() => ZipTask + # root() => ArchiveTask # - # Returns the root path, essentially the ZipTask object itself. In case you are wondering - # down paths and want to go back. + # 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 # - # Pass options to the task. Returns self. ZipTask itself does not support any options, - # but other tasks (e.g. JarTask, WarTask) do. + # 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| @@ -316,11 +319,13 @@ end self end def invoke_prerequisites() #:nodoc: - prerequisites.concat @paths.collect { |name, path| path.sources }.flatten + @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) @@ -329,36 +334,68 @@ # 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 + # 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) ? FileList["#{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 - # Sub-classes override this method to perform additional creation tasks, - # e.g. creating a manifest file in a JAR. - def create(zip) - @paths.each { |name, obj| obj.actions.each { |action| action[zip] } } + # 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 + + private + + def create_from(file_map) + Zip::ZipFile.open(name, Zip::ZipFile::CREATE) do |zip| + zip.restore_permissions = true + file_map.each do |path, content| + zip.mkdir path unless content || zip.find_entry(path) + zip.add path, content if String === content + zip.get_output_stream(path) { |output| content.call(output) } if content.respond_to?(:call) + end + end + end + + end + + # :call-seq: # zip(file) => ZipTask # - # The ZipTask creates a new ZIP file. You can include any number of files and + # 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|