bootsnap 1.4.5 → 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.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +264 -0
  3. data/LICENSE.txt +1 -1
  4. data/README.md +63 -23
  5. data/exe/bootsnap +5 -0
  6. data/ext/bootsnap/bootsnap.c +504 -184
  7. data/ext/bootsnap/extconf.rb +30 -15
  8. data/lib/bootsnap/bundler.rb +3 -1
  9. data/lib/bootsnap/cli/worker_pool.rb +136 -0
  10. data/lib/bootsnap/cli.rb +283 -0
  11. data/lib/bootsnap/compile_cache/iseq.rb +72 -21
  12. data/lib/bootsnap/compile_cache/json.rb +89 -0
  13. data/lib/bootsnap/compile_cache/yaml.rb +316 -41
  14. data/lib/bootsnap/compile_cache.rb +27 -17
  15. data/lib/bootsnap/explicit_require.rb +5 -3
  16. data/lib/bootsnap/load_path_cache/cache.rb +73 -37
  17. data/lib/bootsnap/load_path_cache/change_observer.rb +25 -3
  18. data/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb +27 -82
  19. data/lib/bootsnap/load_path_cache/core_ext/loaded_features.rb +2 -0
  20. data/lib/bootsnap/load_path_cache/loaded_features_index.rb +63 -29
  21. data/lib/bootsnap/load_path_cache/path.rb +42 -19
  22. data/lib/bootsnap/load_path_cache/path_scanner.rb +60 -29
  23. data/lib/bootsnap/load_path_cache/store.rb +64 -23
  24. data/lib/bootsnap/load_path_cache.rb +40 -38
  25. data/lib/bootsnap/setup.rb +3 -36
  26. data/lib/bootsnap/version.rb +3 -1
  27. data/lib/bootsnap.rb +141 -36
  28. metadata +15 -99
  29. data/.github/CODEOWNERS +0 -2
  30. data/.github/probots.yml +0 -2
  31. data/.gitignore +0 -17
  32. data/.rubocop.yml +0 -20
  33. data/.travis.yml +0 -21
  34. data/CODE_OF_CONDUCT.md +0 -74
  35. data/CONTRIBUTING.md +0 -21
  36. data/Gemfile +0 -8
  37. data/README.jp.md +0 -231
  38. data/Rakefile +0 -12
  39. data/bin/ci +0 -10
  40. data/bin/console +0 -14
  41. data/bin/setup +0 -8
  42. data/bin/test-minimal-support +0 -7
  43. data/bin/testunit +0 -8
  44. data/bootsnap.gemspec +0 -45
  45. data/dev.yml +0 -10
  46. data/lib/bootsnap/load_path_cache/core_ext/active_support.rb +0 -106
  47. data/lib/bootsnap/load_path_cache/realpath_cache.rb +0 -32
  48. data/shipit.rubygems.yml +0 -0
@@ -1,19 +1,23 @@
1
- require_relative('../explicit_require')
1
+ # frozen_string_literal: true
2
2
 
3
- Bootsnap::ExplicitRequire.with_gems('msgpack') { require('msgpack') }
4
- Bootsnap::ExplicitRequire.from_rubylibdir('fileutils')
3
+ require_relative "../explicit_require"
4
+
5
+ Bootsnap::ExplicitRequire.with_gems("msgpack") { require "msgpack" }
5
6
 
6
7
  module Bootsnap
7
8
  module LoadPathCache
8
9
  class Store
10
+ VERSION_KEY = "__bootsnap_ruby_version__"
11
+ CURRENT_VERSION = "#{RUBY_REVISION}-#{RUBY_PLATFORM}".freeze # rubocop:disable Style/RedundantFreeze
12
+
9
13
  NestedTransactionError = Class.new(StandardError)
10
14
  SetOutsideTransactionNotAllowed = Class.new(StandardError)
11
15
 
