cocoapods-podfile-local

一个 CocoaPods 插件,让每个开发者通过 Podfile.local 文件覆盖 pod 的引入方式(git 分支、本地路径等),无需修改共享的 Podfile。

使用方法

1. 创建 Podfile.local

在项目根目录创建 Podfile.local(已加入 .gitignore,不会被提交):

# 指向本地路径(联调开发最常用)
edit 'VKRouteKit', :path => '../VKRouteKit'

# 指向 git 仓库 + feature 分支
edit 'CustomLib', :git => 'git@gitlab.com:xxx/CustomLib.git', :branch => 'feature/login'

# 指向 git tag
edit 'SomeSDK', :git => 'git@gitlab.com:xxx/SomeSDK.git', :tag => 'v1.2.3'

# 指向特定 commit
edit 'SomeSDK', :git => 'git@gitlab.com:xxx/SomeSDK.git', :commit => 'abc123'

# 使用自定义 podspec
edit 'MyPod', :podspec => './local_specs/MyPod.podspec'

# 只覆盖非来源选项(保留原始版本/来源)
edit 'DebugTool', :configurations => ['Debug']

# 组合覆盖
edit 'VKWebBridge', :git => 'git@gitlab.com:xxx/VKWebBridge.git', :branch => 'hotfix/crash', :configurations => ['Debug']

2. 执行安装

bundle exec pod install

控制台会输出覆盖日志:

[Podfile.local] Loading /path/to/project/Podfile.local
[Podfile.local] Overriding 'VKRouteKit' with {:path=>"../VKRouteKit"}
[Podfile.local] Overriding 'CustomLib' with {:git=>"git@gitlab.com:xxx/CustomLib.git", :branch=>"feature/login"}

3. 恢复原始状态

删除或清空 Podfile.local,重新 pod install 即可。

互斥源自动清理

覆盖来源时,插件会自动清除与之冲突的旧选项:

edit 指定 自动清除
:git :path, :podspec
:path :git, :branch, :tag, :commit, :podspec
:podspec :git, :branch, :tag, :commit, :path

例如原始 pod 用 :git + :branch 引入,你 edit 为 :path,插件会自动清除 :git:branch,无需手动处理。

工作原理

CocoaPods 生命周期

pod install
    │
    ▼
① 加载 gem(插件在此阶段加载)     ← 插件读取 Podfile.local,拦截 pod 方法
    │
    ▼
② 解析 Podfile(逐行执行 Ruby 代码) ← 拦截的 pod 方法在此阶段替换参数
    │
    ▼
③ 解析依赖(resolver 计算版本来源)   ← 此时 Kingfisher 已经是 :path 来源
    │
    ▼
④ 下载 & 安装

核心机制

插件在加载阶段(早于 Podfile 解析)完成两件事:

1. 读取 Podfile.local,收集覆盖配置

创建一个独立的 PodfileLocalLoader 对象,用 instance_eval 执行 Podfile.local 内容。每个 edit 调用将覆盖选项注册到 OverrideManager 单例。

2. 用 prepend 拦截 Pod::Podfile::DSL#pod 方法

pod 方法的查找链前面插入一个包装模块。当 Podfile 解析到 pod 'Kingfisher', '~> 8.0' 时:

  1. 包装方法检查 OverrideManager 中是否有 'Kingfisher' 的覆盖 → 有
  2. 从原始参数中分离版本号('~> 8.0')和选项 Hash
  3. 执行互斥源清理 + 选项合并
  4. 如果覆盖指定了 :path / :git / :podspec,移除版本号约束
  5. 用合并后的参数调用 super(原始 pod 方法)

对 CocoaPods 来说,就好像 Podfile 里直接写的就是覆盖后的内容。

时序图

sequenceDiagram
    participant User as pod install
    participant CP as CocoaPods
    participant Plugin as cocoapods-podfile-local
    participant OM as OverrideManager
    participant PF as Podfile

    User->>CP: bundle exec pod install
    CP->>Plugin: require cocoapods_plugin.rb
    Plugin->>Plugin: setup!
    Plugin->>Plugin: load_podfile_local!
    Plugin->>OM: edit 'Kingfisher', path:... → register
    Plugin->>Plugin: patch_pod_dsl! (prepend)

    CP->>PF: 开始解析 Podfile
    PF->>Plugin: pod 'Alamofire', '~> 5.9'
    Plugin->>OM: 有覆盖? → 无
    Plugin->>CP: super → 原样注册

    PF->>Plugin: pod 'Kingfisher', '~> 8.0'
    Plugin->>OM: 有覆盖? → 有!
    OM->>OM: merge + 互斥源清理
    Plugin->>CP: super('Kingfisher', path:'...') → 替换注册

    CP->>CP: 解析依赖 (Kingfisher 已是 path 来源)
    CP->>CP: 下载 & 安装

文件结构

plugins/cocoapods-podfile-local/
├── cocoapods-podfile-local.gemspec   # gem 描述
├── README.md                         # 本文档
└── lib/
    ├── cocoapods_plugin.rb           # CocoaPods 插件入口(硬性约定)
    ├── cocoapods-podfile-local.rb    # 主入口,require 各模块 + 调用 setup!
    └── cocoapods_podfile_local/
        ├── version.rb                # 版本号
        ├── dsl.rb                    # 注入 edit 方法到 Pod::Podfile
        ├── override_manager.rb       # 覆盖注册、互斥清理、选项合并
        └── hook.rb                   # 加载 Podfile.local + prepend 拦截 pod 方法

关键设计决策

  • 为什么不用 pre_install hook? pre_install 在依赖解析之后才执行,此时修改 Dependency 对象已经不影响解析结果。必须在 Podfile 解析阶段拦截。

  • 为什么用 prepend 而不是 alias_method prepend 是 Ruby 推荐的方法拦截方式,在方法查找链中插入模块,可以用 super 调用原始方法,不会污染命名空间。

  • 为什么 Podfile.local 用独立 Loader 而不是在 Podfile 上下文中 eval? 插件加载时 Podfile 实例尚不存在,无法在其上下文中执行。用独立的 PodfileLocalLoader 对象来收集配置,与 Podfile 解析过程解耦。

  • 为什么 cocoapods_plugin.rb 是必须的? CocoaPods 通过检查 gem 中是否存在 lib/cocoapods_plugin.rb 来判断是否为合法插件,这是硬性约定。

注意事项

  • Podfile.local 已加入 .gitignore,每个开发者独立维护
  • edit 一个 Podfile 中不存在的 pod 会输出警告但不会中断安装
  • 首次使用需要先 bundle install 安装插件
  • 插件作为本地 gem 内嵌在项目中,无需发布到 RubyGems