cocoapods-core 0.30.0 → 1.15.2

Sign up to get free protection for your applications and to get access to all the features.
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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: c825fb07fe0a09f2aff4865eee906d366068abe9
4
- data.tar.gz: f15b33a4eb444034bc3165faee0080cec4cc7bfc
2
+ SHA256:
3
+ metadata.gz: 28b58aee1d429faed6b698b36263cc7e86e1ebe4540b2c7e03d1883e28e74f59
4
+ data.tar.gz: 81da93773c4f3cff54ae532ef20b3fd9b3190fbca1fc4d7fa89e57da4c840ab3
5
5
  SHA512:
6
- metadata.gz: ed161124fa1f352f2914afeb6eb5ce6fb290eb3fde1ecdb2aa0da7de163b56ac329447bdfc721813d10f458374fa7a44dfd43b8f7920b6553f5e46b8e40f8b50
7
- data.tar.gz: c23cd0cbfa5a2ece6550c30755222d9d9981b18f0ed89a8957b81333771c598c3a271031dfa682074e695d8b99ca2bfb9541a99ecdd1bd9e1bfb978450ccb1af
6
+ metadata.gz: 6ab7a1a1c329774e398f25080dd03f3fda96bc96be0c5582088010b317e2c64876f2d3b01e1ae4d0ea48f32839443ff907f90b9cd27aa681cf9a9e1c94eee43d
7
+ data.tar.gz: 7a82bc2bda4f3f2cf4f54bf70300bc5c633073696d5882cd282d302ec5b00d8a33072edd7cbaa60812b52525b51cce47ca10597dcefd1b82fb001c55e7852aa4
data/README.md CHANGED
@@ -1,17 +1,17 @@
1
1
  # CocoaPods Core
2
2
 
