cocoapods-meitu-bin 3.0.2 → 3.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2f8e34d0eef3e6ae1258472fabbb4750d205a2dee56a51f40b827d95597e0c56
4
- data.tar.gz: c589ba529578d252e84bb7dd2d702c19a3d4d7a64d0d78c53892db2839946e4b
3
+ metadata.gz: 02cd60b43188e551ce7bcadb27453077e9e23353e1470e2d2f3396e901201c9b
4
+ data.tar.gz: 7431d9db2c1f969a60ef6c65f11e1098d9501a5b33c8c102e89c59cd68d8910c
5
5
  SHA512:
6
- metadata.gz: da4f7e694226ced238d9e4d89ebfaafcb3263fbb826e5ab58f4c818baef34357f86ad9b40081e438b1162e735d5955be0425f6b84a24e91a883c9006377accf3
7
- data.tar.gz: 04b73ddef551f520b5b89f4406e5e6dd015adf814e3504d1f062300e6339d11139d7b45863f66eaf735c5aec2c909edff5b9c454eefd95e698f9975359bd5b59
6
+ metadata.gz: 91f2b8468a6b747ab6a69e9ed0d5c61c138f73e3e2624c31f169ec85c8be15aecb799353031211b641179e779f8ba2eb41807f465c6f88b3d267ed7e3cbe8cb6
7
+ data.tar.gz: 70f76e6bbfc54db74e5bd310054747407e87e1be33987ccb3ecdd33e0d970cadc1bb841860e4b7d8e1d8d5177e137dc81225ed3630732ebf8405648a241b8fbd
data/README.md CHANGED
@@ -2,57 +2,296 @@
2
2
 
3
3
  ## 简介
4
4
 
5
- `cocoapods-meitu-bin`是`CocoaPods`的二进制插件,提供了二进制相关的功能,如基于壳工程的二进制制作、二进制 / 源码切换等
5
+ `cocoapods-meitu-bin` 是 `CocoaPods` 的二进制插件,提供两类核心能力:
6
6
 
