bootsnap 1.4.6 → 1.7.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +68 -0
  3. data/README.md +45 -14
  4. data/exe/bootsnap +5 -0
  5. data/ext/bootsnap/bootsnap.c +224 -61
  6. data/ext/bootsnap/extconf.rb +19 -14
  7. data/lib/bootsnap.rb +88 -15
  8. data/lib/bootsnap/cli.rb +246 -0
  9. data/lib/bootsnap/cli/worker_pool.rb +131 -0
  10. data/lib/bootsnap/compile_cache.rb +2 -2
  11. data/lib/bootsnap/compile_cache/iseq.rb +21 -7
  12. data/lib/bootsnap/compile_cache/yaml.rb +109 -40
  13. data/lib/bootsnap/load_path_cache.rb +3 -16
  14. data/lib/bootsnap/load_path_cache/cache.rb +23 -6
  15. data/lib/bootsnap/load_path_cache/change_observer.rb +1 -1
  16. data/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb +16 -4
  17. data/lib/bootsnap/load_path_cache/loaded_features_index.rb +3 -3
  18. data/lib/bootsnap/load_path_cache/path.rb +2 -2
  19. data/lib/bootsnap/load_path_cache/path_scanner.rb +50 -26
  20. data/lib/bootsnap/load_path_cache/realpath_cache.rb +5 -5
  21. data/lib/bootsnap/load_path_cache/store.rb +16 -9
  22. data/lib/bootsnap/setup.rb +1 -36
  23. data/lib/bootsnap/version.rb +1 -1
  24. metadata +14 -28
  25. data/.github/CODEOWNERS +0 -2
  26. data/.github/probots.yml +0 -2
  27. data/.gitignore +0 -17
  28. data/.rubocop.yml +0 -20
  29. data/.travis.yml +0 -21
  30. data/CODE_OF_CONDUCT.md +0 -74
  31. data/CONTRIBUTING.md +0 -21
  32. data/Gemfile +0 -9
  33. data/README.jp.md +0 -231
  34. data/Rakefile +0 -13
  35. data/bin/ci +0 -10
  36. data/bin/console +0 -15
  37. data/bin/setup +0 -8
  38. data/bin/test-minimal-support +0 -7
  39. data/bin/testunit +0 -8
  40. data/bootsnap.gemspec +0 -46
  41. data/dev.yml +0 -10
  42. data/lib/bootsnap/load_path_cache/core_ext/active_support.rb +0 -107
  43. data/shipit.rubygems.yml +0 -0
@@ -34,9 +34,9 @@ module Bootsnap
34
34
  end
35
35
 
36
36
  def self.supported?
37
- # 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
38
38
  RUBY_ENGINE == 'ruby' &&
39
- RUBY_PLATFORM =~ /darwin|linux|bsd/ &&
39
+ RUBY_PLATFORM =~ /darwin|linux|bsd|mswin|mingw|cygwin/ &&
40
40
  Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("2.3.0")
41
41
  end
42
42
  end
@@ -15,7 +15,7 @@ module Bootsnap
15
15
  raise(Uncompilable, 'syntax error')
16
16
  end
17
17
 
18
- def self.storage_to_output(binary)
18
+ def self.storage_to_output(binary, _args)
19
19
  RubyVM::InstructionSequence.load_from_binary(binary)
20
20
  rescue RuntimeError => e
21
21
  if e.message == 'broken binary format'
@@ -26,7 +26,24 @@ module Bootsnap
26
26
  end
27
27
  end
28
28
 
29
- 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)
30
47
  nil # ruby handles this
31
48
  end
32
49
 
@@ -35,11 +52,7 @@ module Bootsnap
35
52
  # Having coverage enabled prevents iseq dumping/loading.
36
53
  return nil if defined?(Coverage) && Bootsnap::CompileCache::Native.coverage_running?
37
54
 
38
- Bootsnap::CompileCache::Native.fetch(
39
- Bootsnap::CompileCache::ISeq.cache_dir,
40
- path.to_s,
41
- Bootsnap::CompileCache::ISeq
42
- )
55
+ Bootsnap::CompileCache::ISeq.fetch(path.to_s)
43
56
  rescue Errno::EACCES
