bootsnap 1.6.0 → 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.
@@ -1,21 +1,25 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative('../explicit_require')
3
+ require_relative "../explicit_require"
4
4
 
5
5
  module Bootsnap
6
6
  module LoadPathCache
7
7
  module PathScanner
8
8
  REQUIRABLE_EXTENSIONS = [DOT_RB] + DL_EXTENSIONS
9
9
  NORMALIZE_NATIVE_EXTENSIONS = !DL_EXTENSIONS.include?(LoadPathCache::DOT_SO)
10
- ALTERNATIVE_NATIVE_EXTENSIONS_PATTERN = /\.(o|bundle|dylib)\z/
10
+ ALTERNATIVE_NATIVE_EXTENSIONS_PATTERN = /\.(o|bundle|dylib)\z/.freeze
11
11
 
12
12
  BUNDLE_PATH = if Bootsnap.bundler?
13
13
  (Bundler.bundle_path.cleanpath.to_s << LoadPathCache::SLASH).freeze
14
14
  else
15
- ''
15
+ ""
16
16
  end
17
17
 
18
+ @ignored_directories = %w(node_modules)
19
+
18
20
  class << self
21
+ attr_accessor :ignored_directories
22
+
19
23
  def call(path)
20
24
  path = File.expand_path(path.to_s).freeze
21
25
  return [[], []] unless File.directory?(path)
@@ -33,10 +37,10 @@ module Bootsnap
33
37
  requirables = []
34
38
  walk(path, nil) do |relative_path, absolute_path, is_directory|
35
39
  if is_directory
36
- dirs << relative_path
40
+ dirs << os_path(relative_path)
37
41
  !contains_bundle_path || !absolute_path.start_with?(BUNDLE_PATH)
38
42
  elsif relative_path.end_with?(*REQUIRABLE_EXTENSIONS)
39
- requirables << relative_path
43
+ requirables << os_path(relative_path)
40
44
  end
41
45
  end
42
46
  [requirables, dirs]
@@ -44,11 +48,14 @@ module Bootsnap
44
48
 
45
49
  def walk(absolute_dir_path, relative_dir_path, &block)
46
50
  Dir.foreach(absolute_dir_path) do |name|
47
- next if name.start_with?('.')
48
- relative_path = relative_dir_path ? "#{relative_dir_path}/#{name}" : name.freeze
51
+ next if name.start_with?(".")
52
+
53
+ relative_path = relative_dir_path ? File.join(relative_dir_path, name) : name
49
54
 
50
55
  absolute_path = "#{absolute_dir_path}/#{name}"
51
56
  if File.directory?(absolute_path)
57
+ next if ignored_directories.include?(name) || ignored_directories.include?(absolute_path)
58
+
52
59
  if yield relative_path, absolute_path, true
53
60
  walk(absolute_path, relative_path, &block)
54
61
  end
@@ -57,6 +64,17 @@ module Bootsnap
57
64
  end
58
65
  end
59
66
  end
67
+
68
+ if RUBY_VERSION >= "3.1"
69
+ def os_path(path)
70
+ path.freeze
71
+ end
72
+ else
73
+ def os_path(path)
74
+ path.force_encoding(Encoding::US_ASCII) if path.ascii_only?
75
+ path.freeze
76
+ end
77
+ end
60
78
  end
61
79
  end
62
80
  end
@@ -1,20 +1,23 @@
1
1
  # frozen_string_literal: true
2
- require_relative('../explicit_require')
3
2
 
4
- Bootsnap::ExplicitRequire.with_gems('msgpack') { require('msgpack') }
5
- Bootsnap::ExplicitRequire.from_rubylibdir('fileutils')
3
+ require_relative "../explicit_require"
4
+
5
+ Bootsnap::ExplicitRequire.with_gems("msgpack") { require "msgpack" }
6
6
 
7
7
  module Bootsnap
8
8
  module LoadPathCache
9
9
  class Store
