對于iOS開發(fā)者而言,cocoaPods并不陌生,通過pod相關(guān)的命令操作,就可以很方便的將項目中用到的三方依賴庫資源集成到項目環(huán)境中,大大的提升了開發(fā)的效率。CocoaPods作為iOS項目的包管理工具,它在命令行背后做了什么操作?而又是通過什么樣的方式將命令指令聲明出來供我們使用的?這些實現(xiàn)的背后底層邏輯是什么?都是本文想要探討挖掘的。
一、ruby是如何讓系統(tǒng)能夠識別已經(jīng)安裝的Pods指令的?
我們都知道在使用CocoaPods管理項目三方庫之前,需要安裝Ruby環(huán)境,同時基于Ruby的包管理工具gem再去安裝CocoaPods。通過安裝過程可以看出來,CocoaPods本質(zhì)就是Ruby的一個gem包。而安裝Cocoapods的時候,使用了以下的安裝命令:
sudo gem install cocoapods
安裝完成之后,就可以使用基于Cocoapods的 pod xxxx 相關(guān)命令了。gem install xxx 到底做了什么也能讓 Terminal 正常的識別 pod 命令?gem的工作原理又是什么?了解這些之前,可以先看一下 RubyGems 的環(huán)境配置,通過以下的命令:
gem environment
??通過以上的命令,可以看到Ruby的版本信息,RubyGem的版本,以及gems包安裝的路徑,進入安裝路徑 /Library/Ruby/Gems/2.6.0 后,我們能看到當前的Ruby環(huán)境下所安裝的擴展包,這里能看到我們熟悉的Cocoapods相關(guān)的功能包。除了安裝包路徑之外,還有一個 EXECUTABLE DIRECTORY 執(zhí)行目錄 /usr/local/bin,可以看到擁有可執(zhí)行權(quán)限的pod文件,如下:
??預覽一下pod文件內(nèi)容:
#!/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.firstif str str = str.b[/A_(.*)_z/, 1] if str and Gem::Version.correct?(str) version = str ARGV.shift endendif Gem.respond_to?(:activate_bin_path)load Gem.activate_bin_path('cocoapods', 'pod', version)elsegem "cocoapods", versionload Gem.bin_path("cocoapods", "pod", version)end
根據(jù)文件注釋內(nèi)容可以發(fā)現(xiàn),當前的可執(zhí)行文件是 RubyGems 在安裝 Cocoapods 的時候自動生成的,同時會將當前的執(zhí)行文件放到系統(tǒng)的環(huán)境變量路徑中,也即存放到了 /usr/local/bin 中了,這也就解釋了為什么我們通過gem安裝cocoapods之后,就立馬能夠識別pod可執(zhí)行環(huán)境了。
雖然能夠識別pod可執(zhí)行文件,但是具體的命令參數(shù)是如何進行識別與實現(xiàn)呢?繼續(xù)看以上的pod的文件源碼,會發(fā)現(xiàn)最終都指向了 Gem 的 activate_bin_path 與 bin_path 方法,為了搞清楚Gem到底做了什么,在官方的RubyGems源碼的rubygems.rb 文件中找到了兩個方法的相關(guān)定義與實現(xiàn),摘取了主要的幾個方法實現(xiàn),內(nèi)容如下:
## # 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) #如果沒有提供可執(zhí)行文件的名稱,則拋出異常 raise ArgumentError, "you must supply exec_name" unless exec_name # 創(chuàng)建一個Dependency對象 dep = Gem::Dependency.new name, requirements # 獲取已經(jīng)加載的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 執(zhí)行名字,如果匹配結(jié)束查找 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 #返回結(jié)果 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中可執(zhí)行文件 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 endendclass 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 endend
通過當前的實現(xiàn)可以看出在兩個方法實現(xiàn)中,通過 find_spec_for_exe 方法依據(jù)名稱name查找sepc對象,匹配成功之后返回sepc對象,最終通過spec對象中的bin_file方法來進行執(zhí)行相關(guān)的命令。以下為gems安裝的配置目錄集合:
??注:bin_file 方法的實現(xiàn)方式取決于 gem 包的類型和所使用的操作系統(tǒng)。在大多數(shù)情況下,它會根據(jù)操作系統(tǒng)的不同,使用不同的查找算法來確定二進制文件的路徑。例如,在Windows上,它會搜索 gem包的 bin 目錄,而在 Unix 上,它會搜索 gem 包的 bin目錄和 PATH 環(huán)境變量中的路徑。
通過當前的實現(xiàn)可以看出在兩個方法實現(xiàn)中,find_spec_for_exe 方法會遍歷所有已安裝的 gem 包,查找其中包含指定可執(zhí)行文件的 gem 包。如果找到了匹配的 gem 包,則會返回該 gem 包的 Gem::Specification 對象,并調(diào)用其 bin_file 方法獲取二進制文件路徑。而 bin_file 是在 Gem::Specification 類中定義的。它是一個實例方法,用于查找與指定的可執(zhí)行文件 exec_name 相關(guān)聯(lián)的 gem 包的二進制文件路徑,定義實現(xiàn)如下:
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命令本質(zhì)是執(zhí)行了RubyGems 的 find_spec_for_exe 方法,用來查找并執(zhí)行g(shù)ems安裝目錄下的bin目錄,也即是 /Library/Ruby/Gems/2.6.0 目錄下的gem包下的bin目錄。而針對于pod的gem包,如下所示:
??至此,可以發(fā)現(xiàn),由系統(tǒng)執(zhí)行環(huán)境 /usr/local/bin 中的可執(zhí)行文件 pod 引導觸發(fā),Ruby通過 Gem.bin_path("cocoapods", "pod", version) 與 Gem.activate_bin_path('cocoapods', 'pod', version) 進行轉(zhuǎn)發(fā),再到gems包安裝目錄的gem查找方法 find_spec_for_exe,最終轉(zhuǎn)到gems安裝包下的bin目錄的執(zhí)行文件進行命令的最終執(zhí)行,流程大致如下:
??而對于pod的命令又是如何進行識別區(qū)分的呢?剛剛的分析可以看出對于gems安裝包的bin下的執(zhí)行文件才是最終的執(zhí)行內(nèi)容,打開cocoapod的bin目錄下的pod可執(zhí)行文件,如下:
#!/usr/bin/env rubyif 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 endendif $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'endSTDOUT.sync = true if ENV['CP_STDOUT_SYNC'] == 'TRUE'require 'cocoapods'# 環(huán)境變量判斷是否配置了profile_filename,如果配置了按照配置內(nèi)容生成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) endelse Pod::Command.run(ARGV)end
可以發(fā)現(xiàn),pod命令參數(shù)的解析運行是通過 Pod::Command.run(ARGV) 實現(xiàn)的。通過該線索,我們接著查看Pod庫源碼的Command類的run方法都做了什么?該類在官方源碼的lib/cocoapods/command.rb 定義的,摘取了部分內(nèi)容如下:
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、檢查當前系統(tǒng)上安裝的 Git 版本是否符合最低要求 3、檢查當前系統(tǒng)上的 Xcode 許可是否已經(jīng)授權(quán)
如果都沒有問題,則會調(diào)用父類的 run 方法,而命令的解析可以看出來應該是在其父類 CLAide::Command 進行的,CLAide 是 CocoaPods的命令行解析庫,在 command.rb 文件中,可以找到如下 Command 類的實現(xiàn):
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 # 轉(zhuǎn)換成ARGV對象 argv = ARGV.coerce(argv) # 處理有效命令行參數(shù) command = parse(argv) ANSI.disabled = !command.ansi_output? unless command.handle_root_options(argv) # 命令處理 command.validate! # 運行命令(由子類進行繼承實現(xiàn)運行) 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) # 移除第一個參數(shù) argv.shift_argument # 解析子命令 subcommand.parse(argv) # 不能執(zhí)行的命令直接加載默認命令 elsif abstract_command? && default_subcommand load_default_subcommand(argv) # 無內(nèi)容則創(chuàng)建一個comand實例返回 else new(argv) end end # 抽象方法,由其子類進行實現(xiàn) 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 轉(zhuǎn)換為 ARGV 對象(ARGV 是一個 Ruby 內(nèi)置的全局變量,它是一個數(shù)組,包含了從命令行傳遞給 Ruby 程序的參數(shù)。例如:ARGV[0] 表示第一個參數(shù),ARGV[1] 表示第二個參數(shù),以此類推),然后獲取第一個參數(shù)作為命令名稱 cmd。如果 cmd 存在,并且能夠找到對應的子命令 subcommand,則將 argv 中的第一個參數(shù)移除,并調(diào)用 subcommand.parse(argv) 方法解析剩余的參數(shù)。如果沒有指定命令或者找不到對應的子命令,但當前命令是一個抽象命令(即不能直接執(zhí)行),并且有默認的子命令,則加載默認子命令并解析參數(shù)。否則,創(chuàng)建一個新的實例,并將 argv 作為參數(shù)傳遞給它。
最終在轉(zhuǎn)換完成之后,通過調(diào)用抽象方法run 調(diào)用子類的實現(xiàn)來執(zhí)行解析后的指令內(nèi)容。到這里,順其自然的就想到了Cocoapods的相關(guān)指令實現(xiàn)必然繼承自了CLAide::Command 類,并實現(xiàn)了其抽象方法 run。為了驗證這個推斷,我們接著看Cocoapods的源碼,在文件 Install.rb 中,有這個 Install 類的定義與實現(xiàn),摘取了核心內(nèi)容:
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 # 實現(xiàn)CLAide::Command 的抽象方法 def run # 驗證工程目錄podfile 是否存在 verify_podfile_exists! # 獲取installer對象 installer = installer_for_config # 更新pods倉庫 installer.repo_update = repo_update?(:default => false) # 設(shè)置更新標識為關(guān)閉 installer.update = false # 透傳依賴設(shè)置 installer.deployment = @deployment # 透傳設(shè)置 installer.clean_install = @clean_install installer.install! end end endend
通過源碼可以看出,cocoaPods的命令解析是通過自身的 CLAide::Command 進行解析處理的,而最終的命令實現(xiàn)則是通過繼承自 Command 的子類,通過實現(xiàn)抽象方法 run 來實現(xiàn)的具體命令功能的。到這里,關(guān)于Pod 命令的識別以及Pod 命令的解析與運行是不是非常清晰了。
階段性小結(jié)一下,我們在Terminal中進行pod命令運行的過程中,背后都經(jīng)歷了哪些過程?整個運行過程可以簡述如下: 1、通過Gem生成在系統(tǒng)環(huán)境目錄下的可執(zhí)行文件 pod,通過該文件引導 RubyGems 查找 gems包目錄下的sepc配置對象,也即是cocoaPods的sepc配置對象 2、查找到配置對象,通過bin_file方法查找cocoaPods包路徑中bin下的可執(zhí)行文件 3、運行rubygems對應cocoaPods的gem安裝包目錄中bin下的二進制可執(zhí)行文件pod 4、通過執(zhí)行 Pod::Command.run(ARGV) 解析命令與參數(shù)并找出最終的 Command 對象執(zhí)行其run方法 5、在繼承自Command的子類的run實現(xiàn)中完成各個命令行指令的實現(xiàn)
以上的1~3階段實際上是Ruby的指令轉(zhuǎn)發(fā)過程,最終將命令轉(zhuǎn)發(fā)給了對應的gems包進行最終的處理。而4~5則是整個的處理過程。同時在Cocoapods的源碼實現(xiàn)中,可以發(fā)現(xiàn)每個命令都對應一個 Ruby 類,該類繼承自 CLAide::Command 類。通過繼承當前類,可以定義該命令所支持的選項和參數(shù),并在執(zhí)行命令時解析這些選項和參數(shù)。
二、Ruby 是如何動態(tài)生成可執(zhí)行文件并集成到系統(tǒng)環(huán)境變量中的?
剛剛在上一節(jié)賣了個關(guān)子,在安裝完成Ruby的gem包之后,在系統(tǒng)環(huán)境變量中就自動生成了相關(guān)的可執(zhí)行文件命令。那么Ruby在這個過程中又做了什么呢?既然是在gem安裝的時候會動態(tài)生成,不如就以gem的安裝命令 sudo gem install xxx 作為切入點去看相關(guān)的處理過程。我們進入系統(tǒng)環(huán)境變量路徑 /usr/bin 找到 Gem 可執(zhí)行二進制文件,如下:
??打開gem,它的內(nèi)容如下:
#!/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}"endargs = ARGV.clonebegin Gem::GemRunner.new.run argsrescue Gem::SystemExitException => e exit e.exit_codeend
可以發(fā)現(xiàn)最終通過執(zhí)行 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
可以看出來命令的執(zhí)行最終轉(zhuǎn)到了 cmd.run Gem.configuration.args, build_args 的方法調(diào)用上,cmd是通過 @command_manager_class 進行裝飾的類,找到其裝飾的地方如下:
def initialize @command_manager_class = Gem::CommandManager @config_file_class = Gem::ConfigFileend
發(fā)現(xiàn)是它其實 Gem::CommandManager 類,接著查看一下 CommandManager 的 run 方法實現(xiàn),在文件 command_manager.rb 中 ,有以下的實現(xiàn)內(nèi)容:
## # 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) # 空參數(shù)退出執(zhí)行 if args.empty? say Gem::Command::HELP terminate_interaction 1 end # 判斷第一個參數(shù) 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 # 執(zhí)行命令 invoke_command(args, build_args) end end def invoke_command(args, build_args) cmd_name = args.shift.downcase # 查找指令,并獲取繼承自 Gem::Commands的實體子類(實現(xiàn)了excute抽象方法) cmd = find_command cmd_name cmd.deprecation_warning if cmd.deprecated? # 執(zhí)行 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 # 這里的[] 是方法調(diào)用,定義在下面 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? # 調(diào)用 `load_and_instantiate` 方法來完成這個過程,并將返回的對象存儲到 `@commands` 哈希表中,這里 ||= 是默認值內(nèi)容,類似于OC中的?: @commands[command_name] ||= load_and_instantiate(command_name) end # 命令分發(fā)選擇以及動態(tài)實例 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})nt#{e}") ui.backtrace e end end
通過以上的源碼,可以發(fā)現(xiàn)命令的執(zhí)行,通過調(diào)用 process_args 執(zhí)行,然后在 process_args 方法中進行判斷命令參數(shù),接著通過 invoke_command 來執(zhí)行命令。在 invoke_command 內(nèi)部,首先通過find_command 查找命令,這里find_command 主要負責查找命令相關(guān)的執(zhí)行對象,需要注意的地方在以下這句:
@commands[command_name] ||= load_and_instantiate(command_name)
通過以上的操作,返回當前命令執(zhí)行的實體對象,而對應的腳本匹配又是如何實現(xiàn)的呢(比如輸入的命令是 gem install 命令)?這里的 load_and_instantiate(command_name) 的方法其實就是查找實體的具體操作,在實現(xiàn)中通過以下的語句來獲取最終的常量的命令指令實體:
Gem::Commands.const_get(const_name).new
上面的語句是通過 Gem::Commands 查找類中的常量,這里的常量其實就是對應gem相關(guān)的一個個指令,在gem中聲明了很多命令的常量,他們繼承自 Gem::Command 基類,同時實現(xiàn)了抽象方法 execute,這一點很重要。比如在 install_command.rb 中定義了命令 gem install 的具體的實現(xiàn):
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 來最終執(zhí)行命令,該方法定義Gem::Command中,在 command.rb 文件中,可以看到內(nèi)容如下:
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 # 子類實現(xiàn)該抽象完成命令的具體實現(xiàn) def execute raise Gem::Exception, "generic command has no actions" end
可以看出來,最終基類中的 invoke_with_build_args 中調(diào)用了抽象方法 execute 來完成命令的運行調(diào)用。在rubyGems里面聲明了很多變量,這些變量在 CommandManager 中通過 run 方法進行命令常量實體的查找,最終通過調(diào)用繼承自 Gem:Command 子類的 execute 完成相關(guān)指令的執(zhí)行。在rubyGems中可以看到很多變量,一個變量對應一個命令,如下所示:
?到這里,我們基本可以知道整個gem命令的查找到調(diào)用的整個流程。那么 gem install 的過程中又是如何自動生成并注冊相關(guān)的gem命令到系統(tǒng)環(huán)境變量中的呢?基于上面的命令查找調(diào)用流程,其實只需要在 install_command.rb 中查看 execute 具體的實現(xiàn)就清楚了,如下:
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 enddef 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}:nt#{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 中查看相關(guān)的實現(xiàn):
## # 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 = [] # 創(chuàng)建下載隊列 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 # 創(chuàng)建線程并執(zhí)行下載 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 # 等待所有線程都執(zhí)行完畢,也就是gem下載完成 threads.each(&:value) # 開始安裝已經(jīng)下載的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
可以發(fā)現(xiàn),整個過程先是執(zhí)行完被加在隊列中的所有的線程任務,然后通過遍歷下載的實例對象,對下載的gem進行安裝,通過 req.sepc.install options 進行安裝,這塊的實現(xiàn)在 specification.rb 中的 Gem::Resolver::Specification 定義如下:
def install(options = {}) require_relative "../installer" # 獲取下載的gem gem = download options # 獲取安裝實例 installer = Gem::Installer.at gem, options # 回調(diào)輸出 yield installer if block_given? # 執(zhí)行安裝 @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 方法中執(zhí)行的。它的執(zhí)行過程如下:
def install # 安裝檢查 pre_install_checks # 運行執(zhí)行前腳本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 # 默認設(shè)置安裝 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目錄可執(zhí)行文件 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
這段源碼中,我們清晰的看到在執(zhí)行安裝的整個過程之后,又通過 generate_bin 與generate_plugins 動態(tài)生成了兩個文件,對于 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 # 生成可執(zhí)行腳本 generate_bin_script filename, @bin_dir else # 生成符號鏈接 generate_bin_symlink filename, @bin_dir end end end
在經(jīng)過一系列的路徑判斷與寫入環(huán)境判斷之后,通過 generate_bin_script 生成動態(tài)可執(zhí)行腳本文件,到這里,是不是對關(guān)于gem進行安裝的時候動態(tài)生成系統(tǒng)可識別的命令指令有了清晰的認識與解答。其實本質(zhì)是Ruby在安裝gem之后,會通過 generate_bin_script 生成可執(zhí)行腳本并動態(tài)注入到系統(tǒng)的環(huán)境變量中,進而能夠讓系統(tǒng)識別到gem安裝的相關(guān)指令,為gem的功能觸發(fā)提供入口。以下是generate_bin_script 的實現(xiàn):
## # 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
關(guān)于腳本具體內(nèi)容的生成,這里就不再細說了,感興趣的話可以去官方的源碼中的installer.rb 中查看細節(jié),摘取了主要內(nèi)容如下:
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.firstif str str = str.b[/A_(.*)_z/, 1] if str and Gem::Version.correct?(str) #{explicit_version_requirement(spec.name)} ARGV.shift endendif Gem.respond_to?(:activate_bin_path)load Gem.activate_bin_path('#{spec.name}', '#{bin_file_name}', version)elsegem #{spec.name.dump}, versionload Gem.bin_path(#{spec.name.dump}, #{bin_file_name.dump}, version)endTEXT end def gemdeps_load(name) return "" if name == "bundler" <<-TEXTGem.use_gemdepsTEXT end
小結(jié)一下:之所以系統(tǒng)能夠識別我們安裝的gems包命令,本質(zhì)原因是RubyGems在進行包安裝的時候,通過 generate_bin_script 動態(tài)的生成了可執(zhí)行的腳本文件,并將其注入到了系統(tǒng)的環(huán)境變量路徑Path中。我們通過系統(tǒng)的環(huán)境變量作為引導入口,再間接的調(diào)取gem安裝包的具體實現(xiàn),進而完成整個gem的功能調(diào)用。
三、CocoaPods是如何在Ruby的基礎(chǔ)上都做了自己的領(lǐng)域型DSL?
想想日常使用cocoaPods引入三方組件的時候,通常都在Podfile中進行相關(guān)的配置就行了,而在Podfile中的配置規(guī)則其實就是Cocoapods在Ruby的基礎(chǔ)上提供給開發(fā)者的領(lǐng)域型DSL,該DSL主要針對與項目的依賴庫管理進行領(lǐng)域規(guī)則描述,由CocoaPods的DSL解析器完成規(guī)則解析,最終通過pods的相關(guān)命令來完成整個項目的庫的日常管理。這么說沒有什么問題,但是Cocoapods的底層邏輯到底是什么?也是接下來想重點探討挖掘的。
繼續(xù)從簡單 pod install 命令來一探究竟,通過第一節(jié)的源碼分析,我們知道,該命令最終會轉(zhuǎn)發(fā)到 cocoaPods 源碼下的 install.rb中,直接看它的 run方法,如下:
class Install < Command··· def run # 是否存在podfile文件 verify_podfile_exists! # 創(chuàng)建installer對象(installer_for_config定義在基類Command中) installer = installer_for_config # 更新倉庫 installer.repo_update = repo_update?(:default => false) # 關(guān)閉更新 installer.update = false # 屬性透傳 installer.deployment = @deployment installer.clean_install = @clean_install # 執(zhí)行安裝 installer.install! end def installer_for_config Installer.new(config.sandbox, config.podfile, config.lockfile) end··· end
執(zhí)行安裝的操作是通過 installer_for_config 方法來完成的,在方法實現(xiàn)中,實例了 Installer 對象,入?yún)?sandbox 、podfile 、lockfile ,而這些入?yún)⒕峭ㄟ^ config 對象方法獲取,而podfile的獲取過程正是我們想要了解的,所以知道 config 的定義地方至關(guān)重要。在 command.rb 中我發(fā)現(xiàn)有如下的內(nèi)容:
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 的時候的三個入?yún)ⅰV攸c看一下 podfile,可以看出 podfile 的實現(xiàn)中通過 Podfile.from_file(podfile_path) 來拿到最終的配置內(nèi)容,那么關(guān)于Podfile 的讀取謎底也就在這個 from_file 方法實現(xiàn)中了,通過搜索發(fā)現(xiàn)在Cocoapods中的源碼中并沒有該方法的定義,只有以下的內(nèi)容:
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 endend
可以看到這里的class Podfile 定義的Podfile 的原始類,同時發(fā)現(xiàn)源碼中引用了 cocoapods-core/podfile 文件,這里應該能猜想到,關(guān)于 from_file 的實現(xiàn)應該是在cocoapods-core/podfile 中完成的。這個資源引入是 Cocoapods的一個核心庫的組件,通過對核心庫 cocoapods-core,進行檢索,發(fā)現(xiàn)在文件 podfile.rb 中有如下的內(nèi)容:
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格式打開文件內(nèi)容 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 # 執(zhí)行podFile內(nèi)容(執(zhí)行之前會先執(zhí)行Podfile初始化Block回調(diào)前的內(nèi)容) 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 語法規(guī)則解析,如果是yaml則按照 yaml 文件格式解析,以上兩者如果都不是,則拋出格式解析異常 3. 如果解析按照 Ruby 格式解析的話過程如下:
?按照utf-8格式讀取 Podfile 文件內(nèi)容,并存儲到 contents 中
?內(nèi)容符號容錯處理,主要涉及" “”‘’?" 等 符號,同時輸出警告信息
?實例 Podfile 對象,同時在實例過程中初始化 TargetDefinition 對象并配置默認的Target 信息
?最終通過 eval(contents, nil, path.to_s) 方法執(zhí)行 Podfile 文件內(nèi)容完成配置記錄
這里或許有一個疑問:Podfile里面定義了 Cocoapods 自己的一套DSL語法,那么執(zhí)行過程中是如何解析DSL語法的呢?上面的源碼文件中,如果仔細查看的話,會發(fā)現(xiàn)有下面這一行內(nèi)容:
include Pod::Podfile::DSL
不錯,這就是DSL解析的本體,其實你可以將DSL語法理解為基于Ruby定義的一系列的領(lǐng)域型方法,DSL的解析的過程本質(zhì)是定義的方法執(zhí)行的過程。在Cocoapods中定義了很多DSL語法,定義與實現(xiàn)均放在了 cocoapods-core 這個核心組件中,比如在dsl.rb 文件中的以下關(guān)于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 ....... endend
看完 DSL的定義實現(xiàn)是不是有種熟悉的味道,對于使用Cocoapods的使用者而言,在沒有接觸Ruby的情況下,依舊能夠通過對Podfile的簡單配置來實現(xiàn)三方庫的管理依賴,不僅使用的學習成本低,而且能夠很容易的上手,之所以能夠這么便捷,就體現(xiàn)出了DSL的魅力所在。
對于領(lǐng)域型語言的方案選用在很多不同的業(yè)務領(lǐng)域中都有了相關(guān)的應用,它對特定的業(yè)務領(lǐng)域場景能夠提供高效簡潔的實現(xiàn)方案,對使用者友好的同時,也能提供高質(zhì)量的領(lǐng)域能力。cocoapods就是借助Ruby強大的面向?qū)ο蟮哪_本能力完成Cocoa庫管理的實現(xiàn),有種偷梁換柱的感覺,為使用者提供了領(lǐng)域性語言,讓其更簡單更高效,尤其是使用者并沒有感知到其本質(zhì)是Ruby。記得一開始使用Cocoapods的時候,曾經(jīng)一度以為它是一種新的語言,現(xiàn)在看來都是Cocoapods的DSL所給我們的錯覺,畢竟使用起來實在是太香了。
作者:京東零售 李臣臣
來源:京東云開發(fā)者社區(qū) 轉(zhuǎn)載請注明來源
版權(quán)聲明:本文內(nèi)容由互聯(lián)網(wǎng)用戶自發(fā)貢獻,該文觀點僅代表作者本人。本站僅提供信息存儲空間服務,不擁有所有權(quán),不承擔相關(guān)法律責任。如發(fā)現(xiàn)本站有涉嫌抄襲侵權(quán)/違法違規(guī)的內(nèi)容, 請發(fā)送郵件至 舉報,一經(jīng)查實,本站將立刻刪除。