44
57
  Bootsnap::CompileCache.permission_error(path)
45
58
  rescue RuntimeError => e
@@ -60,6 +73,7 @@ module Bootsnap
60
73
  crc = Zlib.crc32(option.inspect)
61
74
  Bootsnap::CompileCache::Native.compile_option_crc32 = crc
62
75
  end
76
+ compile_option_updated
63
77
 
64
78
  def self.install!(cache_dir)
65
79
  Bootsnap::CompileCache::ISeq.cache_dir = cache_dir
@@ -5,58 +5,127 @@ module Bootsnap
5
5
  module CompileCache
6
6
  module YAML
7
7
  class << self
8
- attr_accessor(:msgpack_factory)
9
- end
8
+ attr_accessor(:msgpack_factory, :cache_dir, :supported_options)
10
9
 
11
- def self.input_to_storage(contents, _)
12
- raise(Uncompilable) if contents.index("!ruby/object")
13
- obj = ::YAML.load(contents)
14
- msgpack_factory.packer.write(obj).to_s
15
- rescue NoMethodError, RangeError
16
- # if the object included things that we can't serialize, fall back to
17
- # Marshal. It's a bit slower, but can encode anything yaml can.
18
- # NoMethodError is unexpected types; RangeError is Bignums
19
- Marshal.dump(obj)
20
- end
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)
16
+ end
21
17
 
22
- def self.storage_to_output(data)
23
- # This could have a meaning in messagepack, and we're being a little lazy
24
- # about it. -- but a leading 0x04 would indicate the contents of the YAML
25
- # is a positive integer, which is rare, to say the least.
26
- if data[0] == 0x04.chr && data[1] == 0x08.chr
27
- Marshal.load(data)
28
- else
29
- msgpack_factory.unpacker.feed(data).read
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)
30
23
  end
31
- end
32
24
 
33
- def self.input_to_output(data)
34
- ::YAML.load(data)
35
- end
25
+ def input_to_output(data, kwargs)
26
+ ::YAML.load(data, **(kwargs || {}))
27
+ end
36
28
 
37
- def self.install!(cache_dir)
38
- require('yaml')
39
- require('msgpack')
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)
40
35
 
41
- # MessagePack serializes symbols as strings by default.
42
- # We want them to roundtrip cleanly, so we use a custom factory.
43
- # see: https://github.com/msgpack/msgpack-ruby/pull/122
44
- factory = MessagePack::Factory.new
45
- factory.register_type(0x00, Symbol)
46
- Bootsnap::CompileCache::YAML.msgpack_factory = factory
36
+ def precompile(path, cache_dir: YAML.cache_dir)
37
+ Bootsnap::CompileCache::Native.precompile(
38
+ cache_dir,
39
+ path.to_s,
40
+ Bootsnap::CompileCache::YAML,
41
+ )
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
47
115
 
48
- klass = class << ::YAML; self; end
49
- klass.send(:define_method, :load_file) do |path|
50
116
  begin