7
- [美图秀秀 iOS 客户端二进制之路](https://juejin.cn/post/7175023366783385659)
7
+ 1. 基于壳工程/组件工程制作二进制(`pod bin build-all`)。
8
+ 2. 在 `pod install` 期间动态切换源码与二进制(含 monorepo `:path` 组件)。
9
+
10
+ 对应 monorepo `:path` 动态切换的关键实现可参考提交:`6ca9994`(`support worktree identity for local path pods`)。
8
11
 
9
12
  ## 安装
10
13
 
11
- `cocoapods-meitu-bin`有2种安装方式:
14
+ 支持两种方式:
12
15
 
13
- * 安装到本机目录
14
- * 使用`Gemfile`
16
+ 1. 安装到本机 RubyGems。
17
+ 2. 通过项目 `Gemfile` 管理。
15
18
 
16
- ### 安装到本机目录
19
+ ### 方式 1:安装到本机
17
20
 
18
21
  ```shell
19
- $ sudo gem install cocoapods-meitu-bin
22
+ sudo gem install cocoapods-meitu-bin
20
23
  ```
21
-
22
- ### 使用Gemfile
23
24
 
24
- 在`Gemfile`中添加如下代码,然后执行`bundle install`
25
+ ### 方式 2:Gemfile
26
+
27
+ 在 `Gemfile` 中添加:
25
28
 
26
29
  ```ruby
27
30
  gem 'cocoapods-meitu-bin'
28
31
  ```
29
32
 
30
- ## 使用
33
+ 然后执行:
34
+
35
+ ```shell
36
+ bundle install
37
+ ```
38
+
39
+ ## 快速使用
40
+
41
+ ### 1) Podfile 开启二进制
42
+
43
+ ```ruby
44
+ plugin 'cocoapods-meitu-bin'
45
+
46
+ # true: 开启二进制;false: 全源码
47
+ use_binaries!(ENV['MEITU_USE_BINARIES'] != 'false')
48
+
49
+ # 源码白名单(这些组件强制走源码)
50
+ set_use_source_pods ['WCDBSwift']
51
+
52
+ # 可选:按环境切换二进制配置(Debug / Release / Distribution)
53
+ set_configuration(ENV['MEITU_USE_CONFIGURATION'] || 'Debug')
54
+ ```
55
+
56
+ ### 2) monorepo 本地 `:path` 接入示例
57
+
58
+ ```ruby
59
+ MTXX_MONOREPO_ROOT = File.expand_path('../../..', __dir__)
60
+ MTXX_MONOREPO_POD_PATHS = {
61
+ 'MTVIPModule' => 'Modules/MTVIPModule',
62
+ 'MTImageEditor' => 'Modules/MTXXImageModule'
63
+ }.freeze
64
+
65
+ def mtxx_monorepo_pod(name, fallback = nil, fallback_options = nil)
66
+ if MTXX_MONOREPO_POD_PATHS.key?(name)
67
+ options = { :path => File.join(MTXX_MONOREPO_ROOT, MTXX_MONOREPO_POD_PATHS[name]) }
68
+ pod name, options
69
+ elsif fallback_options
70
+ pod name, fallback, fallback_options
71
+ elsif fallback
72
+ pod name, fallback
73
+ else
74
+ pod name
75
+ end
76
+ end
77
+
78
+ target 'MTXX' do
79
+ mtxx_monorepo_pod 'MTVIPModule'
80
+ mtxx_monorepo_pod 'MTImageEditor'
81
+ end
82
+ ```
83
+
84
+ ## Monorepo 专章:`:path` 组件动态切换(重点)
85
+
86
+ 本节专门说明 monorepo 下本地 `:path` 组件如何在一次 `pod install` 中动态决定“走源码”还是“走二进制”。
87
+
88
+ ### 目标与结论
89
+
90
+ 1. 目标:本地组件有改动就切源码,没改动且二进制可用就切 binary。
91
+ 2. 结论:判定依据不是单一 commit,而是 `worktree_identity`(覆盖 `HEAD + staged + unstaged + untracked`)。
92
+ 3. 命中 binary:local path pod 从 `Development Pods` 提升为 binary pod。
93
+ 4. 未命中 binary:自动回退源码,不阻断依赖分析流程。
94
+
95
+ ### 核心实现入口
96
+
97
+ | 文件 | 作用 |
98
+ |---|---|
99
+ | `lib/cocoapods-meitu-bin/helpers/worktree_identity.rb` | 计算 `worktree_identity`,提供规则版本和 git/fallback 双路径。 |
100
+ | `lib/cocoapods-meitu-bin/native/resolver.rb` | 在 `resolve` / `resolver_specs_by_target` 阶段执行 source/binary 决策与替换。 |
101
+ | `lib/cocoapods-meitu-bin/helpers/buildAll/bin_helper.rb` | 生成 binary version,优先使用 `worktree_identity`。 |
102
+ | `lib/cocoapods-meitu-bin/config/config.rb` (`PodUpdateConfig`) | 缓存 identity、checkout options、依赖关系、promote 状态。 |
103
+ | `lib/cocoapods-meitu-bin/source_provider_hook.rb` | `source_provider`/`pre_install` 阶段收集 `:path/:commit` 上下文。 |
104
+
105
+ ### `worktree_identity` 生成规则(详细)
106
+
107
+ 规则版本:`v1_git_tree_diff_untracked_sha1`
108
+
109
+ 计算对象:当前一次依赖分析涉及的本地组件目录(通常是 local podspec 所在目录)。
110
+
111
+ ```text
112
+ base_tree_sha = git rev-parse HEAD:<relative_path>
113
+ staged_diff_sha = sha1(git diff --cached --binary -- <path>)
114
+ unstaged_sha = sha1(git diff --binary -- <path>)
115
+ untracked_sha = sha1(untracked_file_list + file_contents)
116
+
117
+ worktree_identity = sha1(base_tree_sha + "|" + staged_diff_sha + "|" + unstaged_sha + "|" + untracked_sha)
118
+ ```
119
+
120
+ 字段解释:
121
+
122
+ 1. `base_tree_sha`:模块在 `HEAD` 下的快照标识(提交态)。
123
+ 2. `staged_diff_sha`:已 `git add` 但未提交的改动。
124
+ 3. `unstaged_sha`:工作区未暂存改动。
125
+ 4. `untracked_sha`:未跟踪文件列表与内容摘要。
31
126
 
32
- ### 制作二进制
127
+ 为什么必须包含 `unstaged/untracked`:
33
128
 
34
- 进入`Podfile`所在目录,执行`pod bin build-all`即可,根据需要添加相应的`option`选项,支持的`option`选项如下:
129
+ 1. 只用提交态会误把“本地已改但未提交”组件当成可复用旧 binary。
130
+ 2. 新增文件(最常见)如果不纳入 identity,会出现错误命中旧二进制。
131
+
132
+ 回退策略:
133
+
134
+ 1. 若目录不在 git 仓库内,或 git 查询失败,则回退为“目录相对路径 + 文件内容”摘要。
135
+ 2. 回退后仍能保证 identity 随文件变化而变化。
136
+
137
+ ### 在依赖分析中的实际执行过程(按时序)
138
+
139
+ 1. `:pre_install` / `:source_provider`:
140
+ 插件加载并收集 Podfile 中 `:path`、`:commit` 等信息,写入 `PodUpdateConfig`。
141
+ 2. `Resolver#resolve` 前半段:
142
+ 识别本轮本地 root(`sandbox.development_pods + Podfile external_source[:path]`)。
143
+ 3. `Resolver#resolve` 预计算:
144
+ 对本地 root 调用 `WorktreeIdentity.for_sandbox_pod`,把 identity 写入 `PodUpdateConfig`。
145
+ 4. `Resolver#resolve` 依赖关系:
146
+ 构建 `dependency_relationships`,给 binary version 计算提供依赖链输入。
147
+ 5. `Resolver#resolver_specs_by_target` 决策:
148
+ 对每个 rspec 判定 `use_binary`,并处理 `use_source_pods` / 黑名单 / selector。
149
+ 6. binary version 计算:
150
+ `BinHelper.version` 对 local root 优先取 `worktree_identity`,其次 `commit/tag`,最后才是版本号。
151
+ 7. 命中 binary:
152
+ 用 binary source 的 spec 替换 rspec,并执行 `promote_local_path_pod_to_binary`。
153
+ 8. 未命中 binary:
154
+ 保留源码 rspec(必要时补 `checkout_source`),自动回退源码流程。
155
+ 9. root + subspec 一致性:
156
+ 同一 local root 一旦命中 binary,其余 subspec 强制复用同一 binary version,避免 mixed mode。
157
+
158
+ ### 状态流转(`PodUpdateConfig`)
159
+
160
+ | 状态 | 含义 | 主要写入点 |
161
+ |---|---|---|
162
+ | `worktree_identities` | root -> identity | `resolve` 预计算与按需补算 |
163
+ | `checkout_options` | root -> git/commit/tag | 解析 checkout options 后写入 |
164
+ | `dependency_relationships` | root -> 依赖身份串 | `resolve` 依赖图构建后写入 |
165
+ | `promoted_local_pods` | 已提升为 binary 的本地 root | `promote_local_path_pod_to_binary` |
166
+ | `external_source_binary` | 壳工程制作时可参与处理的 external 组件 | resolver 记录 |
167
+
168
+ ### Monorepo 动态切换流程图(详细)
169
+
170
+ ```mermaid
171
+ flowchart TD
172
+ A["pod install"] --> B["pre_install/source_provider 收集 :path/:commit"]
173
+ B --> C["resolve: 识别本地 root 集合"]
174
+ C --> D["预计算 worktree_identity 并写入 PodUpdateConfig"]
175
+ D --> E["构建 dependency_relationships"]
176
+ E --> F["resolver_specs_by_target 逐 rspec 判定 use_binary"]
177
+ F --> G{"是否强制源码\\n(use_source_pods / selector / force env)?"}
178
+ G -->|是| H["保留源码 rspec"]
179
+ G -->|否| I["BinHelper.version 计算 binary version\\n优先 identity"]
180
+ I --> J{"binary source 命中?"}
181
+ J -->|命中| K["替换为 binary spec\\npromote local path pod"]
182
+ J -->|未命中| L["回退源码\\n保留 external/local source"]
183
+ H --> M["生成最终 Pods 工程"]
184
+ K --> M
185
+ L --> M
186
+ ```
187
+
188
+ ## 环境变量与开关
189
+
190
+ | 变量 | 默认值 | 作用 |
191
+ |---|---|---|
192
+ | `MEITU_USE_BINARIES` | `true`(未设时) | Podfile 层总开关。设为 `false` 可全源码。 |
193
+ | `MEITU_USE_CONFIGURATION` | `Debug` | 指定二进制版本计算和命中所用配置(如 `Debug`/`Release`)。 |
194
+ | `MEITU_FORCE_LOCAL_PATH_SOURCE` | `false` | 兼容旧逻辑。设为 `true` 且 `configuration_env == 'dev'` 时,本地 `:path` 组件强制进源码白名单。 |
195
+ | `MEITU_BIN_WORKTREE_DEBUG` | `0` | 设为 `1` 输出 worktree identity 统计和切换调试日志。 |
196
+ | `MEITU_BIN_BUILD_LOCAL_PATH_PODS` | `true` | 控制 `pod bin build-all` 是否制作本地 `:path` 组件。设为 `false` 回退旧行为(跳过本地库)。 |
197
+
198
+ ## 二进制制作(build-all)
199
+
200
+ 进入 `Podfile` 所在目录执行:
201
+
202
+ ```shell
203
+ pod bin build-all --configuration=Debug
204
+ ```
205
+
206
+ 常见参数:
35
207
 
36
208
  | 选项 | 含义 |
37
209
  |---|---|
38
- | `--clean` | 全部二进制包制作完成后删除编译临时目录 |
39
- | `--clean-single` | 每制作完一个二进制包就删除该编译临时目录 |
40
- | `--repo-update` | 更新`Podfile`中指定的`repo`仓库 |
41
- | `--full-build` | 是否全量编译 |
42
- | `--skip-simulator` | 是否跳过模拟器编译 |
43
- | `--configuration=configName` | 在构建每个目标时使用`configName`指定构建配置,如:`Debug`、`Release`等 |
210
+ | `--clean` | 全部制作完成后清理临时目录 |
211
+ | `--clean-single` | 每个组件完成后立刻清理临时目录 |
212
+ | `--repo-update` | 先更新 Podfile 里声明的 source 仓库 |
213
+ | `--full-build` | 强制全量制作(不复用已存在 binary) |
214
+ | `--skip-simulator` | 跳过模拟器架构制作 |
215
+ | `--configuration=configName` | 指定构建配置 |
216
+ | `--shell-project` | 基于壳工程产物执行打包 |
44
217
 
45
- > 如果想查看详细信息,可以使用`pod bin build-all --help`来查看帮助文档
218
+ ### build-all 对不同依赖类型的处理
46
219
 
47
- ### 使用二进制
220
+ 1. 本地 `:path` 组件:默认参与制作。
221
+ 2. external source(如 `:git/:commit/:tag`):
222
+ 在普通 `build-all` 流程中,命中 `sandbox.checkout_sources` 即跳过,不参与制作。
223
+ 在 `--shell-project` 流程中,只有当组件被标记进 `PodUpdateConfig.external_source_binary` 集合时才会参与制作;未命中集合仍跳过。
224
+ `external_source_binary` 集合由 resolver 阶段记录,目的是给后续壳工程制作场景提供“可制作的 external 组件”输入。
225
+ 3. 无源码组件:跳过。
226
+ 4. 已存在同版本 binary:跳过。
48
227
 
49
- 在`Podfile`中添加如下代码,然后执行`pod install`即可
228
+ ## 流程图
50
229
 
51
- ```ruby
52
- # 加载插件
53
- plugin 'cocoapods-meitu-bin'
54
- # 开启二进制
55
- use_binaries!
56
- # 设置源码白名单
57
- set_use_source_pods ['AFNetworking']
230
+ ### 1) `pod install`:monorepo 本地 path 与二进制动态切换
231
+
232
+ ```mermaid
233
+ flowchart TD
234
+ A["pod install"] --> B["source_provider / pre_install"]
235
+ B --> C["resolve 阶段预计算 local path worktree_identity"]
236
+ C --> D["构建 dependency_relationships"]
237
+ D --> E["resolver_specs_by_target 按 rspec 决策 source/binary"]
238
+ E --> F{"是否强制源码\n(MEITU_FORCE_LOCAL_PATH_SOURCE / use_source_pods)?"}
239
+ F -->|是| G["保留源码(Development Pods)"]
240
+ F -->|否| H["计算 binary version\n(identity/commit/tag/依赖关系)"]
241
+ H --> I{"binary spec 是否命中?"}
242
+ I -->|命中| J["promote local path pod -> binary pod\n移出 Development Pods"]
243
+ I -->|未命中| K["回退源码(保留 :path / external source)"]
244
+ G --> L["生成 Pods 工程/Lockfile"]
245
+ J --> L
246
+ K --> L
58
247
  ```
248
+
249
+ ### 2) `pod bin build-all`:二进制制作流程(含本地 `:path`)
250
+
251
+ ```mermaid
252
+ flowchart TD
253
+ A["pod bin build-all"] --> B["读取 BinConfig.yaml + CLI 参数"]
254
+ B --> C["Analyzer 分析 pod_targets"]
255
+ C --> D["遍历每个 pod_target"]
256
+ D --> E{"命中黑名单/不在白名单?"}
257
+ E -->|是| D2["跳过"]
258
+ E -->|否| F{"本地 :path 且\nMEITU_BIN_BUILD_LOCAL_PATH_PODS=false?"}
259
+ F -->|是| D2
260
+ F -->|否| G{"external source 或无源码?"}
261
+ G -->|是| D2
262
+ G -->|否| H["计算 binary version"]
263
+ H --> I{"binary 已存在且非 full-build?"}
264
+ I -->|是| D2
265
+ I -->|否| J["编译产物 -> create_binary"]
266
+ J --> K["zip + upload artifact"]
267
+ K --> L["生成并 push binary podspec"]
268
+ L --> M["汇总 Success/Fail 结果"]
269
+ D2 --> M
270
+ ```
271
+
272
+ ## 常用命令
273
+
274
+ ```shell
275
+ # 开启调试日志看切换细节
276
+ MEITU_BIN_WORKTREE_DEBUG=1 pod install
277
+
278
+ # 强制本地 :path 组件全源码(应急排障,仅在 Podfile 的 configuration_env == 'dev' 时生效)
279
+ MEITU_FORCE_LOCAL_PATH_SOURCE=true pod install
280
+
281
+ # 本地 :path 组件也参与制作(默认就是 true)
282
+ MEITU_BIN_BUILD_LOCAL_PATH_PODS=true pod bin build-all --configuration=Debug
283
+
284
+ # 回退旧行为:build-all 跳过本地 :path
285
+ MEITU_BIN_BUILD_LOCAL_PATH_PODS=false pod bin build-all --configuration=Debug
286
+ ```
287
+
288
+ ## 排障建议
289
+
290
+ 1. 切换不生效:先开 `MEITU_BIN_WORKTREE_DEBUG=1`,看 `resolved/missing` 与 `forced_source` 统计。
291
+ 2. 命中旧 binary:检查是否有未跟踪文件变化、`set_use_source_pods` 白名单、`MEITU_FORCE_LOCAL_PATH_SOURCE`。
292
+ 3. binary 存在但安装失败:检查 binary spec 与 artifact 是否同时可访问。
293
+ 4. 本地库制作被跳过:检查 `MEITU_BIN_BUILD_LOCAL_PATH_PODS` 是否被设为 `false`。
294
+
295
+ ## 参考
296
+
297
+ - [美图秀秀 iOS 客户端二进制之路](https://juejin.cn/post/7175023366783385659)
@@ -49,6 +49,7 @@ module Pod
49
49
  @configuration = argv.option('configuration', 'Debug')
50
50
  @base_dir = ENV['POD_BUILD_BASE_DIR'] || "#{Pathname.pwd}/all_build/Build"
51
51
  @version_helper = BinHelper.new
52
+ @subspec_root_payload_white_list = []
52
53
  super
53
54
  end
54
55
 
@@ -117,7 +118,9 @@ module Pod
117
118
  @post_build = build_config['post_build']
118
119
  @black_list = build_config['black_list']
119
120
  @write_list = build_config['write_list']
121
+ @subspec_root_payload_white_list = Array(build_config['subspec_root_payload_white_list']).compact
120
122
  UI.info "BinConfig:`#{@write_list}`".yellow
123
+ UI.info "BinConfig subspec_root_payload_white_list:`#{@subspec_root_payload_white_list}`".yellow
121
124
  end
122
125
  end
123
126
 
@@ -211,19 +214,18 @@ module Pod
211
214
  created_pods = []
212
215
  pod_targets.map do |pod_target|
213
216
  begin
214
- version = @version_helper.version(pod_target.pod_name, pod_target.root_spec.version.to_s, @analyze_result.specifications, @configuration, podfile.include_dependencies?)
215
217
  # 黑名单(不分全量和非全量)
216
218
  next if skip_build?(pod_target)
217
219
  # 白名单(有白名单,只看白名单,不分全量和非全量)
218
- UI.info "USE_WHITELIST:`#{ ENV['USE_WHITELIST']}`".yellow
219
- UI.info "include?:`#{@write_list.include?(pod_target.pod_name)}`".yellow
220
220
  next if ENV['USE_WHITELIST'] && !@write_list.nil? && !@write_list.empty? && !@write_list.include?(pod_target.pod_name)
221
- # 本地库
222
- if @sandbox.local?(pod_target.pod_name)
221
+ local_pod = @sandbox.local?(pod_target.pod_name)
222
+ if local_pod && !build_local_path_pods?
223
223
  local_pods << pod_target.pod_name
224
- show_skip_tip("#{pod_target.pod_name} 是本地库")
224
+ show_skip_tip("#{pod_target.pod_name} 是本地库(MEITU_BIN_BUILD_LOCAL_PATH_PODS=false,跳过制作)")
225
225
  next
226
226
  end
227
+ # 默认将 :path 组件纳入 build-all,和 resolver 的动态切换保持同一套版本语义。
228
+ show_build_tip("#{pod_target.pod_name} 是本地库,将参与二进制制作") if local_pod
227
229
  # 外部源(如 git)
228
230
  if @sandbox.checkout_sources[pod_target.pod_name]
229
231
  external_pods << pod_target.pod_name
@@ -236,9 +238,11 @@ module Pod
236
238
  show_skip_tip("#{pod_target.pod_name} 无需编译")
237
239
  next
238
240
  end
241
+ version = @version_helper.version(pod_target.pod_name, pod_target.root_spec.version.to_s, @analyze_result.specifications, @configuration, podfile.include_dependencies?)
239
242
  # 非全量编译、不在白名单中且已经有相应的二进制版本
240
243
  if has_created_binary?(pod_target.pod_name, version)
241
244
  created_pods << pod_target.pod_name
245
+ # 版本命中后直接复用已有 binary,避免重复构建上传。
242
246
  show_skip_tip("#{pod_target.pod_name}(#{version}) 已经有二进制版本了")
243
247
  next
244
248
  end
@@ -257,7 +261,13 @@ module Pod
257
261
  fail_pods << pod_target.pod_name unless result
258
262
  next unless result
259
263
  # 生成二进制podspec并上传
260
- podspec_creator = PodspecUtil.new(pod_target, version, builder.build_as_framework?, @configuration)
264
+ podspec_creator = PodspecUtil.new(
265
+ pod_target,
266
+ version,
267
+ builder.build_as_framework?,
268
+ @configuration,
269
+ subspec_root_payload_white_list: @subspec_root_payload_white_list
270
+ )
261
271
  bin_spec = podspec_creator.create_binary_podspec
262
272
  bin_spec_file = podspec_creator.write_binary_podspec(bin_spec)
263
273
  result = podspec_creator.push_binary_podspec(bin_spec_file)
@@ -280,7 +290,8 @@ module Pod
280
290
  'No Source File' => binary_pods,
281
291
  'Created Binary' => created_pods,
282
292
  'Black List' => @black_list || [],
283
- 'Write List' => @write_list || []
293
+ 'Write List' => @write_list || [],
294
+ 'Subspec Root Payload White List' => @subspec_root_payload_white_list || []
284
295
  }
