记录一次 Cocoapods Plugins 插件开发过程

6,764

背景

我们公司主要以项目为主,做项目的过程中免不了需要集成第三方的 SDK,例如人脸识别、即时通讯等,第三方的 SDK 往往比较大,公司为节省 SVN 硬盘资源,不允许 SVN 提交超过 50 MB 的文件,然而这些 SDK 可能会有100MB+ 左右,这不利于管理第三方的 SDK,我们将 SDK 存放在 FTP 服务器上,这也对前线项目开发人员造成难题,无法一次性通过 pod install 成功后运行项目,需要先执行 pod install,再前往 FTP 服务器下载第三方 SDK ,再将 SDK 移动至对应的组件目录,再次执行 pod install,工程才能正常运行,这无非增加项目开发的成本,这也是本次调研 cocoapods 插件开发的原因。

流程

cocoapods 插件开发,允许开发者可以再 pod install 的过程中 hook 其中一个生命周期,对其进行操作,这正是我想要的,那接下来我需要学习以下技术:

  • cocoapods 插件开发流程
  • ruby 语法
  • ruby 文件操作
  • ruby FTP 操作

首先我需要学习下 cocoapods 插件的开发流程

通过百度可以搜索到网上大部分的文章内容,写的很全。

www.jianshu.com/p/5889b25a8…

第1步:安装 cocoapods-plugins 组件

gem install cocoapods-plugins

第2步:创建 Cocoapods Plugins 模版工程

pod plugins create githooks

执行完上述操作后你会得到一个 Cocoapods Plugins 的工程目录,你可以选择 WebStorm、VSCode 来开发,在这里我使用 VSCode,安装 Ruby 插件后,开发比较流畅。

2

第3步:修改 cocoapods-githooks.gemspec 文件

# 修改这行代码
spec.files         = 'git ls-files'.split($/)

# 修改为
spec.files = Dir['lib/**/*']

第4步:编译插件

sudo gem build cocoapods-githooks.gemspec 显示以下信息,编译成功

WARNING:  no author specified
WARNING:  open-ended dependency on rake (>= 0, development) is not recommended
  if rake is semantically versioned, use:
    add_development_dependency 'rake', '~> 0'
WARNING:  See http://guides.rubygems.org/specification-reference/ for help
  Successfully built RubyGem
  Name: cocoapods-githooks
  Version: 0.0.1
  File: cocoapods-githooks-0.0.1.gem

编译成功后,会得到 cocoapods-githooks-0.0.1.gem 文件

第5步:插件安装

sudo gem install cocoapods-githooks-0.0.1.gem

显示以下信息,安装成功

zhudezzhendeMacBook-Pro:cocoapods-githooks zhudezhen$ sudo gem install cocoapods-githooks-0.0.1.gem 
Successfully installed cocoapods-githooks-0.0.1
Parsing documentation for cocoapods-githooks-0.0.1
Installing ri documentation for cocoapods-githooks-0.0.1
Done installing documentation for cocoapods-githooks after 0 seconds
1 gem installed

你可以通过以下命令,检验插件是否安装成功

pod plugins installed

在列表中找到你的插件,说明安装成功了

zhudezzhendeMacBook-Pro:TestWKWebView zhudezhen$ pod plugins installed
[!] The specification of arguments as a string has been deprecated Pod::Command::Vendors: `NAME`
[!] The specification of arguments as a string has been deprecated Pod::Command::Githooks: `NAME`

Installed CocoaPods Plugins:
    - cocoapods-clean       : 0.0.1
    - cocoapods-deintegrate : 1.0.4
    - cocoapods-ftp-vendors : 0.0.1 (pre_install hook)
    - cocoapods-githooks    : 0.0.1 (post_install hook)
    - cocoapods-plugins     : 1.0.0
    - cocoapods-search      : 1.0.0
    - cocoapods-stats       : 1.1.0 (post_install hook)
    - cocoapods-trunk       : 1.5.0
    - cocoapods-try         : 1.2.0

到此就掌握了 cocoapods 插件的开发流程,当然还没有对 pod install 进行 hook。

