cocoapods-dykit 0.2.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.
@@ -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