285
296
  show_results(results)
286
297
  results
@@ -302,24 +313,25 @@ module Pod
302
313
  # mutex = Mutex.new
303
314
  pod_targets.map do |pod_target|
304
315
  begin
305
- version = @version_helper.version(pod_target.pod_name, pod_target.root_spec.version.to_s, @analyze_result.specifications, @configuration, podfile.include_dependencies?)
306
316
  # 黑名单(不分全量和非全量)
307
317
  next if skip_build?(pod_target)
308
318
  # 白名单(有白名单,只看白名单,不分全量和非全量)
309
319
  next if ENV['USE_WHITELIST'] && !@write_list.nil? && !@write_list.empty? && !@write_list.include?(pod_target.pod_name)
310
- # 本地库
311
- if @sandbox.local?(pod_target.pod_name)
320
+ local_pod = @sandbox.local?(pod_target.pod_name)
321
+ if local_pod && !build_local_path_pods?
312
322
  local_pods << pod_target.pod_name
313
- show_skip_tip("#{pod_target.pod_name} 是本地库")
323
+ show_skip_tip("#{pod_target.pod_name} 是本地库(MEITU_BIN_BUILD_LOCAL_PATH_PODS=false,跳过制作)")
314
324
  next
315
325
  end
326
+ # shell-project 模式下同样遵循本地 :path 组件默认参与制作的策略。
327
+ show_build_tip("#{pod_target.pod_name} 是本地库,将参与二进制制作") if local_pod
316
328
  # 外部源(如 git)
