对于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.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

依据文件正文内容能够发现,以后的可执行文件是 RubyGems 在装置 Cocoapods 的时候主动生成的,同时会将以后的执行文件放到零碎的环境变量门路中,也即寄存到了 /usr/local/bin 中了,这也就解释了为什么咱们通过gem装置cocoapods之后,就立马可能辨认pod可执行环境了。

尽管可能辨认pod可执行文件,然而具体的命令参数是如何进行辨认与实现呢?持续看以上的pod的文件源码,会发现最终都指向了 Gemactivate_bin_pathbin_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  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 

通过以后的实现能够看出在两个办法实现中,通过 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 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'# 环境变量判断是否配置了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)  endelse  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 进行的,CLAideCocoaPods的命令行解析库,在 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  endend

通过源码能够看出,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}"endargs = ARGV.clonebegin  Gem::GemRunner.new.run argsrescue Gem::SystemExitException => e  exit e.exit_codeend

能够发现最终通过执行 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::ConfigFileend

发现是它其实 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  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}:\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_setGem::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::Installerinstall 办法中执行的。它的执行过程如下:

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_bingenerate_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.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
小结一下:之所以零碎可能辨认咱们装置的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 对象,入参包含 sandboxpodfilelockfile ,而这些入参均是通过 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  endend

能够看到这里的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 文件中的以下对于PodfileDSL定义(摘取局部):

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的定义实现是不是有种相熟的滋味,对于应用Cocoapods的使用者而言,在没有接触Ruby的状况下,仍旧可能通过对Podfile的简略配置来实现三方库的治理依赖,不仅应用的学习成本低,而且可能很容易的上手,之所以可能这么便捷,就体现出了DSL的魅力所在。

对于**畛域型语言**的计划选用在很多不同的业务畛域中都有了相干的利用,它对特定的**业务畛域场景**可能提供**高效简洁**的实现计划,对使用者敌对的同时,也能提供高质量的畛域能力。**cocoapods**就是借助Ruby弱小的面向对象的脚本能力实现**Cocoa库**治理的实现,有种移花接木的感觉,为使用者提供了畛域性语言,让其更简略更高效,尤其是使用者并没有感知到其本质是**Ruby**记得一开始应用Cocoapods的时候,已经一度认为它是一种新的语言,当初看来都是Cocoapods的DSL所给咱们的错觉,毕竟应用起来切实是太香了。

作者:京东批发 李臣臣

起源:京东云开发者社区 转载请注明起源