前语

在大多数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可实行文件等。

深化了解RubyGems、Bundler、CocoaPods的实行进程

也可以在终端通过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目录下。源码细节可以查看RubyGemsinstaller.rb文件中的installgenerate_bin方法。

RubyGems生成的bundle以及其他Gem的可实行文件里的中心逻辑,是去查找指定版其他Gem包里的可实行文件,加载并实行。以下是RubyGems3.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::Dependencymatches_specs方法是在specifications目录下查找符合条件的gemspec文件,存在多个版别时回来最大版别。但是对bundler做了特别处理,可以通过设置环境变量或许在项意图Gemfile中指定bundler版别等方法,回来需求的bundler版别。

深化了解RubyGems、Bundler、CocoaPods的实行进程


Bundler查找指定版其他Cocoapods

Bundler是处理Gem依靠和版其他东西,其指令行东西的一级指令是bundlebundler,两者是等效的。

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指令的实行都会通过claidePluginManager去加载插件。Pod::Command重写了CLAide::Commandplugin_prefixes,值为%w(claide cocoapods)PluginManager会去加载其时环境下一切包含claide_plugin.rbcocoapods_plugin.rb 文件的 Gem。cocoapods插件中都会有一个cocoapods_plugin.rb文件。

关于cocoapods的其他具体解析,可以参看Cocoapods历险记系列文章。


VSCode断点调试任意Ruby项目

1、VSCode设备扩展:rdbg

深化了解RubyGems、Bundler、CocoaPods的实行进程

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

深化了解RubyGems、Bundler、CocoaPods的实行进程

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的实行进程


断点调试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

深化了解RubyGems、Bundler、CocoaPods的实行进程

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 bundlerbundle info cocoapods,从输出的成果中可以找到途径。

假设提示某些Gem包未设备,实行gem install xxx设备即可。