317
329
  excluded_names = PodUpdateConfig.external_source_binary
318
- .map { |s| s[:name] }
330
+ .map { |s| s[:name] || s['name'] }
319
331
  .compact # 过滤nil值
320
332
  .uniq # 去重
321
333
 
322
- if @sandbox.checkout_sources[pod_target.pod_name] && !excluded_names.include?(pod_target.pod_name) && !pod_target.should_build?
334
+ if excluded_names.include?(pod_target.pod_name) || (@sandbox.checkout_sources[pod_target.pod_name] && !pod_target.should_build?)
323
335
  external_pods << pod_target.pod_name
324
336
  show_skip_tip("#{pod_target.pod_name} 以external方式引入")
325
337
  next
@@ -330,6 +342,7 @@ module Pod
330
342
  show_skip_tip("#{pod_target.pod_name} 无需编译")
331
343
  next
332
344
  end
345
+ version = @version_helper.version(pod_target.pod_name, pod_target.root_spec.version.to_s, @analyze_result.specifications, @configuration, podfile.include_dependencies?)
333
346
  # 非全量编译、不在白名单中且已经有相应的二进制版本
334
347
  if has_created_binary?(pod_target.pod_name, version)
335
348
  created_pods << pod_target.pod_name
@@ -383,7 +396,13 @@ module Pod
383
396
  fail_pods << pod_target.pod_name unless result
384
397
  next unless result
385
398
  # 生成二进制podspec并上传
386
- podspec_creator = PodspecUtil.new(pod_target, version, builder.build_as_framework?, @configuration)
399
+ podspec_creator = PodspecUtil.new(
400
+ pod_target,
401
+ version,
402
+ builder.build_as_framework?,
403
+ @configuration,
404
+ subspec_root_payload_white_list: @subspec_root_payload_white_list
405
+ )
387
406
  bin_spec = podspec_creator.create_binary_podspec
388
407
  bin_spec_file = podspec_creator.write_binary_podspec(bin_spec)
389
408
  result = podspec_creator.push_binary_podspec(bin_spec_file)
@@ -407,7 +426,8 @@ module Pod
407
426
  'No Source File' => binary_pods,
408
427
  'Created Binary' => created_pods,
409
428
  'Black List' => @black_list || [],
410
- 'Write List' => @write_list || []
429
+ 'Write List' => @write_list || [],
430
+ 'Subspec Root Payload White List' => @subspec_root_payload_white_list || []
411
431
  }
412
432
 
413
433
  show_results(results)
@@ -419,11 +439,21 @@ module Pod
419
439
  UI.info title.yellow
420
440
  end
421
441
 
442
+ def show_build_tip(title)
443
+ UI.info title.green
444
+ end
445
+
422
446
  # 是否跳过编译
423
447
  def skip_build?(pod_target)
424
448
  !@black_list.nil? && !@black_list.empty? && @black_list.include?(pod_target.pod_name)
425
449
  end
426
450
 
451
+ # 本地 :path 组件是否参与 build-all 二进制制作
452
+ # 默认开启;设置 MEITU_BIN_BUILD_LOCAL_PATH_PODS=false 可回退旧行为(本地库跳过)
453
+ def build_local_path_pods?
454
+ ENV['MEITU_BIN_BUILD_LOCAL_PATH_PODS'] != 'false'
455
+ end
456
+
427
457
  # 展示结果
428
458
  def show_results(results)
429
459
  UI.title '打包结果:'.green do
@@ -178,6 +178,10 @@ class PodUpdateConfig
178
178
  # 记录 Podfile 中通过 :commit 指定的本地组件及其 git 信息,格式: { pod_name => { git: git_url, commit: commit_hash } }
179
179
  # mbox 激活组件,避免关联组件都切换到源码,影响编译速度
180
180
  @@pods_with_commit = {}
181
+ # local path 组件在当前一次 resolve 中的身份快照(root_name => worktree_identity)
182
+ @@worktree_identities = {}
183
+ # 已从 Development Pods 提升为 binary 的 local path 组件(用于去重和调试)
184
+ @@promoted_local_pods = {}
181
185
 
182
186
  # 设置依赖关系集合
183
187
  def self.set_dependency_relationships(relationships)
@@ -230,6 +234,33 @@ class PodUpdateConfig
230
234
 
231
235
  end
232
236
 
237
+ def self.set_worktree_identity(pod_name, identity)
238
+ return if identity.nil? || identity.empty?
239
+
240
+ @@worktree_identities[Pod::Specification.root_name(pod_name)] = identity
241
+ end
242
+
243
+ def self.get_worktree_identity(pod_name)
244
+ @@worktree_identities[Pod::Specification.root_name(pod_name)]
245
+ end
246
+
247
+ def self.worktree_identities
248
+ @@worktree_identities
249
+ end
250
+
251
+ def self.promote_local_pod(pod_name, metadata = {})
252
+ # metadata 默认包含命中的 binary version 与 identity,便于后续日志/排障追踪。
253
+ @@promoted_local_pods[Pod::Specification.root_name(pod_name)] = metadata
254
+ end
255
+
256
+ def self.promoted_local_pod?(pod_name)
257
+ @@promoted_local_pods.key?(Pod::Specification.root_name(pod_name))
258
+ end
259
+
260
+ def self.promoted_local_pods
261
+ @@promoted_local_pods
262
+ end
263
+
233
264
  def self.set_external_source_binary(dic)