3
- [![Build Status](https://travis-ci.org/CocoaPods/Core.svg?branch=master)](https://travis-ci.org/CocoaPods/Core)
4
- [![Coverage Status](https://img.shields.io/coveralls/CocoaPods/Core.svg)](https://coveralls.io/r/CocoaPods/Core)
5
- [![Code Climate](https://img.shields.io/codeclimate/github/CocoaPods/Core.svg)](https://codeclimate.com/github/CocoaPods/Core)
3
+ [![Build Status](https://github.com/CocoaPods/Core/workflows/Specs/badge.svg)](https://github.com/CocoaPods/Core/actions/workflows/Specs.yml)
4
+ [![Test Coverage](https://api.codeclimate.com/v1/badges/91a2d70b9ed977815c66/test_coverage)](https://codeclimate.com/github/CocoaPods/Core/test_coverage)
5
+ [![Maintainability](https://api.codeclimate.com/v1/badges/91a2d70b9ed977815c66/maintainability)](https://codeclimate.com/github/CocoaPods/Core/maintainability)
6
6
 
7
7
  The CocoaPods-Core gem provides support to work with the models of CocoaPods.
8
- It is intended to be used in place of the CocoaPods when the installation
8
+ It is intended to be used in place of the CocoaPods gem when the installation
9
9
  of the dependencies is not needed. Therefore, it is suitable for web services.
10
10
 
11
11
  Provides support for working with the following models:
12
12
 
13
- - `Pod::Specification` - [podspec files](http://cocoapods.github.com/specification.html).
14
- - `Pod::Podfile` - [podfile specifications](http://cocoapods.github.com/podfile.html).
13
+ - `Pod::Specification` - [Podspec Syntax Reference](https://guides.cocoapods.org/syntax/podspec.html).
14
+ - `Pod::Podfile` - [Podfile Syntax Reference](https://guides.cocoapods.org/syntax/podfile.html).
15
15
  - `Pod::Source` - collections of podspec files like the [CocoaPods Spec repo](https://github.com/CocoaPods/Specs).
16
16
 
17
17
  The gem also provides support for ancillary features like
@@ -25,10 +25,7 @@ files.
25
25
  $ [sudo] gem install cocoapods-core
26
26
  ```
27
27
 
28
- The `cocoapods-core` gem requires either:
29
-
30
- - Ruby 1.8.7 (shipped with OS X 10.8).
31
- - Ruby 1.9.3 (recommended).
28
+ The `cocoapods-core` gem requires Ruby 2.6.0 or later.
32
29
 
33
30
  ## Collaborate
34
31
 
@@ -0,0 +1,121 @@
1
+ module Pod
2
+ class BuildType
3
+ # @return [Array<Symbol>] known packaging options.
4
+ #
5
+ KNOWN_PACKAGING_OPTIONS = %i(library framework).freeze
6
+
7
+ # @return [Array<Symbol>] known linking options.
8
+ #
9
+ KNOWN_LINKAGE_OPTIONS = %i(static dynamic).freeze
10
+
11
+ # @return [Symbol] the packaging for this build type, one of #KNOWN_PACKAGING_OPTIONS
12
+ #
13
+ attr_reader :packaging
14
+
15
+ # @return [Symbol] the linkage for this build type, one of #KNOWN_LINKAGE_OPTIONS
16
+ #
17
+ attr_reader :linkage
18
+
19
+ attr_reader :hash
20
+
21
+ def initialize(linkage: :static, packaging: :library)
22
+ unless KNOWN_LINKAGE_OPTIONS.include?(linkage)
23
+ raise ArgumentError, "Invalid linkage option #{linkage.inspect}, valid options are #{KNOWN_LINKAGE_OPTIONS.inspect}"
24
+ end
25
+ unless KNOWN_PACKAGING_OPTIONS.include?(packaging)
26
+ raise ArgumentError, "Invalid packaging option #{packaging.inspect}, valid options are #{KNOWN_PACKAGING_OPTIONS.inspect}"
27
+ end
28
+ @packaging = packaging
29
+ @linkage = linkage
30
+ @hash = packaging.hash ^ linkage.hash
31
+ end
32
+
33
+ # @return [BuildType] the build type for a dynamic library
34
+ def self.dynamic_library
35
+ new(:linkage => :dynamic, :packaging => :library)
36
+ end
37
+
38
+ # @return [BuildType] the build type for a static library
39
+ #
40
+ def self.static_library
41
+ new(:linkage => :static, :packaging => :library)
42
+ end
43
+
44
+ # @return [BuildType] the build type for a dynamic framework
45
+ #
46
+ def self.dynamic_framework
47
+ new(:linkage => :dynamic, :packaging => :framework)
48
+ end
49
+
50
+ # @return [BuildType] the build type for a static framework
51
+ #
52
+ def self.static_framework
53
+ new(:linkage => :static, :packaging => :framework)
54
+ end
55
+
56
+ # @return [Boolean] whether the target is built dynamically
57
+ #
58
+ def dynamic?
59
+ linkage == :dynamic
60
+ end
61
+
62
+ # @return [Boolean] whether the target is built statically
63
+ #
64
+ def static?
65
+ linkage == :static
66
+ end
67
+
68
+ # @return [Boolean] whether the target is built as a framework
69
+ #
70
+ def framework?
71
+ packaging == :framework
72
+ end
73
+
74
+ # @return [Boolean] whether the target is built as a library
75
+ #
76
+ def library?
77
+ packaging == :library
78
+ end
79
+
80
+ # @return [Boolean] whether the target is built as a dynamic framework
81
+ #
82
+ def dynamic_framework?
83
+ dynamic? && framework?
84
+ end
85
+
86
+ # @return [Boolean] whether the target is built as a dynamic library
87
+ #
88
+ def dynamic_library?
89
+ dynamic? && library?
90
+ end
91
+
92
+ # @return [Boolean] whether the target is built as a static framework
93
+ #
94
+ def static_framework?
95
+ static? && framework?
96
+ end
97
+
98
+ # @return [Boolean] whether the target is built as a static library
99
+ #
100
+ def static_library?
101
+ static? && library?
102
+ end
103
+
104
+ def to_s
105
+ "#{linkage} #{packaging}"
106
+ end
107
+
108
+ def to_hash
109
+ { :linkage => linkage, :packaging => packaging }
110
+ end
111
+
112
+ def inspect
113
+ "#<#{self.class} linkage=#{linkage} packaging=#{packaging}>"
114
+ end
115
+
116
+ def ==(other)
117
+ linkage == other.linkage &&
118
+ packaging == other.packaging
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,501 @@
1
+ require 'cocoapods-core/source'
2
+ require 'rest'
3
+ require 'concurrent'
4
+ require 'netrc'
5
+ require 'addressable'
6
+
7
+ module Pod
8
+ # Subclass of Pod::Source to provide support for CDN-based Specs repositories
9
+ #
10
+ class CDNSource < Source
11
+ include Concurrent
12
+
13
+ MAX_NUMBER_OF_RETRIES = (ENV['COCOAPODS_CDN_MAX_NUMBER_OF_RETRIES'] || 5).to_i
14
+ # Single thread executor for all network activity.
15
+ HYDRA_EXECUTOR = Concurrent::SingleThreadExecutor.new
16
+
17
+ private_constant :HYDRA_EXECUTOR
18
+
19
+ # @param [String] repo The name of the repository
20
+ #
21
+ def initialize(repo)
22
+ @check_existing_files_for_update = false
23
+ # Optimization: we initialize startup_time when the source is first initialized
24
+ # and then test file modification dates against it. Any file that was touched
25
+ # after the source was initialized, is considered fresh enough.
26
+ @startup_time = Time.new
27
+
28
+ @version_arrays_by_fragment_by_name = {}
29
+
30
+ super(repo)
31
+ end
32
+
33
+ # @return [String] The URL of the source.
34
+ #
35
+ def url
36
+ @url ||= File.read(repo.join('.url')).chomp.chomp('/') + '/'
37
+ end
38
+
39
+ # @return [String] The type of the source.
40
+ #
41
+ def type
42
+ 'CDN'
43
+ end
44
+
45
+ def refresh_metadata
46
+ if metadata.nil?
47
+ unless repo.exist?
48
+ debug "CDN: Repo #{name} does not exist!"
49
+ return
50
+ end
51
+
52
+ specs_dir.mkpath
53
+ download_file('CocoaPods-version.yml')
54
+ end
55
+
56
+ super
57
+ end
58
+
59
+ def preheat_existing_files
60
+ files_to_update = files_definitely_to_update + deprecated_local_podspecs - ['deprecated_podspecs.txt']
61
+ debug "CDN: #{name} Going to update #{files_to_update.count} files"
62
+
63
+ concurrent_requests_catching_errors do
64
+ # Queue all tasks first
65
+ loaders = files_to_update.map do |file|
66
+ download_file_async(file)
67
+ end
68
+ # Block and wait for all to complete running on Hydra
69
+ Promises.zip_futures_on(HYDRA_EXECUTOR, *loaders).wait!
70
+ end
71
+ end
72
+
73
+ def files_definitely_to_update
74
+ Pathname.glob(repo.join('**/*.{txt,yml}')).map { |f| f.relative_path_from(repo).to_s }
75
+ end
76
+
77
+ def deprecated_local_podspecs
78
+ download_file('deprecated_podspecs.txt')
79
+ local_file('deprecated_podspecs.txt', &:to_a).
80
+ map { |f| Pathname.new(f.chomp) }.
81
+ select { |f| repo.join(f).exist? }
82
+ end
83
+
84
+ # @return [Pathname] The directory where the specs are stored.
85
+ #
86
+ def specs_dir
87
+ @specs_dir ||= repo + 'Specs'
88
+ end
89
+
90
+ # @!group Querying the source
91
+ #-------------------------------------------------------------------------#
92
+
93
+ # @return [Array<String>] the list of the name of all the Pods.
94
+ #
95
+ def pods
96
+ download_file('all_pods.txt')
97
+ local_file('all_pods.txt', &:to_a).map(&:chomp)
98
+ end
99
+
100
+ # @return [Array<Version>] all the available versions for the Pod, sorted
101
+ # from highest to lowest.
102
+ #
103
+ # @param [String] name
104
+ # the name of the Pod.
105
+ #
106
+ def versions(name)
107
+ return nil unless specs_dir
108
+ raise ArgumentError, 'No name' unless name
109
+
110
+ fragment = pod_shard_fragment(name)
111
+
112
+ ensure_versions_file_loaded(fragment)
113
+
114
+ return @versions_by_name[name] unless @versions_by_name[name].nil?
115
+
116
+ pod_path_actual = pod_path(name)
117
+ pod_path_relative = relative_pod_path(name)
118
+
119
+ return nil if @version_arrays_by_fragment_by_name[fragment][name].nil?
120
+
121
+ concurrent_requests_catching_errors do
122
+ loaders = []
123
+
124
+ @versions_by_name[name] ||= @version_arrays_by_fragment_by_name[fragment][name].map do |version|
125
+ # Optimization: ensure all the podspec files at least exist. The correct one will get refreshed
126
+ # in #specification_path regardless.
127
+ podspec_version_path_relative = Pathname.new(version).join("#{name}.podspec.json")
128
+
129
+ unless pod_path_actual.join(podspec_version_path_relative).exist?
130
+ # Queue all podspec download tasks first
131
+ loaders << download_file_async(pod_path_relative.join(podspec_version_path_relative).to_s)
132
+ end
133
+
134
+ begin
135
+ Version.new(version) if version[0, 1] != '.'
136
+ rescue ArgumentError
137
+ raise Informative, 'An unexpected version directory ' \
138
+ "`#{version}` was encountered for the " \
139
+ "`#{pod_path_actual}` Pod in the `#{name}` repository."
140
+ end
141
+ end.compact.sort.reverse
142
+
143
+ # Block and wait for all to complete running on Hydra
144
+ Promises.zip_futures_on(HYDRA_EXECUTOR, *loaders).wait!
145
+ end
146
+
147
+ @versions_by_name[name]
148
+ end
149
+
150
+ # Returns the path of the specification with the given name and version.
151
+ #
152
+ # @param [String] name
153
+ # the name of the Pod.
154
+ #
155
+ # @param [Version,String] version
156
+ # the version for the specification.
157
+ #
158
+ # @return [Pathname] The path of the specification.
159
+ #
160
+ def specification_path(name, version)
161
+ raise ArgumentError, 'No name' unless name
162
+ raise ArgumentError, 'No version' unless version
163
+ unless versions(name).include?(Version.new(version))
164
+ raise StandardError, "Unable to find the specification #{name} " \
165
+ "(#{version}) in the #{self.name} source."
166
+ end
167
+
168
+ podspec_version_path_relative = Pathname.new(version.to_s).join("#{name}.podspec.json")
169
+ relative_podspec = relative_pod_path(name).join(podspec_version_path_relative).to_s
170
+ download_file(relative_podspec)
171
+ pod_path(name).join(podspec_version_path_relative)
172
+ end
173
+
174
+ # @return [Array<Specification>] all the specifications contained by the
175
+ # source.
176
+ #
177
+ def all_specs
178
+ raise Informative, "Can't retrieve all the specs for a CDN-backed source, it will take forever"
179
+ end
180
+
181
+ # @return [Array<Sets>] the sets of all the Pods.
182
+ #
183
+ def pod_sets
184
+ raise Informative, "Can't retrieve all the pod sets for a CDN-backed source, it will take forever"
185
+ end
186
+
187
+ # @!group Searching the source
188
+ #-------------------------------------------------------------------------#
189
+
190
+ # @return [Set] a set for a given dependency. The set is identified by the
191
+ # name of the dependency and takes into account subspecs.
192
+ #
193
+ # @note This method is optimized for fast lookups by name, i.e. it does
194
+ # *not* require iterating through {#pod_sets}
195
+ #
196
+ # @todo Rename to #load_set
197
+ #
198
+ def search(query)
199
+ unless specs_dir
200
+ raise Informative, "Unable to find a source named: `#{name}`"
201
+ end
202
+ if query.is_a?(Dependency)
203
+ query = query.root_name
204
+ end
205
+
206
+ fragment = pod_shard_fragment(query)
207
+
208
+ ensure_versions_file_loaded(fragment)
209
+
210
+ version_arrays_by_name = @version_arrays_by_fragment_by_name[fragment] || {}
211
+
212
+ found = version_arrays_by_name[query].nil? ? nil : query
213
+
214
+ if found
215
+ set = set(query)
216
+ set if set.specification_name == query
217
+ end
218
+ end
219
+
220
+ # @return [Array<Set>] The list of the sets that contain the search term.
221
+ #
222
+ # @param [String] query
223
+ # the search term. Can be a regular expression.
224
+ #
225
+ # @param [Boolean] full_text_search
226
+ # performed using Algolia
227
+ #
228
+ # @note full text search requires to load the specification for each pod,
229
+ # and therefore not supported.
230
+ #
231
+ def search_by_name(query, full_text_search = false)
232
+ if full_text_search
233
+ require 'algoliasearch'
234
+ begin
235
+ algolia_result = algolia_search_index.search(query, :attributesToRetrieve => 'name')
236
+ names = algolia_result['hits'].map { |r| r['name'] }
237
+ names.map { |n| set(n) }.reject { |s| s.versions.compact.empty? }
238
+ rescue Algolia::AlgoliaError => e
239
+ raise Informative, "CDN: #{name} - Cannot perform full-text search because Algolia returned an error: #{e}"
240
+ end
241
+ else
242
+ super(query)
243
+ end
244
+ end
245
+
246
+ # Check update dates for all existing files.
247
+ # Does not download non-existing specs, since CDN-backed repo is updated live.
248
+ #
249
+ # @param [Boolean] show_output
250
+ #
251
+ # @return [Array<String>] Always returns empty array, as it cannot know
252
+ # everything that actually changed.
253
+ #
254
+ def update(_show_output)
255
+ @check_existing_files_for_update = true
256
+ begin
257
+ preheat_existing_files
258
+ ensure
259
+ @check_existing_files_for_update = false
260
+ end
261
+ []
262
+ end
263
+
264
+ def updateable?
265
+ true
266
+ end
267
+
268
+ def git?
269
+ false
270
+ end
271
+
272
+ def indexable?
273
+ false
274
+ end
275
+
276
+ private
277
+
278
+ def ensure_versions_file_loaded(fragment)
279
+ return if !@version_arrays_by_fragment_by_name[fragment].nil? && !@check_existing_files_for_update
280
+
281
+ # Index file that contains all the versions for all the pods in the shard.
282
+ # We use those because you can't get a directory listing from a CDN.
283
+ index_file_name = index_file_name_for_fragment(fragment)
284
+ download_file(index_file_name)
285
+ versions_raw = local_file(index_file_name, &:to_a).map(&:chomp)
286
+ @version_arrays_by_fragment_by_name[fragment] = versions_raw.reduce({}) do |hash, row|
287
+ row = row.split('/')
288
+ pod = row.shift
289
+ versions = row
290
+
291
+ hash[pod] = versions
292
+ hash
293
+ end
294
+ end
295
+
296
+ def algolia_search_index
297
+ @index ||= begin
298
+ require 'algoliasearch'
299
+
300
+ raise Informative, "Cannot perform full-text search in repo #{name} because it's missing Algolia config" if download_file('AlgoliaSearch.yml').nil?
301
+ algolia_config = YAMLHelper.load_string(local_file('AlgoliaSearch.yml', &:read))
302
+
303
+ client = Algolia::Client.new(:application_id => algolia_config['application_id'], :api_key => algolia_config['api_key'])
304
+ Algolia::Index.new(algolia_config['index'], client)
305
+ end
306
+ end
307
+
308
+ def index_file_name_for_fragment(fragment)
309
+ fragment_joined = fragment.join('_')
310
+ fragment_joined = '_' + fragment_joined unless fragment.empty?
311
+ "all_pods_versions#{fragment_joined}.txt"
312
+ end
313
+
314
+ def pod_shard_fragment(pod_name)
315
+ metadata.path_fragment(pod_name)[0..-2]
316
+ end
317
+
318
+ def local_file_okay?(partial_url)
319
+ file_path = repo.join(partial_url)
320
+ File.exist?(file_path) && File.size(file_path) > 0
321
+ end
322
+
323
+ def local_file(partial_url)
324
+ file_path = repo.join(partial_url)
325
+ File.open(file_path) do |file|
326
+ yield file if block_given?
327
+ end
328
+ end
329
+
330
+ def relative_pod_path(pod_name)
331
+ pod_path(pod_name).relative_path_from(repo)
332
+ end
333
+
334
+ def download_file(partial_url)
335
+ # Block the main thread waiting for Hydra to finish
336
+ #
337
+ # Used for single-file downloads
338
+ download_file_async(partial_url).wait!
339
+ end
340
+
341
+ def download_file_async(partial_url)
342
+ file_remote_url = Addressable::URI.encode(url + partial_url.to_s)
343
+ path = repo + partial_url
344
+
345
+ file_okay = local_file_okay?(partial_url)
346
+ if file_okay
347
+ if @startup_time < File.mtime(path)
348
+ debug "CDN: #{name} Relative path: #{partial_url} modified during this run! Returning local"
349
+ return Promises.fulfilled_future(partial_url, HYDRA_EXECUTOR)
350
+ end
351
+
352
+ unless @check_existing_files_for_update
353
+ debug "CDN: #{name} Relative path: #{partial_url} exists! Returning local because checking is only performed in repo update"
354
+ return Promises.fulfilled_future(partial_url, HYDRA_EXECUTOR)
355
+ end
356
+ end
357
+
358
+ path.dirname.mkpath
359
+
360
+ etag_path = path.sub_ext(path.extname + '.etag')
361
+
362
+ etag = File.read(etag_path) if file_okay && File.exist?(etag_path)
363
+ debug "CDN: #{name} Relative path: #{partial_url}, has ETag? #{etag}" unless etag.nil?
364
+
365
+ download_and_save_with_retries_async(partial_url, file_remote_url, etag)
366
+ end
367
+
368
+ def download_and_save_with_retries_async(partial_url, file_remote_url, etag, retries = MAX_NUMBER_OF_RETRIES)
369
+ path = repo + partial_url
370
+ etag_path = path.sub_ext(path.extname + '.etag')
371
+
372
+ download_task = download_typhoeus_impl_async(file_remote_url, etag).then do |response|
373
+ case response.response_code
374
+ when 301, 302
375
+ redirect_location = response.headers['location']
376
+ debug "CDN: #{name} Redirecting from #{file_remote_url} to #{redirect_location}"
377
+ download_and_save_with_retries_async(partial_url, redirect_location, etag)
378
+ when 304
379
+ debug "CDN: #{name} Relative path not modified: #{partial_url}"
380
+ # We need to update the file modification date, as it is later used for freshness
381
+ # optimization. See #initialize for more information.
382
+ FileUtils.touch path
383
+ partial_url
384
+ when 200
385
+ File.open(path, 'w') { |f| f.write(response.response_body.force_encoding('UTF-8')) }
386
+
387
+ etag_new = response.headers['etag'] unless response.headers.nil?
388
+ debug "CDN: #{name} Relative path downloaded: #{partial_url}, save ETag: #{etag_new}"
389
+ File.open(etag_path, 'w') { |f| f.write(etag_new) } unless etag_new.nil?
390
+ partial_url
391
+ when 404
392
+ debug "CDN: #{name} Relative path couldn't be downloaded: #{partial_url} Response: #{response.response_code}"
393
+ nil
394
+ when 502, 503, 504
395
+ # Retryable HTTP errors, usually related to server overloading
396
+ if retries <= 1
397
+ raise Informative, "CDN: #{name} URL couldn't be downloaded: #{file_remote_url} Response: #{response.response_code} #{response.response_body}"
398
+ else
399
+ debug "CDN: #{name} URL couldn't be downloaded: #{file_remote_url} Response: #{response.response_code} #{response.response_body}, retries: #{retries - 1}"
400
+ exponential_backoff_async(retries).then do
401
+ download_and_save_with_retries_async(partial_url, file_remote_url, etag, retries - 1)
402
+ end
403
+ end
404
+ when 0
405
+ # Non-HTTP errors, usually network layer
406
+ if retries <= 1
407
+ raise Informative, "CDN: #{name} URL couldn't be downloaded: #{file_remote_url} Response: #{response.return_message}"
408
+ else
409
+ debug "CDN: #{name} URL couldn't be downloaded: #{file_remote_url} Response: #{response.return_message}, retries: #{retries - 1}"
410
+ exponential_backoff_async(retries).then do
411
+ download_and_save_with_retries_async(partial_url, file_remote_url, etag, retries - 1)
412
+ end
413
+ end
414
+ else
415
+ raise Informative, "CDN: #{name} URL couldn't be downloaded: #{file_remote_url} Response: #{response.response_code} #{response.response_body}"
416
+ end
417
+ end
418
+
419
+ # Calling `Future#run` flattens the chained futures created by retries or redirects
420
+ #
421
+ # Does not, in fact, run the task - that is already happening in Hydra at this point
422
+ download_task.run
423
+ end
424
+
425
+ def exponential_backoff_async(retries)
426
+ sleep_async(backoff_time(retries))
427
+ end
428
+
429
+ def backoff_time(retries)
430
+ current_retry = MAX_NUMBER_OF_RETRIES - retries
431
+ 4 * 2**current_retry
432
+ end
433
+
434
+ def sleep_async(seconds)
435
+ # Async sleep to avoid blocking either the main or the Hydra thread
436
+ Promises.schedule_on(HYDRA_EXECUTOR, seconds)
437
+ end
438
+
439
+ def download_typhoeus_impl_async(file_remote_url, etag)
440
+ require 'typhoeus'
441
+
442
+ # Create a prefereably HTTP/2 request - the protocol is ultimately responsible for picking
443
+ # the maximum supported protocol
444
+ # When debugging with proxy, use the following extra options:
445
+ # :proxy => 'http://localhost:8888',
446
+ # :ssl_verifypeer => false,
447
+ # :ssl_verifyhost => 0,
448
+ request = Typhoeus::Request.new(
449
+ file_remote_url,
450
+ :method => :get,
451
+ :http_version => :httpv2_0,
452
+ :timeout => 10,
453
+ :connecttimeout => 10,
454
+ :accept_encoding => 'gzip',
455
+ :netrc => :optional,
456
+ :netrc_file => Netrc.default_path,
457
+ :headers => etag.nil? ? {} : { 'If-None-Match' => etag },
458
+ )
459
+
460
+ future = Promises.resolvable_future_on(HYDRA_EXECUTOR)
461
+ queue_request(request)
462
+ request.on_complete do |response|
463
+ future.fulfill(response)
464
+ end
465
+
466
+ # This `Future` should never reject, network errors are exposed on `Typhoeus::Response`
467
+ future
468
+ end
469
+
470
+ def debug(message)
471
+ if defined?(Pod::UI)
472
+ Pod::UI.message(message)
473
+ else
474
+ CoreUI.puts(message)
475
+ end
476
+ end
477
+
478
+ def concurrent_requests_catching_errors
479
+ yield
480
+ rescue MultipleErrors => e
481
+ # aggregated error message from `Concurrent`
482
+ errors = e.errors
483
+ raise Informative, "CDN: #{name} Repo update failed - #{e.errors.size} error(s):\n#{errors.join("\n")}"
484
+ end
485
+
486
+ def queue_request(request)
487
+ @hydra ||= Typhoeus::Hydra.new
488
+
489
+ # Queue the request into the Hydra (libcurl reactor).
490
+ @hydra.queue(request)
491
+
492
+ # Cycle the reactor on a separate thread
493
+ #
494
+ # The way it works is that if more requests are queued while Hydra is in the `#run`
495
+ # method, it will keep executing them
496
+ #
497
+ # The upcoming calls to `#run` will simply run empty.
498
+ HYDRA_EXECUTOR.post(@hydra, &:run)
499
+ end
500
+ end
501
+ end
@@ -1,18 +1,19 @@
1
1
  module Pod
2
-
3
2
  # Manages the UI output so dependent gems can customize it.
4
3
  #
5
4
  module CoreUI
6
-
7
5
  def self.puts(message)
8
6
  STDOUT.puts message
9
7
  end
10
8
 
9
+ def self.print(message)
10
+ STDOUT.print(message)
11
+ end
12
+
11
13
  def self.warn(message)
12
14
  STDERR.puts message
13
15
  end
14
16
 
15
17
  #-------------------------------------------------------------------------#
16
-
17
18
  end
18
19
  end