bootsnap 1.4.6 → 1.18.3

Sign up to get free protection for your applications and to get access to all the features.
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 +487 -171
  7. data/ext/bootsnap/extconf.rb +29 -15
  8. data/lib/bootsnap/bundler.rb +2 -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 +71 -21
  12. data/lib/bootsnap/compile_cache/json.rb +89 -0
  13. data/lib/bootsnap/compile_cache/yaml.rb +315 -41
  14. data/lib/bootsnap/compile_cache.rb +26 -17
  15. data/lib/bootsnap/explicit_require.rb +4 -3
  16. data/lib/bootsnap/load_path_cache/cache.rb +72 -36
  17. data/lib/bootsnap/load_path_cache/change_observer.rb +24 -3
  18. data/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb +26 -82
  19. data/lib/bootsnap/load_path_cache/core_ext/loaded_features.rb +1 -0
  20. data/lib/bootsnap/load_path_cache/loaded_features_index.rb +38 -27
  21. data/lib/bootsnap/load_path_cache/path.rb +41 -19
  22. data/lib/bootsnap/load_path_cache/path_scanner.rb +60 -29
  23. data/lib/bootsnap/load_path_cache/store.rb +64 -24
  24. data/lib/bootsnap/load_path_cache.rb +40 -38
  25. data/lib/bootsnap/setup.rb +2 -36
  26. data/lib/bootsnap/version.rb +2 -1
  27. data/lib/bootsnap.rb +140 -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 -9
  37. data/README.jp.md +0 -231
  38. data/Rakefile +0 -13
  39. data/bin/ci +0 -10
  40. data/bin/console +0 -15
  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 -46
  45. data/dev.yml +0 -10
  46. data/lib/bootsnap/load_path_cache/core_ext/active_support.rb +0 -107
  47. data/lib/bootsnap/load_path_cache/realpath_cache.rb +0 -32
  48. data/shipit.rubygems.yml +0 -0
@@ -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 incompatability
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/
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.4.6"
4
+ VERSION = "1.18.3"
4
5
  end
data/lib/bootsnap.rb CHANGED
@@ -1,48 +1,152 @@
1
1
  # frozen_string_literal: true
2
- require_relative('bootsnap/version')
3
- require_relative('bootsnap/bundler')
4
- require_relative('bootsnap/load_path_cache')
5
- require_relative('bootsnap/compile_cache')
2
+
3
+ require_relative "bootsnap/version"
4
+ require_relative "bootsnap/bundler"
5
+ require_relative "bootsnap/load_path_cache"
6
+ require_relative "bootsnap/compile_cache"
6
7
 
7
8
  module Bootsnap
8
9
  InvalidConfiguration = Class.new(StandardError)
9
10
 
10
- def self.setup(
11
- cache_dir:,
12
- development_mode: true,
13
- load_path_cache: true,
14
- autoload_paths_cache: true,
15
- disable_trace: false,
16
- compile_cache_iseq: true,
17
- compile_cache_yaml: true
18
- )
19
- if autoload_paths_cache && !load_path_cache
20
- raise(InvalidConfiguration, "feature 'autoload_paths_cache' depends on feature 'load_path_cache'")
21
- end
22
-
23
- setup_disable_trace if disable_trace
24
-
25
- Bootsnap::LoadPathCache.setup(
26
- cache_path: cache_dir + '/bootsnap-load-path-cache',
27
- development_mode: development_mode,
28
- active_support: autoload_paths_cache
29
- ) if load_path_cache
30
-
31
- Bootsnap::CompileCache.setup(
32
- cache_dir: cache_dir + '/bootsnap-compile-cache',
33
- iseq: compile_cache_iseq,
34
- 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
35
58
  )
36
- 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
37
67
 
38
- def self.setup_disable_trace
39
- if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('2.5.0')
40
- warn(
41
- "from #{caller_locations(1, 1)[0]}: The 'disable_trace' method is not allowed with this Ruby version. " \
42
- "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,
43
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
44
135
  else
45
- RubyVM::InstructionSequence.compile_option = { trace_instruction: false }
136
+ def absolute_path?(path)
137
+ path.start_with?("/")
138
+ end
46
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
47
151
  end
48
152
  end