bootsnap 1.4.5 → 1.18.3

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 (48) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +264 -0
  3. data/LICENSE.txt +1 -1
  4. data/README.md +63 -23
  5. data/exe/bootsnap +5 -0
  6. data/ext/bootsnap/bootsnap.c +504 -184
  7. data/ext/bootsnap/extconf.rb +30 -15
  8. data/lib/bootsnap/bundler.rb +3 -1
  9. data/lib/bootsnap/cli/worker_pool.rb +136 -0
  10. data/lib/bootsnap/cli.rb +283 -0
  11. data/lib/bootsnap/compile_cache/iseq.rb +72 -21
  12. data/lib/bootsnap/compile_cache/json.rb +89 -0
  13. data/lib/bootsnap/compile_cache/yaml.rb +316 -41
  14. data/lib/bootsnap/compile_cache.rb +27 -17
  15. data/lib/bootsnap/explicit_require.rb +5 -3
  16. data/lib/bootsnap/load_path_cache/cache.rb +73 -37
  17. data/lib/bootsnap/load_path_cache/change_observer.rb +25 -3
  18. data/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb +27 -82
  19. data/lib/bootsnap/load_path_cache/core_ext/loaded_features.rb +2 -0
  20. data/lib/bootsnap/load_path_cache/loaded_features_index.rb +63 -29
  21. data/lib/bootsnap/load_path_cache/path.rb +42 -19
  22. data/lib/bootsnap/load_path_cache/path_scanner.rb +60 -29
  23. data/lib/bootsnap/load_path_cache/store.rb +64 -23
  24. data/lib/bootsnap/load_path_cache.rb +40 -38
  25. data/lib/bootsnap/setup.rb +3 -36
  26. data/lib/bootsnap/version.rb +3 -1
  27. data/lib/bootsnap.rb +141 -36
  28. metadata +15 -99
  29. data/.github/CODEOWNERS +0 -2
  30. data/.github/probots.yml +0 -2
  31. data/.gitignore +0 -17
  32. data/.rubocop.yml +0 -20
  33. data/.travis.yml +0 -21
  34. data/CODE_OF_CONDUCT.md +0 -74
  35. data/CONTRIBUTING.md +0 -21
  36. data/Gemfile +0 -8
  37. data/README.jp.md +0 -231
  38. data/Rakefile +0 -12
  39. data/bin/ci +0 -10
  40. data/bin/console +0 -14
  41. data/bin/setup +0 -8
  42. data/bin/test-minimal-support +0 -7
  43. data/bin/testunit +0 -8
  44. data/bootsnap.gemspec +0 -45
  45. data/dev.yml +0 -10
  46. data/lib/bootsnap/load_path_cache/core_ext/active_support.rb +0 -106
  47. data/lib/bootsnap/load_path_cache/realpath_cache.rb +0 -32
  48. data/shipit.rubygems.yml +0 -0
@@ -1,18 +1,33 @@
1
- require("mkmf")
2
- $CFLAGS << ' -O3 '
3
- $CFLAGS << ' -std=c99'
1
+ # frozen_string_literal: true
4
2
 
5
- # ruby.h has some -Wpedantic fails in some cases
6
- # (e.g. https://github.com/Shopify/bootsnap/issues/15)
7
- unless ['0', '', nil].include?(ENV['BOOTSNAP_PEDANTIC'])
8
- $CFLAGS << ' -Wall'
9
- $CFLAGS << ' -Werror'
10
- $CFLAGS << ' -Wextra'
11
- $CFLAGS << ' -Wpedantic'
3
+ require "mkmf"
12
4
 