51
- Bootsnap::CompileCache::Native.fetch(
52
- cache_dir,
53
- path,
54
- Bootsnap::CompileCache::YAML
117
+ ::Bootsnap::CompileCache::Native.fetch(
118
+ Bootsnap::CompileCache::YAML.cache_dir,
119
+ File.realpath(path),
120
+ ::Bootsnap::CompileCache::YAML,
121
+ kwargs,
55
122
  )
56
123
  rescue Errno::EACCES
57
- Bootsnap::CompileCache.permission_error(path)
124
+ ::Bootsnap::CompileCache.permission_error(path)
58
125
  end
59
126
  end
127
+
128
+ ruby2_keywords :load_file if respond_to?(:ruby2_keywords, true)
60
129
  end
61
130
  end
62
131
  end
@@ -28,10 +28,9 @@ module Bootsnap
28
28
  CACHED_EXTENSIONS = DLEXT2 ? [DOT_RB, DLEXT, DLEXT2] : [DOT_RB, DLEXT]
29
29
 
30
30
  class << self
31
- attr_reader(:load_path_cache, :autoload_paths_cache,
32
- :loaded_features_index, :realpath_cache)
31
+ attr_reader(:load_path_cache, :loaded_features_index, :realpath_cache)
33
32
 
34
- def setup(cache_path:, development_mode:, active_support: true)
33
+ def setup(cache_path:, development_mode:)
35
34
  unless supported?
36
35
  warn("[bootsnap/setup] Load path caching is not supported on this implementation of Ruby") if $VERBOSE
37
36
  return
@@ -45,23 +44,11 @@ module Bootsnap
45
44
  @load_path_cache = Cache.new(store, $LOAD_PATH, development_mode: development_mode)
46
45
  require_relative('load_path_cache/core_ext/kernel_require')
47
46
  require_relative('load_path_cache/core_ext/loaded_features')
48
-
49
- if active_support
50
- # this should happen after setting up the initial cache because it
51
- # loads a lot of code. It's better to do after +require+ is optimized.
52
- require('active_support/dependencies')
53
- @autoload_paths_cache = Cache.new(
54
- store,
55
- ::ActiveSupport::Dependencies.autoload_paths,
56
- development_mode: development_mode
57
- )
58
- require_relative('load_path_cache/core_ext/active_support')
59
- end
60
47
  end
61
48
 
62
49
  def supported?
63
50
  RUBY_ENGINE == 'ruby' &&
64
- RUBY_PLATFORM =~ /darwin|linux|bsd/
51
+ RUBY_PLATFORM =~ /darwin|linux|bsd|mswin|mingw|cygwin/
65
52
  end
66
53
  end
67
54
  end
@@ -10,8 +10,8 @@ module Bootsnap
10
10
  def initialize(store, path_obj, development_mode: false)
11
11
  @development_mode = development_mode
12
12
  @store = store
13
- @mutex = defined?(::Mutex) ? ::Mutex.new : ::Thread::Mutex.new # TODO: Remove once Ruby 2.2 support is dropped.
14
- @path_obj = path_obj.map! { |f| File.exist?(f) ? File.realpath(f) : f }
13
+ @mutex = Mutex.new
14
+ @path_obj = path_obj.map! { |f| PathScanner.os_path(File.exist?(f) ? File.realpath(f) : f.dup) }
15
15
  @has_relative_paths = nil
16
16
  reinitialize
17
17
  end
@@ -46,7 +46,7 @@ module Bootsnap
46
46
  # loadpath.
47
47
  def find(feature)
48
48
  reinitialize if (@has_relative_paths && dir_changed?) || stale?
49
- feature = feature.to_s
49
+ feature = feature.to_s.freeze
50
50
  return feature if absolute_path?(feature)
51
51
  return expand_path(feature) if feature.start_with?('./')
52
52
  @mutex.synchronize do
@@ -194,9 +194,26 @@ module Bootsnap
194
194
  end
195
195
  end
196
196
 
197
- def try_index(f)
198
- if (p = @index[f])
199
- p + '/' + f
197
+ s = rand.to_s.force_encoding(Encoding::US_ASCII).freeze
198
+ if s.respond_to?(:-@)
199
+ if (-s).equal?(s) && (-s.dup).equal?(s)
200
+ def try_index(f)
201
+ if (p = @index[f])
202
+ -(File.join(p, f).freeze)
203
+ end
204
+ end
205
+ else
206
+ def try_index(f)
207
+ if (p = @index[f])
208
+ -File.join(p, f).untaint
209
+ end
210
+ end
211
+ end
212
+ else
213
+ def try_index(f)
214
+ if (p = @index[f])
215
+ File.join(p, f)
216
+ end
200
217
  end
201
218
  end
202
219
 
@@ -26,7 +26,7 @@ module Bootsnap
26
26
  super
27
27
  end
28
28
 
29
- # uniq! keeps the first occurance of each path, otherwise preserving
29
+ # uniq! keeps the first occurrence of each path, otherwise preserving
30
30
  # order, preserving the effective load path
31
31
  def uniq!(*args)
32
32
  ret = super
@@ -38,7 +38,11 @@ module Kernel
38
38
  rescue Bootsnap::LoadPathCache::ReturnFalse
39
39
  false
40
40
  rescue Bootsnap::LoadPathCache::FallbackScan
41
- require_with_bootsnap_lfi(path)
41
+ fallback = true
42
+ ensure
43
+ if fallback
44
+ require_with_bootsnap_lfi(path)
45
+ end
42
46
  end
43
47
 
44
48
  alias_method(:require_relative_without_bootsnap, :require_relative)
@@ -56,7 +60,7 @@ module Kernel
56
60
  end
57
61
 
58
62
  # load also allows relative paths from pwd even when not in $:
59
- if File.exist?(relative = File.expand_path(path))
63
+ if File.exist?(relative = File.expand_path(path).freeze)
60
64
  return load_without_bootsnap(relative, wrap)
61
65
  end
62
66
 
@@ -67,7 +71,11 @@ module Kernel
67
71
  rescue Bootsnap::LoadPathCache::ReturnFalse
68
72
  false
69
73
  rescue Bootsnap::LoadPathCache::FallbackScan
70
- load_without_bootsnap(path, wrap)
74
+ fallback = true
75
+ ensure
76
+ if fallback
77
+ load_without_bootsnap(path, wrap)
78
+ end
71
79
  end
72
80
  end
73
81
 
@@ -88,6 +96,10 @@ class Module
88
96
  rescue Bootsnap::LoadPathCache::ReturnFalse
89
97
  false
90
98
  rescue Bootsnap::LoadPathCache::FallbackScan
91
- autoload_without_bootsnap(const, path)
99
+ fallback = true
100
+ ensure
101
+ if fallback
102
+ autoload_without_bootsnap(const, path)
103
+ end
92
104
  end
93
105
  end
@@ -26,7 +26,7 @@ module Bootsnap
26
26
  class LoadedFeaturesIndex
27
27
  def initialize
28
28
  @lfi = {}
29
- @mutex = defined?(::Mutex) ? ::Mutex.new : ::Thread::Mutex.new # TODO: Remove once Ruby 2.2 support is dropped.
29
+ @mutex = Mutex.new
30
30
 
31
31
  # In theory the user could mutate $LOADED_FEATURES and invalidate our
32
32
  # cache. If this ever comes up in practice — or if you, the
@@ -99,7 +99,7 @@ module Bootsnap
99
99
  altname = if extension_elidable?(short)
100
100
  # Strip the extension off, e.g. 'bundler.rb' -> 'bundler'.
101
101
  strip_extension_if_elidable(short)
102
- elsif long && (ext = File.extname(long))
102
+ elsif long && (ext = File.extname(long.freeze))
103
103
  # We already know the extension of the actual file this
104
104
  # resolves to, so put that back on.
105
105
  short + ext
@@ -129,7 +129,7 @@ module Bootsnap
129
129
  # to name files in a way that assumes otherwise.
130
130
  # (E.g. It's unlikely that someone will know that their code
131
131
  # will _never_ run on MacOS, and therefore think they can get away
132
- # with callling a Ruby file 'x.dylib.rb' and then requiring it as 'x.dylib'.)
132
+ # with calling a Ruby file 'x.dylib.rb' and then requiring it as 'x.dylib'.)
133
133
  #
134
134
  # See <https://ruby-doc.org/core-2.6.4/Kernel.html#method-i-require>.
135
135
  def extension_elidable?(f)
@@ -21,7 +21,7 @@ module Bootsnap
21
21
  attr_reader(:path)
22
22
 
23
23
  def initialize(path)
24
- @path = path.to_s
24
+ @path = path.to_s.freeze
25
25
  end
26
26
 
27
27
  # True if the path exists, but represents a non-directory object
@@ -60,7 +60,7 @@ module Bootsnap
60
60
  end
61
61
 
62
62
  def expanded_path
63
- File.expand_path(path)
63
+ File.expand_path(path).freeze
64
64
  end
65
65
 
66
66
  private
@@ -5,7 +5,6 @@ require_relative('../explicit_require')
5
5
  module Bootsnap
6
6
  module LoadPathCache
7
7
  module PathScanner
8
- ALL_FILES = "/{,**/*/**/}*"
9
8
  REQUIRABLE_EXTENSIONS = [DOT_RB] + DL_EXTENSIONS
10
9
  NORMALIZE_NATIVE_EXTENSIONS = !DL_EXTENSIONS.include?(LoadPathCache::DOT_SO)
11
10
  ALTERNATIVE_NATIVE_EXTENSIONS_PATTERN = /\.(o|bundle|dylib)\z/
@@ -16,34 +15,59 @@ module Bootsnap
16
15
  ''
17
16
  end
18
17
 
19
- def self.call(path)
20
- path = path.to_s
21
-
22
- relative_slice = (path.size + 1)..-1
23
- # If the bundle path is a descendent of this path, we do additional
24
- # checks to prevent recursing into the bundle path as we recurse
25
- # through this path. We don't want to scan the bundle path because
26
- # anything useful in it will be present on other load path items.
27
- #
28
- # This can happen if, for example, the user adds '.' to the load path,
29
- # and the bundle path is '.bundle'.
30
- contains_bundle_path = BUNDLE_PATH.start_with?(path)
31
-
32
- dirs = []
33
- requirables = []
34
-
35
- Dir.glob(path + ALL_FILES).each do |absolute_path|
36
- next if contains_bundle_path && absolute_path.start_with?(BUNDLE_PATH)
37
- relative_path = absolute_path.slice(relative_slice)
38
-
39
- if File.directory?(absolute_path)
40
- dirs << relative_path
41
- elsif REQUIRABLE_EXTENSIONS.include?(File.extname(relative_path))
42
- requirables << relative_path
18
+ class << self
19
+ def call(path)
20
+ path = File.expand_path(path.to_s).freeze
21
+ return [[], []] unless File.directory?(path)
22
+
23
+ # If the bundle path is a descendent of this path, we do additional
24
+ # checks to prevent recursing into the bundle path as we recurse
25
+ # through this path. We don't want to scan the bundle path because
26
+ # anything useful in it will be present on other load path items.
27
+ #
28
+ # This can happen if, for example, the user adds '.' to the load path,
29
+ # and the bundle path is '.bundle'.
30
+ contains_bundle_path = BUNDLE_PATH.start_with?(path)
31
+
32
+ dirs = []
33
+ requirables = []
34
+ walk(path, nil) do |relative_path, absolute_path, is_directory|
35
+ if is_directory
36
+ dirs << os_path(relative_path)
37
+ !contains_bundle_path || !absolute_path.start_with?(BUNDLE_PATH)
38
+ elsif relative_path.end_with?(*REQUIRABLE_EXTENSIONS)
39
+ requirables << os_path(relative_path)
40
+ end
43
41
  end
42
+ [requirables, dirs]
44
43
  end
45
44
 
46
- [requirables, dirs]
45
+ def walk(absolute_dir_path, relative_dir_path, &block)
46
+ Dir.foreach(absolute_dir_path) do |name|
47
+ next if name.start_with?('.')
48
+ relative_path = relative_dir_path ? File.join(relative_dir_path, name) : name
49
+
50
+ absolute_path = "#{absolute_dir_path}/#{name}"
51
+ if File.directory?(absolute_path)
52
+ if yield relative_path, absolute_path, true
53
+ walk(absolute_path, relative_path, &block)
54
+ end
55
+ else
56
+ yield relative_path, absolute_path, false
57
+ end
58
+ end
59
+ end
60
+
61
+ if RUBY_VERSION >= '3.1'
62
+ def os_path(path)
63
+ path.freeze
64
+ end
65
+ else
66
+ def os_path(path)
67
+ path.force_encoding(Encoding::US_ASCII) if path.ascii_only?
68
+ path.freeze
69
+ end
70
+ end
47
71
  end
48
72
  end
49
73
  end