blob: 01e653ad6a38e009e786114dad816da2bf5386c9 [file] [log] [blame]
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with this
# work for additional information regarding copyright ownership. The ASF
# licenses this file to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under
# the License.
module Buildr #:nodoc:
module Packaging #:nodoc:
# Adds packaging for Java projects: JAR, WAR, AAR, EAR, Javadoc.
module Java
class Manifest
STANDARD_HEADER = { 'Manifest-Version'=>'1.0', 'Created-By'=>'Buildr' }
LINE_SEPARATOR = /\r\n|\n|\r[^\n]/ #:nodoc:
SECTION_SEPARATOR = /(#{LINE_SEPARATOR}){2}/ #:nodoc:
class << self
# :call-seq:
# parse(str) => manifest
#
# Parse a string in MANIFEST.MF format and return a new Manifest.
def parse(str)
sections = str.split(SECTION_SEPARATOR).reject { |s| s.strip.empty? }
new sections.map { |section|
lines = section.split(LINE_SEPARATOR).inject([]) { |merged, line|
if line[/^ /] == ' '
merged.last << line[1..-1]
else
merged << line
end
merged
}
lines.map { |line| line.scan(/(.*?):\s*(.*)/).first }.
inject({}) { |map, (key, value)| map.merge(key=>value) }
}
end
# :call-seq:
# from_zip(file) => manifest
#
# Parse the MANIFEST.MF entry of a ZIP (or JAR) file and return a new Manifest.
def from_zip(file)
Zip::File.open(file.to_s) do |zip|
return Manifest.parse zip.read('META-INF/MANIFEST.MF') if zip.find_entry('META-INF/MANIFEST.MF')
end
Manifest.new
end
# :call-seq:
# update_manifest(file) { |manifest| ... }
#
# Updates the MANIFEST.MF entry of a ZIP (or JAR) file. Reads the MANIFEST.MF,
# yields to the block with the Manifest object, and writes the modified object
# back to the file.
def update_manifest(file)
manifest = from_zip(file)
result = yield manifest
Zip::File.open(file.to_s) do |zip|
zip.get_output_stream('META-INF/MANIFEST.MF') do |out|
out.write manifest.to_s
out.write "\n"
end
end
result
end
end
# Returns a new Manifest object based on the argument:
# * nil -- Empty Manifest.
# * Hash -- Manifest with main section using the hash name/value pairs.
# * Array -- Manifest with one section from each entry (must be hashes).
# * String -- Parse (see Manifest#parse).
# * Proc/Method -- New Manifest from result of calling proc/method.
def initialize(arg = nil)
case arg
when nil, Hash then @sections = [arg || {}]
when Array then @sections = arg
when String then @sections = Manifest.parse(arg).sections
when Proc, Method then @sections = Manifest.new(arg.call).sections
else
fail 'Invalid manifest, expecting Hash, Array, file name/task or proc/method.'
end
# Add Manifest-Version and Created-By, if not specified.
STANDARD_HEADER.each do |name, value|
sections.first[name] ||= value
end
end
# The sections of this manifest.
attr_reader :sections
# The main (first) section of this manifest.
def main
sections.first
end
include Enumerable
# Iterate over each section and yield to block.
def each(&block)
@sections.each(&block)
end
# Convert to MANIFEST.MF format.
def to_s
@sections.map { |section|
keys = section.keys
keys.unshift('Name') if keys.delete('Name')
lines = keys.map { |key| manifest_wrap_at_72("#{key}: #{section[key]}") }
lines + ['']
}.flatten.join("\n")
end
private
def manifest_wrap_at_72(line)
return [line] if line.size < 72
[ line[0..70] ] + manifest_wrap_at_72(' ' + line[71..-1])
end
end
# Adds support for MANIFEST.MF and other META-INF files.
module WithManifest #:nodoc:
class << self
def included(base)
base.class_eval do
alias :initialize_without_manifest :initialize
alias :initialize :initialize_with_manifest
end
end
end
# Specifies how to create the manifest file.
attr_accessor :manifest
# Specifies files to include in the META-INF directory.
attr_accessor :meta_inf
def initialize_with_manifest(*args) #:nodoc:
initialize_without_manifest *args
@manifest = false
@meta_inf = []
@dependencies = FileList[]
prepare do
@prerequisites << manifest if String === manifest || Rake::Task === manifest
[meta_inf].flatten.map { |file| file.to_s }.uniq.each { |file| path('META-INF').include file }
end
enhance do
if manifest
# Tempfiles gets deleted on garbage collection, so we're going to hold on to it
# through instance variable not closure variable.
@manifest_tmp = Tempfile.new('MANIFEST.MF')
File.chmod 0644, @manifest_tmp.path
self.manifest = File.read(manifest.to_s) if String === manifest || Rake::Task === manifest
self.manifest = Manifest.new(manifest) unless Manifest === manifest
#@manifest_tmp.write Manifest::STANDARD_HEADER
@manifest_tmp.write manifest.to_s
@manifest_tmp.write "\n"
@manifest_tmp.close
path('META-INF').include @manifest_tmp.path, :as=>'MANIFEST.MF'
end
end
end
end
class ::Buildr::ZipTask
include WithManifest
end
# Extends the ZipTask to create a JAR file.
#
# This task supports two additional attributes: manifest and meta-inf.
#
# The manifest attribute specifies how to create the MANIFEST.MF file.
# * A hash of manifest properties (name/value pairs).
# * An array of hashes, one for each section of the manifest.
# * A string providing the name of an existing manifest file.
# * A file task can be used the same way.
# * Proc or method called to return the contents of the manifest file.
# * False to not generate a manifest file.
#
# The meta-inf attribute lists one or more files that should be copied into
# the META-INF directory.
#
# For example:
# package(:jar).with(:manifest=>'src/MANIFEST.MF')
# package(:jar).meta_inf << file('README')
class JarTask < ZipTask
def initialize(*args) #:nodoc:
super
enhance do
pom.invoke rescue nil if respond_to?(:pom) && pom && pom != self && classifier.nil?
end
end
# :call-seq:
# with(options) => self
#
# Additional
# 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(*args)
super args.pop if Hash === args.last
fail 'package.with() should not contain nil values' if args.include? nil
include :from=>args if args.size > 0
self
end
end
# Extends the JarTask to create a WAR file.
#
# Supports all the same options as JarTask, in additon to these two options:
# * :libs -- An array of files, tasks, artifact specifications, etc that will be added
# to the WEB-INF/lib directory.
# * :classes -- A directory containing class files for inclusion in the WEB-INF/classes
# directory.
#
# For example:
# package(:war).with(:libs=>'log4j:log4j:jar:1.1')
class WarTask < JarTask
# Directories with class files to include under WEB-INF/classes.
attr_accessor :classes
# Artifacts to include under WEB-INF/libs.
attr_accessor :libs
def initialize(*args) #:nodoc:
super
@classes = []
@libs = []
enhance do |war|
@libs.each {|lib| lib.invoke if lib.respond_to?(:invoke) }
@classes.to_a.flatten.each { |classes| include classes, :as => 'WEB-INF/classes' }
path('WEB-INF/lib').include Buildr.artifacts(@libs) unless @libs.nil? || @libs.empty?
end
end
def libs=(value) #:nodoc:
@libs = Buildr.artifacts(value)
end
def classes=(value) #:nodoc:
@classes = [value].flatten.map { |dir| file(dir.to_s) }
end
end
# Extends the JarTask to create an AAR file (Axis2 service archive).
#
# Supports all the same options as JarTask, with the addition of :wsdls, :services_xml and :libs.
#
# * :wsdls -- WSDL files to include (under META-INF). By default packaging will include all WSDL
# files found under src/main/axis2.
# * :services_xml -- Location of services.xml file (included under META-INF). By default packaging
# takes this from src/main/axis2/services.xml. Use a different path if you genereate the services.xml
# file as part of the build.
# * :libs -- Array of files, tasks, artifact specifications, etc that will be added to the /lib directory.
#
# For example:
# package(:aar).with(:libs=>'log4j:log4j:jar:1.1')
#
# filter.from('src/main/axis2').into('target').include('services.xml', '*.wsdl').using('http_port'=>'8080')
# package(:aar).wsdls.clear
# package(:aar).with(:services_xml=>_('target/services.xml'), :wsdls=>_('target/*.wsdl'))
class AarTask < JarTask
# Artifacts to include under /lib.
attr_accessor :libs
# WSDLs to include under META-INF (defaults to all WSDLs under src/main/axis2).
attr_accessor :wsdls
# Location of services.xml file (defaults to src/main/axis2/services.xml).
attr_accessor :services_xml
def initialize(*args) #:nodoc:
super
@libs = []
@wsdls = []
prepare do
path('META-INF').include @wsdls
path('META-INF').include @services_xml, :as=>'services.xml' if @services_xml
path('lib').include Buildr.artifacts(@libs) unless @libs.nil? || @libs.empty?
end
end
def libs=(value) #:nodoc:
@libs = Buildr.artifacts(value)
end
def wsdls=(value) #:nodoc:
@wsdls |= Array(value)
end
end
# Extend the JarTask to create an EAR file.
#
# The following component types are supported by the EARTask:
#
# * :war -- A J2EE Web Application
# * :ejb -- An Enterprise Java Bean
# * :jar -- A J2EE Application Client.[1]
# * :lib -- An ear scoped shared library[2] (for things like logging,
# spring, etc) common to the ear components
#
# The EarTask uses the "Mechanism 2: Bundled Optional Classes" as described on [2].
# All specified libraries are added to the EAR archive and the Class-Path manifiest entry is
# modified for each EAR component. Special care is taken with WebApplications, as they can
# contain libraries on their WEB-INF/lib directory, libraries already included in a war file
# are not referenced by the Class-Path entry of the war in order to avoid class collisions
#
# EarTask supports all the same options as JarTask, in additon to these two options:
#
# * :display_name -- The displayname to for this ear on application.xml
#
# * :map -- A Hash used to map component type to paths within the EAR.
# By default each component type is mapped to a directory with the same name,
# for example, EJBs are stored in the /ejb path. To customize:
# package(:ear).map[:war] = 'web-applications'
# package(:ear).map[:lib] = nil # store shared libraries on root of archive
#
# EAR components are added by means of the EarTask#add, EarTask#<<, EarTask#push methods
# Component type is determined from the artifact's type.
#
# package(:ear) << project('coolWebService').package(:war)
#
# The << method is just an alias for push, with the later you can add multiple components
# at the same time. For example..
#
# package(:ear).push 'org.springframework:spring:jar:2.6',
# projects('reflectUtils', 'springUtils'),
# project('coolerWebService').package(:war)
#
# The add method takes a single component with an optional hash. You can use it to override
# some component attributes.
#
# You can override the component type for a particular artifact. The following example
# shows how you can tell the EarTask to treat a JAR file as an EJB:
#
# # will add an ejb entry for the-cool-ejb-2.5.jar in application.xml
# package(:ear).add 'org.coolguys:the-cool-ejb:jar:2.5', :type=>:ejb
# # A better syntax for this is:
# package(:ear).add :ejb=>'org.coolguys:the-cool-ejb:jar:2.5'
#
# By default, every JAR package is assumed to be a library component, so you need to specify
# the type when including an EJB (:ejb) or Application Client JAR (:jar).
#
# For WebApplications (:war)s, you can customize the context-root that appears in application.xml.
# The following example also specifies a different directory inside the EAR where to store the webapp.
#
# package(:ear).add project(:remoteService).package(:war),
# :path=>'web-services', :context_root=>'/Some/URL/Path'
#
# [1] http://java.sun.com/j2ee/sdk_1.2.1/techdocs/guides/ejb/html/Overview5.html#10106
# [2] http://java.sun.com/j2ee/verified/packaging.html
class EarTask < JarTask
SUPPORTED_TYPES = [:war, :ejb, :jar, :rar, :lib]
# The display-name entry for application.xml
attr_accessor :display_name
# The description entry for application.xml
attr_accessor :description
# Map from component type to path inside the EAR.
attr_accessor :dirs
# Security roles entry for application.xml
attr_accessor :security_roles
def initialize(*args)
super
@dirs = Hash.new { |h, k| k.to_s }
@libs, @components, @security_roles = [], [], []
prepare do
@components.each do |component|
path(component[:path]).include(component[:clone] || component[:artifact])
end
path('META-INF').include(descriptor)
end
end
# Add an artifact to this EAR.
def add(*args)
options = Hash === args.last ? args.pop.clone : {}
args.flatten!
args.map! do |pkg|
case pkg
when Project
pkg.packages.select { |pp| JarTask === pp && SUPPORTED_TYPES.include?(pp.type) }
when Rake::FileTask
pkg # add the explicitly provided file
when Hash
Buildr.artifact(pkg)
when String
begin
Buildr.artifact(pkg)
rescue # not an artifact spec, it must me a filename
file(pkg)
end
else
raise "Invalid EAR component #{pkg.class}: #{pkg}"
end
end
args.flatten!
args.compact!
if args.empty?
raise ":type must not be specified for type=>component argument style" if options.key?(:type)
raise ":as must not be specified for type=>component argument style" if options.key?(:as)
comps = {}
options.delete_if { |k, v| comps[k] = v if SUPPORTED_TYPES.include?(k) }
raise "You must specify at least one valid component to add" if comps.empty?
comps.each { |k, v| add(v, {:as => k}.merge(options)) }
else
args.each do |artifact|
type = options[:as] || options[:type]
unless type
type = artifact.respond_to?(:type) ? artifact.type : artifact.to_s.pathmap('%x').to_sym
type = :lib if type == :jar
end
raise "Unknown EAR component type: #{type}. Perhaps you may explicity tell what component type to use." unless
SUPPORTED_TYPES.include?(type)
component = options.merge(:artifact => artifact, :type => type,
:id=>artifact.respond_to?(:to_spec) ? artifact.id : artifact.to_s.pathmap('%n'),
:path=>options[:path] || dirs[type].to_s)
component[:clone] = component_clone(component) unless :lib == type
# update_classpath(component) unless :lib == type || Artifact === artifact
@components << component
end
end
self
end
alias_method :push, :add
alias_method :<<, :push
protected
def component_clone(component)
file(path_to(component[:path], component[:artifact].to_s.pathmap('%f')) => component[:artifact]) do |task|
mkpath task.to_s.pathmap('%d')
cp component[:artifact].to_s, task.to_s
Manifest.update_manifest(task) do |manifest|
class_path = manifest.main['Class-Path'].to_s.split
included_libs = class_path.map { |fn| fn.pathmap('%f') }
Zip::File.foreach(task.to_s) do |entry|
included_libs << entry.name.pathmap('%f') if entry.file? && entry.name =~ /^WEB-INF\/lib\/[^\/]+$/
end
# Include all other libraries in the classpath.
class_path += libs_classpath(component).reject { |path| included_libs.include?(File.basename(path)) }
manifest.main['Class-Path'] = class_path.join(' ')
end
end
end
def associate(project)
@project = project
end
def path_to(*args) #:nodoc:
@project.path_to(:target, :ear, name.pathmap('%n'), *args)
end
alias_method :_, :path_to
def update_classpath(component)
package = file(component[:artifact].to_s)
package.manifest = (package.manifest || {}).dup # avoid mofifying parent projects manifest
package.prepare do
header = case package.manifest
when Hash then package.manifest
when Array then package.manifest.first
end
if header
# Determine which libraries are already included.
class_path = header['Class-Path'].to_s.split
included_libs = class_path.map { |fn| File.basename(fn) }
included_libs += package.path('WEB-INF/lib').sources.map { |fn| File.basename(fn) }
# Include all other libraries in the classpath.
class_path += libs_classpath(component).reject { |path| included_libs.include?(File.basename(path)) }
header['Class-Path'] = class_path.join(' ')
end
end
end
private
# Classpath of all packages included as libraries (type :lib).
def libs_classpath(component)
from = component[:path]
@classpath = @components.select { |comp| comp[:type] == :lib }.
map do |lib|
basename = lib[:artifact].to_s.pathmap('%f')
full_path = lib[:path].empty? ? basename : File.join(lib[:path], basename)
Util.relative_path(full_path, from)
end
end
def descriptor_xml
buffer = ""
xml = Builder::XmlMarkup.new(:target=>buffer, :indent => 2)
xml.declare! :DOCTYPE, :application, :PUBLIC,
'-//Sun Microsystems, Inc.//DTD J2EE Application 1.2//EN',
'http://java.sun.com/j2ee/dtds/application_1_2.dtd'
xml.application do
xml.tag! 'display-name', display_name
desc = self.description || @project.comment
xml.tag! 'description', desc if desc
@components.each do |comp|
basename = comp[:artifact].to_s.pathmap('%f')
uri = comp[:path].empty? ? basename : File.join(comp[:path], basename)
case comp[:type]
when :war
xml.module :id=>comp[:id] do
xml.web do
xml.tag! 'web-uri', uri
xml.tag! 'context-root', File.join('', (comp[:context_root] || comp[:id])) unless comp[:context_root] == false
end
end
when :ejb
xml.module :id=>comp[:id] do
xml.ejb uri
end
when :jar
xml.jar uri
end
end
@security_roles.each do |role|
xml.tag! 'security-role', :id=>role[:id] do
xml.description role[:description]
xml.tag! 'role-name', role[:name]
end
end
end
buffer
end
# return a FileTask to build the ear application.xml file
def descriptor
return @descriptor if @descriptor
descriptor_path = path_to('META-INF/application.xml')
@descriptor = file(descriptor_path) do |task|
trace "Creating EAR Descriptor: #{task.to_s}"
mkpath File.dirname(task.name)
File.open(task.name, 'w') { |file| file.print task.xml }
end
class << @descriptor
attr_accessor :ear
def xml
@xml ||= ear.send :descriptor_xml
end
def needed?
super || xml != File.read(self.to_s) rescue true
end
end
@descriptor.ear = self
@descriptor
end
end
include Extension
before_define(:package => :build) do |project|
if project.parent && project.parent.manifest
project.manifest = project.parent.manifest.dup
else
project.manifest = {
'Build-By'=>ENV['USER'], 'Build-Jdk'=>ENV_JAVA['java.version'],
'Implementation-Title'=>project.comment || project.name,
'Implementation-Version'=>project.version }
end
if project.parent && project.parent.meta_inf
project.meta_inf = project.parent.meta_inf.dup
else
project.meta_inf = [project.file('LICENSE')].select { |file| File.exist?(file.to_s) }
end
end
# Manifest used for packaging. Inherited from parent project. The default value is a hash that includes
# the Build-By, Build-Jdk, Implementation-Title and Implementation-Version values.
# The later are taken from the project's comment (or name) and version number.
attr_accessor :manifest
# Files to always include in the package META-INF directory. The default value include
# the LICENSE file if one exists in the project's base directory.
attr_accessor :meta_inf
# :call-seq:
# package_with_sources(options?)
#
# Call this when you want the project (and all its sub-projects) to create a source distribution.
# You can use the source distribution in an IDE when debugging.
#
# A source distribution is a jar package with the classifier 'sources', which includes all the
# sources used by the compile task.
#
# Packages use the project's manifest and meta_inf properties, which you can override by passing
# different values (e.g. false to exclude the manifest) in the options.
#
# To create source distributions only for specific projects, use the :only and :except options,
# for example:
# package_with_sources :only=>['foo:bar', 'foo:baz']
#
# (Same as calling package :sources on each project/sub-project that has source directories.)
def package_with_sources(options = nil)
options ||= {}
enhance do
selected = options[:only] ? projects(options[:only]) :
options[:except] ? ([self] + projects - projects(options[:except])) :
[self] + projects
selected.reject { |project| project.compile.sources.empty? && project.resources.target.nil? }.
each { |project| project.package(:sources) }
end
end
# :call-seq:
# package_with_javadoc(options?)
#
# Call this when you want the project (and all its sub-projects) to create a JavaDoc distribution.
# You can use the JavaDoc distribution in an IDE when coding against the API.
#
# A JavaDoc distribution is a ZIP package with the classifier 'javadoc', which includes all the
# sources used by the compile task.
#
# Packages use the project's manifest and meta_inf properties, which you can override by passing
# different values (e.g. false to exclude the manifest) in the options.
#
# To create JavaDoc distributions only for specific projects, use the :only and :except options,
# for example:
# package_with_javadoc :only=>['foo:bar', 'foo:baz']
#
# (Same as calling package :javadoc on each project/sub-project that has source directories.)
def package_with_javadoc(options = nil)
options ||= {}
enhance do
selected = options[:only] ? projects(options[:only]) :
options[:except] ? ([self] + projects - projects(options[:except])) :
[self] + projects
selected.reject { |project| project.compile.sources.empty? }.
each { |project| project.package(:javadoc) }
end
end
def package_as_jar(file_name) #:nodoc:
Java::JarTask.define_task(file_name).tap do |jar|
jar.with :manifest=>manifest, :meta_inf=>meta_inf
jar.with [compile.target, resources.target].compact
end
end
def package_as_war(file_name) #:nodoc:
Java::WarTask.define_task(file_name).tap do |war|
war.with :manifest=>manifest, :meta_inf=>meta_inf
# Add libraries in WEB-INF lib, and classes in WEB-INF classes
war.with :classes=>[compile.target, resources.target].compact
war.with :libs=>compile.dependencies
webapp = path_to(:source, :main, :webapp)
war.with webapp if File.exist?(webapp)
war.enhance([assets])
war.include assets.to_s, :as => '.' unless assets.paths.empty?
end
end
def package_as_aar(file_name) #:nodoc:
Java::AarTask.define_task(file_name).tap do |aar|
aar.with :manifest=>manifest, :meta_inf=>meta_inf
aar.with :wsdls=>path_to(:source, :main, :axis2, '*.wsdl')
aar.with :services_xml=>path_to(:source, :main, :axis2, 'services.xml')
aar.with [compile.target, resources.target].compact
aar.with :libs=>compile.dependencies
end
end
def package_as_ear(file_name) #:nodoc:
Java::EarTask.define_task(file_name).tap do |ear|
ear.send :associate, self
ear.with :display_name=>id, :manifest=>manifest, :meta_inf=>meta_inf
end
end
def package_as_javadoc_spec(spec) #:nodoc:
spec.merge(:type=>:jar, :classifier=>'javadoc')
end
def package_as_javadoc(file_name) #:nodoc:
ZipTask.define_task(file_name).tap do |zip|
zip.include :from=>doc.target
end
end
end
end
end
class Buildr::Project #:nodoc:
include Buildr::Packaging::Java
end