13
- $CFLAGS << ' -Wno-unused-parameter' # VALUE self has to be there but we don't care what it is.
14
- $CFLAGS << ' -Wno-keyword-macro' # hiding return
15
- $CFLAGS << ' -Wno-gcc-compat' # ruby.h 2.6.0 on macos 10.14, dunno
16
- end
5
+ if %w[ruby truffleruby].include?(RUBY_ENGINE)
6
+ have_func "fdatasync", "unistd.h"
7
+
8
+ unless RUBY_PLATFORM.match?(/mswin|mingw|cygwin/)
9
+ append_cppflags ["-D_GNU_SOURCE"] # Needed of O_NOATIME
10
+ end
11
+
12
+ append_cflags ["-O3", "-std=c99"]
17
13
 
18
- create_makefile("bootsnap/bootsnap")
14
+ # ruby.h has some -Wpedantic fails in some cases
15
+ # (e.g. https://github.com/Shopify/bootsnap/issues/15)
16
+ unless ["0", "", nil].include?(ENV["BOOTSNAP_PEDANTIC"])
17
+ append_cflags([
18
+ "-Wall",
19
+ "-Werror",
20
+ "-Wextra",
21
+ "-Wpedantic",
22
+
23
+ "-Wno-unused-parameter", # VALUE self has to be there but we don't care what it is.
24
+ "-Wno-keyword-macro", # hiding return
25
+ "-Wno-gcc-compat", # ruby.h 2.6.0 on macos 10.14, dunno
26
+ "-Wno-compound-token-split-by-macro",
27
+ ])
28
+ end
29
+
30
+ create_makefile("bootsnap/bootsnap")
31
+ else
32
+ File.write("Makefile", dummy_makefile($srcdir).join)
33
+ end
@@ -1,5 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Bootsnap
2
- extend(self)
4
+ extend self
3
5
 
4
6
  def bundler?
5
7
  return false unless defined?(::Bundler)
