关于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完毕中完毕各个指令行指令的完毕
以上的13阶段实际上是Ruby的指令转发进程,毕竟将指令转发给了对应的gems包进行毕竟的处理。而45则是整个的处理进程。一同在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
所给我们的错觉,毕竟运用起来实在是太香了。
作者:京东零售 李臣臣
来历:京东云开发者社区 转载请注明来历