12
- def initialize(store_path)
16
+ def initialize(store_path, readonly: false)
13
17
  @store_path = store_path
14
- # TODO: Remove conditional once Ruby 2.2 support is dropped.
15
- @txn_mutex = defined?(::Mutex) ? ::Mutex.new : ::Thread::Mutex.new
18
+ @txn_mutex = Mutex.new
16
19
  @dirty = false
20
+ @readonly = readonly
17
21
  load_data
18
22
  end
19
23
 
@@ -23,10 +27,11 @@ module Bootsnap
23
27
 
24
28
  def fetch(key)
25
29
  raise(SetOutsideTransactionNotAllowed) unless @txn_mutex.owned?
30
+
26
31
  v = get(key)
27
32
  unless v
28
- @dirty = true
29
33
  v = yield
34
+ mark_for_mutation!
30
35
  @data[key] = v
31
36
  end
32
37
  v
@@ -34,27 +39,32 @@ module Bootsnap
34
39
 
35
40
  def set(key, value)
36
41
  raise(SetOutsideTransactionNotAllowed) unless @txn_mutex.owned?
42
+
37
43
  if value != @data[key]
38
- @dirty = true
44
+ mark_for_mutation!
39
45
  @data[key] = value
40
46
  end
41
47
  end
42
48
 
43
49
  def transaction
44
50
  raise(NestedTransactionError) if @txn_mutex.owned?
51
+
45
52
  @txn_mutex.synchronize do
46
- begin
47
- yield
48
- ensure
49
- commit_transaction
50
- end
53
+ yield
54
+ ensure
55
+ commit_transaction
51
56
  end
52
57
  end
53
58
 
54
59
  private
55
60
 
61
+ def mark_for_mutation!
62
+ @dirty = true
63
+ @data = @data.dup if @data.frozen?
64
+ end
65
+
56
66
  def commit_transaction
57
- if @dirty
67
+ if @dirty && !@readonly
58
68
  dump_data
59
69
  @dirty = false
60
70
  end
@@ -62,27 +72,58 @@ module Bootsnap
62
72
 
63
73
  def load_data
64
74
  @data = begin
65
- MessagePack.load(File.binread(@store_path))
66
- # handle malformed data due to upgrade incompatability
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
67
84
  rescue Errno::ENOENT, MessagePack::MalformedFormatError, MessagePack::UnknownExtTypeError, EOFError
68
- {}
69
- rescue ArgumentError => e
70
- e.message =~ /negative array size/ ? {} : raise
85
+ default_data
86
+ rescue ArgumentError => error
87
+ if error.message =~ /negative array size/
88
+ default_data
89
+ else
90
+ raise
91
+ end
71
92
  end
72
93
  end
73
94
 
74
95
  def dump_data
75
96
  # Change contents atomically so other processes can't get invalid
76
97
  # caches if they read at an inopportune time.
77
- tmp = "#{@store_path}.#{Process.pid}.#{(rand * 100000).to_i}.tmp"
78
- FileUtils.mkpath(File.dirname(tmp))
98
+ tmp = "#{@store_path}.#{Process.pid}.#{(rand * 100_000).to_i}.tmp"
99
+ mkdir_p(File.dirname(tmp))
79
100
  exclusive_write = File::Constants::CREAT | File::Constants::EXCL | File::Constants::WRONLY
80
101
  # `encoding:` looks redundant wrt `binwrite`, but necessary on windows
81
102
  # because binary is part of mode.
82
- File.binwrite(tmp, MessagePack.dump(@data), mode: exclusive_write, encoding: Encoding::BINARY)
83
- 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)
84
107
  rescue Errno::EEXIST
85
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
86
127
  end
87
128
  end
88
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/
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,38 +1,5 @@
1
- require_relative('../bootsnap')
1
+ # frozen_string_literal: true
2
2
 
3
- env = ENV['RAILS_ENV'] || ENV['RACK_ENV'] || ENV['ENV']
4
- development_mode = ['', nil, 'development'].include?(env)
3
+ require_relative "../bootsnap"
5
4
 
