lib/tasks/zip.rb in buildr-0.18.0 vs lib/tasks/zip.rb in buildr-0.19.0

- old
+ new

@@ -5,16 +5,47 @@ 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 - # :nodoc: - module IncludeFiles + # 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. + class Path #:nodoc: - # Include the specified files or directories. + attr_reader :actions + + def initialize(zip, path) + @zip = zip + @path = "#{path}/" if path + expand_src = proc { (@files || []).map(&:to_s).uniq } + @sources = [ expand_src ] + @actions = [] << proc do |zip| + expand_src.call.each do |file| + if File.directory?(file) + in_directory(file, @files) do |file, rel_path| + puts "Adding #{@path}#{rel_path}" if Rake.application.options.trace + zip.add("#{@path}#{rel_path}", file) { true } + end + else + puts "Adding #{@path}#{File.basename(file)}" if Rake.application.options.trace + zip.add("#{@path}#{File.basename(file)}", file) { true } + end + end + end + end + + # Documented in ZipTask. def include(*files) if Hash === files.last options = files.pop else options = {} @@ -35,18 +66,19 @@ else raise "Unrecognized option #{options.keys.join(", ")}" end self end + alias :add :include - # Exclude the specified file or directories. + # Documented in ZipTask. def exclude(*files) (@files ||= FileList[]).exclude *files self end - alias :add :include + # Documented in ZipTask. def merge(*files) if Hash === files.last options = files.pop else options = {} @@ -54,112 +86,70 @@ if options[:path] path(options[:path]).merge *files +[ options.reject { |k,v| k == :path } ] elsif options.keys.empty? files.collect do |file| - @expand_sources << proc { file.to_s } + @sources << proc { file.to_s } expander = ZipExpander.new(file) - @add_files << proc { |zip| expander.expand(zip, @path) } + @actions << proc { |zip| expander.expand(zip, @path) } expander end.first else raise "Unrecognized option #{options.keys.join(", ")}" end end - protected + # Documented in ZipTask. + def path(path) + path.blank? ? self : @zip.path("#{@path}#{path}") + end - def setup_path(path = nil) - @path = "#{path}/" if path - expand_src = proc { (@files || []).map(&:to_s).uniq } - @expand_sources = [ expand_src ] - @add_files = [] << proc do |zip| - expand_src.call.each do |file| - if File.directory?(file) - in_directory(file, @files) do |file, rel_path| - puts "Adding #{@path}#{rel_path}" if Rake.application.options.trace - zip.add "#{@path}#{rel_path}", file - end - else - puts "Adding #{@path}#{File.basename(file)}" if Rake.application.options.trace - zip.add "#{@path}#{File.basename(file)}", file - end - end - end + # Documented in ZipTask. + def root() + @zip end + # Returns all the source files. + def sources() + @sources.map(&:call).flatten + end + + protected + def include_as(source, as) - @expand_sources << proc { source } - @add_files << proc do |zip| + @sources << proc { source } + @actions << proc do |zip| file = source.to_s if File.directory?(file) in_directory(file) do |file, rel_path| - puts "Adding #{@path}#{as}/#{rel_path}" if Rake.application.options.trace - zip.add file, "#{@path}#{as}/#{rel_path}" + if as == "." + dest = (@path || "") + rel_path.split("/")[1..-1].join("/") + else + dest = "#{@path}#{as}#{rel_path}" + end + puts "Adding #{dest}" if Rake.application.options.trace + zip.add(dest, file) { true } end else puts "Adding #{@path}#{as}" if Rake.application.options.trace - zip.add "#{@path}#{as}", file + zip.add("#{@path}#{as}", file) { true } end end end def in_directory(dir, excludes = nil) prefix = Regexp.new("^" + Regexp.escape(File.dirname(dir) + File::SEPARATOR)) - Dir[File.join(dir, "**", "*")]. + Dir["#{dir}/**/*"]. reject { |file| File.directory?(file) || (excludes && excludes.exclude?(file)) }. each { |file| yield file, file.sub(prefix, "") } end - def expand_sources() - @expand_sources.map(&:call).flatten - end - - def add_file(zip) - @add_files.each { |action| action.call zip } - end - end + # Extend one Zip file into another. + class ZipExpander #:nodoc: - # Which files go where. - class Path - - include IncludeFiles - - def initialize(path) - setup_path path - end - - end - - include IncludeFiles - - def initialize(*args) - super - @paths = { nil=>self } - setup_path - 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) { |zip| create zip } - rescue - rm task.name, :verbose=>false rescue nil - raise - end - end - end - - - class ZipExpander - def initialize(zip_file) @zip_file = zip_file.to_s end def include(*files) @@ -181,46 +171,157 @@ 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) } - # TODO: read and write file 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 + end + rescue + rm task.name, :verbose=>false rescue nil + raise + end + end + end - # Returns a path to which you can include/exclude files. + # :call-seq: + # include(*files) => self + # include(*files, :path=>path) => self + # include(file, :as=>name) => self + # include(*zips, :merge=>true) => self # + # Include files in the ZIP (or current path) and returns self. + # + # This method accepts three options. You can use :path to include files under + # a specific path, for example: # zip(..).include("foo", :path=>"bar") - # is equivalen to: + # includes the file bar as foo/bar. See also #path. + # + # You can use :as to include a file under a different name, for example: + # 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. + # + # 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. + 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. + 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). + # + # The returned object supports two methods: include and exclude. You can use these to + # merge only specific files from the ZIP. 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: # zip(..).path("bar").include("foo") - def path(path) - path.blank? ? @paths[nil] : (@paths[path] ||= Path.new(path)) + # 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") + def path(name) + name.blank? ? @paths[nil] : (@paths[name] ||= Path.new(self, name)) end - # Pass options to the task. Returns self. + # :call-seq: + # root() => ZipTask + # + # Returns the root path, essentially the ZipTask object itself. In case you are wondering + # down paths and want to go back. + 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. + # + # For example: + # package(:jar).with(:manifest=>"MANIFEST_MF") def with(options) options.each do |key, value| self[key] = value end self end + # :call-seq: + # [name] = value + # + # Used by with method to set specific options. For example: + # package(:jar).with(:manifest=>"MANIFEST_MF") + # Or: + # package(:jar)[:manifest] = "MANIFEST_MF" def []=(key, value) fail "#{self.class} does not support the attribute #{key}" end - - def invoke_prerequisites() - super - @paths.collect { |name, path| path.expand_sources }.flatten.each { |src| file(src).invoke } - end - def needed?() + def prerequisites() #:nodoc: + super + @paths.collect { |name, path| path.sources }.flatten.each { |src| file(src) } + 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 @@ -228,88 +329,105 @@ # # 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.expand_sources }.flatten. - each { |src| File.directory?(src) ? FileList[File.join(src, "**", "*")] | [src] : src }.flatten. + 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.add_file zip } + @paths.each { |name, obj| obj.actions.each { |action| action[zip] } } 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: - # returning(zip("test.zip")) { |task| + # zip("test.zip").tap do |task| # task.include "srcs" # task.include "README", "LICENSE" # end def zip(file) ZipTask.define_task(file) end - # The UnzipTask expands the contents of a ZIP file into a target directory. - # You can include any number of files and directories, use exclusion patterns, - # and expand files from relative paths. + # 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. # - # The file(s) to unzip is the first prerequisite. - class UnzipTask < Rake::Task + # 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 target directory. + # The zip file to extract. + attr_accessor :zip_file + # The target directory to extract to. attr_accessor :target - def initialize(*args) - super + # Initialize with hash argument of the form target=>zip_file. + def initialize(args) + @target, @zip_file = Rake.application.resolve_args(args) @paths = {} - enhance do |task| - fail "Where do you want the file unzipped" unless target + end - # 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(nil) - @paths[nil].include "*" - end + # :call-seq: + # extract() + # + # 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(nil) + @paths[nil].include "*" + end - # Otherwise, empty unzip creates target as a file when touching. - mkpath target, :verbose=>false - prerequisites.each do |file| - Zip::ZipFile.open(file) 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) - puts "Extracting #{dest}" if Rake.application.options.trace - mkpath File.dirname(dest), :verbose=>false rescue nil - entry.extract(dest) { true } - end - 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) + puts "Extracting #{dest}" if Rake.application.options.trace + mkpath File.dirname(dest), :verbose=>false rescue nil + entry.extract(dest) { true } end end - # Let other tasks know we updated the target directory. - touch target, :verbose=>false end + # Let other tasks know we updated the target directory. + touch target.to_s, :verbose=>false end - # Specifies directory to unzip to and return self. - def into(target) - self.target = target - self - end - + # :call-seq: + # include(*files) => self + # include(*files, :path=>name) => self + # # 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. @@ -321,10 +439,13 @@ end self end alias :add :include + # :call-seq: + # exclude(*files) => self + # # 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) @@ -334,10 +455,13 @@ from_path(nil).exclude *files end self 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: @@ -345,47 +469,43 @@ # will unzip etc/LICENSE into ./LICENSE. # # This is different from: # unzip("test.jar").into(Dir.pwd).include("etc/LICENSE") # which unzips etc/LICENSE into ./etc/LICENSE. - def from_path(path) - @paths[path] ||= FromPath.new(path) + def from_path(name) + @paths[name] ||= FromPath.new(name) end - def needed?() - #return true unless target && File.exist?(target) - #return true if prerequisites.any? { |prereq| File.stat(prereq).mtime > File.stat(target).mtime } - #false - true + # Returns the path to the target directory. + def to_s() + target.to_s end - # :nodoc: - class FromPath + class FromPath #:nodoc: def initialize(path) if path @path = path[-1] == ?/ ? path : path + "/" else @path = "" end end # See UnzipTask#include - def include(*files) + def include(*files) #:doc: @include ||= [] @include |= files self end # See UnzipTask#exclude - def exclude(*files) + def exclude(*files) #:doc: @exclude ||= [] @exclude |= files self end - # :nodoc: def map(entries) includes = @include || ["*"] excludes = @exclude || [] entries.inject({}) do |map, entry| short = entry.name.sub(@path, "") @@ -399,24 +519,32 @@ end end - # Defines a task that will unzip the specified file, into the directory - # specified by calling #into. It is the second call to into that creates - # and returns the task. + # :call-seq: + # unzip(to_dir=>zip_file) => Zip # - # You can unzip only some files by specifying an inclusion or exclusion - # pattern, and unzip files from a path in the ZIP file. See UnzipTask - # for more information. + # 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("test.zip").into("test") - # unzip("test.zip").into("etc").include("README", "LICENSE") - # unzip("test.zip").into("src").from_path("srcs") - def unzip(file) - task = nil - namespace { task = UnzipTask.define_task("unzip"=>file) } - task + # unzip("all"=>"test.zip") + # unzip("src"=>"test.zip").include("README", "LICENSE") + # unzip("libs"=>"test.zip").from_path("libs") + def unzip(args) + target, zip_file = Rake.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