10
+ VERSION_KEY = "__bootsnap_ruby_version__"
11
+ CURRENT_VERSION = "#{RUBY_REVISION}-#{RUBY_PLATFORM}".freeze # rubocop:disable Style/RedundantFreeze
12
+
10
13
  NestedTransactionError = Class.new(StandardError)
11
14
  SetOutsideTransactionNotAllowed = Class.new(StandardError)
12
15
 
13
- def initialize(store_path)
16
+ def initialize(store_path, readonly: false)
14
17
  @store_path = store_path
15
- # TODO: Remove conditional once Ruby 2.2 support is dropped.
16
- @txn_mutex = defined?(::Mutex) ? ::Mutex.new : ::Thread::Mutex.new
18
+ @txn_mutex = Mutex.new
17
19
  @dirty = false
20
+ @readonly = readonly
18
21
  load_data
19
22
  end
20
23
 
@@ -24,10 +27,11 @@ module Bootsnap
24
27
 
25
28
  def fetch(key)
26
29
  raise(SetOutsideTransactionNotAllowed) unless @txn_mutex.owned?
30
+
27
31
  v = get(key)
28
32
  unless v
29
- @dirty = true
30
33
  v = yield
34
+ mark_for_mutation!
31
35
  @data[key] = v
32
36
  end
33
37
  v
@@ -35,27 +39,32 @@ module Bootsnap
35
39
 
36
40
  def set(key, value)
37
41
  raise(SetOutsideTransactionNotAllowed) unless @txn_mutex.owned?
42
+
38
43
  if value != @data[key]
39
- @dirty = true
44
+ mark_for_mutation!
40
45
  @data[key] = value
41
46
  end
42
47
  end
43
48
 
44
49
  def transaction
45
50
  raise(NestedTransactionError) if @txn_mutex.owned?
51
+
46
52
  @txn_mutex.synchronize do
47
- begin
48
- yield
49
- ensure
50
- commit_transaction
51
- end
53
+ yield
54
+ ensure
55
+ commit_transaction
52
56
  end
53
57
  end
54
58
 
55
59
  private
56
60
 
61
+ def mark_for_mutation!
62
+ @dirty = true
63
+ @data = @data.dup if @data.frozen?
64
+ end
65
+
57
66
  def commit_transaction
58
- if @dirty
67
+ if @dirty && !@readonly
59
68
  dump_data
60
69
  @dirty = false
61
70
  end
@@ -63,27 +72,58 @@ module Bootsnap
63
72
 
64
73
  def load_data
65
74
  @data = begin
66
- MessagePack.load(File.binread(@store_path))
67
- # handle malformed data due to upgrade incompatibility
68
- rescue Errno::ENOENT, MessagePack::MalformedFormatError, MessagePack::UnknownExtTypeError, EOFError
69
- {}
70
- rescue ArgumentError => e
71
- e.message =~ /negative array size/ ? {} : raise
75
+ data = File.open(@store_path, encoding: Encoding::BINARY) do |io|
76
+ MessagePack.load(io, freeze: true)
77
+ end
78
+ if data.is_a?(Hash) && data[VERSION_KEY] == CURRENT_VERSION
79
+ data
80
+ else
81
+ default_data
82
+ end
83
+ # handle malformed data due to upgrade incompatibility
84
+ rescue Errno::ENOENT, MessagePack::MalformedFormatError, MessagePack::UnknownExtTypeError, EOFError
85
+ default_data
86
+ rescue ArgumentError => error
87
+ if error.message =~ /negative array size/
88
+ default_data
89
+ else
90
+ raise
91
+ end
72
92
  end
73
93
  end
74
94
 
75
95
  def dump_data
76
96
  # Change contents atomically so other processes can't get invalid
77
97
  # caches if they read at an inopportune time.
78
- tmp = "#{@store_path}.#{Process.pid}.#{(rand * 100000).to_i}.tmp"
79
- FileUtils.mkpath(File.dirname(tmp))
98
+ tmp = "#{@store_path}.#{Process.pid}.#{(rand * 100_000).to_i}.tmp"
99
+ mkdir_p(File.dirname(tmp))
80
100
  exclusive_write = File::Constants::CREAT | File::Constants::EXCL | File::Constants::WRONLY
