mini_racer 0.4.0 → 0.6.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,8 +1,13 @@
1
1
  require 'mkmf'
2
2
 
3
+ if RUBY_ENGINE == "truffleruby"
4
+ File.write("Makefile", dummy_makefile($srcdir).join(""))
5
+ return
6
+ end
7
+
3
8
  extension_name = 'mini_racer_loader'
4
9
  dir_config extension_name
5
10
 
6
- $CPPFLAGS += " -fvisibility=hidden "
11
+ $CXXFLAGS += " -fvisibility=hidden "
7
12
 
8
13
  create_makefile extension_name
@@ -0,0 +1,353 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MiniRacer
4
+
5
+ class Context
6
+
7
+ class ExternalFunction
8
+ private
9
+
10
+ def notify_v8
11
+ name = @name.encode(::Encoding::UTF_8)
12
+ wrapped = lambda do |*args|
13
+ converted = @parent.send(:convert_js_to_ruby, args)
14
+ begin
15
+ result = @callback.call(*converted)
16
+ rescue Polyglot::ForeignException => e
17
+ e = RuntimeError.new(e.message)
18
+ e.set_backtrace(e.backtrace)
19
+ @parent.instance_variable_set(:@current_exception, e)
20
+ raise e
21
+ rescue => e
22
+ @parent.instance_variable_set(:@current_exception, e)
23
+ raise e
24
+ end
25
+ @parent.send(:convert_ruby_to_js, result)
26
+ end
27
+
28
+ if @parent_object.nil?
29
+ # set global name to proc
30
+ result = @parent.eval_in_context('this')
31
+ result[name] = wrapped
32
+ else
33
+ parent_object_eval = @parent_object_eval.encode(::Encoding::UTF_8)
34
+ begin
35
+ result = @parent.eval_in_context(parent_object_eval)
36
+ rescue Polyglot::ForeignException, StandardError => e
37
+ raise ParseError, "Was expecting #{@parent_object} to be an object", e.backtrace
38
+ end
39
+ result[name] = wrapped
40
+ # set evaluated object results name to proc
41
+ end
42
+ end
43
+ end
44
+
45
+ def heap_stats
46
+ {
47
+ total_physical_size: 0,
48
+ total_heap_size_executable: 0,
49
+ total_heap_size: 0,
50
+ used_heap_size: 0,
51
+ heap_size_limit: 0,
52
+ }
53
+ end
54
+
55
+ def stop
56
+ if @entered
57
+ @context.stop
58
+ @stopped = true
59
+ stop_attached
60
+ end
61
+ end
62
+
63
+ private
64
+
65
+ @context_initialized = false
66
+ @use_strict = false
67
+
68
+ def init_unsafe(isolate, snapshot)
69
+ unless defined?(Polyglot::InnerContext)
70
+ raise "TruffleRuby #{RUBY_ENGINE_VERSION} does not have support for inner contexts, use a more recent version"
71
+ end
72
+
73
+ unless Polyglot.languages.include? "js"
74
+ warn "You also need to install the 'js' component with 'gu install js' on GraalVM 22.2+", uplevel: 0 if $VERBOSE
75
+ end
76
+
77
+ @context = Polyglot::InnerContext.new(on_cancelled: -> {
78
+ raise ScriptTerminatedError, 'JavaScript was terminated (either by timeout or explicitly)'
79
+ })
80
+ Context.instance_variable_set(:@context_initialized, true)
81
+ @js_object = @context.eval('js', 'Object')
82
+ @isolate_mutex = Mutex.new
83
+ @stopped = false
84
+ @entered = false
85
+ @has_entered = false
86
+ @current_exception = nil
87
+ if isolate && snapshot
88
+ isolate.instance_variable_set(:@snapshot, snapshot)
89
+ end
90
+ if snapshot
91
+ @snapshot = snapshot
92
+ elsif isolate
93
+ @snapshot = isolate.instance_variable_get(:@snapshot)
94
+ else
95
+ @snapshot = nil
96
+ end
97
+ @is_object_or_array_func, @is_time_func, @js_date_to_time_func, @is_symbol_func, @js_symbol_to_symbol_func, @js_new_date_func, @js_new_array_func = eval_in_context <<-CODE
98
+ [
99
+ (x) => { return (x instanceof Object || x instanceof Array) && !(x instanceof Date) && !(x instanceof Function) },
100
+ (x) => { return x instanceof Date },
101
+ (x) => { return x.getTime(x) },
102
+ (x) => { return typeof x === 'symbol' },
103
+ (x) => { var r = x.description; return r === undefined ? 'undefined' : r },
104
+ (x) => { return new Date(x) },
105
+ (x) => { return new Array(x) },
106
+ ]
107
+ CODE
108
+ end
109
+
110
+ def dispose_unsafe
111
+ @context.close
112
+ end
113
+
114
+ def eval_unsafe(str, filename)
115
+ @entered = true
116
+ if !@has_entered && @snapshot
117
+ snapshot_src = encode(@snapshot.instance_variable_get(:@source))
118
+ begin
119
+ eval_in_context(snapshot_src, filename)
120
+ rescue Polyglot::ForeignException => e
121
+ raise RuntimeError, e.message, e.backtrace
122
+ end
123
+ end
124
+ @has_entered = true
125
+ raise RuntimeError, "TruffleRuby does not support eval after stop" if @stopped
126
+ raise TypeError, "wrong type argument #{str.class} (should be a string)" unless str.is_a?(String)
127
+ raise TypeError, "wrong type argument #{filename.class} (should be a string)" unless filename.nil? || filename.is_a?(String)
128
+
129
+ str = encode(str)
130
+ begin
131
+ translate do
132
+ eval_in_context(str, filename)
133
+ end
134
+ rescue Polyglot::ForeignException => e
135
+ raise RuntimeError, e.message, e.backtrace
136
+ rescue ::RuntimeError => e
137
+ if @current_exception
138
+ e = @current_exception
139
+ @current_exception = nil
140
+ raise e
141
+ else
142
+ raise e, e.message
143
+ end
144
+ end
145
+ ensure
146
+ @entered = false
147
+ end
148
+
149
+ def call_unsafe(function_name, *arguments)
150
+ @entered = true
151
+ if !@has_entered && @snapshot
152
+ src = encode(@snapshot.instance_variable_get(:source))
153
+ begin
154
+ eval_in_context(src)
155
+ rescue Polyglot::ForeignException => e
156
+ raise RuntimeError, e.message, e.backtrace
157
+ end
158
+ end
159
+ @has_entered = true
160
+ raise RuntimeError, "TruffleRuby does not support call after stop" if @stopped
161
+ begin
162
+ translate do
163
+ function = eval_in_context(function_name)
164
+ function.call(*convert_ruby_to_js(arguments))
165
+ end
166
+ rescue Polyglot::ForeignException => e
167
+ raise RuntimeError, e.message, e.backtrace
168
+ end
169
+ ensure
170
+ @entered = false
171
+ end
172
+
173
+ def create_isolate_value
174
+ # Returning a dummy object since TruffleRuby does not have a 1-1 concept with isolate.
175
+ # However, code and ASTs are shared between contexts.
176
+ Isolate.new
177
+ end
178
+
179
+ def isolate_mutex
180
+ @isolate_mutex
181
+ end
182
+
183
+ def translate
184
+ convert_js_to_ruby yield
185
+ rescue Object => e
186
+ message = e.message
187
+ if @current_exception
188
+ raise @current_exception
189
+ elsif e.message && e.message.start_with?('SyntaxError:')
190
+ error_class = MiniRacer::ParseError
191
+ elsif e.is_a?(MiniRacer::ScriptTerminatedError)
192
+ error_class = MiniRacer::ScriptTerminatedError
193
+ else
194
+ error_class = MiniRacer::RuntimeError
195
+ end
196
+
197
+ if error_class == MiniRacer::RuntimeError
198
+ bls = e.backtrace_locations&.select { |bl| bl&.source_location&.language == 'js' }
199
+ if bls && !bls.empty?
200
+ if '(eval)' != bls[0].path
201
+ message = "#{e.message}\n at #{bls[0]}\n" + bls[1..].map(&:to_s).join("\n")
202
+ else
203
+ message = "#{e.message}\n" + bls.map(&:to_s).join("\n")
204
+ end
205
+ end
206
+ raise error_class, message
207
+ else
208
+ raise error_class, message, e.backtrace
209
+ end
210
+ end
211
+
212
+ def convert_js_to_ruby(value)
213
+ case value
214
+ when true, false, Integer, Float
215
+ value
216
+ else
217
+ if value.nil?
218
+ nil
219
+ elsif value.respond_to?(:call)
220
+ MiniRacer::JavaScriptFunction.new
221
+ elsif value.respond_to?(:to_str)
222
+ value.to_str.dup
223
+ elsif value.respond_to?(:to_ary)
224
+ value.to_ary.map do |e|
225
+ if e.respond_to?(:call)
226
+ nil
227
+ else
228
+ convert_js_to_ruby(e)
229
+ end
230
+ end
231
+ elsif time?(value)
232
+ js_date_to_time(value)
233
+ elsif symbol?(value)
234
+ js_symbol_to_symbol(value)
235
+ else
236
+ object = value
237
+ h = {}
238
+ object.instance_variables.each do |member|
239
+ v = object[member]
240
+ unless v.respond_to?(:call)
241
+ h[member.to_s] = convert_js_to_ruby(v)
242
+ end
243
+ end
244
+ h
245
+ end
246
+ end
247
+ end
248
+
249
+ def object_or_array?(val)
250
+ @is_object_or_array_func.call(val)
251
+ end
252
+
253
+ def time?(value)
254
+ @is_time_func.call(value)
255
+ end
256
+
257
+ def js_date_to_time(value)
258
+ millis = @js_date_to_time_func.call(value)
259
+ Time.at(Rational(millis, 1000))
260
+ end
261
+
262
+ def symbol?(value)
263
+ @is_symbol_func.call(value)
264
+ end
265
+
266
+ def js_symbol_to_symbol(value)
267
+ @js_symbol_to_symbol_func.call(value).to_s.to_sym
268
+ end
269
+
270
+ def js_new_date(value)
271
+ @js_new_date_func.call(value)
272
+ end
273
+
274
+ def js_new_array(size)
275
+ @js_new_array_func.call(size)
276
+ end
277
+
278
+ def convert_ruby_to_js(value)
279
+ case value
280
+ when nil, true, false, Integer, Float
281
+ value
282
+ when Array
283
+ ary = js_new_array(value.size)
284
+ value.each_with_index do |v, i|
285
+ ary[i] = convert_ruby_to_js(v)
286
+ end
287
+ ary
288
+ when Hash
289
+ h = @js_object.new
290
+ value.each_pair do |k, v|
291
+ h[convert_ruby_to_js(k.to_s)] = convert_ruby_to_js(v)
292
+ end
293
+ h
294
+ when String, Symbol
295
+ Truffle::Interop.as_truffle_string value
296
+ when Time
297
+ js_new_date(value.to_f * 1000)
298
+ when DateTime
299
+ js_new_date(value.to_time.to_f * 1000)
300
+ else
301
+ "Undefined Conversion"
302
+ end
303
+ end
304
+
305
+ def encode(string)
306
+ raise ArgumentError unless string
307
+ string.encode(::Encoding::UTF_8)
308
+ end
309
+
310
+ class_eval <<-'RUBY', "(mini_racer)", 1
311
+ def eval_in_context(code, file = nil); code = ('"use strict";' + code) if Context.instance_variable_get(:@use_strict); @context.eval('js', code, file || '(mini_racer)'); end
312
+ RUBY
313
+
314
+ end
315
+
316
+ class Isolate
317
+ def init_with_snapshot(snapshot)
318
+ # TruffleRuby does not have a 1-1 concept with isolate.
319
+ # However, isolate can hold a snapshot, and code and ASTs are shared between contexts.
320
+ @snapshot = snapshot
321
+ end
322
+
323
+ def low_memory_notification
324
+ GC.start
325
+ end
326
+
327
+ def idle_notification(idle_time)
328
+ true
329
+ end
330
+ end
331
+
332
+ class Platform
333
+ def self.set_flag_as_str!(flag)
334
+ raise TypeError, "wrong type argument #{flag.class} (should be a string)" unless flag.is_a?(String)
335
+ raise MiniRacer::PlatformAlreadyInitialized, "The platform is already initialized." if Context.instance_variable_get(:@context_initialized)
336
+ Context.instance_variable_set(:@use_strict, true) if "--use_strict" == flag
337
+ end
338
+ end
339
+
340
+ class Snapshot
341
+ def load(str)
342
+ raise TypeError, "wrong type argument #{str.class} (should be a string)" unless str.is_a?(String)
343
+ # Intentionally noop since TruffleRuby mocks the snapshot API
344
+ end
345
+
346
+ def warmup_unsafe!(src)
347
+ raise TypeError, "wrong type argument #{src.class} (should be a string)" unless src.is_a?(String)
348
+ # Intentionally noop since TruffleRuby mocks the snapshot API
349
+ # by replaying snapshot source before the first eval/call
350
+ self
351
+ end
352
+ end
353
+ end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MiniRacer
4
- VERSION = "0.4.0"
5
- LIBV8_NODE_VERSION = "~> 15.14.0.0"
4
+ VERSION = "0.6.3"
5
+ LIBV8_NODE_VERSION = "~> 16.10.0.0"
6
6
  end