对 pod install 进行 hook

Ruby 语法的学习,基本都是从菜鸟教程上看的,这里不展开了

www.runoob.com/ruby/ruby-s…

第1步:改造 plugin 代码

对目录 ‘lib/cocoapods_plugins.rb’ 进行修改,修改内容如下:

require 'cocoapods-githooks/command'
# 引用 cocoapods 包
require 'cocoapods'

module CocoapodsGithooks
	  # 注册 pod install 钩子
    Pod::HooksManager.register('cocoapods-githooks', :post_install) do |context|
        p "hello world!"
    end
end

注意:module 的名称,来源于 cocoapods-githooks.gemspec 的 version 字段,钩子注册的名称必须和组件名称保持一致。

3

修改完毕后,重新编译安装,为了方便调试,每次修改内容你可以通过以下命令,重新执行

sudo gem uninstall cocoapods-githooks-0.0.1.gem && sudo gem build cocoapods-githooks.gemspec  && sudo gem install cocoapods-githooks-0.0.1.gem

第2步:更改项目 Podfile 文件

你需要在项目的 Podfile 文件的顶部,增加如下代码:

platform :ios, '9.3'

# 此处为新增代码
plugin 'cocoapods-githooks'

target ....

好了,我们执行 pod install 试一下

zhudezzhendeMacBook-Pro:TestWKWebView zhudezhen$ pod install
[!] The specification of arguments as a string has been deprecated Pod::Command::Vendors: `NAME`
[!] The specification of arguments as a string has been deprecated Pod::Command::Githooks: `NAME`
Analyzing dependencies
Downloading dependencies
Using Masonry (1.1.0)
Using arcface (1.2.0)
Generating Pods project
Integrating client project
"hello world!"
Sending stats
Pod installation complete! There is 1 dependency from the Podfile and 2 total pods installed.
zhudezzhendeMacBook-Pro:TestWKWebView zhudezhen$ 

我们看到了 "hello world!"。

Ruby FTP 连接服务器

Ruby 本身自带 FTP 模块,可以直接连接 FTP 服务器,这里需要注意,连接的如果是 Window 的 FTP 服务器的话,需要对参数进行 UTF-8 -> gb2312 的转换。

腾讯云文档:cloud.tencent.com/developer/s…

Ruby 连接 FTP 模块的代码如下:

require "net/ftp"
ftpclient = Net::FTP.new
ftpclient.connect(host, port)
ftpclient.passive = true
ftpclient.login(username, password)
p ftpclient.nlst

执行上述代码后,就可以成功链接到 FTP 服务器,如果连接的是 Window 服务器,目录存在中文的话,需要进行编码转换,DirectoryName.encode("utf-8", "gb2312"),当需要访问 FTP 指定目录时,如果是 Window 服务器,路径中存在中文的话,路径需要进行编码转换,"/我的组件库/组件1/组件2/abcd.framework".encode("gb2312", "utf-8")

可以通通过 system 获取 FTP 服务器信息

p ftpclient.system
# print Window_NT

通过 getbinaryfile 下载文件,路径问题同上。

ftpclient.getbinaryfile 文件路径

由于在 iOS 中,*.framework 静态库属于特殊的文件夹形式,因此我们需要递归下载静态库:

def self.d_f_p(ftp_key = '', path = '', rootpath = '')
  # 从连接池中,获取 FTP 连接客户端
  ftp = FtpClient.ftp_connect_pool[ftp_key]
  if !ftp 
  	return nil;
  end
  # 获取文件名
  subpaths = path.split("/")
  filename = subpaths[subpaths.size - 1];
  # 先创建目录
  subfiles = ftp.nlst(path)
  # 判断是否是文件夹、还是文件
  if  !((subfiles.size == 1) && (subfiles[0] == "#{path}/#{filename}"))
      # 如果是目录,则构建目录
  		buildDir = "#{rootpath}/#{filename}"
  		if !(File.directory? buildDir)
  			Dir.mkdir buildDir
  		end
  
  		# 获取子目录
  		subfiles = ftp.nlst path
      
      # 获取目录下子文件
  		for sf in subfiles do  
  			d_f_p(ftp_key, sf, buildDir + "/")
  		end 
  else 
    # 普通文件,直接下载
  	filesize = ftp.size path;
  	ftp.getbinaryfile(path, "#{rootpath}/#{filename}", filesize)
  end