81
101
  # `encoding:` looks redundant wrt `binwrite`, but necessary on windows
82
102
  # because binary is part of mode.
83
- File.binwrite(tmp, MessagePack.dump(@data), mode: exclusive_write, encoding: Encoding::BINARY)
84
- FileUtils.mv(tmp, @store_path)
103
+ File.open(tmp, mode: exclusive_write, encoding: Encoding::BINARY) do |io|
104
+ MessagePack.dump(@data, io)
105
+ end
106
+ File.rename(tmp, @store_path)
85
107
  rescue Errno::EEXIST
86
108
  retry
109
+ rescue SystemCallError
110
+ end
111
+
112
+ def default_data
113
+ {VERSION_KEY => CURRENT_VERSION}
114
+ end
115
+
116
+ def mkdir_p(path)
117
+ stack = []
118
+ until File.directory?(path)
119
+ stack.push path
120
+ path = File.dirname(path)
121
+ end
122
+ stack.reverse_each do |dir|
123
+ Dir.mkdir(dir)
124
+ rescue SystemCallError
125
+ raise unless File.directory?(dir)
126
+ end
87
127
  end
88
128
  end
89
129
  end
@@ -2,20 +2,14 @@
2
2
 
3
3
  module Bootsnap
4
4
  module LoadPathCache
5
- ReturnFalse = Class.new(StandardError)
6
- FallbackScan = Class.new(StandardError)
5
+ FALLBACK_SCAN = BasicObject.new
7
6
 
8
- DOT_RB = '.rb'
9
- DOT_SO = '.so'
10
- SLASH = '/'
11
-
12
- # If a NameError happens several levels deep, don't re-handle it
13
- # all the way up the chain: mark it once and bubble it up without
14
- # more retries.
15
- ERROR_TAG_IVAR = :@__bootsnap_rescued
7
+ DOT_RB = ".rb"
8
+ DOT_SO = ".so"
9
+ SLASH = "/"
16
10
 
17
11
  DL_EXTENSIONS = ::RbConfig::CONFIG
18
- .values_at('DLEXT', 'DLEXT2')
12
+ .values_at("DLEXT", "DLEXT2")
19
13
  .reject { |ext| !ext || ext.empty? }
20
14
  .map { |ext| ".#{ext}" }
21
15
  .freeze
@@ -27,52 +21,60 @@ module Bootsnap
27
21
 
28
22
  CACHED_EXTENSIONS = DLEXT2 ? [DOT_RB, DLEXT, DLEXT2] : [DOT_RB, DLEXT]
29
23
 
24
+ @enabled = false
25
+
30
26
  class << self
31
- attr_reader(:load_path_cache, :autoload_paths_cache,
32
- :loaded_features_index, :realpath_cache)
27
+ attr_reader(:load_path_cache, :loaded_features_index, :enabled)
28
+ alias_method :enabled?, :enabled
29
+ remove_method(:enabled)
33
30
 
34
- def setup(cache_path:, development_mode:, active_support: true)
31
+ def setup(cache_path:, development_mode:, ignore_directories:, readonly: false)
35
32
  unless supported?
36
33
  warn("[bootsnap/setup] Load path caching is not supported on this implementation of Ruby") if $VERBOSE
37
34
  return
38
35
  end
39
36
 
40
- store = Store.new(cache_path)
37
+ store = Store.new(cache_path, readonly: readonly)
41
38
 
42
39
  @loaded_features_index = LoadedFeaturesIndex.new
43
- @realpath_cache = RealpathCache.new
44
40
 
41
+ PathScanner.ignored_directories = ignore_directories if ignore_directories
45
42
  @load_path_cache = Cache.new(store, $LOAD_PATH, development_mode: development_mode)
