共计 28803 个字符,预计需要花费 73 分钟才能阅读完成。
对于 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 specify specific gem versions. | |
def self.bin_path(name, exec_name = nil, *requirements) | |
requirements = Gem::Requirement.default if | |
requirements.empty? | |
# 通过 exec_name 查找 gem 中可执行文件 | |
find_spec_for_exe(name, exec_name, requirements).bin_file exec_name | |
end | |
class Gem::Dependency | |
def matching_specs(platform_only = false) | |
env_req = Gem.env_requirement(name) | |
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) | |
if prioritizes_bundler? | |
require_relative "bundler_version_finder" | |
Gem::BundlerVersionFinder.prioritize!(matches) | |
end | |
if platform_only | |
matches.reject! do |spec| | |
spec.nil? || !Gem::Platform.match_spec?(spec) | |
end | |
end | |
matches | |
end | |
end | |
class Gem::Specification < Gem::BasicSpecification | |
def self.stubs_for(name) | |
if @@stubs | |
@@stubs_by_name[name] || [] | |
else | |
@@stubs_by_name[name] ||= stubs_for_pattern("#{name}-*.gemspec").select do |s| | |
s.name == name | |
end | |
end | |
end | |
end | |
通过以后的实现能够看出在两个办法实现中,通过 find\_spec\_for_exe 办法根据名称 name 查找 sepc 对象,匹配胜利之后返回 sepc 对象,最终通过 spec 对象中的 bin_file 办法来进行执行相干的命令。以下为 gems 装置的配置目录汇合:
注:bin_file
办法的实现形式取决于 gem 包
的类型和所应用的操作系统。在大多数状况下,它会依据操作系统的不同,应用不同的查找算法来确定二进制文件的门路。例如,在 Windows
上,它会搜寻 gem
包的 bin
目录,而在 Unix
上,它会搜寻 gem
包的 bin
目录和 PATH
环境变量中的门路。
通过以后的实现能够看出在两个办法实现中,find\_spec\_for\_exe 办法会遍历所有已装置的 gem 包,查找其中蕴含指定可执行文件的 gem 包。如果找到了匹配的 gem 包,则会返回该 gem 包的 Gem::Specification 对象,并调用其 bin\_file 办法获取二进制文件门路。而 bin_file
是在 Gem::Specification 类中定义的。它是一个实例办法,用于查找与指定的可执行文件 exec_name 相关联的 gem 包的二进制文件门路,定义实现如下:
def bin_dir | |
@bin_dir ||= File.join gem_dir, bindir | |
end | |
## | |
# Returns the full path to installed gem's bin directory. | |
# | |
# NOTE: do not confuse this with +bindir+, which is just 'bin', not | |
# a full path. | |
def bin_file(name) | |
File.join bin_dir, name | |
end |
到这里,能够看出,pod 命令实质是执行了 RubyGems 的 find\_spec\_for_exe 办法,用来查找并执行 gems 装置目录下的 bin 目录,也即是 /Library/Ruby/Gems/2.6.0
目录下的 gem 包下的 bin 目录。而针对于 pod 的 gem 包,如下所示:
至此,能够发现,由零碎执行环境 /usr/local/bin 中的可执行文件 pod 疏导触发,Ruby 通过 Gem.bin_path(“cocoapods”, “pod”, version) 与 Gem.activate\_bin\_path(‘cocoapods’, ‘pod’, version) 进行转发,再到 gems 包装置目录的 gem 查找办法 find\_spec\_for_exe,最终转到 gems 安装包下的 bin 目录的执行文件进行命令的最终执行,流程大抵如下:
而对于 pod 的命令又是如何进行辨认辨别的呢?刚刚的剖析能够看出对于 gems 安装包的 bin 下的执行文件才是最终的执行内容,关上 cocoapod 的 bin 目录下的 pod 可执行文件,如下:
#!/usr/bin/env ruby | |
if Encoding.default_external != Encoding::UTF_8 | |
if ARGV.include? '--no-ansi' | |
STDERR.puts <<-DOC | |
WARNING: CocoaPods requires your terminal to be using UTF-8 encoding. | |
Consider adding the following to ~/.profile: | |
export LANG=en_US.UTF-8 | |
DOC | |
else | |
STDERR.puts <<-DOC | |
\e[33mWARNING: CocoaPods requires your terminal to be using UTF-8 encoding. | |
Consider adding the following to ~/.profile: | |
export LANG=en_US.UTF-8 | |
\e[0m | |
DOC | |
end | |
end | |
if $PROGRAM_NAME == __FILE__ && !ENV['COCOAPODS_NO_BUNDLER'] | |
ENV['BUNDLE_GEMFILE'] = File.expand_path('../../Gemfile', __FILE__) | |
require 'rubygems' | |
require 'bundler/setup' | |
$LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) | |
elsif ENV['COCOAPODS_NO_BUNDLER'] | |
require 'rubygems' | |
gem 'cocoapods' | |
end | |
STDOUT.sync = true if ENV['CP_STDOUT_SYNC'] == 'TRUE' | |
require 'cocoapods' | |
# 环境变量判断是否配置了 profile_filename,如果配置了依照配置内容生成 | |
if profile_filename = ENV['COCOAPODS_PROFILE'] | |
require 'ruby-prof' | |
reporter = | |
case (profile_extname = File.extname(profile_filename)) | |
when '.txt' | |
RubyProf::FlatPrinterWithLineNumbers | |
when '.html' | |
RubyProf::GraphHtmlPrinter | |
when '.callgrind' | |
RubyProf::CallTreePrinter | |
else | |
raise "Unknown profiler format indicated by extension: #{profile_extname}" | |
end | |
File.open(profile_filename, 'w') do |io| | |
reporter.new(RubyProf.profile { Pod::Command.run(ARGV) }).print(io) | |
end | |
else | |
Pod::Command.run(ARGV) | |
end | |
能够发现,pod 命令参数的解析运行是通过 Pod::Command.run(ARGV) 实现的。通过该线索,咱们接着查看 Pod 库源码的 Command 类的 run 办法都做了什么?该类在官网源码的 lib/cocoapods/command.rb 定义的,摘取了局部内容如下:
class Command < CLAide::Command | |
def self.run(argv) | |
ensure_not_root_or_allowed! argv | |
verify_minimum_git_version! | |
verify_xcode_license_approved! | |
super(argv) | |
ensure | |
UI.print_warnings | |
end | |
end |
源码中在进行命令解析之前,进行了前置条件查看判断:1、查看以后用户是否为 root 用户或是否在容许的用户列表中 2、查看以后零碎上装置的 Git 版本是否合乎最低要求 3、查看以后零碎上的 Xcode 许可是否曾经受权
如果都没有问题,则会调用父类的 run
办法,而命令的解析能够看进去应该是在其父类 CLAide::Command
进行的,CLAide
是 CocoaPods
的命令行解析库,在 command.rb
文件中,能够找到如下 Command
类的实现:
def initialize(argv) | |
argv = ARGV.coerce(argv) | |
@verbose = argv.flag?('verbose') | |
@ansi_output = argv.flag?('ansi', Command.ansi_output?) | |
@argv = argv | |
@help_arg = argv.flag?('help') | |
end | |
def self.run(argv = []) | |
plugin_prefixes.each do |plugin_prefix| | |
PluginManager.load_plugins(plugin_prefix) | |
end | |
# 转换成 ARGV 对象 | |
argv = ARGV.coerce(argv) | |
# 解决无效命令行参数 | |
command = parse(argv) | |
ANSI.disabled = !command.ansi_output? | |
unless command.handle_root_options(argv) | |
# 命令解决 | |
command.validate! | |
# 运行命令(由子类进行继承实现运行)command.run | |
end | |
rescue Object => exception | |
handle_exception(command, exception) | |
end | |
def self.parse(argv) | |
argv = ARGV.coerce(argv) | |
cmd = argv.arguments.first | |
# 命令存在,且子命令存在,进行再次解析 | |
if cmd && subcommand = find_subcommand(cmd) | |
# 移除第一个参数 | |
argv.shift_argument | |
# 解析子命令 | |
subcommand.parse(argv) | |
# 不能执行的命令间接加载默认命令 | |
elsif abstract_command? && default_subcommand | |
load_default_subcommand(argv) | |
# 无内容则创立一个 comand 实例返回 | |
else | |
new(argv) | |
end | |
end | |
# 形象办法,由其子类进行实现 | |
def run | |
raise 'A subclass should override the `CLAide::Command#run` method to' \ | |
'actually perform some work.' | |
end | |
# 返回 [CLAide::Command, nil] | |
def self.find_subcommand(name) | |
subcommands_for_command_lookup.find {|sc| sc.command == name} | |
end |
通过将 argv
转换为 ARGV
对象(ARGV 是一个 Ruby 内置的全局变量,它是一个数组,蕴含了从命令行传递给 Ruby 程序的参数。例如:ARGV[0] 示意第一个参数,ARGV[1] 示意第二个参数,以此类推),而后获取第一个参数作为命令名称 cmd
。如果 cmd
存在,并且可能找到对应的子命令 subcommand
,则将 argv
中的第一个参数移除,并调用 subcommand.parse(argv)
办法解析残余的参数。如果没有指定命令或者找不到对应的子命令,但以后命令是一个形象命令(即不能间接执行),并且有默认的子命令,则加载默认子命令并解析参数。否则,创立一个新的实例,并将 argv
作为参数传递给它。
最终在转换实现之后,通过调用形象办法run
调用子类的实现来执行解析后的指令内容。到这里,顺其自然的就想到了 Cocoapods 的相干指令实现必然继承自了CLAide::Command
类,并实现了其形象办法 run
。为了验证这个推断,咱们接着看 Cocoapods 的源码,在文件 Install.rb
中,有这个 Install 类的定义与实现,摘取了核心内容:
module Pod | |
class Command | |
class Install < Command | |
include RepoUpdate | |
include ProjectDirectory | |
def self.options | |
[['--repo-update', 'Force running `pod repo update` before install'], | |
['--deployment', 'Disallow any changes to the Podfile or the Podfile.lock during installation'], | |
['--clean-install', 'Ignore the contents of the project cache and force a full pod installation. This only' \ | |
'applies to projects that have enabled incremental installation'], | |
].concat(super).reject {|(name, _)| name == '--no-repo-update' } | |
end | |
def initialize(argv) | |
super | |
@deployment = argv.flag?('deployment', false) | |
@clean_install = argv.flag?('clean-install', false) | |
end | |
# 实现 CLAide::Command 的形象办法 | |
def run | |
# 验证工程目录 podfile 是否存在 | |
verify_podfile_exists! | |
# 获取 installer 对象 | |
installer = installer_for_config | |
# 更新 pods 仓库 | |
installer.repo_update = repo_update?(:default => false) | |
# 设置更新标识为敞开 | |
installer.update = false | |
# 透传依赖设置 | |
installer.deployment = @deployment | |
# 透传设置 | |
installer.clean_install = @clean_install | |
installer.install! | |
end | |
end | |
end | |
end |
通过源码能够看出,cocoaPods
的命令解析是通过本身的 CLAide::Command
进行解析解决的,而最终的命令实现则是通过继承自 Command
的子类,通过实现形象办法 run
来实现的具体命令性能的。到这里,对于 Pod 命令的辨认以及 Pod 命令的解析与运行是不是十分清晰了。
阶段性小结一下,咱们在 Terminal 中进行 pod 命令运行的过程中,背地都经验了哪些过程?整个运行过程能够简述如下:1、通过 Gem 生成在零碎环境目录下的可执行文件 pod,通过该文件
疏导 RubyGems 查找 gems 包目录下的 sepc 配置对象,也即是 cocoaPods 的 sepc 配置对象 2、查找到配置对象,通过 bin_file 办法查找 cocoaPods 包门路中 bin 下的可执行文件 3、运行 rubygems 对应 cocoaPods 的 gem 安装包目录中 bin 下的二进制可执行文件 pod 4、通过执行 Pod::Command.run(ARGV)
解析命令与参数并找出最终的 Command 对象执行其 run 办法 5、在继承自 Command 的子类的 run 实现中实现各个命令行指令的实现
以上的 1~3 阶段实际上是 Ruby 的指令转发过程,最终将命令转发给了对应的 gems 包进行最终的解决。而 4~5 则是整个的处理过程。同时在 Cocoapods 的源码实现中,能够发现每个命令都对应一个 Ruby 类,该类继承自 CLAide::Command 类。通过继承以后类,能够定义该命令所反对的选项和参数,并在执行命令时解析这些选项和参数。
二、Ruby 是如何动静生成可执行文件并集成到零碎环境变量中的?
刚刚在上一节卖了个关子,在装置实现 Ruby 的 gem 包之后,在零碎环境变量中就主动生成了相干的可执行文件命令。那么 Ruby 在这个过程中又做了什么呢?既然是在 gem 装置的时候会动静生成,不如就以 gem 的装置命令 sudo gem install xxx 作为切入点去看相干的处理过程。咱们进入零碎环境变量门路 /usr/bin 找到 Gem 可执行二进制文件,如下:
关上 gem,它的内容如下:
#!/System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/bin/ruby | |
#-- | |
# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. | |
# All rights reserved. | |
# See LICENSE.txt for permissions. | |
#++ | |
require 'rubygems' | |
require 'rubygems/gem_runner' | |
require 'rubygems/exceptions' | |
required_version = Gem::Requirement.new ">= 1.8.7" | |
unless required_version.satisfied_by? Gem.ruby_version then | |
abort "Expected Ruby Version #{required_version}, is #{Gem.ruby_version}" | |
end | |
args = ARGV.clone | |
begin | |
Gem::GemRunner.new.run args | |
rescue Gem::SystemExitException => e | |
exit e.exit_code | |
end | |
能够发现最终通过执行 Gem::GemRunner.new.run args 来实现装置,显然装置的过程就在 Gem::GemRunner 类中。仍旧查看 RubyGems 的源码,在 gem_runner.rb 中,有着以下的定义:
def run(args) | |
build_args = extract_build_args args | |
do_configuration args | |
begin | |
Gem.load_env_plugins | |
rescue StandardError | |
nil | |
end | |
Gem.load_plugins | |
cmd = @command_manager_class.instance | |
cmd.command_names.each do |command_name| | |
config_args = Gem.configuration[command_name] | |
config_args = case config_args | |
when String | |
config_args.split " " | |
else | |
Array(config_args) | |
end | |
Gem::Command.add_specific_extra_args command_name, config_args | |
end | |
cmd.run Gem.configuration.args, build_args | |
end |
能够看进去命令的执行最终转到了 cmd.run Gem.configuration.args, build_args
的办法调用上,cmd 是通过 @command_manager_class
进行装璜的类,找到其装璜的中央如下:
def initialize | |
@command_manager_class = Gem::CommandManager | |
@config_file_class = Gem::ConfigFile | |
end |
发现是它其实 Gem::CommandManager 类,接着查看一下 CommandManager 的 run 办法实现,在文件 command_manager.rb 中,有以下的实现内容:
## | |
# Run the command specified by +args+. | |
def run(args, build_args=nil) | |
process_args(args, build_args) | |
# 异样解决 | |
rescue StandardError, Timeout::Error => ex | |
if ex.respond_to?(:detailed_message) | |
msg = ex.detailed_message(highlight: false).sub(/\A(.*?)(?: \(.+?\))/) {$1} | |
else | |
msg = ex.message | |
end | |
alert_error clean_text("While executing gem ... (#{ex.class})\n #{msg}") | |
ui.backtrace ex | |
terminate_interaction(1) | |
rescue Interrupt | |
alert_error clean_text("Interrupted") | |
terminate_interaction(1) | |
end | |
def process_args(args, build_args=nil) | |
# 空参数退出执行 | |
if args.empty? | |
say Gem::Command::HELP | |
terminate_interaction 1 | |
end | |
# 判断第一个参数 | |
case args.first | |
when "-h", "--help" then | |
say Gem::Command::HELP | |
terminate_interaction 0 | |
when "-v", "--version" then | |
say Gem::VERSION | |
terminate_interaction 0 | |
when "-C" then | |
args.shift | |
start_point = args.shift | |
if Dir.exist?(start_point) | |
Dir.chdir(start_point) {invoke_command(args, build_args) } | |
else | |
alert_error clean_text("#{start_point} isn't a directory.") | |
terminate_interaction 1 | |
end | |
when /^-/ then | |
alert_error clean_text("Invalid option: #{args.first}. See'gem --help'.") | |
terminate_interaction 1 | |
else | |
# 执行命令 | |
invoke_command(args, build_args) | |
end | |
end | |
def invoke_command(args, build_args) | |
cmd_name = args.shift.downcase | |
# 查找指令,并获取继承自 Gem::Commands 的实体子类(实现了 excute 形象办法)cmd = find_command cmd_name | |
cmd.deprecation_warning if cmd.deprecated? | |
# 执行 invoke_with_build_args 办法(该办法来自基类 Gem::Commands)cmd.invoke_with_build_args args, build_args | |
end | |
def find_command(cmd_name) | |
cmd_name = find_alias_command cmd_name | |
possibilities = find_command_possibilities cmd_name | |
if possibilities.size > 1 | |
raise Gem::CommandLineError, | |
"Ambiguous command #{cmd_name} matches [#{possibilities.join(", ")}]" | |
elsif possibilities.empty? | |
raise Gem::UnknownCommandError.new(cmd_name) | |
end | |
# 这里的[] 是办法调用,定义在上面 | |
self[possibilities.first] | |
end | |
## | |
# Returns a Command instance for +command_name+ | |
def [](command_name) | |
command_name = command_name.intern | |
return nil if @commands[command_name].nil? | |
# 调用 `load_and_instantiate` 办法来实现这个过程,并将返回的对象存储到 `@commands` 哈希表中,这里 ||= 是默认值内容,相似于 OC 中的?: | |
@commands[command_name] ||= load_and_instantiate(command_name) | |
end | |
# 命令散发抉择以及动静实例 | |
def load_and_instantiate(command_name) | |
command_name = command_name.to_s | |
const_name = command_name.capitalize.gsub(/_(.)/) {$1.upcase} << "Command" | |
load_error = nil | |
begin | |
begin | |
require "rubygems/commands/#{command_name}_command" | |
rescue LoadError => e | |
load_error = e | |
end | |
# 通过 Gem::Commands 获取注册的变量 | |
Gem::Commands.const_get(const_name).new | |
rescue StandardError => e | |
e = load_error if load_error | |
alert_error clean_text("Loading command: #{command_name} (#{e.class})\n\t#{e}") | |
ui.backtrace e | |
end | |
end |
通过以上的源码,能够发现命令的执行,通过调用 process_args
执行,而后在 process_args
办法中进行判断命令参数,接着通过 invoke_command
来执行命令。在 invoke_command
外部,首先通过find_command
查找命令,这里find_command
次要负责查找命令相干的执行对象,须要留神的中央在以下这句:
@commands[command_name] ||= load_and_instantiate(command_name)
通过以上的操作,返回以后命令执行的实体对象,而对应的脚本匹配又是如何实现的呢(比方输出的命令是 gem install 命令)?这里的 load_and_instantiate(command_name)
的办法其实就是查找实体的具体操作,在实现中通过以下的语句来获取最终的常量的命令指令实体:
Gem::Commands.const_get(const_name).new
下面的语句是通过 Gem::Commands
查找类中的常量,这里的常量其实就是对应 gem 相干的一个个指令,在 gem 中申明了很多命令的常量,他们继承自 Gem::Command
基类,同时实现了形象办法 execute
,这一点很重要。比方在 install_command.rb
中定义了命令 gem install
的具体的实现:
def execute | |
if options.include? :gemdeps | |
install_from_gemdeps | |
return # not reached | |
end | |
@installed_specs = [] | |
ENV.delete "GEM_PATH" if options[:install_dir].nil? | |
check_install_dir | |
check_version | |
load_hooks | |
exit_code = install_gems | |
show_installed | |
say update_suggestion if eglible_for_update? | |
terminate_interaction exit_code | |
end |
在 invoke_command
办法中,最终通过 invoke_with_build_args
来最终执行命令,该办法定义 Gem::Command
中,在 command.rb
文件中,能够看到内容如下:
def invoke_with_build_args(args, build_args) | |
handle_options args | |
options[:build_args] = build_args | |
if options[:silent] | |
old_ui = ui | |
self.ui = ui = Gem::SilentUI.new | |
end | |
if options[:help] | |
show_help | |
elsif @when_invoked | |
@when_invoked.call options | |
else | |
execute | |
end | |
ensure | |
if ui | |
self.ui = old_ui | |
ui.close | |
end | |
end | |
# 子类实现该形象实现命令的具体实现 | |
def execute | |
raise Gem::Exception, "generic command has no actions" | |
end |
能够看进去,最终基类中的 invoke_with_build_args
中调用了形象办法 execute
来实现命令的运行调用。在 rubyGems 外面申明了很多变量,这些变量在 CommandManager
中通过 run
办法进行命令常量实体的查找,最终通过调用继承自 Gem:Command
子类的 execute
实现相干指令的执行。在 rubyGems 中能够看到很多变量,一个变量对应一个命令,如下所示:
到这里,咱们根本能够晓得整个 gem 命令的查找到调用的整个流程。那么 gem install
的过程中又是如何主动生成并注册相干的 gem 命令到零碎环境变量中的呢?基于下面的命令查找调用流程,其实只须要在 install_command.rb
中查看 execute
具体的实现就分明了,如下:
def execute | |
if options.include? :gemdeps | |
install_from_gemdeps | |
return # not reached | |
end | |
@installed_specs = [] | |
ENV.delete "GEM_PATH" if options[:install_dir].nil? | |
check_install_dir | |
check_version | |
load_hooks | |
exit_code = install_gems | |
show_installed | |
say update_suggestion if eglible_for_update? | |
terminate_interaction exit_code | |
end | |
def install_from_gemdeps # :nodoc: | |
require_relative "../request_set" | |
rs = Gem::RequestSet.new | |
specs = rs.install_from_gemdeps options do |req, inst| | |
s = req.full_spec | |
if inst | |
say "Installing #{s.name} (#{s.version})" | |
else | |
say "Using #{s.name} (#{s.version})" | |
end | |
end | |
@installed_specs = specs | |
terminate_interaction | |
end | |
def install_gem(name, version) # :nodoc: | |
return if options[:conservative] && | |
!Gem::Dependency.new(name, version).matching_specs.empty? | |
req = Gem::Requirement.create(version) | |
dinst = Gem::DependencyInstaller.new options | |
request_set = dinst.resolve_dependencies name, req | |
if options[:explain] | |
say "Gems to install:" | |
request_set.sorted_requests.each do |activation_request| | |
say "#{activation_request.full_name}" | |
end | |
else | |
@installed_specs.concat request_set.install options | |
end | |
show_install_errors dinst.errors | |
end | |
def install_gems # :nodoc: | |
exit_code = 0 | |
get_all_gem_names_and_versions.each do |gem_name, gem_version| | |
gem_version ||= options[:version] | |
domain = options[:domain] | |
domain = :local unless options[:suggest_alternate] | |
suppress_suggestions = (domain == :local) | |
begin | |
install_gem gem_name, gem_version | |
rescue Gem::InstallError => e | |
alert_error "Error installing #{gem_name}:\n\t#{e.message}" | |
exit_code |= 1 | |
rescue Gem::GemNotFoundException => e | |
show_lookup_failure e.name, e.version, e.errors, suppress_suggestions | |
exit_code |= 2 | |
rescue Gem::UnsatisfiableDependencyError => e | |
show_lookup_failure e.name, e.version, e.errors, suppress_suggestions, | |
"'#{gem_name}' (#{gem_version})" | |
exit_code |= 2 | |
end | |
end | |
exit_code | |
end | |
能够看出,最终通过request_set.install
来实现最终的 gem 装置,而request_set
是Gem::RequestSet
的实例对象,接着在 request_set.rb
中查看相干的实现:
## | |
# Installs gems for this RequestSet using the Gem::Installer +options+. | |
# | |
# If a +block+ is given an activation +request+ and +installer+ are yielded. | |
# The +installer+ will be +nil+ if a gem matching the request was already | |
# installed. | |
def install(options, &block) # :yields: request, installer | |
if dir = options[:install_dir] | |
requests = install_into dir, false, options, &block | |
return requests | |
end | |
@prerelease = options[:prerelease] | |
requests = [] | |
# 创立下载队列 | |
download_queue = Thread::Queue.new | |
# Create a thread-safe list of gems to download | |
sorted_requests.each do |req| | |
# 存储下载实例 | |
download_queue << req | |
end | |
# Create N threads in a pool, have them download all the gems | |
threads = Array.new(Gem.configuration.concurrent_downloads) do | |
# When a thread pops this item, it knows to stop running. The symbol | |
# is queued here so that there will be one symbol per thread. | |
download_queue << :stop | |
# 创立线程并执行下载 | |
Thread.new do | |
# The pop method will block waiting for items, so the only way | |
# to stop a thread from running is to provide a final item that | |
# means the thread should stop. | |
while req = download_queue.pop | |
break if req == :stop | |
req.spec.download options unless req.installed? | |
end | |
end | |
end | |
# 期待所有线程都执行结束,也就是 gem 下载实现 | |
threads.each(&:value) | |
# 开始装置曾经下载的 gem | |
sorted_requests.each do |req| | |
if req.installed? | |
req.spec.spec.build_extensions | |
if @always_install.none? {|spec| spec == req.spec.spec} | |
yield req, nil if block_given? | |
next | |
end | |
end | |
spec = | |
begin | |
req.spec.install options do |installer| | |
yield req, installer if block_given? | |
end | |
rescue Gem::RuntimeRequirementNotMetError => e | |
suggestion = "There are no versions of #{req.request} compatible with your Ruby & RubyGems" | |
suggestion += ". Maybe try installing an older version of the gem you're looking for?" unless @always_install.include?(req.spec.spec) | |
e.suggestion = suggestion | |
raise | |
end | |
requests << spec | |
end | |
return requests if options[:gemdeps] | |
install_hooks requests, options | |
requests | |
end |
能够发现,整个过程先是执行完被加在队列中的所有的线程工作,而后通过遍历下载的实例对象,对下载的 gem 进行装置,通过 req.sepc.install options
进行装置,这块的实现在 specification.rb
中的 Gem::Resolver::Specification
定义如下:
def install(options = {}) | |
require_relative "../installer" | |
# 获取下载的 gem | |
gem = download options | |
# 获取装置实例 | |
installer = Gem::Installer.at gem, options | |
# 回调输入 | |
yield installer if block_given? | |
# 执行装置 | |
@spec = installer.install | |
end | |
def download(options) | |
dir = options[:install_dir] || Gem.dir | |
Gem.ensure_gem_subdirectories dir | |
source.download spec, dir | |
end | |
从下面的源码能够晓得,最终装置放在了 Gem::Installer
的 install
办法中执行的。它的执行过程如下:
def install | |
# 装置查看 | |
pre_install_checks | |
# 运行执行前脚本 hook | |
run_pre_install_hooks | |
# Set loaded_from to ensure extension_dir is correct | |
if @options[:install_as_default] | |
spec.loaded_from = default_spec_file | |
else | |
spec.loaded_from = spec_file | |
end | |
# Completely remove any previous gem files | |
FileUtils.rm_rf gem_dir | |
FileUtils.rm_rf spec.extension_dir | |
dir_mode = options[:dir_mode] | |
FileUtils.mkdir_p gem_dir, :mode => dir_mode && 0o755 | |
# 默认设置装置 | |
if @options[:install_as_default] | |
extract_bin | |
write_default_spec | |
else | |
extract_files | |
build_extensions | |
write_build_info_file | |
run_post_build_hooks | |
end | |
# 生成 bin 目录可执行文件 | |
generate_bin | |
# 生成插件 | |
generate_plugins | |
unless @options[:install_as_default] | |
write_spec | |
write_cache_file | |
end | |
File.chmod(dir_mode, gem_dir) if dir_mode | |
say spec.post_install_message if options[:post_install_message] && !spec.post_install_message.nil? | |
Gem::Specification.add_spec(spec) | |
# 运行 install 的 hook 脚本 | |
run_post_install_hooks | |
spec |
这段源码中,咱们清晰的看到在执行装置的整个过程之后,又通过 generate_bin
与generate_plugins
动静生成了两个文件,对于 generate_bin
的生成过程如下:
def generate_bin # :nodoc: | |
return if spec.executables.nil? || spec.executables.empty? | |
ensure_writable_dir @bin_dir | |
spec.executables.each do |filename| | |
filename.tap(&Gem::UNTAINT) | |
bin_path = File.join gem_dir, spec.bindir, filename | |
next unless File.exist? bin_path | |
mode = File.stat(bin_path).mode | |
dir_mode = options[:prog_mode] || (mode | 0o111) | |
unless dir_mode == mode | |
require "fileutils" | |
FileUtils.chmod dir_mode, bin_path | |
end | |
# 查看是否存在同名文件被复写 | |
check_executable_overwrite filename | |
if @wrappers | |
# 生成可执行脚本 | |
generate_bin_script filename, @bin_dir | |
else | |
# 生成符号链接 | |
generate_bin_symlink filename, @bin_dir | |
end | |
end | |
end |
在通过一系列的门路判断与写入环境判断之后,通过 generate_bin_script
生成动静可执行脚本文件,到这里,是不是对对于 gem 进行装置的时候动静生成零碎可辨认的命令指令有了清晰的意识与解答。其实实质是 Ruby 在装置 gem 之后,会通过 generate_bin_script
生成可执行脚本并动静注入到零碎的环境变量中,进而可能让零碎辨认到 gem 装置的相干指令,为 gem 的性能触发提供入口。以下是generate_bin_script
的实现:
## | |
# Creates the scripts to run the applications in the gem. | |
#-- | |
# The Windows script is generated in addition to the regular one due to a | |
# bug or misfeature in the Windows shell's pipe. See | |
# https://blade.ruby-lang.org/ruby-talk/193379 | |
def generate_bin_script(filename, bindir) | |
bin_script_path = File.join bindir, formatted_program_filename(filename) | |
require "fileutils" | |
FileUtils.rm_f bin_script_path # prior install may have been --no-wrappers | |
File.open bin_script_path, "wb", 0o755 do |file| | |
file.print app_script_text(filename) | |
file.chmod(options[:prog_mode] || 0o755) | |
end | |
verbose bin_script_path | |
generate_windows_script filename, bindir | |
end | |
对于脚本具体内容的生成,这里就不再细说了,感兴趣的话能够去官网的源码中的installer.rb
中查看细节,摘取了次要内容如下:
def app_script_text(bin_file_name) | |
# NOTE: that the `load` lines cannot be indented, as old RG versions match | |
# against the beginning of the line | |
<<-TEXT | |
#{shebang bin_file_name} | |
# | |
# This file was generated by RubyGems. | |
# | |
# The application '#{spec.name}' is installed as part of a gem, and | |
# this file is here to facilitate running it. | |
# | |
require 'rubygems' | |
#{gemdeps_load(spec.name)} | |
version = "#{Gem::Requirement.default_prerelease}" | |
str = ARGV.first | |
if str | |
str = str.b[/\\A_(.*)_\\z/, 1] | |
if str and Gem::Version.correct?(str) | |
#{explicit_version_requirement(spec.name)} | |
ARGV.shift | |
end | |
end | |
if Gem.respond_to?(:activate_bin_path) | |
load Gem.activate_bin_path('#{spec.name}', '#{bin_file_name}', version) | |
else | |
gem #{spec.name.dump}, version | |
load Gem.bin_path(#{spec.name.dump}, #{bin_file_name.dump}, version) | |
end | |
TEXT | |
end | |
def gemdeps_load(name) | |
return ""if name =="bundler" | |
<<-TEXT | |
Gem.use_gemdeps | |
TEXT | |
end |
小结一下:之所以零碎可能辨认咱们装置的 gems 包命令,实质起因是 RubyGems 在进行包装置的时候,通过 generate\_bin\_script 动静的生成了可执行的脚本文件,并将其注入到了零碎的环境变量门路 Path 中。咱们通过零碎的环境变量作为疏导入口,再间接的调取 gem 安装包的具体实现,进而实现整个 gem 的性能调用。
三、CocoaPods 是如何在 Ruby 的根底上都做了本人的畛域型 DSL?
想想日常应用 cocoaPods 引入三方组件的时候,通常都在 Podfile 中进行相干的配置就行了,而在 Podfile 中的配置规定其实就是 Cocoapods 在 Ruby 的根底上提供给开发者的畛域型 DSL,该 DSL 次要针对与我的项目的依赖库治理进行畛域规定形容,由 CocoaPods 的 DSL 解析器实现规定解析,最终通过 pods 的相干命令来实现整个我的项目的库的日常治理。这么说没有什么问题,然而 Cocoapods 的底层逻辑到底是什么?也是接下来想重点探讨开掘的。
持续从简略 pod install
命令来一探到底,通过第一节的源码剖析,咱们晓得,该命令最终会转发到 cocoaPods
源码下的 install.rb
中,间接看它的 run
办法,如下:
class Install < Command | |
··· | |
def run | |
# 是否存在 podfile 文件 | |
verify_podfile_exists! | |
# 创立 installer 对象(installer_for_config 定义在基类 Command 中)installer = installer_for_config | |
# 更新仓库 | |
installer.repo_update = repo_update?(:default => false) | |
# 敞开更新 | |
installer.update = false | |
# 属性透传 | |
installer.deployment = @deployment | |
installer.clean_install = @clean_install | |
# 执行装置 | |
installer.install! | |
end | |
def installer_for_config | |
Installer.new(config.sandbox, config.podfile, config.lockfile) | |
end | |
··· | |
end |
执行装置的操作是通过 installer_for_config
办法来实现的,在办法实现中,实例了 Installer
对象,入参包含 sandbox
、podfile
、lockfile
,而这些入参均是通过 config
对象办法获取,而 podfile 的获取过程正是咱们想要理解的,所以晓得 config
的定义中央至关重要。在 command.rb
中我发现有如下的内容:
include Config::Mixin
这段代码引入了 Config::Mixin
类,而他在 Config
中的定义如下:
class Config | |
··· | |
module Mixin | |
def config | |
Config.instance | |
end | |
end | |
def self.instance | |
@instance ||= new | |
end | |
def sandbox | |
@sandbox ||= Sandbox.new(sandbox_root) | |
end | |
def podfile | |
@podfile ||= Podfile.from_file(podfile_path) if podfile_path | |
end | |
attr_writer :podfile | |
def lockfile | |
@lockfile ||= Lockfile.from_file(lockfile_path) if lockfile_path | |
end | |
def podfile_path | |
@podfile_path ||= podfile_path_in_dir(installation_root) | |
end | |
··· | |
end |
定义了一个名为 Mixin
的模块,其中蕴含一个名为 config
的办法,在该办法中实例了 Config
对象。这里定义了刚刚实例 Installer
的时候的三个入参。重点看一下 podfile
,能够看出 podfile
的实现中通过 Podfile.from_file(podfile_path)
来拿到最终的配置内容,那么对于 Podfile
的读取谜底也就在这个 from_file
办法实现中了,通过搜寻发现在Cocoapods
中的源码中并没有该办法的定义,只有以下的内容:
require 'cocoapods-core/podfile' | |
module Pod | |
class Podfile | |
autoload :InstallationOptions, 'cocoapods/installer/installation_options' | |
# @return [Pod::Installer::InstallationOptions] the installation options specified in the Podfile | |
# | |
def installation_options | |
@installation_options ||= Pod::Installer::InstallationOptions.from_podfile(self) | |
end | |
end | |
end |
能够看到这里的 class Podfile
定义的Podfile
的原始类,同时发现源码中援用了 cocoapods-core/podfile
文件,这里应该能猜想到,对于 from_file
的实现应该是在cocoapods-core/podfile
中实现的。这个资源引入是 Cocoapods
的一个外围库的组件,通过对外围库 cocoapods-core
,进行检索,发现在文件 podfile.rb
中有如下的内容:
module Pod | |
class Podfile | |
# @!group DSL support | |
include Pod::Podfile::DSL | |
··· | |
def self.from_file(path) | |
path = Pathname.new(path) | |
# 门路是否无效 | |
unless path.exist? | |
raise Informative, "No Podfile exists at path `#{path}`." | |
end | |
# 判断扩展名文件 | |
case path.extname | |
when '','.podfile','.rb' | |
# 依照 Ruby 格局解析 | |
Podfile.from_ruby(path) | |
when '.yaml' | |
# 依照 yaml 格局进行解析 | |
Podfile.from_yaml(path) | |
else | |
# 格局异样抛出 | |
raise Informative, "Unsupported Podfile format `#{path}`." | |
end | |
end | |
def self.from_ruby(path, contents = nil) | |
# 以 utf- 8 格局关上文件内容 | |
contents ||= File.open(path, 'r:utf-8', &:read) | |
# Work around for Rubinius incomplete encoding in 1.9 mode | |
if contents.respond_to?(:encoding) && contents.encoding.name != 'UTF-8' | |
contents.encode!('UTF-8') | |
end | |
if contents.tr!('“”‘’‛', %(""''')) | |
# Changes have been made | |
CoreUI.warn "Smart quotes were detected and ignored in your #{path.basename}." \ | |
'To avoid issues in the future, you should not use' \ | |
'TextEdit for editing it. If you are not using TextEdit,' \ | |
'you should turn off smart quotes in your editor of choice.' | |
end | |
# 实例 podfile 对象 | |
podfile = Podfile.new(path) do | |
# rubocop:disable Lint/RescueException | |
begin | |
# 执行 podFile 内容(执行之前会先执行 Podfile 初始化 Block 回调前的内容)eval(contents, nil, path.to_s) | |
# DSL 的异样抛出 | |
rescue Exception => e | |
message = "Invalid `#{path.basename}` file: #{e.message}" | |
raise DSLError.new(message, path, e, contents) | |
end | |
# rubocop:enable Lint/RescueException | |
end | |
podfile | |
end | |
def self.from_yaml(path) | |
string = File.open(path, 'r:utf-8', &:read) | |
# Work around for Rubinius incomplete encoding in 1.9 mode | |
if string.respond_to?(:encoding) && string.encoding.name != 'UTF-8' | |
string.encode!('UTF-8') | |
end | |
hash = YAMLHelper.load_string(string) | |
from_hash(hash, path) | |
end | |
def initialize(defined_in_file = nil, internal_hash = {}, &block) | |
self.defined_in_file = defined_in_file | |
@internal_hash = internal_hash | |
if block | |
default_target_def = TargetDefinition.new('Pods', self) | |
default_target_def.abstract = true | |
@root_target_definitions = [default_target_def] | |
@current_target_definition = default_target_def | |
instance_eval(&block) | |
else | |
@root_target_definitions = [] | |
end | |
end |
从下面的源码能够晓得,整个的 Podfile
的读取流程如下:1. 判断门路是否非法,不非法抛出异样 2. 判断扩展名类型,如果是 ”, ‘.podfile’, ‘.rb’ 扩大依照 ruby
语法规定解析,如果是 yaml
则依照 yaml
文件格式解析,以上两者如果都不是,则抛出格局解析异样 3. 如果解析依照 Ruby
格局解析的话过程如下:
• 依照 utf-8
格局读取 Podfile
文件内容,并存储到 contents
中
• 内容符号容错解决,次要波及 ”“”‘’‛” 等 符号,同时输入正告信息
• 实例 Podfile
对象,同时在实例过程中初始化 TargetDefinition
对象并配置默认的Target
信息
• 最终通过 eval(contents, nil, path.to_s)
办法执行 Podfile
文件内容实现配置记录
这里或者有一个疑难:Podfile 外面定义了 Cocoapods
本人的一套 DSL 语法
,那么执行过程中是如何解析DSL 语法
的呢?下面的源码文件中,如果认真查看的话,会发现有上面这一行内容:
include Pod::Podfile::DSL
不错,这就是 DSL 解析
的本体,其实你能够将 DSL 语法
了解为基于 Ruby
定义的一系列的畛域型办法,DSL 的解析的过程实质是 定义的办法执行的过程
。在Cocoapods
中定义了很多 DSL 语法
,定义与实现均放在了 cocoapods-core
这个外围组件中,比方在dsl.rb
文件中的以下对于Podfile
的 DSL
定义(摘取局部):
module Pod | |
class Podfile | |
module DSL | |
def install!(installation_method, options = {}) | |
unless current_target_definition.root? | |
raise Informative, 'The installation method can only be set at the root level of the Podfile.' | |
end | |
set_hash_value('installation_method', 'name' => installation_method, 'options' => options) | |
end | |
def pod(name = nil, *requirements) | |
unless name | |
raise StandardError, 'A dependency requires a name.' | |
end | |
current_target_definition.store_pod(name, *requirements) | |
end | |
def podspec(options = nil) | |
current_target_definition.store_podspec(options) | |
end | |
def target(name, options = nil) | |
if options | |
raise Informative, "Unsupported options `#{options}` for" \ | |
"target `#{name}`." | |
end | |
parent = current_target_definition | |
definition = TargetDefinition.new(name, parent) | |
self.current_target_definition = definition | |
yield if block_given? | |
ensure | |
self.current_target_definition = parent | |
end | |
def inherit!(inheritance) | |
current_target_definition.inheritance = inheritance | |
end | |
def platform(name, target = nil) | |
# Support for deprecated options parameter | |
target = target[:deployment_target] if target.is_a?(Hash) | |
current_target_definition.set_platform!(name, target) | |
end | |
def project(path, build_configurations = {}) | |
current_target_definition.user_project_path = path | |
current_target_definition.build_configurations = build_configurations | |
end | |
def xcodeproj(*args) | |
CoreUI.warn '`xcodeproj` was renamed to `project`. Please update your Podfile accordingly.' | |
project(*args) | |
end | |
....... | |
end | |
end |
看完 DSL 的定义实现
是不是有种相熟的滋味,对于应用 Cocoapods
的使用者而言,在没有接触 Ruby
的状况下,仍旧可能通过对 Podfile
的简略配置来实现三方库的治理依赖,不仅应用的学习成本低,而且可能很容易的上手,之所以可能这么便捷,就体现出了 DSL
的魅力所在。
对于 ** 畛域型语言 **
的计划选用在很多不同的业务畛域中都有了相干的利用,它对特定的 ** 业务畛域场景 **
可能提供 ** 高效简洁 **
的实现计划,对使用者敌对的同时,也能提供高质量的畛域能力。**cocoapods**
就是借助 Ruby 弱小的面向对象的脚本能力实现 **Cocoa 库 **
治理的实现,有种移花接木的感觉,为使用者提供了畛域性语言,让其更简略更高效,尤其是使用者并没有感知到其本质是 **Ruby**
。 记得一开始应用 Cocoapods
的时候,已经一度认为它是一种新的语言,当初看来都是 Cocoapods 的 DSL
所给咱们的错觉,毕竟应用起来切实是太香了。
作者:京东批发 李臣臣
起源:京东云开发者社区 转载请注明起源