234
265
  @@external_source_binary << dic
235
266
  end
@@ -400,16 +431,12 @@ class PodUpdateConfig
400
431
  @@pods = []
401
432
  @@lockfile = nil
402
433
  @@is_clear = true
434
+ @@worktree_identities = {}
435
+ @@promoted_local_pods = {}
403
436
  end
404
437
  def self.is_clear
405
438
  @@is_clear
406
439
  end
407
-
408
- def self.clear
409
- @@pods = []
410
- @@lockfile = nil
411
- @@is_clear = true
412
- end
413
440
  def self.is_podfile_lock_nil
414
441
  @@is_podfile_lock_nil
415
442
  end
@@ -1,5 +1,5 @@
1
1
  module CBin
2
- VERSION = "3.0.2"
2
+ VERSION = "3.0.3"
3
3
  end
4
4
 
5
5
  module Pod
@@ -21,7 +21,12 @@ module CBin
21
21
  # 将当前 pod 自身的 commit/tag 加入版本号计算字符串
22
22
  # 修复:Podfile 中通过 :commit/:tag 指定的 pod,其自身 commit 信息必须参与 MD5 计算
23
23
  # 否则更新 Podfile 的 commit 后版本号不变,会错误命中旧的二进制缓存
24
- self_checkout = PodUpdateConfig.get_checkout_option(pod_name)
24
+ self_checkout = PodUpdateConfig.get_worktree_identity(pod_name)
25
+ if ENV['MEITU_BIN_WORKTREE_DEBUG'] == '1' && self_checkout
26
+ UI.puts "#{pod_name} worktree_identity:#{self_checkout}".yellow
27
+ end
28
+
29
+ self_checkout ||= PodUpdateConfig.get_checkout_option(pod_name)
25
30
  self_checkout ||= PodUpdateConfig.get_with_commit(pod_name)
26
31
  specs << "#{pod_name}(#{self_checkout})" if self_checkout
27
32
 
@@ -67,6 +72,13 @@ module CBin
67
72
  return '' if all_dependencies.nil? || all_dependencies.empty?
68
73
  result = []
69
74
  all_dependencies.each do |pod_name|
75
+ worktree_identity = PodUpdateConfig.get_worktree_identity(pod_name)
76
+ if worktree_identity
77
+ # local :path 组件优先采用 worktree identity,避免仅靠 commit/tag 导致命中旧缓存。
78
+ result << "#{pod_name}(#{worktree_identity})"
79
+ next
80
+ end
81
+
70
82
  # 优先使用 get_checkout_option 中的 commit 或 tag
71
83
  checkout_option = PodUpdateConfig.get_checkout_option(pod_name)
72
84
  if checkout_option
@@ -4,11 +4,12 @@ module CBin
4
4
  class PodspecUtil
5
5
  include Pod
6
6
 
7
- def initialize(pod_target, version, build_as_framework = false, configuration = 'Debug')
7
+ def initialize(pod_target, version, build_as_framework = false, configuration = 'Debug', options = {})
8
8
  @pod_target = pod_target
9
9
  @version = version
10
10
  @build_as_framework = build_as_framework
11
11
  @configuration = configuration
12
+ @subspec_root_payload_white_list = Array(options[:subspec_root_payload_white_list]).compact
12
13
  end
13
14
 
14
15
  # 创建二进制podspec
@@ -93,9 +94,9 @@ module CBin
93
94
  private
94
95
 
95
96
  # 删除无用的字段
96
- def delete_unused(spec)
97
+ def delete_unused(spec, keep_resource_bundles: false)
97
98
  spec.delete('project_header_files')
98
- spec.delete('resource_bundles')
99
+ spec.delete('resource_bundles') unless keep_resource_bundles
99
100
  spec.delete('exclude_files')
100
101
  spec.delete('preserve_paths')
101
102
  spec.delete('prepare_command')
@@ -103,35 +104,51 @@ module CBin
103
104
 
104
105
  # 处理subspecs
105
106
  def handle_subspecs(spec)
107
+ root_payload_only = subspec_root_payload_only?
108
+ if root_payload_only
109
+ UI.info "PodspecUtil:`#{@pod_target.pod_name}` 命中 subspec_root_payload_white_list,subspec 不复制 vendored/resources".yellow
110
+ end
106
111
  spec['subspecs'].map do |subspec|
107
112
  # 处理单个subspec
108
- handle_single_subspec(subspec, spec)
113
+ handle_single_subspec(subspec, spec, root_payload_only)
109
114
  # 递归处理subspec
110
- recursive_handle_subspecs(subspec['subspecs'], spec)
115
+ recursive_handle_subspecs(subspec['subspecs'], spec, root_payload_only)
111
116
  end if spec && spec['subspecs']
112
117
  end
113
118
 
114
119
  # 递归处理subspecs
115
- def recursive_handle_subspecs(subspecs, spec)
120
+ def recursive_handle_subspecs(subspecs, spec, root_payload_only)
116
121
  subspecs.map do |s|
117
122
  # 处理单个subspec
118
- handle_single_subspec(s, spec)
123
+ handle_single_subspec(s, spec, root_payload_only)
119
124
  # 递归处理
120
- recursive_handle_subspecs(s['subspecs'], spec)
125
+ recursive_handle_subspecs(s['subspecs'], spec, root_payload_only)
121
126
  end if subspecs
122
127
  end
123
128
 
124
129
  # 处理单个subspec
125
- def handle_single_subspec(subspec, spec)
130
+ def handle_single_subspec(subspec, spec, root_payload_only)
126
131
  subspec['source_files'] = spec['source_files']
127
132
  subspec['public_header_files'] = spec['public_header_files']
128
133
  subspec['private_header_files'] = spec['private_header_files']
129
- subspec['vendored_frameworks'] = spec['vendored_frameworks']
130
- subspec['vendored_libraries'] = spec['vendored_libraries']
131
- subspec['resources'] = spec['resources']
134
+ if root_payload_only
135
+ # 仅 root 声明二进制 payload,避免 subspec 重复嵌入同名 framework
136
+ subspec.delete('vendored_frameworks')
137
+ subspec.delete('vendored_libraries')
138
+ subspec.delete('resources')
139
+ subspec.delete('resource_bundles')
140
+ else
141
+ subspec['vendored_frameworks'] = spec['vendored_frameworks']
142
+ subspec['vendored_libraries'] = spec['vendored_libraries']
143
+ subspec['resources'] = spec['resources']
144
+ end
132
145
  # 删除无用字段
133
146
  subspec.delete('source')
134
- delete_unused(subspec)
147
+ delete_unused(subspec, keep_resource_bundles: !root_payload_only)
148
+ end
149
+
150
+ def subspec_root_payload_only?
151
+ @subspec_root_payload_white_list.include?(@pod_target.pod_name)
135
152
  end
136
153
 
137
154
  def source