46
- require_relative('load_path_cache/core_ext/kernel_require')
47
- require_relative('load_path_cache/core_ext/loaded_features')
43
+ @enabled = true
44
+ require_relative "load_path_cache/core_ext/kernel_require"
45
+ require_relative "load_path_cache/core_ext/loaded_features"
46
+ end
48
47
 
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
48
+ def unload!
49
+ @enabled = false
50
+ @loaded_features_index = nil
51
+ @realpath_cache = nil
52
+ @load_path_cache = nil
53
+ ChangeObserver.unregister($LOAD_PATH) if supported?
60
54
  end
61
55
 
62
56
  def supported?
63
- RUBY_ENGINE == 'ruby' &&
64
- RUBY_PLATFORM =~ /darwin|linux|bsd|mswin|mingw|cygwin/
57
+ if RUBY_PLATFORM.match?(/darwin|linux|bsd|mswin|mingw|cygwin/)
58
+ case RUBY_ENGINE
59
+ when "truffleruby"
60
+ # https://github.com/oracle/truffleruby/issues/3131
61
+ RUBY_ENGINE_VERSION >= "23.1.0"
62
+ when "ruby"
63
+ true
64
+ else
65
+ false
66
+ end
67
+ end
65
68
  end
66
69
  end
67
70
  end
68
71
  end
69
72
 
70
73
  if Bootsnap::LoadPathCache.supported?
71
- require_relative('load_path_cache/path_scanner')
72
- require_relative('load_path_cache/path')
73
- require_relative('load_path_cache/cache')
74
- require_relative('load_path_cache/store')
75
- require_relative('load_path_cache/change_observer')
76
- require_relative('load_path_cache/loaded_features_index')
77
- require_relative('load_path_cache/realpath_cache')
74
+ require_relative "load_path_cache/path_scanner"
75
+ require_relative "load_path_cache/path"
76
+ require_relative "load_path_cache/cache"
77
+ require_relative "load_path_cache/store"
78
+ require_relative "load_path_cache/change_observer"
79
+ require_relative "load_path_cache/loaded_features_index"
78
80
  end
@@ -1,39 +1,5 @@
1
1
  # frozen_string_literal: true
2
- require_relative('../bootsnap')
3
2
 
4
- env = ENV['RAILS_ENV'] || ENV['RACK_ENV'] || ENV['ENV']
5
- development_mode = ['', nil, 'development'].include?(env)
3
+ require_relative "../bootsnap"
6
4
 
7
- cache_dir = ENV['BOOTSNAP_CACHE_DIR']
8
- unless cache_dir
9
- config_dir_frame = caller.detect do |line|
10
- line.include?('/config/')
11
- end
12
-
13
- unless config_dir_frame
14
- $stderr.puts("[bootsnap/setup] couldn't infer cache directory! Either:")
15
- $stderr.puts("[bootsnap/setup] 1. require bootsnap/setup from your application's config directory; or")
16
- $stderr.puts("[bootsnap/setup] 2. Define the environment variable BOOTSNAP_CACHE_DIR")
17
-
18
- raise("couldn't infer bootsnap cache directory")
19
- end
20
-
21
- path = config_dir_frame.split(/:\d+:/).first
22
- path = File.dirname(path) until File.basename(path) == 'config'
23
- app_root = File.dirname(path)
24
-
25
- cache_dir = File.join(app_root, 'tmp', 'cache')
26
- end
27
-
28
- ruby_version = Gem::Version.new(RUBY_VERSION)
29
- iseq_cache_enabled = ruby_version < Gem::Version.new('2.5.0') || ruby_version >= Gem::Version.new('2.6.0')
30
-
31
- Bootsnap.setup(
32
- cache_dir: cache_dir,
33
- development_mode: development_mode,
34
- load_path_cache: true,
35
- autoload_paths_cache: true, # assume rails. open to PRs to impl. detection
36
- disable_trace: false,
37
- compile_cache_iseq: iseq_cache_enabled,
38
- compile_cache_yaml: true,
39
- )
5
+ Bootsnap.default_setup
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module Bootsnap
3
- VERSION = "1.6.0"
4
+ VERSION = "1.18.3"
4
5
  end