data/lib/mini_racer.rb CHANGED
@@ -1,20 +1,28 @@
1
1
  require "mini_racer/version"
2
- require "mini_racer_loader"
3
2
  require "pathname"
4
3
 
5
- ext_filename = "mini_racer_extension.#{RbConfig::CONFIG['DLEXT']}"
6
- ext_path = Gem.loaded_specs['mini_racer'].require_paths
7
- .map { |p| (p = Pathname.new(p)).absolute? ? p : Pathname.new(__dir__).parent + p }
8
- ext_found = ext_path.map { |p| p + ext_filename }.find { |p| p.file? }
9
-
10
- raise LoadError, "Could not find #{ext_filename} in #{ext_path.map(&:to_s)}" unless ext_found
11
- MiniRacer::Loader.load(ext_found.to_s)
4
+ if RUBY_ENGINE == "truffleruby"
5
+ require "mini_racer/truffleruby"
6
+ else
7
+ require "mini_racer_loader"
8
+ ext_filename = "mini_racer_extension.#{RbConfig::CONFIG['DLEXT']}"
9
+ ext_path = Gem.loaded_specs['mini_racer'].require_paths
10
+ .map { |p| (p = Pathname.new(p)).absolute? ? p : Pathname.new(__dir__).parent + p }
11
+ ext_found = ext_path.map { |p| p + ext_filename }.find { |p| p.file? }
12
+
13
+ raise LoadError, "Could not find #{ext_filename} in #{ext_path.map(&:to_s)}" unless ext_found
14
+ MiniRacer::Loader.load(ext_found.to_s)
15
+ end
12
16
 
