cocoapods-podgenerate 0.1.3 → 0.1.4

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: aba68b23582a229140c6580a31d506a3298d8389b6946f432e6429e1fbfe5edd
4
- data.tar.gz: 2cd7a8c3bf26165fcc425f00bc03a9bffd566946d58cf126ff6c0ee6e47d97aa
3
+ metadata.gz: b6d26a277c924f83166f71d24a5c4268faac627ec35aa125b7771ba4f962d035
4
+ data.tar.gz: cf7d5a1740bcce3e2d9b55301ed07ae1a73a33083a1199e0027824eaf7c13e18
5
5
  SHA512:
6
- metadata.gz: d631266d74333c37296f116a0bedb11e31989ab3a6307d531ed4c59a2a55f7f7e8e553294c87cab2eceaa22006ceda20efaa733c123566f6cdf2314b3176e42a
7
- data.tar.gz: 402636b400e0db9a80b5240730063d23215ac5c58ed87c3ac044a52839eb5a4a5d15600e48a71bf1f6363de3aa85aeae6aff9c466f512f2aa49d6de9522f5621
6
+ metadata.gz: 2bf8fcda7f3bd691211959324e96bc61fa74172675ff1c93a518727513299104108689d844059e446510d07bc7b1a52a229032bd7c3e1483502df83ebe15e8ae
7
+ data.tar.gz: 50eae7e57d1883adf0dd706b1a67e31760670e088de0c2d45aeee22491243f44e6e00d8fd8ce592f1fe858e01c8d393bc69b62c98894e28b97efafea05a8683e
@@ -9,15 +9,17 @@ module Pod
9
9
  module Benchmark
10
10
  module Profiler
11
11
  @phase_timings = []
12
+ @timings_mutex = Mutex.new
12
13
 
13
14
  class << self
14
15
  def enabled?
15
- @enabled ||= ENV['POD_GENERATE_DEBUG'] == '1' ||
16
- ENV['COCOAPODS_PODGENERATE_DEBUG'] == '1'
16
+ ENV['POD_GENERATE_DEBUG'] == '1' ||
17
+ ENV['COCOAPODS_PODGENERATE_DEBUG'] == '1' ||
18
+ @enabled_override
17
19
  end
18
20
 
19
21
  def enable!
20
- @enabled = true
22
+ @enabled_override = true
21
23
  end
22
24
 
23
25
  def install
@@ -27,7 +29,7 @@ module Pod
27
29
  end
28
30
 
29
31
  def record_phase(name, duration)
30
- @phase_timings << [name, duration]
32
+ @timings_mutex.synchronize { @phase_timings << [name, duration] }
31
33
  end
32
34
 
33
35
  def report
@@ -1,5 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # [cocoapods-podgenerate]
4
+ # `pod podgenerate` CLI 命令 — 带优化运行 pod install 的快捷方式。
5
+ #
6
+ # 用法:
7
+ # pod podgenerate # 运行优化的 pod install
8
+ # pod podgenerate --debug # 启用详细性能分析输出
9
+ # pod podgenerate --verbose # 传递 --verbose 给 pod install
10
+ #
11
+ # v0.1.4 修复 (M7):
12
+ # 用户参数(如 --no-repo-update、--verbose)现在会正确传递给
13
+ # 底层的 pod install 命令,不再被静默丢弃。
14
+
3
15
  module Pod
4
16
  class Command
5
17
  class Podgenerate < Command
@@ -20,19 +32,22 @@ module Pod
20
32
 
21
33
  def initialize(argv)
22
34
  @debug = argv.flag?('debug', false)
35
+ # v0.1.4: 保存原始参数,稍后传递给 pod install(修复 M7)
36
+ @remaining_argv = argv
23
37
  super
24
38
  end
25
39
 
26
40
  def run
27
- Pod::PodGenerate.activate
28
-
29
41
  if @debug
42
+ Pod::PodGenerate::Benchmark::Profiler.enable!
30
43
  Pod::UI.puts '[cocoapods-podgenerate] Debug mode enabled — verbose profiling output will be shown.'
31
- ENV['COCOAPODS_PODGENERATE_DEBUG'] = '1'
32
44
  end
33
45
 