@@ -0,0 +1,93 @@
1
+ require 'digest'
2
+ require 'find'
3
+ require 'open3'
4
+ require 'pathname'
5
+
6
+ module CBin
7
+ module Helpers
8
+ module WorktreeIdentity
9
+ module_function
10
+ # 规则版本会进入调试日志,便于在不同实现版本间快速对齐排障。
11
+ RULE_VERSION = 'v1_git_tree_diff_untracked_sha1'.freeze
12
+
13
+ def rule_version
14
+ RULE_VERSION
15
+ end
16
+
17
+ def for_sandbox_pod(sandbox, pod_name)
18
+ podspec_path = sandbox.local_podspec(pod_name)
19
+ return nil unless podspec_path
20
+
21
+ # local podspec 所在目录即该组件在 monorepo 中的实际模块目录。
22
+ compute(Pathname.new(podspec_path).dirname)
23
+ end
24
+
25
+ def compute(module_dir)
26
+ module_dir = Pathname.new(module_dir).expand_path
27
+ git_root = git_output(module_dir.to_s, %w[rev-parse --show-toplevel])
28
+ return fallback_directory_digest(module_dir) if git_root.nil? || git_root.empty?
29
+
30
+ git_root_path = Pathname.new(git_root)
31
+ relative_path = module_dir.relative_path_from(git_root_path).to_s
32
+ # 四段输入覆盖 HEAD/index/worktree/untracked:
33
+ # 1) base_tree: HEAD 下模块快照
34
+ # 2) staged_diff: 已暂存改动
35
+ # 3) unstaged_diff: 未暂存改动
36
+ # 4) untracked_digest: 未跟踪文件列表 + 内容
37
+ base_tree = git_output(git_root, ['rev-parse', "HEAD:#{relative_path}"], allow_failure: true).to_s
38
+ staged_diff = git_output(git_root, ['diff', '--cached', '--binary', '--no-ext-diff', '--', relative_path], allow_failure: true).to_s
39
+ unstaged_diff = git_output(git_root, ['diff', '--binary', '--no-ext-diff', '--', relative_path], allow_failure: true).to_s
40
+ untracked_lines = git_output(git_root, ['ls-files', '--others', '--exclude-standard', '--', relative_path], allow_failure: true).to_s
41
+
42
+ untracked_digest = Digest::SHA1.new
43
+ untracked_lines.lines.map(&:chomp).reject(&:empty?).sort.each do |entry|
44
+ abs_path = git_root_path.join(entry)
45
+ untracked_digest.update(entry)
46
+ untracked_digest.update("\0")
47
+ untracked_digest.file(abs_path.to_s) if abs_path.file?
48
+ untracked_digest.update("\0")
49
+ end
50
+
51
+ payload = [
52
+ base_tree,
53
+ Digest::SHA1.hexdigest(staged_diff),
54
+ Digest::SHA1.hexdigest(unstaged_diff),
55
+ untracked_digest.hexdigest,
56
+ ].join('|')
57
+
58
+ Digest::SHA1.hexdigest(payload)
59
+ rescue ArgumentError, Errno::ENOENT
60
+ # Git 查询异常时回退到全目录摘要,确保仍有稳定 identity 可用于版本计算。
61
+ fallback_directory_digest(module_dir)
62
+ end
63
+
64
+ def git_output(chdir, args, allow_failure: false)
65
+ stdout, status = Open3.capture2('git', *args, chdir: chdir)
66
+ return stdout.strip if status.success?
67
+ # allow_failure 为 true 的场景用于容忍“模块首次接入/路径不存在于 HEAD”等可预期情况。
68
+ return nil if allow_failure
69
+
70
+ nil
71
+ rescue Errno::ENOENT
72
+ nil
73
+ end
74
+
75
+ def fallback_directory_digest(module_dir)
76
+ digest = Digest::SHA1.new
77
+
78
+ # 非 git 场景:按相对路径 + 文件内容做摘要,保证目录态变化可感知。
79
+ Find.find(module_dir.to_s) do |path|
80
+ next if File.directory?(path)
81
+
82
+ rel_path = Pathname.new(path).relative_path_from(module_dir).to_s
83
+ digest.update(rel_path)
84
+ digest.update("\0")
85
+ digest.file(path)
86
+ digest.update("\0")
87
+ end
88
+
89
+ digest.hexdigest
90
+ end
91
+ end
92
+ end
93
+ end
@@ -80,7 +80,7 @@ module Pod
80
80
  UI.section 'Analyzing dependencies' do
81
81
  analyze(analyzer)
82
82
  excluded_names = PodUpdateConfig.external_source
83
- .map { |s| s[:name] }
83
+ .map { |s| s[:name] || s['name'] }
84
84
  .compact
85
85
  .uniq
86
86
 
@@ -73,7 +73,7 @@ module Pod
73
73
  #根据 CocoaPods 依赖处理机制,修改后的方法应增加过滤逻辑:
74
74
  def generate_external_sources_data(podfile)
75
75
  excluded_names = PodUpdateConfig.external_source
76
- .map { |s| s[:name] }
76
+ .map { |s| s[:name] || s['name'] }
77
77
  .compact # 过滤nil值
78
78
  .uniq # 去重
79
79
 
@@ -6,6 +6,7 @@ require 'cocoapods-meitu-bin/native/installation_options'
6
6
  require 'cocoapods-meitu-bin/gem_version'
7
7
  # require 'cocoapods-meitu-bin/command/bin/archive'
8
8
  require 'cocoapods-meitu-bin/helpers/buildAll/bin_helper'
9
+ require 'cocoapods-meitu-bin/helpers/worktree_identity'
9
10
  require 'cocoapods-meitu-bin/config/config'
10
11
 
11
12
  module Pod
@@ -150,16 +151,24 @@ module Pod
150
151
 
151
152
 
152
153
  if podfile.use_binaries?
154
+ # 预计算 local path 组件 identity,避免在 rspec 循环里重复扫描目录。
153
155
  path_pods = @podfile_dependency_cache.podfile_dependencies
154
156
  .select { |dep| dep.external_source && dep.external_source[:path] }
155
157
  .map(&:root_name)
156
158
  .uniq
159
+ local_pods = (sandbox.development_pods.keys + path_pods)
160
+ .map { |name| Pod::Specification.root_name(name) }
161
+ .uniq
157
162
  path_commit_pods = {}
158
- path_pods.each do |pod_name|
163
+ local_pods.each do |pod_name|
164
+ worktree_identity = CBin::Helpers::WorktreeIdentity.for_sandbox_pod(sandbox, pod_name)
165
+ PodUpdateConfig.set_worktree_identity(pod_name, worktree_identity) if worktree_identity
159
166
  if PodUpdateConfig.pods_with_commit[pod_name]
167
+ # :path + :commit 的组件继续保留 commit 输入,避免 identity 缺失时版本退化。
160
168
  path_commit_pods[pod_name] = PodUpdateConfig.pods_with_commit[pod_name]
161
169
  end
162
170
  end
171
+ print_worktree_identity_debug(local_pods, 'after_precompute')
163
172
  PodUpdateConfig.parse_checkout_options_from_specs(checkout_options,podfile,path_commit_pods)
164
173
  end
165
174
 
@@ -238,8 +247,12 @@ module Pod
238
247
  # end
239
248
  else
240
249
  name_version = "#{pod_name}_#{dep_info[:version].version}"
250
+ worktree_identity = PodUpdateConfig.get_worktree_identity(pod_name)
241
251
  opts = path_commit_pods&.[](pod_name)
242
- if opts && opts[:commit]
252
+ if worktree_identity
253
+ name_worktree = "#{pod_name}_#{worktree_identity}"
254
+ pod_names << name_worktree unless pod_names.join("|").include?(pod_name)
255
+ elsif opts && opts[:commit]
243
256
  commit = opts[:commit]
244
257
  name_commit = "#{pod_name}_#{commit}"
245
258
  pod_names << name_commit unless pod_names.join("|").include?(pod_name)
@@ -278,14 +291,29 @@ module Pod
278
291
 
279
292
  sources_manager = Config.instance.sources_manager
280
293
  use_source_pods = podfile.use_source_pods
294
+ use_source_roots = use_source_pods.map { |name| Pod::Specification.root_name(name) }.uniq
281
295
 
282
296
  # 从BinConfig读取black_list
283
297
  black_list = read_black_list