6
- cache_dir = ENV['BOOTSNAP_CACHE_DIR']
7
- unless cache_dir
8
- config_dir_frame = caller.detect do |line|
9
- line.include?('/config/')
10
- end
11
-
12
- unless config_dir_frame
13
- $stderr.puts("[bootsnap/setup] couldn't infer cache directory! Either:")
14
- $stderr.puts("[bootsnap/setup] 1. require bootsnap/setup from your application's config directory; or")
15
- $stderr.puts("[bootsnap/setup] 2. Define the environment variable BOOTSNAP_CACHE_DIR")
16
-
17
- raise("couldn't infer bootsnap cache directory")
18
- end
19
-
20
- path = config_dir_frame.split(/:\d+:/).first
21
- path = File.dirname(path) until File.basename(path) == 'config'
22
- app_root = File.dirname(path)
23
-
24
- cache_dir = File.join(app_root, 'tmp', 'cache')
25
- end
26
-
27
- ruby_version = Gem::Version.new(RUBY_VERSION)
28
- iseq_cache_enabled = ruby_version < Gem::Version.new('2.5.0') || ruby_version >= Gem::Version.new('2.6.0')
29
-
30
- Bootsnap.setup(
31
- cache_dir: cache_dir,
32
- development_mode: development_mode,
33
- load_path_cache: true,
34
- autoload_paths_cache: true, # assume rails. open to PRs to impl. detection
35
- disable_trace: false,
36
- compile_cache_iseq: iseq_cache_enabled,
37
- compile_cache_yaml: true,
38
- )
5
+ Bootsnap.default_setup
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Bootsnap
2
- VERSION = "1.4.5"
4
+ VERSION = "1.18.3"
3
5
  end
data/lib/bootsnap.rb CHANGED
@@ -1,47 +1,152 @@
1
- require_relative('bootsnap/version')
2
- require_relative('bootsnap/bundler')
3
- require_relative('bootsnap/load_path_cache')
4
- require_relative('bootsnap/compile_cache')
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "bootsnap/version"
4
+ require_relative "bootsnap/bundler"
5
+ require_relative "bootsnap/load_path_cache"
6
+ require_relative "bootsnap/compile_cache"
5
7
 
6
8
  module Bootsnap
7
9
  InvalidConfiguration = Class.new(StandardError)
8
10
 
9
- def self.setup(
10
- cache_dir:,
11
- development_mode: true,
12
- load_path_cache: true,
13
- autoload_paths_cache: true,
14
- disable_trace: false,
15
- compile_cache_iseq: true,
16
- compile_cache_yaml: true
17
- )
18
- if autoload_paths_cache && !load_path_cache
19
- raise(InvalidConfiguration, "feature 'autoload_paths_cache' depends on feature 'load_path_cache'")
20
- end
21
-
22
- setup_disable_trace if disable_trace
23
-
24
- Bootsnap::LoadPathCache.setup(
25
- cache_path: cache_dir + '/bootsnap-load-path-cache',
26
- development_mode: development_mode,
27
- active_support: autoload_paths_cache
28
- ) if load_path_cache
29
-
30
- Bootsnap::CompileCache.setup(
31
- cache_dir: cache_dir + '/bootsnap-compile-cache',
32
- iseq: compile_cache_iseq,
33
- 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
34
58
  )
35
- 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
36
67
 
37
- def self.setup_disable_trace
38
- if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('2.5.0')
39
- warn(
40
- "from #{caller_locations(1, 1)[0]}: The 'disable_trace' method is not allowed with this Ruby version. " \
41
- "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,
42
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
43
135
  else
44
- RubyVM::InstructionSequence.compile_option = { trace_instruction: false }
136
+ def absolute_path?(path)
137
+ path.start_with?("/")
138
+ end
45
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
46
151
  end
47
152
  end