cocoapods-dykit 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,3 @@
1
+ module CocoapodsDylint
2
+ VERSION = "0.2.0"
3
+ end
data/lib/validator.rb ADDED
@@ -0,0 +1,889 @@
1
+ require 'active_support/core_ext/array'
2
+ require 'active_support/core_ext/string/inflections'
3
+
4
+ module Pod
5
+ # Validates a Specification.
6
+ #
7
+ # Extends the Linter from the Core to add additional which require the
8
+ # LocalPod and the Installer.
9
+ #
10
+ # In detail it checks that the file patterns defined by the user match
11
+ # actually do match at least a file and that the Pod builds, by installing
12
+ # it without integration and building the project with xcodebuild.
13
+ #
14
+ class DyValidator
15
+ include Config::Mixin
16
+
17
+ # @return [Specification::Linter] the linter instance from CocoaPods
18
+ # Core.
19
+ #
20
+ attr_reader :linter
21
+
22
+ # Initialize a new instance
23
+ #
24
+ # @param [Specification, Pathname, String] spec_or_path
25
+ # the Specification or the path of the `podspec` file to lint.
26
+ #
27
+ # @param [Array<String>] source_urls
28
+ # the Source URLs to use in creating a {Podfile}.
29
+ #
30
+ def initialize(spec_or_path, source_urls)
31
+ @linter = Specification::Linter.new(spec_or_path)
32
+ @source_urls = if @linter.spec && @linter.spec.dependencies.empty? && @linter.spec.recursive_subspecs.all? { |s| s.dependencies.empty? }
33
+ []
34
+ else
35
+ source_urls.map { |url| config.sources_manager.source_with_name_or_url(url) }.map(&:url)
36
+ end
37
+ end
38
+
39
+ #-------------------------------------------------------------------------#
40
+
41
+ # @return [Specification] the specification to lint.
42
+ #
43
+ def spec
44
+ @linter.spec
45
+ end
46
+
47
+ # @return [Pathname] the path of the `podspec` file where {#spec} is
48
+ # defined.
49
+ #
50
+ def file
51
+ @linter.file
52
+ end
53
+
54
+ # @return [Sandbox::FileAccessor] the file accessor for the spec.
55
+ #
56
+ attr_accessor :file_accessor
57
+
58
+ #-------------------------------------------------------------------------#
59
+
60
+ # Lints the specification adding a {Result} for any
61
+ # failed check to the {#results} list.
62
+ #
63
+ # @note This method shows immediately which pod is being processed and
64
+ # overrides the printed line once the result is known.
65
+ #
66
+ # @return [Bool] whether the specification passed validation.
67
+ #
68
+ def validate
69
+ @results = []
70
+
71
+ # Replace default spec with a subspec if asked for
72
+ a_spec = spec
73
+ if spec && @only_subspec
74
+ subspec_name = @only_subspec.start_with?(spec.root.name) ? @only_subspec : "#{spec.root.name}/#{@only_subspec}"
75
+ a_spec = spec.subspec_by_name(subspec_name, true, true)
76
+ @subspec_name = a_spec.name
77
+ end
78
+
79
+ UI.print " -> #{a_spec ? a_spec.name : file.basename}\r" unless config.silent?
80
+ $stdout.flush
81
+
82
+ perform_linting
83
+ perform_extensive_analysis(a_spec) if a_spec && !quick
84
+
85
+ UI.puts ' -> '.send(result_color) << (a_spec ? a_spec.to_s : file.basename.to_s)
86
+ print_results
87
+ validated?
88
+ end
89
+
90
+ # Prints the result of the validation to the user.
91
+ #
92
+ # @return [void]
93
+ #
94
+ def print_results
95
+ UI.puts results_message
96
+ end
97
+
98
+ def results_message
99
+ message = ''
100
+ results.each do |result|
101
+ if result.platforms == [:ios]
102
+ platform_message = '[iOS] '
103
+ elsif result.platforms == [:osx]
104
+ platform_message = '[OSX] '
105
+ elsif result.platforms == [:watchos]
106
+ platform_message = '[watchOS] '
107
+ elsif result.platforms == [:tvos]
108
+ platform_message = '[tvOS] '
109
+ end
110
+
111
+ subspecs_message = ''
112
+ if result.is_a?(Result)
113
+ subspecs = result.subspecs.uniq
114
+ if subspecs.count > 2
115
+ subspecs_message = '[' + subspecs[0..2].join(', ') + ', and more...] '
116
+ elsif subspecs.count > 0
117
+ subspecs_message = '[' + subspecs.join(',') + '] '
118
+ end
119
+ end
120
+
121
+ case result.type
122
+ when :error then type = 'ERROR'
123
+ when :warning then type = 'WARN'
124
+ when :note then type = 'NOTE'
125
+ else raise "#{result.type}" end
126
+ message << " - #{type.ljust(5)} | #{platform_message}#{subspecs_message}#{result.attribute_name}: #{result.message}\n"
127
+ end
128
+ message << "\n"
129
+ end
130
+
131
+ def failure_reason
132
+ results_by_type = results.group_by(&:type)
133
+ results_by_type.default = []
134
+ return nil if validated?
135
+ reasons = []
136
+ if (size = results_by_type[:error].size) && size > 0
137
+ reasons << "#{size} #{'error'.pluralize(size)}"
138
+ end
139
+ if !allow_warnings && (size = results_by_type[:warning].size) && size > 0
140
+ reason = "#{size} #{'warning'.pluralize(size)}"
141
+ pronoun = size == 1 ? 'it' : 'them'
142
+ reason << " (but you can use `--allow-warnings` to ignore #{pronoun})" if reasons.empty?
143
+ reasons << reason
144
+ end
145
+ if results.all?(&:public_only)
146
+ reasons << 'all results apply only to public specs, but you can use ' \
147
+ '`--private` to ignore them if linting the specification for a private pod'
148
+ end
149
+
150
+ reasons.to_sentence
151
+ end
152
+
153
+ #-------------------------------------------------------------------------#
154
+
155
+ #  @!group Configuration
156
+
157
+ # @return [Bool] whether the validation should skip the checks that
158
+ # requires the download of the library.
159
+ #
160
+ attr_accessor :quick
161
+
162
+ # @return [Bool] whether the linter should not clean up temporary files
163
+ # for inspection.
164
+ #
165
+ attr_accessor :no_clean
166
+
167
+ # @return [Bool] whether the linter should fail as soon as the first build
168
+ # variant causes an error. Helpful for i.e. multi-platforms specs,
169
+ # specs with subspecs.
170
+ #
171
+ attr_accessor :fail_fast
172
+
173
+ # @return [Bool] whether the validation should be performed against the root of
174
+ # the podspec instead to its original source.
175
+ #
176
+ # @note Uses the `:path` option of the Podfile.
177
+ #
178
+ attr_accessor :local
179
+ alias_method :local?, :local
180
+
181
+ # @return [Bool] Whether the validator should fail on warnings, or only on errors.
182
+ #
183
+ attr_accessor :allow_warnings
184
+
185
+ # @return [String] name of the subspec to check, if nil all subspecs are checked.
186
+ #
187
+ attr_accessor :only_subspec
188
+
189
+ # @return [Bool] Whether the validator should validate all subspecs.
190
+ #
191
+ attr_accessor :no_subspecs
192
+
193
+ # @return [Bool] Whether the validator should skip building and running tests.
194
+ #
195
+ attr_accessor :skip_tests
196
+
197
+ # @return [Bool] Whether frameworks should be used for the installation.
198
+ #
199
+ attr_accessor :use_frameworks
200
+
201
+ # @return [Boolean] Whether attributes that affect only public sources
202
+ # Bool be skipped.
203
+ #
204
+ attr_accessor :ignore_public_only_results
205
+
206
+ attr_accessor :skip_import_validation
207
+ alias_method :skip_import_validation?, :skip_import_validation
208
+
209
+ #-------------------------------------------------------------------------#
210
+
211
+ # !@group Lint results
212
+
213
+ #
214
+ #
215
+ attr_reader :results
216
+
217
+ # @return [Boolean]
218
+ #
219
+ def validated?
220
+ result_type != :error && (result_type != :warning || allow_warnings)
221
+ end
222
+
223
+ # @return [Symbol] The type, which should been used to display the result.
224
+ # One of: `:error`, `:warning`, `:note`.
225
+ #
226
+ def result_type
227
+ applicable_results = results
228
+ applicable_results = applicable_results.reject(&:public_only?) if ignore_public_only_results
229
+ types = applicable_results.map(&:type).uniq
230
+ if types.include?(:error) then :error
231
+ elsif types.include?(:warning) then :warning
232
+ else :note
233
+ end
234
+ end
235
+
236
+ # @return [Symbol] The color, which should been used to display the result.
237
+ # One of: `:green`, `:yellow`, `:red`.
238
+ #
239
+ def result_color
240
+ case result_type
241
+ when :error then :red
242
+ when :warning then :yellow
243
+ else :green end
244
+ end
245
+
246
+ # @return [Pathname] the temporary directory used by the linter.
247
+ #
248
+ def validation_dir
249
+ @validation_dir ||= Pathname(Dir.mktmpdir(['CocoaPods-Lint-', "-#{spec.name}"]))
250
+ end
251
+
252
+ # @return [String] the SWIFT_VERSION to use for validation.
253
+ #
254
+ def swift_version
255
+ @swift_version ||= dot_swift_version || '3.0'
256
+ end
257
+
258
+ # Set the SWIFT_VERSION that should be used to validate the pod.
259
+ #
260
+ attr_writer :swift_version
261
+
262
+ # @return [String] the SWIFT_VERSION in the .swift-version file or nil.
263
+ #
264
+ def dot_swift_version
265
+ return unless file
266
+ swift_version_path = file.dirname + '.swift-version'
267
+ return unless swift_version_path.exist?
268
+ swift_version_path.read.strip
269
+ end
270
+
271
+ # @return [String] A string representing the Swift version used during linting
272
+ # or nil, if Swift was not used.
273
+ #
274
+ def used_swift_version
275
+ swift_version if @installer.pod_targets.any?(&:uses_swift?)
276
+ end
277
+
278
+ #-------------------------------------------------------------------------#
279
+
280
+ private
281
+
282
+ # !@group Lint steps
283
+
284
+ #
285
+ #
286
+ def perform_linting
287
+ linter.lint
288
+ @results.concat(linter.results.to_a)
289
+ end
290
+
291
+ # Perform analysis for a given spec (or subspec)
292
+ #
293
+ def perform_extensive_analysis(spec)
294
+ if spec.test_specification?
295
+ error('spec', "Validating a test spec (`#{spec.name}`) is not supported.")
296
+ return false
297
+ end
298
+ validate_homepage(spec)
299
+ validate_screenshots(spec)
300
+ validate_social_media_url(spec)
301
+ validate_documentation_url(spec)
302
+
303
+ valid = spec.available_platforms.send(fail_fast ? :all? : :each) do |platform|
304
+ UI.message "\n\n#{spec} - Analyzing on #{platform} platform.".green.reversed
305
+ @consumer = spec.consumer(platform)
306
+ setup_validation_environment
307
+ begin
308
+ create_app_project
309
+ download_pod
310
+ check_file_patterns
311
+ install_pod
312
+ validate_dot_swift_version
313
+ add_app_project_import
314
+ validate_vendored_dynamic_frameworks
315
+ build_pod
316
+ test_pod unless skip_tests
317
+ ensure
318
+ tear_down_validation_environment
319
+ end
320
+ validated?
321
+ end
322
+ return false if fail_fast && !valid
323
+ perform_extensive_subspec_analysis(spec) unless @no_subspecs
324
+ rescue => e
325
+ message = e.to_s
326
+ message << "\n" << e.backtrace.join("\n") << "\n" if config.verbose?
327
+ error('unknown', "Encountered an unknown error (#{message}) during validation.")
328
+ false
329
+ end
330
+
331
+ # Recursively perform the extensive analysis on all subspecs
332
+ #
333
+ def perform_extensive_subspec_analysis(spec)
334
+ spec.subspecs.reject(&:test_specification?).send(fail_fast ? :all? : :each) do |subspec|
335
+ @subspec_name = subspec.name
336
+ perform_extensive_analysis(subspec)
337
+ end
338
+ end
339
+
340
+ attr_accessor :consumer
341
+ attr_accessor :subspec_name
342
+
343
+ # Performs validation of a URL
344
+ #
345
+ def validate_url(url)
346
+ resp = Pod::HTTP.validate_url(url)
347
+
348
+ if !resp
349
+ warning('url', "There was a problem validating the URL #{url}.", true)
350
+ elsif !resp.success?
351
+ warning('url', "The URL (#{url}) is not reachable.", true)
352
+ end
353
+
354
+ resp
355
+ end
356
+
357
+ # Performs validations related to the `homepage` attribute.
358
+ #
359
+ def validate_homepage(spec)
360
+ if spec.homepage
361
+ validate_url(spec.homepage)
362
+ end
363
+ end
364
+
365
+ # Performs validation related to the `screenshots` attribute.
366
+ #
367
+ def validate_screenshots(spec)
368
+ spec.screenshots.compact.each do |screenshot|
369
+ response = validate_url(screenshot)
370
+ if response && !(response.headers['content-type'] && response.headers['content-type'].first =~ /image\/.*/i)
371
+ warning('screenshot', "The screenshot #{screenshot} is not a valid image.")
372
+ end
373
+ end
374
+ end
375
+
376
+ # Performs validations related to the `social_media_url` attribute.
377
+ #
378
+ def validate_social_media_url(spec)
379
+ validate_url(spec.social_media_url) if spec.social_media_url
380
+ end
381
+
382
+ # Performs validations related to the `documentation_url` attribute.
383
+ #
384
+ def validate_documentation_url(spec)
385
+ validate_url(spec.documentation_url) if spec.documentation_url
386
+ end
387
+
388
+ def validate_dot_swift_version
389
+ if !used_swift_version.nil? && dot_swift_version.nil?
390
+ warning(:swift_version,
391
+ 'The validator for Swift projects uses ' \
392
+ 'Swift 3.0 by default, if you are using a different version of ' \
393
+ 'swift you can use a `.swift-version` file to set the version for ' \
394
+ "your Pod. For example to use Swift 2.3, run: \n" \
395
+ ' `echo "2.3" > .swift-version`')
396
+ end
397
+ end
398
+
399
+ def setup_validation_environment
400
+ validation_dir.rmtree if validation_dir.exist?
401
+ validation_dir.mkpath
402
+ @original_config = Config.instance.clone
403
+ config.installation_root = validation_dir
404
+ config.silent = !config.verbose
405
+ end
406
+
407
+ def tear_down_validation_environment
408
+ validation_dir.rmtree unless no_clean
409
+ Config.instance = @original_config
410
+ end
411
+
412
+ def deployment_target
413
+ deployment_target = spec.subspec_by_name(subspec_name).deployment_target(consumer.platform_name)
414
+ if consumer.platform_name == :ios && use_frameworks
415
+ minimum = Version.new('8.0')
416
+ deployment_target = [Version.new(deployment_target), minimum].max.to_s
417
+ end
418
+ deployment_target
419
+ end
420
+
421
+ def download_pod
422
+ podfile = podfile_from_spec(consumer.platform_name, deployment_target, use_frameworks, consumer.spec.test_specs.map(&:name))
423
+ sandbox = Sandbox.new(config.sandbox_root)
424
+ @installer = Installer.new(sandbox, podfile)
425
+ @installer.use_default_plugins = false
426
+ @installer.has_dependencies = !spec.dependencies.empty?
427
+ %i(prepare resolve_dependencies download_dependencies).each { |m| @installer.send(m) }
428
+ @file_accessor = @installer.pod_targets.flat_map(&:file_accessors).find { |fa| fa.spec.name == consumer.spec.name }
429
+ end
430
+
431
+ def create_app_project
432
+ app_project = Xcodeproj::Project.new(validation_dir + 'App.xcodeproj')
433
+ app_project.new_target(:application, 'App', consumer.platform_name, deployment_target)
434
+ app_project.root_object.attributes['TargetAttributes'] = {app_project.targets[0].uuid => {"ProvisioningStyle" => "Manual"}}
435
+ target = app_project.targets[0]
436
+ info_plist_path = validation_dir + 'Info.plist'
437
+ info_plist = Pod::Generator::InfoPlistFile.new(target, :bundle_package_type => :appl)
438
+ f = File.new(info_plist_path, 'w+')
439
+ f.write(info_plist.generate)
440
+ f.close
441
+ target.build_configurations.each do |c|
442
+ c.build_settings['INFOPLIST_FILE'] = 'Info.plist'
443
+ c.build_settings['PRODUCT_BUNDLE_IDENTIFIER'] = 'com.douyu.app'
444
+ c.build_settings['PROVISIONING_PROFILE_SPECIFIER'] = 'match Development *'
445
+ c.build_settings['CODE_SIGN_IDENTITY'] = 'iPhone Developer'
446
+ c.build_settings['DEVELOPMENT_TEAM'] = 'KW288R5J73'
447
+ end
448
+ app_project.save
449
+ app_project.recreate_user_schemes
450
+ end
451
+
452
+ def add_app_project_import
453
+ app_project = Xcodeproj::Project.open(validation_dir + 'App.xcodeproj')
454
+ pod_target = @installer.pod_targets.find { |pt| pt.pod_name == spec.root.name }
455
+
456
+ source_file = write_app_import_source_file(pod_target)
457
+ source_file_ref = app_project.new_group('App', 'App').new_file(source_file)
458
+ app_target = app_project.targets.first
459
+ app_target.add_file_references([source_file_ref])
460
+ add_swift_version(app_target)
461
+ add_xctest(app_target) if @installer.pod_targets.any? { |pt| pt.spec_consumers.any? { |c| c.frameworks.include?('XCTest') } }
462
+ app_project.save
463
+ Xcodeproj::XCScheme.share_scheme(app_project.path, 'App')
464
+ # Share the pods xcscheme only if it exists. For pre-built vendored pods there is no xcscheme generated.
465
+ Xcodeproj::XCScheme.share_scheme(@installer.pods_project.path, pod_target.label) if shares_pod_target_xcscheme?(pod_target)
466
+ end
467
+
468
+ def add_swift_version(app_target)
469
+ app_target.build_configurations.each do |configuration|
470
+ configuration.build_settings['SWIFT_VERSION'] = swift_version
471
+ end
472
+ end
473
+
474
+ def add_xctest(app_target)
475
+ app_target.build_configurations.each do |configuration|
476
+ search_paths = configuration.build_settings['FRAMEWORK_SEARCH_PATHS'] ||= '$(inherited)'
477
+ search_paths << ' "$(PLATFORM_DIR)/Developer/Library/Frameworks"'
478
+ end
479
+ end
480
+
481
+ def write_app_import_source_file(pod_target)
482
+ language = pod_target.uses_swift? ? :swift : :objc
483
+
484
+ if language == :swift
485
+ source_file = validation_dir.+('App/main.swift')
486
+ source_file.parent.mkpath
487
+ import_statement = use_frameworks && pod_target.should_build? ? "import #{pod_target.product_module_name}\n" : ''
488
+ source_file.open('w') { |f| f << import_statement }
489
+ else
490
+ source_file = validation_dir.+('App/main.m')
491
+ source_file.parent.mkpath
492
+ import_statement = if use_frameworks && pod_target.should_build?
493
+ "@import #{pod_target.product_module_name};\n"
494
+ else
495
+ header_name = "#{pod_target.product_module_name}/#{pod_target.product_module_name}.h"
496
+ if pod_target.sandbox.public_headers.root.+(header_name).file?
497
+ "#import <#{header_name}>\n"
498
+ else
499
+ ''
500
+ end
501
+ end
502
+ source_file.open('w') do |f|
503
+ f << "@import Foundation;\n"
504
+ f << "@import UIKit;\n" if consumer.platform_name == :ios || consumer.platform_name == :tvos
505
+ f << "@import Cocoa;\n" if consumer.platform_name == :osx
506
+ f << "#{import_statement}int main() {}\n"
507
+ end
508
+ end
509
+ source_file
510
+ end
511
+
512
+ # It creates a podfile in memory and builds a library containing the pod
513
+ # for all available platforms with xcodebuild.
514
+ #
515
+ def install_pod
516
+ %i(validate_targets generate_pods_project integrate_user_project
517
+ perform_post_install_actions).each { |m| @installer.send(m) }
518
+
519
+ deployment_target = spec.subspec_by_name(subspec_name).deployment_target(consumer.platform_name)
520
+ @installer.aggregate_targets.each do |target|
521
+ target.pod_targets.each do |pod_target|
522
+ next unless (native_target = pod_target.native_target)
523
+ native_target.build_configuration_list.build_configurations.each do |build_configuration|
524
+ (build_configuration.build_settings['OTHER_CFLAGS'] ||= '$(inherited)') << ' -Wincomplete-umbrella'
525
+ build_configuration.build_settings['SWIFT_VERSION'] = swift_version if pod_target.uses_swift?
526
+ end
527
+ end
528
+ if target.pod_targets.any?(&:uses_swift?) && consumer.platform_name == :ios &&
529
+ (deployment_target.nil? || Version.new(deployment_target).major < 8)
530
+ uses_xctest = target.spec_consumers.any? { |c| (c.frameworks + c.weak_frameworks).include? 'XCTest' }
531
+ error('swift', 'Swift support uses dynamic frameworks and is therefore only supported on iOS > 8.') unless uses_xctest
532
+ end
533
+ end
534
+ @installer.pods_project.save
535
+ end
536
+
537
+ def validate_vendored_dynamic_frameworks
538
+ deployment_target = spec.subspec_by_name(subspec_name).deployment_target(consumer.platform_name)
539
+
540
+ unless file_accessor.nil?
541
+ dynamic_frameworks = file_accessor.vendored_dynamic_frameworks
542
+ dynamic_libraries = file_accessor.vendored_dynamic_libraries
543
+ if (dynamic_frameworks.count > 0 || dynamic_libraries.count > 0) && consumer.platform_name == :ios &&
544
+ (deployment_target.nil? || Version.new(deployment_target).major < 8)
545
+ error('dynamic', 'Dynamic frameworks and libraries are only supported on iOS 8.0 and onwards.')
546
+ end
547
+ end
548
+ end
549
+
550
+ # Performs platform specific analysis. It requires to download the source
551
+ # at each iteration
552
+ #
553
+ # @note Xcode warnings are treaded as notes because the spec maintainer
554
+ # might not be the author of the library
555
+ #
556
+ # @return [void]
557
+ #
558
+ def build_pod
559
+ if !xcodebuild_available?
560
+ UI.warn "Skipping compilation with `xcodebuild' because it can't be found.\n".yellow
561
+ else
562
+ UI.message "\nBuilding with xcodebuild.\n".yellow do
563
+ scheme = if skip_import_validation?
564
+ @installer.pod_targets.find { |pt| pt.pod_name == spec.root.name }.label
565
+ else
566
+ 'App'
567
+ end
568
+ output = xcodebuild('build', scheme, 'Release')
569
+ UI.puts output
570
+ parsed_output = parse_xcodebuild_output(output)
571
+ translate_output_to_linter_messages(parsed_output)
572
+ end
573
+ end
574
+ end
575
+
576
+ # Builds and runs all test sources associated with the current specification being validated.
577
+ #
578
+ # @note Xcode warnings are treaded as notes because the spec maintainer
579
+ # might not be the author of the library
580
+ #
581
+ # @return [void]
582
+ #
583
+ def test_pod
584
+ if !xcodebuild_available?
585
+ UI.warn "Skipping test validation with `xcodebuild' because it can't be found.\n".yellow
586
+ else
587
+ UI.message "\nTesting with xcodebuild.\n".yellow do
588
+ pod_target = @installer.pod_targets.find { |pt| pt.pod_name == spec.root.name }
589
+ consumer.spec.test_specs.each do |test_spec|
590
+ scheme = pod_target.native_target_for_spec(test_spec)
591
+ output = xcodebuild('test', scheme, 'Debug')
592
+ UI.puts output
593
+ parsed_output = parse_xcodebuild_output(output)
594
+ translate_output_to_linter_messages(parsed_output)
595
+ end
596
+ end
597
+ end
598
+ end
599
+
600
+ def xcodebuild_available?
601
+ !Executable.which('xcodebuild').nil? && ENV['COCOAPODS_VALIDATOR_SKIP_XCODEBUILD'].nil?
602
+ end
603
+
604
+ FILE_PATTERNS = %i(source_files resources preserve_paths vendored_libraries
605
+ vendored_frameworks public_header_files preserve_paths
606
+ private_header_files resource_bundles).freeze
607
+
608
+ # It checks that every file pattern specified in a spec yields
609
+ # at least one file. It requires the pods to be already present
610
+ # in the current working directory under Pods/spec.name.
611
+ #
612
+ # @return [void]
613
+ #
614
+ def check_file_patterns
615
+ FILE_PATTERNS.each do |attr_name|
616
+ if respond_to?("_validate_#{attr_name}", true)
617
+ send("_validate_#{attr_name}")
618
+ end
619
+
620
+ if !file_accessor.spec_consumer.send(attr_name).empty? && file_accessor.send(attr_name).empty?
621
+ error('file patterns', "The `#{attr_name}` pattern did not match any file.")
622
+ end
623
+ end
624
+
625
+ _validate_header_mappings_dir
626
+ if consumer.spec.root?
627
+ _validate_license
628
+ _validate_module_map
629
+ end
630
+ end
631
+
632
+ def _validate_private_header_files
633
+ _validate_header_files(:private_header_files)
634
+ end
635
+
636
+ def _validate_public_header_files
637
+ _validate_header_files(:public_header_files)
638
+ end
639
+
640
+ def _validate_license
641
+ unless file_accessor.license || spec.license && (spec.license[:type] == 'Public Domain' || spec.license[:text])
642
+ warning('license', 'Unable to find a license file')
643
+ end
644
+ end
645
+
646
+ def _validate_module_map
647
+ if spec.module_map
648
+ unless file_accessor.module_map.exist?
649
+ error('module_map', 'Unable to find the specified module map file.')
650
+ end
651
+ unless file_accessor.module_map.extname == '.modulemap'
652
+ relative_path = file_accessor.module_map.relative_path_from file_accessor.root
653
+ error('module_map', "Unexpected file extension for modulemap file (#{relative_path}).")
654
+ end
655
+ end
656
+ end
657
+
658
+ def _validate_resource_bundles
659
+ file_accessor.resource_bundles.each do |bundle, resource_paths|
660
+ next unless resource_paths.empty?
661
+ error('file patterns', "The `resource_bundles` pattern for `#{bundle}` did not match any file.")
662
+ end
663
+ end
664
+
665
+ # Ensures that a list of header files only contains header files.
666
+ #
667
+ def _validate_header_files(attr_name)
668
+ header_files = file_accessor.send(attr_name)
669
+ non_header_files = header_files.
670
+ select { |f| !Sandbox::FileAccessor::HEADER_EXTENSIONS.include?(f.extname) }.
671
+ map { |f| f.relative_path_from(file_accessor.root) }
672
+ unless non_header_files.empty?
673
+ error(attr_name, "The pattern matches non-header files (#{non_header_files.join(', ')}).")
674
+ end
675
+ non_source_files = header_files - file_accessor.source_files
676
+ unless non_source_files.empty?
677
+ error(attr_name, 'The pattern includes header files that are not listed ' \
678
+ "in source_files (#{non_source_files.join(', ')}).")
679
+ end
680
+ end
681
+
682
+ def _validate_header_mappings_dir
683
+ return unless header_mappings_dir = file_accessor.spec_consumer.header_mappings_dir
684
+ absolute_mappings_dir = file_accessor.root + header_mappings_dir
685
+ unless absolute_mappings_dir.directory?
686
+ error('header_mappings_dir', "The header_mappings_dir (`#{header_mappings_dir}`) is not a directory.")
687
+ end
688
+ non_mapped_headers = file_accessor.headers.
689
+ reject { |h| h.to_path.start_with?(absolute_mappings_dir.to_path) }.
690
+ map { |f| f.relative_path_from(file_accessor.root) }
691
+ unless non_mapped_headers.empty?
692
+ error('header_mappings_dir', "There are header files outside of the header_mappings_dir (#{non_mapped_headers.join(', ')}).")
693
+ end
694
+ end
695
+
696
+ #-------------------------------------------------------------------------#
697
+
698
+ private
699
+
700
+ # !@group Result Helpers
701
+
702
+ def error(*args)
703
+ add_result(:error, *args)
704
+ end
705
+
706
+ def warning(*args)
707
+ add_result(:warning, *args)
708
+ end
709
+
710
+ def note(*args)
711
+ add_result(:note, *args)
712
+ end
713
+
714
+ def translate_output_to_linter_messages(parsed_output)
715
+ parsed_output.each do |message|
716
+ # Checking the error for `InputFile` is to work around an Xcode
717
+ # issue where linting would fail even though `xcodebuild` actually
718
+ # succeeds. Xcode.app also doesn't fail when this issue occurs, so
719
+ # it's safe for us to do the same.
720
+ #
721
+ # For more details see https://github.com/CocoaPods/CocoaPods/issues/2394#issuecomment-56658587
722
+ #
723
+ if message.include?("'InputFile' should have")
724
+ next
725
+ end
726
+
727
+ if message =~ /\S+:\d+:\d+: error:/
728
+ error('xcodebuild', message)
729
+ elsif message =~ /\S+:\d+:\d+: warning:/
730
+ warning('xcodebuild', message)
731
+ else
732
+ note('xcodebuild', message)
733
+ end
734
+ end
735
+ end
736
+
737
+ def shares_pod_target_xcscheme?(pod_target)
738
+ Pathname.new(@installer.pods_project.path + pod_target.label).exist?
739
+ end
740
+
741
+ def add_result(type, attribute_name, message, public_only = false)
742
+ result = results.find do |r|
743
+ r.type == type && r.attribute_name && r.message == message && r.public_only? == public_only
744
+ end
745
+ unless result
746
+ result = Result.new(type, attribute_name, message, public_only)
747
+ results << result
748
+ end
749
+ result.platforms << consumer.platform_name if consumer
750
+ result.subspecs << subspec_name if subspec_name && !result.subspecs.include?(subspec_name)
751
+ end
752
+
753
+ # Specialized Result to support subspecs aggregation
754
+ #
755
+ class Result < Specification::Linter::Results::Result
756
+ def initialize(type, attribute_name, message, public_only = false)
757
+ super(type, attribute_name, message, public_only)
758
+ @subspecs = []
759
+ end
760
+
761
+ attr_reader :subspecs
762
+ end
763
+
764
+ #-------------------------------------------------------------------------#
765
+
766
+ private
767
+
768
+ # !@group Helpers
769
+
770
+ # @return [Array<String>] an array of source URLs used to create the
771
+ # {Podfile} used in the linting process
772
+ #
773
+ attr_reader :source_urls
774
+
775
+ # @param [String] platform_name
776
+ # the name of the platform, which should be declared
777
+ # in the Podfile.
778
+ #
779
+ # @param [String] deployment_target
780
+ # the deployment target, which should be declared in
781
+ # the Podfile.
782
+ #
783
+ # @param [Bool] use_frameworks
784
+ # whether frameworks should be used for the installation
785
+ #
786
+ # @param [Array<String>] test_spec_names
787
+ # the test spec names to include in the podfile.
788
+ #
789
+ # @return [Podfile] a podfile that requires the specification on the
790
+ # current platform.
791
+ #
792
+ # @note The generated podfile takes into account whether the linter is
793
+ # in local mode.
794
+ #
795
+ def podfile_from_spec(platform_name, deployment_target, use_frameworks = true, test_spec_names = [])
796
+ name = subspec_name || spec.name
797
+ podspec = file.realpath
798
+ local = local?
799
+ urls = source_urls
800
+ Pod::Podfile.new do
801
+ install! 'cocoapods', :deterministic_uuids => false
802
+ urls.each { |u| source(u) }
803
+ target 'App' do
804
+ use_frameworks!(use_frameworks)
805
+ platform(platform_name, deployment_target)
806
+ if local
807
+ pod name, :path => podspec.dirname.to_s
808
+ else
809
+ pod name, :podspec => podspec.to_s
810
+ end
811
+ test_spec_names.each do |test_spec_name|
812
+ if local
813
+ pod test_spec_name, :path => podspec.dirname.to_s
814
+ else
815
+ pod test_spec_name, :podspec => podspec.to_s
816
+ end
817
+ end
818
+ end
819
+ end
820
+ end
821
+
822
+ # Parse the xcode build output to identify the lines which are relevant
823
+ # to the linter.
824
+ #
825
+ # @param [String] output the output generated by the xcodebuild tool.
826
+ #
827
+ # @note The indentation and the temporary path is stripped form the
828
+ # lines.
829
+ #
830
+ # @return [Array<String>] the lines that are relevant to the linter.
831
+ #
832
+ def parse_xcodebuild_output(output)
833
+ lines = output.split("\n")
834
+ selected_lines = lines.select do |l|
835
+ l.include?('error: ') && (l !~ /errors? generated\./) && (l !~ /error: \(null\)/) ||
836
+ l.include?('warning: ') && (l !~ /warnings? generated\./) && (l !~ /frameworks only run on iOS 8/) ||
837
+ l.include?('note: ') && (l !~ /expanded from macro/)
838
+ end
839
+ selected_lines.map do |l|
840
+ new = l.gsub(%r{#{validation_dir}/Pods/}, '')
841
+ new.gsub!(/^ */, ' ')
842
+ end
843
+ end
844
+
845
+ # @return [String] Executes xcodebuild in the current working directory and
846
+ # returns its output (both STDOUT and STDERR).
847
+ #
848
+ def xcodebuild(action, scheme, configuration)
849
+ require 'fourflusher'
850
+ command = %W(clean #{action} -workspace #{File.join(validation_dir, 'App.xcworkspace')} -scheme #{scheme} -configuration #{configuration})
851
+ case consumer.platform_name
852
+ when :osx, :macos
853
+ command += %w(CODE_SIGN_IDENTITY=)
854
+ when :ios
855
+ #command += %w(CODE_SIGN_IDENTITY=- -sdk iphonesimulator)
856
+ #command += Fourflusher::SimControl.new.destination(:oldest, 'iOS', deployment_target)
857
+ command += %w(-sdk iphoneos -destination generic/platform=iOS)
858
+ when :watchos
859
+ command += %w(CODE_SIGN_IDENTITY=- -sdk watchsimulator)
860
+ command += Fourflusher::SimControl.new.destination(:oldest, 'watchOS', deployment_target)
861
+ when :tvos
862
+ command += %w(CODE_SIGN_IDENTITY=- -sdk appletvsimulator)
863
+ command += Fourflusher::SimControl.new.destination(:oldest, 'tvOS', deployment_target)
864
+ end
865
+
866
+ output, status = _xcodebuild(command)
867
+
868
+ unless status.success?
869
+ message = 'Returned an unsuccessful exit code.'
870
+ message += ' You can use `--verbose` for more information.' unless config.verbose?
871
+ error('xcodebuild', message)
872
+ end
873
+
874
+ output
875
+ end
876
+
877
+ # Executes the given command in the current working directory.
878
+ #
879
+ # @return [(String, Status)] The output of the given command and its
880
+ # resulting status.
881
+ #
882
+ def _xcodebuild(command)
883
+ UI.puts 'xcodebuild ' << command.join(' ') if config.verbose
884
+ Executable.capture_command('xcodebuild', command, :capture => :merge)
885
+ end
886
+
887
+ #-------------------------------------------------------------------------#
888
+ end
889
+ end