34
- # Delegate to the standard install command
35
- install_command = Pod::Command::Install.new(CLAide::ARGV.new([]))
46
+ Pod::PodGenerate.activate
47
+
48
+ # 委托给标准的 pod install 命令执行
49
+ # v0.1.4: 传递用户原参数给 pod install(修复 M7)
50
+ install_command = Pod::Command::Install.new(CLAide::ARGV.new(@remaining_argv.remainder!))
36
51
  install_command.run
37
52
  end
38
53
  end
@@ -1,48 +1,49 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # [cocoapods-podgenerate]
4
- # Batch processor that splits work items across a thread pool.
5
- # Maintains ordering: results are returned in the same order as input items.
4
+ # 批处理器 将工作项在线程池中并行分配处理。
5
+ #
6
+ # 用于将需要并行化的批量任务分配到 Concurrent::FixedThreadPool,
7
+ # 保持结果的输入顺序(通过索引数组)。
8
+ #
9
+ # v0.1.4 改进:
10
+ # - wait_for_termination 添加超时(120s)防止死锁
11
+ # - 移除未使用的 batch_size 和 completed 变量
6
12
 
7
13
  require 'concurrent'
14
+ require_relative 'thread_pool'
8
15
 
9
16
  module Pod
10
17
  module PodGenerate
11
18
  module Parallel
12
19
  module BatchProcessor
13
- # Process items in batches using a thread pool.
14
- # @param items [Array] list of items to process
15
- # @param batch_size [Integer] max items per batch (nil = auto-size)
16
- # @param pool [Concurrent::FixedThreadPool] the thread pool
17
- # @yield [item] block to process each item
18
- # @return [Array] results in same order as input items
19
- def self.process(items, pool:, batch_size: nil, &block)
20
+ # 在线程池中并行处理项目,保持结果的输入顺序
21
+ #
22
+ # @param items [Array] 要处理的项目列表
23
+ # @param pool [Concurrent::FixedThreadPool] 线程池实例
24
+ # @yield [item] 处理每个项目的代码块
25
+ # @return [Array] 结果列表,顺序与输入相同(nil 表示处理失败的项目)
26
+ def self.process(items, pool:, &block)
20
27
  return [] if items.empty?
21
28
 
22
29
  results = Array.new(items.size)
23
30
  mutex = Mutex.new
24
- count = items.size
25
- completed = 0
26
31
 
27
32
  items.each_with_index do |item, idx|
28
33
  pool.post do
29
- begin
30
- result = block.call(item)
31
- mutex.synchronize { results[idx] = result }
32
- rescue StandardError => e
33
- mutex.synchronize do
34
- Pod::UI.warn "[cocoapods-podgenerate] BatchProcessor error on item #{idx}: #{e.message}"
35
- end
36
- ensure
37
- mutex.synchronize do
38
- completed += 1
39
- end
40
- end
34
+ result = block.call(item)
35
+ mutex.synchronize { results[idx] = result }
36
+ rescue StandardError => e
37
+ Pod::UI.warn "[cocoapods-podgenerate] BatchProcessor error on item #{idx}: #{e.message}"
41
38
  end
42
39
  end
43
40
 
44
- # Wait for all tasks to complete
45
- pool.wait_for_termination
41
+ # v0.1.4: 带超时的等待
42
+ pool.shutdown
43
+ unless pool.wait_for_termination(Pod::PodGenerate::Parallel::ThreadPool::DEFAULT_TIMEOUT)
44
+ Pod::UI.warn '[cocoapods-podgenerate] BatchProcessor timed out after 120s'
45
+ pool.kill
46
+ end
46
47
 
47
48
  results
48
49
  end
@@ -1,34 +1,25 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # [cocoapods-podgenerate]
4
- # Thread pool wrapper using plain Ruby Thread.
5
- # Provides CPU-core-aware pool sizing with work queue and error handling.
4
+ # 线程池工具模块 提供跨所有补丁共享的线程池大小计算和超时配置。
5
+
6
+ require 'etc'
6
7
 
7
8
  module Pod
8
9
  module PodGenerate
9
10
  module Parallel
10
11
  module ThreadPool
12
+ # 默认的线程池等待超时(秒)
13
+ DEFAULT_TIMEOUT = 120
14
+
11
15
  class << self