13
17
  require "thread"
14
18
  require "json"
19
+ require "io/wait"
15
20
 
16
21
  module MiniRacer
17
22
 
23
+ MARSHAL_STACKDEPTH_DEFAULT = 2**9-2
24
+ MARSHAL_STACKDEPTH_MAX_VALUE = 2**10-2
25
+
18
26
  class Error < ::StandardError; end
19
27
 
20
28
  class ContextDisposedError < Error; end
@@ -140,10 +148,10 @@ module MiniRacer
140
148
  end
141
149
  end
142
150
 
143
- def initialize(max_memory: nil, timeout: nil, isolate: nil, ensure_gc_after_idle: nil, snapshot: nil)
151
+ def initialize(max_memory: nil, timeout: nil, isolate: nil, ensure_gc_after_idle: nil, snapshot: nil, marshal_stack_depth: nil)
144
152
  options ||= {}
145
153
 
146
- check_init_options!(isolate: isolate, snapshot: snapshot, max_memory: max_memory, ensure_gc_after_idle: ensure_gc_after_idle, timeout: timeout)
154
+ check_init_options!(isolate: isolate, snapshot: snapshot, max_memory: max_memory, marshal_stack_depth: marshal_stack_depth, ensure_gc_after_idle: ensure_gc_after_idle, timeout: timeout)
147
155
 
