sq_mini_racer 0.2.4.sqreen3 → 0.2.5.0.1.beta1

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: da4cace43c9af136e8d30f3918dbd99af25f72fd904d7db9a888a843925000ba
4
- data.tar.gz: 3f234d5dafea154baa80d0a459bcff351cb0c0d01ed2604b293bd22756809303
3
+ metadata.gz: a4deece72e6d02478b4381a59ab9b1988a2fbff6a30e811596183240699fd018
4
+ data.tar.gz: ac83f077c9b56423cd8b8e716d8102bec73050c8a4ae0e525fda931f43ec6762
5
5
  SHA512:
6
- metadata.gz: 9b461007d884a961d3b081ceac7c5d815477d3993ced64f108ae078ede7d54318a0a3aa3b504431b50d6b7167ea4f0ac592f097406e31c35cd184bb25e84ac6b
7
- data.tar.gz: a8e76f1ddd78a6578aaa35424f87ffce467065cf1b981af9673a6a7e01d5c6e0263161eaa68f1263acffd20e400ae0453a9a388cc0735d7d75b539f6d2973b09
6
+ metadata.gz: cad6011c1066ccc51e1c3bfe3064c50e7dbdc6dd04127ce24f855a97f9ba8fd492a5de3b1af54b810b4a112d74f03bcd746c8d95af822a5f9d59e8cd9cdce8a2
7
+ data.tar.gz: f985a398202455acfadd914606306d5469928bfbf69394fd39b5a79b097f81a6994bf4875bb7765d01b4fd2797b00378a5e3d23de4a05b4d3588216e3ac59cc9
data/CHANGELOG CHANGED
@@ -1,3 +1,18 @@
1
+ - 14-05-2019
2
+
3
+ - 0.2.6
4
+
5
+ - FEATURE: add support for write_heap_snapshot which helps you analyze memory
6
+
7
+ - 25-04-2019
8
+
9
+ - 0.2.5
10
+
11
+ - FIX: Compatiblity fixes for V8 7 and above @ignisf
12
+ - FIX: Memory leak in gc_callback @messense
13
+ - IMPROVEMENT: Added example of sourcemap support @ianks
14
+ - URGENT: you will need this release for latest version of libv8 to work
15
+
1
16
  - 02-11-2018
2
17
 
3
18
  - 0.2.4
data/README.md CHANGED
@@ -101,6 +101,27 @@ context.eval('bar()', filename: 'a/bar.js')
101
101
 
