对于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 的环境配置,通过以下的命令:
通过以上的命令,可以看到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
复制代码 可以发现最终通过执行 Gem::GemRunner.new.run args 来完成安装,显然安装的过程就在 Gem::GemRunner 类中。依旧查看RubyGems的源码,在 gem_runner.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
复制代码 可以看出来命令的执行最终转到了 cmd.run Gem.configuration.args, build_args 的方法调用上,cmd是通过 @command_manager_class 进行装饰的类,找到其装饰的地方如下:- 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
复制代码 发现是它其实 Gem::CommandManager 类,接着查看一下 CommandManager 的 run 方法实现,在文件 command_manager.rb 中 ,有以下的实现内容:- 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
复制代码 通过以上的源码,可以发现命令的执行,通过调用 process_args 执行,然后在 process_args 方法中进行判断命令参数,接着通过 invoke_command 来执行命令。在 invoke_command 内部,首先通过find_command 查找命令,这里find_command 主要负责查找命令相关的执行对象,需要注意的地方在以下这句:- #!/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 install 命令)?这里的 load_and_instantiate(command_name) 的方法其实就是查找实体的具体操作,在实现中通过以下的语句来获取最终的常量的命令指令实体:- 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
复制代码 上面的语句是通过 Gem::Commands 查找类中的常量,这里的常量其实就是对应gem相关的一个个指令,在gem中声明了很多命令的常量,他们继承自 Gem::Command 基类,同时实现了抽象方法 execute,这一点很重要。比如在 install_command.rb 中定义了命令 gem install 的具体的实现:- def initialize
- @command_manager_class = Gem::CommandManager
- @config_file_class = Gem::ConfigFile
- end
复制代码 在 invoke_command 方法中,最终通过 invoke_with_build_args 来最终执行命令,该方法定义Gem::Command中,在 command.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
复制代码 可以看出来,最终基类中的 invoke_with_build_args 中调用了抽象方法 execute 来完成命令的运行调用。在rubyGems里面声明了很多变量,这些变量在 CommandManager 中通过 run 方法进行命令常量实体的查找,最终通过调用继承自 Gem:Command 子类的 execute 完成相关指令的执行。在rubyGems中可以看到很多变量,一个变量对应一个命令,如下所示:
到这里,我们基本可以知道整个gem命令的查找到调用的整个流程。那么 gem install 的过程中又是如何自动生成并注册相关的gem命令到系统环境变量中的呢?基于上面的命令查找调用流程,其实只需要在 install_command.rb 中查看 execute 具体的实现就清楚了,如下:- @commands[command_name] ||= load_and_instantiate(command_name)
复制代码 可以看出,最终通过request_set.install 来完成最终的gem安装,而request_set 是Gem::RequestSet 的实例对象,接着在 request_set.rb 中查看相关的实现:- Gem::Commands.const_get(const_name).new
复制代码 从上面的源码可以知道,整个的 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语法的呢?上面的源码文件中,如果仔细查看的话,会发现有下面这一行内容:- 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
复制代码 不错,这就是DSL解析的本体,其实你可以将DSL语法理解为基于Ruby定义的一系列的领域型方法,DSL的解析的过程本质是定义的方法执行的过程。在Cocoapods中定义了很多DSL语法,定义与实现均放在了 cocoapods-core 这个核心组件中,比如在dsl.rb 文件中的以下关于Podfile的 DSL定义(摘取部分):- 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
复制代码 看完 DSL的定义实现是不是有种熟悉的味道,对于使用Cocoapods的使用者而言,在没有接触Ruby的情况下,依旧能够通过对Podfile的简单配置来实现三方库的管理依赖,不仅使用的学习成本低,而且能够很容易的上手,之所以能够这么便捷,就体现出了DSL的魅力所在。
对于**领域型语言**的方案选用在很多不同的业务领域中都有了相关的应用,它对特定的**业务领域场景**能够提供**高效简洁**的实现方案,对使用者友好的同时,也能提供高质量的领域能力。**cocoapods**就是借助Ruby强大的面向对象的脚本能力完成**Cocoa库**管理的实现,有种偷梁换柱的感觉,为使用者提供了领域性语言,让其更简单更高效,尤其是使用者并没有感知到其本质是**Ruby**。记得一开始使用Cocoapods的时候,曾经一度以为它是一种新的语言,现在看来都是Cocoapods的DSL所给我们的错觉,毕竟使用起来实在是太香了。
作者:京东零售 李臣臣
来源:京东云开发者社区 转载请注明来源
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |