前语
在大多数iOS项目中,都会运用CocoaPods作为三方库的包处理东西,某些项目还会运用Bundler来束缚CocoaPods的版别、处理CocoaPods的插件等,而CocoaPods和Bundler作为Gem包,通常会运用RubyGems来设备和处理。
RubyGems、Bundler、CocoaPods都是Ruby语言开发的东西,咱们在运用这套东西链的进程中,或许对中心的工作进程知之甚少,遇到问题也会有许多疑问。
本文将从Bundler和CocoaPods指令实行流程的角度,来了解整个东西链的工作原理。让咱们在后续运用进程中,知道在终端敲下指令后,背后发生了什么,以及遇到问题怎样定位,甚至可以学习长辈们的思路,创造自己的东西。
bundle exec pod xxx 实行流程
直接实行pod
指令,流程中只会涉及到RubyGems和Cocoapods,为了了解包含Bundler在内的整体东西链的工作原理,本文将对bundle exec pod xxx
的工作进程进行分析(xxx
代表pod
的任意子指令),了解了bundle exec pod xxx
工作实行进程,关于pod
指令工作进程的了解就是瓜熟蒂落的事。
先简略整理下bundle exec pod xxx
的实行流程,假设有不了解的地方可以先越过,后边会翻开描绘各个环节的细节。
当在终端敲下bundle exec pod xxx
时:
1、Shell指令行说明器解分出bundle指令,根据环境变量$PATH查找到bundle可实行文件
2、读取bundle可实行文件榜首行shebang(!#),找到ruby说明器途径,敞开新进程,加载实行ruby说明器程序,后续由ruby说明器说明实行bundle可实行文件的其他代码
3、RubyGems从已设备的Gem包中查找指定版其他bundler,加载实行bundler中的bundle脚本文件,进入bundler程序
4、bundler的CLI解析指令,解分出pod指令和参数install,分发给Bundler::Exec
5、Bundler::Exec查找pod的可实行文件,加载并实行pod可实行文件的代码
6、pod可实行文件和前面的bundle可实行文件相似,查找指定版其他Cocoapods,并找到Cocoapods里的pod可实行文件加载实行,进入Cocoapods程序
以上就是整体流程,接下来分析流程中各环节的细节
Shell指令行说明器处理bundle
指令
每开一个终端,操作体系就会发起一个Shell指令行说明器程序,Shell指令行说明器会进入一个循环,等候并解析用户输入指令。
终端上可以通过watch ps
查看其时正在工作的进程。假设没有watch
指令,可通过brew install watch
设备。
从 macOS Catalina 开始,Zsh 成为默许的Shell说明器。可以通过echo $SHELL
查看其时的Shell说明器。更多关于mac上终端和Shell相关常识可以参看 终端运用手册。关于Zsh 的源码可以通过zsh.sourceforge.io/下载。
当用户输入指令并按回车键后,Shell说明器解分出指令,例如bundle
,然后通过环境变量$PATH
查找名为bundle
的可实行文件的途径。
$ echo $PATH
/Users/用户名/.rvm/gems/ruby-3.0.0/bin:/Users/用户名/.rvm/gems/ruby-3.0.0@global/bin:/Users/用户名/.rvm/rubies/ruby-3.0.0/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/Library/Apple/usr/bin:/Users/用户名/.rvm/bin
以:
将$PATH分隔成多个途径,然后从前往后,查找某途径下是否有指令的可实行文件。例如翻开/Users/用户名/.rvm/gems/ruby-3.0.0/bin
,可以看到bundle
可实行文件等。
也可以在终端通过which bundle
查看可实行文件的途径:
$ which bundle
/Users/用户名/.rvm/gems/ruby-3.0.0/bin/bundle
Ruby说明器
通过cat
查看上述bundle
可实行文件的内容:
$ cat /Users/用户名/.rvm/gems/ruby-3.0.0/bin/bundle
#!/Users/用户名/.rvm/rubies/ruby-3.0.0/bin/ruby
#
# This file was generated by RubyGems.
#
# The application 'bundler' is installed as part of a gem, and
# this file is here to facilitate running it.
#
# 加载rubygems (lib目录下)
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
# rubygems新版别中实行activate_bin_path方法
if Gem.respond_to?(:activate_bin_path)
# 查找bundler中名为bundle的可实行文件,加载并实行 (bundler是Gem包的称号,bundle是可实行文件称号)
load Gem.activate_bin_path('bundler', 'bundle', version)
else
gem "bundler", version
load Gem.bin_path("bundler", "bundle", version)
end
其内容的榜首行shebang(#!)指明晰实行该程序的说明器为#!/Users/用户名/.rvm/rubies/ruby-3.0.0/bin/ruby
。Shell说明器读到这一行之后,便会敞开一个新进程,加载ruby说明器,后续的作业交给ruby说明器程序。
这儿的ruby是运用了RVM进行了版别操控,假设是homebrew设备的,途径是/usr/local/opt/ruby/bin/ruby
,体系自带的ruby的途径是/usr/bin/ruby
。
可以通过途径/Users/用户名/.rvm/src/ruby-3.0.0
看到ruby的源码。简略看一下ruby的main
函数:
int
main(int argc, char **argv)
{
#ifdef RUBY_DEBUG_ENV
ruby_set_debug_option(getenv("RUBY_DEBUG"));
#endif
#ifdef HAVE_LOCALE_H
setlocale(LC_CTYPE, "");
#endif
ruby_sysinit(&argc, &argv);
{
RUBY_INIT_STACK;
ruby_init();
return ruby_run_node(ruby_options(argc, argv));
}
}
ruby程序正式工作前,会通过ruby_options
函数读取环境变量RUBYOPT
(ruby说明器选项),可以通过设置环境变量RUBYOPT
来自界说ruby说明器的行为。
例如在用户目录下创建一个ruby文件.auto_bundler.rb
,然后在Zsh的环境变量配备文件.zshrc
中添加:export RUBYOPT="-r/Users/用户名/.auto_bundler.rb"
,实行一下source .zshrc
或许新开一个终端,ruby程序工作前便会加载.auto_bundler.rb
。咱们可以运用该机制,在.auto_bundler.rb
添加逻辑,在iOS项目下实行pod xxx
时,查看假设存在Gemfile
文件,自动将pod xxx
替换成bundle exec pod xxx
,从而达到省去bundle exec
的意图。
RubyGems中查找Gem包
通过上述bundle
可实行文件的内容,咱们还可以知道该文件是由RubyGems
在设备bundler时生成,也就是在gem install bundler
进程中生成的。
RubyGems
是ruby库(Gem)的包处理东西,github源码地址 github.com/rubygems/ru…, 设备到电脑上的源码地址 ~/.rvm/rubies/ruby-x.x.x/lib/ruby/x.x.x
。其指令行东西的一级指令是gem
。
当实行gem install xxx
设备Gem完成后,会结合Gem的gemspec
文件里executables
指定的称号,生成对应的可实行文件,并写入~/.rvm/rubies/ruby-x.x.x/bin
目录下。源码细节可以查看RubyGems
的installer.rb
文件中的install
和generate_bin
方法。
RubyGems
生成的bundle
以及其他Gem的可实行文件里的中心逻辑,是去查找指定版其他Gem包里的可实行文件,加载并实行。以下是RubyGems
3.0.0版其他查找逻辑:
rubygems.rb:
def self.activate_bin_path(name, exec_name = nil, *requirements) # :nodoc:
# 查找gemspec文件,回来Gem::Specification方针
spec = find_spec_for_exe name, exec_name, requirements
Gem::LOADED_SPECS_MUTEX.synchronize do
# 这两行中心逻辑是将Gem的依靠项以及自己的gemspec文件里的require_paths(lib目录)添加到$LOAD_PATH中
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
# 通过Gem名和参数创建一个Gem::Dependency方针
dep = Gem::Dependency.new name, requirements
# 根据Gem名获取已加载的Gem的spec
loaded = Gem.loaded_specs[name]
# 假设获取到已加载的Gem的spec并且是符合条件的,则直接回来
return loaded if loaded && dep.matches_spec?(loaded)
#查找一切满足条件的spec
specs = dep.matching_specs(true)
# 过滤出executables包含传进来的可实行文件名的spec
#(bundler的spec文件的executables:%w[bundle bundler])
specs = specs.find_all do |spec|
spec.executables.include? exec_name
end if exec_name
# 假设有多个版别,回来找到的榜首个,一般是最大版别,bunder在外
unless spec = specs.first
msg = "can't find gem #{dep} with executable #{exec_name}"
raise Gem::GemNotFoundException, msg
end
spec
end
dependency.rb:
def matching_specs(platform_only = false)
env_req = Gem.env_requirement(name)
# 关于多个版其他Gem,这儿得到的是按版别降序的数组
matches = Gem::Specification.stubs_for(name).find_all do |spec|
requirement.satisfied_by?(spec.version) && env_req.satisfied_by?(spec.version)
end.map(&:to_spec)
# 这儿会针对bundler特别处理
# 例如读取其时目录下Gemfile.lock文件里的“BUNDLED WITH xxx”的版别号xxx,将xxx版别号的spec放到matches的首位
# 关于bundler特别处理的逻辑详见RubyGems里的bundler_version_finder
Gem::BundlerVersionFinder.filter!(matches) if name == "bundler".freeze && !requirement.specific?
if platform_only
matches.reject! do |spec|
spec.nil? || !Gem::Platform.match_spec?(spec)
end
end
matches
end
def self.stubs_for(name)
if @@stubs_by_name[name]
@@stubs_by_name[name]
else
pattern = "#{name}-*.gemspec"
stubs = installed_stubs(dirs, pattern).select {|s| Gem::Platform.match_spec? s } + default_stubs(pattern)
stubs = stubs.uniq {|stub| stub.full_name }.group_by(&:name)
stubs.each_value {|v| _resort!(v) }
@@stubs_by_name.merge! stubs
@@stubs_by_name[name] ||= EMPTY
end
end
# Gem名升序,版别号降序
def self._resort!(specs) # :nodoc:
specs.sort! do |a, b|
names = a.name <=> b.name
next names if names.nonzero?
versions = b.version <=> a.version
next versions if versions.nonzero?
Gem::Platform.sort_priority(b.platform)
end
end
Gem::Dependency
的matches_specs
方法是在specifications
目录下查找符合条件的gemspec
文件,存在多个版别时回来最大版别。但是对bundler做了特别处理,可以通过设置环境变量或许在项意图Gemfile中
指定bundler版别等方法,回来需求的bundler版别。
Bundler查找指定版其他Cocoapods
Bundler是处理Gem依靠和版其他东西,其指令行东西的一级指令是bundle
和bundler
,两者是等效的。
Bundler的gemspec文件里的executables为%w[bundle bundler]
bundler.gemspec部分内容
Gem::Specification.new do |s|
s.name = "bundler"
s.version = Bundler::VERSION
# ...
s.files = Dir.glob("lib/bundler{.rb,/**/*}", File::FNM_DOTMATCH).reject {|f| File.directory?(f) }
# include the gemspec itself because warbler breaks w/o it
s.files += %w[bundler.gemspec]
s.files += %w[CHANGELOG.md LICENSE.md README.md]
s.bindir = "exe"
s.executables = %w[bundle bundler]
s.require_paths = ["lib"]
end
设备之后RubyGems会生成bundle和bundler两个可实行文件,而Bundler包里既有bundle,也有bundler可实行文件,bundler的逻辑实际上是去加载bundle可实行文件,中心逻辑在bundle可实行文件中。
Bundler中的bundle可实行文件的中心代码:
#!/usr/bin/env ruby
base_path = File.expand_path("../lib", __dir__)
if File.exist?(base_path)
$LOAD_PATH.unshift(base_path)
end
Bundler::CLI.start(args, :debug => true)
这个函数通过指令解析和分发,抵达CLI::Exec的run函数:
def run
validate_cmd!
# 设置bundle环境
SharedHelpers.set_bundle_environment
# 查找pod的可实行文件
if bin_path = Bundler.which(cmd)
if !Bundler.settings[:disable_exec_load] && ruby_shebang?(bin_path)
# 加载pod可实行文件
return kernel_load(bin_path, *args)
end
kernel_exec(bin_path, *args)
else
# exec using the given command
kernel_exec(cmd, *args)
end
end
def kernel_load(file, *args)
args.pop if args.last.is_a?(Hash)
ARGV.replace(args)
$0 = file
Process.setproctitle(process_title(file, args)) if Process.respond_to?(:setproctitle)
# 加载实行setup.rb文件
require_relative "../setup"
TRAPPED_SIGNALS.each {|s| trap(s, "DEFAULT") }
# 加载实行pod可实行文件
Kernel.load(file)
rescue SystemExit, SignalException
raise
rescue Exception # rubocop:disable Lint/RescueException
Bundler.ui.error "bundler: failed to load command: #{cmd} (#{file})"
Bundler::FriendlyErrors.disable!
raise
end
终端上通过which pod查看pod可实行文件的途径,再通过cat查看其内容,可以看到内容和bundler共同
$ which pod
/Users/用户名/.rvm/gems/ruby-3.0.0/bin/pod
$ cat /Users/用户名/.rvm/gems/ruby-3.0.0/bin/pod
#!/Users/用户名/.rvm/rubies/ruby-3.0.0/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查找bundler的方法,会找到最高版其他Cocoapods。那么,bundler将指令转发给pod前,是怎样查找到Gemfile.lock文件中指定版其他cocoapods或许其他Gem呢?
实际上Bundler替换了RubyGems的activate_bin_path和find_spec_for_exe等方法的完成。
上述的setup.rb中的中心代码是Bundler.setup
,最终会实行到runtime.rb
文件的setup
方法
runtime.rb
def setup(*groups)
# @definition是有Gemfile和Gemfile.lock文件生成的
@definition.ensure_equivalent_gemfile_and_lockfile if Bundler.frozen_bundle?
# Has to happen first
clean_load_path
# 根据definition获取一切Gem的spec信息
specs = @definition.specs_for(groups)
# 设置bundle的环境变量等
SharedHelpers.set_bundle_environment
# 替换RubyGems的一些方法,比方activate_bin_path和find_spec_for_exe等
# 使Gem包从specs中获取(获取Gemfile中指定版其他Gem)
Bundler.rubygems.replace_entrypoints(specs)
# 将Gem包lib目录添加到$Load_PATH
# Activate the specs
load_paths = specs.map do |spec|
check_for_activated_spec!(spec)
Bundler.rubygems.mark_loaded(spec)
spec.load_paths.reject {|path| $LOAD_PATH.include?(path) }
end.reverse.flatten
Bundler.rubygems.add_to_load_path(load_paths)
setup_manpath
lock(:preserve_unknown_sections => true)
self
end
可以通过终端上在工程目录下实行 bundle info cocoapods
找到Gemfile中指定版其他cocoapods的设备途径, 再通过cat查看其bin目录下的pod文件内容,其中心逻辑如下:
#!/usr/bin/env ruby
# ... 忽略一些关于编码处理的代码
require 'cocoapods'
# 假设环境变量配备文件文件中设置了COCOAPODS_PROFILE,会讲Cocoapod的方法耗时写入COCOAPODS_PROFILE对应的文件中
if profile_filename = ENV['COCOAPODS_PROFILE']
require 'ruby-prof'
# ...
File.open(profile_filename, 'w') do |io|
reporter.new(RubyProf.profile { Pod::Command.run(ARGV) }).print(io)
end
else
# 指令解析和转发等
Pod::Command.run(ARGV)
end
Cocoapods依靠claide
解析指令,Pod::Command继承自CLAide::Command,CLAide::Command的run方法如下:
def self.run(argv = [])
# 加载插件
plugin_prefixes.each do |plugin_prefix|
PluginManager.load_plugins(plugin_prefix)
end
argv = ARGV.coerce(argv)
# 解分出子指令
command = parse(argv)
ANSI.disabled = !command.ansi_output?
unless command.handle_root_options(argv)
command.validate!
#指令的子类实行,例如Pod::Command::Install
command.run
end
rescue Object => exception
handle_exception(command, exception)
end
插件加载:
每个pod指令的实行都会通过claide
的PluginManager
去加载插件。Pod::Command
重写了CLAide::Command
的plugin_prefixes
,值为%w(claide cocoapods)
。PluginManager
会去加载其时环境下一切包含claide_plugin.rb
或 cocoapods_plugin.rb
文件的 Gem。cocoapods插件中都会有一个cocoapods_plugin.rb
文件。
关于cocoapods的其他具体解析,可以参看Cocoapods历险记系列文章。
VSCode断点调试任意Ruby项目
1、VSCode设备扩展:rdbg
2、工程目录中创建Gemfile,添加以下Gem包,然后终端实行bundle install。
gem 'ruby-debug-ide'
gem 'debase', '0.2.5.beta2'
0.2.5.beta2是debase的最高版别,0.2.5.beta1和0.2.4.1都会报错,issue: 0.2.4.1 and 0.2.5.beta Fail to build on macOS Catalina 10.15.7
3、用VSCode翻开要调试的ruby项目,例如Cocoapods。
-
假设调试其时运用版其他Cocopods,找的Cocopods地点目录,VSCode翻开即可。
-
假设调试从github克隆的Cocopods,Gemfile里需求用path实行该Cocopods, 例如:
gem "cocoapods", :path => '~/dev/CocoaPods/'
4、创建lanch.json
launch.json:
{
// 运用 IntelliSense 了解相关特点。
// 悬停以查看现有特点的描绘。
// 欲了解更多信息,请拜访: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "rdbg",
"name": "pod install", //配备称号,用于在调试器中标识该配备
"request": "launch",
"script": "/Users/用户名/.rvm/gems/ruby-3.0.0/bin/pod", //指定要实行的脚本或可实行文件的途径
"cwd": "/Users/用户名/Project/NC", //指定在哪个目录下实行
"args": ["install"], //传递给脚本或可实行文件的指令行参数
"askParameters": true, //在发起调试会话之前提示用户输入其他参数
"useBundler": true, //运用Bundler
}
]
}
5、工作
断点调试RubyGems -> Bundler -> Cocoapods的流程
1、实行which ruby
找到ruby目录,在ruby-x.x.x目录下找到lib/ruby/x.x.x/
,取得rubygems源码方位
$ which ruby
/Users/用户名/.rvm/rubies/ruby-3.0.0/bin/ruby
其源码方位:/Users/用户名/.rvm/gems/ruby-3.0.0/lib/ruby/3.0.0
2、用VSCode翻开/Users/用户名/.rvm/gems/ruby-3.0.0/bin/ruby/3.0.0
,找到rubygems.rb
文件,在load Gem.activate_bin_path
这一行加上断点
3、在包含Gemfile的iOS项目目录下,实行which bundle
,获取到bundle的可实行文件途径:
$ which bundle
/Users/用户名/.rvm/gems/ruby-3.0.0/bin/bundle
4、创建launch.json,在launch.json中添加如下配备:
launch.json:
{
"version": "0.2.0",
"configurations": [
{
"type": "rdbg",
"name": "exec pod install", // 名字任意
"request": "launch",
"script": "/Users/用户名/.rvm/gems/ruby-3.0.0/bin/bundle", // `which bundle`找到的途径
"args": ["exec pod install"],
"askParameters": false,
"cwd": "/Users/用户名/iOSProject" //替换为自己的iOS项目途径
}
]
}
至此便可以点击VSCode的run按钮断点调试ruby东西链的主体流程了。
工作断点假设跳不到bundler或许cocoapods项目中,可以将bundler或许cocoapods源码中的文件拖到VSCode工程中。
获取项目中正在运用的bundler或许cocoapods源码的源码方位,可以在项目目录下实行bundle info bundler
和bundle info cocoapods
,从输出的成果中可以找到途径。
假设提示某些Gem包未设备,实行gem install xxx
设备即可。