blob: 90d30ac25d6045fa94364398bcf9f59ddaf8e500 [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:
# An ArtifactNamespace is a hierarchical dictionary used to manage ArtifactRequirements.
# It can be used to have different artifact versions per project
# or to allow users to select a version for addons or modules.
#
# Namespaces are opened using the Buildr.artifact_ns method, most important methods are:
# [need] Used to create a requirement on the namespace.
# [use] Set the artifact version to use for a requirement.
# [values_at] Reference requirements by name.
# [each] Return each ArtifactRequirement in the namespace.
# The method_missing method for instances provides some syntactic sugar to these.
# See the following examples, and the methods for ArtifactRequirement.
#
# = Avoiding constant pollution on buildfile
#
# Each project has its own ArtifactNamespace inheriting the one from the
# parent project up to the root namespace.
#
# Consider the following snippet, as project grows, each subproject
# may need different artifact combinations and/or versions. Assigning
# artifact specifications to constants can make it painful to maintain
# their references even if using structs/hashes.
#
# -- buildfile --
# SPRING = 'org.springframework:spring:jar:2.5'
# SPRING_OLD = 'org.springframework:spring:jar:1.0'
# LOGGING = ['comons-logging:commons-logging:jar:1.1.1',
# 'log4j:log4j:jar:1.2.15']
# WL_LOGGING = artifact('bea:wlcommons-logging:jar:8.1').from('path/to/wlcommons-logging.jar')
# LOGGING_WEBLOGIC = ['comons-logging:commons-logging:jar:1.1.1',
# WL_LOGGING]
# COMMONS = struct :collections => 'commons-collection:commons-collection:jar:3.1',
# :net => 'commons-net:commons-net:jar:1.4.0'
#
# define 'example1' do
# define 'one' do
# compile.with SPRING, LOGGING_WEBLOGIC, COMMONS
# end
# define 'two' do
# compile.with SPRING_OLD, LOGGING, COMMONS
# end
# define 'three' do
# compile.with "commons-collections:commons-collections:jar:2.2"
# end
# end
#
#
# With ArtifactNamespace you can do some more advanced stuff, the following
# annotated snipped could still be reduced if default artifact definitions were
# loaded from yaml file (see section below and ArtifactNamespace.load).
#
# -- buildfile --
# artifact_ns do |ns| # the current namespace (root if called outside a project)
# # default artifacts
# ns.spring = 'org.springframework:spring:jar:2.5'
# # default logger is log4j
# ns.logger = 'log4j:log4j:jar:1.2.15'
#
# # create a sub namespace by calling the #ns method,
# # artifacts defined on the sub-namespace can be referenced by
# # name :commons_net or by calling commons.net
# ns.ns :commons, :net => 'commons-net:commons-net:jar:1.4.0',
# :logging => 'comons-logging:commons-logging:jar:1.1.1'
#
#
# # When a child namespace asks for the :log artifact,
# # these artifacts will be searched starting from the :current namespace.
# ns.virtual :log, :logger, :commons_logging
# end
#
# artifact_ns('example2:one') do |ns| # namespace for the one subproject
# ns.logger = artifact('bea:wlcommons-logging:jar:8.1').from('path/to/wlcommons-logging.jar')
# end
# artifact_ns('example2:two') do |ns|
# ns.spring = '1.0' # for project two use an older spring version (just for an example)
# end
# artifact_ns('example2:three').commons_collections = 2.2'
# artifact_ns('example2:four') do |ns|
# ns.beanutils = 'commons-beanutils:commons-beanutils:jar:1.5' # just for this project
# ns.ns(:compilation).use :commons_logging, :beanutils, :spring # compile time dependencies
# ns.ns(:testing).use :log, :beanutils, 'cglib:cglib-nodep:jar:2.1.3' # run time dependencies
# end
#
# define 'example2' do
# define 'one' do
# compile.with :spring, :log, :commons # uses weblogic logging
# end
# define 'two' do
# compile.with :spring, :log, :commons # will take old spring
# end
# define 'three' do
# compile.with :commons_collections
# test.with artifact_ns('example2:two').spring # use spring from project two
# end
# define 'four' do
# compile.with artifact_ns.compilation
# test.with artifact_ns.testing
# end
# task(:down_them_all) do # again, just to fill this space with something ;)
# parent.projects.map(&method(:artifact_ns)).map(&:artifacts).map(&:invoke)
# end
# end
#
# = Loading from a yaml file (e. profiles.yaml)
#
# If your projects use lots of jars (after all we are using java ;) you may prefer
# to have constant artifact definitions on an external file.
# Doing so would allow an external tool (or future Buildr feature) to maintain
# an artifacts.yaml for you.
# An example usage is documented on the ArtifactNamespace.load method.
#
# = For addon/plugin writers & Customizing artifact versions
#
# Sometimes users would need to change the default artifact versions used by some
# module, for example, the XMLBeans compiler needs this, because of compatibility
# issues. Another example would be to select the groovy version to use on all our
# projects so that Buildr modules requiring groovy jars can use user preferred versions.
#
# To meet this goal, an ArtifactNamespace allows to specify ArtifactRequirement objects.
# In fact the only difference with the examples you have already seen is that requirements
# have an associated VersionRequirement, so that each time a user tries to select a version,
# buildr checks if it satisfies the requirements.
#
# Requirements are declared using the ArtifactNamespace#need method, but again,
# syntactic sugar is provided by ArtifactNamespace#method_missing.
#
# The following example is taken from the XMLBeans compiler module.
# And illustrates how addon authors should specify their requirements,
# provide default versions, and document the namespace for users to customize.
#
# module Buildr::XMLBeans
#
# # You need to document this constant, giving users some hints
# # about when are (maybe some of) these artifacts used. I mean,
# # some modules, add jars to the Buildr classpath when its file
# # is required, you would need to tell your users, so that they
# # can open the namespace and specify their defaults. Of course
# # when the requirements are defined, buildr checks if any compatible
# # version has been already defined, if so, uses it.
# #
# # Some things here have been changed to illustrate their meaning.
# REQUIRES = ArtifactNamespace.for(self).tap do |ns|
#
# # This jar requires a >2.0 version, default being 2.3.0
# ns.xmlbeans! 'org.apache.xmlbeans:xmlbeans:jar:2.3.0', '>2'
#
# # Users can customize with Buildr::XMLBeans::REQUIRES.stax_api = '1.2'
# # This is a non-flexible requirement, only satisfied by version 1.0.1
# ns.stax_api! 'stax:stax-api:jar:1.0.1'
#
# # This one is not part of XMLBeans, but is just another example
# # illustrating an `artifact requirement spec`.
#
# ns.need " some_name -> ar:ti:fact:3.2.5 -> ( >2 & <4)"
#
# # As you can see it's just an artifact spec, prefixed with
# # ' some_name -> ', this means users can use that name to
# # reference the requirement, also this string has a VersionRequirement
# # just after another ->.
# end
#
# # The REQUIRES constant is an ArtifactNamespace instance,
# # that means we can use it directly. Note that calling
# # Buildr.artifact_ns would lead to the currently executing context,
# # not the one for this module.
# def use
# # test if user specified his own version, if so, we could perform some
# # functionallity based on this.
# REQUIRES.some_name.selected? # => false
#
# REQUIRES.some_name.satisfied_by?('1.5') # => false
# puts REQUIRES.some_name.requirement # => ( >2 & <4 )
#
# REQUIRES.artifacts # get the Artifact tasks
# end
#
# end
#
# A more advanced example using ArtifactRequirement listeners is included
# in the artifact_namespace_spec.rb description for 'Extension using ArtifactNamespace'
# That's it for addon writers, now, users can select their preferred version with
# something like:
#
# require 'buildr/xmlbeans'
# Buildr::XMLBeans::REQUIRES.xmlbeans = '2.2.0'
#
# More advanced stuff, if users really need to select an xmlbeans version
# per project, they can do so letting :current (that is, the currently running
# namespace) be parent of the REQUIRES namespace:
#
# Buildr::XMLBeans::REQUIRES.parent = :current
#
# Now, provided that the compiler does not caches its artifacts, it will
# select the correct version. (See the first section for how to select per project
# artifacts).
#
#
class ArtifactNamespace
class << self
# Forget all namespaces, create a new ROOT
def clear
@instances = nil
remove_const(:ROOT) rescue nil
const_set(:ROOT, new('root'))
end
# Differs from Artifact.to_hash in that 1) it does not choke when version isn't present
# and 2) it assumes that if an artifact spec ends with a colon, e.g. "org.example:library:jdk5:"
# it indicates the last segment ("jdk5") is a classifier.
def to_hash(spec)
if spec.respond_to?(:to_spec)
to_hash spec.to_spec
elsif Hash === spec
return spec
elsif String === spec || Symbol === spec
spec = spec.to_s
if spec[-1,1] == ':'
group, id, type, classifier, *rest = spec.split(':').map { |part| part.empty? ? nil : part }
else
group, id, type, version, *rest = spec.split(':').map { |part| part.empty? ? nil : part }
unless rest.empty?
# Optional classifier comes before version.
classifier, version = version, rest.shift
end
end
fail "Expecting <group:id:type:version> or <group:id:type:classifier:version>, found <#{spec}>" unless rest.empty?
{ :group => group, :id => id, :type => type, :version => version, :classifier => classifier }.reject { |k,v| v == nil }
else
fail "Unexpected artifact spec: #{spec.inspect}"
end
end
# Populate namespaces from a hash of hashes.
# The following example uses the profiles yaml to achieve this.
#
# -- profiles.yaml --
# development:
# artifacts:
# root: # root namespace
# spring: org.springframework:spring:jar:2.5
# groovy: org.codehaus.groovy:groovy:jar:1.5.4
# logging: # define a named group
# - log4j:log4j:jar:1.2.15
# - commons-logging:commons-logging:jar:1.1.1
#
# # open Buildr::XMLBeans namespace
# Buildr::XMLBeans:
# xmlbeans: 2.2
#
# # for subproject one:oldie
# one:oldie:
# spring: org.springframework:spring:jar:1.0
#
# -- buildfile --
# ArtifactNamespace.load(Buildr.settings.profile['artifacts'])
def load(namespaces = {})
namespaces.each_pair { |name, uses| instance(name).use(uses) }
end
# :call-seq:
# ArtifactNamespace.instance { |current_ns| ... } -> current_ns
# ArtifactNamespace.instance(name) { |ns| ... } -> ns
# ArtifactNamespace.instance(:current) { |current_ns| ... } -> current_ns
# ArtifactNamespace.instance(:root) { |root_ns| ... } -> root_ns
#
# Obtain an instance for the given name
def instance(name = nil)
case name
when :root, 'root' then return ROOT
when ArtifactNamespace then return name
when Array then name = name.join(':')
when Module, Project then name = name.name
when :current, 'current', nil then
task = Thread.current[:rake_chain]
task = task.instance_variable_get(:@value) if task
name = case task
when Project then task.name
when Rake::Task then task.scope.join(':')
when nil then Buildr.application.current_scope.join(':')
end
end
name = name.to_s
if name.size == 0
instance = ROOT
else
name = name.to_s
@instances ||= Hash.new { |h, k| h[k] = new(k) }
instance = @instances[name]
end
yield(instance) if block_given?
instance
end
alias_method :[], :instance
alias_method :for, :instance
# :call-seq:
# ArtifactNamespace.root { |ns| ... } -> ns
#
# Obtain the root namespace, returns the ROOT constant
def root
yield ROOT if block_given?
ROOT
end
end
module DClone #:nodoc:
def dclone
clone = self.clone
clone.instance_variables.each do |i|
value = clone.instance_variable_get(i)
value = value.dclone rescue
clone.instance_variable_set(i, value)
end
clone
end
end
class Registry < Hash #:nodoc:
include DClone
attr_accessor :parent
def alias(new_name, old_name)
new_name = new_name.to_sym
old_name = old_name.to_sym
if obj = get(old_name, true)
self[new_name] = obj
@aliases ||= []
group = @aliases.find { |a| a.include?(new_name) }
group.delete(new_name) if group
group = @aliases.find { |a| a.include?(old_name) }
@aliases << (group = [old_name]) unless group
group << new_name unless group.include?(new_name)
end
obj
end
def aliases(name)
return [] unless name
name = name.to_sym
((@aliases ||= []).find { |a| a.include?(name) } || [name]).dup
end
def []=(key, value)
return unless key
super(key.to_sym, value)
end
def get(key, include_parent = nil)
[].tap { |a| aliases(key).select { |n| a[0] = self[n] } }.first ||
(include_parent && parent && parent.get(key, include_parent))
end
def keys(include_parent = nil)
(super() | (include_parent && parent && parent.keys(include_parent) || [])).uniq
end
def values(include_parent = nil)
(super() | (include_parent && parent && parent.values(include_parent) || [])).uniq
end
def key?(key, include_parent = nil)
return false unless key
super(key.to_sym) || (include_parent && parent && parent.key?(key, include_parent))
end
def delete(key, include_parent = nil)
aliases(key).map {|n| super(n) } && include_parent && parent && parent.delete(key, include_parent)
end
end
# An artifact requirement is an object that ActsAsArtifact and has
# an associated VersionRequirement. It also knows the name (some times equal to the
# artifact id) that is used to store it in an ArtifactNamespace.
class ArtifactRequirement
attr_accessor :version
attr_reader :name, :requirement
include DClone
# Create a requirement from an `artifact requirement spec`.
# This spec has three parts, separated by ->
#
# some_name -> ar:ti:fact:3.2.5 -> ( >2 & <4)
#
# As you can see it's just an artifact spec, prefixed with
# some_name ->
# the :some_name symbol becomes this object's name and
# is used to store it on an ArtifactNamespace.
#
# ar:ti:fact:3.2.5
#
# The second part is an artifact spec by itself, and specifies
# all remaining attributes, the version of this spec becomes
# the default version of this requirement.
#
# The last part consist of a VersionRequirement.
# -> ( >2 & <4)
#
# VersionRequirement supports RubyGem's comparison operators
# in adition to parens, logical and, logical or and negation.
# See the docs for VersionRequirement for more info on operators.
def initialize(spec)
self.class.send :include, ActsAsArtifact unless ActsAsArtifact === self
if ArtifactRequirement === spec
copy_attrs(spec)
else
spec = requirement_hash(spec)
apply_spec_no_validation(spec[:spec])
self.name = spec[:name]
@requirement = spec[:requirement]
@version = @requirement.default if VersionRequirement.requirement?(@version)
end
end
def apply_spec_no_validation(spec)
spec = ArtifactNamespace.to_hash(spec)
ActsAsArtifact::ARTIFACT_ATTRIBUTES.each { |key| instance_variable_set("@#{key}", spec[key]) }
self
end
# Copy attributes from other to this object
def copy_attrs(other)
(ActsAsArtifact::ARTIFACT_ATTRIBUTES + [:name, :requirement]).each do |attr|
value = other.instance_variable_get("@#{attr}")
value = value.dup if value && !value.kind_of?(Numeric) && !value.kind_of?(Symbol)
instance_variable_set("@#{attr}", value)
end
end
def name=(name)
@name = name.to_s
end
# Set a the requirement, this must be an string formatted for
# VersionRequirement#create to parse.
def requirement=(version_requirement)
@requirement = VersionRequirement.create(version_requirement.to_s)
end
# Return a hash consisting of :name, :spec, :requirement
def requirement_hash(spec = self)
result = {}
if String === spec
parts = spec.split(/\s*->\s*/, 3).map(&:strip)
case parts.size
when 1
result[:spec] = ArtifactNamespace.to_hash(parts.first)
when 2
if /^\w+$/ === parts.first
result[:name] = parts.first
result[:spec] = ArtifactNamespace.to_hash(parts.last)
else
result[:spec] = ArtifactNamespace.to_hash(parts.first)
result[:requirement] = VersionRequirement.create(parts.last)
end
when 3
result[:name] = parts.first
result[:spec] = ArtifactNamespace.to_hash(parts[1])
result[:requirement] = VersionRequirement.create(parts.last)
end
else
result[:spec] = ArtifactNamespace.to_hash(spec)
end
result[:name] ||= result[:spec][:id].to_s.to_sym
result[:requirement] ||= VersionRequirement.create(result[:spec][:version])
result
end
# Test if this requirement is satisfied by an artifact spec.
def satisfied_by?(spec)
return false unless requirement
spec = ArtifactNamespace.to_hash(spec)
hash = to_spec_hash
hash.delete(:version)
version = spec.delete(:version)
hash == spec && requirement.satisfied_by?(version)
end
# Has user selected a version for this requirement?
def selected?
@selected
end
def selected! #:nodoc:
@selected = true
@listeners.each { |l| l.call(self) } if @listeners
self
end
def add_listener(&callback)
(@listeners ||= []) << callback
end
# Return the Artifact object for the currently selected version
def artifact
::Buildr.artifact(self)
end
# Format this requirement as an `artifact requirement spec`
def to_requirement_spec
result = to_spec
result = "#{name} -> #{result}" if name
result = "#{result} -> #{requirement}" if requirement
result
end
def to_s #:nodoc:
id ? to_requirement_spec : version
end
# Return an artifact spec without the version part.
def unversioned_spec
hash = to_spec_hash
return nil if hash.values.compact.length <= 1
if hash[:classifier]
"#{hash[:group]}:#{hash[:id]}:#{hash[:type]}:#{hash[:classifier]}:"
else
"#{hash[:group]}:#{hash[:id]}:#{hash[:type]}"
end
end
class << self
def unversioned_spec(spec)
hash = ArtifactNamespace.to_hash(spec)
return nil if hash.values.compact.length <= 1
if hash[:classifier]
"#{hash[:group]}:#{hash[:id]}:#{hash[:type]}:#{hash[:classifier]}:"
else
"#{hash[:group]}:#{hash[:id]}:#{hash[:type]}"
end
end
end
end
include DClone
include Enumerable
attr_reader :name
def initialize(name = nil) #:nodoc:
@name = name.to_s if name
end
clear
def root
yield ROOT if block_given?
ROOT
end
# ROOT namespace has no parent
def parent
if root?
nil
elsif @parent.kind_of?(ArtifactNamespace)
@parent
elsif @parent
ArtifactNamespace.instance(@parent)
elsif name
parent_name = name.gsub(/::?[^:]+$/, '')
parent_name == name ? root : ArtifactNamespace.instance(parent_name)
else
root
end
end
# Set the parent for the current namespace, except if it is ROOT
def parent=(other)
raise 'Cannot set parent of root namespace' if root?
@parent = other
@registry = nil
end
# Is this the ROOT namespace?
def root?
ROOT == self
end
# Create a named sub-namespace, sub-namespaces are themselves
# ArtifactNamespace instances but cannot be referenced by
# the Buildr.artifact_ns, ArtifactNamespace.instance methods.
# Reference needs to be through this object using the given +name+
#
# artifact_ns('foo').ns(:bar).need :thing => 'some:thing:jar:1.0'
# artifact_ns('foo').bar # => the sub-namespace 'foo.bar'
# artifact_ns('foo').bar.thing # => the some thing artifact
#
# See the top level ArtifactNamespace documentation for examples
def ns(name, *uses, &block)
name = name.to_sym
sub = registry[name]
if sub
raise TypeError.new("#{name} is not a sub namespace of #{self}") unless sub.kind_of?(ArtifactNamespace)
else
sub = ArtifactNamespace.new("#{self.name}.#{name}")
sub.parent = self
registry[name] = sub
end
sub.use(*uses)
yield sub if block_given?
sub
end
# Test if a sub-namespace by the given name exists
def ns?(name)
sub = registry[name.to_sym]
ArtifactNamespace === sub
end
# :call-seq:
# artifact_ns.need 'name -> org:foo:bar:jar:~>1.2.3 -> 1.2.5'
# artifact_ns.need :name => 'org.foo:bar:jar:1.0'
#
# Create a new ArtifactRequirement on this namespace.
# ArtifactNamespace#method_missing provides syntactic sugar for this.
def need(*specs)
named = specs.flatten.inject({}) do |seen, spec|
if Hash === spec && (spec.keys & ActsAsArtifact::ARTIFACT_ATTRIBUTES).empty?
spec.each_pair do |name, spec|
if Array === spec # a group
seen[name] ||= spec.map { |s| ArtifactRequirement.new(s) }
else
artifact = ArtifactRequirement.new(spec)
artifact.name = name
seen[artifact.name] ||= artifact
end
end
else
artifact = ArtifactRequirement.new(spec)
seen[artifact.name] ||= artifact
end
seen
end
named.each_pair do |name, artifact|
if Array === artifact # a group
artifact.each do |a|
unvers = a.unversioned_spec
previous = registry[unvers]
if previous && previous.selected? && a.satisfied_by?(previous)
a.version = previous.version
end
registry[unvers] = a
end
group(name, *(artifact.map { |a| a.unversioned_spec } + [{:namespace => self}]))
else
unvers = artifact.unversioned_spec
previous = registry.get(unvers, true)
if previous && previous.selected? && artifact.satisfied_by?(previous)
artifact.version = previous.version
artifact.selected!
end
registry[unvers] = artifact
registry.alias name, unvers unless name.to_s[/^\s*$/]
end
end
self
end
# :call-seq:
# artifact_ns.use 'name -> org:foo:bar:jar:1.2.3'
# artifact_ns.use :name => 'org:foo:bar:jar:1.2.3'
# artifact_ns.use :name => '2.5.6'
#
# First and second form are equivalent, the third is used when an
# ArtifactRequirement has been previously defined with :name, so it
# just selects the version.
#
# ArtifactNamespace#method_missing provides syntactic sugar for this.
def use(*specs)
named = specs.flatten.inject({}) do |seen, spec|
if Hash === spec && (spec.keys & ActsAsArtifact::ARTIFACT_ATTRIBUTES).empty?
spec.each_pair do |name, spec|
if ArtifactNamespace === spec # create as subnamespace
raise ArgumentError.new("Circular reference") if self == spec
registry[name.to_sym] = spec
elsif Numeric === spec || (String === spec && VersionRequirement.version?(spec))
artifact = ArtifactRequirement.allocate
artifact.name = name
artifact.version = spec.to_s
seen[artifact.name] ||= artifact
elsif Symbol === spec
self.alias name, spec
elsif Array === spec # a group
seen[name] ||= spec.map { |s| ArtifactRequirement.new(s) }
else
artifact = ArtifactRequirement.new(spec)
artifact.name = name
seen[artifact.name] ||= artifact
end
end
else
if Symbol === spec
artifact = get(spec).dclone
else
artifact = ArtifactRequirement.new(spec)
end
seen[artifact.name] ||= artifact
end
seen
end
named.each_pair do |name, artifact|
is_group = Array === artifact
artifact = [artifact].flatten.map do |artifact|
unvers = artifact.unversioned_spec
previous = get(unvers, false) || get(name, false)
if previous # have previous on current namespace
if previous.requirement # we must satisfy the requirement
if unvers
satisfied = previous.satisfied_by?(artifact)
else # we only have the version
satisfied = previous.requirement.satisfied_by?(artifact.version)
end
raise "Unsatisfied dependency #{previous} " +
"not satisfied by #{artifact}" unless satisfied
previous.version = artifact.version # OK, set new version
artifact = previous # use the same object for aliases
else # not a requirement, set the new values
unless artifact.id == previous.id && name != previous.name
previous.copy_attrs(artifact)
artifact = previous
end
end
else
if unvers.nil? && # we only have the version
(previous = get(unvers, true, false, false))
version = artifact.version
artifact.copy_attrs(previous)
artifact.version = version
end
artifact.requirement = nil
end
artifact.selected!
end
artifact = artifact.first unless is_group
if is_group
names = artifact.map do |art|
unv = art.unversioned_spec
registry[unv] = art
unv
end
group(name, *(names + [{:namespace => self}]))
elsif artifact.id
unvers = artifact.unversioned_spec
registry[name] = artifact
registry.alias unvers, name
else
registry[name] = artifact
end
end
self
end
# Like Hash#fetch
def fetch(name, default = nil, &block)
block ||= proc { raise IndexError.new("No artifact found by name #{name.inspect} in namespace #{self}") }
real_name = name.to_s[/^[\w\-\.]+$/] ? name : ArtifactRequirement.unversioned_spec(name)
get(real_name.to_sym) || default || block.call(name)
end
# :call-seq:
# artifact_ns[:name] -> ArtifactRequirement
# artifact_ns[:many, :names] -> [ArtifactRequirement]
def [](*names)
ary = values_at(*names)
names.size == 1 ? ary.first : ary
end
# :call-seq:
# artifact_ns[:name] = 'some:cool:jar:1.0.2'
# artifact_ns[:name] = '1.0.2'
#
# Just like the use method
def []=(*names)
values = names.pop
values = [values] unless Array === values
names.each_with_index do |name, i|
use name => (values[i] || values.last)
end
end
# yield each ArtifactRequirement
def each(&block)
values.each(&block)
end
# return Artifact objects for each requirement
def artifacts(*names)
(names.empty? && values || values_at(*names)).map(&:artifact)
end
# Return all requirements for this namespace
def values(include_parents = false, include_groups = true)
seen, dict = {}, registry
while dict
dict.each do |k, v|
v = v.call if v.respond_to?(:call)
v = v.values if v.kind_of?(ArtifactNamespace)
if Array === v && include_groups
v.compact.each { |v| seen[v.name] = v unless seen.key?(v.name) }
else
seen[v.name] = v unless seen.key?(v.name)
end
end
dict = include_parents ? dict.parent : nil
end
seen.values
end
# Return only the named requirements
def values_at(*names)
names.map do |name|
catch :artifact do
unless name.to_s[/^[\w\-\.]+$/]
unvers = ArtifactRequirement.unversioned_spec(name)
unless unvers.to_s == name.to_s
req = ArtifactRequirement.new(name)
reg = self
while reg
candidate = reg.send(:get, unvers, false, false, true)
throw :artifact, candidate if req.satisfied_by?(candidate)
reg = reg.parent
end
end
end
get(name.to_sym)
end
end
end
def key?(name, include_parents = false)
name = ArtifactRequirement.unversioned_spec(name) unless name.to_s[/^[\w\-\.]+$/]
registry.key?(name, include_parents)
end
def keys
values.map(&:name)
end
def delete(name, include_parents = false)
registry.delete(name, include_parents)
self
end
def clear
keys.each { |k| delete(k) }
end
# :call-seq:
# group :who, :me, :you
# group :them, :me, :you, :namespace => ns
#
# Create a virtual group on this namespace. When the namespace
# is asked for the +who+ artifact, it's value is an array made from
# the remaining names. These names are searched by default from the current
# namespace.
# Second form specified the starting namespace to search from.
def group(group_name, *members)
namespace = (Hash === members.last && members.pop[:namespace]) || :current
registry[group_name] = lambda do
artifacts = self.class[namespace].values_at(*members)
artifacts = artifacts.first if members.size == 1
artifacts
end
self
end
alias_method :virtual, :group
# Create an alias for a named requirement.
def alias(new_name, old_name)
registry.alias(new_name, old_name) or
raise NameError.new("Undefined artifact name: #{old_name}")
end
def to_s #:nodoc:
name.to_s
end
# :call-seq:
# artifact_ns.cool_aid!('cool:aid:jar:2.3.4', '~>2.3') -> artifact_requirement
# artifact_ns.cool_aid = '2.3.5'
# artifact_ns.cool_aid -> artifact_requirement
# artifact_ns.cool_aid? -> true | false
#
# First form creates an ArtifactRequirement on the namespace.
# It is equivalent to providing a requirement_spec to the #need method:
# artifact_ns.need "cool_aid -> cool:aid:jar:2.3.4 -> ~>2.3"
# the second argument is optional.
#
# Second form can be used to select an artifact version
# and is equivalent to:
# artifact_ns.use :cool_aid => '1.0'
#
# Third form obtains the named ArtifactRequirement, can be
# used to test if a named requirement has been defined.
# It is equivalent to:
# artifact_ns.fetch(:cool_aid) { nil }
#
# Last form tests if the ArtifactRequirement has been defined
# and a version has been selected for use.
# It is equivalent to:
#
# artifact_ns.has_cool_aid?
# artifact_ns.values_at(:cool_aid).flatten.all? { |a| a && a.selected? }
#
def method_missing(name, *args, &block)
case name.to_s
when /!$/ then
name = $`.intern
if args.size < 1 || args.size > 2
raise ArgumentError.new("wrong number of arguments for #{name}!(spec, version_requirement?)")
end
need name => args.first
get(name).tap { |r| r.requirement = args.last if args.size == 2 }
when /=$/ then use $` => args.first
when /\?$/ then
name = $`.gsub(/^(has|have)_/, '').intern
[get(name)].flatten.all? { |a| a && a.selected? }
else
if block || args.size > 0
raise ArgumentError.new("wrong number of arguments #{args.size} for 0 or block given")
end
get(name)
end
end
# Return an anonymous module
# # first create a requirement
# artifact_ns.cool_aid! 'cool:aid:jar:>=1.0'
#
# # extend an object as a cool_aid delegator
# jars = Object.new.extend(artifact_ns.accessor(:cool_aid))
# jars.cool_aid = '2.0'
#
# artifact_ns.cool_aid.version # -> '2.0'
def accessor(*names)
ns = self
Module.new do
names.each do |name|
define_method("#{name}") { ns.send("#{name}") }
define_method("#{name}?") { ns.send("#{name}?") }
define_method("#{name}=") { |vers| ns.send("#{name}=", vers) }
end
end
end
private
def get(name, include_parents = true, include_subs = true, include_self = true) #:nodoc:
artifact = nil
if include_subs && name.to_s[/_/] # try sub namespaces first
sub, parts = self, name.to_s.split('_')
sub_name = parts.shift.to_sym
until sub != self || parts.empty?
if registry[sub_name].kind_of?(ArtifactNamespace)
sub = registry[sub_name]
artifact = sub[parts.join('_')]
else
sub_name = [sub_name, parts.shift].join('_').to_sym
end
end
end
unless artifact
if include_self
artifact = registry.get(name, include_parents)
elsif include_parents && registry.parent
artifact = registry.parent.get(name, true)
end
end
artifact = artifact.call if artifact && artifact.respond_to?(:call)
artifact
end
def registry
@registry ||= Registry.new.tap do |m|
m.parent = parent.send(:registry) unless root?
end
end
end # ArtifactNamespace
# :call-seq:
# project.artifact_ns -> ArtifactNamespace
# Buildr.artifact_ns(name) -> ArtifactNamespace
# Buildr.artifact_ns -> ArtifactNamespace for the currently running Project
#
# Open an ArtifactNamespace.
# If a block is provided, the namespace is yielded to it.
#
# See also ArtifactNamespace.instance
def artifact_ns(name = nil, &block)
name = self if name.nil? && self.kind_of?(Project)
ArtifactNamespace.instance(name, &block)
end
end