-
Notifications
You must be signed in to change notification settings - Fork 2.6k
/
installer.rb
1112 lines (957 loc) · 42.8 KB
/
installer.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
require 'active_support/core_ext/string/inflections'
require 'fileutils'
require 'cocoapods/podfile'
module Pod
# The Installer is responsible of taking a Podfile and transform it in the
# Pods libraries. It also integrates the user project so the Pods
# libraries can be used out of the box.
#
# The Installer is capable of doing incremental updates to an existing Pod
# installation.
#
# The Installer gets the information that it needs mainly from 3 files:
#
# - Podfile: The specification written by the user that contains
# information about targets and Pods.
# - Podfile.lock: Contains information about the pods that were previously
# installed and in concert with the Podfile provides information about
# which specific version of a Pod should be installed. This file is
# ignored in update mode.
# - Manifest.lock: A file contained in the Pods folder that keeps track of
# the pods installed in the local machine. This files is used once the
# exact versions of the Pods has been computed to detect if that version
# is already installed. This file is not intended to be kept under source
# control and is a copy of the Podfile.lock.
#
# The Installer is designed to work in environments where the Podfile folder
# is under source control and environments where it is not. The rest of the
# files, like the user project and the workspace are assumed to be under
# source control.
#
class Installer
autoload :Analyzer, 'cocoapods/installer/analyzer'
autoload :InstallationOptions, 'cocoapods/installer/installation_options'
autoload :PostInstallHooksContext, 'cocoapods/installer/post_install_hooks_context'
autoload :PreInstallHooksContext, 'cocoapods/installer/pre_install_hooks_context'
autoload :BaseInstallHooksContext, 'cocoapods/installer/base_install_hooks_context'
autoload :PostIntegrateHooksContext, 'cocoapods/installer/post_integrate_hooks_context'
autoload :PreIntegrateHooksContext, 'cocoapods/installer/pre_integrate_hooks_context'
autoload :SourceProviderHooksContext, 'cocoapods/installer/source_provider_hooks_context'
autoload :PodfileValidator, 'cocoapods/installer/podfile_validator'
autoload :PodSourceDownloader, 'cocoapods/installer/pod_source_downloader'
autoload :PodSourceInstaller, 'cocoapods/installer/pod_source_installer'
autoload :PodSourcePreparer, 'cocoapods/installer/pod_source_preparer'
autoload :UserProjectIntegrator, 'cocoapods/installer/user_project_integrator'
autoload :Xcode, 'cocoapods/installer/xcode'
autoload :SandboxHeaderPathsInstaller, 'cocoapods/installer/sandbox_header_paths_installer'
autoload :SandboxDirCleaner, 'cocoapods/installer/sandbox_dir_cleaner'
autoload :ProjectCache, 'cocoapods/installer/project_cache/project_cache'
autoload :TargetUUIDGenerator, 'cocoapods/installer/target_uuid_generator'
include Config::Mixin
MASTER_SPECS_REPO_GIT_URL = 'https://github.com/CocoaPods/Specs.git'.freeze
# @return [Sandbox] The sandbox where the Pods should be installed.
#
attr_reader :sandbox
# @return [Podfile] The Podfile specification that contains the information
# of the Pods that should be installed.
#
attr_reader :podfile
# @return [Lockfile] The Lockfile that stores the information about the
# Pods previously installed on any machine.
#
attr_reader :lockfile
# Initialize a new instance
#
# @param [Sandbox] sandbox @see #sandbox
# @param [Podfile] podfile @see #podfile
# @param [Lockfile] lockfile @see #lockfile
#
def initialize(sandbox, podfile, lockfile = nil)
@sandbox = sandbox || raise(ArgumentError, 'Missing required argument `sandbox`')
@podfile = podfile || raise(ArgumentError, 'Missing required argument `podfile`')
@lockfile = lockfile
@use_default_plugins = true
@has_dependencies = true
@pod_installers = []
end
# @return [Hash, Boolean, nil] Pods that have been requested to be
# updated or true if all Pods should be updated.
# If all Pods should been updated the contents of the Lockfile are
# not taken into account for deciding what Pods to install.
#
attr_accessor :update
# @return [Boolean] Whether it has dependencies. Defaults to true.
#
attr_accessor :has_dependencies
alias_method :has_dependencies?, :has_dependencies
# @return [Boolean] Whether the spec repos should be updated.
#
attr_accessor :repo_update
alias_method :repo_update?, :repo_update
# @return [Boolean] Whether default plugins should be used during
# installation. Defaults to true.
#
attr_accessor :use_default_plugins
alias_method :use_default_plugins?, :use_default_plugins
# @return [Boolean] Whether installation should verify that there are no
# Podfile or Lockfile changes. Defaults to false.
#
attr_accessor :deployment
alias_method :deployment?, :deployment
# @return [Boolean] Whether installation should ignore the contents of the project cache
# when incremental installation is enabled.
#
attr_accessor :clean_install
alias_method :clean_install?, :clean_install
#-------------------------------------------------------------------------#
private
# @return [Array<PodSourceInstaller>] the pod installers created
# while installing pod targets
#
attr_reader :pod_installers
# @return [ProjectInstallationCache] The installation cache stored in Pods/.project_cache/installation_cache
#
attr_reader :installation_cache
# @return [ProjectMetadataCache] The metadata cache stored in Pods/.project_cache/metadata_cache
#
attr_reader :metadata_cache
# @return [ProjectCacheVersion] The version of the project cache stored in Pods/.project_cache/version
#
attr_reader :project_cache_version
#-------------------------------------------------------------------------#
public
# Installs the Pods.
#
# The installation process is mostly linear with a few minor complications
# to keep in mind:
#
# - The stored podspecs need to be cleaned before the resolution step
# otherwise the sandbox might return an old podspec and not download
# the new one from an external source.
# - The resolver might trigger the download of Pods from external sources
# necessary to retrieve their podspec (unless it is instructed not to
# do it).
#
# @return [void]
#
def install!
prepare
resolve_dependencies
download_dependencies
validate_targets
clean_sandbox
if installation_options.skip_pods_project_generation?
show_skip_pods_project_generation_message
run_podfile_post_install_hooks
else
integrate
end
write_lockfiles
perform_post_install_actions
end
def show_skip_pods_project_generation_message
UI.section 'Skipping Pods Project Creation'
UI.section 'Skipping User Project Integration'
end
def integrate
run_podfile_pre_integrate_hooks
generate_pods_project
if installation_options.integrate_targets?
integrate_user_project
else
UI.section 'Skipping User Project Integration'
end
end
def analyze_project_cache
user_projects = aggregate_targets.map(&:user_project).compact.uniq
object_version = user_projects.min_by { |p| p.object_version.to_i }.object_version.to_i unless user_projects.empty?
if !installation_options.incremental_installation
# Run entire installation.
ProjectCache::ProjectCacheAnalysisResult.new(pod_targets, aggregate_targets, {},
analysis_result.all_user_build_configurations, object_version)
else
UI.message 'Analyzing Project Cache' do
@installation_cache = ProjectCache::ProjectInstallationCache.from_file(sandbox, sandbox.project_installation_cache_path)
@metadata_cache = ProjectCache::ProjectMetadataCache.from_file(sandbox, sandbox.project_metadata_cache_path)
@project_cache_version = ProjectCache::ProjectCacheVersion.from_file(sandbox.project_version_cache_path)
force_clean_install = clean_install || project_cache_version.version != Version.create(VersionMetadata.project_cache_version)
cache_result = ProjectCache::ProjectCacheAnalyzer.new(sandbox, installation_cache, analysis_result.all_user_build_configurations,
object_version, plugins, pod_targets, aggregate_targets, installation_options.to_h, :clean_install => force_clean_install).analyze
aggregate_targets_to_generate = cache_result.aggregate_targets_to_generate || []
pod_targets_to_generate = cache_result.pod_targets_to_generate
(aggregate_targets_to_generate + pod_targets_to_generate).each do |target|
UI.message "- Regenerating #{target.label}"
end
cache_result
end
end
end
def prepare
# Raise if pwd is inside Pods
if Dir.pwd.start_with?(sandbox.root.to_path)
message = 'Command should be run from a directory outside Pods directory.'
message << "\n\n\tCurrent directory is #{UI.path(Pathname.pwd)}\n"
raise Informative, message
end
UI.message 'Preparing' do
deintegrate_if_different_major_version
sandbox.prepare
ensure_plugins_are_installed!
run_plugins_pre_install_hooks
end
end
# @return [Analyzer] The analyzer used to resolve dependencies
#
def resolve_dependencies
plugin_sources = run_source_provider_hooks
analyzer = create_analyzer(plugin_sources)
UI.section 'Updating local specs repositories' do
analyzer.update_repositories
end if repo_update?
UI.section 'Analyzing dependencies' do
analyze(analyzer)
validate_build_configurations
end
UI.section 'Verifying no changes' do
verify_no_podfile_changes!
verify_no_lockfile_changes!
end if deployment?
analyzer
end
def download_dependencies
UI.section 'Downloading dependencies' do
install_pod_sources
run_podfile_pre_install_hooks
clean_pod_sources
end
end
# Stages the sandbox after analysis.
#
# @param [Sandbox] sandbox
# The sandbox to stage.
#
# @param [Array<PodTarget>] pod_targets
# The list of all pod targets.
#
# @return [void]
#
def stage_sandbox(sandbox, pod_targets)
SandboxHeaderPathsInstaller.new(sandbox, pod_targets).install!
end
#-------------------------------------------------------------------------#
# @!group Pods Project Generation
private
def create_generator(pod_targets_to_generate, aggregate_targets_to_generate, build_configurations, project_object_version, generate_multiple_pod_projects = false)
if generate_multiple_pod_projects
Xcode::MultiPodsProjectGenerator.new(sandbox, aggregate_targets_to_generate, pod_targets_to_generate,
build_configurations, installation_options, config, project_object_version, metadata_cache)
else
Xcode::SinglePodsProjectGenerator.new(sandbox, aggregate_targets_to_generate, pod_targets_to_generate, build_configurations, installation_options, config, project_object_version)
end
end
# Generates the Xcode project(s) that go inside the `Pods/` directory.
#
def generate_pods_project
stage_sandbox(sandbox, pod_targets)
cache_analysis_result = analyze_project_cache
pod_targets_to_generate = cache_analysis_result.pod_targets_to_generate
aggregate_targets_to_generate = cache_analysis_result.aggregate_targets_to_generate
pod_targets_to_generate.each do |pod_target|
pod_target.build_headers.implode_path!(pod_target.headers_sandbox)
sandbox.public_headers.implode_path!(pod_target.headers_sandbox)
end
create_and_save_projects(pod_targets_to_generate, aggregate_targets_to_generate,
cache_analysis_result.build_configurations, cache_analysis_result.project_object_version)
SandboxDirCleaner.new(sandbox, pod_targets, aggregate_targets).clean!
update_project_cache(cache_analysis_result, target_installation_results)
end
def create_and_save_projects(pod_targets_to_generate, aggregate_targets_to_generate, build_configurations, project_object_version)
UI.section 'Generating Pods project' do
generator = create_generator(pod_targets_to_generate, aggregate_targets_to_generate,
build_configurations, project_object_version,
installation_options.generate_multiple_pod_projects)
pod_project_generation_result = generator.generate!
@target_installation_results = pod_project_generation_result.target_installation_results
@pods_project = pod_project_generation_result.project
# The `pod_target_subprojects` is used for backwards compatibility so that consumers can iterate over
# all pod targets across projects without needing to open each one.
@pod_target_subprojects = pod_project_generation_result.projects_by_pod_targets.keys
@generated_projects = ([pods_project] + pod_target_subprojects || []).compact
@generated_pod_targets = pod_targets_to_generate
@generated_aggregate_targets = aggregate_targets_to_generate || []
projects_by_pod_targets = pod_project_generation_result.projects_by_pod_targets
predictabilize_uuids(generated_projects) if installation_options.deterministic_uuids?
stabilize_target_uuids(generated_projects)
projects_writer = Xcode::PodsProjectWriter.new(sandbox, generated_projects,
target_installation_results.pod_target_installation_results, installation_options)
projects_writer.write! do
run_podfile_post_install_hooks
end
pods_project_pod_targets = pod_targets_to_generate - projects_by_pod_targets.values.flatten
all_projects_by_pod_targets = {}
pods_project_by_targets = { pods_project => pods_project_pod_targets } if pods_project
all_projects_by_pod_targets.merge!(pods_project_by_targets) if pods_project_by_targets
all_projects_by_pod_targets.merge!(projects_by_pod_targets) if projects_by_pod_targets
all_projects_by_pod_targets.each do |project, pod_targets|
generator.configure_schemes(project, pod_targets, pod_project_generation_result)
end
end
end
def predictabilize_uuids(projects)
UI.message('- Generating deterministic UUIDs') { Xcodeproj::Project.predictabilize_uuids(projects) }
end
def stabilize_target_uuids(projects)
UI.message('- Stabilizing target UUIDs') { TargetUUIDGenerator.new(projects).generate! }
end
#-------------------------------------------------------------------------#
public
# @!group Installation results
# @return [Analyzer::AnalysisResult] the result of the analysis performed during installation
#
attr_reader :analysis_result
# @return [Array<Hash{String, TargetInstallationResult}>] the installation results produced by the pods project
# generator
#
attr_reader :target_installation_results
# @return [Pod::Project] the `Pods/Pods.xcodeproj` project.
#
attr_reader :pods_project
# @return [Array<Pod::Project>] the subprojects nested under pods_project.
#
attr_reader :pod_target_subprojects
# @return [Array<AggregateTarget>] The model representations of an
# aggregation of pod targets generated for a target definition
# in the Podfile as result of the analyzer.
#
attr_reader :aggregate_targets
# @return [Array<PodTarget>] The model representations of pod targets
# generated as result of the analyzer.
#
attr_reader :pod_targets
# @return [Array<Project>] The list of projects generated from the installation.
#
attr_reader :generated_projects
# @return [Array<PodTarget>] The list of pod targets that were generated from the installation.
#
attr_reader :generated_pod_targets
# @return [Array<AggregateTarget>] The list of aggregate targets that were generated from the installation.
#
attr_reader :generated_aggregate_targets
# @return [Array<Specification>] The specifications that were installed.
#
attr_accessor :installed_specs
#-------------------------------------------------------------------------#
private
# @!group Installation steps
# Performs the analysis.
#
# @param [Analyzer] analyzer the analyzer to use for analysis
#
# @return [void]
#
def analyze(analyzer = create_analyzer)
@analysis_result = analyzer.analyze
@aggregate_targets = @analysis_result.targets
@pod_targets = @analysis_result.pod_targets
end
def create_analyzer(plugin_sources = nil)
Analyzer.new(sandbox, podfile, lockfile, plugin_sources, has_dependencies?, update)
end
# Ensures that the white-listed build configurations are known to prevent
# silent typos.
#
# @raise If an unknown user configuration is found.
#
def validate_build_configurations
whitelisted_configs = pod_targets.
flat_map(&:target_definitions).
flat_map(&:all_whitelisted_configurations).
map(&:downcase).
uniq
all_user_configurations = analysis_result.all_user_build_configurations.keys.map(&:downcase)
remainder = whitelisted_configs - all_user_configurations
unless remainder.empty?
raise Informative,
"Unknown #{'configuration'.pluralize(remainder.size)} whitelisted: #{remainder.sort.to_sentence}. " \
"CocoaPods found #{all_user_configurations.sort.to_sentence}, did you mean one of these?"
end
end
# @return [void] Performs a general clean up of the sandbox related to the sandbox state that was
# calculated. For example, pods that were marked for deletion are removed.
#
def clean_sandbox
unless sandbox_state.deleted.empty?
title_options = { :verbose_prefix => '-> '.red }
sandbox_state.deleted.each do |pod_name|
UI.titled_section("Removing #{pod_name}".red, title_options) do
root_name = Specification.root_name(pod_name)
pod_dir = sandbox.local?(root_name) ? nil : sandbox.pod_dir(root_name)
sandbox.clean_pod(pod_name, pod_dir)
end
end
end
# Check any changed pods that became local pods and used to be remote pods and
# ensure the sandbox is cleaned up.
unless sandbox_state.changed.empty?
sandbox_state.changed.each do |pod_name|
previous_spec_repo = sandbox.manifest.spec_repo(pod_name)
should_clean = !previous_spec_repo.nil? && sandbox.local?(pod_name)
sandbox.clean_pod(pod_name, sandbox.sources_root + Specification.root_name(pod_name)) if should_clean
end
end
end
# @raise [Informative] If there are any Podfile changes
#
def verify_no_podfile_changes!
return unless analysis_result.podfile_needs_install?
changed_state = analysis_result.podfile_state.to_s(:states => %i(added deleted changed))
raise Informative, "There were changes to the podfile in deployment mode:\n#{changed_state}"
end
# @raise [Informative] If there are any Lockfile changes
#
def verify_no_lockfile_changes!
new_lockfile = generate_lockfile
return if new_lockfile == lockfile
return unless diff = Xcodeproj::Differ.hash_diff(lockfile.to_hash, new_lockfile.to_hash, :key_1 => 'Old Lockfile', :key_2 => 'New Lockfile')
pretty_diff = YAMLHelper.convert_hash(diff, Lockfile::HASH_KEY_ORDER, "\n\n")
pretty_diff.gsub!(':diff:', 'diff:'.yellow)
raise Informative, "There were changes to the lockfile in deployment mode:\n#{pretty_diff}"
end
# Downloads, installs the documentation and cleans the sources of the Pods
# which need to be installed.
#
# @return [void]
#
def install_pod_sources
@downloaded_specs = []
@installed_specs = []
pods_to_install = sandbox_state.added | sandbox_state.changed
title_options = { :verbose_prefix => '-> '.green }
sorted_root_specs = root_specs.sort_by(&:name)
# Download pods in parallel before installing if the option is set
if installation_options.parallel_pod_downloads
require 'concurrent/executor/fixed_thread_pool'
thread_pool_size = installation_options.parallel_pod_download_thread_pool_size
thread_pool = Concurrent::FixedThreadPool.new(thread_pool_size, :idletime => 300)
sorted_root_specs.each do |spec|
if pods_to_install.include?(spec.name)
title = section_title(spec, 'Downloading')
UI.titled_section(title.green, title_options) do
thread_pool.post do
download_source_of_pod(spec.name)
end
end
end
end
thread_pool.shutdown
thread_pool.wait_for_termination
end
# Install pods, which includes downloading only if parallel_pod_downloads is set to false
sorted_root_specs.each do |spec|
if pods_to_install.include?(spec.name)
title = section_title(spec, 'Installing')
UI.titled_section(title.green, title_options) do
install_source_of_pod(spec.name)
end
else
UI.section("Using #{spec}", title_options[:verbose_prefix]) do
create_pod_installer(spec.name)
end
end
end
end
def section_title(spec, current_action)
if sandbox_state.changed.include?(spec.name) && sandbox.manifest
current_version = spec.version
previous_version = sandbox.manifest.version(spec.name)
has_changed_version = current_version != previous_version
current_repo = analysis_result.specs_by_source.detect { |key, values| break key if values.map(&:name).include?(spec.name) }
current_repo &&= (Pod::TrunkSource::TRUNK_REPO_NAME if current_repo.name == Pod::TrunkSource::TRUNK_REPO_NAME) || current_repo.url || current_repo.name
previous_spec_repo = sandbox.manifest.spec_repo(spec.name)
has_changed_repo = !previous_spec_repo.nil? && current_repo && !current_repo.casecmp(previous_spec_repo).zero?
title = "#{current_action} #{spec.name} #{spec.version}"
title << " (was #{previous_version} and source changed to `#{current_repo}` from `#{previous_spec_repo}`)" if has_changed_version && has_changed_repo
title << " (was #{previous_version})" if has_changed_version && !has_changed_repo
title << " (source changed to `#{current_repo}` from `#{previous_spec_repo}`)" if !has_changed_version && has_changed_repo
else
title = "#{current_action} #{spec}"
end
title
end
def create_pod_installer(pod_name)
specs_by_platform = specs_for_pod(pod_name)
if specs_by_platform.empty?
requiring_targets = pod_targets.select { |pt| pt.recursive_dependent_targets.any? { |dt| dt.pod_name == pod_name } }
message = "Could not install '#{pod_name}' pod"
message += ", depended upon by #{requiring_targets.to_sentence}" unless requiring_targets.empty?
message += '. There is either no platform to build for, or no target to build.'
raise StandardError, message
end
pod_installer = PodSourceInstaller.new(sandbox, podfile, specs_by_platform, :can_cache => installation_options.clean?)
pod_installers << pod_installer
pod_installer
end
def create_pod_downloader(pod_name)
specs_by_platform = specs_for_pod(pod_name)
if specs_by_platform.empty?
requiring_targets = pod_targets.select { |pt| pt.recursive_dependent_targets.any? { |dt| dt.pod_name == pod_name } }
message = "Could not download '#{pod_name}' pod"
message += ", depended upon by #{requiring_targets.to_sentence}" unless requiring_targets.empty?
message += '. There is either no platform to build for, or no target to build.'
raise StandardError, message
end
PodSourceDownloader.new(sandbox, podfile, specs_by_platform, :can_cache => installation_options.clean?)
end
# The specifications matching the specified pod name
#
# @param [String] pod_name the name of the pod
#
# @return [Hash{Platform => Array<Specification>}] the specifications grouped by platform
#
def specs_for_pod(pod_name)
pod_targets.each_with_object({}) do |pod_target, hash|
if pod_target.root_spec.name == pod_name
hash[pod_target.platform] ||= []
hash[pod_target.platform].concat(pod_target.specs)
end
end
end
# Install the Pods. If the resolver indicated that a Pod should be
# installed and it exits, it is removed and then reinstalled. In any case if
# the Pod doesn't exits it is installed.
#
# @return [void]
#
def install_source_of_pod(pod_name)
pod_installer = create_pod_installer(pod_name)
pod_installer.install!
@installed_specs.concat(pod_installer.specs_by_platform.values.flatten.uniq)
end
# Download the pod unless it is local or has been predownloaded from an
# external source.
#
# @return [void]
#
def download_source_of_pod(pod_name)
return if sandbox.local?(pod_name) || sandbox.predownloaded?(pod_name)
pod_downloader = create_pod_downloader(pod_name)
pod_downloader.download!
end
# Cleans the sources of the Pods if the config instructs to do so.
#
def clean_pod_sources
return unless installation_options.clean?
return if installed_specs.empty?
pod_installers.each(&:clean!)
end
# Unlocks the sources of the Pods.
#
def unlock_pod_sources
pod_installers.each do |installer|
pod_target = pod_targets.find { |target| target.pod_name == installer.name }
installer.unlock_files!(pod_target.file_accessors)
end
end
# Locks the sources of the Pods if the config instructs to do so.
#
def lock_pod_sources
return unless installation_options.lock_pod_sources?
pod_installers.each do |installer|
pod_target = pod_targets.find { |target| target.pod_name == installer.name }
installer.lock_files!(pod_target.file_accessors)
end
end
def validate_targets
validator = Xcode::TargetValidator.new(aggregate_targets, pod_targets, installation_options)
validator.validate!
end
# Runs the registered callbacks for the plugins pre install hooks.
#
# @return [void]
#
def run_plugins_pre_install_hooks
context = PreInstallHooksContext.generate(sandbox, podfile, lockfile)
HooksManager.run(:pre_install, context, plugins)
end
# Performs any post-installation actions
#
# @return [void]
#
def perform_post_install_actions
run_plugins_post_install_hooks
warn_for_deprecations
warn_for_installed_script_phases
warn_for_removing_git_master_specs_repo
print_post_install_message
end
def print_post_install_message
podfile_dependencies = analysis_result.podfile_dependency_cache.podfile_dependencies.size
pods_installed = root_specs.size
title_options = { :verbose_prefix => '-> '.green }
UI.titled_section('Pod installation complete! ' \
"There #{podfile_dependencies == 1 ? 'is' : 'are'} #{podfile_dependencies} " \
"#{'dependency'.pluralize(podfile_dependencies)} from the Podfile " \
"and #{pods_installed} total #{'pod'.pluralize(pods_installed)} installed.".green,
title_options)
end
# Runs the registered callbacks for the plugins pre integrate hooks.
#
def run_plugins_pre_integrate_hooks
if any_plugin_pre_integrate_hooks?
context = PreIntegrateHooksContext.generate(sandbox, pods_project, pod_target_subprojects, aggregate_targets)
HooksManager.run(:pre_integrate, context, plugins)
end
end
# Runs the registered callbacks for the plugins post install hooks.
#
def run_plugins_post_install_hooks
# This short-circuits because unlocking pod sources is expensive
if any_plugin_post_install_hooks?
unlock_pod_sources
context = PostInstallHooksContext.generate(sandbox, pods_project, pod_target_subprojects, aggregate_targets)
HooksManager.run(:post_install, context, plugins)
end
lock_pod_sources
end
# Runs the registered callbacks for the plugins post integrate hooks.
#
def run_plugins_post_integrate_hooks
if any_plugin_post_integrate_hooks?
context = PostIntegrateHooksContext.generate(sandbox, pods_project, pod_target_subprojects, aggregate_targets)
HooksManager.run(:post_integrate, context, plugins)
end
end
# @return [Boolean] whether there are any plugin pre-integrate hooks to run
#
def any_plugin_pre_integrate_hooks?
HooksManager.hooks_to_run(:pre_integrate, plugins).any?
end
# @return [Boolean] whether there are any plugin post-install hooks to run
#
def any_plugin_post_install_hooks?
HooksManager.hooks_to_run(:post_install, plugins).any?
end
# @return [Boolean] whether there are any plugin post-integrate hooks to run
#
def any_plugin_post_integrate_hooks?
HooksManager.hooks_to_run(:post_integrate, plugins).any?
end
# Runs the registered callbacks for the source provider plugin hooks.
#
# @return [Array<Pod::Source>] the plugin sources
#
def run_source_provider_hooks
context = SourceProviderHooksContext.generate
HooksManager.run(:source_provider, context, plugins)
context.sources
end
# Run the deintegrator against all projects in the installation root if the
# current CocoaPods major version part is different than the one in the
# lockfile.
#
# @return [void]
#
def deintegrate_if_different_major_version
return unless lockfile
return if lockfile.cocoapods_version.major == Version.create(VERSION).major
UI.section('Re-creating CocoaPods due to major version update.') do
projects = Pathname.glob(config.installation_root + '*.xcodeproj').map { |path| Xcodeproj::Project.open(path) }
deintegrator = Deintegrator.new
projects.each do |project|
config.with_changes(:silent => true) { deintegrator.deintegrate_project(project) }
project.save if project.dirty?
end
end
end
# Ensures that all plugins specified in the {#podfile} are loaded.
#
# @return [void]
#
def ensure_plugins_are_installed!
require 'claide/command/plugin_manager'
loaded_plugins = Command::PluginManager.specifications.map(&:name)
podfile.plugins.keys.each do |plugin|
unless loaded_plugins.include? plugin
raise Informative, "Your Podfile requires that the plugin `#{plugin}` be installed. Please install it and try installation again."
end
end
end
DEFAULT_PLUGINS = {}
# Returns the plugins that should be run, as indicated by the default
# plugins and the podfile's plugins
#
# @return [Hash<String, Hash>] The plugins to be used
#
def plugins
if use_default_plugins?
DEFAULT_PLUGINS.merge(podfile.plugins)
else
podfile.plugins
end
end
# Prints a warning for any pods that are deprecated
#
# @return [void]
#
def warn_for_deprecations
deprecated_pods = root_specs.select do |spec|
spec.deprecated || spec.deprecated_in_favor_of
end
deprecated_pods.each do |spec|
if spec.deprecated_in_favor_of
UI.warn "#{spec.name} has been deprecated in " \
"favor of #{spec.deprecated_in_favor_of}"
else
UI.warn "#{spec.name} has been deprecated"
end
end
end
# Prints a warning for any pods that included script phases
#
# @return [void]
#
def warn_for_installed_script_phases
pods_to_install = sandbox_state.added | sandbox_state.changed
pod_targets.group_by(&:pod_name).each do |name, pod_targets|
if pods_to_install.include?(name) && !sandbox.local?(name)
script_phase_count = pod_targets.inject(0) { |sum, target| sum + target.script_phases.count }
unless script_phase_count.zero?
UI.warn "#{name} has added #{script_phase_count} #{'script phase'.pluralize(script_phase_count)}. " \
'Please inspect before executing a build. See `https://guides.cocoapods.org/syntax/podspec.html#script_phases` for more information.'
end
end
end
end
# Prints a warning if the project is not explicitly using the git based master specs repo.
#
# Helps users to delete the git based master specs repo from the repos directory which reduces `--repo-update`
# speed and hopefully reduces Github workload.
#
# @return [void]
#
def warn_for_removing_git_master_specs_repo
return unless installation_options.warn_for_unused_master_specs_repo?
plugin_sources = run_source_provider_hooks
all_sources = podfile.sources + plugin_sources.map(&:url)
master_source = all_sources.find { |source| source == MASTER_SPECS_REPO_GIT_URL }
master_repo = config.sources_manager.all.find { |s| s.url == MASTER_SPECS_REPO_GIT_URL }
if master_source.nil? && !master_repo.nil?
UI.warn 'Your project does not explicitly specify the CocoaPods master specs repo. Since CDN is now used as the' \
' default, you may safely remove it from your repos directory via `pod repo remove master`. To suppress this warning' \
' please add `warn_for_unused_master_specs_repo => false` to your Podfile.'
end
end
# @return [Lockfile] The lockfile to write to disk.
#
def generate_lockfile
external_source_pods = analysis_result.podfile_dependency_cache.podfile_dependencies.select(&:external_source).map(&:root_name).uniq
checkout_options = sandbox.checkout_sources.select { |root_name, _| external_source_pods.include? root_name }
Lockfile.generate(podfile, analysis_result.specifications, checkout_options, analysis_result.specs_by_source)
end
# Writes the Podfile and the lock files.
#
# @return [void]
#
def write_lockfiles
@lockfile = generate_lockfile
UI.message "- Writing Lockfile in #{UI.path config.lockfile_path}" do
# No need to invoke Sandbox#update_changed_file here since this logic already handles checking if the
# contents of the file are the same.
@lockfile.write_to_disk(config.lockfile_path)
end
UI.message "- Writing Manifest in #{UI.path sandbox.manifest_path}" do
# No need to invoke Sandbox#update_changed_file here since this logic already handles checking if the
# contents of the file are the same.
@lockfile.write_to_disk(sandbox.manifest_path)
end
end
# @param [ProjectCacheAnalysisResult] cache_analysis_result
# The cache analysis result for the current installation.
#
# @param [Hash{String => TargetInstallationResult}] target_installation_results
# The installation results for pod targets installed.
#
def update_project_cache(cache_analysis_result, target_installation_results)
return unless installation_cache || metadata_cache
installation_cache.update_cache_key_by_target_label!(cache_analysis_result.cache_key_by_target_label)
installation_cache.update_project_object_version!(cache_analysis_result.project_object_version)
installation_cache.update_build_configurations!(cache_analysis_result.build_configurations)
installation_cache.update_podfile_plugins!(plugins)
installation_cache.update_installation_options!(installation_options.to_h)
installation_cache.save_as(sandbox.project_installation_cache_path)
metadata_cache.update_metadata!(target_installation_results.pod_target_installation_results || {},
target_installation_results.aggregate_target_installation_results || {})
metadata_cache.save_as(sandbox.project_metadata_cache_path)
cache_version = ProjectCache::ProjectCacheVersion.new(VersionMetadata.project_cache_version)
cache_version.save_as(sandbox.project_version_cache_path)
end
# Integrates the user projects adding the dependencies on the CocoaPods
# libraries, setting them up to use the xcconfigs and performing other
# actions. This step is also responsible of creating the workspace if
# needed.
#
# @return [void]
#
def integrate_user_project
UI.section "Integrating client #{'project'.pluralize(aggregate_targets.map(&:user_project_path).uniq.count)}" do
installation_root = config.installation_root
integrator = UserProjectIntegrator.new(podfile, sandbox, installation_root, aggregate_targets, generated_aggregate_targets,
:use_input_output_paths => !installation_options.disable_input_output_paths?)
integrator.integrate!
run_podfile_post_integrate_hooks
end
end
#-------------------------------------------------------------------------#
private
# @!group Hooks
# Runs the pre install hooks of the installed specs and of the Podfile.
#
# @return [void]
#
def run_podfile_pre_install_hooks
UI.message '- Running pre install hooks' do
executed = run_podfile_pre_install_hook
UI.message '- Podfile' if executed
end
end
# Runs the pre install hook of the Podfile
#
# @raise Raises an informative if the hooks raises.
#
# @return [Boolean] Whether the hook was run.
#
def run_podfile_pre_install_hook
podfile.pre_install!(self)
rescue => e
raise Informative, 'An error occurred while processing the pre-install ' \
'hook of the Podfile.' \
"\n\n#{e.message}\n\n#{e.backtrace * "\n"}"
end
# Runs the pre integrate hooks of the installed specs and of the Podfile.
#
# @note Pre integrate hooks run _before_ generation of the Pods project.
#
# @return [void]
#
def run_podfile_pre_integrate_hooks
UI.message '- Running pre integrate hooks' do
executed = run_podfile_pre_integrate_hook
UI.message '- Podfile' if executed
end
end
# Runs the pre integrate hook of the Podfile.
#
# @raise Raises an informative if the hooks raises.
#
# @return [Boolean] Whether the hook was run.
#
def run_podfile_pre_integrate_hook
podfile.pre_integrate!(self)
rescue => e
raise Informative, 'An error occurred while processing the pre-integrate ' \
'hook of the Podfile.' \
"\n\n#{e.message}\n\n#{e.backtrace * "\n"}"
end
# Runs the post install hooks of the installed specs and of the Podfile.
#
# @note Post install hooks run _before_ saving of project, so that they
# can alter it before it is written to the disk.
#
# @return [void]
#
def run_podfile_post_install_hooks
UI.message '- Running post install hooks' do