Command 基类在 lib/claide/command.rb 中,提供了大量基础功能,包括 run 、 options、 help 等。当每次执行 pod xxx 命令时候,会执行 bin 目录下的可执行文件 pod:
require 'cocoapods'if profile_filename = ENV['PROFILE']
# 忽略不相关内容...else
Pod::Command.run(ARGV)
end
这里实际上是 Pod 模块从 CLAide 继承子类 Command < CLAide::Command,执行 Pod 命令时候,就会调用:
def self.run(argv)
help! 'You cannot run CocoaPods as root.' if Process.uid ==0
verify_minimum_git_version!
verify_xcode_license_approved!super(argv)
ensure
UI.print_warnings
end
这里最核心的就是 cocoapods_plugin.rb,前面分析过,执行 pod 命令时候会主动加载所有 cocoapods_plugin.rb 文件,那么只要将需要扩展的类加到这里面,执行命令时候就会生效:
class Test < Command
self.summary = 'Short description of cocoapods-test.'
self.description =<<-DESC
Longer description of cocoapods-test.
DESC
self.arguments ='NAME'
def initialize(argv)@name = argv.shift_argument
super
end
def validate!super
help!'A Pod name is required.' unless @name
end
def run
UI.puts "Add your implementation for the cocoapods-test plugin in #{__FILE__}"
end
end
CocoaPods 为开发者提供了插件注册功能,可以使用 pod plugins create NAME 命令创建插件,并在 Podfile 中通过 plugin ‘NAME’ 语句引入插件。虽然在一般情况下很少使用这个功能,但在某些场景下,利用插件能比较方便快捷地解决问题,比如清除 input,output 文件、创建 Podfile DSL 等。
首先,由于 pod install 过程会涉及到插件的加载,因此直接查看 installer.rb 文件:
#Runs the registered callbacks for the plugins post install hooks.#defrun_plugins_post_install_hooks
context = PostInstallHooksContext.generate(sandbox, aggregate_targets)
HooksManager.run(:post_install, context, plugins)
end
#Runs the registered callbacks for the plugins pre install hooks.
#
# @return[void]#defrun_plugins_pre_install_hooks
context = PreInstallHooksContext.generate(sandbox, podfile, lockfile)
HooksManager.run(:pre_install, context, plugins)
end
#Ensures that all plugins specified in the {#podfile} are loaded.
#
# @return[void]#defensure_plugins_are_installed!
require 'claide/command/plugin_manager'
loaded_plugins = Command::PluginManager.specifications.map(&:name)
podfile.plugins.keys.each do|plugin|
unless loaded_plugins.include? plugin
raise Informative,"Your Podfile requires that the plugin `#{plugin}` be installed. Please install it and try installation again."
end
end
end
# @return[Array<Gem::Specification>] Loads plugins via RubyGems looking
#forfiles named after the `PLUGIN_PREFIX_plugin` and returns the#specificationsof the gems loaded successfully.#Plugins are required safely.#defself.load_plugins(plugin_prefix)
loaded_plugins[plugin_prefix]||=plugin_gems_for_prefix(plugin_prefix).map do|spec, paths|
spec ifsafe_activate_and_require(spec, paths)
end.compact
end
# @group Helper Methods
# @return[Array<[Gem::Specification, Array<String>]>]#Returns an array of tuples containing the specifications and#pluginfiles to require for a given plugin prefix.#defself.plugin_gems_for_prefix(prefix)
glob ="#{prefix}_plugin#{Gem.suffix_pattern}"
Gem::Specification.latest_specs(true).map do|spec|
matches = spec.matches_for_glob(glob)[spec, matches] unless matches.empty?
end.compact
end
#Activates the given spec and requires the given paths.#If any exception occurs it is caught and an#informativemessage is printed.
#
# @param [Gem::Specification] spec
#The spec to be activated.
#
# @param [String] paths
#The paths to require.
#
# @return[Bool] Whether activation and requiring succeeded.#defself.safe_activate_and_require(spec, paths)
spec.activate
paths.each {|path|require(path)}
true
rescue Exception => exception # rubocop:disable RescueException
message ="\n---------------------------------------------"
message <<"\nError loading the plugin `#{spec.full_name}`.\n"
message <<"\n#{exception.class} - #{exception.message}"
message <<"\n#{exception.backtrace.join("\n")}"
message <<"\n---------------------------------------------\n"
warn message.ansi.yellow
false
end
以上代码调用几个的 Gem::Specification 方法如下:
# 获取最新 spec 集合
#Return the latest specs, optionally including prerelease specs if prerelease is true.latest_specs(prerelease = false)
# 获取 gem 中匹配的文件路径
#Return all files in this gem that match for glob.matches_for_glob(glob)
# 激活 spec,注册并将其 lib 路径添加到 $LOAD_PATH ($LOAD_PATH 环境变量存储 require 文件时查找的路径)
#Activate this spec, registering it as a loaded spec and adding it's lib paths to $LOAD_PATH. Returns true if the spec was activated, false if it was previously activated. Freaks out if there are conflicts upon activation.activate()
另外 CLAide::Command 类会在 run 类方法中加载所有插件,然后根据解析后的信息,执行对应的命令:
# @param [Array, ARGV] argv
#A list of(remaining) parameters.
#
# @return[Command] An instance of the command class that was matched by
#goingthrough the arguments in the parameters and drilling down#commandclasses.#defself.run(argv =[])
plugin_prefixes.each do|plugin_prefix|
PluginManager.load_plugins(plugin_prefix)
end
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
#Handles plugin related logic logic for the `Command` class.
#
#Plugins are loaded the first time a command run and are identified by the#prefixspecified in the command class. Plugins must adopt the following#conventions:
#
# - Support being loaded by a file located under the
# `lib/#{plugin_prefix}_plugin` relative path.
# - Be stored in a folder named after the plugin.
# - 支持通过 `lib/#{plugin_prefix}_plugin` 路径的文件加载
# (也就是说,如果要对外暴露插件内部存的方法,需要在此文件中 require 之,比如自定义的 Podfile DSL 文件)
# - 保存在以插件命名的文件夹中
module Pod
class Source
class Manager
# 私有源 source
def private_source
url = 'https://xyz.com/ios/pod-specs.git'
source =source_with_url(url)return source if source
Command::Repo::Add.parse(['lebbay-spec', url,'master']).run
source_with_url(url)
end
# 公有源 source
def public_source
url = 'https://github.com/CocoaPods/Specs.git'
source =source_with_url(url)return source if source
Command::Repo::Add.parse(['master', url,'master']).run
source_with_url(url)
end
end
end
end
② Podfile 内提供 dev_pods 自定义方法用于提测过程中实时拉取组件分支最新 commit
在组件开发过程中经常会修改几个 pod 的代码,需要一个个的将 pod 指向本地开发目录,在项目测试过程中又要将 pod 一个个指向提测分支,比如:
# 开发阶段
pod 'PodA',:path =>'../PodA'
pod 'PodB',:path =>'../PodB'
# 测试阶段
pod 'PodA',:git =>'https://xyz.com/ios/PodA.git',:branch =>'release/1.0.0'
pod 'PodB',:git =>'https://xyz.com/ios/PodB.git',:branch =>'release/1.0.0'
为了简化写法,我们提供了 dev_pods 方法,简化逻辑后思路大致如下:
def dev_pods(pods, branch ='')if branch.length >0
# 测试阶段
pods.each do|name|
pod name,:git =>"https://xyz.com/ios/#{name}.git",:branch =>"#{branch}"
end
else
# 开发阶段
development_path = File.read('./bin/.development_path').chomp
pods.each do|name|
pod name,:path =>"#{development_path}#{name}"
end
end
end
module Pod
class Podfile
module DSL
public
def dev_pods(pods, branch ='')if branch.length >0
pods.each do|name|
pod name,:git =>"https://xyz.com/ios/#{name}.git",:branch =>"#{branch}"
end
pull_latest_code_and_resolve_conflict(pods)
puts "lebbay: using remote pods with branch: #{branch}".green
else
# 自定义开发目录
development_path = Config.instance.dev_pods_path
pods.each do|name|
pod name,:path =>"#{development_path}#{name}"
end
puts "lebbay: using local pods with path: #{development_path}xxx".green
end
end
#--------------------------------------#
private
def pull_latest_code_and_resolve_conflict(pods)
# 1、Podfile.lock
rewrite_lock_file(pods, Config.instance.lockfile_path)
# 2、Manifest.lock
rewrite_lock_file(pods, Config.instance.sandbox.manifest_path)
end
def rewrite_lock_file(pods, lock_path)return unless lock_path.exist?
lock_hash = Lockfile.from_file(lock_path).to_hash
# 1、PODS
lock_pods = lock_hash['PODS']if lock_pods
target_pods =[]
lock_pods.each do|pod|if pod.is_a? Hash
first_key = pod.keys[0]
first_value = pod.values[0]if(first_key.is_a? String)&&(first_value.is_a? Array)
next ifis_include_key_in_pods(first_key, pods)
dep_pods = first_value.reject {|dep_pod|is_include_key_in_pods(dep_pod, pods)}
target_pods <<(dep_pods.count >0?{first_key => dep_pods}: first_key)
next
end
elsif pod.is_a? String
next ifis_include_key_in_pods(pod, pods)
end
target_pods << pod
end
lock_hash['PODS']= target_pods
end
# 2、DEPENDENCIES
locak_dependencies = lock_hash['DEPENDENCIES']if locak_dependencies
target_dependencies =[]
locak_dependencies.each do|dependence|if dependence.is_a? String
next ifis_include_key_in_pods(dependence, pods)
end
target_dependencies << dependence
end
lock_hash['DEPENDENCIES']= target_dependencies
end
Lockfile.new(lock_hash).write_to_disk(lock_path)
end
def is_include_key_in_pods(target_key, pods)
pods.each do|pod|if target_key.include? pod
return true
end
end
return false
end
#--------------------------------------#
end
end
end
我们同时修改了 Pods/ 文件夹下的 Manifest.lock 文件,是因为 CooaPods 在 pod install 过程中会对比 lock 文件里记录的 version 版本号,若 Manifest.lock 文件里记录的版本没变的话,在执行 pod install 时 Pods/ 文件夹里对应 Pod 的代码很可能是不会更新的。其中关于开发目录(development_path = Config.instance.dev_pods_path),给 Pod::Config 扩展了两个方法:设置开发目录 & 读取开发目录:
module Pod
class Config
# 读取目录
def dev_pods_path
config_path_file = dev_pods_path_config_file
dev_path = File.read(config_path_file).chomp
end
# 设置目录
def config_dev_pods_path(dev_path)
raise Informative,"input can't be nil" unless dev_path.length >0
dev_path +='/' unless dev_path[dev_path.length -1]=='/'
config_path_file = dev_pods_path_config_file
File.open(config_path_file,"w")do|file|
file.syswrite(dev_path)
end
end
# 配置文件
def dev_pods_path_config_file
config_path = File.expand_path('~/.cocoapods-lebbay')
FileUtils.makedirs(config_path) unless File.exists?config_path
config_path_file = config_path +'/dev_pods_path_config'
unless File.exist?(config_path_file)
File.open(config_path_file,"w")do|file|
file.syswrite('../../')
end
end
config_path_file
end
end
end
给 pod 扩展了两个方法入口分别执行这俩方法,读取开发目录(pod dev-pods-path cat),设置开发目录(pod dev-pods-path set):
require 'cocoapods-lebbay/cache_config'
module Pod
class Command
class DevPodsPath < Command
self.abstract_command = true
self.summary ='set or cat dev_pods path'
def self.options
[]
end
end
class Set < DevPodsPath
self.summary ='set dev_pods path'
def run
UI.puts "Please input dev_path for dev_pods command:".green
answer = STDIN.gets.chomp.strip
Config.instance.config_dev_pods_path(answer)
end
end
class Cat < DevPodsPath
self.summary ='cat dev_pods path'
def run
UI.puts Config.instance.dev_pods_path.green
end
end
end
end
③ 解决 libwebp 网络问题:修改公有源里 podspec git 地址为 github 地址