podspec文件查询方法
podspec文件是cocoapods的自定义的dsl,实际上是Spec这个类中定义的方法,所有的方法都在Cocoapods/Core里面。Spec这个类的定义就在dsl.rb文件中,里面每个参数的意义和每个参数怎么写都有详细说明。
如果要查看Podfile文件怎么书写,可以进入Podfile对应的dsl查看。
1. 创建cocoapods插件
cocoapods内置了创建插件的功能,终端输入pod plugins --help:
可以通过create命令直接创建插件,或者可以创建一个gem把它变成cocoapods插件。
我们使用后者。
2. 创建gem工程
cd到要创建工程的目录,执行bundle gem cocoapods-sj-bin,一顿回车,就可以创建出cocoapods-sj-bin这个工程了。
3. 修改配置
修改cocoapods-sj-bin.gemspec文件。制作的是gem的三方库,三方库的信息都在这个文件中。
首先,
spec.version是依赖version.rb这个文件的。
修改文件夹:
- 把
cocoapods文件夹名字修改为cocoapods-sj-bin - 把
bin.rb文件放到cocoapods-sj-bin目录下 - 删除
sj文件夹 - 在
cocoapods-sj-bin目录下创建文件gem_version.rb
4. 在gem_version.rb写代码
首先要给三方库起一个命名空间,防止类名冲突,包括别人可以通过命名空间访问到我们的代码。
在Ruby里面,起命名空间使用module。在命名空间可以直接定义变量:
module SjBin
VERSION = '0.1.0'
end
5. 修改cocoapods-sj-bin.gemspec中的version
如果要访问gem_version.rb,首先要保证require里面的路径是正确引用这个文件的。
可以参照Xcodeproj-master项目里面xcodeproj.gemspec的写法,先复制过来require File.expand_path('../lib/xcodeproj/gem_version', __FILE__)。
File:Ruby的一个内置类,专门用来op file。
expand_path:基于当前文件,再基于后面写的相对路径,变成绝对路径。
修改后路径引用:
require File.expand_path('../lib/cocoapods-sj-bin/gem_version', __FILE__)
修改version:
spec.version = SjBin::VERSION
删除
spec.metadata["allowed_push_host"] = "TODO: Set to your gem server 'https://example.com'"
spec.metadata["homepage_uri"] = spec.homepage
spec.metadata["source_code_uri"] = "TODO: Put your gem's public repo URL here."
spec.metadata["changelog_uri"] = "TODO: Put your gem's CHANGELOG.md URL here."
spec.bindir = "exe"
spec.files代码中哪些文件需要被别人引入过去,与podspec中的source_files类似。
再抄一下s.files = %w(README.md LICENSE) + Dir['lib/**/*.rb'],Dir代表lib文件夹下的文件夹中的所有.rb文件,%w后面可以用{}或()或[],代表定义了一个Ruby数组,里面放一组文件,中间用空格隔开。
bin文件夹下存放着终端可以直接调用的ruby文件。可以通过executables声明这个三方库是支持在终端直接调用的,会把当前文件加载到Shell的PATH搜索路径中。把bin文件夹中的console重命名sj_bin。
spec.executables = %w(sj_bin)
spec.require_paths = ["lib"]这个路径是从lib开始的,如果想使用里面的bin.rb,把bin.rb放到lib文件夹下。
添加依赖,因为我们是基于cocoapods进行开发,所以使用add_runtime_dependency运行时以来的三方库:
spec.add_runtime_dependency 'cocoapods', '~>1.10.1'
6. 修改Gemfile
注释掉gem "rake", "~> 13.0"
7. 修改bin.rb
想把这个文件作为暴露的文件,别人可以直接使用。比如他导入了这个文件,就代表导入当前的整个gem。修改下名称cocoapods-sj-bin.rb。
8. bundle install
spec.summary = "bin gen tool"
spec.description = "bin gen tool"
spec.homepage = "https://github.com/CocoaPods/CocoaPods"
spec.required_ruby_version = ">= 2.6.0"
9. 创建cocoapods_plugin.rb文件
cocoapods要求在lib文件夹下必须有cocoapods_plugin.rb文件,自己写的命令需要写在这个文件里,cocoapods才能读到这个命令,终端才能使用这个命令。
10. 创建launch.json文件
如果现在调试bin文件夹下的sj_bin文件,json文件内容:
{
"version": "0.2.0",
"configurations": [
{
"name": "Debug Sj Bin",
"type": "Ruby",
"request": "launch",
"debuggerPort": "1237",
"program": "${workspaceRoot}/bin/sj_bin",
"args": []
}
]
}
11. 调试
在sj_bin文件写:p 'Sj1234',按F5开始调试,终端输出Sj1234
12. 创建自己命令
比如现在想实现pod sj这个命令,sj就是pod的子命令,仿照cocoapods在lib->cocoapods-sj-bin文件夹下创建文件夹command,在command下创建文件sj.rb。
要使用cocoapods命令,首先要require引用cocoapods:require 'cocoapods'。
之后怎么写可以参照cocoapods,在lib->cocoapods->command下比如init.rb文件:
Init小写就是给cocoapods定义的命令,<代表继承,给cocoapods定义一个sj命令代码如下:
module Pod
class Command
class Sj < Command
end
end
end
self.summary = 'G.... 简介
self.description = <<-DESC... 描述
def initialize(argv).... 类似OC的init方法,接收参数argv
def run... 调用sj这个命令时候,真正想做的操作
修改后代码:
module Pod
class Command
class Sj < Command
self.summary = 'sj bin'
self.description = 'sj bin'
def initialize(argv)
super
end
def run
p '111111'
end
#-----------------------
end
end
end
在run方法打个断点,调试下能不能过来,那就要调试sj.rb这个文件,之前我们调试sj_bin这个文件,那就要在这个文件中调用sj.rb。
首先要引入,按照cocoapods-sj-bin.gemspec中的spec.require_paths来搜索:require 'cocoapods-sj-bin/command/sj.rb'
然后我们调用Command,要先使用Pod命名空间才能调用到Command,Pod::Command此时调用的是pod这个命令,如果再想调用sj,可以通过参数传递给ARGV:Pod::Command.run(ARGV)。
ARGV就是通过launch.json传递的参数,所以args里面要写sj。
{
"version": "0.2.0",
"configurations": [
{
"name": "Debug Sj Bin",
"type": "Ruby",
"request": "launch",
"debuggerPort": "1237",
"program": "${workspaceRoot}/bin/sj_bin",
"args": [
"sj"
]
}
]
}
按F5调试,发现报错:
这个报错就是在require之前,要使用bundler/setup来清空一下当前LOAD_PATH,在require之前添加代码:
if $PROGRAM_NAME == __FILE__
ENV['BUNDLE_GEMFILE'] = File.expand_path('../Gemfile', __dir__)
require 'bundler/setup'
end
再F5调试,就可以进入到run方法了。
13. 创建build命令
做组件二进制化,首先可以想到两个命令,build:把代码组件变成二进制组件,debug:调试组件二进制原码。
在command文件夹下创建build.rb,如果我想调用pod sj build这种形式,意味着build是Sj的子命令:
require 'cocoapods'
module Pod
class Command
class Sj < Command
class Build < Sj
self.summary = 'sj bin'
self.description = 'sj bin'
def initialize(argv)
super
end
def run
p '111111'
end
#-----------------------
end
end
end
end
把sj_bin引入修改为require 'cocoapods-sj-bin/command/build.rb',引入build。
launch.json的args里面增加个参数build。
{
"version": "0.2.0",
"configurations": [
{
"name": "Debug Sj Bin",
"type": "Ruby",
"request": "launch",
"debuggerPort": "1237",
"program": "${workspaceRoot}/bin/sj_bin",
"args": [
"sj",
"build"
]
}
]
}
按F5调试就没有问题了。
14. 设置命令参数,解析参数
这个build命令就是制作二进制包,考虑下他是怎么制作的,首先去下载AFNetworking源码。
比如我现在要把AFNetworking源码生成二进制组件,就需要进入AFNetworking执行build来生成xcframework,推到指定的源,在使用时下载下来即可。
生成二进制组件需要的参数:
- 是
framework还是library,是静态framework还是动态framework - 在哪个环境生成,
debug还是release - 支持的平台
我们先写options,这里面的内容就是当执行--help的时候,会把options的内容输出出来:
def self.options
[ ['--platforms=ios,macos', 'Lint against specific platforms (defaults to all platforms supported by the ' \ 'podspec). Multiple platforms must be comma-delimited'],
['--library', '构建静态产物,默认为true'],
['--framework', '构建静态Framework,默认为false'],
['--dynamic', '构建动态Framwork,獸认为false'],
['--configuration', 'Build the specified configuration. Default to Release'],
['--sources', '构建工程需要依赖的sources源']
].concat(super).uniq
end
这些只是声明的参数,接下来要在initialize方法去解析这些参数,cocoapods提供了内置的解析工具。比如解析dynamic:argv.flag?('dynamic', false)。Ruby里面问号返回是Bool类型,第二个参数是默认值。
我们解析了这些值,还需要一个属性去保存它,Ruby中定义类属性用@符号,实例属性用@@定义,定义一个属性:@product_type = :framework。里面的:代表定义了一个Ruby的符号,这个属性可以定义字符串形式保存比如@product_type = "framework",定义字符串需要开辟空间存储它,并且还要存它的符号,用:表示直接定义了一个符号去使用。
@product_type = :framework
@product_type = :static_library if argv.flag?('library', false)
@product_type = :dynamic_framework if argv.flag?('dynamic', false)
定义的product_type默认值是framework也就是静态framework。如果设置library,产物是静态库,如果设置dynamic产物就是动态framework。
平台参数,因为支持平台写的是-platforms=*****,这个接收参数的在cocoapods里面叫option,默认参数写空字符串,如果不传的话,这个组件支持什么类型就编译什么平台类型。
@platforms = argv.option('platforms', '').split(',')
环境,默认Release
@configuration = argv.option('configuration', 'Release')
source我们先写个空
@sources = []
15. 编译逻辑
修改run方法里面代码。
- 要把准备编译的源码进行拷贝,为了防止修改导致原工程出问题
- 构建当前平台架构支持的
APP工程,把源码导入进去,然后生成二进制 - 编译产物
- 不同架构编译产物打包成
xcframework
16. 定义root目录
这个目录代表每次要拷贝的目录是哪个。Pathname这个类就可以通过点语法形式操作路径。定义拷贝路径:
def root
# 所有要进行二进制的库都拷贝到这个目录下
Pathname.new("/private/tmp/cocoapods-bin")
end
17. 定义方法处理podspec文件
我们创建App工程都是根据podspec文件,所以定义一个属性接收所有的podspec文件路径:@podspec_paths = []
定义一个方法:
def podspecs_to_build
if @podspec_paths.empty?
@podspec_paths = Pathname.glob(Pathname.pwd + '*.podspec{.json}')
# 如果赋值完还是空
if @podspec_paths.empty?
# 抛出异常
raise Informative, 'Unable to find a podspec in the working' \
'directory'
end
end
@podspec_paths
end
18. 拷贝工程
File.expand_path可以把部分路径恢复成完整路径。
Pathname.pwd.parent获取当前路径的上一层。
Dir.chdir("/Users/shangjie/Desktop/工程化/代码/AFNetworking-master")让当前的工作目录进入指定的目录。
拷贝前要把要拷贝到的目录进行清空,需要调用Ruby内置的一个工具类require 'fileutils'
copy_path = Pathname.pwd
name = File.basename(Pathname.pwd)
# 要拷贝到的目录
root_path = root
# 清理目标目录
FileUtils.rm_rf(root_path)
# 清理完再重新创建下目标目录
root_path.mkpath
# copy
FileUtils.cp_r(copy_path, root_path)
19. 创建工程
定义一个app的目录:
def app
root.join('App')
end
pod lib lint验证pod库也是创建个工程,然后去github把代码拉下来,按照podspec添加到工程里面,然后进行编译。
cocoapods有一些三方工具可以使用,直接创建App工程,比如cocoapods-generate,这个插件的描述A CocoaPods plugin that allows you to easily generate a workspace from a podspec.,就是一个cocoapods差价比较容易根据podspec去生成一个worksoace。
这个插件有小小的bug,应对99%以上场景都可以,但是特殊的就不行。
我们知道cocoapods导入第三方库有两种,一种走网络,一种走本地:
- 走网络:根据
podspec去github把源码下载下来,在根据podspec去清理不需要的文件。cocoapods内置有清理工具。这就是为什么比如AFNetworking里面有很多文件,但是导入项目后只有很少一部分文件的原因。 - 走本地:并不会拷贝文件,只是添加引用。
为什么cocoapods-generate有bug?因为这个插件是用path的方式导入的第三方库。我们还原一下这个bug。
创建一个测试工程,下载reakm源码并放到工程目录下。
创建Podfile文件:
target 'test' do
# Comment the next line if you don't want to use dynamic frameworks
use_frameworks!
pod 'Realm', :path => './realm-swift-master'
end
执行pod install。
编译项目,可以看到报了很多错误:
这个错误就是在RLMUtil.mm文件中重复引用了RLMInt。打开RLMUtil.mm,选择Preprocess预处理这个文件,预处理的作用就是把文件导入到这个文件,搜索RLMInt,可以找到有两个@protocol RLMInt @end声明。
出现这个错误的原因是我们使用Realm源码是通过链接的方式引入的,编译器在编译的时候,会在.mm文件的当前目录搜索.h文件,在同目录找到了.h文件,cocoapods又在include中配置了头文件的搜索路径。
出现这个问题的根源在于podspec文件中有个s.prepare_command = 'sh scripts/setup-cocoapods.sh'。说明如下:
打开这个脚本,框住的内容就是把Realm文件下的所有.h文件拷贝到include下。
既然有这个bug,那我们就使用本地网络结合的方式,如果走网络,那就还是网络,如果走本地,我们按照网络的流程把代码拷贝到pods目录下,这样上面的bug就可以解决了。
有位靓仔写的cocoapods-project-gen就可以完美实现我们这个需求。
首先导入require 'cocoapods-project-gen',在cocoapods-sj-bin.gemspec文件中添加依赖spec.add_runtime_dependency 'cocoapods-project-gen',在终端中执行bundle install。
我们生成工程是根据Podifle文件里面的platfrom,一个platfrom生成一个工程。
# 创建工程
generator = ProjectGen::ProjectGenerator.new(podspecs_to_build.first, [], @platforms)
generator.local = false
generator.no_clean = false
generator.allow_warnings = true
generator.no_subspecs = true
generator.only_subspec = false
generator.use_frameworks = @product_type == :dynamic_framework
generator.use_static_frameworks = @product_type == :framework
# generator.external_podspecs = external_podspecs.drop(1)
generator.skip_import_validation = true
# generator.swift_version = swift_version
generator.configuration = @configuration
generator.skip_tests = true
begin
generator.generate!(app) do |platform, pod_targets, validated|
raise 'Could not generator App.xcodeproj' unless validated
end
rescue StandardError => e
raise Pod::Informative, "The `#{@include_podspecs.join(' ')}` specification does not validate." \
"\n\n#{e.message}"
end
直接按F5就可以看到运行成功,/private/tmp/cocoapods-bin目录下生成App的项目:
编译也是没问题的,这里有个注意点,你项目路径是不能包含中文的,否则就会报错:
我们把AFNetworking换成Realm,修改Dir.chdir("/Users/shangjie/Desktop/testtttt/realm-swift-master")。
编译出来的不知道为何是macos的,在launch.json文件中添加参数"--platforms=ios"即可,生成项目可以编译成功了。
这里面有个可以优化的点,在raise 'Could not generator App.xcodeproj' unless validated这行代码打断点,可以发现这个断点走了好几次,就是我们指定platforms,cocoapods会根据平台复制三方库生成App复制三方库生成App复制三方库生成App重复流程,我们一颗让他只复制一次三方库,不同平台生成不同target。
20. 编译
编译我们有两个选择:
- 之前我们生成的工程是完整
App+pods,我们可以编译这个App,通过隐性依赖编译Pods_App通过显式依赖编译pods。- 通过
pods直接编译pod,通过scheme方式来编译。
为了减小编译量,我们选择通过scheme直接编译pods。
接下来编译方式有两种:
build:产物目录不能控制,有个App名字加随机字符串,动态库的调试符号放在framework下。Archive:产物目录可以控制,动态库的调试符号放在指定目录下,对xcframework格式的支持更好。
在编译swift相关的库的时候,最好把BUILD_LIBRARY_FOR_DISTRIBUTION打开,每个人指定swift编译器可能不一样,就会报一些错误。
编译命令
- 对于模拟器,要生成两个架构:
x8664和arm64 - 对于
.a库头文件处理 - Swift模块文件处理
xcodebuild clean archive BUILD_LIBRARY_FOR_DISTRIBUTION=YES \
-project 'Pods/Pods.xcodeproj' \
-scheme 'Pods-App' \
-destination "generic/platform=iOS Simulator" -sdk 'iphonesimulator' \
-archivePath './archive/Pods-App.xcarchive' \
SKIP_INSTALL=NO -showBuildTimingSummary
SKIP_INSTALL这个环境变量默认是YES,控制产物是否放在archive目录里面,默认是不放的,所以我们要改成NO。
generic/这个含义你光指定编译架构还不行,你需要指定具体的机型,或者不指定具体机型,选择一个通用的你帮我选择一个即可。
我们可以通过下面命令打印编译时需要的环境变量:
xcodebuild clean archive BUILD_LIBRARY_FOR_DISTRIBUTION=YES \
-project 'Pods/Pods.xcodeproj' \
-scheme 'Pods-podtest' \
-destination "generic/platform=iOS Simulator" \
-showBuildSettings
搜索Archs,可以看到支持的架构是arm64
我们再制定-sdk 'iphonesimulator',可以看到archs支持架构变成了arm64 x86_64
编译产物需要单独放一个目录,定义一个产物目录:
/// 专门放产物的东西
def product_dir
Pathname.new(root).join('./Products').expand_path
end
/// 放其他头文件等
def archive_dir
Pathname.new(product_dir).join('./archive').expand_path
end
一个platform生成一个app工程,我们去编译产物,保存archive路径,最后通过这些路径生成xcframework。
我们在编译的时候,destination、sdk、archivePath这三个参数是变化的,剩下的参数是固定不变的。
framework里面的头文件,都是按结构放在.framework/Headers里面,它进行了格式化。.a库并不是。
podfile文件把use_frameworks!删除,默认生成的就是.a库。找一个工程编译.a库,编译完Show Build Folder In Finder,找到路径Build/Products/Debug-iphonesimulator/MJRefresh,可以看到就一个.a库,其他什么信息也没有。现在的头文件是cocoapods按照格式放在了Pods/Headers/Private/MJRefresh和Pods/Headers/Public/MJRefresh里面。只有.a库会这么处理,通过软链接的方式显示。如果现在生成二进制文件,就需要把这个头文件按cocoapods的规则给保存起来。
现在我们打开use_frameworks!,再编译可以在产物信息里看见头文件了:
cocoapods源码中有个link_headers方法,里面如果是use_frameworks直接next什么也不做。如果不是,按软连接方法显示,我们只需要把这个方法进行一些修改,复制出真实文件即可。
def link_headers(target, product_path)
Pod::UI.message '- Linking headers' do
# When integrating Pod as frameworks, built Pods are built into
# frameworks, whose headers are included inside the built
# framework. Those headers do not need to be linked from the
# sandbox.
next if target.build_as_framework? && target.should_build?
sandbox = Pod::Sandbox.new(product_path)
build_headers = AmberBin::HeadersStore.new(sandbox, '', :public)
pod_target_header_mappings = target.header_mappings_by_file_accessor.values
public_header_mappings = target.public_header_mappings_by_file_accessor.values
headers = pod_target_header_mappings + public_header_mappings
headers.uniq.each do |header_mappings|
header_mappings.each do |namespaced_path, files|
hs = build_headers.add_files('', files)
build_headers.add_files(namespaced_path, hs, ln: true)
end
end
root_name = Pod::Specification.root_name(target.pod_name)
pod_dir = target.sandbox.sources_root.join(root_name)
# swift头文件处理
module_headers = pod_dir.join(Constants::COPY_LIBRARY_SWIFT_HEADERS)
build_headers.add_files(root_name, module_headers.children)
build_headers.root
end
end
这个方法还有个swift头文件处理,-Swift.h文件是放在产物目录下的,还有一些module等文件,这些也需要处理。这些文件是cocoapods通过脚本拷贝到产物目录的:
我们选择在生成xcode工程阶段也生成一个脚本,把swift相关的文件拷贝到指定目录。
cocoapods源码中,add_swift_library_compatibility_header_phase就是添加脚本的方法。cocoapods-project-gen中也用类似方法add_swift_library_compatibility_header去添加了脚本。
XCFramework:是苹果官方推荐的、支持的,可以更方便的表示一个多 个平台和架构的分发二进制库的格式。
需要Xcode11以上支持。
是为更好的支持Mac Catalyst和ARM芯片的macOS。 专⻔在2019年提出的framework的另一种先进格式。
和传统的framework相比:
- 可以用单个
.xcframework文件提供多个平台的分发二进制文件; - 与
Fat Header相比,可以按照平台划分,可以包含相同架构的不同平 台的文件; - 在使用时,不需要再通过脚本去剥离不需要的架构体系。
21. 创建私有库
把创建的xcframework推到自己的source中