end

通过上述代码,可以自动下载 framework 静态库。

串联整体流程

解决了以上问题后,我们开始串联整体流程,大致流程如下:

4

配置文件

FTP 配置信息,最开始有两种思路:

  • 配置在对应组件的 podspec 文件中
  • 配置在对应工程的 Podfile 文件中

我选择了后者,如果选择了前者,那将会对所有需要 FTP 的组件进行改造,而后者可以以插件的形式改造工程。

一开始我将 ftp 的信息放在 Podfile 对应组件的后面,如下:

platform :ios, '9.3'
plugin 'cocoapods-githooks'

target 'TestWKWebView' do
  
    pod 'sm', :path => 'xxxx/sm2', :ftp => { "host" => "xxx.xxx.xxx.xxx", "port" => "xxxxx", "user" => "xxxx", "pwd" => "xxxx", "vendors" => [{ "path" => "/xxxxxx/xxxx/1.0.1~1.0.11.a/xxxx.framework", "destination" => "sm2" ,"version" => "1.0.1" }]}

end

这样配置可以在 Cocoapods Plugin 中正常获取到,并且 pod install 也可以成功,但是换成 svn 之后,这样配置在 pod install 会失败,如下:

platform :ios, '9.3'
plugin 'cocoapods-githooks'

target 'TestWKWebView' do
  
    pod 'sm', :svn => 'svn:xxxx:xxx/xxx/xxxx/sm2', :tag => 'xxx.xx', :ftp => { "host" => "xxx.xxx.xxx.xxx", "port" => "xxxxx", "user" => "xxxx", "pwd" => "xxxx", "vendors" => [{ "path" => "/xxxxxx/xxxx/1.0.1~1.0.11.a/xxxx.framework", "destination" => "sm2" ,"version" => "1.0.1" }]}

end

这时候无论怎么操作,终究会在 pod install 最后发生失败,这让我不得不换个思路,索性采用配置文件的形式读取 FTP 信息,这样可以完全和 Podfile 隔离。

5

文件内容如下:

{
   "arcface":{
       "host":"xxx.xxx.xxx.xxx 【ftp 服务器地址】", 
       "port":"xxxxx 【ftp 端口号】",
       "user":"xxxx 【ftp 账号】",
       "pwd":"xxxx 【ftp 密码】",
        "vendors":[
           {
              "path":"/xxxx/xxxx/iOS/1.0.1/ArcSoftFaceEngine.framework 【组件 ftp 绝对路径】",
              "destination":"xxxx/xxx 【组件本地存放路径,相对于工程 Pods/】",
              "version":"1.0.1【组件版本号信息】"
          }
        ]
  },
}

通过配置文件读取,pod install 成功,并且插件中也获取到了配置信息。

Cocoapods Context 上下文

在 hook 的回调方法中,有一个 context 变量,很明显这是当前钩子的上下文,这时候需要理解上下文中的内容:

Cocoapoads API 在线文档:www.rubydoc.info/github/coco… 这一块内容,网上比较少,只能自己阅读文档来学习。

通过阅读 API,我们可以在 context 中获取工程的 pods_project、sandbox、sandbox_root、umbrella_targets。同时还了解,除了 :post_install Hook、还有 :pre_install、:post_integrate、:source_provider 可以 hook,其中 :post_integrate 只有 cocoapods 1.10.0+ 以上版本支持,由于目前公司的特性,我们只能维持 cocoapods 1.4.0 版本,这带来了组件移动完毕后,工程合成的问题。每一个 hook,都有自己的 context,context 中携带的信息不一样,这里需要注意。目前为止,可能还是理解 context 中携带的数据,所以我去搜了一波“Ruby 反射”,发现在 Runtime 阶段,调用 context.methods 可以获取当前 Ruby 对象的所有方法,这对整个流程带来了关键性的进展。

