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.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +110 -0
  3. data/README.md +68 -13
  4. data/exe/bootsnap +5 -0
  5. data/ext/bootsnap/bootsnap.c +285 -87
  6. data/ext/bootsnap/extconf.rb +20 -14
  7. data/lib/bootsnap/bundler.rb +1 -0
  8. data/lib/bootsnap/cli/worker_pool.rb +135 -0
  9. data/lib/bootsnap/cli.rb +246 -0
  10. data/lib/bootsnap/compile_cache/iseq.rb +24 -7
  11. data/lib/bootsnap/compile_cache/yaml.rb +113 -39
  12. data/lib/bootsnap/compile_cache.rb +15 -2
  13. data/lib/bootsnap/explicit_require.rb +1 -0
  14. data/lib/bootsnap/load_path_cache/cache.rb +44 -9
  15. data/lib/bootsnap/load_path_cache/change_observer.rb +5 -1
  16. data/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb +30 -6
  17. data/lib/bootsnap/load_path_cache/core_ext/loaded_features.rb +11 -0
  18. data/lib/bootsnap/load_path_cache/loaded_features_index.rb +43 -11
  19. data/lib/bootsnap/load_path_cache/path.rb +3 -2
  20. data/lib/bootsnap/load_path_cache/path_scanner.rb +53 -27
  21. data/lib/bootsnap/load_path_cache/realpath_cache.rb +5 -5
  22. data/lib/bootsnap/load_path_cache/store.rb +28 -14
  23. data/lib/bootsnap/load_path_cache.rb +10 -16
  24. data/lib/bootsnap/setup.rb +2 -33
  25. data/lib/bootsnap/version.rb +2 -1
  26. data/lib/bootsnap.rb +91 -15
  27. metadata +17 -29
  28. data/.gitignore +0 -17
  29. data/.rubocop.yml +0 -20
  30. data/.travis.yml +0 -24
  31. data/CODE_OF_CONDUCT.md +0 -74
  32. data/CONTRIBUTING.md +0 -21
  33. data/Gemfile +0 -8
  34. data/README.jp.md +0 -231
  35. data/Rakefile +0 -12
  36. data/bin/ci +0 -10
  37. data/bin/console +0 -14
  38. data/bin/setup +0 -8
  39. data/bin/test-minimal-support +0 -7
  40. data/bin/testunit +0 -8
  41. data/bootsnap.gemspec +0 -45
  42. data/dev.yml +0 -10
  43. data/lib/bootsnap/load_path_cache/core_ext/active_support.rb +0 -100
  44. data/shipit.rubygems.yml +0 -4
@@ -1,18 +1,24 @@
1
+ # frozen_string_literal: true
1
2
  require("mkmf")
2
- $CFLAGS << ' -O3 '
3
- $CFLAGS << ' -std=c99'
4
3
 
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'
4
+ if RUBY_ENGINE == 'ruby'
5
+ $CFLAGS << ' -O3 '
6
+ $CFLAGS << ' -std=c99'
12
7
 
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
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
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module Bootsnap
2
3
  extend(self)
3
4
 
@@ -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
@@ -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.input_to_output(_)
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::Native.fetch(
38
- Bootsnap::CompileCache::ISeq.cache_dir,
39
- path.to_s,
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
- def self.storage_to_output(data)
22
- # This could have a meaning in messagepack, and we're being a little lazy
23
- # about it. -- but a leading 0x04 would indicate the contents of the YAML
24
- # is a positive integer, which is rare, to say the least.
25
- if data[0] == 0x04.chr && data[1] == 0x08.chr
26
- Marshal.load(data)
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
- def self.input_to_output(data)
33
- ::YAML.load(data)
34
- end
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
- def self.install!(cache_dir)
37
- require('yaml')
38
- require('msgpack')
25
+ def input_to_output(data, kwargs)
26
+ ::YAML.load(data, **(kwargs || {}))
27
+ end
39
28
 
40
- # MessagePack serializes symbols as strings by default.
41
- # We want them to roundtrip cleanly, so we use a custom factory.
42
- # see: https://github.com/msgpack/msgpack-ruby/pull/122
43
- factory = MessagePack::Factory.new
44
- factory.register_type(0x00, Symbol)
45
- Bootsnap::CompileCache::YAML.msgpack_factory = factory
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
- klass = class << ::YAML; self; end
48
- klass.send(:define_method, :load_file) do |path|
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
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module Bootsnap
2
3
  module ExplicitRequire
3
4
  ARCHDIR = RbConfig::CONFIG['archdir']