148
156
  @functions = {}
149
157
  @timeout = nil
@@ -151,6 +159,7 @@ module MiniRacer
151
159
  @current_exception = nil
152
160
  @timeout = timeout
153
161
  @max_memory = max_memory
162
+ @marshal_stack_depth = marshal_stack_depth
154
163
 
155
164
  # false signals it should be fetched if requested
156
165
  @isolate = isolate || false
@@ -198,7 +207,7 @@ module MiniRacer
198
207
  end
199
208
 
200
209
  if !(File === f)
201
- raise ArgumentError("file_or_io")
210
+ raise ArgumentError, "file_or_io"
202
211
  end
203
212
 
204
213
  write_heap_snapshot_unsafe(f)
@@ -345,7 +354,7 @@ module MiniRacer
345
354
 
346
355
  t = Thread.new do
347
356
  begin
348
- result = IO.select([rp],[],[],(@timeout/1000.0))
357
+ result = rp.wait_readable(@timeout/1000.0)
349
358
  if !result
350
359
  mutex.synchronize do
351
360
  stop unless done
@@ -362,7 +371,7 @@ module MiniRacer
362
371
  done = true
363
372
  end
364
373
 
365
- wp.write("done")
374
+ wp.close
366
375
 
367
376
  # ensure we do not leak a thread in state
