require "zip/zip" 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. class ZipTask < Rake::FileTask def initialize(*args) super @paths = {} enhance do |task| verbose { puts "Creating #{task.name}" } # 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 # Include the specified files and directories. Returns self. # # You can specify a path for inclusion by passing :path=> as the # last argument. For example: # task.include "src" # Include the src directory # task.include foobar, :path=>"libs" # Include under /libs/ def include(*files) if Hash === files.last path = files.pop[:path] end (@paths[path] ||= FileList[]).include *files self end alias :add :include # Excludes the specified files and directories. Returns self. Use this # in combination with include; make sure to use the same path. def exclude(*files) if Hash === files.last path = files.pop[:path] end (@paths[path] ||= FileList[]).exclude *files self end # Returns a path to which you can include/exclude files. # # zip(..).include("foo", :path=>"bar") # is equivalen to: # zip(..).path("bar").include("foo") def path(path) @paths[path] ||= FileList[] end # Pass options to the task. Returns self. def with(options) options.each do |key, value| self[key] = value end self end def []=(key, value) fail "#{self.class} does not support the attribute #{key}" end def invoke_prerequisites() super @paths.each { |p, files| artifacts(*files).each { |f| file(f).invoke } } end def needed?() # 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. #@paths.collect{ |p, files| files }.flatten.each { |f| file(f).invoke } return true unless File.exist?(name) # 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. files = @paths.collect { |path, files| files.map(&:to_s) }.flatten files = files.collect { |f| File.directory?(f) ? FileList[File.join(f, "**", "*")].collect | [f] : f }.flatten most_recent = files.select { |f| File.exist?(f) }.collect { |f| File.stat(f).mtime }.max File.stat(name).mtime < (most_recent || Rake::EARLY) end protected def create(zip) zip_map.keys.sort.each do |path| puts "Adding #{path}" if Rake.application.options.trace zip.add path, zip_map[path] end end def zip_map() unless @zip_map args = @paths.collect do |path, files| if path dest_for = proc { |f| File.join(path, f) } else dest_for = proc { |f| f } end artifacts(*files).map(&:to_s).collect do |file| if File.directory?(file) # Include all files inside the directory, with path starting # from the directory itself. prefix = File.dirname(file) + File::SEPARATOR Dir[File.join(file, "**", "*")].sort. reject { |file| File.directory?(file) || files.exclude?(file) }. collect { |file| [ dest_for[file.sub(prefix, "")], file ] } else # Include just that one file, sans its diectory path. [ dest_for[File.basename(file)], file ] end end end.flatten @zip_map = Hash[ *args ] end @zip_map 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: # returning(zip("test.zip")) { |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. # # The file(s) to unzip is the first prerequisite. class UnzipTask < Rake::Task # The target directory. attr_accessor :target def initialize(*args) super @paths = {} enhance do |task| fail "Where do you want the file unzipped" unless target # 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 end end # Let other tasks know we updated the target directory. touch target, :verbose=>false end end # Specifies directory to unzip to and return self. def into(target) self.target = target self end # 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 end alias :add :include # 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 end # 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("test.jar").into(Dir.pwd).from_path("etc").include("LICENSE") # 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) 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 end # :nodoc: class FromPath def initialize(path) if path @path = path[-1] == ?/ ? path : path + "/" else @path = "" end end # See UnzipTask#include def include(*files) @include ||= [] @include |= files self end # See UnzipTask#exclude def exclude(*files) @exclude ||= [] @exclude |= files self end # :nodoc: def map(entries) includes = @include || ["*"] excludes = @exclude || [] entries.inject({}) do |map, entry| short = entry.name.sub(@path, "") if includes.any? { |pat| File.fnmatch(pat, short) } && !excludes.any? { |pat| File.fnmatch(pat, short) } map[short] = entry end map end end 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. # # 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. # # 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 end end