@qidiandasheng
2022-01-04T22:04:32.000000Z
字数 14692
阅读 871
Cocoapods
CocoaPods-Core
用于CocoaPods
中配置文件的解析,包括Podfile
、Podspec
以及解析后的依赖锁存文件,如Podfile.lock
等。
我们先通过入口文件 lib/cocoapods-core.rb
来一窥 Core 项目的主要文件:
module Pod
require 'cocoapods-core/gem_version'
class PlainInformative < StandardError; end
class Informative < PlainInformative; end
require 'pathname'
require 'cocoapods-core/vendor'
# 用于存储 PodSpec 中的版本号
autoload :Version, 'cocoapods-core/version'
# pod 的版本限制
autoload :Requirement, 'cocoapods-core/requirement'
# 配置 Podfile 或 PodSpec 中的 pod 依赖
autoload :Dependency, 'cocoapods-core/dependency'
# 获取 Github 仓库信息
autoload :GitHub, 'cocoapods-core/github'
# 处理 HTTP 请求
autoload :HTTP, 'cocoapods-core/http'
# 记录最终 pod 的依赖信息
autoload :Lockfile, 'cocoapods-core/lockfile'
# 记录 SDK 的名称和 target 版本
autoload :Platform, 'cocoapods-core/platform'
# 对应 Podfile 文件的 class
autoload :Podfile, 'cocoapods-core/podfile'
# 管理 PodSpec 的集合
autoload :Source, 'cocoapods-core/source'
# 管理基于 CDN 来源的 PodSpec 集合
autoload :CDNSource, 'cocoapods-core/cdn_source'
# 管理基于 Trunk 来源的 PodSpec 集合
autoload :TrunkSource, 'cocoapods-core/trunk_source'
# 对应 PodSpec 文件的 class
autoload :Specification, 'cocoapods-core/specification'
# 将 pod 信息转为 .yml 文件,用于 lockfile 的序列化
autoload :YAMLHelper, 'cocoapods-core/yaml_helper'
# 记录 pod 依赖类型,是静态库/动态库
autoload :BuildType, 'cocoapods-core/build_type'
...
Spec = Specification
end
将这些 Model 类按照对应的依赖关系进行划分,层级如下:
TargetDefinition
是一个多叉树结构,每个节点记录着 Podfile
中定义的 Pod
的 Source
来源、Build Setting
、Pod
子依赖等。该树的根节点指向 Podfile
,而 Podfile
中的 root_target_definitions
则记录着所有的 TargetDefinition
的根节点,正常情况下该 list
中只有一个 root
即 Pods.project。
为了便于阅读,简化了大量的 DSL 配置相关的方法和属性并对代码顺序做了调整,大致结构如下:
module Pod
class Podfile
class TargetDefinition
# 父节点: TargetDefinition 或者 Podfile
attr_reader :parent
# 子节点: TargetDefinition
attr_reader :children
# 记录 tareget 的配置信息
attr_accessor :internal_hash
def root?
parent.is_a?(Podfile) || parent.nil?
end
def root
if root?
self
else
parent.root
end
end
def podfile
root.parent
end
# ...
end
end
在初始化Podfile
对象时就会初始化根TargetDefinition
:
# podfile.rb
def initialize(defined_in_file = nil, internal_hash = {}, &block)
self.defined_in_file = defined_in_file
@internal_hash = internal_hash
if block
# TargetDefinition名为Pods,parent父节点为podfile
default_target_def = TargetDefinition.new('Pods', self)
default_target_def.abstract = true
@root_target_definitions = [default_target_def]
@current_target_definition = default_target_def
instance_eval(&block)
else
@root_target_definitions = []
end
end
然后在Podfile
文件里的每一个target
声明都会调用dsl.rb
里对应的target
函数,初始化对应的子TargetDefinition
:
Specification
即存储PodSpec
的内容,是用于描述一个Pod库的源代码和资源将如何被打包编译成链接库或framework
。
Podspec
支持的文件格式为 .podspec
和 .json
两种,而 .podspec
本质是 Ruby
文件。
在数据结构上Specification
与TargetDefinition
是类似的,同为多叉树结构。这里的parent
是为 subspec
保留的,用于指向其父节点的Spec
。简化后的Spec
的类如下:
require 'active_support/core_ext/string/strip.rb'
# 记录对应 platform 上 Spec 的其他 pod 依赖
require 'cocoapods-core/specification/consumer'
# 解析 DSL
require 'cocoapods-core/specification/dsl'
# 校验 Spec 的正确性,并抛出对应的错误和警告
require 'cocoapods-core/specification/linter'
# 用于解析 DSL 内容包含的配置信息
require 'cocoapods-core/specification/root_attribute_accessors'
# 记录一个 Pod 所有依赖的 Spec 来源信息
require 'cocoapods-core/specification/set'
# json 格式数据解析
require 'cocoapods-core/specification/json'
module Pod
class Specification
include Pod::Specification::DSL
include Pod::Specification::DSL::Deprecations
include Pod::Specification::RootAttributesAccessors
include Pod::Specification::JSONSupport
# `subspec` 的父节点
attr_reader :parent
# `Spec` 的唯一 id,由 name + version 的 hash 构成
attr_reader :hash_value
# 记录 `Spec` 的配置信息
attr_accessor :attributes_hash
# `Spec` 包含的 `subspec`
attr_accessor :subspecs
# 递归调用获取 Specification 的根节点
def root
parent ? parent.root : self
end
def hash
if @hash_value.nil?
@hash_value = (name.hash * 53) ^ version.hash
end
@hash_value
end
# ...
end
end
Specification
同样用 map attributes_hash
来记录配置信息。
Podfile
是用于描述一个或多个 Xcode Project
中各个 Targets
之间的依赖关系。
这些 Targets
的依赖关系对应的就是 TargetDefinition
树中的各子节点的层级关系。如前面所说,有了 Podfile
这个根节点的指向,仅需对依赖树进行遍历,就能轻松获取完整的依赖关系。
有了这层依赖树,对于某个 Pod 库的更新即是对树节点的更新,便可轻松的分析出此次更新涉及的影响。
简化调整后的 Podfile
代码如下:
require 'cocoapods-core/podfile/dsl'
require 'cocoapods-core/podfile/target_definition'
module Pod
class Podfile
include Pod::Podfile::DSL
# podfile 路径
attr_accessor :defined_in_file
# 所有的 TargetDefinition 的根节点, 正常只有一个,即 Pods.project target
attr_accessor :root_target_definitions
# 记录 Pods.project 项目的配置信息
attr_accessor :internal_hash
# 当前 DSL 解析使用的 TargetDefinition
attr_accessor :current_target_definition
# ...
end
end
直接看 dsl.rb
,该文件内部定义了 Podfile DSL
支持的所有方法。通过 include
的使用将 Pod::Podfile::DSL
模块 Mix-in
后插入到 Podfile
类中。
Lockfile
,顾名思义是用于记录最后一次 CocoaPods
所安装的Pod
依赖库版本的信息快照。也就是生成的 Podfile.lock
。
在 pod install
过程,Podfile
会结合它来确认最终所安装的 Pod
版本,固定 Pod
依赖库版本防止其自动更新。Lockfile
也作为 Pods
状态清单(mainfest
),用于记录安装过程的中哪些 Pod 需要被删除或安装或更新等。
Lockfile
可获取的信息:
pod install
命令执行后的 verify_podfile_exists!
中读取podfile:
def verify_podfile_exists!
unless config.podfile
raise Informative, "No `Podfile' found in the project directory."
end
end
Podfile
文件的读取就是config.podfile
里触发的,代码在 CocoaPods
的 config.rb
文件中:
def podfile_path_in_dir(dir)
PODFILE_NAMES.each do |filename|
candidate = dir + filename
if candidate.file?
return candidate
end
end
nil
end
def podfile_path
@podfile_path ||= podfile_path_in_dir(installation_root)
end
def podfile
@podfile ||= Podfile.from_file(podfile_path) if podfile_path
end
最后 Core 里的podfile.rb
的from_file
函数将依据目录下的 Podfile
文件类型选择调用 from_yaml
或者 from_ruby
:
def self.from_file(path)
path = Pathname.new(path)
unless path.exist?
raise Informative, "No Podfile exists at path `#{path}`."
end
case path.extname
when '', '.podfile', '.rb'
Podfile.from_ruby(path)
when '.yaml'
Podfile.from_yaml(path)
else
raise Informative, "Unsupported Podfile format `#{path}`."
end
end
读取到文件之后我们需要对Podfile内容进行解析,这里主要看Core
下面的podfile.rb
文件下的from_ruby
函数:
# podfile.rb
def self.from_ruby(path, contents = nil)
contents ||= File.open(path, 'r:utf-8', &:read)
...
# 初始化生成Podfile对象,eval执行自己定义的dsl语句
podfile = Podfile.new(path) do
# rubocop:disable Lint/RescueException
begin
# rubocop:disable Security/Eval
eval(contents, nil, path.to_s)
# rubocop:enable Security/Eval
rescue Exception => e
message = "Invalid `#{path.basename}` file: #{e.message}"
raise DSLError.new(message, path, e, contents)
end
# rubocop:enable Lint/RescueException
end
podfile
end
其中核心的一段代码是eval(contents, nil, path.to_s)
,它将Podfile
中的文本内容转化为方法执行,也就是说里面的参数是一段Ruby
的代码字符串,通过eval
方法可以直接执行。
YAML
格式的 Podfile
加载需要借助 YAMLHelper
类来完成,YAMLHelper
则是基于 yaml 的简单封装。
# podfile.rb
def self.from_yaml(path)
string = File.open(path, 'r:utf-8', &:read)
# 为了解决 Rubinius incomplete encoding in 1.9 mode
# https://github.com/rubinius/rubinius/issues/1539
if string.respond_to?(:encoding) && string.encoding.name != 'UTF-8'
string.encode!('UTF-8')
end
hash = YAMLHelper.load_string(string)
from_hash(hash, path)
end
def self.from_hash(hash, path = nil)
internal_hash = hash.dup
target_definitions = internal_hash.delete('target_definitions') || []
podfile = Podfile.new(path, internal_hash)
target_definitions.each do |definition_hash|
definition = TargetDefinition.from_hash(definition_hash, podfile)
podfile.root_target_definitions << definition
end
podfile
end
通过from_yaml
将文件内容转成Ruby hash
后转入from_hash
方法。
区别于from_ruby
,这里调用的initialize
将读取的hash
直接存入internal_hash
,然后利用TargetDefinition.from_hash
来完成的hash
内容到targets
的转换,因此,这里无需传入block
进行DSL
解析和方法转换。
Podfile
的initialize
方法:
def initialize(defined_in_file = nil, internal_hash = {}, &block)
self.defined_in_file = defined_in_file
@internal_hash = internal_hash
if block
default_target_def = TargetDefinition.new('Pods', self)
default_target_def.abstract = true
@root_target_definitions = [default_target_def]
@current_target_definition = default_target_def
instance_eval(&block)
else
@root_target_definitions = []
end
end
它定义了三个参数:
Podfile
文件路径yaml
序列化得到的 Podfile
配置信息,保存在internal_hash
中(Podfile From YAML
使用)Podfile
的 DSL
配置(Podfile From Ruby
使用)当 block
存在,会初始化名为Pods
的TargetDefinition
对象,用于保存Pods project
的相关信息和 Pod 依赖。然后调用instance_eval
执行传入的block
,将 Podfile
的 DSL
内容转换成对应的方法和参数,最终将参数存入 internal_hash
和对应的 target_definitions
中。
Podfile
的内容最终保存在 internal_hash
和 target_definitions
中,本质上都是使用了 hash
来保存数据。由于 YAML 文件格式的 Podfile 加载后就是 hash 对象,无需过多加工。唯一需要处理的是递归调用 TargetDefinition
的 from_hash
方法来解析 target
子节点的数据。
因此,接下来的内容解析主要针对 Ruby 文件格式的 DSL 解析,我们以 pod 方法为例:
target 'Example' do
pod 'Alamofire'
end
当解析到 pod 'Alamofire'
时,会先通过 eval(contents, nil, path.to_s)
将其转换为 dsl.rb
中的方法:
def pod(name = nil, *requirements)
unless name
raise StandardError, 'A dependency requires a name.'
end
current_target_definition.store_pod(name, *requirements)
end
name 为 Alamofire
,由于我们没有指定对应的 Alamofire
版本,默认会使用最新版本。requirements
是控制 该 pod 来源获取或者 pod target
的编译选项等,例如:
pod 'Alamofire', '0.9'
pod 'Alamofire', :modular_headers => true
pod 'Alamofire', :configurations => ['Debug', 'Beta']
pod 'Alamofire', :source => 'https://github.com/CocoaPods/Specs.git'
pod 'Alamofire', :subspecs => ['Attribute', 'QuerySet']
pod 'Alamofire', :testspecs => ['UnitTests', 'SomeOtherTests']
pod 'Alamofire', :path => '~/Documents/AFNetworking'
pod 'Alamofire', :podspec => 'https://example.com/Alamofire.podspec'
pod 'Alamofire', :git => 'https://github.com/looseyi/Alamofire.git', :tag => '0.7.0'
对 name 进行校验后,直接转入 current_target_definition
毕竟 Pod 库都是存在 Pods.project
之下:
# target_definition.rb
def store_pod(name, *requirements)
return if parse_subspecs(name, requirements) # This parse method must be called first
parse_inhibit_warnings(name, requirements)
parse_modular_headers(name, requirements)
parse_configuration_whitelist(name, requirements)
parse_project_name(name, requirements)
if requirements && !requirements.empty?
pod = { name => requirements }
else
pod = name
end
get_hash_value('dependencies', []) << pod
nil
end
def get_hash_value(key, base_value = nil)
unless HASH_KEYS.include?(key)
raise StandardError, "Unsupported hash key `#{key}`"
end
internal_hash[key] = base_value if internal_hash[key].nil?
internal_hash[key]
end
def set_hash_value(key, value)
unless HASH_KEYS.include?(key)
raise StandardError, "Unsupported hash key `#{key}`"
end
internal_hash[key] = value
end
经过一系列检查之后,调用 get_hash_value
获取 internal_hash
的 dependencies
,并将 name
和 requirements
选项存入。
我们能看到当前的current_target_definition
的internal_hash['dependencies']
存的就是pod podname
声明的所有依赖,是一个数组,数组里面的值如果有requirements
则是Hash
,没有则是pod名的字符串:
整个映射过程如下:
# source.rb
def specification(name, version)
Specification.from_file(specification_path(name, version))
end
# 根据name和version读取到podspec的路径
def specification_path(name, version)
raise ArgumentError, 'No name' unless name
raise ArgumentError, 'No version' unless version
path = pod_path(name) + version.to_s
specification_path = path + "#{name}.podspec.json"
unless specification_path.exist?
specification_path = path + "#{name}.podspec"
end
unless specification_path.exist?
raise StandardError, "Unable to find the specification #{name} " \
"(#{version}) in the #{self.name} source."
end
specification_path
end
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
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
# 直接通过eval执行podspec里的ruby语句,返回Specifiction对象
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)
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
以AFNetworking
为例,.podspec
文件就是简单直接地声明了一个Specifiction
对象,然后通过 block
块定制来完成配置:
Pod::Spec.new do |s|
s.name = 'AFNetworking'
s.version = '2.7.2'
s.license = 'MIT'
s.summary = 'A delightful iOS and OS X networking framework.'
s.homepage = 'https://github.com/AFNetworking/AFNetworking'
.
.
.
类似于podfile
的内容保存在internal_hash
的hash
里,像podspec
的内容name
、source_files
这些配置参数最终都会转换为方法调用并将值存入attributes_hash
的hash
中。
这些方法调用的实现方式分两种:
attribute
和 root_attribute
来动态添加的 setter
方法;subspec
、dependency
方法等
# lib/cocoapods-core/specification/dsl.rb
module Pod
class Specification
module DSL
extend Pod::Specification::DSL::AttributeSupport
# Deprecations must be required after include AttributeSupport
require 'cocoapods-core/specification/dsl/deprecations'
attribute :name,
:required => true,
:inherited => false,
:multi_platform => false
root_attribute :version,
:required => true
# ...
end
end
end
可以看出 name
和 version
的方法声明与普通的不太一样,其实 attribute
和 root_attribute
是通过 Ruby 的方法包装器来实现的,这些装饰器所声明的方法会在其模块被加载时动态生成,来看其实现:
# lib/cocoapods-core/specification/attribute_support.rb
module Pod
class Specification
module DSL
class << self
attr_reader :attributes
end
module AttributeSupport
def root_attribute(name, options = {})
options[:root_only] = true
options[:multi_platform] = false
store_attribute(name, options)
end
def attribute(name, options = {})
store_attribute(name, options)
end
def store_attribute(name, options)
attr = Attribute.new(name, options)
@attributes ||= {}
@attributes[name] = attr
end
end
end
end
end
attribute
和 root_attribute
最终都走到了 store_attribute
保存在创建的 Attribute
对象内,并以配置的 Symbol
名称作为 KEY 存入 @attributes
,用于生成最终的 attributes setter
方法。
最关键的一步,让我们回到 specification
文件:
# /lib/coocapods-core/specification
module Pod
class Specification
# ...
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
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
end
end
Specification
类被加载时,会先遍历 DSL module
加载后所保存的 attributes
,再通过 define_method
动态生成对应的配置方法。最终数据还是保存在 attributes_hash
中。
source
、static_framework
、module_name
等;source_files
除了 attribute
装饰器声明的 setter
方法,还有几个自定义的方法是直接通过 eval
调用的:
# lib/cocoapods-core/specification/dsl.rb
# 三种不同类型的 Subspec 经 eval 转换为对应的 Specification 对象,注意这里初始化后都将 parent 节点指向 self 同时存入 @subspecs 数组中,完成 SubSpec 依赖链的构造。
def subspec(name, &block)
subspec = Specification.new(self, name, &block)
@subspecs << subspec
subspec
end
def test_spec(name = 'Tests', &block)
subspec = Specification.new(self, name, true, &block)
@subspecs << subspec
subspec
end
def app_spec(name = 'App', &block)
appspec = Specification.new(self, name, :app_specification => true, &block)
@subspecs << appspec
appspec
end
# 对于其他 pod 依赖的添加我们通过 dependency 方法来实现
def dependency(*args)
name, *version_requirements = args
# dependency args 有效性校验 ...
attributes_hash['dependencies'] ||= {}
attributes_hash['dependencies'][name] = version_requirements
unless whitelisted_configurations.nil?
# configuration 白名单过滤和校验 ...
attributes_hash['configuration_pod_whitelist'] ||= {}
attributes_hash['configuration_pod_whitelist'][name] = whitelisted_configurations
end
end
文件几乎完全一样摘抄自Podfile 的解析逻辑,这篇文章条理已经很清晰,本人只是为了个人查阅方便记录一下。