368
377
  t.join
@@ -371,19 +380,17 @@ module MiniRacer
371
380
  rval
372
381
  ensure
373
382
  # exceptions need to be handled
374
- if t && wp
375
- wp.write("done")
376
- t.join
377
- end
378
- wp.close if wp
379
- rp.close if rp
383
+ wp&.close
384
+ t&.join
385
+ rp&.close
380
386
  end
381
387
 
382
- def check_init_options!(isolate:, snapshot:, max_memory:, ensure_gc_after_idle:, timeout:)
388
+ def check_init_options!(isolate:, snapshot:, max_memory:, marshal_stack_depth:, ensure_gc_after_idle:, timeout:)
383
389
  assert_option_is_nil_or_a('isolate', isolate, Isolate)
384
390
  assert_option_is_nil_or_a('snapshot', snapshot, Snapshot)
385
391
 
386
- assert_numeric_or_nil('max_memory', max_memory, min_value: 10_000)
392
+ assert_numeric_or_nil('max_memory', max_memory, min_value: 10_000, max_value: 2**32-1)
393
+ assert_numeric_or_nil('marshal_stack_depth', marshal_stack_depth, min_value: 1, max_value: MARSHAL_STACKDEPTH_MAX_VALUE)
387
394
  assert_numeric_or_nil('ensure_gc_after_idle', ensure_gc_after_idle, min_value: 1)
388
395
  assert_numeric_or_nil('timeout', timeout, min_value: 1)
389
396
 
@@ -392,9 +399,13 @@ module MiniRacer
392
399
  end
393
400
  end
394
401
 
395
- def assert_numeric_or_nil(option_name, object, min_value:)
402
+ def assert_numeric_or_nil(option_name, object, min_value:, max_value: nil)
403
+ if max_value && object.is_a?(Numeric) && object > max_value
404
+ raise ArgumentError, "#{option_name} must be less than or equal to #{max_value}"
405
+ end
406
+
396
407
  if object.is_a?(Numeric) && object < min_value
397
- raise ArgumentError, "#{option_name} must be larger than #{min_value}"
408
+ raise ArgumentError, "#{option_name} must be larger than or equal to #{min_value}"
398
409
  end
399
410
 
400
411
  if !object.nil? && !object.is_a?(Numeric)
data/mini_racer.gemspec CHANGED
@@ -22,8 +22,6 @@ Gem::Specification.new do |spec|
22
22
  }
23
23
 
24
24
  spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(benchmark|test|spec|features|examples)/}) }
25
- spec.bindir = "exe"
26
- spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
27
25
  spec.require_paths = ["lib"]
28
26
 
29
27
  spec.add_development_dependency "bundler"
@@ -37,5 +35,5 @@ Gem::Specification.new do |spec|
37
35
 
38
36
  spec.extensions = ["ext/mini_racer_loader/extconf.rb", "ext/mini_racer_extension/extconf.rb"]
39
37
 
