bootsnap 1.4.0 → 1.7.7
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 +4 -4
- data/CHANGELOG.md +110 -0
- data/README.md +68 -13
- data/exe/bootsnap +5 -0
- data/ext/bootsnap/bootsnap.c +285 -87
- data/ext/bootsnap/extconf.rb +20 -14
- data/lib/bootsnap/bundler.rb +1 -0
- data/lib/bootsnap/cli/worker_pool.rb +135 -0
- data/lib/bootsnap/cli.rb +246 -0
- data/lib/bootsnap/compile_cache/iseq.rb +24 -7
- data/lib/bootsnap/compile_cache/yaml.rb +113 -39
- data/lib/bootsnap/compile_cache.rb +15 -2
- data/lib/bootsnap/explicit_require.rb +1 -0
- data/lib/bootsnap/load_path_cache/cache.rb +44 -9
- data/lib/bootsnap/load_path_cache/change_observer.rb +5 -1
- data/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb +30 -6
- data/lib/bootsnap/load_path_cache/core_ext/loaded_features.rb +11 -0
- data/lib/bootsnap/load_path_cache/loaded_features_index.rb +43 -11
- data/lib/bootsnap/load_path_cache/path.rb +3 -2
- data/lib/bootsnap/load_path_cache/path_scanner.rb +53 -27
- data/lib/bootsnap/load_path_cache/realpath_cache.rb +5 -5
- data/lib/bootsnap/load_path_cache/store.rb +28 -14
- data/lib/bootsnap/load_path_cache.rb +10 -16
- data/lib/bootsnap/setup.rb +2 -33
- data/lib/bootsnap/version.rb +2 -1
- data/lib/bootsnap.rb +91 -15
- metadata +17 -29
- data/.gitignore +0 -17
- data/.rubocop.yml +0 -20
- data/.travis.yml +0 -24
- data/CODE_OF_CONDUCT.md +0 -74
- data/CONTRIBUTING.md +0 -21
- data/Gemfile +0 -8
- data/README.jp.md +0 -231
- data/Rakefile +0 -12
- data/bin/ci +0 -10
- data/bin/console +0 -14
- data/bin/setup +0 -8
- data/bin/test-minimal-support +0 -7
- data/bin/testunit +0 -8
- data/bootsnap.gemspec +0 -45
- data/dev.yml +0 -10
- data/lib/bootsnap/load_path_cache/core_ext/active_support.rb +0 -100
- data/shipit.rubygems.yml +0 -4
data/ext/bootsnap/extconf.rb
CHANGED
@@ -1,18 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
require("mkmf")
|
2
|
-
$CFLAGS << ' -O3 '
|
3
|
-
$CFLAGS << ' -std=c99'
|
4
3
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
$CFLAGS << ' -Wall'
|
9
|
-
$CFLAGS << ' -Werror'
|
10
|
-
$CFLAGS << ' -Wextra'
|
11
|
-
$CFLAGS << ' -Wpedantic'
|
4
|
+
if RUBY_ENGINE == 'ruby'
|
5
|
+
$CFLAGS << ' -O3 '
|
6
|
+
$CFLAGS << ' -std=c99'
|
12
7
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
8
|
+
# ruby.h has some -Wpedantic fails in some cases
|
9
|
+
# (e.g. https://github.com/Shopify/bootsnap/issues/15)
|
10
|
+
unless ['0', '', nil].include?(ENV['BOOTSNAP_PEDANTIC'])
|
11
|
+
$CFLAGS << ' -Wall'
|
12
|
+
$CFLAGS << ' -Werror'
|
13
|
+
$CFLAGS << ' -Wextra'
|
14
|
+
$CFLAGS << ' -Wpedantic'
|
15
|
+
|
16
|
+
$CFLAGS << ' -Wno-unused-parameter' # VALUE self has to be there but we don't care what it is.
|
17
|
+
$CFLAGS << ' -Wno-keyword-macro' # hiding return
|
18
|
+
$CFLAGS << ' -Wno-gcc-compat' # ruby.h 2.6.0 on macos 10.14, dunno
|
19
|
+
end
|
17
20
|
|
18
|
-
create_makefile("bootsnap/bootsnap")
|
21
|
+
create_makefile("bootsnap/bootsnap")
|
22
|
+
else
|
23
|
+
File.write("Makefile", dummy_makefile($srcdir).join(""))
|
24
|
+
end
|
data/lib/bootsnap/bundler.rb
CHANGED
@@ -0,0 +1,135 @@
|
|
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
|
+
@jobs.fetch(job).call(*args)
|
67
|
+
end
|
68
|
+
rescue IOError
|
69
|
+
nil
|
70
|
+
end
|
71
|
+
|
72
|
+
def spawn
|
73
|
+
@pid = Process.fork do
|
74
|
+
to_io.close
|
75
|
+
work_loop
|
76
|
+
exit!(0)
|
77
|
+
end
|
78
|
+
@pipe_out.close
|
79
|
+
true
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def initialize(size:, jobs: {})
|
84
|
+
@size = size
|
85
|
+
@jobs = jobs
|
86
|
+
@queue = Queue.new
|
87
|
+
@pids = []
|
88
|
+
end
|
89
|
+
|
90
|
+
def spawn
|
91
|
+
@workers = @size.times.map { Worker.new(@jobs) }
|
92
|
+
@workers.each(&:spawn)
|
93
|
+
@dispatcher_thread = Thread.new { dispatch_loop }
|
94
|
+
@dispatcher_thread.abort_on_exception = true
|
95
|
+
true
|
96
|
+
end
|
97
|
+
|
98
|
+
def dispatch_loop
|
99
|
+
loop do
|
100
|
+
case job = @queue.pop
|
101
|
+
when nil
|
102
|
+
@workers.each do |worker|
|
103
|
+
worker.write([:exit])
|
104
|
+
worker.close
|
105
|
+
end
|
106
|
+
return true
|
107
|
+
else
|
108
|
+
unless @workers.sample.write(job, block: false)
|
109
|
+
free_worker.write(job)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def free_worker
|
116
|
+
IO.select(nil, @workers)[1].sample
|
117
|
+
end
|
118
|
+
|
119
|
+
def push(*args)
|
120
|
+
@queue.push(args)
|
121
|
+
nil
|
122
|
+
end
|
123
|
+
|
124
|
+
def shutdown
|
125
|
+
@queue.close
|
126
|
+
@dispatcher_thread.join
|
127
|
+
@workers.each do |worker|
|
128
|
+
_pid, status = Process.wait2(worker.pid)
|
129
|
+
return status.exitstatus unless status.success?
|
130
|
+
end
|
131
|
+
nil
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
data/lib/bootsnap/cli.rb
ADDED
@@ -0,0 +1,246 @@
|
|
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, :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
|
+
end
|
36
|
+
|
37
|
+
def precompile_command(*sources)
|
38
|
+
require 'bootsnap/compile_cache/iseq'
|
39
|
+
require 'bootsnap/compile_cache/yaml'
|
40
|
+
|
41
|
+
fix_default_encoding do
|
42
|
+
Bootsnap::CompileCache::ISeq.cache_dir = self.cache_dir
|
43
|
+
Bootsnap::CompileCache::YAML.init!
|
44
|
+
Bootsnap::CompileCache::YAML.cache_dir = self.cache_dir
|
45
|
+
|
46
|
+
@work_pool = WorkerPool.create(size: jobs, jobs: {
|
47
|
+
ruby: method(:precompile_ruby),
|
48
|
+
yaml: method(:precompile_yaml),
|
49
|
+
})
|
50
|
+
@work_pool.spawn
|
51
|
+
|
52
|
+
main_sources = sources.map { |d| File.expand_path(d) }
|
53
|
+
precompile_ruby_files(main_sources)
|
54
|
+
precompile_yaml_files(main_sources)
|
55
|
+
|
56
|
+
if compile_gemfile
|
57
|
+
# Some gems embed their tests, they're very unlikely to be loaded, so not worth precompiling.
|
58
|
+
gem_exclude = Regexp.union([exclude, '/spec/', '/test/'].compact)
|
59
|
+
precompile_ruby_files($LOAD_PATH.map { |d| File.expand_path(d) }, exclude: gem_exclude)
|
60
|
+
|
61
|
+
# Gems that include YAML files usually don't put them in `lib/`.
|
62
|
+
# So we look at the gem root.
|
63
|
+
gem_pattern = %r{^#{Regexp.escape(Bundler.bundle_path.to_s)}/?(?:bundler/)?gems\/[^/]+}
|
64
|
+
gem_paths = $LOAD_PATH.map { |p| p[gem_pattern] }.compact.uniq
|
65
|
+
precompile_yaml_files(gem_paths, exclude: gem_exclude)
|
66
|
+
end
|
67
|
+
|
68
|
+
if exitstatus = @work_pool.shutdown
|
69
|
+
exit(exitstatus)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
0
|
73
|
+
end
|
74
|
+
|
75
|
+
dir_sort = begin
|
76
|
+
Dir[__FILE__, sort: false]
|
77
|
+
true
|
78
|
+
rescue ArgumentError, TypeError
|
79
|
+
false
|
80
|
+
end
|
81
|
+
|
82
|
+
if dir_sort
|
83
|
+
def list_files(path, pattern)
|
84
|
+
if File.directory?(path)
|
85
|
+
Dir[File.join(path, pattern), sort: false]
|
86
|
+
elsif File.exist?(path)
|
87
|
+
[path]
|
88
|
+
else
|
89
|
+
[]
|
90
|
+
end
|
91
|
+
end
|
92
|
+
else
|
93
|
+
def list_files(path, pattern)
|
94
|
+
if File.directory?(path)
|
95
|
+
Dir[File.join(path, pattern)]
|
96
|
+
elsif File.exist?(path)
|
97
|
+
[path]
|
98
|
+
else
|
99
|
+
[]
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def run
|
105
|
+
parser.parse!(argv)
|
106
|
+
command = argv.shift
|
107
|
+
method = "#{command}_command"
|
108
|
+
if respond_to?(method)
|
109
|
+
public_send(method, *argv)
|
110
|
+
else
|
111
|
+
invalid_usage!("Unknown command: #{command}")
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
private
|
116
|
+
|
117
|
+
def precompile_yaml_files(load_paths, exclude: self.exclude)
|
118
|
+
return unless yaml
|
119
|
+
|
120
|
+
load_paths.each do |path|
|
121
|
+
if !exclude || !exclude.match?(path)
|
122
|
+
list_files(path, '**/*.{yml,yaml}').each do |yaml_file|
|
123
|
+
# We ignore hidden files to not match the various .ci.yml files
|
124
|
+
if !File.basename(yaml_file).start_with?('.') && (!exclude || !exclude.match?(yaml_file))
|
125
|
+
@work_pool.push(:yaml, yaml_file)
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
def precompile_yaml(*yaml_files)
|
133
|
+
Array(yaml_files).each do |yaml_file|
|
134
|
+
if CompileCache::YAML.precompile(yaml_file, cache_dir: cache_dir)
|
135
|
+
STDERR.puts(yaml_file) if verbose
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
def precompile_ruby_files(load_paths, exclude: self.exclude)
|
141
|
+
return unless iseq
|
142
|
+
|
143
|
+
load_paths.each do |path|
|
144
|
+
if !exclude || !exclude.match?(path)
|
145
|
+
list_files(path, '**/*.rb').each do |ruby_file|
|
146
|
+
if !exclude || !exclude.match?(ruby_file)
|
147
|
+
@work_pool.push(:ruby, ruby_file)
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
def precompile_ruby(*ruby_files)
|
155
|
+
Array(ruby_files).each do |ruby_file|
|
156
|
+
if CompileCache::ISeq.precompile(ruby_file, cache_dir: cache_dir)
|
157
|
+
STDERR.puts(ruby_file) if verbose
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
def fix_default_encoding
|
163
|
+
if Encoding.default_external == Encoding::US_ASCII
|
164
|
+
Encoding.default_external = Encoding::UTF_8
|
165
|
+
begin
|
166
|
+
yield
|
167
|
+
ensure
|
168
|
+
Encoding.default_external = Encoding::US_ASCII
|
169
|
+
end
|
170
|
+
else
|
171
|
+
yield
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
def invalid_usage!(message)
|
176
|
+
STDERR.puts message
|
177
|
+
STDERR.puts
|
178
|
+
STDERR.puts parser
|
179
|
+
1
|
180
|
+
end
|
181
|
+
|
182
|
+
def cache_dir=(dir)
|
183
|
+
@cache_dir = File.expand_path(File.join(dir, 'bootsnap/compile-cache'))
|
184
|
+
end
|
185
|
+
|
186
|
+
def exclude_pattern(pattern)
|
187
|
+
(@exclude_patterns ||= []) << Regexp.new(pattern)
|
188
|
+
self.exclude = Regexp.union(@exclude_patterns)
|
189
|
+
end
|
190
|
+
|
191
|
+
def parser
|
192
|
+
@parser ||= OptionParser.new do |opts|
|
193
|
+
opts.banner = "Usage: bootsnap COMMAND [ARGS]"
|
194
|
+
opts.separator ""
|
195
|
+
opts.separator "GLOBAL OPTIONS"
|
196
|
+
opts.separator ""
|
197
|
+
|
198
|
+
help = <<~EOS
|
199
|
+
Path to the bootsnap cache directory. Defaults to tmp/cache
|
200
|
+
EOS
|
201
|
+
opts.on('--cache-dir DIR', help.strip) do |dir|
|
202
|
+
self.cache_dir = dir
|
203
|
+
end
|
204
|
+
|
205
|
+
help = <<~EOS
|
206
|
+
Print precompiled paths.
|
207
|
+
EOS
|
208
|
+
opts.on('--verbose', '-v', help.strip) do
|
209
|
+
self.verbose = true
|
210
|
+
end
|
211
|
+
|
212
|
+
help = <<~EOS
|
213
|
+
Number of workers to use. Default to number of processors, set to 0 to disable multi-processing.
|
214
|
+
EOS
|
215
|
+
opts.on('--jobs JOBS', '-j', help.strip) do |jobs|
|
216
|
+
self.jobs = Integer(jobs)
|
217
|
+
end
|
218
|
+
|
219
|
+
opts.separator ""
|
220
|
+
opts.separator "COMMANDS"
|
221
|
+
opts.separator ""
|
222
|
+
opts.separator " precompile [DIRECTORIES...]: Precompile all .rb files in the passed directories"
|
223
|
+
|
224
|
+
help = <<~EOS
|
225
|
+
Precompile the gems in Gemfile
|
226
|
+
EOS
|
227
|
+
opts.on('--gemfile', help) { self.compile_gemfile = true }
|
228
|
+
|
229
|
+
help = <<~EOS
|
230
|
+
Path pattern to not precompile. e.g. --exclude 'aws-sdk|google-api'
|
231
|
+
EOS
|
232
|
+
opts.on('--exclude PATTERN', help) { |pattern| exclude_pattern(pattern) }
|
233
|
+
|
234
|
+
help = <<~EOS
|
235
|
+
Disable ISeq (.rb) precompilation.
|
236
|
+
EOS
|
237
|
+
opts.on('--no-iseq', help) { self.iseq = false }
|
238
|
+
|
239
|
+
help = <<~EOS
|
240
|
+
Disable YAML precompilation.
|
241
|
+
EOS
|
242
|
+
opts.on('--no-yaml', help) { self.yaml = false }
|
243
|
+
end
|
244
|
+
end
|
245
|
+
end
|
246
|
+
end
|
@@ -1,3 +1,4 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
require('bootsnap/bootsnap')
|
2
3
|
require('zlib')
|
3
4
|
|
@@ -14,7 +15,7 @@ module Bootsnap
|
|
14
15
|
raise(Uncompilable, 'syntax error')
|
15
16
|
end
|
16
17
|
|
17
|
-
def self.storage_to_output(binary)
|
18
|
+
def self.storage_to_output(binary, _args)
|
18
19
|
RubyVM::InstructionSequence.load_from_binary(binary)
|
19
20
|
rescue RuntimeError => e
|
20
21
|
if e.message == 'broken binary format'
|
@@ -25,7 +26,24 @@ module Bootsnap
|
|
25
26
|
end
|
26
27
|
end
|
27
28
|
|
28
|
-
def self.
|
29
|
+
def self.fetch(path, cache_dir: ISeq.cache_dir)
|
30
|
+
Bootsnap::CompileCache::Native.fetch(
|
31
|
+
cache_dir,
|
32
|
+
path.to_s,
|
33
|
+
Bootsnap::CompileCache::ISeq,
|
34
|
+
nil,
|
35
|
+
)
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.precompile(path, cache_dir: ISeq.cache_dir)
|
39
|
+
Bootsnap::CompileCache::Native.precompile(
|
40
|
+
cache_dir,
|
41
|
+
path.to_s,
|
42
|
+
Bootsnap::CompileCache::ISeq,
|
43
|
+
)
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.input_to_output(_data, _kwargs)
|
29
47
|
nil # ruby handles this
|
30
48
|
end
|
31
49
|
|
@@ -34,11 +52,9 @@ module Bootsnap
|
|
34
52
|
# Having coverage enabled prevents iseq dumping/loading.
|
35
53
|
return nil if defined?(Coverage) && Bootsnap::CompileCache::Native.coverage_running?
|
36
54
|
|
37
|
-
Bootsnap::CompileCache::
|
38
|
-
|
39
|
-
|
40
|
-
Bootsnap::CompileCache::ISeq
|
41
|
-
)
|
55
|
+
Bootsnap::CompileCache::ISeq.fetch(path.to_s)
|
56
|
+
rescue Errno::EACCES
|
57
|
+
Bootsnap::CompileCache.permission_error(path)
|
42
58
|
rescue RuntimeError => e
|
43
59
|
if e.message =~ /unmatched platform/
|
44
60
|
puts("unmatched platform for file #{path}")
|
@@ -57,6 +73,7 @@ module Bootsnap
|
|
57
73
|
crc = Zlib.crc32(option.inspect)
|
58
74
|
Bootsnap::CompileCache::Native.compile_option_crc32 = crc
|
59
75
|
end
|
76
|
+
compile_option_updated
|
60
77
|
|
61
78
|
def self.install!(cache_dir)
|
62
79
|
Bootsnap::CompileCache::ISeq.cache_dir = cache_dir
|
@@ -1,57 +1,131 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
require('bootsnap/bootsnap')
|
2
3
|
|
3
4
|
module Bootsnap
|
4
5
|
module CompileCache
|
5
6
|
module YAML
|
6
7
|
class << self
|
7
|
-
attr_accessor(:msgpack_factory)
|
8
|
-
end
|
9
|
-
|
10
|
-
def self.input_to_storage(contents, _)
|
11
|
-
raise(Uncompilable) if contents.index("!ruby/object")
|
12
|
-
obj = ::YAML.load(contents)
|
13
|
-
msgpack_factory.packer.write(obj).to_s
|
14
|
-
rescue NoMethodError, RangeError
|
15
|
-
# if the object included things that we can't serialize, fall back to
|
16
|
-
# Marshal. It's a bit slower, but can encode anything yaml can.
|
17
|
-
# NoMethodError is unexpected types; RangeError is Bignums
|
18
|
-
Marshal.dump(obj)
|
19
|
-
end
|
8
|
+
attr_accessor(:msgpack_factory, :cache_dir, :supported_options)
|
20
9
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
else
|
28
|
-
msgpack_factory.unpacker.feed(data).read
|
10
|
+
def input_to_storage(contents, _)
|
11
|
+
obj = strict_load(contents)
|
12
|
+
msgpack_factory.dump(obj)
|
13
|
+
rescue NoMethodError, RangeError
|
14
|
+
# The object included things that we can't serialize
|
15
|
+
raise(Uncompilable)
|
29
16
|
end
|
30
|
-
end
|
31
17
|
|
32
|
-
|
33
|
-
|
34
|
-
|
18
|
+
def storage_to_output(data, kwargs)
|
19
|
+
if kwargs && kwargs.key?(:symbolize_names)
|
20
|
+
kwargs[:symbolize_keys] = kwargs.delete(:symbolize_names)
|
21
|
+
end
|
22
|
+
msgpack_factory.load(data, kwargs)
|
23
|
+
end
|
35
24
|
|
36
|
-
|
37
|
-
|
38
|
-
|
25
|
+
def input_to_output(data, kwargs)
|
26
|
+
::YAML.load(data, **(kwargs || {}))
|
27
|
+
end
|
39
28
|
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
29
|
+
def strict_load(payload, *args)
|
30
|
+
ast = ::YAML.parse(payload)
|
31
|
+
return ast unless ast
|
32
|
+
strict_visitor.create(*args).visit(ast)
|
33
|
+
end
|
34
|
+
ruby2_keywords :strict_load if respond_to?(:ruby2_keywords, true)
|
46
35
|
|
47
|
-
|
48
|
-
|
49
|
-
Bootsnap::CompileCache::Native.fetch(
|
36
|
+
def precompile(path, cache_dir: YAML.cache_dir)
|
37
|
+
Bootsnap::CompileCache::Native.precompile(
|
50
38
|
cache_dir,
|
51
|
-
path,
|
52
|
-
Bootsnap::CompileCache::YAML
|
39
|
+
path.to_s,
|
40
|
+
Bootsnap::CompileCache::YAML,
|
53
41
|
)
|
54
42
|
end
|
43
|
+
|
44
|
+
def install!(cache_dir)
|
45
|
+
self.cache_dir = cache_dir
|
46
|
+
init!
|
47
|
+
::YAML.singleton_class.prepend(Patch)
|
48
|
+
end
|
49
|
+
|
50
|
+
def init!
|
51
|
+
require('yaml')
|
52
|
+
require('msgpack')
|
53
|
+
require('date')
|
54
|
+
|
55
|
+
# MessagePack serializes symbols as strings by default.
|
56
|
+
# We want them to roundtrip cleanly, so we use a custom factory.
|
57
|
+
# see: https://github.com/msgpack/msgpack-ruby/pull/122
|
58
|
+
factory = MessagePack::Factory.new
|
59
|
+
factory.register_type(0x00, Symbol)
|
60
|
+
|
61
|
+
if defined? MessagePack::Timestamp
|
62
|
+
factory.register_type(
|
63
|
+
MessagePack::Timestamp::TYPE, # or just -1
|
64
|
+
Time,
|
65
|
+
packer: MessagePack::Time::Packer,
|
66
|
+
unpacker: MessagePack::Time::Unpacker
|
67
|
+
)
|
68
|
+
|
69
|
+
marshal_fallback = {
|
70
|
+
packer: ->(value) { Marshal.dump(value) },
|
71
|
+
unpacker: ->(payload) { Marshal.load(payload) },
|
72
|
+
}
|
73
|
+
{
|
74
|
+
Date => 0x01,
|
75
|
+
Regexp => 0x02,
|
76
|
+
}.each do |type, code|
|
77
|
+
factory.register_type(code, type, marshal_fallback)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
self.msgpack_factory = factory
|
82
|
+
|
83
|
+
self.supported_options = []
|
84
|
+
params = ::YAML.method(:load).parameters
|
85
|
+
if params.include?([:key, :symbolize_names])
|
86
|
+
self.supported_options << :symbolize_names
|
87
|
+
end
|
88
|
+
if params.include?([:key, :freeze])
|
89
|
+
if factory.load(factory.dump('yaml'), freeze: true).frozen?
|
90
|
+
self.supported_options << :freeze
|
91
|
+
end
|
92
|
+
end
|
93
|
+
self.supported_options.freeze
|
94
|
+
end
|
95
|
+
|
96
|
+
def strict_visitor
|
97
|
+
self::NoTagsVisitor ||= Class.new(Psych::Visitors::ToRuby) do
|
98
|
+
def visit(target)
|
99
|
+
if target.tag
|
100
|
+
raise Uncompilable, "YAML tags are not supported: #{target.tag}"
|
101
|
+
end
|
102
|
+
super
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
module Patch
|
109
|
+
def load_file(path, *args)
|
110
|
+
return super if args.size > 1
|
111
|
+
if kwargs = args.first
|
112
|
+
return super unless kwargs.is_a?(Hash)
|
113
|
+
return super unless (kwargs.keys - ::Bootsnap::CompileCache::YAML.supported_options).empty?
|
114
|
+
end
|
115
|
+
|
116
|
+
begin
|
117
|
+
::Bootsnap::CompileCache::Native.fetch(
|
118
|
+
Bootsnap::CompileCache::YAML.cache_dir,
|
119
|
+
File.realpath(path),
|
120
|
+
::Bootsnap::CompileCache::YAML,
|
121
|
+
kwargs,
|
122
|
+
)
|
123
|
+
rescue Errno::EACCES
|
124
|
+
::Bootsnap::CompileCache.permission_error(path)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
ruby2_keywords :load_file if respond_to?(:ruby2_keywords, true)
|
55
129
|
end
|
56
130
|
end
|
57
131
|
end
|
@@ -1,5 +1,9 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
module Bootsnap
|
2
3
|
module CompileCache
|
4
|
+
Error = Class.new(StandardError)
|
5
|
+
PermissionError = Class.new(Error)
|
6
|
+
|
3
7
|
def self.setup(cache_dir:, iseq:, yaml:)
|
4
8
|
if iseq
|
5
9
|
if supported?
|
@@ -20,10 +24,19 @@ module Bootsnap
|
|
20
24
|
end
|
21
25
|
end
|
22
26
|
|
27
|
+
def self.permission_error(path)
|
28
|
+
cpath = Bootsnap::CompileCache::ISeq.cache_dir
|
29
|
+
raise(
|
30
|
+
PermissionError,
|
31
|
+
"bootsnap doesn't have permission to write cache entries in '#{cpath}' " \
|
32
|
+
"(or, less likely, doesn't have permission to read '#{path}')",
|
33
|
+
)
|
34
|
+
end
|
35
|
+
|
23
36
|
def self.supported?
|
24
|
-
# only enable on 'ruby' (MRI), POSIX (darwin, linux, *bsd), and >= 2.3.0
|
37
|
+
# only enable on 'ruby' (MRI), POSIX (darwin, linux, *bsd), Windows (RubyInstaller2) and >= 2.3.0
|
25
38
|
RUBY_ENGINE == 'ruby' &&
|
26
|
-
RUBY_PLATFORM =~ /darwin|linux|bsd/ &&
|
39
|
+
RUBY_PLATFORM =~ /darwin|linux|bsd|mswin|mingw|cygwin/ &&
|
27
40
|
Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("2.3.0")
|
28
41
|
end
|
29
42
|
end
|