lib/cocoapods-core/specification.rb
require 'active_support/core_ext/string/strip.rb'
require 'cocoapods-core/specification/consumer'
require 'cocoapods-core/specification/dsl'
require 'cocoapods-core/specification/linter'
require 'cocoapods-core/specification/root_attribute_accessors'
require 'cocoapods-core/specification/set'
require 'cocoapods-core/specification/json'
module Pod
# The Specification provides a DSL to describe a Pod. A pod is defined as a
# library originating from a source. A specification can support detailed
# attributes for modules of code through subspecs.
#
# Usually it is stored in files with `podspec` extension.
#
class Specification
include Pod::Specification::DSL
include Pod::Specification::DSL::Deprecations
include Pod::Specification::RootAttributesAccessors
include Pod::Specification::JSONSupport
# @return [Specification] the parent of the specification unless the
# specification is a root.
#
attr_reader :parent
# @return [Integer] the cached hash value for this spec.
#
attr_reader :hash_value
# @param [Specification] parent @see parent
#
# @param [String] name
# the name of the specification.
#
# @param [Boolean] test_specification
# Whether the specification is a test specification
#
# @param [Boolean] app_specification
# Whether the specification is an app specification
#
def initialize(parent = nil, name = nil, test_specification = false, app_specification: false)
raise StandardError, "#{self} can not be both an app and test specification." if test_specification && app_specification
@attributes_hash = {}
@subspecs = []
@consumers = {}
@parent = parent
@hash_value = nil
@test_specification = test_specification
@app_specification = app_specification
attributes_hash['name'] = name
attributes_hash['test_type'] = :unit if test_specification
yield self if block_given?
end
def initialize_copy(other)
super
@subspecs = @subspecs.map do |subspec|
subspec = subspec.dup
subspec.instance_variable_set :@parent, self
subspec
end
end
# @return [Hash] the hash that stores the information of the attributes of
# the specification.
#
attr_accessor :attributes_hash
# @return [Array<Specification>] The subspecs of the specification.
#
attr_accessor :subspecs
# @return [Boolean] If this specification is a test specification.
#
attr_accessor :test_specification
alias_method :test_specification?, :test_specification
# @return [Boolean] If this specification is an app specification.
#
attr_accessor :app_specification
alias_method :app_specification?, :app_specification
# Checks if a specification is equal to the given one according its name
# and to its version.
#
# @param [Specification] other
# the specification to compare with.
#
# @todo Not sure if comparing only the name and the version is the way to
# go. This is used by the installer to group specifications by root
# spec.
#
# @return [Boolean] Whether the specifications are equal.
#
def ==(other)
other.is_a?(self.class) &&
name == other.name &&
version == other.version
end
alias_method :eql?, :==
# Return the hash value for this specification according to its attributes
# hash.
#
# @note This function must have the property that a.eql?(b) implies
# a.hash == b.hash.
#
# @note This method is used by the Hash class.
#
# @return [Fixnum] The hash value.
#
def hash
if @hash_value.nil?
@hash_value = (name.hash * 53) ^ version.hash
end
@hash_value
end
# @return [String] A string suitable for representing the specification in
# clients.
#
def to_s
specified_version = raw_version || ''
if name && !specified_version.empty?
"#{name} (#{specified_version})"
elsif name
name
else
'No-name'
end
end
# @return [String] A string suitable for debugging.
#
def inspect
"#<#{self.class.name} name=#{name.inspect}>"
end
# @param [String] string_representation
# the string that describes a {Specification} generated from
# {Specification#to_s}.
#
# @example Input examples
#
# "libPusher (1.0)"
# "RestKit/JSON (1.0)"
#
# @return [Array<String, Version>] the name and the version of a
# pod.
#
def self.name_and_version_from_string(string_representation)
match_data = string_representation.match(/\A((?:\s?[^\s(])+)(?: \((.+)\))?\Z/)
unless match_data
raise Informative, 'Invalid string representation for a ' \
"specification: `#{string_representation}`. " \
'The string representation should include the name and ' \
'optionally the version of the Pod.'
end
name = match_data[1]
vers = Version.new(match_data[2])
[name, vers]
end
# Returns the root name of a specification.
#
# @param [String] the name of a specification or of a subspec.
#
# @return [String] the root name
#
def self.root_name(full_name)
if index = full_name.index('/')
full_name.slice(0, index)
else
full_name
end
end
# Returns the module name of a specification
#
# @return [String] the module name
#
def module_name
attributes_hash['module_name'] ||
c99ext_identifier(attributes_hash['header_dir']) ||
c99ext_identifier(attributes_hash['name'])
end
private
# Transforms the given string into a valid +identifier+ after C99ext
# standard, so that it can be used in source code where escaping of
# ambiguous characters is not applicable.
#
# @param [String] name
# any name, which may contain leading numbers, spaces or invalid
# characters.
#
# @return [String]
#
def c99ext_identifier(name)
return nil if name.nil?
I18n.transliterate(name).gsub(/^([0-9])/, '_\1').
gsub(/[^a-zA-Z0-9_]/, '_').gsub(/_+/, '_')
end
# @return [Object, Nil]
# the raw value specified for the version attribute, or nil
#
def raw_version
root.attributes_hash['version']
end
#-------------------------------------------------------------------------#
public
# @!group Hierarchy
# @return [Specification] The root specification or itself if it is root.
#
def root
parent ? parent.root : self
end
# @return [Boolean] whether the specification is root.
#
def root?
parent.nil?
end
# @return [Boolean] whether the specification is a subspec.
#
def subspec?
!parent.nil?
end
#-------------------------------------------------------------------------#
public
# @return [Symbol] Spec type of the current spec.
#
# @note see Attribute#SUPPORTED_SPEC_TYPES for the list of available spec_types.
#
def spec_type
return :app if app_specification?
return :test if test_specification?
:library
end
# @!group Dependencies & Subspecs
# @return [Boolean] If this specification is a library specification.
#
# @note a library specification is a specification that is not of type app or test.
#
def library_specification?
!app_specification? && !test_specification?
end
# @return [Boolean] If this specification is not a library specification.
#
# @note see #library_specification?
#
def non_library_specification?
!library_specification?
end
# @return [Symbol] the test type supported if this is a test specification.
#
def test_type
attributes_hash['test_type'].to_sym
end
# @return [Array<Specification>] the list of all the test subspecs of
# a specification.
#
def test_specs
subspecs.select(&:test_specification?)
end
# @return [Array<Specification>] the list of all the app subspecs of
# a specification.
#
def app_specs
subspecs.select(&:app_specification?)
end
# @return [Array<Specification>] the list of all the non libary (app or test) subspecs of
# a specification.
#
def non_library_specs
subspecs.select(&:non_library_specification?)
end
# @return [Array<Specification>] the recursive list of all the subspecs of
# a specification.
#
def recursive_subspecs
mapper = lambda do |spec|
spec.subspecs.map do |subspec|
[subspec, *mapper.call(subspec)]
end.flatten
end
mapper.call(self)
end
# Returns the subspec with the given name or the receiver if the name is
# nil or equal to the name of the receiver.
#
# @param [String] relative_name
# the relative name of the subspecs starting from the receiver
# including the name of the receiver.
#
# @param [Boolean] raise_if_missing
# whether an exception should be raised if no specification named
# `relative_name` is found.
#
# @example Retrieving a subspec
#
# s.subspec_by_name('Pod/subspec').name #=> 'subspec'
#
# @return [Specification] the subspec with the given name or self.
#
def subspec_by_name(relative_name, raise_if_missing = true, include_non_library_specifications = false)
if relative_name.nil? || relative_name == base_name
self
elsif base_name.nil?
if raise_if_missing
raise Informative, "Trying to access a `#{relative_name}` " \
"specification from `#{defined_in_file}`, which has no contents."
else
return nil
end
elsif relative_name.downcase == base_name.downcase
raise Informative, "Trying to access a `#{relative_name}` " \
"specification from `#{base_name}`, which has a different case."
else
remainder = relative_name[base_name.size + 1..-1]
subspec_name = remainder.split('/').shift
subspec = subspecs.find { |s| s.base_name == subspec_name && (include_non_library_specifications || !s.non_library_specification?) }
unless subspec
if raise_if_missing
raise Informative, 'Unable to find a specification named ' \
"`#{relative_name}` in `#{name} (#{version})`."
else
return nil
end
end
subspec.subspec_by_name(remainder, raise_if_missing, include_non_library_specifications)
end
end
# @return [Array<String>, Symbol] the name(s) of the default subspecs if provided or :none for no default subspecs.
#
def default_subspecs
# TODO: remove singular form and update the JSON specs.
value = Array(attributes_hash['default_subspecs'] || attributes_hash['default_subspec'])
first = value.first
if first == :none || first == 'none'
first.to_sym
else
value
end
end
# Returns the dependencies on subspecs.
#
# @note A specification has a dependency on either the
# {#default_subspecs} or each of its children subspecs that are
# compatible with its platform.
#
# @param [Platform] platform
# return only dependencies supported on the given platform.
#
# @return [Array<Dependency>] the dependencies on subspecs.
#
def subspec_dependencies(platform = nil)
specs = if default_subspecs.empty?
subspecs.compact.reject(&:non_library_specification?)
elsif default_subspecs == :none
[]
else
default_subspecs.map do |subspec_name|
root.subspec_by_name("#{name}/#{subspec_name}")
end
end
if platform
specs = specs.select { |s| s.supported_on_platform?(platform) }
end
specs.map { |s| Dependency.new(s.name, version) }
end
# Returns the dependencies on other Pods or subspecs of other Pods.
#
# @param [Platform] platform
# return only dependencies supported on the given platform.
#
# @note External dependencies are inherited by subspecs
#
# @return [Array<Dependency>] the dependencies on other Pods.
#
def dependencies(platform = nil)
if platform
consumer(platform).dependencies || []
else
available_platforms.map do |spec_platform|
consumer(spec_platform).dependencies
end.flatten.uniq
end
end
# @return [Array<Dependency>] all the dependencies of the specification.
#
def all_dependencies(platform = nil)
dependencies(platform) + subspec_dependencies(platform)
end
# Returns whether a dependency is whitelisted for the given configuration.
#
# @param [Pod::Dependency] dependency
# the dependency verify.
#
# @param [Symbol, String] configuration
# the configuration to check against.
#
# @return [Boolean] whether the dependency is whitelisted or not.
#
def dependency_whitelisted_for_configuration?(dependency, configuration)
inherited = -> { root? ? true : parent.dependency_whitelisted_for_configuration?(dependency, configuration) }
return inherited[] unless configuration_whitelist = attributes_hash['configuration_pod_whitelist']
return inherited[] unless whitelist_for_pod = configuration_whitelist[dependency.name]
whitelist_for_pod.include?(configuration.to_s.downcase)
end
# Returns a consumer to access the multi-platform attributes.
#
# @param [String, Symbol, Platform] platform
# the platform of the consumer
#
# @return [Specification::Consumer] the consumer for the given platform
#
def consumer(platform)
platform = platform.to_sym
@consumers[platform] ||= Consumer.new(self, platform)
end
# @return [Bool, String] The prefix_header_file value.
#
def prefix_header_file
attributes_hash['prefix_header_file']
end
# @return [Array<Hash{Symbol=>String}>] The script_phases value.
#
def script_phases
script_phases = attributes_hash['script_phases'] || []
script_phases.map do |script_phase|
phase = Specification.convert_keys_to_symbol(script_phase)
phase[:execution_position] = if phase.key?(:execution_position)
phase[:execution_position].to_sym
else
:any
end
phase
end
end
# @return [Hash] The on demand resources value.
#
def on_demand_resources
attributes_hash['on_demand_resources'] || {}
end
# @return [Hash] The scheme value.
#
def scheme
value = attributes_hash['scheme'] || {}
Specification.convert_keys_to_symbol(value, :recursive => false)
end
# @return [Hash] The Info.plist value.
#
def info_plist
attributes_hash['info_plist'] || {}
end
#-------------------------------------------------------------------------#
public
# @!group DSL helpers
# @return [Boolean] whether the specification should use a directory as its
# source.
#
def local?
return true if source[:path]
false
end
# @return [Boolean] whether the specification is supported in the given
# platform.
#
# @overload supported_on_platform?(platform)
#
# @param [Platform] platform
# the platform which is checked for support.
#
# @overload supported_on_platform?(symbolic_name, deployment_target)
#
#
def supported_on_platform?(*platform)
platform = Platform.new(*platform)
available_platforms.any? { |available| platform.supports?(available) }
end
# @return [Array<Platform>] The platforms that the Pod is supported on.
#
# @note If no platform is specified, this method returns all known
# platforms.
#
def available_platforms
names = supported_platform_names
names = PLATFORMS if names.empty?
names.map { |name| Platform.new(name, deployment_target(name)) }
end
# Returns the deployment target for the specified platform.
#
# @param [String] platform_name
# the symbolic name of the platform.
#
# @return [String] the deployment target
# @return [Nil] if not deployment target was specified for the platform.
#
def deployment_target(platform_name)
result = platform_hash[platform_name.to_s]
result ||= parent.deployment_target(platform_name) if parent
result
end
protected
# @return [Array[Symbol]] the symbolic name of the platform in which the
# specification is supported.
#
# @return [Nil] if the specification is supported on all the known
# platforms.
#
def supported_platform_names
result = platform_hash.keys
if result.empty? && parent
result = parent.supported_platform_names
end
result
end
# @return [Hash] the normalized hash which represents the platform
# information.
#
def platform_hash
case value = attributes_hash['platforms']
when String
{ value => nil }
when Array
result = {}
value.each do |a_value|
result[a_value] = nil
end
result
when Hash
value
else
{}
end
end
public
#-------------------------------------------------------------------------#
# @!group DSL attribute writers
# Sets the value for the attribute with the given name.
#
# @param [Symbol] name
# the name of the attribute.
#
# @param [Object] value
# the value to store.
#
# @param [Symbol] platform_name
# If provided the attribute is stored only for the given platform.
#
# @note If the provides value is Hash the keys are converted to a string.
#
# @return void
#
def store_attribute(name, value, platform_name = nil)
name = name.to_s
value = Specification.convert_keys_to_string(value) if value.is_a?(Hash)
value = value.strip_heredoc.strip if value.respond_to?(:strip_heredoc)
if platform_name
platform_name = platform_name.to_s
attributes_hash[platform_name] ||= {}
attributes_hash[platform_name][name] = value
else
attributes_hash[name] = value
end
end
# Defines the setters methods for the attributes providing support for the
# Ruby DSL.
#
DSL.attributes.values.each do |a|
define_method(a.writer_name) do |value|
store_attribute(a.name, value)
end
if a.writer_singular_form
alias_method(a.writer_singular_form, a.writer_name)
end
end
# Converts the keys of the given hash to a string.
#
# @param [Object] value
# the value that needs to be stripped from the Symbols.
#
# @param [Boolean] recursive
# whether to convert keys of nested hashes.
#
# @return [Hash] the hash with the keys as strings instead of symbols.
#
def self.convert_keys_to_string(value, recursive: true)
return unless value
result = {}
value.each do |key, subvalue|
subvalue = Specification.convert_keys_to_string(subvalue) if recursive && subvalue.is_a?(Hash)
result[key.to_s] = subvalue
end
result
end
# Converts the keys of the given hash to a symbol.
#
# @param [Object] value
# the value that needs to be stripped from the Strings.
#
# @param [Boolean] recursive
# whether to convert keys of nested hashes.
#
# @return [Hash] the hash with the keys as symbols instead of strings.
#
def self.convert_keys_to_symbol(value, recursive: true)
return unless value
result = {}
value.each do |key, subvalue|
subvalue = Specification.convert_keys_to_symbol(subvalue) if recursive && subvalue.is_a?(Hash)
result[key.to_sym] = subvalue
end
result
end
#-------------------------------------------------------------------------#
public
# @!group File representation
# @return [String] The SHA1 digest of the file in which the specification
# is defined.
#
# @return [Nil] If the specification is not defined in a file.
#
def checksum
@checksum ||= begin
if root?
unless defined_in_file.nil?
require 'digest'
checksum = Digest::SHA1.hexdigest(File.read(defined_in_file))
checksum = checksum.encode('UTF-8') if checksum.respond_to?(:encode)
checksum
end
else
root.checksum
end
end
end
# @return [String] the path where the specification is defined, if loaded
# from a file.
#
def defined_in_file
root? ? @defined_in_file : root.defined_in_file
end
# Loads a specification form the given path.
#
# @param [Pathname, String] path
# the path of the `podspec` file.
#
# @param [String] subspec_name
# the name of the specification that should be returned. If it is
# nil returns the root specification.
#
# @raise If the file doesn't return a Pods::Specification after
# evaluation.
#
# @return [Specification] the specification
#
def self.from_file(path, subspec_name = nil)
path = Pathname.new(path)
unless path.exist?
raise Informative, "No podspec exists at path `#{path}`."
end
string = File.open(path, 'r:utf-8', &:read)
# Work around for Rubinius incomplete encoding in 1.9 mode
if string.respond_to?(:encoding) && string.encoding.name != 'UTF-8'
string.encode!('UTF-8')
end
from_string(string, path, subspec_name)
end
# Loads a specification with the given string.
# The specification is evaluated in the context of `path`.
#
# @param [String] spec_contents
# A string describing a specification.
#
# @param [Pathname, String] path @see from_file
# @param [String] subspec_name @see from_file
#
# @return [Specification] the specification
#
def self.from_string(spec_contents, path, subspec_name = nil)
path = Pathname.new(path).expand_path
spec = nil
case path.extname
when '.podspec'
Dir.chdir(path.parent.directory? ? path.parent : Dir.pwd) do
spec = ::Pod._eval_podspec(spec_contents, path)
unless spec.is_a?(Specification)
raise Informative, "Invalid podspec file at path `#{path}`."
end
end
when '.json'
spec = Specification.from_json(spec_contents, path)
else
raise Informative, "Unsupported specification format `#{path.extname}` for spec at `#{path}`."
end
spec.defined_in_file = path
spec.subspec_by_name(subspec_name, true)
end
# Sets the path of the `podspec` file used to load the specification.
#
# @param [String] file
# the `podspec` file.
#
# @return [void]
#
# @visibility private
#
def defined_in_file=(file)
unless root?
raise StandardError, 'Defined in file can be set only for root specs.'
end
@defined_in_file = file
end
# Sets the name of the `podspec`.
#
# @param [String] name
# the `podspec` name.
#
# @return [void]
#
# @visibility private
#
def name=(name)
@hash_value = nil
attributes_hash['name'] = name
end
# Sets the version of the `podspec`.
#
# @param [String] version
# the `podspec` version.
#
# @return [void]
#
# @visibility private
#
def version=(version)
@hash_value = nil
store_attribute(:version, version)
@version = nil
end
# @!group Validation
# Validates the cocoapods_version in the specification against the current version of Core.
# It will raise an Informative error if the version is not satisfied.
#
def validate_cocoapods_version
unless cocoapods_version.satisfied_by?(Version.create(CORE_VERSION))
raise Informative, "`#{name}` requires CocoaPods version `#{cocoapods_version}`, " \
"which is not satisfied by your current version, `#{CORE_VERSION}`."
end
end
end
#---------------------------------------------------------------------------#
# @visibility private
#
# Evaluates the given string in the namespace of the Pod module.
#
# @param [String] string
# The string containing the Ruby description of the Object to
# evaluate.
#
# @param [Pathname] path
# The path where the object to evaluate is stored.
#
# @return [Object] it can return any object but, is expected to be called on
# `podspec` files that should return a #{Specification}.
#
#
def self._eval_podspec(string, path)
# rubocop:disable Security/Eval
eval(string, nil, path.to_s)
# rubocop:enable Security/Eval
# rubocop:disable Lint/RescueException
rescue Exception => e
# rubocop:enable Lint/RescueException
message = "Invalid `#{path.basename}` file: #{e.message}"
raise DSLError.new(message, path, e, string)
end
end