data/lib/bootsnap.rb CHANGED
@@ -1,49 +1,152 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative('bootsnap/version')
4
- require_relative('bootsnap/bundler')
5
- require_relative('bootsnap/load_path_cache')
6
- require_relative('bootsnap/compile_cache')
3
+ require_relative "bootsnap/version"
4
+ require_relative "bootsnap/bundler"
5
+ require_relative "bootsnap/load_path_cache"
6
+ require_relative "bootsnap/compile_cache"
7
7
 
8
8
  module Bootsnap
9
9
  InvalidConfiguration = Class.new(StandardError)
10
10
 
11
- def self.setup(
12
- cache_dir:,
13
- development_mode: true,
14
- load_path_cache: true,
15
- autoload_paths_cache: true,
16
- disable_trace: false,
17
- compile_cache_iseq: true,
18
- compile_cache_yaml: true
19
- )
20
- if autoload_paths_cache && !load_path_cache
21
- raise(InvalidConfiguration, "feature 'autoload_paths_cache' depends on feature 'load_path_cache'")
22
- end
23
-
24
- setup_disable_trace if disable_trace
25
-
26
- Bootsnap::LoadPathCache.setup(
27
- cache_path: cache_dir + '/bootsnap/load-path-cache',
28
- development_mode: development_mode,
29
- active_support: autoload_paths_cache
30
- ) if load_path_cache
31
-
32
- Bootsnap::CompileCache.setup(
33
- cache_dir: cache_dir + '/bootsnap/compile-cache',
34
- iseq: compile_cache_iseq,
35
- yaml: compile_cache_yaml
11
+ class << self
12
+ attr_reader :logger
13
+
14
+ def log_stats!
15
+ stats = {hit: 0, revalidated: 0, miss: 0, stale: 0}
16
+ self.instrumentation = ->(event, _path) { stats[event] += 1 }
17
+ Kernel.at_exit do
18
+ stats.each do |event, count|
19
+ $stderr.puts "bootsnap #{event}: #{count}"
20
+ end
21
+ end
22
+ end
23
+
24
+ def log!
25
+ self.logger = $stderr.method(:puts)
26
+ end
27
+
28
+ def logger=(logger)
29
+ @logger = logger
30
+ self.instrumentation = if logger.respond_to?(:debug)
31
+ ->(event, path) { @logger.debug("[Bootsnap] #{event} #{path}") unless event == :hit }
32
+ else
33
+ ->(event, path) { @logger.call("[Bootsnap] #{event} #{path}") unless event == :hit }
34
+ end
35
+ end
36
+
37
+ def instrumentation=(callback)
38
+ @instrumentation = callback
39
+ if respond_to?(:instrumentation_enabled=, true)
40
+ self.instrumentation_enabled = !!callback
41
+ end
42
+ end
43
+
44
+ def _instrument(event, path)
45
+ @instrumentation.call(event, path)
46
+ end
47
+
48
+ def setup(
49
+ cache_dir:,
50
+ development_mode: true,
51
+ load_path_cache: true,
52
+ ignore_directories: nil,
53
+ readonly: false,
54
+ revalidation: false,
55
+ compile_cache_iseq: true,
56
+ compile_cache_yaml: true,
57
+ compile_cache_json: true
36
58
  )
37
- end
59
+ if load_path_cache
60
+ Bootsnap::LoadPathCache.setup(
61
+ cache_path: "#{cache_dir}/bootsnap/load-path-cache",
62
+ development_mode: development_mode,
63
+ ignore_directories: ignore_directories,
64
+ readonly: readonly,
65
+ )
66
+ end
38
67
 
39
- def self.setup_disable_trace
40
- if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('2.5.0')
41
- warn(
42
- "from #{caller_locations(1, 1)[0]}: The 'disable_trace' method is not allowed with this Ruby version. " \
43
- "current: #{RUBY_VERSION}, allowed version: < 2.5.0",
68
+ Bootsnap::CompileCache.setup(
69
+ cache_dir: "#{cache_dir}/bootsnap/compile-cache",
70
+ iseq: compile_cache_iseq,
71
+ yaml: compile_cache_yaml,
72
+ json: compile_cache_json,
73
+ readonly: readonly,
74
+ revalidation: revalidation,
44
75
  )
76
+ end
77
+
78
+ def unload_cache!
79
+ LoadPathCache.unload!
80
+ end
81
+
82
+ def default_setup
83
+ env = ENV["RAILS_ENV"] || ENV["RACK_ENV"] || ENV["ENV"]
84
+ development_mode = ["", nil, "development"].include?(env)
85
+
86
+ unless ENV["DISABLE_BOOTSNAP"]
87
+ cache_dir = ENV["BOOTSNAP_CACHE_DIR"]
88
+ unless cache_dir
89
+ config_dir_frame = caller.detect do |line|
90
+ line.include?("/config/")
91
+ end
92
+
93
+ unless config_dir_frame
94
+ $stderr.puts("[bootsnap/setup] couldn't infer cache directory! Either:")
95
+ $stderr.puts("[bootsnap/setup] 1. require bootsnap/setup from your application's config directory; or")
96
+ $stderr.puts("[bootsnap/setup] 2. Define the environment variable BOOTSNAP_CACHE_DIR")
97
+
98
+ raise("couldn't infer bootsnap cache directory")
99
+ end
100
+
101
+ path = config_dir_frame.split(/:\d+:/).first
102
+ path = File.dirname(path) until File.basename(path) == "config"
103
+ app_root = File.dirname(path)
104
+
105
+ cache_dir = File.join(app_root, "tmp", "cache")
106
+ end
107
+
108
+ ignore_directories = if ENV.key?("BOOTSNAP_IGNORE_DIRECTORIES")
109
+ ENV["BOOTSNAP_IGNORE_DIRECTORIES"].split(",")
110
+ end
111
+
112
+ setup(
113
+ cache_dir: cache_dir,
114
+ development_mode: development_mode,
115
+ load_path_cache: !ENV["DISABLE_BOOTSNAP_LOAD_PATH_CACHE"],
116
+ compile_cache_iseq: !ENV["DISABLE_BOOTSNAP_COMPILE_CACHE"],
117
+ compile_cache_yaml: !ENV["DISABLE_BOOTSNAP_COMPILE_CACHE"],
118
+ compile_cache_json: !ENV["DISABLE_BOOTSNAP_COMPILE_CACHE"],
119
+ readonly: !!ENV["BOOTSNAP_READONLY"],
120
+ ignore_directories: ignore_directories,
121
+ )
122
+
123
+ if ENV["BOOTSNAP_LOG"]
124
+ log!
125
+ elsif ENV["BOOTSNAP_STATS"]
126
+ log_stats!
127
+ end
128
+ end
129
+ end
130
+
131
+ if RbConfig::CONFIG["host_os"] =~ /mswin|mingw|cygwin/
132
+ def absolute_path?(path)
133
+ path[1] == ":"
134
+ end
45
135
  else
46
- RubyVM::InstructionSequence.compile_option = { trace_instruction: false }
136
+ def absolute_path?(path)
137
+ path.start_with?("/")
138
+ end
47
139
  end
140
+
141
+ # This is a semi-accurate ruby implementation of the native `rb_get_path(VALUE)` function.
142
+ # The native version is very intricate and may behave differently on windows etc.
143
+ # But we only use it for non-MRI platform.
144
+ def rb_get_path(fname)
145
+ path_path = fname.respond_to?(:to_path) ? fname.to_path : fname
146
+ String.try_convert(path_path) || raise(TypeError, "no implicit conversion of #{path_path.class} into String")
147
+ end
148
+
149
+ # Allow the C extension to redefine `rb_get_path` without warning.
150
+ alias_method :rb_get_path, :rb_get_path # rubocop:disable Lint/DuplicateMethods
48
151
  end
49
152
  end