40
- spec.required_ruby_version = '>= 2.3'
38
+ spec.required_ruby_version = '>= 2.6'
41
39
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mini_racer
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.6.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sam Saffron
8
8
  autorequire:
9
- bindir: exe
9
+ bindir: bin
10
10
  cert_chain: []
11
- date: 2021-04-12 00:00:00.000000000 Z
11
+ date: 2022-08-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -86,14 +86,14 @@ dependencies:
86
86
  requirements:
87
87
  - - "~>"
88
88
  - !ruby/object:Gem::Version
89
- version: 15.14.0.0
89
+ version: 16.10.0.0
90
90
  type: :runtime
91
91
  prerelease: false
92
92
  version_requirements: !ruby/object:Gem::Requirement
93
93
  requirements:
94
94
  - - "~>"
95
95
  - !ruby/object:Gem::Version
96
- version: 15.14.0.0
96
+ version: 16.10.0.0
97
97
  description: Minimal embedded v8 engine for Ruby
98
98
  email:
99
99
  - sam.saffron@gmail.com
@@ -106,7 +106,6 @@ files:
106
106
  - ".dockerignore"
107
107
  - ".github/workflows/ci.yml"
108
108
  - ".gitignore"
109
- - ".travis.yml"
110
109
  - CHANGELOG
111
110
  - CODE_OF_CONDUCT.md
112
111
  - Dockerfile
@@ -121,6 +120,7 @@ files:
121
120
  - ext/mini_racer_loader/extconf.rb
122
121
  - ext/mini_racer_loader/mini_racer_loader.c
123
122
  - lib/mini_racer.rb
123
+ - lib/mini_racer/truffleruby.rb
124
124
  - lib/mini_racer/version.rb
125
125
  - mini_racer.gemspec
126
126
  homepage: https://github.com/discourse/mini_racer
@@ -128,9 +128,9 @@ licenses:
128
128
  - MIT
129
129
  metadata:
130
130
  bug_tracker_uri: https://github.com/discourse/mini_racer/issues
131
- changelog_uri: https://github.com/discourse/mini_racer/blob/v0.4.0/CHANGELOG
132
- documentation_uri: https://www.rubydoc.info/gems/mini_racer/0.4.0
133
- source_code_uri: https://github.com/discourse/mini_racer/tree/v0.4.0
131
+ changelog_uri: https://github.com/discourse/mini_racer/blob/v0.6.3/CHANGELOG
132
+ documentation_uri: https://www.rubydoc.info/gems/mini_racer/0.6.3
133
+ source_code_uri: https://github.com/discourse/mini_racer/tree/v0.6.3
134
134
  post_install_message:
135
135
  rdoc_options: []
136
136
  require_paths:
@@ -140,14 +140,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
140
140
  requirements:
141
141
  - - ">="
142
142
  - !ruby/object:Gem::Version
143
- version: '2.3'
143
+ version: '2.6'
144
144
  required_rubygems_version: !ruby/object:Gem::Requirement
145
145
  requirements:
146
146
  - - ">="
147
147
  - !ruby/object:Gem::Version
148
148
  version: '0'
149
149
  requirements: []
150
- rubygems_version: 3.2.2
150
+ rubygems_version: 3.3.20
151
151
  signing_key:
152
152
  specification_version: 4
153
153
  summary: Minimal embedded v8 for Ruby
data/.travis.yml DELETED
@@ -1,31 +0,0 @@
1
- language: ruby
2
- os: linux
3
- rvm:
4
- - 2.4
5
- - 2.5
6
- - 2.6
7
- - 2.7
8
- - 3.0
9
- - ruby-head
10
- arch:
11
- - amd64
12
- - arm64
13
- jobs:
14
- include:
15
- - rvm: 2.5
16
- os: osx
17
- osx_image: xcode9.4
18
- - rvm: 2.6
19
- os: osx
20
- osx_image: xcode11.3
21
- - rvm: 2.6
22
- os: osx
23
- osx_image: xcode12.2
24
- - rvm: 2.7
25
- os: osx
26
- osx_image: xcode12.2
27
- dist: xenial
28
- before_install:
29
- - gem update --system
30
- - gem install bundler -v 1.16.2
31
- cache: bundler