对于iOS开发者而言,CocoaPods并不陌生,通过pod相关的命令操作,就可以很方便的将项目中用到的三方依赖库资源集成到项目环境中,大大的提升了开发的效率。CocoaPods作为iOS项目的包管理工具,它在命令行背后做了什么操作?而又是通过什么样的方式将命令指令声明出来供我们使用的?这些实现的背后底层逻辑是什么?都是本文想要探讨挖掘的。
一、Ruby是如何让系统能够识别已经安装的Pods指令的?
我们都知道在使用CocoaPods管理项目三方库之前,需要安装Ruby环境,同时基于Ruby的包管理工具gem再去安装CocoaPods。通过安装过程可以看出来,CocoaPods本质就是Ruby的一个gem包。而安装Cocoapods的时候,使用了以下的安装命令:
sudo gem install cocoapods
安装完成之后,就可以使用基于Cocoapods的 pod xxxx
相关命令了。gem install xxx
到底做了什么也能让 Terminal
正常的识别 pod 命令?gem的工作原理又是什么?了解这些之前,可以先看一下 RubyGems
的环境配置,通过以下的命令:
gem environment
通过以上的命令,可以看到Ruby的版本信息,RubyGem的版本,以及gems包安装的路径,进入安装路径 /Library/Ruby/Gems/2.6.0 后,我们能看到当前的Ruby环境下所安装的扩展包,这里能看到我们熟悉的Cocoapods相关的功能包。除了安装包路径之外,还有一个 EXECUTABLE DIRECTORY 执行目录 /usr/local/bin,可以看到拥有可执行权限的pod文件,如下:
预览一下pod文件内容:
#!/System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/bin/ruby
#
# This file was generated by RubyGems.
#
# The application 'cocoapods' is installed as part of a gem, and
# this file is here to facilitate running it.
#
require 'rubygems'
version = ">= 0.a"
str = ARGV.first
if str
str = str.b[/\A_(.*)_\z/, 1]
if str and Gem::Version.correct?(str)
version = str
ARGV.shift
end
end
if Gem.respond_to?(:activate_bin_path)
load Gem.activate_bin_path('cocoapods', 'pod', version)
else
gem "cocoapods", version
load Gem.bin_path("cocoapods", "pod", version)
end
根据文件注释内容可以发现,当前的可执行文件是 RubyGems
在安装 Cocoapods
的时候自动生成的,同时会将当前的执行文件放到系统的环境变量路径中,也即存放到了 /usr/local/bin
中了,这也就解释了为什么我们通过gem安装cocoapods之后,就立马能够识别pod可执行环境了。
虽然能够识别pod可执行文件,但是具体的命令参数是如何进行识别与实现呢?继续看以上的pod的文件源码,会发现最终都指向了 Gem
的 activate_bin_path
与 bin_path
方法,为了搞清楚Gem到底做了什么,在官方的RubyGems源码的rubygems.rb
文件中找到了两个方法的相关定义与实现,摘取了主要的几个方法实现,内容如下:
##
# Find the full path to the executable for gem +name+. If the +exec_name+
# is not given, an exception will be raised, otherwise the
# specified executable's path is returned. +requirements+ allows
# you to specify specific gem versions.
#
# A side effect of this method is that it will activate the gem that
# contains the executable.
#
# This method should *only* be used in bin stub files.
def self.activate_bin_path(name, exec_name = nil, *requirements) # :nodoc:
spec = find_spec_for_exe name, exec_name, requirements
Gem::LOADED_SPECS_MUTEX.synchronize do
spec.activate
finish_resolve
end
spec.bin_file exec_name
end
def self.find_spec_for_exe(name, exec_name, requirements)
#如果没有提供可执行文件的名称,则抛出异常
raise ArgumentError, "you must supply exec_name" unless exec_name
# 创建一个Dependency对象
dep = Gem::Dependency.new name, requirements
# 获取已经加载的gem
loaded = Gem.loaded_specs[name]
# 存在直接返回
return loaded if loaded && dep.matches_spec?(loaded)
# 查找复合条件的gem配置
specs = dep.matching_specs(true)
specs = specs.find_all do |spec|
# 匹配exec_name 执行名字,如果匹配结束查找
spec.executables.include? exec_name
end if exec_name
# 如果没有找到符合条件的gem,抛出异常
unless spec = specs.first
msg = "can't find gem #{dep} with executable #{exec_name}"
raise Gem::GemNotFoundException, msg
end
#返回结果
spec
end
private_class_method :find_spec_for_exe
##
# Find the full path to the executable for gem +name+. If the +exec_name+
# is not given, an exception will be raised, otherwise the
# specified executable's path is returned. +requirements+ allows
# you to speci