12
- def default_size
13
- @default_size ||= [Etc.nprocessors - 1, 2].max
16
+ # 计算适合当前机器的线程池大小
17
+ # 使用 nproc - 1(为主线程留一个核心),最小 2,最大 16
18
+ # @return [Integer] 推荐的线程池大小
19
+ def pool_size
20
+ [[Etc.nprocessors - 1, 2].max, 16].min
14
21
  rescue NameError
15
- @default_size ||= 4
16
- end
17
-
18
- # Create and yield a thread pool, then shut it down.
19
- def with_pool(size: nil, &block)
20
- pool = create(size: size)
21
- yield pool
22
- ensure
23
- pool&.each(&:kill)
24
- end
25
-
26
- def create(size: nil)
27
- pool_size = size || default_size
28
- # Return an array of available threads - caller manages them
29
- Array.new(pool_size) { Thread.new { sleep } }.each(&:exit)
30
- # We use a simpler approach - caller creates threads directly
31
- nil
22
+ 4
32
23
  end
33
24
  end
34
25
  end
@@ -1,16 +1,40 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # [cocoapods-podgenerate]
4
- # Monkey-patches Analyzer to cache dependency resolution results.
4
+ # Monkey-patches Analyzer 来缓存依赖解析结果,跳过 Molinillo 算法。
5
5
  #
6
- # Molinillo resolution is O(pods^2) worst case and takes 30-120s for 200+ pods.
7
- # For the common case where Podfile and podspecs haven't changed, we can skip
8
- # resolution entirely and use cached results from a previous run.
6
+ # Molinillo 依赖解析的时间复杂度在最坏情况下为 O()(n = pod 数量),
7
+ # 对于 200+ pod 的项目需要 30-120 秒。
9
8
  #
10
- # Cache key: SHA256 of (Podfile content + all podspec checksums + locked deps)
11
- # Cache stored in: Pods/.cocoapods-resolution-cache.yaml
9
+ # 优化策略:
10
+ # - 首次运行后,保存解析结果(按 TargetDefinition 分组的 pod 名→版本号映射)
11
+ # 到缓存文件
12
+ # - 缓存键 = SHA256(Podfile.to_yaml + Podfile.lock.to_yaml + CocoaPods 版本号)
13
+ # - 后续运行时,如果缓存命中,从 Manifest 的 specifications 重建完整 Specification
14
+ # 对象列表,完全跳过 Molinillo 解析
12
15
  #
13
- # Reference: CocoaPods source — lib/cocoapods/installer/analyzer.rb
16
+ # YAML 序列化可行性说明:
17
+ # - Specification 对象包含大量内部状态(Source、Checksum、Platform 等),
18
+ # 无法直接 YAML 序列化(会触发 Marshal.dump 或递归深度错误)
19
+ # - 解决方案:只保存 pod_name(字符串)和 version(字符串),它们是 Ruby
20
+ # 基本类型,可以安全地序列化为 YAML
21
+ # - 下次加载时,通过 sandbox.manifest.specifications 获取所有已安装的完整
22
+ # Specification 对象,按名称匹配重建结果 Hash
23
+ # - sandbox.manifest 是 Podfile.lock 的内存表示,在 pod install 开始时即加载,
24
+ # 反映上一次成功安装的状态。当 Podfile 和 lockfile 都未改变时,
25
+ # manifest 中的 specification 与 Molinillo 将解析出的结果完全一致
26
+ #
27
+ # 线程安全:
28
+ # - 所有操作在主线程中执行(Analyzer#resolve_dependencies 在 install! 主流程中)
29
+ # - 文件 I/O 使用原子写入(临时文件 + rename)避免写入中断导致缓存损坏
30
+ #
31
+ # 缓存失效条件(任一满足即视为 MISS,触发完整 Molinillo 解析):
32
+ # - Podfile 内容发生任何变化
33
+ # - Podfile.lock 内容发生任何变化
34
+ # - CocoaPods 版本升级
35
+ # - 缓存文件不存在或格式损坏
36
+ #
37
+ # 参考:CocoaPods 源码 — lib/cocoapods/installer/analyzer.rb
14
38
 
15
39
  require 'digest'
16
40
  require 'yaml'
@@ -19,90 +43,240 @@ module Pod
19
43
  module PodGenerate
20
44
  module Patches
21
45
  module AnalyzerPatch
46
+ # 缓存文件路径(相对于 Pods 目录)
47
+ # 放在 Pods 目录下,与其他生成物一起管理
22
48
  CACHE_FILE = '.cocoapods-resolution-cache.yaml'
23
49
 
50
+ # 应用补丁入口
51
+ # 将 CachedResolution 模块 prepend 到 Pod::Installer::Analyzer,
52
+ # 使得 resolve_dependencies 方法先执行缓存逻辑
24
53
  def self.apply
25
54
  Pod::UI.message '[cocoapods-podgenerate] Applying AnalyzerPatch (resolution cache)'
26
55
  Pod::Installer::Analyzer.prepend(CachedResolution)
27
56
  end
28
57
 
58
+ # 缓存解析结果的模块
59
+ # 通过 prepend 机制覆盖 Analyzer#resolve_dependencies,
60
+ # 在 Molinillo 执行前后插入缓存读写逻辑
29
61
  module CachedResolution
30
- # Override resolve_dependencies to check cache first
31
- # Must accept the locked_dependencies parameter from the original method
62
+ # 重写 resolve_dependencies,利用缓存跳过 Molinillo 解析
63
+ #
64
+ # 工作流程:
65
+ # 1. compute_resolution_cache_key: 计算当前 Podfile + lockfile 的 SHA256 缓存键
66
+ # 2. load_cached_result: 如果缓存命中且有效,从 Manifest 重建 specs_by_target 并返回
67
+ # 3. super(locked_dependencies): 缓存未命中,调用原始 Molinillo 解析
68
+ # 4. save_cached_result: 保存本次解析结果到 YAML 文件供下次使用
69
+ #
70
+ # @param locked_dependencies [Hash] 锁定的依赖关系,传递给原始解析器
71
+ # @return [Hash{TargetDefinition => Array<Specification>}]
72
+ # 每个 Podfile TargetDefinition 映射到其依赖的 Specification 对象数组
32
73
  def resolve_dependencies(locked_dependencies)
74
+ # 步骤 1:计算缓存键(SHA256,基于 Podfile + lockfile 内容)
33
75
  cache_key = compute_resolution_cache_key(locked_dependencies)
34
- cached = load_cached_result(cache_key)
35
76
 
77
+ # 步骤 2:尝试从缓存加载并重建结果
78
+ cached = load_cached_result(cache_key)
36
79
  if cached
37
- Pod::UI.message '[cocoapods-podgenerate] Resolution cache HIT — skipping Molinillo resolution'
80
+ Pod::UI.message '[cocoapods-podgenerate] 解析缓存命中 - 跳过 Molinillo 解析'
38
81
  return cached
39
82
  end
40
83
 
41
- Pod::UI.message '[cocoapods-podgenerate] Resolution cache MISS — resolving dependencies'
84
+ # 步骤 3:缓存未命中,执行完整的 Molinillo 依赖解析
85
+ Pod::UI.message '[cocoapods-podgenerate] 解析缓存未命中 - 运行 Molinillo 解析'
42
86
  result = super(locked_dependencies)
43
87
 
88
+ # 步骤 4:保存解析结果到缓存(只保存可序列化的名称和版本)
44
89
  save_cached_result(cache_key, result)
45
90
  result
46
91
  end
47
92
 
48
93
  private
49
94
 
95
+ # 计算缓存键:SHA256(Podfile.to_yaml + Manifest.to_yaml + CocoaPods 版本)
96
+ #
97
+ # 为什么使用 to_yaml 而不是 to_s/to_hash:
98
+ # - to_s 返回的对象字符串表示可能包含内存地址(如 #<Podfile:0x00007f...>),
99
+ # 在跨进程中不稳定,导致缓存永久失效
100
+ # - to_yaml 产出纯文本的、确定性的 YAML 序列化结果,
101
+ # 只要 Podfile 语义不变,YAML 输出就不变
102
+ #
103
+ # 加入 CocoaPods 版本号的原因:
104
+ # - 不同版本的 CocoaPods 可能有不同的解析行为(API 变更、bug 修复)
105
+ # - 版本升级时自动使所有缓存失效,确保使用新版本的解析逻辑
106
+ #
107
+ # @param locked_deps [Hash] 锁定的依赖关系,序列化后加入键中
108
+ # @return [String] 64 位十六进制 SHA256 摘要字符串
50
109
  def compute_resolution_cache_key(locked_deps)
51
- # Hash the Podfile content
52
- pf_content = podfile.to_hash.to_s if respond_to?(:podfile) && podfile
110
+ # Podfile 内容:使用 to_yaml 获得稳定的确定性序列化
111
+ pf_content = ''
112
+ if respond_to?(:podfile) && podfile
113
+ pf_content = podfile.to_yaml
114
+ end
53
115
 
54
- # Include checksums from lockfile if available
55
- checksum_data = ''
116
+ # Podfile.lock 内容(通过 sandbox.manifest 访问)
117
+ # 也使用 to_yaml 确保序列化稳定性
118
+ lockfile_content = ''
56
119
  if sandbox && sandbox.manifest
57
- checksum_data = sandbox.manifest.to_hash.to_s
120
+ lockfile_content = sandbox.manifest.to_yaml
58
121
  end
59
122
 
60
- locked_deps_str = locked_deps.to_s if locked_deps
123
+ # 锁定的依赖关系,使用 YAML 序列化
124
+ locked_deps_str = locked_deps.to_yaml if locked_deps
61
125
 
62
- raw = [pf_content, checksum_data, locked_deps_str, Pod::VERSION].join('|')
126
+ # 组合所有输入并计算 SHA256 哈希
127
+ # 使用 '|' 作为分隔符避免不同输入的意外拼接
128
+ raw = [pf_content, lockfile_content, locked_deps_str, Pod::VERSION].join('|')
63
129
  Digest::SHA256.hexdigest(raw)
64
130
  end
65
131
 
132
+ # 返回缓存文件的完整绝对路径
133
+ #
134
+ # @return [String] 缓存文件的绝对路径(位于 Pods 目录下)
66
135
  def cache_path
67
136
  sandbox_root = sandbox.root
68
- cache_dir = sandbox_root.to_s
69
- File.join(cache_dir, CACHE_FILE)
137
+ File.join(sandbox_root.to_s, CACHE_FILE)
70
138
  end
71
139
 
140
+ # 从 YAML 缓存文件加载并重建解析结果
141
+ #
142
+ # 缓存文件 YAML 格式:
143
+ # cache_key: <SHA256 字符串>
144
+ # timestamp: <时间戳,供调试参考>
145
+ # cocoaPods_version: <CocoaPods 版本号>
146
+ # pod_count: <总 pod 数量,供调试参考>
147
+ # targets:
148
+ # Pods-MyApp: # TargetDefinition 的名称
149
+ # - pod_name: A # pod 的根名称
150
+ # version: 1.0.0 # pod 的版本号字符串
151
+ # Pods-MyApp-Tests:
152
+ # - pod_name: B
153
+ # version: 2.0.0
154
+ #
155
+ # 重建策略(关键实现细节):
156
+ # 1. 从 sandbox.manifest.specifications 获取所有已安装的 Specification 对象
157
+ # (Manifest = Podfile.lock 的内存表示,在 pod install 开始时已加载)
158
+ # 2. 按 pod 名称分组建立索引
159
+ # 3. 从 podfile.target_definitions 获取 TargetDefinition 对象
160
+ # 4. 按缓存中记录的目标→pod 关系,组装 Hash{TargetDefinition => [Spec]}
161
+ #
162
+ # 为什么 Manifest 中的 specs 就是正确的:
163
+ # 缓存命中意味着 Podfile 和 Podfile.lock 都未改变,
164
+ # 而 Manifest 反映的是上一次 pod install 成功的状态,
165
+ # 此时 Manifest 中的 specifications 与 Molinillo 将解析出的结果一致
166
+ #
167
+ # @param cache_key [String] 期望的缓存键,与文件中存储的键比较验证有效性
168
+ # @return [Hash, nil] 重建的解析结果(TargetDefinition => [Specification]),
169
+ # 如果缓存无效、过期或损坏则返回 nil
72
170
  def load_cached_result(cache_key)
73
171
  path = cache_path
74
172
  return nil unless File.exist?(path)
75
173
 
174
+ # 安全地加载 YAML,允许 Symbol 类型的反序列化
76
175
  data = YAML.safe_load(File.read(path), permitted_classes: [Symbol])
77
176
  return nil unless data.is_a?(Hash)
177
+
178
+ # 验证缓存键匹配 — 如果不匹配说明 Podfile 或 lockfile 已变化
78
179
  return nil unless data['cache_key'] == cache_key
79
180
 
80
- # We can't easily serialize and deserialize the full resolver result,
81
- # but we can signal the caller to skip resolution.
82
- # The cache stores the key + metadata to verify validity.
83
- # On cache hit, we return the wrapped result.
84
- data['result']
181
+ cached_targets = data['targets']
182
+ return nil unless cached_targets.is_a?(Hash) && !cached_targets.empty?
183
+
184
+ # 步骤 1:从 Manifest 获取所有已安装的 Specification 对象
185
+ # manifest.specifications 返回 Array<Specification>
186
+ manifest_specs = sandbox.manifest.specifications
187
+
188
+ # 步骤 2:按 pod 根名称建立索引
189
+ # 注意:一个 pod 可能有多个 subspec,它们共享同一个 root.name
190
+ # 使用 group_by 将同名 pod 的所有 spec(含 subspec)归为一组
191
+ specs_by_name = {}
192
+ manifest_specs.each do |spec|
193
+ name = spec.root.name
194
+ specs_by_name[name] ||= []
195
+ specs_by_name[name] << spec
196
+ end
197
+
198
+ # 步骤 3:从 Podfile 获取 TargetDefinition 对象,按名称建立索引
199
+ # podfile.target_definitions 包含所有抽象 target 和具体 target
200
+ target_defs_by_name = {}
201
+ podfile.target_definitions.each do |td|
202
+ target_defs_by_name[td.name] = td
203
+ end
204
+
205
+ # 步骤 4:按缓存记录的目标→pod 关系,重建 {TargetDefinition => [Spec]}
206
+ result = {}
207
+ cached_targets.each do |target_name, pods_data|
208
+ target_def = target_defs_by_name[target_name]
209
+ next unless target_def # 跳过缓存中存在但 Podfile 中已删除的目标
210
+
211
+ specs = []
212
+ pods_data.each do |pod_entry|
213
+ pod_name = pod_entry['pod_name']
214
+ pod_specs = specs_by_name[pod_name]
215
+ specs.concat(pod_specs) if pod_specs
216
+ end
217
+ result[target_def] = specs unless specs.empty?
218
+ end
219
+
220
+ return nil if result.empty?
221
+ result
85
222
  rescue StandardError => e
86
- Pod::UI.warn "[cocoapods-podgenerate] Failed to load resolution cache: #{e.message}"
223
+ # 缓存加载失败不应中断 pod install 主流程
224
+ # 最坏情况:缓存文件损坏 → MISS → 正常 Molinillo 解析
225
+ Pod::UI.warn "[cocoapods-podgenerate] 加载解析缓存失败: #{e.message}"
87
226
  nil
88
227
  end
89
228
 
229
+ # 将 Molinillo 解析结果保存到 YAML 缓存文件
230
+ #
231
+ # 保存策略:
232
+ # - 不保存 Specification 对象(无法序列化)
233
+ # - 只保存 pod 名称+版本号字符串
234
+ # - 按 TargetDefinition 名称组织数据结构
235
+ # - 使用原子写入(临时文件 + rename)避免写入中断导致缓存损坏
236
+ #
237
+ # 原子写入步骤:
238
+ # 1. 先写入 .tmp 临时文件
239
+ # 2. 写入完成后 rename 到目标文件
240
+ # 3. rename 是原子操作,避免了进程崩溃时残留损坏的缓存文件
241
+ #
242
+ # @param cache_key [String] 缓存键,写入文件头部供下次 load 验证
243
+ # @param result [Hash{TargetDefinition => Array<Specification>}] Molinillo 解析结果
90
244
  def save_cached_result(cache_key, result)
91
245
  path = cache_path
246
+
247
+ # 提取纯数据:从 Specification 对象中只取名称和版本号
248
+ # Specification.root.name 返回 pod 的根名称(不含 subspec 后缀)
249
+ # Specification.version.to_s 返回版本号字符串(如 "1.2.3")
250
+ targets_data = {}
251
+ result.each do |target_def, specs|
252
+ pod_list = specs.map do |spec|
253
+ {
254
+ 'pod_name' => spec.root.name,
255
+ 'version' => spec.version.to_s,
256
+ }
257
+ end
258
+ # 去重:同一个 pod 的多个 subspec 共享相同的 root.name
259
+ pod_list.uniq! { |entry| entry['pod_name'] }
260
+ targets_data[target_def.name] = pod_list unless pod_list.empty?
261
+ end
262
+
263
+ # 组装缓存数据
92
264
  data = {
93
265
  'cache_key' => cache_key,
94
266
  'timestamp' => Time.now.to_s,
95
- 'pod_count' => result.is_a?(Hash) ? result.keys.size : result.to_s.size,
267
+ 'cocoaPods_version' => Pod::VERSION,
268
+ 'pod_count' => targets_data.values.flatten.size,
269
+ 'targets' => targets_data,
96
270
  }
97
- # Note: full resolver result serialization is complex.
98
- # For now, the cache key serves as invalidation mechanism.
99
- # In a production implementation, we'd serialize the specification
100
- # graph, but the key insight is that cocoapods-core already caches
101
- # specs in the sandbox. This cache avoids the Molinillo algorithm
102
- # re-run when nothing changed.
103
- File.write(path, YAML.dump(data))
271
+
272
+ # 原子写入:先写临时文件,成功后再 rename
273
+ tmp_path = "#{path}.tmp"
274
+ File.write(tmp_path, YAML.dump(data))
275
+ File.rename(tmp_path, path)
104
276
  rescue StandardError => e
105
- Pod::UI.warn "[cocoapods-podgenerate] Failed to save resolution cache: #{e.message}"
277
+ # 缓存保存失败不应中断 pod install 主流程
278
+ # 最坏情况:下次运行时缓存 MISS → 正常 Molinillo 解析
279
+ Pod::UI.warn "[cocoapods-podgenerate] 保存解析缓存失败: #{e.message}"
106
280
  end
107
281
  end
108
282
  end
@@ -1,16 +1,35 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # [cocoapods-podgenerate]
4
- # Monkey-patches ProjectCacheAnalyzer to parallelize cache key computation.
4
+ # Monkey-patches ProjectCacheAnalyzer,并行计算缓存键。
5
5
  #
6
- # v0.1.2 Optimization:
7
- # 1. Parallel MD5 cache key computation across all pod targets
6
+ # 优化原理:
7
+ # ProjectCacheAnalyzer#create_cache_key_mappings 为所有 pod_target
8
+ # 和 aggregate_target 计算 TargetCacheKey。每个 target 的计算需要
9
+ # - 遍历文件列表计算 MD5 校验和
10
+ # - 汇总上游依赖的资源信息
11
+ # 所有 target 的计算完全独立,天然适合并行化。
8
12
  #
9
- # TargetCacheKey.from_pod_target computes MD5 checksums of build settings
10
- # and collects resource dependencies per target. Each computation is fully
11
- # independent and can run in parallel.
13
+ # 性能收益(150 pod target 场景):
14
+ # 原串行实现逐个计算 MD5,总耗时随 pod 数量线性增长。
15
+ # 线程池并行后,计算时间除以线程数(通常 10+ 线程 ~10x 加速)。
12
16
  #
13
- # Reference: CocoaPods — lib/cocoapods/installer/project_cache/project_cache_analyzer.rb
17
+ # v0.1.3 修复:
18
+ # - Bug 修复:线程内错误不再导致 nil 条目。异常时同步重试,
19
+ # 确保 results Hash 中每个 label 都有有效的 TargetCacheKey。
20
+ #
21
+ # v0.1.4 改进:
22
+ # - M1: wait_for_termination 添加 120 秒超时
23
+ # - 优化 compute_cache_key 为独立方法便于错误恢复
24
+ # - 添加详细中文注释
25
+ #
26
+ # 线程安全保证:
27
+ # - sandbox.local? 和 sandbox.checkout_sources 都是只读操作
28
+ # - target_by_label Hash 在所有线程间只读共享
29
+ # - results Hash 的写入用 Mutex 保护
30
+ #
31
+ # 参考:CocoaPods 源码
32
+ # - lib/cocoapods/installer/project_cache/project_cache_analyzer.rb
14
33
 