102
102
  ```
103
103
 
104
+ ### Fork safety
105
+
106
+ Some Ruby web servers employ forking (for example unicorn or puma in clustered mode). V8 is not fork safe.
107
+ Sadly Ruby does not have support for fork notifications per [#5446](https://bugs.ruby-lang.org/issues/5446).
108
+
109
+ If you want to ensure your application does not leak memory after fork either:
110
+
111
+ 1. Ensure no MiniRacer::Context objects are created in the master process
112
+
113
+ Or
114
+
115
+ 2. Dispose manually of all MiniRacer::Context objects prior to forking
116
+
117
+ ```ruby
118
+ # before fork
119
+
120
+ require 'objspace'
121
+ ObjectSpace.each_object(MiniRacer::Context){|c| c.dispose}
122
+
123
+ # fork here
124
+ ```
104
125
 
105
126
  ### Threadsafe
106
127
 
@@ -350,6 +371,10 @@ Note how the global interpreter lock release leads to 2 threads doing the same w
350
371
 
351
372
  As a rule MiniRacer strives to always support and depend on the latest stable version of libv8.
352
373
 
374
+ ## Source Maps
375
+
376
+ MiniRacer can fully support source maps but must be configured correctly to do so. [Check out this example](./examples/source-map-support/) for a working implementation.
377
+
353
378
  ## Installation
354
379
 
355
380
  Add this line to your application's Gemfile:
data/Rakefile CHANGED
@@ -19,6 +19,22 @@ Rake::ExtensionTask.new('prv_ext_loader', gem)
19
19
 
20
20
  # via http://blog.flavorjon.es/2009/06/easily-valgrind-gdb-your-ruby-c.html
21
21
  namespace :test do
22
+ desc "run test suite with Address Sanitizer"
23
+ task :asan do
24
+ ENV["CONFIGURE_ARGS"] = [ENV["CONFIGURE_ARGS"], '--enable-asan'].compact.join(' ')
25
+ Rake::Task['compile'].invoke
26
+
27
+ asan_path = `ldconfig -N -p |grep libasan | grep -v 32 | sed 's/.* => \\(.*\\)$/\\1/'`.chomp.split("\n")[-1]
28
+
29
+
30
+ cmdline = "env LD_PRELOAD=\"#{asan_path}\" ruby test/test_leak.rb"
31
+ puts cmdline
32
+ system cmdline
33
+
34
+ cmdline = "env LD_PRELOAD=\"#{asan_path}\" rake test"
35
+ puts cmdline
36
+ system cmdline
37
+ end
22
38
  # partial-loads-ok and undef-value-errors necessary to ignore
23
39
  # spurious (and eminently ignorable) warnings from the ruby
24
40
  # interpreter
@@ -55,3 +71,28 @@ namespace :test do
55
71
  end
56
72
  end
57
73
  end
74
+
75
+ desc 'run clang-tidy linter on mini_racer_extension.cc'
76
+ task :lint do
77
+ require 'mkmf'
78
+ require 'libv8'
79
+
80
+ Libv8.configure_makefile
81
+
82
+ conf = RbConfig::CONFIG.merge('hdrdir' => $hdrdir.quote, 'srcdir' => $srcdir.quote,
83
+ 'arch_hdrdir' => $arch_hdrdir.quote,
84
+ 'top_srcdir' => $top_srcdir.quote)
85
+ if $universal and (arch_flag = conf['ARCH_FLAG']) and !arch_flag.empty?
86
+ conf['ARCH_FLAG'] = arch_flag.gsub(/(?:\G|\s)-arch\s+\S+/, '')
87
+ end
88
+
89
+ checks = %W(bugprone-*
90
+ cert-*
91
+ cppcoreguidelines-*
92
+ clang-analyzer-*
93
+ performance-*
94
+ portability-*
95
+ readability-*).join(',')
96
+
97
+ sh RbConfig::expand("clang-tidy -checks='#{checks}' ext/mini_racer_extension/mini_racer_extension.cc -- #$INCFLAGS #$CPPFLAGS", conf)
98
+ end
@@ -1,24 +1,191 @@
1
1
  require 'mkmf'
2
+
2
3
  require 'fileutils'
4
+ require 'net/http'
5
+ require 'json'
6
+ require 'rubygems'
7
+ require 'rubygems/package'
3
8
 
4
- IS_DARWIN = RUBY_PLATFORM =~ /darwin/
5
9
  IS_SOLARIS = RUBY_PLATFORM =~ /solaris/
10
+ IS_LINUX_MUSL = RUBY_PLATFORM =~ /linux-musl/
11
+
12
+ def cppflags_clear_std!
13
+ $CPPFLAGS.gsub! /-std=[^\s]+/, ''
14
+ end
15
+
16
+ def cppflags_add_frame_pointer!
17
+ $CPPFLAGS += " -fno-omit-frame-pointer"
18
+ end
19
+
20
+ def cppflags_add_cpu_extension!
21
+ if enable_config('avx2')
22
+ $CPPFLAGS += " -mavx2"
23
+ else
24
+ $CPPFLAGS += " -mssse3"
25
+ end
26
+ end
27
+
28
+ def libv8_gem_name
29
+ return "libv8-solaris" if IS_SOLARIS
30
+ return "libv8-alpine" if IS_LINUX_MUSL
31
+
32
+ 'libv8'
33
+ end
34
+
35
+ def libv8_version
36
+ '7.3.492.27.1'
37
+ end
38
+
39
+ def libv8_basename
40
+ "#{libv8_gem_name}-#{libv8_version}-#{ruby_platform}"
41
+ end
42
+
43
+ def libv8_gemspec
44
+ "#{libv8_basename}.gemspec"
45
+ end
46
+
47
+ def libv8_local_path
48
+ puts "looking for #{libv8_gemspec} in installed gems"
49
+ candidates = Gem.path.map { |p| File.join(p, 'specifications', libv8_gemspec) }
50
+ found = candidates.select { |f| File.exist?(f) }.first
51
+
52
+ unless found
53
+ puts "#{libv8_gemspec} not found in installed gems"
54
+ return
55
+ end
56
+
57
+ puts "found in installed specs: #{found}"
58
+
59
+ dir = File.expand_path(File.join(found, '..', '..', 'gems', libv8_basename))
60
+
61
+ unless Dir.exist?(dir)
62
+ puts "not found in installed gems: #{dir}"
63
+ return
64
+ end
65
+
66
+ puts "found in installed gems: #{dir}"
67
+
68
+ dir
69
+ end
70
+
71
+ def vendor_path
72
+ File.join(Dir.pwd, 'vendor')
73
+ end
74
+
75
+ def libv8_vendor_path
76
+ puts "looking for #{libv8_basename} in #{vendor_path}"
77
+ path = Dir.glob("#{vendor_path}/#{libv8_basename}").first
78
+
79
+ unless path
80
+ puts "#{libv8_basename} not found in #{vendor_path}"
81
+ return
82
+ end
83
+
84
+ puts "looking for #{libv8_basename}/lib/libv8.rb in #{vendor_path}"
85
+ unless Dir.glob(File.join(vendor_path, libv8_basename, 'lib', 'libv8.rb')).first
86
+ puts "#{libv8_basename}/lib/libv8.rb not found in #{vendor_path}"
87
+ return
88
+ end
89
+
90
+ path
91
+ end
92
+
93
+ def parse_platform(str)
94
+ Gem::Platform.new(str).tap { |p| p.instance_eval { @os = 'linux-musl' } if str =~ /musl/ }
95
+ end
96
+
97
+ def ruby_platform
98
+ parse_platform(RUBY_PLATFORM)
99
+ end
100
+
101
+ def http_get(uri)
102
+ Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') do |http|
103
+ res = http.get(uri.path)
104
+
105
+ abort("HTTP error #{res.code}: #{uri}") unless res.code == '200'
106
+
107
+ return res.body
108
+ end
109
+ end
110
+
111
+ def libv8_remote_search
112
+ body = http_get(URI("https://rubygems.org/api/v1/versions/#{libv8_gem_name}.json"))
113
+ json = JSON.parse(body)
114
+
115
+ versions = json.select do |v|
116
+ Gem::Version.new(v['number']) == Gem::Version.new(libv8_version)
117
+ end
118
+ abort(<<-ERROR) if versions.empty?
119
+ ERROR: could not find #{libv8_version}
120
+ ERROR
121
+
122
+ platform_versions = versions.select do |v|
123
+ parse_platform(v['platform']) == ruby_platform
124
+ end
125
+ abort(<<-ERROR) if platform_versions.empty?
126
+ ERROR: found #{libv8_gem_name}-#{libv8_version}, but no binary for #{ruby_platform}
127
+ try "gem install #{libv8_gem_name} -v '#{libv8_version}'" to attempt to build libv8 from source
128
+ ERROR
129
+
130
+ platform_versions.first
131
+ end
132
+
133
+ def libv8_download_uri(name, version, platform)
134
+ URI("https://rubygems.org/downloads/#{name}-#{version}-#{platform}.gem")
135
+ end
136
+
137
+ def libv8_downloaded_gem(name, version, platform)
138
+ "#{name}-#{version}-#{platform}.gem"
139
+ end
140
+
141
+ def libv8_download(name, version, platform)
142
+ FileUtils.mkdir_p(vendor_path)
143
+ body = http_get(libv8_download_uri(name, version, platform))
144
+ File.open(File.join(vendor_path, libv8_downloaded_gem(name, version, platform)), 'wb') { |f| f.write(body) }
145
+ end
146
+
147
+ def libv8_vendor!
148
+ version = libv8_remote_search
149
+
150
+ puts "downloading #{libv8_downloaded_gem(libv8_gem_name, version['number'], version['platform'])} to #{vendor_path}"
151
+ libv8_download(libv8_gem_name, version['number'], version['platform'])
152
+
153
+ package = Gem::Package.new(File.join(vendor_path, libv8_downloaded_gem(libv8_gem_name, version['number'], version['platform'])))
154
+ package.extract_files(File.join(vendor_path, File.basename(libv8_downloaded_gem(libv8_gem_name, version['number'], version['platform']), '.gem')))
155
+
156
+ libv8_vendor_path
157
+ end
158
+
159
+ def ensure_libv8_load_path
160
+ puts "detected platform #{RUBY_PLATFORM} => #{ruby_platform}"
161
+
162
+ libv8_path = libv8_local_path || libv8_vendor_path || libv8_vendor!
163
+
164
+ abort(<<-ERROR) unless libv8_path
165
+ ERROR: could not find #{libv8_gem_name}
166
+ ERROR
167
+
168
+ $LOAD_PATH.unshift(File.join(libv8_path, 'ext'))
169
+ $LOAD_PATH.unshift(File.join(libv8_path, 'lib'))
170
+ end
171
+
172
+ ensure_libv8_load_path
173
+
174
+ require 'libv8'
175
+
176
+ IS_DARWIN = RUBY_PLATFORM =~ /darwin/
6
177
 
7
178
  have_library('pthread')
8
179
  have_library('objc') if IS_DARWIN
9
- $CPPFLAGS.gsub! /-std=[^\s]+/, ''
180
+ cppflags_clear_std!
10
181
  $CPPFLAGS += " -Wall" unless $CPPFLAGS.split.include? "-Wall"
11
182
  $CPPFLAGS += " -g" unless $CPPFLAGS.split.include? "-g"
12
183
  $CPPFLAGS += " -rdynamic" unless $CPPFLAGS.split.include? "-rdynamic"
13
184
  $CPPFLAGS += " -fPIC" unless $CPPFLAGS.split.include? "-rdynamic" or IS_DARWIN
14
185
  $CPPFLAGS += " -std=c++0x"
15
186
  $CPPFLAGS += " -fpermissive"
16
- $CPPFLAGS += " -fno-omit-frame-pointer"
17
- if enable_config('avx2')
18
- $CPPFLAGS += " -mavx2"
19
- else
20
- $CPPFLAGS += " -mssse3"
21
- end
187
+ cppflags_add_frame_pointer!
188
+ cppflags_add_cpu_extension!
22
189
 
23
190
  $CPPFLAGS += " -Wno-reserved-user-defined-literal" if IS_DARWIN
24
191
 
@@ -57,103 +224,15 @@ if CONFIG['warnflags']
57
224
  CONFIG['warnflags'].gsub!('-Wimplicit-function-declaration', '')
58
225
  end
59
226
 
60
- if enable_config('debug')
227
+ if enable_config('debug') || enable_config('asan')
61
228
  CONFIG['debugflags'] << ' -ggdb3 -O0'
62
229
  end
63
230
 
64
- def fixup_libtinfo
65
- dirs = %w[/lib64 /usr/lib64 /lib /usr/lib]
66
- found_v5 = dirs.map { |d| "#{d}/libtinfo.so.5" }.find &File.method(:file?)
67
- return '' if found_v5
68
- found_v6 = dirs.map { |d| "#{d}/libtinfo.so.6" }.find &File.method(:file?)
69
- return '' unless found_v6
70
- FileUtils.ln_s found_v6, 'gemdir/libtinfo.so.5', :force => true
71
- "LD_LIBRARY_PATH='#{File.expand_path('gemdir')}:#{ENV['LD_LIBRARY_PATH']}'"
72
- end
73
-
74
- def libv8_gem_name
75
- return "libv8-solaris" if IS_SOLARIS
76
-
77
- is_musl = false
78
- begin
79
- is_musl = !!(File.read('/proc/self/maps') =~ /ld-musl-x86_64/)
80
- rescue; end
81
-
82
- is_musl ? 'libv8-alpine' : 'libv8'
83
- end
84
-
85
- # 1) old rubygem versions prefer source gems to binary ones
86
- # ... and their --platform switch is broken too, as it leaves the 'ruby'
87
- # platform in Gem.platforms.
88
- # 2) the ruby binaries distributed with alpine (platform ending in -musl)
89
- # refuse to load binary gems by default
90
- def force_platform_gem
91
- gem_version = `gem --version`
92
- return 'gem' unless $?.success?
93
-
94
- if RUBY_PLATFORM != 'x86_64-linux-musl'
95
- return 'gem' if gem_version.to_f.zero? || gem_version.to_f >= 2.3
96
- return 'gem' if RUBY_PLATFORM != 'x86_64-linux'
97
- end
98
-
99
- gem_binary = `which gem`
100
- return 'gem' unless $?.success?
101
-
102
- ruby = File.foreach(gem_binary.strip).first.sub(/^#!/, '').strip
103
- unless File.file? ruby
104
- warn "No valid ruby: #{ruby}"
105
- return 'gem'
106
- end
107
-
108
- require 'tempfile'
109
- file = Tempfile.new('sq_mini_racer')
110
- file << <<EOS
111
- require 'rubygems'
112
- platforms = Gem.platforms
113
- platforms.reject! { |it| it == 'ruby' }
114
- if platforms.empty?
115
- platforms << Gem::Platform.new('x86_64-linux')
116
- end
117
- Gem.send(:define_method, :platforms) { platforms }
118
- #{IO.read(gem_binary.strip)}
119
- EOS
120
- file.close
121
- "#{ruby} '#{file.path}'"
122
- end
123
-
124
- LIBV8_VERSION = '6.7.288.46.1'
125
- libv8_rb = Dir.glob('**/libv8.rb').first
126
- FileUtils.mkdir_p('gemdir')
127
- unless libv8_rb
128
- gem_name = libv8_gem_name
129
- cmd = "#{fixup_libtinfo} #{force_platform_gem} install --version '= #{LIBV8_VERSION}' --install-dir gemdir #{gem_name}"
130
- puts "Will try downloading #{gem_name} gem: #{cmd}"
131
- `#{cmd}`
132
- unless $?.success?
133
- warn <<EOS
134
-
135
- WARNING: Could not download a private copy of the libv8 gem. Please make
136
- sure that you have internet access and that the `gem` binary is available.
137
-
138
- EOS
139
- end
140
-
141
- libv8_rb = Dir.glob('**/libv8.rb').first
142
- unless libv8_rb
143
- warn <<EOS
144
-
145
- WARNING: Could not find libv8 after the local copy of libv8 having supposedly
146
- been installed.
147
-
148
- EOS
149
- end
150
- end
231
+ Libv8.configure_makefile
151
232
 
