bootsnap 1.4.5 → 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 +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