bootsnap 1.6.0 → 1.15.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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)
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,51 @@ 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
 
45
41
  @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')
42
+ PathScanner.ignored_directories = ignore_directories if ignore_directories
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)
60
54
  end
61
55
 
62
56
  def supported?
63
- RUBY_ENGINE == 'ruby' &&
64
- RUBY_PLATFORM =~ /darwin|linux|bsd|mswin|mingw|cygwin/
57
+ RUBY_ENGINE == "ruby" &&
58
+ RUBY_PLATFORM =~ /darwin|linux|bsd|mswin|mingw|cygwin/
65
59
  end
66
60
  end
67
61
  end
68
62
  end
69
63
 
70
64
  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')
65
+ require_relative("load_path_cache/path_scanner")
66
+ require_relative("load_path_cache/path")
67
+ require_relative("load_path_cache/cache")
68
+ require_relative("load_path_cache/store")
69
+ require_relative("load_path_cache/change_observer")
70
+ require_relative("load_path_cache/loaded_features_index")
78
71
  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.15.0"
4
5
  end
data/lib/bootsnap.rb CHANGED
@@ -1,49 +1,138 @@
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!
15
+ self.logger = $stderr.method(:puts)
16
+ end
17
+
18
+ def logger=(logger)
19
+ @logger = logger
20
+ self.instrumentation = if logger.respond_to?(:debug)
21
+ ->(event, path) { @logger.debug("[Bootsnap] #{event} #{path}") }
22
+ else
23
+ ->(event, path) { @logger.call("[Bootsnap] #{event} #{path}") }
24
+ end
25
+ end
26
+
27
+ def instrumentation=(callback)
28
+ @instrumentation = callback
29
+ if respond_to?(:instrumentation_enabled=, true)
30
+ self.instrumentation_enabled = !!callback
31
+ end
32
+ end
33
+
34
+ def _instrument(event, path)
35
+ @instrumentation.call(event, path)
36
+ end
37
+
38
+ def setup(
39
+ cache_dir:,
40
+ development_mode: true,
41
+ load_path_cache: true,
42
+ ignore_directories: nil,
43
+ readonly: false,
44
+ compile_cache_iseq: true,
45
+ compile_cache_yaml: true,
46
+ compile_cache_json: true
36
47
  )
37
- end
48
+ if load_path_cache
49
+ Bootsnap::LoadPathCache.setup(
50
+ cache_path: "#{cache_dir}/bootsnap/load-path-cache",
51
+ development_mode: development_mode,
52
+ ignore_directories: ignore_directories,
53
+ readonly: readonly,
54
+ )
55
+ end
38
56
 
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",
57
+ Bootsnap::CompileCache.setup(
58
+ cache_dir: "#{cache_dir}/bootsnap/compile-cache",
59
+ iseq: compile_cache_iseq,
60
+ yaml: compile_cache_yaml,
61
+ json: compile_cache_json,
62
+ readonly: readonly,
44
63
  )
64
+ end
65
+
66
+ def unload_cache!
67
+ LoadPathCache.unload!
68
+ end
69
+
70
+ def default_setup
71
+ env = ENV["RAILS_ENV"] || ENV["RACK_ENV"] || ENV["ENV"]
72
+ development_mode = ["", nil, "development"].include?(env)
73
+
74
+ unless ENV["DISABLE_BOOTSNAP"]
75
+ cache_dir = ENV["BOOTSNAP_CACHE_DIR"]
76
+ unless cache_dir
77
+ config_dir_frame = caller.detect do |line|
78
+ line.include?("/config/")
79
+ end
80
+
81
+ unless config_dir_frame
82
+ $stderr.puts("[bootsnap/setup] couldn't infer cache directory! Either:")
83
+ $stderr.puts("[bootsnap/setup] 1. require bootsnap/setup from your application's config directory; or")
84
+ $stderr.puts("[bootsnap/setup] 2. Define the environment variable BOOTSNAP_CACHE_DIR")
85
+
86
+ raise("couldn't infer bootsnap cache directory")
87
+ end
88
+
89
+ path = config_dir_frame.split(/:\d+:/).first
90
+ path = File.dirname(path) until File.basename(path) == "config"
91
+ app_root = File.dirname(path)
92
+
93
+ cache_dir = File.join(app_root, "tmp", "cache")
94
+ end
95
+
96
+ ignore_directories = if ENV.key?("BOOTSNAP_IGNORE_DIRECTORIES")
97
+ ENV["BOOTSNAP_IGNORE_DIRECTORIES"].split(",")
98
+ end
99
+
100
+ setup(
101
+ cache_dir: cache_dir,
102
+ development_mode: development_mode,
103
+ load_path_cache: !ENV["DISABLE_BOOTSNAP_LOAD_PATH_CACHE"],
104
+ compile_cache_iseq: !ENV["DISABLE_BOOTSNAP_COMPILE_CACHE"],
105
+ compile_cache_yaml: !ENV["DISABLE_BOOTSNAP_COMPILE_CACHE"],
106
+ compile_cache_json: !ENV["DISABLE_BOOTSNAP_COMPILE_CACHE"],
107
+ readonly: !!ENV["BOOTSNAP_READONLY"],
108
+ ignore_directories: ignore_directories,
109
+ )
110
+
111
+ if ENV["BOOTSNAP_LOG"]
112
+ log!
113
+ end
114
+ end
115
+ end
116
+
117
+ if RbConfig::CONFIG["host_os"] =~ /mswin|mingw|cygwin/
118
+ def absolute_path?(path)
119
+ path[1] == ":"
120
+ end
45
121
  else
46
- RubyVM::InstructionSequence.compile_option = { trace_instruction: false }
122
+ def absolute_path?(path)
123
+ path.start_with?("/")
124
+ end
47
125
  end
126
+
127
+ # This is a semi-accurate ruby implementation of the native `rb_get_path(VALUE)` function.
128
+ # The native version is very intricate and may behave differently on windows etc.
129
+ # But we only use it for non-MRI platform.
130
+ def rb_get_path(fname)
131
+ path_path = fname.respond_to?(:to_path) ? fname.to_path : fname
132
+ String.try_convert(path_path) || raise(TypeError, "no implicit conversion of #{path_path.class} into String")
133
+ end
134
+
135
+ # Allow the C extension to redefine `rb_get_path` without warning.
136
+ alias_method :rb_get_path, :rb_get_path # rubocop:disable Lint/DuplicateMethods
48
137
  end
49
138
  end