cocoapods-core 0.30.0 → 1.15.2

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.
Files changed (50) hide show
  1. checksums.yaml +5 -5
  2. data/README.md +7 -10
  3. data/lib/cocoapods-core/build_type.rb +121 -0
  4. data/lib/cocoapods-core/cdn_source.rb +501 -0
  5. data/lib/cocoapods-core/core_ui.rb +4 -3
  6. data/lib/cocoapods-core/dependency.rb +100 -73
  7. data/lib/cocoapods-core/gem_version.rb +1 -2
  8. data/lib/cocoapods-core/github.rb +32 -5
  9. data/lib/cocoapods-core/http.rb +86 -0
  10. data/lib/cocoapods-core/lockfile.rb +161 -56
  11. data/lib/cocoapods-core/metrics.rb +47 -0
  12. data/lib/cocoapods-core/platform.rb +99 -11
  13. data/lib/cocoapods-core/podfile/dsl.rb +623 -124
  14. data/lib/cocoapods-core/podfile/target_definition.rb +662 -109
  15. data/lib/cocoapods-core/podfile.rb +138 -65
  16. data/lib/cocoapods-core/requirement.rb +37 -8
  17. data/lib/cocoapods-core/source/acceptor.rb +16 -13
  18. data/lib/cocoapods-core/source/aggregate.rb +79 -103
  19. data/lib/cocoapods-core/source/health_reporter.rb +9 -18
  20. data/lib/cocoapods-core/source/manager.rb +488 -0
  21. data/lib/cocoapods-core/source/metadata.rb +79 -0
  22. data/lib/cocoapods-core/source.rb +241 -70
  23. data/lib/cocoapods-core/specification/consumer.rb +187 -47
  24. data/lib/cocoapods-core/specification/dsl/attribute.rb +49 -85
  25. data/lib/cocoapods-core/specification/dsl/attribute_support.rb +6 -8
  26. data/lib/cocoapods-core/specification/dsl/deprecations.rb +9 -126
  27. data/lib/cocoapods-core/specification/dsl/platform_proxy.rb +30 -20
  28. data/lib/cocoapods-core/specification/dsl.rb +943 -296
  29. data/lib/cocoapods-core/specification/json.rb +64 -23
  30. data/lib/cocoapods-core/specification/linter/analyzer.rb +218 -0
  31. data/lib/cocoapods-core/specification/linter/result.rb +128 -0
  32. data/lib/cocoapods-core/specification/linter.rb +310 -309
  33. data/lib/cocoapods-core/specification/root_attribute_accessors.rb +90 -39
  34. data/lib/cocoapods-core/specification/set/presenter.rb +35 -71
  35. data/lib/cocoapods-core/specification/set.rb +42 -96
  36. data/lib/cocoapods-core/specification.rb +368 -130
  37. data/lib/cocoapods-core/standard_error.rb +45 -24
  38. data/lib/cocoapods-core/trunk_source.rb +14 -0
  39. data/lib/cocoapods-core/vendor/requirement.rb +133 -53
  40. data/lib/cocoapods-core/vendor/version.rb +197 -156
  41. data/lib/cocoapods-core/vendor.rb +1 -5
  42. data/lib/cocoapods-core/version.rb +137 -42
  43. data/lib/cocoapods-core/yaml_helper.rb +334 -0
  44. data/lib/cocoapods-core.rb +10 -4
  45. metadata +100 -27
  46. data/lib/cocoapods-core/source/abstract_data_provider.rb +0 -71
  47. data/lib/cocoapods-core/source/file_system_data_provider.rb +0 -150
  48. data/lib/cocoapods-core/source/github_data_provider.rb +0 -143
  49. data/lib/cocoapods-core/specification/set/statistics.rb +0 -266
  50. data/lib/cocoapods-core/yaml_converter.rb +0 -192
