Podfile 解析最佳实践

需求源于测试爸爸要知道项目当前都引入了哪些库,库的作用、版本、引用方式等信息。所以,最简单的方式就是解析 PodfilePodfile.lock。期初,是通过正则匹配 pod 字段来实现,但是不用说也知道这种方式并不好,可读性差、不易于维护、不易于扩展。所以,优化了一下 🤪。


Cocoapods 在实现的时候非常取巧,Podfile 作为平时配置依赖的文件,其实是 Ruby 代码。采用的解析 Podfile 的方式,也是从 Ruby 的角度出发的。

优劣势

首先来对比一下优劣势,其实根本不用对比,百利而无一害。直接说优势吧~

  • 摆脱正则繁琐的语法,可以直接通过 Ruby 对象读取参数。
  • 像多个 target、多个 configuration 这种情况,正则真的很恶心。
  • 正则无法处理程序逻辑。也就是说,无法判断某个方法的调用,只能匹配全部 pod
  • 一定要说缺点的话,也有一个。这种解析方式对 Podfile 语法要求较高,也就是不能出现语法错误。当然,如果出现语法错误,pod install 这关也是过不了的,所以也还好。

基本语法

首先需要做的是,看懂一个 Podfile。那么需要了解一些最基本的 ruby 语法,这部分非常简单:

1
2
3
4
source 'https://github.com/CocoaPods/Specs.git'
platform :ios, '8.0'
pod 'FLEX', :configurations => ['Debug'], :branch => 'develop'
use_frameworks!

以上三行代码是 Podfile 中最为常见的,其实这三行是在调用不同的方法。

方法调用

Ruby 中,方法调用的参数列表可以以空格形式接在方法名后,多个参数以逗号隔开,所以等价于:

1
2
3
4
5
6
7
source 'https://github.com/CocoaPods/Specs.git'
# =>
source('https://github.com/CocoaPods/Specs.git')

platform :ios, '8.0'
# =>
platform(:ios, '8.0')

如果是最后一个参数是字典,那么字典的大括号也可以省略,所以 pod 的调用等价于:

1
2
3
4
5
6
7
pod 'FLEX', :configurations => ['Debug'], :branch => 'develop'
#=>
map = {
:configurations => ['Debug'],
:branch => 'develop'
}
pod('FLEX', map)

符号(Symbol)

Symbol 是 Ruby 中的一种对象类型,一般作为名称标签,为了不影响阅读,我把 Symbol 的定义放在最后,这里可以暂且把它当做前面加了 : 的 string。

所以,上面的代码中,出现的 :ios:configuration:branch 以及常见的 :git:tag 等都是 Symbol

方法定义

Ruby 的方法定义更加灵活,语义也更加丰富。

方法名

比如 nil?empty?merge! 这类方法。

方法名小写,可包含 !? 这类符号。用法可以学习系统的定义:

  • ? 常用于判断,取代了 is_ 开头的定义习惯。
  • ! 常用于需要注意的方法,比如 arr.merge!(other_arr) 表示合并到 arr;与之对应的是 arr.merge(other_arr),表示合并,但不修改 arr,而是返回合并后的结果。

在很多开源库中,! 的用法就比较巧妙,有可能并不表示在当前对象上进行修改,仅仅为了优雅好看也是可能的。

所以,Podfile 中出现的 use_frameworks! 也是在调用方法。

参数列表

为了简单,这里仅介绍可空的参数定义。还是以 pod 方法举栗子:

1
2
3
pod 'Masonry'
pod 'pop', '~> 1.0.7'
pod 'Reachability', :git => 'xxx.git', :tag => 'v3.2.1'

常见的 pod 调用如上,通过调用就能猜出 pod 方法的声明:

1
2
3
4
5
6
# pname: 库名
# version: 指定版本,且可空
# map: 用键值对接收其他参数
def pod(pod_name, version = nil, **map)
# ...
end

大致就是这样,这里的 * 和指针没关系 🌚。完整参数列表的定义方式,我写在文末吧。

返回值

其实解析这部分用不上返回值,不过可以介绍一下。Ruby 返回值有以下几个特点:

  • 如果是最后一行,可以不写 return
  • 支持多个返回值。

代码块(Block)

这个和 Objective-C 差不多,常用于回调。当然 Podfile 也不缺少:

1
2
3
target :Meitu do
pod 'Masonry'
end

do...end 可以看成大括号,:Meitutarget 方法的第一个参数。综合之前介绍的语法,target 的定义就呼之欲出了:

