iOS依赖的SDK一般都是通过Cocoapod来管理的,我们一般会把依赖的SDK的信息描述在Podfile文件中,比如SDK的name、branch、version等,但是podfile中的代码都是ruby代码,我们如何将依赖的SDK解析成json格式的文件呢?比如下面podfile中的依赖
pod 'AFNetWorking','1.1.26'
或者
pod 'Reachability', :git => 'xxx.git', :tag => 'v3.2.1', :branch=> 'dev'
我们要把他解析成
{
"AFNetWorking": {
"comes_from": "",
"version": "1.1.26"
},
"Reachability": {
"comes_from": "tag",
"version": "v3.2.1",
},
}
下面我们就来实现这个解析过程。
基本语法
首先需要做的是,看懂一个 Podfile。那么需要了解一些最基本的 ruby 语法,这部分非常简单:
source 'https://github.com/CocoaPods/Specs.git'
platform :ios, '8.0'
pod 'FLEX', :configurations => ['Debug'], :branch => 'develop'
use_frameworks!
以上三行代码是 Podfile 中最为常见的,其实这三行是在调用不同的方法。
方法调用
Ruby 中,方法调用的参数列表可以以空格形式接在方法名后,多个参数以逗号隔开,所以等价于:
source 'https://github.com/CocoaPods/Specs.git'
# =>
source('https://github.com/CocoaPods/Specs.git')
platform :ios, '8.0'
# =>
platform(:ios, '8.0')
如果是最后一个参数是字典,那么字典的大括号也可以省略,所以 pod 的调用等价于:
pod 'pop', 1.0.7 :configurations => ['Debug'], :branch => 'develop'
#=>
map = {
:configurations => ['Debug'],
:branch => 'develop'
}
pod('pop',1.0.7, 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 方法举栗子:
pod 'Masonry'
pod 'pop', '~> 1.0.7'
pod 'Reachability', :git => 'xxx.git', :tag => 'v3.2.1'
常见的 pod
调用如上,通过调用就能猜出 pod
方法的声明:
# pname: 库名
# version: 指定版本,且可空
# map: 用键值对接收其他参数
def pod(pod_name, version = nil, **map)
# ...
end
大致就是这样,这里的 *
和指针没关系 :new_moon_with_face:。完整参数列表的定义方式,我写在文末吧。
返回值
其实解析这部分用不上返回值,不过可以介绍一下。Ruby 返回值有以下几个特点:
如果是最后一行,可以不写 return 。
支持多个返回值。
代码块(Block)
这个和 Objective-C 差不多,常用于回调。当然 Podfile 也不缺少:
target :Meitu do
pod 'Masonry'
end
do...end
可以看成大括号, :Meitu
是 target
方法的第一个参数。综合之前介绍的语法,target 的定义就呼之欲出了:
# tname: target 名称
# block: 回调
def target(tname, &block)
# ...
# 调用
yield if block_given?
end
语法到这里就基本够用了,接着介绍如何解析。
解析
既然 Podfile 中是 Ruby 代码,也就表示,可以通过调用 Ruby 脚本的方式,直接执行 Podfile。
ruby ~/Desktop/Podfile
然后就报错了…(编译器又不知道 source
, pod
这都是些什么方法…
定义方法
首先需要定义解析需要调用的方法,让指定的变量乖乖的被对应参数接收。最简易的版本,需要实现 target
和 pod
两个方法:
target
工程可能对应多个 target,具体要解析哪个 target,需要对应到打包时指定的 target,所以采用外部传入的方式: $target_argv
:
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
参数的:
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
除了 target
和 pod
方法外,Podfile 中还存在 source
、 platform
等各种各样的方法,一一实现是不可能的。对此,Ruby 提供了 method_missing
方法,该方法的作用类似于消息转发。当程序调用没有实现的方法时,统一走 method_missing
。
# m: 方法名
# args: 位置参数(也就是数组)
def method_missing(m, *args); end
导出
到此,整个解析就已经完成了,比起以前用正则写的版本,清爽了很多。最后一步,将解析结果导出为 JSON 文件。代码很简单:
File.open($result_path, "w") do |f|
f.write(JSON.pretty_generate($map))
end
Podfile.lock
这里简单提一下 lock 文件,因为 lock 文件中有准确的版本号,所以对应引用版本都从 lock 当中读取。而 lock 文件其实是 yaml 格式的,可以通过 yaml
库将它解析为 hash 和 array 进行读取。
整个流程
那么,应该如何将解析和导出两个步骤串起来呢?方法需要定义在代码开头,导出需要放在代码末尾,所以有了以下结构:
# 定义
#INJECT_PODFILE#
# 导出
然后整个文件其实就是一个模板, inject_template.rb ,在解析之前,将 #INJECT_PODFILE# 替换为 Podfile 的内容,最后是调用和传参:
ruby inject_template.rb target_name configuration_name result_path
其他
Symbol
symbol是 Ruby 中最为基础的对象类型,存储在 Symbol Table 中,可以看做 name 和 ID 的对应。Symbol 不可写,地址不变,全局唯一。这和 String 不同,两个值相同的 String,其实是不同的地址。
"some_string".object_id == "some_string".object_id #=> false
:some_string.object_id == :some_string.object_id #=> true
类似于 Java 的 String
和 static String
,一个是用完重新分配,一个是始终是一个存储单元。针对于这个特性,Symbol 的效率会比 String 高一些。常用于成员变量名,hash 的 key 等。
后记 2019-11-18
上面的整个流程部分还有些问题,比如#INJECT_PODFILE
如何替换为Pofile中的内容呢, 其实对于我们来说# 定义
和#导出
的一般是不会变化的,而Podfile中的内容可能随时都会变动,我们不可能每次都要手动把Podfile中的内容替换#INJECT_PODFILE
,这种重复性的工作交给脚本来实现就行了,这里我们定义一个source.rb文件,该文件中只有# 定义
和#导出
的代码,我们在shell脚本中执行一个copy指令,每次会把source.rb copy一份到destination.rb中,然后将Podfile中的内容插入到destination.rb# 定义
和#导出
中间就可以了。
source.rb代码实例
#!/usr/bin/ruby
require 'json'
$map = Hash.new
def target(target_name = nil, &block)
# target name 可能是 String,可能是 Symbol,统一 to_s 一下
# 如果不是当前打包的 target,直接返回就行了
# return if target_name.to_s != $target_argv
puts "当前的target和变量保持一致!"
# 调用 block
yield if block_given?
end
def pod(pod_name, version = nil, **args)
git = args[:git]
branch = args[:branch]
tag = args[:tag]
commit = args[:commit]
configureations = args[:configureations]
comes_from = "tag"
$map[pod_name] = {
comes_from: comes_from,
version: version
}
end
def method_missing(m, *args); end
File.open("rubyJson.json", "w+") do |f|
f.write(JSON.pretty_generate($map))
end
整合Podfile脚本示例,Integrate.sh
#!/bin/bash
cp -f source.rb destination.rb
sed -i "" '/method_missing/r Podfile' destination.rb
ruby destination.rb
后记2019-11-19
本地工程里的Podfile我们已经解析出来了,但是如果我们想要和gitlab工程中的某一个版本的Podfile做对比,那么我们也要解析出gitlab中某个版本的Podfile,主要有以下步骤:
1、通过gitlab提供的api获取访问工程里的Podfile文件内容
2、将podfile中的依赖版本信息转换成json格式并保留在本地temp文件中。
3、执行比对脚本,列出不一样的依赖版本信息。
对于第一步,我们可以根据gitlab api中的get-file-from-repository说明,可以知道,如果要访问工程文件,需要:access_token
、项目id
、file_path
这三个参数
1> access_token
获取:先我们可以在userSetting ---> AccessToken--->Add a personal access token,添加一个access_token,注意添加accessToken时scope要选择api
,否则会报访问权限错误。生成的token要copy一份,否则添加以后就看不到了。
2> 项目id
获取:setting
—>General
3> file_path
获取:Repository
--->Files
---> 键盘点击t --->项目后的输入框中中输入的路径才是file_path
我们新建一个request.sh
,获取这三个参数后我们就可以用curl
来请求获取Podfile内容了
curl --request GET --header 'PRIVATE-TOKEN: <your_access_token>' 'https://gitlab.example.com/api/v4/projects/13083/repository/files/app%2Fmodels%2Fkey%2Erb?ref=master'
但是我们发现,请求返回的是内容是
{
"file_name": "key.rb",
"file_path": "app/models/key.rb",
"size": 1476,
"encoding": "base64",
"content": "IyA9PSBTY2hlbWEgSW5mb3...",
"content_sha256": "4c294617b60715c1d218e61164a3abd4808a4284cbc30e6728a01ad9aada4481",
"ref": "master",
"blob_id": "79f7bbd25901e8334750839545a9bd021f0e4c83",
"commit_id": "d5a3ff139356ce33e37e73add446f16869741b50",
"last_commit_id": "570e7b2abdd848b95f2f578043fc23bd6f6fd24d"
}
其中content
是podfile中真正的内容,但是他被base64
编码了,所以我们需要解析返回的json中的content
字段,并用base64
decode, 通常sh中我们用
jq
来解析json
,通过jq -r .content
我们就可以解析处理content字段的内容,注意这里的-r
可以去掉content字段中的引号
。
通过jq -r .content > temp
我们可以将content字段中的内容保存到本地temp中,但是temp中的是base64 encode的内容,所以我们要decode ,并将decode的内容保存到新的文件,base64 -D temp > remotePodfile
,这样我们就将远程的Podfile,读取到本地了。接下来就是和前面的步骤一样,通过ruby将remotePodfile中的内容解析成json文件。
对于第二步,我们已经获取到了remote的Podfile,那么可以通过上面的Integrate.sh
脚本来实现remotePodfile
的解析,但是我们发现Integrate.sh
和source.rb
中的对解析的Podfile源文件和解析后生成的json文件路径都是写死的,那么需要我们通过参数做进一步区分,如果是解析的是remotePodfile
,那么Integrate.sh
中的
sed -i "" "/method_missing/r Podfile" destination.rb
应改为
sed -i "" "/method_missing/r remotePodfile" destination.rb
source.rb
中的
File.open("rubyJson.json", "w+") do |f|
f.write(JSON.pretty_generate($map))
end
应改为
File.open("remote.json", "w+") do |f|
f.write(JSON.pretty_generate($map))
end
所以我们只需要给这两个文件设置一个参数控制文件的读取和输出路径就可以了。可以通过环境变量来来实现参数的传递,ruby中的环境变量都是通过ENV
对象管理的,我们定义一个REMOTE
的环境变量,如果是REMOTE
存在那么我问就将json的输出路径改为
remote.json
,否则还是之前的rubyJson.json
;在Integrate.sh
中,我们可以通过$1
,$2
...接收传递的参数,我们将Integrate.sh
接收的第一个参数定义为源文件的名称,如果设置了源文件名称那么我们就解析同目录下的源文件,如果没有设置,默认读取的是目录下Podfile
文件。但Integrate.sh
会执行destination.rb
文件,我们要根据$1
的值,判断是否向destination.rb
传递环境变量,如果我们传入的$1为remotePodfile
,那么Integrate.sh
中执行ruby时应该是REMOTE=1 ruby destination.rb -e "ENV['REMOTE']"
第三步: 只要不叫两个json文件的内容就可以了。
网友评论