152
- if libv8_rb
153
- $:.unshift(File.dirname(libv8_rb) + '/../ext')
154
- $:.unshift File.dirname(libv8_rb)
233
+ if enable_config('asan')
234
+ $CPPFLAGS.insert(0, " -fsanitize=address ")
235
+ $LDFLAGS.insert(0, " -fsanitize=address ")
155
236
  end
156
237
 
157
- require 'libv8'
158
- Libv8.configure_makefile
159
238
  create_makefile 'sq_mini_racer_extension'
@@ -23,7 +23,9 @@
23
23
  #if RUBY_API_VERSION_MAJOR > 1
24
24
  #include <ruby/thread.h>
25
25
  #endif
26
+ #include <ruby/io.h>
26
27
  #include <v8.h>
28
+ #include <v8-profiler.h>
27
29
  #include <libplatform/libplatform.h>
28
30
  #include <ruby/encoding.h>
29
31
  #include <pthread.h>
@@ -174,7 +176,7 @@ static VALUE rb_mJSON;
174
176
  static VALUE rb_cFailedV8Conversion;
175
177
  static VALUE rb_cDateTime = Qnil;
176
178
 
177
- static Platform* current_platform = NULL;
179
+ static std::unique_ptr<Platform> current_platform = NULL;
178
180
  static std::mutex platform_lock;