1
2
3
4
5
6
7
# tname: target 名称
# block: 回调
def target(tname, &block)
# ...
# 调用
yield if block_given?
end

语法到这里就基本够用了,接着介绍如何解析。

解析

既然 Podfile 中是 Ruby 代码,也就表示,可以通过调用 Ruby 脚本的方式,直接执行 Podfile。

1
ruby ~/Desktop/Podfile

然后就报错了…(编译器又不知道 sourcepod 这都是些什么方法…

定义方法

首先需要定义解析需要调用的方法,让指定的变量乖乖的被对应参数接收。最简易的版本,需要实现 targetpod 两个方法:

target

工程可能对应多个 target,具体要解析哪个 target,需要对应到打包时指定的 target,所以采用外部传入的方式:$target_argv

1
2
3
4
5
6
7
def target(target_name = nil, &block = nil)
# target name 可能是 String,可能是 Symbol,统一 to_s 一下
# 如果不是当前打包的 target,直接返回就行了
return if target_name.to_s != $target_argv
# 调用 block
yield if block_given?
end

pod

实现 pod 以后,就可以通过参数读取这种值了。同样,pod 可能包含 configuration 信息,这也是需要对应打包的 configuration 参数的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def pod(pod_name, version = nil, **args)
git = args[:git]
branch = args[:branch]
tag = args[:tag]
commit = args[:commit]
configurations = args[:configurations]

# 如果 pod 指定了 configuration,则判断是否包含当前 configuration
unless configurations.nil?
return unless configurations.include?($configuration)
end

# 通过哪种方式引用,这里可以通过 tag、commit、branch 的 nil? 来判断来源
comes_from = "tag"

# $map 为全局变量
$map[pod_name] = {
comes_from: comes_from,
version: version
}
end

method_missing

除了 targetpod 方法外,Podfile 中还存在 sourceplatform 等各种各样的方法,一一实现是不可能的。对此,Ruby 提供了 method_missing 方法,该方法的作用类似于消息转发。当程序调用没有实现的方法时,统一走 method_missing

1
2
3
# m: 方法名
# args: 位置参数(也就是数组)
def method_missing(m, *args); end

因为我们不需要处理未实现方法的逻辑,所以方法体是空的,不需要实现。

导出

到此,整个解析就已经完成了,比起以前用正则写的版本,清爽了很多。最后一步,将解析结果导出为 JSON 文件。代码很简单:

1
2
3
File.open($result_path, "w") do |f|
f.write(JSON.pretty_generate($map))
end

Podfile.lock

这里简单提一下 lock 文件,因为 lock 文件中有准确的版本号,所以对应引用版本都从 lock 当中读取。而 lock 文件其实是 yaml 格式的,可以通过 yaml 库将它解析为 hash 和 array 进行读取。

整个流程

那么,应该如何将解析和导出两个步骤串起来呢?方法需要定义在代码开头,导出需要放在代码末尾,所以有了以下结构:

1
2
3
4
5
# 定义

eval(File.read(podfile_path))

# 导出

这里用到了非常强大的 eval 函数,也就是将 podfile 内容读取为将字符串并当做代码执行。

1
ruby inject_template.rb target_name configuration_name result_path podfile_path

🚣‍ 🚣 🚣‍ 🚣‍

最后

Symbol

Symbol 是 Ruby 中最为基础的对象类型,存储在 Symbol Table 中,可以看做 name 和 ID 的对应。Symbol 不可写,地址不变,全局唯一。这和 String 不同,两个值相同的 String,其实是不同的地址。

1
2
"some_string".object_id == "some_string".object_id #=> false
:some_string.object_id == :some_string.object_id #=> true

类似于 Java 的 Stringstatic String,一个是用完重新分配,一个是始终是一个存储单元。针对于这个特性,Symbol 的效率会比 String 高一些。常用于成员变量名,hash 的 key 等。

如果想继续了解 Symbol,推荐阅读:

方法定义

1
2
3
def foo(a, b="b_default", *c, d:, e: "e_default", **f, &g)
# do stuff
end
  • a:Positional argument,位置参数。也就是说,声明在第一个,第一个传入的就一定是 a 接收。
  • b:和 a 含义相同,且有默认值。
  • c:连续多个 Positional argument。
  • d:Keyword argument,对位置没要求,接收指定 keyword 对应的值。
  • e:和 d 含义相同,且有默认值。
  • f:连续多个 Keyword argument。
  • g:block。