工程合成问题

流程串联完毕后,很开心,测试了,第一个试验性大型项目工程,FTP 从下载、组件移动、pod install 一切顺利,就在运行项目的时候,发现编译失败,原因是第三方静态库没有被引用,很奇怪,我不是已经组件移动到指定目录了吗?经过分析发现,pod install 其实是有流程的,我从 github 上下载 cocoapods 1.4.0 的源码后发现,post_install 会在整体工程合成完毕后执行,也就是说静态库没有被自动引用进 Pods Target 的工程配置中,造成上述问题。

接下来准备分析 pod install 流程,从源码中得到 cocoapods 会执行以下流程:

def install!
	prepare
	resolve_dependencies
	download_dependencies
	validate_targets
	generate_pods_project
	if installation_options.integrate_targets?
		integrate_user_project
	else
		UI.section 'Skipping User Project Integration'
	end
	perform_post_install_actions
end

generate_pods_project 方法已经在构建 Pods Target 工程,而我们的钩子在 perform_post_install_actions 的时候执行,所以没有被引用进工程中。那我们可以使用 :pre_install 来进行 Hook?由于 context 不一样,需要进行代码改造,改造完毕后再次执行,还是一样编译失败,而这一次的原因是,组件移动成功后,又被 download_dependencies 方法冲刷掉了。

接下来开始阅读 generate_pods_project 源码

def generate_pods_project(generator = create_generator)
	UI.section 'Generating Pods project' do
		generator.generate!
		@pods_project = generator.project
		run_podfile_post_install_hooks
		generator.write
		generator.share_development_pod_schemes
		write_lockfiles
	end
end

我发现源码中有 run_podfile_post_install_hooks 方法,这是一个钩子,接着往下看发现

# Runs the post install hooks of the installed specs and of the Podfile.
#
# @note   Post install hooks run _before_ saving of project, so that they
#         can alter it before it is written to the disk.
#
# @return [void]
#
def run_podfile_post_install_hooks
	UI.message '- Running post install hooks' do
		executed = run_podfile_post_install_hook
		UI.message '- Podfile' if executed
	end
end

# Runs the post install hook of the Podfile
#
# @raise  Raises an informative if the hooks raises.
#
# @return [Boolean] Whether the hook was run.
#
def run_podfile_post_install_hook
	podfile.post_install!(self)
	rescue => e
		raise Informative, 'An error occurred while processing the post-install ' \
			'hook of the Podfile.' \
			"\n\n#{e.message}\n\n#{e.backtrace * "\n"}"
end

这里居然执行了 podfile.post_install , 也就是说,在工程目录中 Podfile 中,编写 post_install Hook 就行了,最后我在工程 Podfile 文件的末尾,添加如下代码:

pre_install do |installer|
   p "hello world!"
end

这里可以成功输出。

还需要解决如何在这里拿到 Cocoapods Plugins 计算的结果?在这里我通过生成 .lock 文件来解决跨代码文件通信问题,首先,我们必须要在 :pre_install 中下载组件,并将组件移动信息的写入.lock 文件中,之后我们在 Podfile 文件中的钩子,读取 .lock 文件信息,移动组件目录,最终在 Podfile 文件末尾添加如下代码:

def download_from_ftpandmove
  # 如果 lock 文件不存在,则不执行
  if !File.exists?("PodfileFtp.lock")
      return ;
  end
  # 读取 json 文件
  require "json"
  file = File.open   "PodfileFtp.lock"
  vendor_plugins = JSON.load file
  # 遍历组件、并移动到指定目录
  for item in vendor_plugins do
      from = item["from"]
      to = item["to"]
      # 创建目录
      if !File.directory?(to)
         FileUtils.mkdir_p to
      end
      # 拷贝移动
      FileUtils.cp_r(from, to)
   end
end

pre_install do |installer|
   download_from_ftpandmove
end

最后重新执行 pod install 之后,工程下载自动引用组件、编译运行成功!