179
181
 
180
182
  static VALUE rb_platform_set_flag_as_str(VALUE _klass, VALUE flag_as_str) {
@@ -211,8 +213,8 @@ static void init_v8() {
211
213
 
212
214
  if (current_platform == NULL) {
213
215
  V8::InitializeICU();
214
- current_platform = platform::CreateDefaultPlatform();
215
- V8::InitializePlatform(current_platform);
216
+ current_platform = platform::NewDefaultPlatform();
217
+ V8::InitializePlatform(current_platform.get());
216
218
  V8::Initialize();
217
219
  }
218
220
 
@@ -254,16 +256,16 @@ static void prepare_result(MaybeLocal<Value> v8res,
254
256
  Local<Value> local_value = v8res.ToLocalChecked();
255
257
  if ((local_value->IsObject() || local_value->IsArray()) &&
256
258
  !local_value->IsDate() && !local_value->IsFunction()) {
257
- Local<Object> JSON = context->Global()->Get(
258
- String::NewFromUtf8(isolate, "JSON"))->ToObject();
259
+ Local<Object> JSON = context->Global()->Get(String::NewFromUtf8(isolate, "JSON"))
260
+ ->ToObject(context).ToLocalChecked();
259
261
 
260
262
  Local<Function> stringify = JSON->Get(v8::String::NewFromUtf8(isolate, "stringify"))
261
263
  .As<Function>();
262
264
 
263
- Local<Object> object = local_value->ToObject();
265
+ Local<Object> object = local_value->ToObject(context).ToLocalChecked();
264
266
  const unsigned argc = 1;
265
267
  Local<Value> argv[argc] = { object };
266
- MaybeLocal<Value> json = stringify->Call(JSON, argc, argv);
268
+ MaybeLocal<Value> json = stringify->Call(context, JSON, argc, argv);
267
269
 
268
270
  if (json.IsEmpty()) {
269
271
  evalRes.executed = false;
@@ -287,11 +289,21 @@ static void prepare_result(MaybeLocal<Value> v8res,
287
289
  evalRes.message = new Persistent<Value>();
288
290
  Local<Message> message = trycatch.Message();
289
291
  char buf[1000];
290
- int len;
292
+ int len, line, column;
293
+
294
+ if (!message->GetLineNumber(context).To(&line)) {
295
+ line = 0;
296
+ }
297
+
298
+ if (!message->GetStartColumn(context).To(&column)) {
299
+ column = 0;
300
+ }
301
+
291
302
  len = snprintf(buf, sizeof(buf), "%s at %s:%i:%i", *String::Utf8Value(isolate, message->Get()),
292
- *String::Utf8Value(isolate, message->GetScriptResourceName()->ToString()),
293
- message->GetLineNumber(),
294
- message->GetStartColumn());
303
+ *String::Utf8Value(isolate, message->GetScriptResourceName()->ToString(context).ToLocalChecked()),
304
+ line,
305
+ column);
306
+
295
307
  if ((size_t) len >= sizeof(buf)) {
296
308
  len = sizeof(buf) - 1;
297
309
  buf[len] = '\0';
@@ -307,7 +319,8 @@ static void prepare_result(MaybeLocal<Value> v8res,
307
319
  }
308
320
  if (!trycatch.StackTrace(context).IsEmpty()) {
309
321
  evalRes.backtrace = new Persistent<Value>();
310
- evalRes.backtrace->Reset(isolate, trycatch.StackTrace(context).ToLocalChecked()->ToString());
322
+ evalRes.backtrace->Reset(isolate,
323
+ trycatch.StackTrace(context).ToLocalChecked()->ToString(context).ToLocalChecked());
311
324
  }
312
325
  }
313
326
  }
@@ -391,11 +404,11 @@ static VALUE convert_v8_to_ruby(Isolate* isolate, Local<Context> context,
391
404
  }
392
405
 
393
406
  if (value->IsInt32()) {
394
- return INT2FIX(value->Int32Value());
407
+ return INT2FIX(value->Int32Value(context).ToChecked());
395
408
  }
396
409
 
397
410
  if (value->IsNumber()) {
398
- return rb_float_new(value->NumberValue());
411
+ return rb_float_new(value->NumberValue(context).ToChecked());
399
412
  }
400
413
 
401
414
  if (value->IsTrue()) {
@@ -436,7 +449,7 @@ static VALUE convert_v8_to_ruby(Isolate* isolate, Local<Context> context,
436
449
  VALUE rb_hash = rb_hash_new();
437
450
  TryCatch trycatch(isolate);
438
451
 
439
- Local<Object> object = value->ToObject();
452
+ Local<Object> object = value->ToObject(context).ToLocalChecked();
440
453
  auto maybe_props = object->GetOwnPropertyNames(context);
441
454
  if (!maybe_props.IsEmpty()) {
442
455
  Local<Array> props = maybe_props.ToLocalChecked();
@@ -459,8 +472,8 @@ static VALUE convert_v8_to_ruby(Isolate* isolate, Local<Context> context,
459
472
  return rb_hash;
460
473
  }
461
474
 
462
- Local<String> rstr = value->ToString();
463
- return rb_enc_str_new(*String::Utf8Value(isolate, rstr), rstr->Utf8Length(), rb_enc_find("utf-8"));
475
+ Local<String> rstr = value->ToString(context).ToLocalChecked();
476
+ return rb_enc_str_new(*String::Utf8Value(isolate, rstr), rstr->Utf8Length(isolate), rb_enc_find("utf-8"));
464
477
  }
465
478
 
466
479
  static VALUE convert_v8_to_ruby(Isolate* isolate,
@@ -557,7 +570,7 @@ treat_as_latin1:
557
570
  return v8str.ToLocalChecked();
558
571
  }
559
572
 
560
- static Local<Value> convert_ruby_to_v8(Isolate* isolate, VALUE value)
573
+ static Local<Value> convert_ruby_to_v8(Isolate* isolate, Local<Context> context, VALUE value)
561
574
  {
562
575
  EscapableHandleScope scope(isolate);
563
576
 
@@ -571,87 +584,86 @@ static Local<Value> convert_ruby_to_v8(Isolate* isolate, VALUE value)
571
584
  VALUE klass;
572
585
 
573
586
  switch (TYPE(value)) {
574
- case T_FIXNUM:
587
+ case T_FIXNUM:
575
588
  {
576
- fixnum = NUM2LONG(value);
577
- if (fixnum > INT_MAX)
578
- {
579
- return scope.Escape(Number::New(isolate, (double)fixnum));
580
- }
581
- return scope.Escape(Integer::New(isolate, (int)fixnum));
589
+ fixnum = NUM2LONG(value);
590
+ if (fixnum > INT_MAX)
591
+ {
592
+ return scope.Escape(Number::New(isolate, (double)fixnum));
593
+ }
594
+ return scope.Escape(Integer::New(isolate, (int)fixnum));
582
595
  }
583
- case T_FLOAT:
584
- return scope.Escape(Number::New(isolate, NUM2DBL(value)));
585
- case T_STRING:
596
+ case T_FLOAT:
597
+ return scope.Escape(Number::New(isolate, NUM2DBL(value)));
598
+ case T_STRING:
586
599
  return scope.Escape(convert_ruby_str_to_v8(scope, isolate, value));
587
- case T_NIL:
588
- return scope.Escape(Null(isolate));
589
- case T_TRUE:
590
- return scope.Escape(True(isolate));
591
- case T_FALSE:
592
- return scope.Escape(False(isolate));
593
- case T_ARRAY:
600
+ case T_NIL:
601
+ return scope.Escape(Null(isolate));
602
+ case T_TRUE:
603
+ return scope.Escape(True(isolate));
604
+ case T_FALSE:
605
+ return scope.Escape(False(isolate));
606
+ case T_ARRAY:
594
607
  {
595
- length = RARRAY_LEN(value);
596
- array = Array::New(isolate, (int)length);
597
- for(i=0; i<length; i++) {
598
- array->Set(i, convert_ruby_to_v8(isolate, rb_ary_entry(value, i)));
599
- }
600
- return scope.Escape(array);
608
+ length = RARRAY_LEN(value);
609
+ array = Array::New(isolate, (int)length);
610
+ for(i=0; i<length; i++) {
611
+ array->Set(i, convert_ruby_to_v8(isolate, context, rb_ary_entry(value, i)));
612
+ }
613
+ return scope.Escape(array);
601
614
  }
602
- case T_HASH:
615
+ case T_HASH:
603
616
  {
604
- object = Object::New(isolate);
605
- hash_as_array = rb_funcall(value, rb_intern("to_a"), 0);
606
- length = RARRAY_LEN(hash_as_array);
607
- for(i=0; i<length; i++) {
608
- pair = rb_ary_entry(hash_as_array, i);
609
- object->Set(convert_ruby_to_v8(isolate, rb_ary_entry(pair, 0)),
610
- convert_ruby_to_v8(isolate, rb_ary_entry(pair, 1)));
611
- }
612
- return scope.Escape(object);
617
+ object = Object::New(isolate);
618
+ hash_as_array = rb_funcall(value, rb_intern("to_a"), 0);
619
+ length = RARRAY_LEN(hash_as_array);
620
+ for(i=0; i<length; i++) {
621
+ pair = rb_ary_entry(hash_as_array, i);
622
+ object->Set(convert_ruby_to_v8(isolate, context, rb_ary_entry(pair, 0)),
623
+ convert_ruby_to_v8(isolate, context, rb_ary_entry(pair, 1)));
624
+ }
625
+ return scope.Escape(object);
613
626
  }
614
- case T_SYMBOL:
627
+ case T_SYMBOL:
615
628
  {
616
- value = rb_funcall(value, rb_intern("to_s"), 0);
629
+ value = rb_funcall(value, rb_intern("to_s"), 0);
617
630
  return scope.Escape(convert_ruby_str_to_v8(scope, isolate, value));
618
631
  }
619
- case T_DATA:
632
+ case T_DATA:
633
+ {
634
+ klass = rb_funcall(value, rb_intern("class"), 0);
635
+ if (klass == rb_cTime || klass == rb_cDateTime)
620
636
  {
621
- klass = rb_funcall(value, rb_intern("class"), 0);
622
- if (klass == rb_cTime || klass == rb_cDateTime)
637
+ if (klass == rb_cDateTime)
623
638
  {
624
- if (klass == rb_cDateTime)
625
- {
626
- value = rb_funcall(value, rb_intern("to_time"), 0);
627
- }
628
- value = rb_funcall(value, rb_intern("to_f"), 0);
629
- return scope.Escape(Date::New(isolate, NUM2DBL(value) * 1000));
639
+ value = rb_funcall(value, rb_intern("to_time"), 0);
630
640
  }
641
+ value = rb_funcall(value, rb_intern("to_f"), 0);
642
+ return scope.Escape(Date::New(context, NUM2DBL(value) * 1000).ToLocalChecked());
643
+ }
631
644
  // break intentionally missing
632
645
  }
633
- case T_OBJECT:
634
- case T_CLASS:
635
- case T_ICLASS:
636
- case T_MODULE:
637
- case T_REGEXP:
638
- case T_MATCH:
639
- case T_STRUCT:
640
- case T_BIGNUM:
641
- case T_FILE:
642
- case T_UNDEF:
643
- case T_NODE:
644
- default:
646
+ case T_OBJECT:
647
+ case T_CLASS:
648
+ case T_ICLASS:
649
+ case T_MODULE:
650
+ case T_REGEXP:
651
+ case T_MATCH:
652
+ case T_STRUCT:
653
+ case T_BIGNUM:
654
+ case T_FILE:
655
+ case T_UNDEF:
656
+ case T_NODE:
657
+ default:
645
658
  {
646
659
  if (rb_respond_to(value, rb_intern("to_s"))) {
647
660
  // TODO: if this throws we're screwed
648
661
  value = rb_funcall(value, rb_intern("to_s"), 0);
649
662
  return scope.Escape(convert_ruby_str_to_v8(scope, isolate, value));
650
663
  }
651
- return scope.Escape(String::NewFromUtf8(isolate, "Undefined Conversion"));
652
- }
664
+ return scope.Escape(String::NewFromUtf8(isolate, "Undefined Conversion"));
665
+ }
653
666
  }
654
-
655
667
  }
656
668
 
657
669
  static void unblock_eval(void *ptr) {
@@ -659,6 +671,91 @@ static void unblock_eval(void *ptr) {
659
671
  eval->context_info->isolate_info->interrupted = true;
660
672
  }
661
673
 
674
+ /*
675
+ * The implementations of the run_extra_code(), create_snapshot_data_blob() and
676
+ * warm_up_snapshot_data_blob() functions have been derived from V8's test suite.
677
+ */
678
+ bool run_extra_code(Isolate *isolate, Local<v8::Context> context,
679
+ const char *utf8_source, const char *name) {
680
+ Context::Scope context_scope(context);
681
+ TryCatch try_catch(isolate);
682
+ Local<String> source_string;
683
+ if (!String::NewFromUtf8(isolate, utf8_source,
684
+ NewStringType::kNormal)
685
+ .ToLocal(&source_string)) {
686
+ return false;
687
+ }
688
+ Local<v8::String> resource_name =
689
+ String::NewFromUtf8(isolate, name, NewStringType::kNormal)
690
+ .ToLocalChecked();
691
+ ScriptOrigin origin(resource_name);
692
+ ScriptCompiler::Source source(source_string, origin);
693
+ Local<Script> script;
694
+ if (!ScriptCompiler::Compile(context, &source).ToLocal(&script))
695
+ return false;
696
+ if (script->Run(context).IsEmpty())
697
+ return false;
698
+ // CHECK(!try_catch.HasCaught());
699
+ return true;
700
+ }
701
+
702
+ StartupData
703
+ create_snapshot_data_blob(const char *embedded_source = nullptr) {
704
+ // Create a new isolate and a new context from scratch, optionally run
705
+ // a script to embed, and serialize to create a snapshot blob.
706
+ StartupData result = {nullptr, 0};
707
+ {
708
+ SnapshotCreator snapshot_creator;
709
+ Isolate *isolate = snapshot_creator.GetIsolate();
710
+ {
711
+ HandleScope scope(isolate);
712
+ Local<Context> context = Context::New(isolate);
713
+ if (embedded_source != nullptr &&
714
+ !run_extra_code(isolate, context, embedded_source,
715
+ "<embedded>")) {
716
+ return result;
717
+ }
718
+ snapshot_creator.SetDefaultContext(context);
719
+ }
720
+ result = snapshot_creator.CreateBlob(
721
+ SnapshotCreator::FunctionCodeHandling::kClear);
722
+ }
723
+ return result;
724
+ }
725
+
726
+ StartupData warm_up_snapshot_data_blob(StartupData cold_snapshot_blob,
727
+ const char *warmup_source) {
728
+ // Use following steps to create a warmed up snapshot blob from a cold one:
729
+ // - Create a new isolate from the cold snapshot.
730
+ // - Create a new context to run the warmup script. This will trigger
731
+ // compilation of executed functions.
732
+ // - Create a new context. This context will be unpolluted.
733
+ // - Serialize the isolate and the second context into a new snapshot blob.
734
+ StartupData result = {nullptr, 0};
735
+
736
+ if (cold_snapshot_blob.raw_size > 0 && cold_snapshot_blob.data != nullptr &&
737
+ warmup_source != NULL) {
738
+ SnapshotCreator snapshot_creator(nullptr, &cold_snapshot_blob);
739
+ Isolate *isolate = snapshot_creator.GetIsolate();
740
+ {
741
+ HandleScope scope(isolate);
742
+ Local<Context> context = Context::New(isolate);
743
+ if (!run_extra_code(isolate, context, warmup_source, "<warm-up>")) {
744
+ return result;
745
+ }
746
+ }
747
+ {
748
+ HandleScope handle_scope(isolate);
749
+ isolate->ContextDisposedNotification(false);
750
+ Local<Context> context = Context::New(isolate);
751
+ snapshot_creator.SetDefaultContext(context);
752
+ }
753
+ result = snapshot_creator.CreateBlob(
754
+ SnapshotCreator::FunctionCodeHandling::kKeep);
755
+ }
756
+ return result;
757
+ }
758
+
662
759
  static VALUE rb_snapshot_size(VALUE self, VALUE str) {
663
760
  SnapshotInfo* snapshot_info;
664
761
  Data_Get_Struct(self, SnapshotInfo, snapshot_info);
@@ -677,7 +774,7 @@ static VALUE rb_snapshot_load(VALUE self, VALUE str) {
677
774
 
678
775
  init_v8();
679
776
 
680
- StartupData startup_data = V8::CreateSnapshotDataBlob(RSTRING_PTR(str));
777
+ StartupData startup_data = create_snapshot_data_blob(RSTRING_PTR(str));
681
778
 
682
779
  if (startup_data.data == NULL && startup_data.raw_size == 0) {
683
780
  rb_raise(rb_eSnapshotError, "Could not create snapshot, most likely the source is incorrect");
@@ -708,7 +805,7 @@ static VALUE rb_snapshot_warmup_unsafe(VALUE self, VALUE str) {
708
805
  init_v8();
709
806
 
710
807
  StartupData cold_startup_data = {snapshot_info->data, snapshot_info->raw_size};
711
- StartupData warm_startup_data = V8::WarmUpSnapshotDataBlob(cold_startup_data, RSTRING_PTR(str));
808
+ StartupData warm_startup_data = warm_up_snapshot_data_blob(cold_startup_data, RSTRING_PTR(str));
712
809
 
713
810
  if (warm_startup_data.data == NULL && warm_startup_data.raw_size == 0) {
714
811
  rb_raise(rb_eSnapshotError, "Could not warm up snapshot, most likely the source is incorrect");
@@ -887,8 +984,8 @@ static VALUE convert_result_to_ruby(VALUE self /* context */,
887
984
  Local<Value> tmp = Local<Value>::New(isolate, *result.value);
888
985
 
889
986
  if (result.json) {
890
- Local<String> rstr = tmp->ToString();
891
- VALUE json_string = rb_enc_str_new(*String::Utf8Value(isolate, rstr), rstr->Utf8Length(), rb_enc_find("utf-8"));
987
+ Local<String> rstr = tmp->ToString(p_ctx->Get(isolate)).ToLocalChecked();
988
+ VALUE json_string = rb_enc_str_new(*String::Utf8Value(isolate, rstr), rstr->Utf8Length(isolate), rb_enc_find("utf-8"));
892
989
  ret = rb_funcall(rb_mJSON, rb_intern("parse"), 1, json_string);
893
990
  } else {
894
991
  ret = convert_v8_to_ruby(isolate, *p_ctx, tmp);
@@ -1009,6 +1106,8 @@ gvl_ruby_callback(void* data) {
1009
1106
  VALUE result;
1010
1107
  VALUE self;
1011
1108
  VALUE parent;
1109
+ ContextInfo* context_info;
1110
+
1012
1111
  {
1013
1112
  HandleScope scope(args->GetIsolate());
1014
1113
  Local<External> external = Local<External>::Cast(args->Data());
@@ -1021,7 +1120,6 @@ gvl_ruby_callback(void* data) {
1021
1120
  return NULL;
1022
1121
  }
1023
1122
 
1024
- ContextInfo* context_info;
1025
1123
  Data_Get_Struct(parent, ContextInfo, context_info);
1026
1124
 
1027
1125
  if (length > 0) {
@@ -1031,7 +1129,7 @@ gvl_ruby_callback(void* data) {
1031
1129
  for (int i = 0; i < length; i++) {
1032
1130
  Local<Value> value = ((*args)[i]).As<Value>();
1033
1131
  VALUE tmp = convert_v8_to_ruby(args->GetIsolate(),
1034
- *context_info->context, value);
1132
+ *context_info->context, value);
1035
1133
  rb_ary_push(ruby_args, tmp);
1036
1134
  }
1037
1135
  }
@@ -1062,7 +1160,7 @@ gvl_ruby_callback(void* data) {
1062
1160
  }
1063
1161
  else {
1064
1162
  HandleScope scope(args->GetIsolate());
1065
- Handle<Value> v8_result = convert_ruby_to_v8(args->GetIsolate(), result);
1163
+ Handle<Value> v8_result = convert_ruby_to_v8(args->GetIsolate(), context_info->context->Get(args->GetIsolate()), result);
1066
1164
  args->GetReturnValue().Set(v8_result);
1067
1165
  }
1068
1166
 
@@ -1119,8 +1217,10 @@ static VALUE rb_external_function_notify_v8(VALUE self) {
1119
1217
  Local<Context> context = context_info->context->Get(isolate);
1120
1218
  Context::Scope context_scope(context);
1121
1219
 
1122
- Local<String> v8_str = String::NewFromUtf8(isolate, RSTRING_PTR(name),
1123
- NewStringType::kNormal, (int)RSTRING_LEN(name)).ToLocalChecked();
1220
+ Local<String> v8_str =
1221
+ String::NewFromUtf8(isolate, RSTRING_PTR(name),
1222
+ NewStringType::kNormal, (int)RSTRING_LEN(name))
1223
+ .ToLocalChecked();
1124
1224
 
1125
1225
  // copy self so we can access from v8 external
1126
1226
  VALUE* self_copy;
@@ -1130,23 +1230,34 @@ static VALUE rb_external_function_notify_v8(VALUE self) {
1130
1230
  Local<Value> external = External::New(isolate, self_copy);
1131
1231
 
1132
1232
  if (parent_object == Qnil) {
1133
- context->Global()->Set(v8_str, FunctionTemplate::New(isolate, ruby_callback, external)->GetFunction());
1134
- } else {
1233
+ context->Global()->Set(
1234
+ v8_str, FunctionTemplate::New(isolate, ruby_callback, external)
1235
+ ->GetFunction(context)
1236
+ .ToLocalChecked());
1135
1237
 
1136
- Local<String> eval = String::NewFromUtf8(isolate, RSTRING_PTR(parent_object_eval),
1137
- NewStringType::kNormal, (int)RSTRING_LEN(parent_object_eval)).ToLocalChecked();
1238
+ } else {
1239
+ Local<String> eval =
1240
+ String::NewFromUtf8(isolate, RSTRING_PTR(parent_object_eval),
1241
+ NewStringType::kNormal,
1242
+ (int)RSTRING_LEN(parent_object_eval))
1243
+ .ToLocalChecked();
1138
1244
 
1139
1245
  MaybeLocal<Script> parsed_script = Script::Compile(context, eval);
1140
1246
  if (parsed_script.IsEmpty()) {
1141
1247
  parse_error = true;
1142
1248
  } else {
1143
- MaybeLocal<Value> maybe_value = parsed_script.ToLocalChecked()->Run(context);
1249
+ MaybeLocal<Value> maybe_value =
1250
+ parsed_script.ToLocalChecked()->Run(context);
1144
1251
  attach_error = true;
1145
1252
 
1146
1253
  if (!maybe_value.IsEmpty()) {
1147
1254
  Local<Value> value = maybe_value.ToLocalChecked();
1148
- if (value->IsObject()){
1149
- value.As<Object>()->Set(v8_str, FunctionTemplate::New(isolate, ruby_callback, external)->GetFunction());
1255
+ if (value->IsObject()) {
1256
+ value.As<Object>()->Set(
1257
+ v8_str, FunctionTemplate::New(
1258
+ isolate, ruby_callback, external)
1259
+ ->GetFunction(context)
1260
+ .ToLocalChecked());
1150
1261
  attach_error = false;
1151
1262
  }
1152
1263
  }
@@ -1350,6 +1461,69 @@ rb_heap_stats(VALUE self) {
1350
1461
  return rval;
1351
1462
  }
1352
1463
 
1464
+ // https://github.com/bnoordhuis/node-heapdump/blob/master/src/heapdump.cc
1465
+ class FileOutputStream : public OutputStream {
1466
+ public:
1467
+ FileOutputStream(FILE* stream) : stream_(stream) {}
1468
+
1469
+ virtual int GetChunkSize() {
1470
+ return 65536;
1471
+ }
1472
+
1473
+ virtual void EndOfStream() {}
1474
+
1475
+ virtual WriteResult WriteAsciiChunk(char* data, int size) {
1476
+ const size_t len = static_cast<size_t>(size);
1477
+ size_t off = 0;
1478
+
1479
+ while (off < len && !feof(stream_) && !ferror(stream_))
1480
+ off += fwrite(data + off, 1, len - off, stream_);
1481
+
1482
+ return off == len ? kContinue : kAbort;
1483
+ }
1484
+
1485
+ private:
1486
+ FILE* stream_;
1487
+ };
1488
+
1489
+
1490
+ static VALUE
1491
+ rb_heap_snapshot(VALUE self, VALUE file) {
1492
+
1493
+ rb_io_t *fptr;
1494
+
1495
+ fptr = RFILE(file)->fptr;
1496
+
1497
+ if (!fptr) return Qfalse;
1498
+
1499
+ FILE* fp;
1500
+ fp = fdopen(fptr->fd, "w");
1501
+ if (fp == NULL) return Qfalse;
1502
+
1503
+
1504
+ ContextInfo* context_info;
1505
+ Data_Get_Struct(self, ContextInfo, context_info);
1506
+ Isolate* isolate;
1507
+ isolate = context_info->isolate_info ? context_info->isolate_info->isolate : NULL;
1508
+
1509
+ if (!isolate) return Qfalse;
1510
+
1511
+ Locker lock(isolate);
1512
+ Isolate::Scope isolate_scope(isolate);
1513
+ HandleScope handle_scope(isolate);
1514
+
1515
+ HeapProfiler* heap_profiler = isolate->GetHeapProfiler();
1516
+
1517
+ const HeapSnapshot* const snap = heap_profiler->TakeHeapSnapshot();
1518
+
1519
+ FileOutputStream stream(fp);
1520
+ snap->Serialize(&stream, HeapSnapshot::kJSON);
1521
+
1522
+ const_cast<HeapSnapshot*>(snap)->Delete();
1523
+
1524
+ return Qtrue;
1525
+ }
1526
+
1353
1527
  static VALUE
1354
1528
  rb_context_stop(VALUE self) {
1355
1529
 
@@ -1494,7 +1668,7 @@ static VALUE rb_context_call_unsafe(int argc, VALUE *argv, VALUE self) {
1494
1668
  return Qnil;
1495
1669
  }
1496
1670
  for(int i=0; i < fun_argc; i++) {
1497
- call.argv[i] = convert_ruby_to_v8(isolate, call_argv[i]);
1671
+ call.argv[i] = convert_ruby_to_v8(isolate, context, call_argv[i]);
1498
1672
  }
1499
1673
  }
1500
1674
  #if RUBY_API_VERSION_MAJOR > 1
@@ -1558,6 +1732,8 @@ extern "C" {
1558
1732
  rb_define_method(rb_cContext, "dispose_unsafe", (VALUE(*)(...))&rb_context_dispose, 0);
1559
1733
  rb_define_method(rb_cContext, "low_memory_notification", (VALUE(*)(...))&rb_context_low_memory_notification, 0);
1560
1734
  rb_define_method(rb_cContext, "heap_stats", (VALUE(*)(...))&rb_heap_stats, 0);
1735
+ rb_define_method(rb_cContext, "write_heap_snapshot_unsafe", (VALUE(*)(...))&rb_heap_snapshot, 1);
1736
+
1561
1737
  rb_define_private_method(rb_cContext, "create_isolate_value",(VALUE(*)(...))&rb_context_create_isolate_value, 0);
1562
1738
  rb_define_private_method(rb_cContext, "eval_unsafe",(VALUE(*)(...))&rb_context_eval_unsafe, 2);
1563
1739
  rb_define_private_method(rb_cContext, "call_unsafe", (VALUE(*)(...))&rb_context_call_unsafe, -1);
@@ -178,6 +178,28 @@ module MiniRacer
178
178
  eval(File.read(filename))
179
179
  end
180
180
 
181
+ def write_heap_snapshot(file_or_io)
182
+ f = nil
183
+ implicit = false
184
+
185
+
186
+ if String === file_or_io
187
+ f = File.open(file_or_io, "w")
188
+ implicit = true
189
+ else
190
+ f = file_or_io
191
+ end
192
+
193
+ if !(File === f)
194
+ raise ArgumentError("file_or_io")
195
+ end
196
+
197
+ write_heap_snapshot_unsafe(f)
198
+
199
+ ensure
200
+ f.close if implicit
201
+ end
202
+
181
203
  def eval(str, options=nil)
182
204
  raise(ContextDisposedError, 'attempted to call eval on a disposed context!') if @disposed
183
205
 
@@ -2,6 +2,6 @@ module Sqreen
2
2
  module MiniRacer
3
3
  # part before qualifier is the number of the last upstream release
4
4
  # since we synced with it
5
- VERSION = "0.2.4.sqreen3"
5
+ VERSION = "0.2.5.0.1.beta1"
6
6
  end
7
7
  end
@@ -15,7 +15,7 @@ Gem::Specification.new do |spec|
15
15
  spec.license = "MIT"
16
16
 
17
17
 
18
- REJECTS = %r{\A((benchmark|test|spec|features)/|bench\.rb|.+\.sh|Jenkinsfile)}
18
+ REJECTS = %r{\A((benchmark|test|spec|features|examples)/|bench\.rb|.+\.sh|Jenkinsfile)}
19
19
  spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(REJECTS) }
20
20
  spec.bindir = "exe"
21
21
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sq_mini_racer
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.4.sqreen3
4
+ version: 0.2.5.0.1.beta1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sam Saffron
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2019-04-19 00:00:00.000000000 Z
11
+ date: 2019-08-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler