bootsnap 1.1.0 → 1.2.0

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.
@@ -11,78 +11,104 @@ module Bootsnap
11
11
  end
12
12
 
13
13
  module Kernel
14
- alias_method :require_without_cache, :require
14
+ alias_method :require_without_bootsnap, :require
15
+
16
+ # Note that require registers to $LOADED_FEATURES while load does not.
17
+ def require_with_bootsnap_lfi(path, resolved = nil)
18
+ Bootsnap::LoadPathCache.loaded_features_index.register(path, resolved) do
19
+ require_without_bootsnap(resolved || path)
20
+ end
21
+ end
22
+
15
23
  def require(path)
24
+ return false if Bootsnap::LoadPathCache.loaded_features_index.key?(path)
25
+
16
26
  if resolved = Bootsnap::LoadPathCache.load_path_cache.find(path)
17
- require_without_cache(resolved)
18
- else
19
- raise Bootsnap::LoadPathCache::CoreExt.make_load_error(path)
27
+ return require_with_bootsnap_lfi(path, resolved)
20
28
  end
29
+
30
+ raise Bootsnap::LoadPathCache::CoreExt.make_load_error(path)
21
31
  rescue Bootsnap::LoadPathCache::ReturnFalse
22
32
  return false
23
33
  rescue Bootsnap::LoadPathCache::FallbackScan
24
- require_without_cache(path)
34
+ require_with_bootsnap_lfi(path)
25
35
  end
26
36
 
27
- alias_method :load_without_cache, :load
37
+ alias_method :load_without_bootsnap, :load
28
38
  def load(path, wrap = false)
29
39
  if resolved = Bootsnap::LoadPathCache.load_path_cache.find(path)
30
- load_without_cache(resolved, wrap)
31
- else
32
- # load also allows relative paths from pwd even when not in $:
33
- relative = File.expand_path(path)
34
- if File.exist?(File.expand_path(path))
35
- return load_without_cache(relative, wrap)
36
- end
37
- raise Bootsnap::LoadPathCache::CoreExt.make_load_error(path)
40
+ return load_without_bootsnap(resolved, wrap)
38
41
  end
42
+
43
+ # load also allows relative paths from pwd even when not in $:
44
+ if File.exist?(relative = File.expand_path(path))
45
+ return load_without_bootsnap(relative, wrap)
46
+ end
47
+
48
+ raise Bootsnap::LoadPathCache::CoreExt.make_load_error(path)
39
49
  rescue Bootsnap::LoadPathCache::ReturnFalse
40
50
  return false
41
51
  rescue Bootsnap::LoadPathCache::FallbackScan
42
- load_without_cache(path, wrap)
52
+ load_without_bootsnap(path, wrap)
43
53
  end
44
54
  end
45
55
 
46
56
  class << Kernel
47
- alias_method :require_without_cache, :require
57
+ alias_method :require_without_bootsnap, :require
58
+
59
+ def require_with_bootsnap_lfi(path, resolved = nil)
60
+ Bootsnap::LoadPathCache.loaded_features_index.register(path, resolved) do
61
+ require_without_bootsnap(resolved || path)
62
+ end
63
+ end
64
+
48
65
  def require(path)
66
+ return false if Bootsnap::LoadPathCache.loaded_features_index.key?(path)
67
+
49
68
  if resolved = Bootsnap::LoadPathCache.load_path_cache.find(path)
50
- require_without_cache(resolved)
51
- else
52
- raise Bootsnap::LoadPathCache::CoreExt.make_load_error(path)
69
+ return require_with_bootsnap_lfi(path, resolved)
53
70
  end
71
+
72
+ raise Bootsnap::LoadPathCache::CoreExt.make_load_error(path)
54
73
  rescue Bootsnap::LoadPathCache::ReturnFalse
55
74
  return false
56
75
  rescue Bootsnap::LoadPathCache::FallbackScan
57
- require_without_cache(path)
76
+ require_with_bootsnap_lfi(path)
58
77
  end
59
78
 
60
- alias_method :load_without_cache, :load
79
+ alias_method :load_without_bootsnap, :load
61
80
  def load(path, wrap = false)
62
81
  if resolved = Bootsnap::LoadPathCache.load_path_cache.find(path)
63
- load_without_cache(resolved, wrap)
64
- else
65
- # load also allows relative paths from pwd even when not in $:
66
- relative = File.expand_path(path)
67
- if File.exist?(relative)
68
- return load_without_cache(relative, wrap)
69
- end
70
- raise Bootsnap::LoadPathCache::CoreExt.make_load_error(path)
82
+ return load_without_bootsnap(resolved, wrap)
71
83
  end
84
+
85
+ # load also allows relative paths from pwd even when not in $:
86
+ if File.exist?(relative = File.expand_path(path))
87
+ return load_without_bootsnap(relative, wrap)
88
+ end
89
+
90
+ raise Bootsnap::LoadPathCache::CoreExt.make_load_error(path)
72
91
  rescue Bootsnap::LoadPathCache::ReturnFalse
73
92
  return false
74
93
  rescue Bootsnap::LoadPathCache::FallbackScan
75
- load_without_cache(path, wrap)
94
+ load_without_bootsnap(path, wrap)
76
95
  end
77
96
  end
78
97
 
79
98
  class Module
80
- alias_method :autoload_without_cache, :autoload
99
+ alias_method :autoload_without_bootsnap, :autoload
81
100
  def autoload(const, path)
82
- autoload_without_cache(const, Bootsnap::LoadPathCache.load_path_cache.find(path) || path)
101
+ # NOTE: This may defeat LoadedFeaturesIndex, but it's not immediately
102
+ # obvious how to make it work. This feels like a pretty niche case, unclear
103
+ # if it will ever burn anyone.
104
+ #
105
+ # The challenge is that we don't control the point at which the entry gets
106
+ # added to $LOADED_FEATURES and won't be able to hook that modification
107
+ # since it's done in C-land.
108
+ autoload_without_bootsnap(const, Bootsnap::LoadPathCache.load_path_cache.find(path) || path)
83
109
  rescue Bootsnap::LoadPathCache::ReturnFalse
84
110
  return false
85
111
  rescue Bootsnap::LoadPathCache::FallbackScan
86
- autoload_without_cache(const, path)
112
+ autoload_without_bootsnap(const, path)
87
113
  end
88
114
  end
@@ -0,0 +1,95 @@
1
+ module Bootsnap
2
+ module LoadPathCache
3
+ # LoadedFeaturesIndex partially mirrors an internal structure in ruby that
4
+ # we can't easily obtain an interface to.
5
+ #
6
+ # This works around an issue where, without bootsnap, *ruby* knows that it
7
+ # has already required a file by its short name (e.g. require 'bundler') if
8
+ # a new instance of bundler is added to the $LOAD_PATH which resolves to a
9
+ # different absolute path. This class makes bootsnap smart enough to
10
+ # realize that it has already loaded 'bundler', and not just
11
+ # '/path/to/bundler'.
12
+ #
13
+ # If you disable LoadedFeaturesIndex, you can see the problem this solves by:
14
+ #
15
+ # 1. `require 'a'`
16
+ # 2. Prepend a new $LOAD_PATH element containing an `a.rb`
17
+ # 3. `require 'a'`
18
+ #
19
+ # Ruby returns false from step 3.
20
+ # With bootsnap but with no LoadedFeaturesIndex, this loads two different
21
+ # `a.rb`s.
22
+ # With bootsnap and with LoadedFeaturesIndex, this skips the second load,
23
+ # returning false like ruby.
24
+ class LoadedFeaturesIndex
25
+ def initialize
26
+ @lfi = {}
27
+ @mutex = defined?(::Mutex) ? ::Mutex.new : ::Thread::Mutex.new # TODO: Remove once Ruby 2.2 support is dropped.
28
+
29
+ # In theory the user could mutate $LOADED_FEATURES and invalidate our
30
+ # cache. If this ever comes up in practice — or if you, the
31
+ # enterprising reader, feels inclined to solve this problem — we could
32
+ # parallel the work done with ChangeObserver on $LOAD_PATH to mirror
33
+ # updates to our @lfi.
34
+ $LOADED_FEATURES.each do |feat|
35
+ $LOAD_PATH.each do |lpe|
36
+ next unless feat.start_with?(lpe)
37
+ # /a/b/lib/my/foo.rb
38
+ # ^^^^^^^^^
39
+ short = feat[(lpe.length + 1)..-1]
40
+ @lfi[short] = true
41
+ @lfi[strip_extension(short)] = true
42
+ end
43
+ end
44
+ end
45
+
46
+ def key?(feature)
47
+ @mutex.synchronize { @lfi.key?(feature) }
48
+ end
49
+
50
+ # There is a relatively uncommon case where we could miss adding an
51
+ # entry:
52
+ #
53
+ # If the user asked for e.g. `require 'bundler'`, and we went through the
54
+ # `FallbackScan` pathway in `kernel_require.rb` and therefore did not
55
+ # pass `long` (the full expanded absolute path), then we did are not able
56
+ # to confidently add the `bundler.rb` form to @lfi.
57
+ #
58
+ # We could either:
59
+ #
60
+ # 1. Just add `bundler.rb`, `bundler.so`, and so on, which is close but
61
+ # not quite right; or
62
+ # 2. Inspect $LOADED_FEATURES upon return from yield to find the matching
63
+ # entry.
64
+ def register(short, long = nil)
65
+ ret = yield
66
+
67
+ # do we have 'bundler' or 'bundler.rb'?
68
+ altname = if File.extname(short) != ''
69
+ # strip the path from 'bundler.rb' -> 'bundler'
70
+ strip_extension(short)
71
+ elsif long && ext = File.extname(long)
72
+ # get the extension from the expanded path if given
73
+ # 'bundler' + '.rb'
74
+ short + ext
75
+ end
76
+
77
+ @mutex.synchronize do
78
+ @lfi[short] = true
79
+ (@lfi[altname] = true) if altname
80
+ end
81
+
82
+ ret
83
+ end
84
+
85
+ private
86
+
87
+ STRIP_EXTENSION = /\..*?$/
88
+ private_constant :STRIP_EXTENSION
89
+
90
+ def strip_extension(f)
91
+ f.sub(STRIP_EXTENSION, '')
92
+ end
93
+ end
94
+ end
95
+ end
@@ -99,7 +99,7 @@ module Bootsnap
99
99
  @stability ||= begin
100
100
  if Gem.path.detect { |p| expanded_path.start_with?(p.to_s) }
101
101
  STABLE
102
- elsif expanded_path.start_with?(Bundler.bundle_path.to_s)
102
+ elsif Bootsnap.bundler? && expanded_path.start_with?(Bundler.bundle_path.to_s)
103
103
  STABLE
104
104
  elsif expanded_path.start_with?(RUBY_LIBDIR) && !expanded_path.start_with?(RUBY_SITEDIR)
105
105
  STABLE
@@ -1,13 +1,22 @@
1
- require_relative '../load_path_cache'
1
+ require_relative '../explicit_require'
2
2
 
3
3
  module Bootsnap
4
4
  module LoadPathCache
5
5
  module PathScanner
6
- REQUIRABLES_AND_DIRS = "/**/*{#{DOT_RB},#{DL_EXTENSIONS.join(',')},/}"
7
- IS_DIR = %r{(.*)/\z}
6
+ # Glob pattern to find requirable files and subdirectories in given path.
7
+ # It expands to:
8
+ #
9
+ # * `/*{.rb,.so,/}` - It matches requirable files, directories and
10
+ # symlinks to directories at given path.
11
+ # * `/*/**/*{.rb,.so,/}` - It matches requirable files and
12
+ # subdirectories in any (even symlinked) directory at given path at
13
+ # any directory tree depth.
14
+ #
15
+ REQUIRABLES_AND_DIRS = "/{,*/**/}*{#{DOT_RB},#{DL_EXTENSIONS.join(',')},/}"
8
16
  NORMALIZE_NATIVE_EXTENSIONS = !DL_EXTENSIONS.include?(LoadPathCache::DOT_SO)
9
17
  ALTERNATIVE_NATIVE_EXTENSIONS_PATTERN = /\.(o|bundle|dylib)\z/
10
- BUNDLE_PATH = (Bundler.bundle_path.cleanpath.to_s << LoadPathCache::SLASH).freeze
18
+ BUNDLE_PATH = Bootsnap.bundler? ?
19
+ (Bundler.bundle_path.cleanpath.to_s << LoadPathCache::SLASH).freeze : ''.freeze
11
20
 
12
21
  def self.call(path)
13
22
  path = path.to_s
@@ -29,12 +38,13 @@ module Bootsnap
29
38
  next if contains_bundle_path && absolute_path.start_with?(BUNDLE_PATH)
30
39
  relative_path = absolute_path.slice!(relative_slice)
31
40
 
32
- if md = relative_path.match(IS_DIR)
33
- dirs << md[1]
41
+ if relative_path.end_with?('/')
42
+ dirs << relative_path[0..-2]
34
43
  else
35
44
  requirables << relative_path
36
45
  end
37
46
  end
47
+
38
48
  [requirables, dirs]
39
49
  end
40
50
  end
@@ -11,6 +11,8 @@ module Bootsnap
11
11
 
12
12
  def initialize(store_path)
13
13
  @store_path = store_path
14
+ @in_txn = false
15
+ @dirty = false
14
16
  load_data
15
17
  end
16
18
 
@@ -67,10 +69,15 @@ module Bootsnap
67
69
  def dump_data
68
70
  # Change contents atomically so other processes can't get invalid
69
71
  # caches if they read at an inopportune time.
70
- tmp = "#{@store_path}.#{(rand * 100000).to_i}.tmp"
72
+ tmp = "#{@store_path}.#{Process.pid}.#{(rand * 100000).to_i}.tmp"
71
73
  FileUtils.mkpath(File.dirname(tmp))
72
- File.binwrite(tmp, MessagePack.dump(@data))
74
+ exclusive_write = File::Constants::CREAT | File::Constants::EXCL | File::Constants::WRONLY
75
+ # `encoding:` looks redundant wrt `binwrite`, but necessary on windows
76
+ # because binary is part of mode.
77
+ File.binwrite(tmp, MessagePack.dump(@data), mode: exclusive_write, encoding: Encoding::BINARY)
73
78
  FileUtils.mv(tmp, @store_path)
79
+ rescue Errno::EEXIST
80
+ retry
74
81
  end
75
82
  end
76
83
  end
@@ -21,11 +21,13 @@ module Bootsnap
21
21
  CACHED_EXTENSIONS = DLEXT2 ? [DOT_RB, DLEXT, DLEXT2] : [DOT_RB, DLEXT]
22
22
 
23
23
  class << self
24
- attr_reader :load_path_cache, :autoload_paths_cache
24
+ attr_reader :load_path_cache, :autoload_paths_cache, :loaded_features_index
25
25
 
26
26
  def setup(cache_path:, development_mode:, active_support: true)
27
27
  store = Store.new(cache_path)
28
28
 
29
+ @loaded_features_index = LoadedFeaturesIndex.new
30
+
29
31
  @load_path_cache = Cache.new(store, $LOAD_PATH, development_mode: development_mode)
30
32
  require_relative 'load_path_cache/core_ext/kernel_require'
31
33
 
@@ -50,3 +52,4 @@ require_relative 'load_path_cache/path'
50
52
  require_relative 'load_path_cache/cache'
51
53
  require_relative 'load_path_cache/store'
52
54
  require_relative 'load_path_cache/change_observer'
55
+ require_relative 'load_path_cache/loaded_features_index'
@@ -4,16 +4,10 @@ env = ENV['RAILS_ENV'] || ENV['RACK_ENV'] || ENV['ENV']
4
4
  development_mode = ['', nil, 'development'].include?(env)
5
5
 
6
6
  # only enable on 'ruby' (MRI), POSIX (darin, linux, *bsd), and >= 2.3.0
7
- enable_cc = \
8
- RUBY_ENGINE == 'ruby' && \
9
- RUBY_PLATFORM =~ /darwin|linux|bsd/ && \
10
- RUBY_VERSION # "1.9.3"
11
- .split('.') # ["1", "9", "3"]
12
- .map(&:to_i) # [1, 9, 3]
13
- .zip([2, 3, -1]) # [[1, 2], [9, 3], [3, -1]]
14
- .map { |a, b| a <=> b } # [-1, 1, 1]
15
- .detect { |e| !e.zero? } # -1
16
- .==(1) # false
7
+ enable_cc =
8
+ RUBY_ENGINE == 'ruby' &&
9
+ RUBY_PLATFORM =~ /darwin|linux|bsd/ &&
10
+ Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("2.3.0")
17
11
 
18
12
  cache_dir = ENV['BOOTSNAP_CACHE_DIR']
19
13
  unless cache_dir
@@ -1,3 +1,3 @@
1
1
  module Bootsnap
2
- VERSION = "1.1.0"
2
+ VERSION = "1.2.0"
3
3
  end
data/lib/bootsnap.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  require_relative 'bootsnap/version'
2
+ require_relative 'bootsnap/bundler'
2
3
  require_relative 'bootsnap/load_path_cache'
3
4
  require_relative 'bootsnap/compile_cache'
4
5
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bootsnap
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Burke Libbey
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-06-19 00:00:00.000000000 Z
11
+ date: 2018-03-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -94,7 +94,7 @@ dependencies:
94
94
  - - "~>"
95
95
  - !ruby/object:Gem::Version
96
96
  version: '1.0'
97
- description: wip.
97
+ description: Boot large ruby/rails apps faster
98
98
  email:
99
99
  - burke.libbey@shopify.com
100
100
  executables: []
@@ -106,9 +106,11 @@ files:
106
106
  - ".rubocop.yml"
107
107
  - ".travis.yml"
108
108
  - CHANGELOG.md
109
+ - CODE_OF_CONDUCT.md
109
110
  - CONTRIBUTING.md
110
111
  - Gemfile
111
- - LICENSE
112
+ - LICENSE.txt
113
+ - README.jp.md
112
114
  - README.md
113
115
  - Rakefile
114
116
  - bin/console
@@ -120,6 +122,7 @@ files:
120
122
  - ext/bootsnap/bootsnap.h
121
123
  - ext/bootsnap/extconf.rb
122
124
  - lib/bootsnap.rb
125
+ - lib/bootsnap/bundler.rb
123
126
  - lib/bootsnap/compile_cache.rb
124
127
  - lib/bootsnap/compile_cache/iseq.rb
125
128
  - lib/bootsnap/compile_cache/yaml.rb
@@ -129,6 +132,7 @@ files:
129
132
  - lib/bootsnap/load_path_cache/change_observer.rb
130
133
  - lib/bootsnap/load_path_cache/core_ext/active_support.rb
131
134
  - lib/bootsnap/load_path_cache/core_ext/kernel_require.rb
135
+ - lib/bootsnap/load_path_cache/loaded_features_index.rb
132
136
  - lib/bootsnap/load_path_cache/path.rb
133
137
  - lib/bootsnap/load_path_cache/path_scanner.rb
134
138
  - lib/bootsnap/load_path_cache/store.rb
@@ -154,8 +158,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
154
158
  version: '0'
155
159
  requirements: []
156
160
  rubyforge_project:
157
- rubygems_version: 2.6.10
161
+ rubygems_version: 2.7.6
158
162
  signing_key:
159
163
  specification_version: 4
160
- summary: wip
164
+ summary: Boot large ruby/rails apps faster
161
165
  test_files: []
data/LICENSE DELETED
@@ -1,20 +0,0 @@
1
- Copyright (c) 2017 Shopify
2
-
3
- Permission is hereby granted, free of charge, to any person obtaining
4
- a copy of this software and associated documentation files (the
5
- "Software"), to deal in the Software without restriction, including
6
- without limitation the rights to use, copy, modify, merge, publish,
7
- distribute, sublicense, and/or sell copies of the Software, and to
8
- permit persons to whom the Software is furnished to do so, subject to
9
- the following conditions:
10
-
11
- The above copyright notice and this permission notice shall be
12
- included in all copies or substantial portions of the Software.
13
-
14
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
- EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
- MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
- NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
- LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
- OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
- WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.