@@ -0,0 +1,488 @@
1
+ require 'public_suffix'
2
+
3
+ module Pod
4
+ class Source
5
+ class Manager
6
+ # @return [Pathname] The directory that contains the source repo
7
+ # directories.
8
+ #
9
+ attr_reader :repos_dir
10
+
11
+ def initialize(repos_dir)
12
+ @repos_dir = Pathname(repos_dir).expand_path
13
+ end
14
+
15
+ # @return [Array<Pathname>] The source repo directories.
16
+ #
17
+ def source_repos
18
+ return [] unless repos_dir.exist?
19
+ repos_dir.children.select(&:directory?).sort_by { |d| d.basename.to_s.downcase }
20
+ end
21
+
22
+ # @return [Source::Aggregate] The aggregate of all the sources with the
23
+ # known Pods.
24
+ #
25
+ def aggregate
26
+ aggregate_with_repos(source_repos)
27
+ end
28
+
29
+ # @return [Source::Aggregate] The aggregate of the sources from repos.
30
+ #
31
+ # @param [Dependency] dependency
32
+ # The dependency for which to find or build the appropriate.
33
+ # aggregate. If the dependency specifies a source podspec repo
34
+ # then only that source will be used, otherwise all sources
35
+ # will be used.
36
+ #
37
+ def aggregate_for_dependency(dependency)
38
+ return aggregate if dependency.podspec_repo.nil?
39
+
40
+ source = source_with_url(dependency.podspec_repo) || source_with_name(dependency.podspec_repo)
41
+ return aggregate if source.nil?
42
+
43
+ aggregate_with_repos([source.repo])
44
+ end
45
+
46
+ # @return [Array<Source>] The list of the sources with the given names.
47
+ #
48
+ # @param [Array<#to_s>] names
49
+ # The names of the sources.
50
+ #
51
+ def sources(names)
52
+ dirs = names.map { |name| source_dir(name) }
53
+ dirs.map { |repo| source_from_path(repo) }
54
+ end
55
+
56
+ # @return [Array<Source>] The list of all the sources known to this
57
+ # installation of CocoaPods.
58
+ #
59
+ def all
60
+ aggregate.sources
61
+ end
62
+
63
+ # @return [Array<Source>] The list of all the non-indexable sources known to this
64
+ # installation of CocoaPods.
65
+ #
66
+ def all_non_indexable
67
+ aggregate.sources.reject(&:indexable?)
68
+ end
69
+
70
+ # @return [Array<Source>] The CocoaPods Master Repo source.
71
+ #
72
+ def master
73
+ sources([Pod::TrunkSource::TRUNK_REPO_NAME]).select { |s| s.repo.directory? }
74
+ end
75
+
76
+ # @!group Master repo
77
+
78
+ # @return [Pathname] The path of the master repo.
79
+ #
80
+ def master_repo_dir
81
+ source_dir(Pod::TrunkSource::TRUNK_REPO_NAME)
82
+ end
83
+
84
+ # @return [Boolean] Checks if the master repo is usable.
85
+ #
86
+ # @note Note this is used to automatically setup the master repo if
87
+ # needed.
88
+ #
89
+ def master_repo_functional?
90
+ return false unless master_repo = master.first
91
+ master_repo.metadata.compatible?(CORE_VERSION)
92
+ end
93
+
94
+ # Search the appropriate sources to match the set for the given dependency.
95
+ #
96
+ # @return [Set, nil] a set for a given dependency including all the
97
+ # {Source} that contain the Pod. If no sources containing the
98
+ # Pod where found it returns nil.
99
+ #
100
+ # @raise If no source can be found that includes the dependency.
101
+ #
102
+ def search(dependency)
103
+ aggregate_for_dependency(dependency).search(dependency)
104
+ end
105
+
106
+ # Search all the sources with the given search term.
107
+ #
108
+ # @param [String] query
109
+ # The search term.
110
+ #
111
+ # @param [Boolean] full_text_search
112
+ # Whether the search should be limited to the name of the Pod or
113
+ # should include also the author, the summary, and the
114
+ # description.
115
+ #
116
+ # @raise If no source including the set can be found.
117
+ #
118
+ # @return [Array<Set>] The sets that contain the search term.
119
+ #
120
+ def search_by_name(query, full_text_search = false)
121
+ query_word_regexps = query.split.map { |word| /#{word}/i }
122
+ if full_text_search
123
+ query_word_results_hash = {}
124
+ updated_search_index.each_value do |word_spec_hash|
125
+ word_spec_hash.each_pair do |word, spec_names|
126
+ query_word_regexps.each do |query_word_regexp|
127
+ set = (query_word_results_hash[query_word_regexp] ||= Set.new)
128
+ set.merge(spec_names) if word =~ query_word_regexp
129
+ end
130
+ end
131
+ end
132
+ found_set_names = query_word_results_hash.values.reduce(:&)
133
+ found_set_names ||= []
134
+
135
+ sets_from_non_indexable = all_non_indexable.map { |s| s.search_by_name(query, true) }.flatten
136
+
137
+ found_set_names += sets_from_non_indexable.map(&:name).flatten.uniq
138
+
139
+ sets = found_set_names.map do |name|
140
+ aggregate.representative_set(name)
141
+ end
142
+
143
+ # Remove nil values because representative_set return nil if no pod is found in any of the sources.
144
+ sets.compact!
145
+ else
146
+ sets = aggregate.search_by_name(query, false)
147
+ end
148
+ if sets.empty?
149
+ extra = ', author, summary, or description' if full_text_search
150
+ raise Informative, "Unable to find a pod with name#{extra} " \
151
+ "matching `#{query}`"
152
+ end
153
+ sorted_sets(sets, query_word_regexps)
154
+ end
155
+
156
+ # Returns given set array by sorting it in-place.
157
+ #
158
+ # @param [Array<Set>] sets
159
+ # Array of sets to be sorted.
160
+ #
161
+ # @param [Array<Regexp>] query_word_regexps
162
+ # Array of regexp objects for user query.
163
+ #
164
+ # @return [Array<Set>] Given sets parameter itself after sorting it in-place.
165
+ #
166
+ def sorted_sets(sets, query_word_regexps)
167
+ sets.sort_by! do |set|
168
+ pre_match_length = nil
169
+ found_query_index = nil
170
+ found_query_count = 0
171
+ query_word_regexps.each_with_index do |q, idx|
172
+ if (m = set.name.match(/#{q}/i))
173
+ pre_match_length ||= m.pre_match.length
174
+ found_query_index ||= idx
175
+ found_query_count += 1
176
+ end
177
+ end
178
+ pre_match_length ||= 1000
179
+ found_query_index ||= 1000
180
+ [-found_query_count, pre_match_length, found_query_index, set.name.downcase]
181
+ end
182
+ sets
183
+ end
184
+
185
+ # Returns the search data. If a saved search data exists, retrieves it from file and returns it.
186
+ # Else, creates the search data from scratch, saves it to file system, and returns it.
187
+ # Search data is grouped by source repos. For each source, it contains a hash where keys are words
188
+ # and values are the pod names containing corresponding word.
189
+ #
190
+ # For each source, list of unique words are generated from the following spec information.
191
+ # - version
192
+ # - summary
193
+ # - description
194
+ # - authors
195
+ #
196
+ # @return [Hash{String => Hash{String => Array<String>}}] The up to date search data.
197
+ #
198
+ def updated_search_index
199
+ index = stored_search_index || {}
200
+ indexable_sources.each do |source|
201
+ source_name = source.name
202
+ unless index[source_name]
203
+ CoreUI.print "Creating search index for spec repo '#{source_name}'.."
204
+ index[source_name] = aggregate.generate_search_index_for_source(source)
205
+ CoreUI.puts ' Done!'
206
+ end
207
+ end
208
+ save_search_index(index)
209
+ index
210
+ end
211
+
212
+ # Updates the stored search index if there are changes in spec repos while updating them.
213
+ # Update is performed incrementally. Only the changed pods' search data is re-generated and updated.
214
+ # @param [Hash{Source => Array<String>}] changed_spec_paths
215
+ # A hash containing changed specification paths for each source.
216
+ #
217
+ def update_search_index_if_needed(changed_spec_paths)
218
+ search_index = stored_search_index
219
+ return unless search_index
220
+ changed_spec_paths.each_pair do |source, spec_paths|
221
+ next unless source.indexable?
222
+ index_for_source = search_index[source.name]
223
+ next unless index_for_source && !spec_paths.empty?
224
+ updated_pods = source.pods_for_specification_paths(spec_paths)
225
+
226
+ new_index = aggregate.generate_search_index_for_changes_in_source(source, spec_paths)
227
+ # First traverse search_index and update existing words
228
+ # Remove traversed words from new_index after adding to search_index,
229
+ # so that only non existing words will remain in new_index after enumeration completes.
230
+ index_for_source.each_pair do |word, _|
231
+ if new_index[word]
232
+ index_for_source[word] |= new_index[word]
233
+ new_index.delete(word)
234
+ else
235
+ index_for_source[word] -= updated_pods
236
+ end
237
+ end
238
+
239
+ # Now add non existing words remained in new_index to search_index
240
+ index_for_source.merge!(new_index)
241
+ end
242
+ save_search_index(search_index)
243
+ end
244
+
245
+ # Updates search index for changed pods in background
246
+ # @param [Hash{Source => Array<String>}] changed_spec_paths
247
+ # A hash containing changed specification paths for each source.
248
+ #
249
+ def update_search_index_if_needed_in_background(changed_spec_paths)
250
+ if Gem.win_platform?
251
+ update_search_index_if_needed(changed_spec_paths)
252
+ return
253
+ end
254
+ Process.fork do
255
+ Process.daemon
256
+ update_search_index_if_needed(changed_spec_paths)
257
+ exit
258
+ end
259
+ end
260
+
261
+ # Returns the search data stored in the file system.
262
+ # If existing data in the file system is not valid, returns nil.
263
+ #
264
+ def stored_search_index
265
+ @updated_search_index ||= begin
266
+ if search_index_path.exist?
267
+ require 'json'
268
+ begin
269
+ index = JSON.parse(search_index_path.read)
270
+ unless index # JSON.parse("null") => nil
271
+ search_index_path.delete
272
+ return nil
273
+ end
274
+
275
+ index if index.is_a?(Hash) # TODO: should we also check if hash has correct hierarchy?
276
+ rescue JSON::ParserError
277
+ search_index_path.delete
278
+ nil
279
+ end
280
+ end
281
+ end
282
+ end
283
+
284
+ # Stores given search data in the file system.
285
+ # @param [Hash] index
286
+ # Index to be saved in file system
287
+ #
288
+ def save_search_index(index)
289
+ require 'json'
290
+ @updated_search_index = index
291
+ search_index_path.open('w') do |io|
292
+ io.write(@updated_search_index.to_json)
293
+ end
294
+ end
295
+
296
+ # Allows to clear the search index.
297
+ #
298
+ attr_writer :updated_search_index
299
+
300
+ # @return [Pathname] The path where the search index should be stored.
301
+ #
302
+ attr_accessor :search_index_path
303
+
304
+ private
305
+
306
+ # @return [Source] The Source at a given path.
307
+ #
308
+ # @param [Pathname] path
309
+ # The local file path to one podspec repo.
310
+ #
311
+ def source_from_path(path)
312
+ @sources_by_path ||= Hash.new do |hash, key|
313
+ hash[key] = case
314
+ when key.basename.to_s == Pod::TrunkSource::TRUNK_REPO_NAME
315
+ TrunkSource.new(key)
316
+ when (key + '.url').exist?
317
+ CDNSource.new(key)
318
+ else
319
+ Source.new(key)
320
+ end
321
+ end
322
+ @sources_by_path[path]
323
+ end
324
+
325
+ # @return [Source::Aggregate] The aggregate of the sources from repos.
326
+ #
327
+ # @param [Array<Pathname>] repos
328
+ # The local file paths to one or more podspec repo caches.
329
+ #
330
+ def aggregate_with_repos(repos)
331
+ sources = repos.map { |path| source_from_path(path) }
332
+ @aggregates_by_repos ||= {}
333
+ @aggregates_by_repos[repos] ||= Source::Aggregate.new(sources)
334
+ end
335
+
336
+ # @return [Source] The source with the given name.
337
+ #
338
+ # @param [String] name
339
+ # The name of the source.
340
+ #
341
+ def source_with_name(name)
342
+ source = sources([name]).first
343
+ return nil unless source.repo.exist?
344
+ source
345
+ end
346
+
347
+ # @return [Source] The updateable source with the given name. If no updateable source
348
+ # with given name is found it raises.
349
+ #
350
+ # @param [String] name
351
+ # The name of the source.
352
+ #
353
+ def updateable_source_named(name)
354
+ specified_source = source_with_name(name)
355
+ unless specified_source
356
+ raise Informative, "Unable to find the `#{name}` repo."
357
+ end
358
+ unless specified_source.updateable?
359
+ raise Informative, "The `#{name}` repo is not a updateable repo."
360
+ end
361
+ specified_source
362
+ end
363
+
364
+ # @return [Source] The list of the updateable sources.
365
+ #
366
+ def updateable_sources
367
+ all.select(&:updateable?)
368
+ end
369
+
370
+ # @return [Source] The list of the indexable sources.
371
+ #
372
+ def indexable_sources
373
+ all.select(&:indexable?)
374
+ end
375
+
376
+ # @return [Pathname] The path of the source with the given name.
377
+ #
378
+ # @param [String] name
379
+ # The name of the source.
380
+ #
381
+ def source_dir(name)
382
+ repos_dir + name
383
+ end
384
+
385
+ # @return [Source] The source whose {Source#url} is equal to `url`.
386
+ #
387
+ # @param [String] url
388
+ # The URL of the source.
389
+ #
390
+ def source_with_url(url)
391
+ url = canonic_url(url)
392
+ url = 'https://github.com/cocoapods/specs' if url =~ %r{github.com[:/]+cocoapods/specs}
393
+ all.find do |source|
394
+ source.url && canonic_url(source.url) == url
395
+ end
396
+ end
397
+
398
+ def canonic_url(url)
399
+ url.downcase.gsub(/\.git$/, '').gsub(%r{\/$}, '')
400
+ end
401
+
402
+ # Returns a suitable repository name for `url`.
403
+ #
404
+ # @example A GitHub.com URL
405
+ #
406
+ # name_for_url('https://github.com/Artsy/Specs.git')
407
+ # # "artsy"
408
+ # name_for_url('https://github.com/Artsy/Specs.git')
409
+ # # "artsy-1"
410
+ #
411
+ # @example A non-Github.com URL
412
+ #
413
+ # name_for_url('https://sourceforge.org/Artsy/Specs.git')
414
+ # # sourceforge-artsy
415
+ #
416
+ # @example A file URL
417
+ #
418
+ # name_for_url('file:///Artsy/Specs.git')
419
+ # # artsy
420
+ #
421
+ # @param [#to_s] url
422
+ # The URL of the source.
423
+ #
424
+ # @return [String] A suitable repository name for `url`.
425
+ #
426
+ def name_for_url(url)
427
+ base_from_host_and_path = lambda do |host, path|
428
+ if host && !host.empty?
429
+ domain = PublicSuffix.parse(host) rescue nil
430
+ base = [domain&.sld || host]
431
+ base = [] if base == %w(github)
432
+ else
433
+ base = []
434
+ end
435
+
436
+ path = path.gsub(/.git$/, '').gsub(%r{^/}, '').split('/')
437
+ path.pop if path.last == 'specs'
438
+
439
+ (base + path).join('-')
440
+ end
441
+
442
+ valid_url = lambda do |url|
443
+ url =~ URI.regexp && (URI(url) rescue false)
444
+ end
445
+
446
+ valid_scp_url = lambda do |url|
447
+ valid_url['scp://' + url]
448
+ end
449
+
450
+ url = url.to_s.downcase
451
+
452
+ case url
453
+ when %r{https://#{Regexp.quote(trunk_repo_hostname)}}i
454
+ # Main CDN repo
455
+ base = Pod::TrunkSource::TRUNK_REPO_NAME
456
+ when valid_url
457
+ # HTTPS URL or something similar
458
+ url = valid_url[url]
459
+ base = base_from_host_and_path[url.host, url.path]
460
+ when valid_scp_url
461
+ # SCP-style URLs for private git repos
462
+ url = valid_scp_url[url]
463
+ base = base_from_host_and_path[url.host, url.path]
464
+ when %r{(?:git|ssh|https?|[a-z0-9_-]+@([-\w.]+)):(\/\/)?(.*?)(\.git)?(\/?|\#[-\d\w._]+?)$}i
465
+ # Additional SCP-style URLs for private git repos
466
+ host, _, path = Regexp.last_match.captures
467
+ base = base_from_host_and_path[host, path]
468
+ else
469
+ # This is nearly impossible, with all the previous cases
470
+ raise Informative, "Couldn't determine repo name for URL: #{url}"
471
+ end
472
+
473
+ name = base
474
+ (1..).each do |i|
475
+ break unless source_dir(name).exist?
476
+ name = "#{base}-#{i}"
477
+ end
478
+ name
479
+ end
480
+
481
+ # Returns hostname for for `trunk` URL.
482
+ #
483
+ def trunk_repo_hostname
484
+ URI.parse(TrunkSource::TRUNK_REPO_URL).host.downcase.freeze
485
+ end
486
+ end
487
+ end
488
+ end
@@ -0,0 +1,79 @@
1
+ autoload :Digest, 'digest/md5'
2
+ require 'active_support/hash_with_indifferent_access'
3
+ require 'active_support/core_ext/hash/indifferent_access'
4
+
5
+ module Pod
6
+ class Source
7
+ class Metadata
8
+ attr_reader :minimum_cocoapods_version
9
+ attr_reader :maximum_cocoapods_version
10
+ attr_reader :latest_cocoapods_version
11
+ attr_reader :prefix_lengths
12
+ attr_reader :last_compatible_versions
13
+
14
+ def initialize(hash = {})
15
+ hash = hash.with_indifferent_access
16
+ @minimum_cocoapods_version = hash['min']
17
+ @minimum_cocoapods_version &&= Pod::Version.new(@minimum_cocoapods_version)
18
+ @maximum_cocoapods_version = hash['max']
19
+ @maximum_cocoapods_version &&= Pod::Version.new(@maximum_cocoapods_version)
20
+ @latest_cocoapods_version = hash['last']
21
+ @latest_cocoapods_version &&= Pod::Version.new(@latest_cocoapods_version)
22
+ @prefix_lengths = Array(hash['prefix_lengths']).map!(&:to_i)
23
+ @last_compatible_versions = Array(hash['last_compatible_versions']).map(&Pod::Version.method(:new)).sort
24
+ end
25
+
26
+ def self.from_file(file)
27
+ hash = file.file? ? YAMLHelper.load_file(file) : {}
28
+ new(hash)
29
+ end
30
+
31
+ def to_hash
32
+ hash = ActiveSupport::HashWithIndifferentAccess.new
33
+ hash['min'] = @minimum_cocoapods_version.to_s if @minimum_cocoapods_version
34
+ hash['max'] = @maximum_cocoapods_version.to_s if @maximum_cocoapods_version
35
+ hash['last'] = @latest_cocoapods_version.to_s if @latest_cocoapods_version
36
+ hash['prefix_lengths'] = @prefix_lengths unless @prefix_lengths.empty?
37
+ hash['last_compatible_versions'] = @last_compatible_versions.map(&:to_s) unless @last_compatible_versions.empty?
38
+ hash
39
+ end
40
+
41
+ def path_fragment(pod_name, version = nil)
42
+ prefixes = if prefix_lengths.empty?
43
+ []
44
+ else
45
+ hashed = Digest::MD5.hexdigest(pod_name)
46
+ prefix_lengths.map do |length|
47
+ hashed.slice!(0, length)
48
+ end
49
+ end
50
+ prefixes.concat([pod_name, version]).compact
51
+ end
52
+
53
+ def last_compatible_version(target_version)
54
+ return unless minimum_cocoapods_version
55
+ return if minimum_cocoapods_version <= target_version
56
+ @last_compatible_versions.reverse.bsearch { |v| v <= target_version }.tap do |version|
57
+ raise Informative, 'Unable to find compatible version' unless version
58
+ end
59
+ end
60
+
61
+ # Returns whether a source is compatible with the current version of
62
+ # CocoaPods.
63
+ #
64
+ # @param [Pathname] dir
65
+ # The directory where the source is stored.
66
+ #
67
+ # @return [Boolean] whether the source is compatible.
68
+ #
69
+ def compatible?(version)
70
+ bin_version = Gem::Version.new(version)
71
+ supports_min = !minimum_cocoapods_version ||
72
+ (bin_version >= Gem::Version.new(minimum_cocoapods_version))
73
+ supports_max = !maximum_cocoapods_version ||
74
+ (bin_version <= Gem::Version.new(maximum_cocoapods_version))
75
+ supports_min && supports_max
76
+ end
77
+ end
78
+ end
79
+ end