284
298
  use_source_pods.concat(black_list).uniq! unless black_list.nil?
299
+ use_source_roots = use_source_pods.map { |name| Pod::Specification.root_name(name) }.uniq
300
+ if podfile.use_binaries? && ENV['MEITU_BIN_WORKTREE_DEBUG'] == '1'
301
+ local_path_roots = sandbox.development_pods.keys.map { |name| Pod::Specification.root_name(name) }.uniq
302
+ forced_source_roots = local_path_roots & use_source_roots
303
+ candidate_binary_roots = local_path_roots - forced_source_roots
304
+ UI.puts "worktree_identity_debug(use_source): local_path=#{local_path_roots.size}, forced_source=#{forced_source_roots.size}, binary_candidates=#{candidate_binary_roots.size}".yellow
305
+ unless forced_source_roots.empty?
306
+ UI.puts "worktree_identity_debug(forced_source_local): #{forced_source_roots.sort.join(',')}".yellow
307
+ end
308
+ end
285
309
 
286
310
  specifications = specs_by_target.values.flatten.map(&:spec).uniq
287
311
 
288
312
  missing_binary_specs = []
313
+ # 注意这里取快照:后续 promote_local_path_pod_to_binary 会改 sandbox.local? 状态。
314
+ local_path_roots_snapshot = sandbox.development_pods.keys.map { |name| Pod::Specification.root_name(name) }.uniq
315
+ binary_version_by_local_root = {}
316
+ local_root_binary_state = {}
289
317
  specs_by_target.each do |target, rspecs|
290
318
  # use_binaries 并且 use_source_pods 不包含 本地可过滤
291
319
  use_binary_rspecs = if podfile.use_binaries? || podfile.use_binaries_selector
@@ -305,30 +333,52 @@ module Pod
305
333
 
306
334
  # developments 组件采用默认输入的 spec (development pods 的 source 为 nil)
307
335
  # 可以使 :podspec => "htts://IMYFoundation.podspec"可以走下去,by slj
308
- dependency_source = PodUpdateConfig.get_checkout_option(rspec.root.name)
336
+ root_name = Pod::Specification.root_name(rspec.root.name)
337
+ dependency_source = PodUpdateConfig.get_checkout_option(root_name)
338
+ # 注意:本轮中会调用 promote_local_path_pod_to_binary 修改 sandbox.local? 状态,
339
+ # 这里使用快照保证同一 root 的 root+subspec 判定一致,避免 mixed mode。
340
+ is_local_path_pod = local_path_roots_snapshot.include?(root_name)
341
+ local_root_state = if is_local_path_pod
342
+ local_root_binary_state[root_name] ||= { all_binary_hits: true, spec_version: nil }
343
+ end
344
+ if is_local_path_pod && PodUpdateConfig.get_worktree_identity(rspec.root.name).nil?
345
+ worktree_identity = CBin::Helpers::WorktreeIdentity.for_sandbox_pod(sandbox, rspec.root.name)
346
+ PodUpdateConfig.set_worktree_identity(rspec.root.name, worktree_identity) if worktree_identity
347
+ end
309
348
 
310
349
  # 关键修复:本地 path 依赖的组件(sandbox.local? 为 true)
311
350
  # 应该直接使用原始 rspec,不应走 spec repo 查找和替换逻辑
312
351
  # 否则如果 spec repo 中的旧版 podspec 缺少某些 subspecs,
313
352
  # subspec_by_name 会返回 nil,导致该 subspec 丢失
314
- if sandbox.local?(rspec.root.name)
353
+ if is_local_path_pod && !use_binary_rspecs.include?(rspec)
354
+ local_root_state[:all_binary_hits] = false if local_root_state
315
355
  next rspec
316
356
  end
317
357
 
318
358
  unless rspec.spec.respond_to?(:spec_source) && rspec.spec.spec_source
319
359
 
320
- if dependency_source.nil?
360
+ if dependency_source.nil? && !is_local_path_pod
321
361
  next rspec
322
362
  end
323
363
  end
324
364
 
325
365
  # 采用二进制依赖并且不为开发组件
326
366
  use_binary = use_binary_rspecs.include?(rspec)
367
+ if !use_binary && binary_version_by_local_root[root_name]
368
+ # 同一 local root 已命中 binary 时,其余 subspec 也强制复用同一 binary version
369
+ # 避免 root 与 subspec 混合 source/binary 造成 Swift module/header 不一致。
370
+ use_binary = true
371
+ end
327
372
 
328
373
  if use_binary
329
374
  source = sources_manager.binary_source
330
- configuration = ENV['configuration'] || podfile.configuration
331
- spec_version = version_helper.version(rspec.root.name, rspec.spec.version, specifications, configuration, podfile.include_dependencies?)
375
+ spec_version = binary_version_by_local_root[root_name]
376
+ if spec_version.nil?
377
+ configuration = ENV['configuration'] || podfile.configuration
378
+ spec_version = version_helper.version(rspec.root.name, rspec.spec.version, specifications, configuration, podfile.include_dependencies?)
379
+ binary_version_by_local_root[root_name] = spec_version if is_local_path_pod
380
+ end
381
+ local_root_state[:spec_version] ||= spec_version if local_root_state
332
382
  else
333
383
  # 关键修复:对于通过 commit/tag 指定的组件,在切换到源码时
334
384
  # 应该保持原始 rspec,让 CocoaPods 走正常的 external source 下载流程
@@ -368,6 +418,10 @@ module Pod
368
418
 
369
419
  # 这里可能出现分析依赖的 source 和切换后的 source 对应 specification 的 subspec 对应不上
370
420
  # 造成 subspec_by_name 返回 nil,这个是正常现象
421
+ if specification.nil? && is_local_path_pod && use_binary && local_root_state
422
+ local_root_state[:all_binary_hits] = false
423
+ binary_version_by_local_root.delete(root_name)
424
+ end
371
425
  next unless specification
372
426
  is_exist_binary_version = true
373
427
  used_by_only = if Pod.match_version?('~> 1.7')
@@ -386,6 +440,9 @@ module Pod
386
440
  # 没有从新的 source 找到对应版本组件
387
441
  missing_binary_specs << rspec.spec if use_binary
388
442
  is_exist_binary_version = false
443
+ # 同一 local root 若某个 subspec 未命中,需要回退该 root 的 binary 版本缓存。
444
+ binary_version_by_local_root.delete(root_name) if is_local_path_pod
445
+ local_root_state[:all_binary_hits] = false if is_local_path_pod && use_binary && local_root_state
389
446
 
390
447
  # 关键修复:对于有 commit/tag 指定的组件,二进制不存在时:
391
448
  # 1. 将完整的 checkout_options 添加到 sandbox.checkout_sources
@@ -420,18 +477,29 @@ module Pod
420
477
  end
421
478
  # 有对应二进制组件版本才进行替换记录,否则走之前pre-downloading 逻辑
422
479
  if is_exist_binary_version && sandbox.predownloaded?(rspec.root.name) && spec_version.is_a?(String) && spec_version.include?("bin")
423
- PodUpdateConfig.set_external_source({"name":rspec.root.name,"source":dependency_source})
480
+ PodUpdateConfig.set_external_source({ name: rspec.root.name, source: dependency_source })
424
481
  end
425
482
  if $ARGV.include?("bin") || !is_exist_binary_version
426
483
  # 记录为制作的 external_source 组件,为后续二进制制作准备
427
484
  if dependency_source && spec_version.is_a?(String) && spec_version.include?("bin")
428
- PodUpdateConfig.set_external_source_binary({"name":rspec.root.name,"source":dependency_source})
485
+ PodUpdateConfig.set_external_source_binary({ name: rspec.root.name, source: dependency_source })
429
486
  end
430
487
  end
431
488
 
432
489
  rspec
433
490
  end.compact
434
491
  end