@@ -0,0 +1,136 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bootsnap
4
+ class CLI
5
+ class WorkerPool
6
+ class << self
7
+ def create(size:, jobs:)
8
+ if size > 0 && Process.respond_to?(:fork)
9
+ new(size: size, jobs: jobs)
10
+ else
11
+ Inline.new(jobs: jobs)
12
+ end
13
+ end
14
+ end
15
+
16
+ class Inline
17
+ def initialize(jobs: {})
18
+ @jobs = jobs
19
+ end
20
+
21
+ def push(job, *args)
22
+ @jobs.fetch(job).call(*args)
23
+ nil
24
+ end
25
+
26
+ def spawn
27
+ # noop
28
+ end
29
+
30
+ def shutdown
31
+ # noop
32
+ end
33
+ end
34
+
35
+ class Worker
36
+ attr_reader :to_io, :pid
37
+
38
+ def initialize(jobs)
39
+ @jobs = jobs
40
+ @pipe_out, @to_io = IO.pipe(binmode: true)
41
+ # Set the writer encoding to binary since IO.pipe only sets it for the reader.
42
+ # https://github.com/rails/rails/issues/16514#issuecomment-52313290
43
+ @to_io.set_encoding(Encoding::BINARY)
44
+
45
+ @pid = nil
46
+ end
47
+
48
+ def write(message, block: true)
49
+ payload = Marshal.dump(message)
50
+ if block
51
+ to_io.write(payload)
52
+ true
53
+ else
54
+ to_io.write_nonblock(payload, exception: false) != :wait_writable
55
+ end
56
+ end
57
+
58
+ def close
59
+ to_io.close
60
+ end
61
+
62
+ def work_loop
63
+ loop do
64
+ job, *args = Marshal.load(@pipe_out)
65
+ return if job == :exit
66
+
67
+ @jobs.fetch(job).call(*args)
68
+ end
69
+ rescue IOError
70
+ nil
71
+ end
72
+
73
+ def spawn
74
+ @pid = Process.fork do
75
+ to_io.close
76
+ work_loop
77
+ exit!(0)
78
+ end
79
+ @pipe_out.close
80
+ true
81
+ end
82
+ end
83
+
84
+ def initialize(size:, jobs: {})
85
+ @size = size
86
+ @jobs = jobs
87
+ @queue = Queue.new
88
+ @pids = []
89
+ end
90
+
91
+ def spawn
92
+ @workers = @size.times.map { Worker.new(@jobs) }
93
+ @workers.each(&:spawn)
94
+ @dispatcher_thread = Thread.new { dispatch_loop }
95
+ @dispatcher_thread.abort_on_exception = true
96
+ true
97
+ end
98
+
99
+ def dispatch_loop
100
+ loop do
101
+ case job = @queue.pop
102
+ when nil
103
+ @workers.each do |worker|
104
+ worker.write([:exit])
105
+ worker.close
106
+ end
107
+ return true
108
+ else
109
+ unless @workers.sample.write(job, block: false)
110
+ free_worker.write(job)
111
+ end
112
+ end
113
+ end
114
+ end
115
+
116
+ def free_worker
117
+ IO.select(nil, @workers)[1].sample
118
+ end
119
+
120
+ def push(*args)
121
+ @queue.push(args)
122
+ nil
123
+ end
124
+
125
+ def shutdown
126
+ @queue.close
127
+ @dispatcher_thread.join
128
+ @workers.each do |worker|
129
+ _pid, status = Process.wait2(worker.pid)
130
+ return status.exitstatus unless status.success?
131
+ end
132
+ nil
133
+ end
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,283 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bootsnap"
4
+ require "bootsnap/cli/worker_pool"
5
+ require "optparse"
6
+ require "fileutils"
7
+ require "etc"
8
+
9
+ module Bootsnap
10
+ class CLI
11
+ unless Regexp.method_defined?(:match?)
12
+ module RegexpMatchBackport
13
+ refine Regexp do
14
+ def match?(string)
15
+ !!match(string)
16
+ end
17
+ end
18
+ end
19
+ using RegexpMatchBackport
20
+ end
21
+
22
+ attr_reader :cache_dir, :argv
23
+
24
+ attr_accessor :compile_gemfile, :exclude, :verbose, :iseq, :yaml, :json, :jobs
25
+
26
+ def initialize(argv)
27
+ @argv = argv
28
+ self.cache_dir = ENV.fetch("BOOTSNAP_CACHE_DIR", "tmp/cache")
29
+ self.compile_gemfile = false
30
+ self.exclude = nil
31
+ self.verbose = false
32
+ self.jobs = Etc.nprocessors
33
+ self.iseq = true
34
+ self.yaml = true
35
+ self.json = true
36
+ end
37
+
38
+ def precompile_command(*sources)
39
+ require "bootsnap/compile_cache/iseq"
40
+ require "bootsnap/compile_cache/yaml"
41
+ require "bootsnap/compile_cache/json"
42
+
43
+ fix_default_encoding do
44
+ Bootsnap::CompileCache::ISeq.cache_dir = cache_dir
45
+ Bootsnap::CompileCache::YAML.init!
46
+ Bootsnap::CompileCache::YAML.cache_dir = cache_dir
47
+ Bootsnap::CompileCache::JSON.init!
48
+ Bootsnap::CompileCache::JSON.cache_dir = cache_dir
49
+
50
+ @work_pool = WorkerPool.create(size: jobs, jobs: {
51
+ ruby: method(:precompile_ruby),
52
+ yaml: method(:precompile_yaml),
53
+ json: method(:precompile_json),
54
+ })
55
+ @work_pool.spawn
56
+
57
+ main_sources = sources.map { |d| File.expand_path(d) }
58
+ precompile_ruby_files(main_sources)
59
+ precompile_yaml_files(main_sources)
60
+ precompile_json_files(main_sources)
61
+
62
+ if compile_gemfile
63
+ # Gems that include JSON or YAML files usually don't put them in `lib/`.
64
+ # So we look at the gem root.
65
+ # Similarly, gems that include Rails engines generally file Ruby files in `app/`.
66
+ # However some gems embed their tests, they're very unlikely to be loaded, so not worth precompiling.
67
+ gem_exclude = Regexp.union([exclude, "/spec/", "/test/", "/features/"].compact)
68
+
69
+ gem_pattern = %r{^#{Regexp.escape(Bundler.bundle_path.to_s)}/?(?:bundler/)?gems/[^/]+}
70
+ gem_paths = $LOAD_PATH.map { |p| p[gem_pattern] || p }.uniq
71
+
72
+ precompile_ruby_files(gem_paths, exclude: gem_exclude)
73
+ precompile_yaml_files(gem_paths, exclude: gem_exclude)
74
+ precompile_json_files(gem_paths, exclude: gem_exclude)
75
+ end
76
+
77
+ if (exitstatus = @work_pool.shutdown)
78
+ exit(exitstatus)
79
+ end
80
+ end
81
+ 0
82
+ end
83
+
84
+ dir_sort = begin
85
+ Dir[__FILE__, sort: false]
86
+ true
87
+ rescue ArgumentError, TypeError
88
+ false
89
+ end
90
+
91
+ if dir_sort
92
+ def list_files(path, pattern)
93
+ if File.directory?(path)
94
+ Dir[File.join(path, pattern), sort: false]
95
+ elsif File.exist?(path)
96
+ [path]
97
+ else
98
+ []
99
+ end
100
+ end
101
+ else
102
+ def list_files(path, pattern)
103
+ if File.directory?(path)
104
+ Dir[File.join(path, pattern)]
105
+ elsif File.exist?(path)
106
+ [path]
107
+ else
108
+ []
109
+ end
110
+ end
111
+ end
112
+
113
+ def run
114
+ parser.parse!(argv)
115
+ command = argv.shift
116
+ method = "#{command}_command"
117
+ if respond_to?(method)
118
+ public_send(method, *argv)
119
+ else
120
+ invalid_usage!("Unknown command: #{command}")
121
+ end
122
+ end
123
+
124
+ private
125
+
126
+ def precompile_yaml_files(load_paths, exclude: self.exclude)
127
+ return unless yaml
128
+
129
+ load_paths.each do |path|
130
+ if !exclude || !exclude.match?(path)
131
+ list_files(path, "**/*.{yml,yaml}").each do |yaml_file|
132
+ # We ignore hidden files to not match the various .ci.yml files
133
+ if !File.basename(yaml_file).start_with?(".") && (!exclude || !exclude.match?(yaml_file))
134
+ @work_pool.push(:yaml, yaml_file)
135
+ end
136
+ end
137
+ end
138
+ end
139
+ end
140
+
141
+ def precompile_yaml(*yaml_files)
142
+ Array(yaml_files).each do |yaml_file|
143
+ if CompileCache::YAML.precompile(yaml_file) && verbose
144
+ $stderr.puts(yaml_file)
145
+ end
146
+ end
147
+ end
148
+
149
+ def precompile_json_files(load_paths, exclude: self.exclude)
150
+ return unless json
151
+
152
+ load_paths.each do |path|
153
+ if !exclude || !exclude.match?(path)
154
+ list_files(path, "**/*.json").each do |json_file|
155
+ # We ignore hidden files to not match the various .config.json files
156
+ if !File.basename(json_file).start_with?(".") && (!exclude || !exclude.match?(json_file))
157
+ @work_pool.push(:json, json_file)
158
+ end
159
+ end
160
+ end
161
+ end
162
+ end
163
+
164
+ def precompile_json(*json_files)
165
+ Array(json_files).each do |json_file|
166
+ if CompileCache::JSON.precompile(json_file) && verbose
167
+ $stderr.puts(json_file)
168
+ end
169
+ end
170
+ end
171
+
172
+ def precompile_ruby_files(load_paths, exclude: self.exclude)
173
+ return unless iseq
174
+
175
+ load_paths.each do |path|
176
+ if !exclude || !exclude.match?(path)
177
+ list_files(path, "**/{*.rb,*.rake,Rakefile}").each do |ruby_file|
178
+ if !exclude || !exclude.match?(ruby_file)
179
+ @work_pool.push(:ruby, ruby_file)
180
+ end
181
+ end
182
+ end
183
+ end
184
+ end
185
+
186
+ def precompile_ruby(*ruby_files)
187
+ Array(ruby_files).each do |ruby_file|
188
+ if CompileCache::ISeq.precompile(ruby_file) && verbose
189
+ $stderr.puts(ruby_file)
190
+ end
191
+ end
192
+ end
193
+
194
+ def fix_default_encoding
195
+ if Encoding.default_external == Encoding::US_ASCII
196
+ Encoding.default_external = Encoding::UTF_8
197
+ begin
198
+ yield
199
+ ensure
200
+ Encoding.default_external = Encoding::US_ASCII
201
+ end
202
+ else
203
+ yield
204
+ end
205
+ end
206
+
207
+ def invalid_usage!(message)
208
+ $stderr.puts message
209
+ $stderr.puts
210
+ $stderr.puts parser
211
+ 1
212
+ end
213
+
214
+ def cache_dir=(dir)
215
+ @cache_dir = File.expand_path(File.join(dir, "bootsnap/compile-cache"))
216
+ end
217
+
218
+ def exclude_pattern(pattern)
219
+ (@exclude_patterns ||= []) << Regexp.new(pattern)
220
+ self.exclude = Regexp.union(@exclude_patterns)
221
+ end
222
+
223
+ def parser
224
+ @parser ||= OptionParser.new do |opts|
225
+ opts.banner = "Usage: bootsnap COMMAND [ARGS]"
226
+ opts.separator ""
227
+ opts.separator "GLOBAL OPTIONS"
228
+ opts.separator ""
229
+
230
+ help = <<~HELP
231
+ Path to the bootsnap cache directory. Defaults to tmp/cache
232
+ HELP
233
+ opts.on("--cache-dir DIR", help.strip) do |dir|
234
+ self.cache_dir = dir
235
+ end
236
+
237
+ help = <<~HELP
238
+ Print precompiled paths.
239
+ HELP
240
+ opts.on("--verbose", "-v", help.strip) do
241
+ self.verbose = true
242
+ end
243
+
244
+ help = <<~HELP
245
+ Number of workers to use. Default to number of processors, set to 0 to disable multi-processing.
246
+ HELP
247
+ opts.on("--jobs JOBS", "-j", help.strip) do |jobs|
248
+ self.jobs = Integer(jobs)
249
+ end
250
+
251
+ opts.separator ""
252
+ opts.separator "COMMANDS"
253
+ opts.separator ""
254
+ opts.separator " precompile [DIRECTORIES...]: Precompile all .rb files in the passed directories"
255
+
256
+ help = <<~HELP
257
+ Precompile the gems in Gemfile
258
+ HELP
259
+ opts.on("--gemfile", help) { self.compile_gemfile = true }
260
+
261
+ help = <<~HELP
262
+ Path pattern to not precompile. e.g. --exclude 'aws-sdk|google-api'
263
+ HELP
264
+ opts.on("--exclude PATTERN", help) { |pattern| exclude_pattern(pattern) }
265
+
266
+ help = <<~HELP
267
+ Disable ISeq (.rb) precompilation.
268
+ HELP
269
+ opts.on("--no-iseq", help) { self.iseq = false }
270
+
271
+ help = <<~HELP
272
+ Disable YAML precompilation.
273
+ HELP
274
+ opts.on("--no-yaml", help) { self.yaml = false }
275
+
276
+ help = <<~HELP
277
+ Disable JSON precompilation.
278
+ HELP
279
+ opts.on("--no-json", help) { self.json = false }
280
+ end
281
+ end
282
+ end
283
+ end
@@ -1,31 +1,83 @@
1
- require('bootsnap/bootsnap')
2
- require('zlib')
1
+ # frozen_string_literal: true
2
+
3
+ require "bootsnap/bootsnap"
4
+ require "zlib"
3
5
 
4
6
  module Bootsnap
5
7
  module CompileCache
6
8
  module ISeq
7
9
  class << self
8
- attr_accessor(:cache_dir)
10
+ attr_reader(:cache_dir)
11
+
12
+ def cache_dir=(cache_dir)
13
+ @cache_dir = cache_dir.end_with?("/") ? "#{cache_dir}iseq" : "#{cache_dir}-iseq"
14
+ end
15
+
16
+ def supported?
17
+ CompileCache.supported? && defined?(RubyVM)
18
+ end
9
19
  end
10
20
 
11
- def self.input_to_storage(_, path)
12
- RubyVM::InstructionSequence.compile_file(path).to_binary
13
- rescue SyntaxError
14
- raise(Uncompilable, 'syntax error')
21
+ has_ruby_bug_18250 = begin # https://bugs.ruby-lang.org/issues/18250
22
+ if defined? RubyVM::InstructionSequence
23
+ RubyVM::InstructionSequence.compile("def foo(*); ->{ super }; end; def foo(**); ->{ super }; end").to_binary
24
+ end
25
+ false
26
+ rescue TypeError
27
+ true
28
+ end
29
+
30
+ if has_ruby_bug_18250
31
+ def self.input_to_storage(_, path)
32
+ iseq = begin
33
+ RubyVM::InstructionSequence.compile_file(path)
34
+ rescue SyntaxError
35
+ return UNCOMPILABLE # syntax error
36
+ end
37
+
38
+ begin
39
+ iseq.to_binary
40
+ rescue TypeError
41
+ UNCOMPILABLE # ruby bug #18250
42
+ end
43
+ end
44
+ else
45
+ def self.input_to_storage(_, path)
46
+ RubyVM::InstructionSequence.compile_file(path).to_binary
47
+ rescue SyntaxError
48
+ UNCOMPILABLE # syntax error
49
+ end
15
50
  end
16
51
 
17
- def self.storage_to_output(binary)
52
+ def self.storage_to_output(binary, _args)
18
53
  RubyVM::InstructionSequence.load_from_binary(binary)
19
- rescue RuntimeError => e
20
- if e.message == 'broken binary format'
21
- STDERR.puts("[Bootsnap::CompileCache] warning: rejecting broken binary")
54
+ rescue RuntimeError => error
55
+ if error.message == "broken binary format"
56
+ $stderr.puts("[Bootsnap::CompileCache] warning: rejecting broken binary")
22
57
  nil
23
58
  else
24
59
  raise
25
60
  end
26
61
  end
27
62
 
28
- def self.input_to_output(_)
63
+ def self.fetch(path, cache_dir: ISeq.cache_dir)
64
+ Bootsnap::CompileCache::Native.fetch(
65
+ cache_dir,
66
+ path.to_s,
67
+ Bootsnap::CompileCache::ISeq,
68
+ nil,
69
+ )
70
+ end
71
+
72
+ def self.precompile(path)
73
+ Bootsnap::CompileCache::Native.precompile(
74
+ cache_dir,
75
+ path.to_s,
76
+ Bootsnap::CompileCache::ISeq,
77
+ )
78
+ end
79
+
80
+ def self.input_to_output(_data, _kwargs)
29
81
  nil # ruby handles this
30
82
  end
31
83
 
@@ -34,15 +86,9 @@ module Bootsnap
34
86
  # Having coverage enabled prevents iseq dumping/loading.
35
87
  return nil if defined?(Coverage) && Bootsnap::CompileCache::Native.coverage_running?
36
88
 
37
- Bootsnap::CompileCache::Native.fetch(
38
- Bootsnap::CompileCache::ISeq.cache_dir,
39
- path.to_s,
40
- Bootsnap::CompileCache::ISeq
41
- )
42
- rescue Errno::EACCES
43
- Bootsnap::CompileCache.permission_error(path)
44
- rescue RuntimeError => e
45
- if e.message =~ /unmatched platform/
89
+ Bootsnap::CompileCache::ISeq.fetch(path.to_s)
90
+ rescue RuntimeError => error
91
+ if error.message =~ /unmatched platform/
46
92
  puts("unmatched platform for file #{path}")
47
93
  end
48
94
  raise
@@ -59,10 +105,15 @@ module Bootsnap
59
105
  crc = Zlib.crc32(option.inspect)
60
106
  Bootsnap::CompileCache::Native.compile_option_crc32 = crc
61
107
  end
108
+ compile_option_updated if supported?
62
109
 
63
110
  def self.install!(cache_dir)
64
111
  Bootsnap::CompileCache::ISeq.cache_dir = cache_dir
112
+
113
+ return unless supported?
114
+
65
115
  Bootsnap::CompileCache::ISeq.compile_option_updated
116
+
66
117
  class << RubyVM::InstructionSequence
67
118
  prepend(InstructionSequenceMixin)
68
119
  end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bootsnap/bootsnap"
4
+
5
+ module Bootsnap
6
+ module CompileCache
7
+ module JSON
8
+ class << self
9
+ attr_accessor(:msgpack_factory, :supported_options)
10
+ attr_reader(:cache_dir)
11
+
12
+ def cache_dir=(cache_dir)
13
+ @cache_dir = cache_dir.end_with?("/") ? "#{cache_dir}json" : "#{cache_dir}-json"
14
+ end
15
+
16
+ def input_to_storage(payload, _)
17
+ obj = ::JSON.parse(payload)
18
+ msgpack_factory.dump(obj)
19
+ end
20
+
21
+ def storage_to_output(data, kwargs)
22
+ if kwargs&.key?(:symbolize_names)
23
+ kwargs[:symbolize_keys] = kwargs.delete(:symbolize_names)
24
+ end
25
+ msgpack_factory.load(data, kwargs)
26
+ end
27
+
28
+ def input_to_output(data, kwargs)
29
+ ::JSON.parse(data, **(kwargs || {}))
30
+ end
31
+
32
+ def precompile(path)
33
+ Bootsnap::CompileCache::Native.precompile(
34
+ cache_dir,
35
+ path.to_s,
36
+ self,
37
+ )
38
+ end
39
+
40
+ def install!(cache_dir)
41
+ self.cache_dir = cache_dir
42
+ init!
43
+ if ::JSON.respond_to?(:load_file)
44
+ ::JSON.singleton_class.prepend(Patch)
45
+ end
46
+ end
47
+
48
+ def init!
49
+ require "json"
50
+ require "msgpack"
51
+
52
+ self.msgpack_factory = MessagePack::Factory.new
53
+ self.supported_options = [:symbolize_names]
54
+ if supports_freeze?
55
+ self.supported_options = [:freeze]
56
+ end
57
+ supported_options.freeze
58
+ end
59
+
60
+ private
61
+
62
+ def supports_freeze?
63
+ ::JSON.parse('["foo"]', freeze: true).first.frozen? &&
64
+ MessagePack.load(MessagePack.dump("foo"), freeze: true).frozen?
65
+ end
66
+ end
67
+
68
+ module Patch
69
+ def load_file(path, *args)
70
+ return super if args.size > 1
71
+
72
+ if (kwargs = args.first)
73
+ return super unless kwargs.is_a?(Hash)
74
+ return super unless (kwargs.keys - ::Bootsnap::CompileCache::JSON.supported_options).empty?
75
+ end
76
+
77
+ ::Bootsnap::CompileCache::Native.fetch(
78
+ Bootsnap::CompileCache::JSON.cache_dir,
79
+ File.realpath(path),
80
+ ::Bootsnap::CompileCache::JSON,
81
+ kwargs,
82
+ )
83
+ end
84
+
85
+ ruby2_keywords :load_file if respond_to?(:ruby2_keywords, true)
86
+ end
87
+ end
88
+ end
89
+ end