cocoapods-podgenerate 0.1.0
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 +7 -0
- data/lib/cocoapods-podgenerate/benchmark/profiler.rb +94 -0
- data/lib/cocoapods-podgenerate/command.rb +40 -0
- data/lib/cocoapods-podgenerate/hooks.rb +9 -0
- data/lib/cocoapods-podgenerate/parallel/batch_processor.rb +52 -0
- data/lib/cocoapods-podgenerate/parallel/thread_pool.rb +37 -0
- data/lib/cocoapods-podgenerate/patches/analyzer_patch.rb +111 -0
- data/lib/cocoapods-podgenerate/patches/installer_patch.rb +56 -0
- data/lib/cocoapods-podgenerate/patches/project_patch.rb +53 -0
- data/lib/cocoapods-podgenerate/patches/project_writer_patch.rb +96 -0
- data/lib/cocoapods-podgenerate/patches/user_integrator_patch.rb +92 -0
- data/lib/cocoapods-podgenerate.rb +43 -0
- data/lib/cocoapods_plugin.rb +7 -0
- metadata +122 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 7c23dcfa65c98d07b1facb75c25c0b48c4899886f88397544887c38d95f1d695
|
|
4
|
+
data.tar.gz: f5a1805ef912d83ce6ac2520b48a40543c76f02634bef505bab121910427fb1a
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: f2f2628e7f34e56c628f006cdf785fc38df4e37e0e86cd04c52fe155b8eacf398ec656172b9476871438807acf987e13f3894c19501ce99c6e0a7a9cb0bae488
|
|
7
|
+
data.tar.gz: 81d85b61da2db4da471411f0bc4ec3d4f42513d45a5358a2211c8bd0e352f01e0d02b888c99ef4c2ddf0ae6d12e8248b624eb98dda78e60a8a07691528a270f5
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# [cocoapods-podgenerate]
|
|
4
|
+
# Performance profiler. Hooks into Pod::Installer to time each phase.
|
|
5
|
+
# Output: per-phase wall-clock timing breakdown.
|
|
6
|
+
|
|
7
|
+
module Pod
|
|
8
|
+
module PodGenerate
|
|
9
|
+
module Benchmark
|
|
10
|
+
module Profiler
|
|
11
|
+
@phase_timings = []
|
|
12
|
+
|
|
13
|
+
class << self
|
|
14
|
+
def enabled?
|
|
15
|
+
@enabled ||= ENV['POD_GENERATE_DEBUG'] == '1' ||
|
|
16
|
+
ENV['COCOAPODS_PODGENERATE_DEBUG'] == '1'
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def enable!
|
|
20
|
+
@enabled = true
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def install
|
|
24
|
+
return unless enabled?
|
|
25
|
+
Pod::Installer.prepend(ProfilerHooks)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def record_phase(name, duration)
|
|
29
|
+
@phase_timings << [name, duration]
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def swap_or_default(phase_name)
|
|
33
|
+
# Called from hooks: returns a timing helper or nil
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def report
|
|
37
|
+
return if @phase_timings.empty?
|
|
38
|
+
total = @phase_timings.map(&:last).sum
|
|
39
|
+
Pod::UI.puts "\n[cocoapods-podgenerate] Performance Report:"
|
|
40
|
+
@phase_timings.each do |name, dur|
|
|
41
|
+
pct = total > 0 ? (dur / total * 100) : 0
|
|
42
|
+
Pod::UI.puts " #{format('%-35s', name)} #{format('%.2f', dur)}s (#{format('%.1f', pct)}%)"
|
|
43
|
+
end
|
|
44
|
+
Pod::UI.puts " #{'─' * 50}"
|
|
45
|
+
Pod::UI.puts " #{format('%-35s', 'TOTAL')} #{format('%.2f', total)}s"
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
module ProfilerHooks
|
|
50
|
+
def install!
|
|
51
|
+
t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
52
|
+
super
|
|
53
|
+
ensure
|
|
54
|
+
elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - t0
|
|
55
|
+
Profiler.record_phase('Total install!', elapsed)
|
|
56
|
+
Profiler.report
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def resolve_dependencies
|
|
60
|
+
t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
61
|
+
super
|
|
62
|
+
ensure
|
|
63
|
+
elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - t0
|
|
64
|
+
Profiler.record_phase(' Resolve dependencies', elapsed)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def download_dependencies
|
|
68
|
+
t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
69
|
+
super
|
|
70
|
+
ensure
|
|
71
|
+
elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - t0
|
|
72
|
+
Profiler.record_phase(' Download dependencies', elapsed)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def generate_pods_project
|
|
76
|
+
t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
77
|
+
super
|
|
78
|
+
ensure
|
|
79
|
+
elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - t0
|
|
80
|
+
Profiler.record_phase(' Generate Pods project', elapsed)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def integrate_user_project
|
|
84
|
+
t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
85
|
+
super
|
|
86
|
+
ensure
|
|
87
|
+
elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - t0
|
|
88
|
+
Profiler.record_phase(' Integrate user project', elapsed)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pod
|
|
4
|
+
class Command
|
|
5
|
+
class Podgenerate < Command
|
|
6
|
+
self.summary = 'Run pod install with PodGenerate optimizations'
|
|
7
|
+
self.description = <<-DESC
|
|
8
|
+
Speeds up pod install for large projects (200+ pods) by enabling
|
|
9
|
+
parallel processing, optimized dependency analysis, and incremental
|
|
10
|
+
project generation.
|
|
11
|
+
DESC
|
|
12
|
+
|
|
13
|
+
self.arguments = []
|
|
14
|
+
|
|
15
|
+
def self.options
|
|
16
|
+
[
|
|
17
|
+
['--debug', 'Enable verbose profiling output and detailed timing logs']
|
|
18
|
+
].concat(super)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def initialize(argv)
|
|
22
|
+
@debug = argv.flag?('debug', false)
|
|
23
|
+
super
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def run
|
|
27
|
+
Pod::PodGenerate.activate
|
|
28
|
+
|
|
29
|
+
if @debug
|
|
30
|
+
Pod::UI.puts '[cocoapods-podgenerate] Debug mode enabled — verbose profiling output will be shown.'
|
|
31
|
+
ENV['COCOAPODS_PODGENERATE_DEBUG'] = '1'
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Delegate to the standard install command
|
|
35
|
+
install_command = Pod::Command::Install.new(CLAide::ARGV.new([]))
|
|
36
|
+
install_command.run
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# [cocoapods-podgenerate]
|
|
4
|
+
# Hook registration for CocoaPods plugin system.
|
|
5
|
+
# Registers a :pre_install hook so patches are applied before the install flow.
|
|
6
|
+
|
|
7
|
+
Pod::HooksManager.register('cocoapods-podgenerate', :pre_install) do |_context|
|
|
8
|
+
Pod::PodGenerate.activate
|
|
9
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
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.
|
|
6
|
+
|
|
7
|
+
require 'concurrent'
|
|
8
|
+
|
|
9
|
+
module Pod
|
|
10
|
+
module PodGenerate
|
|
11
|
+
module Parallel
|
|
12
|
+
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
|
+
return [] if items.empty?
|
|
21
|
+
|
|
22
|
+
results = Array.new(items.size)
|
|
23
|
+
mutex = Mutex.new
|
|
24
|
+
count = items.size
|
|
25
|
+
completed = 0
|
|
26
|
+
|
|
27
|
+
items.each_with_index do |item, idx|
|
|
28
|
+
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
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Wait for all tasks to complete
|
|
45
|
+
pool.wait_for_termination
|
|
46
|
+
|
|
47
|
+
results
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# [cocoapods-podgenerate]
|
|
4
|
+
# Thread pool wrapper using plain Ruby Thread.
|
|
5
|
+
# Provides CPU-core-aware pool sizing with work queue and error handling.
|
|
6
|
+
|
|
7
|
+
module Pod
|
|
8
|
+
module PodGenerate
|
|
9
|
+
module Parallel
|
|
10
|
+
module ThreadPool
|
|
11
|
+
class << self
|
|
12
|
+
def default_size
|
|
13
|
+
@default_size ||= [Etc.nprocessors - 1, 2].max
|
|
14
|
+
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
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# [cocoapods-podgenerate]
|
|
4
|
+
# Monkey-patches Analyzer to cache dependency resolution results.
|
|
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.
|
|
9
|
+
#
|
|
10
|
+
# Cache key: SHA256 of (Podfile content + all podspec checksums + locked deps)
|
|
11
|
+
# Cache stored in: Pods/.cocoapods-resolution-cache.yaml
|
|
12
|
+
#
|
|
13
|
+
# Reference: CocoaPods source — lib/cocoapods/installer/analyzer.rb
|
|
14
|
+
|
|
15
|
+
require 'digest'
|
|
16
|
+
require 'yaml'
|
|
17
|
+
|
|
18
|
+
module Pod
|
|
19
|
+
module PodGenerate
|
|
20
|
+
module Patches
|
|
21
|
+
module AnalyzerPatch
|
|
22
|
+
CACHE_FILE = '.cocoapods-resolution-cache.yaml'
|
|
23
|
+
|
|
24
|
+
def self.apply
|
|
25
|
+
Pod::UI.message '[cocoapods-podgenerate] Applying AnalyzerPatch (resolution cache)'
|
|
26
|
+
Pod::Installer::Analyzer.prepend(CachedResolution)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
module CachedResolution
|
|
30
|
+
# Override resolve_dependencies to check cache first
|
|
31
|
+
# Must accept the locked_dependencies parameter from the original method
|
|
32
|
+
def resolve_dependencies(locked_dependencies)
|
|
33
|
+
cache_key = compute_resolution_cache_key(locked_dependencies)
|
|
34
|
+
cached = load_cached_result(cache_key)
|
|
35
|
+
|
|
36
|
+
if cached
|
|
37
|
+
Pod::UI.message '[cocoapods-podgenerate] Resolution cache HIT — skipping Molinillo resolution'
|
|
38
|
+
return cached
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
Pod::UI.message '[cocoapods-podgenerate] Resolution cache MISS — resolving dependencies'
|
|
42
|
+
result = super(locked_dependencies)
|
|
43
|
+
|
|
44
|
+
save_cached_result(cache_key, result)
|
|
45
|
+
result
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
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
|
|
53
|
+
|
|
54
|
+
# Include checksums from lockfile if available
|
|
55
|
+
checksum_data = ''
|
|
56
|
+
if sandbox && sandbox.manifest
|
|
57
|
+
checksum_data = sandbox.manifest.to_hash.to_s
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
locked_deps_str = locked_deps.to_s if locked_deps
|
|
61
|
+
|
|
62
|
+
raw = [pf_content, checksum_data, locked_deps_str, Pod::VERSION].join('|')
|
|
63
|
+
Digest::SHA256.hexdigest(raw)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def cache_path
|
|
67
|
+
sandbox_root = sandbox.root
|
|
68
|
+
cache_dir = sandbox_root.to_s
|
|
69
|
+
File.join(cache_dir, CACHE_FILE)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def load_cached_result(cache_key)
|
|
73
|
+
path = cache_path
|
|
74
|
+
return nil unless File.exist?(path)
|
|
75
|
+
|
|
76
|
+
data = YAML.safe_load(File.read(path), permitted_classes: [Symbol])
|
|
77
|
+
return nil unless data.is_a?(Hash)
|
|
78
|
+
return nil unless data['cache_key'] == cache_key
|
|
79
|
+
|
|
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']
|
|
85
|
+
rescue StandardError => e
|
|
86
|
+
Pod::UI.warn "[cocoapods-podgenerate] Failed to load resolution cache: #{e.message}"
|
|
87
|
+
nil
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def save_cached_result(cache_key, result)
|
|
91
|
+
path = cache_path
|
|
92
|
+
data = {
|
|
93
|
+
'cache_key' => cache_key,
|
|
94
|
+
'timestamp' => Time.now.to_s,
|
|
95
|
+
'pod_count' => result.is_a?(Hash) ? result.keys.size : result.to_s.size,
|
|
96
|
+
}
|
|
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))
|
|
104
|
+
rescue StandardError => e
|
|
105
|
+
Pod::UI.warn "[cocoapods-podgenerate] Failed to save resolution cache: #{e.message}"
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# [cocoapods-podgenerate]
|
|
4
|
+
# Monkey-patches PodsProjectGenerator to optimize "Generating Pods project".
|
|
5
|
+
#
|
|
6
|
+
# Optimizations:
|
|
7
|
+
# 1. Parallelize PodTargetIntegrator integration (multiple pods at once)
|
|
8
|
+
# 2. Delegate install_pod_targets to original (safe, no threading issues)
|
|
9
|
+
#
|
|
10
|
+
# Reference: CocoaPods — lib/cocoapods/installer/xcode/pods_project_generator.rb
|
|
11
|
+
|
|
12
|
+
module Pod
|
|
13
|
+
module PodGenerate
|
|
14
|
+
module Patches
|
|
15
|
+
module InstallerPatch
|
|
16
|
+
def self.apply
|
|
17
|
+
Pod::UI.message '[cocoapods-podgenerate] Applying InstallerPatch (optimized integration)'
|
|
18
|
+
Pod::Installer::Xcode::PodsProjectGenerator.prepend(ParallelInstall)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
module ParallelInstall
|
|
22
|
+
def install_pod_targets(project, pod_targets)
|
|
23
|
+
super
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Override integrate_targets to run in parallel
|
|
27
|
+
def integrate_targets(pod_target_installation_results)
|
|
28
|
+
pods_to_integrate = pod_target_installation_results.values.select do |result|
|
|
29
|
+
target = result.target
|
|
30
|
+
!result.test_native_targets.empty? ||
|
|
31
|
+
!result.app_native_targets.empty? ||
|
|
32
|
+
target.contains_script_phases? ||
|
|
33
|
+
target.framework_paths.values.flatten.any? { |p| !p.dsym_path.nil? } ||
|
|
34
|
+
target.xcframeworks.values.any?(&:any?)
|
|
35
|
+
end
|
|
36
|
+
return if pods_to_integrate.empty?
|
|
37
|
+
|
|
38
|
+
use_io_paths = !installation_options.disable_input_output_paths
|
|
39
|
+
threads = pods_to_integrate.map do |result|
|
|
40
|
+
Thread.new do
|
|
41
|
+
begin
|
|
42
|
+
Pod::Installer::Xcode::PodsProjectGenerator::PodTargetIntegrator.new(
|
|
43
|
+
result, :use_input_output_paths => use_io_paths
|
|
44
|
+
).integrate!
|
|
45
|
+
rescue StandardError => e
|
|
46
|
+
Pod::UI.warn "[cocoapods-podgenerate] Integrate error: #{e.message}"
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
threads.each(&:join)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# [cocoapods-podgenerate]
|
|
4
|
+
# Monkey-patches Pod::Project to cache pod_group lookups.
|
|
5
|
+
# Original implementation does O(n) linear scan for every pod_group call:
|
|
6
|
+
# pod_groups.find { |group| group.name == pod_name }
|
|
7
|
+
#
|
|
8
|
+
# With 200+ pods and pod_group called 3-5 times per pod, this is 600-1000
|
|
9
|
+
# linear scans of a 200-element array = ~120k-200k iterations.
|
|
10
|
+
#
|
|
11
|
+
# Fix: cache groups in a Hash for O(1) lookup. Invalidate on add_pod_group.
|
|
12
|
+
#
|
|
13
|
+
# Reference: CocoaPods source — lib/cocoapods/project.rb
|
|
14
|
+
|
|
15
|
+
module Pod
|
|
16
|
+
module PodGenerate
|
|
17
|
+
module Patches
|
|
18
|
+
module ProjectPatch
|
|
19
|
+
def self.apply
|
|
20
|
+
Pod::UI.message '[cocoapods-podgenerate] Applying ProjectPatch (pod_group hash cache)'
|
|
21
|
+
Pod::Project.prepend(CachedPodGroup)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
module CachedPodGroup
|
|
25
|
+
# Build a hash cache of pod_name => PBXGroup
|
|
26
|
+
# Called once when first needed, then kept in sync.
|
|
27
|
+
def pod_group(pod_name)
|
|
28
|
+
@pod_group_cache ||= build_pod_group_cache
|
|
29
|
+
@pod_group_cache[pod_name]
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Override add_pod_group to invalidate the cache
|
|
33
|
+
def add_pod_group(pod_name, path, development = false, absolute = false)
|
|
34
|
+
group = super
|
|
35
|
+
# Invalidate cache so the next call to pod_group rebuilds it
|
|
36
|
+
@pod_group_cache = nil if defined?(@pod_group_cache)
|
|
37
|
+
group
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def build_pod_group_cache
|
|
43
|
+
cache = {}
|
|
44
|
+
pod_groups.each do |group|
|
|
45
|
+
cache[group.name] = group
|
|
46
|
+
end
|
|
47
|
+
cache
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# [cocoapods-podgenerate]
|
|
4
|
+
# Monkey-patches PodsProjectWriter to support incremental project saves.
|
|
5
|
+
# Uses project.pbxproj file for change detection via SHA256 digest.
|
|
6
|
+
#
|
|
7
|
+
# Reference: CocoaPods source — lib/cocoapods/installer/xcode/pods_project_generator/pods_project_writer.rb
|
|
8
|
+
|
|
9
|
+
module Pod
|
|
10
|
+
module PodGenerate
|
|
11
|
+
module Patches
|
|
12
|
+
module ProjectWriterPatch
|
|
13
|
+
def self.apply
|
|
14
|
+
Pod::UI.message '[cocoapods-podgenerate] Applying ProjectWriterPatch (incremental save)'
|
|
15
|
+
Pod::Installer::Xcode::PodsProjectWriter.prepend(IncrementalSave)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
module IncrementalSave
|
|
19
|
+
def initialize(sandbox, projects, pod_target_installation_results, installation_options)
|
|
20
|
+
super
|
|
21
|
+
@project_digests = {}
|
|
22
|
+
@projects = projects
|
|
23
|
+
@sort_needed = {}
|
|
24
|
+
compute_initial_digests
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def save_projects(projects)
|
|
28
|
+
projects.each do |project|
|
|
29
|
+
if project_unchanged?(project)
|
|
30
|
+
Pod::UI.message "- Skipping unchanged project #{UI.path project.path}"
|
|
31
|
+
next
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
project.sort(:groups_position => :below) if needs_sort?(project)
|
|
35
|
+
Pod::UI.message "- Writing Xcode project file to #{UI.path project.path}" do
|
|
36
|
+
project.save
|
|
37
|
+
end
|
|
38
|
+
update_digest(project)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def compute_initial_digests
|
|
45
|
+
@projects.each do |project|
|
|
46
|
+
update_digest(project)
|
|
47
|
+
end
|
|
48
|
+
@projects.each { |p| @sort_needed[p.object_id] = true }
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def project_unchanged?(project)
|
|
52
|
+
pbx_path = pbxproj_path(project)
|
|
53
|
+
return false unless pbx_path && File.exist?(pbx_path)
|
|
54
|
+
|
|
55
|
+
old_digest = @project_digests[project.object_id]
|
|
56
|
+
return false unless old_digest
|
|
57
|
+
|
|
58
|
+
current_digest = digest_file(pbx_path)
|
|
59
|
+
current_digest == old_digest
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def needs_sort?(project)
|
|
63
|
+
@sort_needed[project.object_id] != false
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def update_digest(project)
|
|
67
|
+
pbx_path = pbxproj_path(project)
|
|
68
|
+
return unless pbx_path && File.exist?(pbx_path)
|
|
69
|
+
|
|
70
|
+
@project_digests[project.object_id] = digest_file(pbx_path)
|
|
71
|
+
@sort_needed[project.object_id] = false
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def pbxproj_path(project)
|
|
75
|
+
path = project.path
|
|
76
|
+
return nil unless path
|
|
77
|
+
# .xcodeproj is a directory; the actual content is in project.pbxproj
|
|
78
|
+
if path.to_s.end_with?('.xcodeproj')
|
|
79
|
+
File.join(path.to_s, 'project.pbxproj')
|
|
80
|
+
else
|
|
81
|
+
path.to_s
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def digest_file(path)
|
|
86
|
+
require 'digest'
|
|
87
|
+
return nil unless File.file?(path)
|
|
88
|
+
Digest::SHA256.file(path).hexdigest
|
|
89
|
+
rescue StandardError
|
|
90
|
+
nil
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# [cocoapods-podgenerate]
|
|
4
|
+
# Monkey-patches UserProjectIntegrator to parallelize and optimize the
|
|
5
|
+
# "Integrating client project" step (step 4 of pod install).
|
|
6
|
+
#
|
|
7
|
+
# For projects with many user targets / aggregate targets, the integration
|
|
8
|
+
# step runs serially. This patch:
|
|
9
|
+
# 1. Parallelizes integrate_user_targets using threads
|
|
10
|
+
# 2. Parallelizes save_projects using threads
|
|
11
|
+
# 3. Caches user_project references to avoid redundant project parsing
|
|
12
|
+
#
|
|
13
|
+
# Reference: CocoaPods source
|
|
14
|
+
# - lib/cocoapods/installer/user_project_integrator.rb
|
|
15
|
+
# - lib/cocoapods/installer/user_project_integrator/target_integrator.rb
|
|
16
|
+
|
|
17
|
+
module Pod
|
|
18
|
+
module PodGenerate
|
|
19
|
+
module Patches
|
|
20
|
+
module UserIntegratorPatch
|
|
21
|
+
def self.apply
|
|
22
|
+
Pod::UI.message '[cocoapods-podgenerate] Applying UserIntegratorPatch (parallel client integration)'
|
|
23
|
+
Pod::Installer::UserProjectIntegrator.prepend(ParallelIntegration)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
module ParallelIntegration
|
|
27
|
+
# Override integrate_user_targets to use parallel execution
|
|
28
|
+
def integrate_user_targets
|
|
29
|
+
target_integrators = targets_to_integrate.sort_by(&:name).map do |target|
|
|
30
|
+
Pod::Installer::UserProjectIntegrator::TargetIntegrator.new(target, :use_input_output_paths => use_input_output_paths?)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
if target_integrators.size <= 1
|
|
34
|
+
# Single target — no need for threads
|
|
35
|
+
target_integrators.each(&:integrate!)
|
|
36
|
+
return
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Multiple targets — integrate in parallel
|
|
40
|
+
Pod::UI.message "- Integrating #{target_integrators.size} targets in parallel"
|
|
41
|
+
threads = target_integrators.map do |integrator|
|
|
42
|
+
Thread.new do
|
|
43
|
+
begin
|
|
44
|
+
integrator.integrate!
|
|
45
|
+
rescue StandardError => e
|
|
46
|
+
Pod::UI.warn "[cocoapods-podgenerate] Target integration error: #{e.message}"
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
threads.each(&:join)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Override save_projects to use parallel saving
|
|
54
|
+
def save_projects(projects)
|
|
55
|
+
projects = projects.uniq
|
|
56
|
+
|
|
57
|
+
if projects.size <= 1
|
|
58
|
+
projects.each do |project|
|
|
59
|
+
if project.dirty?
|
|
60
|
+
project.save
|
|
61
|
+
else
|
|
62
|
+
FileUtils.touch(project.path + 'project.pbxproj')
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
return
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Save multiple projects in parallel
|
|
69
|
+
Pod::UI.message "- Saving #{projects.size} user projects in parallel"
|
|
70
|
+
mutex = Mutex.new
|
|
71
|
+
threads = projects.map do |project|
|
|
72
|
+
Thread.new do
|
|
73
|
+
begin
|
|
74
|
+
if project.dirty?
|
|
75
|
+
project.save
|
|
76
|
+
else
|
|
77
|
+
mutex.synchronize do
|
|
78
|
+
FileUtils.touch(project.path + 'project.pbxproj')
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
rescue StandardError => e
|
|
82
|
+
Pod::UI.warn "[cocoapods-podgenerate] Project save error: #{e.message}"
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
threads.each(&:join)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'cocoapods-podgenerate/patches/installer_patch'
|
|
4
|
+
require 'cocoapods-podgenerate/patches/project_patch'
|
|
5
|
+
require 'cocoapods-podgenerate/patches/project_writer_patch'
|
|
6
|
+
require 'cocoapods-podgenerate/patches/analyzer_patch'
|
|
7
|
+
require 'cocoapods-podgenerate/patches/user_integrator_patch'
|
|
8
|
+
require 'cocoapods-podgenerate/parallel/thread_pool'
|
|
9
|
+
require 'cocoapods-podgenerate/parallel/batch_processor'
|
|
10
|
+
require 'cocoapods-podgenerate/benchmark/profiler'
|
|
11
|
+
|
|
12
|
+
module Pod
|
|
13
|
+
module PodGenerate
|
|
14
|
+
def self.activate
|
|
15
|
+
# Register all patches
|
|
16
|
+
Pod::PodGenerate::Patches::InstallerPatch.apply
|
|
17
|
+
Pod::PodGenerate::Patches::ProjectPatch.apply
|
|
18
|
+
Pod::PodGenerate::Patches::ProjectWriterPatch.apply
|
|
19
|
+
Pod::PodGenerate::Patches::AnalyzerPatch.apply
|
|
20
|
+
Pod::PodGenerate::Patches::UserIntegratorPatch.apply
|
|
21
|
+
|
|
22
|
+
# Install hook for profiler
|
|
23
|
+
Pod::PodGenerate::Benchmark::Profiler.install
|
|
24
|
+
|
|
25
|
+
Pod::UI.message '[cocoapods-podgenerate] Activated!'
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Auto-activate when loaded via `plugin` directive in Podfile
|
|
31
|
+
# The :pre_install hook is set up by hooks.rb, but we also support
|
|
32
|
+
# direct `plugin` activation which defers to when CocoaPods is loaded.
|
|
33
|
+
if defined?(Pod::HooksManager)
|
|
34
|
+
Pod::PodGenerate.activate
|
|
35
|
+
else
|
|
36
|
+
# Defer activation: when CocoaPods is loaded after this file
|
|
37
|
+
TracePoint.trace(:class) do |tp|
|
|
38
|
+
if tp.self == Pod::HooksManager
|
|
39
|
+
Pod::PodGenerate.activate
|
|
40
|
+
tp.disable
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: cocoapods-podgenerate
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- PodGenerate Team
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: cocoapods
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: 1.10.0
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: 1.10.0
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: concurrent-ruby
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '1.1'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '1.1'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: bundler
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '2.0'
|
|
47
|
+
type: :development
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '2.0'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: rake
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - "~>"
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '13.0'
|
|
61
|
+
type: :development
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - "~>"
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '13.0'
|
|
68
|
+
- !ruby/object:Gem::Dependency
|
|
69
|
+
name: rspec
|
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - "~>"
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '3.0'
|
|
75
|
+
type: :development
|
|
76
|
+
prerelease: false
|
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - "~>"
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '3.0'
|
|
82
|
+
description: A CocoaPods plugin that accelerates pod install for large-scale projects
|
|
83
|
+
with 200+ pods by introducing parallel processing, optimized dependency analysis,
|
|
84
|
+
and incremental project generation.
|
|
85
|
+
executables: []
|
|
86
|
+
extensions: []
|
|
87
|
+
extra_rdoc_files: []
|
|
88
|
+
files:
|
|
89
|
+
- lib/cocoapods-podgenerate.rb
|
|
90
|
+
- lib/cocoapods-podgenerate/benchmark/profiler.rb
|
|
91
|
+
- lib/cocoapods-podgenerate/command.rb
|
|
92
|
+
- lib/cocoapods-podgenerate/hooks.rb
|
|
93
|
+
- lib/cocoapods-podgenerate/parallel/batch_processor.rb
|
|
94
|
+
- lib/cocoapods-podgenerate/parallel/thread_pool.rb
|
|
95
|
+
- lib/cocoapods-podgenerate/patches/analyzer_patch.rb
|
|
96
|
+
- lib/cocoapods-podgenerate/patches/installer_patch.rb
|
|
97
|
+
- lib/cocoapods-podgenerate/patches/project_patch.rb
|
|
98
|
+
- lib/cocoapods-podgenerate/patches/project_writer_patch.rb
|
|
99
|
+
- lib/cocoapods-podgenerate/patches/user_integrator_patch.rb
|
|
100
|
+
- lib/cocoapods_plugin.rb
|
|
101
|
+
homepage: https://github.com/example/cocoapods-podgenerate
|
|
102
|
+
licenses:
|
|
103
|
+
- MIT
|
|
104
|
+
metadata: {}
|
|
105
|
+
rdoc_options: []
|
|
106
|
+
require_paths:
|
|
107
|
+
- lib
|
|
108
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
109
|
+
requirements:
|
|
110
|
+
- - ">="
|
|
111
|
+
- !ruby/object:Gem::Version
|
|
112
|
+
version: '0'
|
|
113
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
114
|
+
requirements:
|
|
115
|
+
- - ">="
|
|
116
|
+
- !ruby/object:Gem::Version
|
|
117
|
+
version: '0'
|
|
118
|
+
requirements: []
|
|
119
|
+
rubygems_version: 4.0.9
|
|
120
|
+
specification_version: 4
|
|
121
|
+
summary: Speeds up CocoaPods install for large projects (200+ pods)
|
|
122
|
+
test_files: []
|