492
+ local_root_binary_state.each do |root_name, state|
493
+ next unless state[:all_binary_hits]
494
+ next unless state[:spec_version]
495
+
496
+ # 本地 :path 仅在同一 root 的 binary 解析全部成功后,统一提升为普通 binary pod。
497
+ promote_local_path_pod_to_binary(root_name, state[:spec_version])
498
+ end
499
+ if podfile.use_binaries?
500
+ local_roots = specs_by_target.values.flatten.map { |rs| Pod::Specification.root_name(rs.root.name) }.uniq.select { |name| sandbox.local?(name) }
501
+ print_worktree_identity_debug(local_roots, 'after_resolver_specs')
502
+ end
435
503
 
436
504
  def print_dependency_tree(vertex, indent = 0)
437
505
  puts "#{' ' * indent}#{vertex.name} (#{vertex.payload&.version || '?'})"
@@ -480,6 +548,34 @@ module Pod
480
548
  end
481
549
  end
482
550
 
551
+ def print_worktree_identity_debug(local_pods, stage)
552
+ return unless ENV['MEITU_BIN_WORKTREE_DEBUG'] == '1'
553
+
554
+ roots = local_pods.map { |name| Pod::Specification.root_name(name) }.uniq.sort
555
+ missing = roots.reject { |name| PodUpdateConfig.get_worktree_identity(name) }
556
+ if stage == 'after_precompute'
557
+ UI.puts "worktree_identity_debug(rule): #{CBin::Helpers::WorktreeIdentity.rule_version}".yellow
558
+ end
559
+ UI.puts "worktree_identity_debug(#{stage}): local=#{roots.size}, resolved=#{roots.size - missing.size}, missing=#{missing.size}".yellow
560
+ unless missing.empty?
561
+ UI.puts "worktree_identity_debug_missing(#{stage}): #{missing.join(',')}".yellow
562
+ end
563
+ end
564
+
565
+ def promote_local_path_pod_to_binary(pod_name, spec_version)
566
+ root_name = Pod::Specification.root_name(pod_name)
567
+ return if PodUpdateConfig.promoted_local_pod?(root_name)
568
+
569
+ # 提升后的 root 不再视为本地 development pod,后续安装流程将按 binary pod 处理。
570
+ PodUpdateConfig.promote_local_pod(root_name, {
571
+ version: spec_version,
572
+ identity: PodUpdateConfig.get_worktree_identity(root_name),
573
+ })
574
+ PodUpdateConfig.set_external_source({ name: root_name, source: { promoted_from: 'path' } })
575
+ sandbox.development_pods.delete(root_name)
576
+ sandbox.remove_local_podspec(root_name) if sandbox.respond_to?(:remove_local_podspec)
577
+ end
578
+
483
579
  # 获取组件的所有依赖(包括直接和间接依赖)
484
580
  def get_all_dependencies(vertex, visited = Set.new, dependencies = {})
485
581
  return dependencies if vertex.nil? || visited.include?(vertex.name)
@@ -221,8 +221,10 @@ Pod::HooksManager.register('cocoapods-meitu-bin', :pre_install) do |_context|
221
221
  end
222
222
  # get_podfile_lock
223
223
 
224
- # 有插件/本地库 且是dev环境下,默认进入源码白名单 过滤 archive命令
225
- if _context.podfile.plugins.keys.include?('cocoapods-meitu-bin') && _context.podfile.configuration_env == 'dev'
224
+ # 兼容旧逻辑:dev 环境下将 :path 组件自动加入源码白名单
225
+ # 默认关闭,避免阻断动态二进制切换。
226
+ force_local_path_source = ENV['MEITU_FORCE_LOCAL_PATH_SOURCE'] == 'true'
227
+ if force_local_path_source && _context.podfile.plugins.keys.include?('cocoapods-meitu-bin') && _context.podfile.configuration_env == 'dev'
226
228
  dependencies = _context.podfile.dependencies
227
229
  dependencies.each do |d|
228
230
  next unless d.respond_to?(:external_source) &&
@@ -231,7 +233,7 @@ Pod::HooksManager.register('cocoapods-meitu-bin', :pre_install) do |_context|
231
233
  $ARGV[1] != 'archive'
232
234
  _context.podfile.set_use_source_pods d.name
233
235
  end
234
-
236
+ Pod::UI.puts "MEITU_FORCE_LOCAL_PATH_SOURCE=true: local :path pods forced to source".yellow if ENV['MEITU_BIN_WORKTREE_DEBUG'] == '1'
235
237
  end
236
238
  PodUpdateConfig.set_prepare_time(Time.now - start_time)
237
239
  # 同步 BinPodfile 文件
@@ -269,30 +271,27 @@ Pod::HooksManager.register('cocoapods-meitu-bin', :source_provider) do |context,
269
271
  sources_manager = Pod::Config.instance.sources_manager
270
272
  podfile = Pod::Config.instance.podfile
271
273
 
272
- podfile_path = podfile.defined_in_file
273
274
  pods_with_commit = {}
274
275
  pods_with_path = 0
275
- File.readlines(podfile_path).each_with_index do |line, index|
276
- # 去除行首空白后检查是否为注释行
277
- trimmed_line = line.lstrip
278
- next if trimmed_line.start_with?('#')
276
+ # source_provider 阶段直接使用 Podfile 解析结果:
277
+ # 1) 收集 external_source 中的 :commit 信息,参与后续 binary version 输入
278
+ # 2) 统计 :path 数量,辅助决定是否规避 lockfile 缓存
279
+ podfile.dependencies.each do |dependency|
280
+ source = dependency.respond_to?(:external_source) ? dependency.external_source : nil
281
+ next unless source.is_a?(Hash)
279
282
 
280
- if line.include?(':commit =>')
281
- # 提取 pod 名称
282
- if line =~ /pod\s+['"]([^'"]+)['"]/
283
- pod_name = $1
284
- # 提取 commit hash
285
- if line =~ /:commit\s*=>\s*['"]([^'"]+)['"]/
286
- commit_hash = $1
287
- pods_with_commit[pod_name] = { commit: commit_hash }
288
- end
289
- end
290
- elsif line.include?(':path =>')
291
- pods_with_path = pods_with_path + 1
283
+ normalized_source = source.each_with_object({}) do |(key, value), memo|
284
+ memo[key.to_sym] = value
292
285
  end
286
+
287
+ root_name = Pod::Specification.root_name(dependency.name)
288
+ commit_hash = normalized_source[:commit]
289
+ pods_with_commit[root_name] = { commit: commit_hash } if commit_hash
290
+ pods_with_path += 1 if normalized_source[:path]
293
291
  end
294
292
  # 解决多个组件切换从二进制切换到源码受podfile.lock 缓存影响导致切换异常的问题
295
293
  if pods_with_path > 2
294
+ # :path 组件较多时,强制触发重新分析,避免沿用旧 lockfile 造成切换不一致。
296
295
  PodUpdateConfig.set_is_podfile_lock_nil(true)
297
296
  end
298
297
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cocoapods-meitu-bin
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.0.2
4
+ version: 3.0.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jensen
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-03-04 00:00:00.000000000 Z
11
+ date: 2026-03-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: parallel
@@ -140,6 +140,7 @@ files:
140
140
  - lib/cocoapods-meitu-bin/helpers/spec_files_helper.rb
141
141
  - lib/cocoapods-meitu-bin/helpers/spec_source_creator.rb
142
142
  - lib/cocoapods-meitu-bin/helpers/upload_helper.rb
143
+ - lib/cocoapods-meitu-bin/helpers/worktree_identity.rb
143
144
  - lib/cocoapods-meitu-bin/native.rb
144
145
  - lib/cocoapods-meitu-bin/native/acknowledgements.rb
145
146
  - lib/cocoapods-meitu-bin/native/analyzer.rb