ruby实现podfile依赖SDK解析

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 可以看成大括号, :Meitutarget 方法的第一个参数。综合之前介绍的语法,target 的定义就呼之欲出了:

# tname: target 名称
# block: 回调
def target(tname, &block)
  # ...
  # 调用
  yield if block_given?
end

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

解析

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

ruby ~/Desktop/Podfile

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

定义方法

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

除了 targetpod 方法外,Podfile 中还存在 sourceplatform 等各种各样的方法,一一实现是不可能的。对此,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 的 Stringstatic 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项目idfile_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.shsource.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文件的内容就可以了。

参考
ruby
sh
gitlab api get file
gitlab api finder

jq工具

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 156,069评论 4 358
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,212评论 1 287
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 105,912评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,424评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,741评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,194评论 1 206
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,553评论 2 307
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,289评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,923评论 1 237
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,251评论 2 240
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,775评论 1 255
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,144评论 2 249
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,698评论 3 228
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,936评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,658评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,214评论 2 267
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,159评论 2 258

推荐阅读更多精彩内容