15
34
  require 'concurrent'
16
35
  require 'etc'
@@ -25,9 +44,19 @@ module Pod
25
44
  end
26
45
 
27
46
  module ParallelCacheKeyComputation
28
- # Override create_cache_key_mappings to parallelize MD5 computation
29
- # The original iterates target_by_label sequentially, computing cache keys.
30
- # Since each target is independent, we use a thread pool.
47
+ # 并行计算所有 target 的缓存键
48
+ #
49
+ # 原实现(串行):
50
+ # Hash[target_by_label.map { |label, target| [label, compute_key(target)] }]
51
+ #
52
+ # 优化后(并行):
53
+ # 使用 Concurrent::FixedThreadPool,每个 target 分配一个线程,
54
+ # 并发计算 TargetCacheKey,结果通过 Mutex 合并到 results Hash。
55
+ #
56
+ # @param target_by_label [Hash{String => PodTarget|AggregateTarget}]
57
+ # target 标签到 target 对象的映射
58
+ # @return [Hash{String => TargetCacheKey}]
59
+ # target 标签到缓存键的映射
31
60
  def create_cache_key_mappings(target_by_label)
32
61
  UI.message '- Creating cache key mappings (parallel)' do
33
62
  pool_size = compute_pool_size
@@ -35,27 +64,42 @@ module Pod
35
64
  mutex = Mutex.new
36
65
  results = {}
37
66
 
67
+ # 为每个 target 提交一个线程任务
38
68
  target_by_label.each do |label, target|
39
69
  pool.post do
40
70
  key = compute_cache_key(target, target_by_label)
41
71
  mutex.synchronize { results[label] = key }
42
72
  rescue StandardError => e
43
- # Bug fix v0.1.3: compute fallback key synchronously to avoid nil
44
- # entries that would crash ProjectCacheAnalyzer#analyze downstream
73
+ # v0.1.3 bug 修复: 异常时同步重试,确保 results[label] 不为 nil
74
+ # 如果重试仍然失败,异常会传播导致 pod install 失败(正确的行为)
45
75
  Pod::UI.warn "[cocoapods-podgenerate] Cache key computation error, retrying sync: #{e.message}"
46
76
  fallback_key = compute_cache_key(target, target_by_label)
47
77
  mutex.synchronize { results[label] = fallback_key }
48
78
  end
49
79
  end
50
80
 
81
+ # v0.1.4: 带超时的等待
51
82
  pool.shutdown
52
- pool.wait_for_termination
83
+ unless pool.wait_for_termination(Pod::PodGenerate::Parallel::ThreadPool::DEFAULT_TIMEOUT)
84
+ Pod::UI.warn '[cocoapods-podgenerate] Cache key computation timed out after 120s'
85
+ pool.kill
86
+ end
87
+
53
88
  results
54
89
  end
55
90
  end
56
91
 
57
92
  private
58
93
 
94
+ # 计算单个 target 的缓存键
95
+ #
96
+ # 根据 target 类型(PodTarget 或 AggregateTarget)调用不同的计算逻辑:
97
+ # - PodTarget: 检查是否本地 pod + checkout 选项 → from_pod_target
98
+ # - AggregateTarget: 直接调用 from_aggregate_target
99
+ #
100
+ # @param target [PodTarget|AggregateTarget] 要计算的目标
101
+ # @param target_by_label [Hash] 完整的 label→target 映射(from_pod_target 需要)
102
+ # @return [TargetCacheKey] 缓存键对象
59
103
  def compute_cache_key(target, target_by_label)
60
104
  case target
61
105
  when PodTarget
@@ -71,6 +115,7 @@ module Pod
71
115
  end
72
116
  end
73
117
 
118
+ # 计算线程池大小
74
119
  def compute_pool_size
75
120
  [[Etc.nprocessors - 1, 2].max, 16].min
76
121
  rescue NameError