bootsnap 1.4.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +7 -0
  2. data/.github/CODEOWNERS +2 -0
  3. data/.github/probots.yml +2 -0
  4. data/.gitignore +17 -0
  5. data/.rubocop.yml +20 -0
  6. data/.travis.yml +21 -0
  7. data/CHANGELOG.md +122 -0
  8. data/CODE_OF_CONDUCT.md +74 -0
  9. data/CONTRIBUTING.md +21 -0
  10. data/Gemfile +9 -0
  11. data/LICENSE.txt +21 -0
  12. data/README.jp.md +231 -0
  13. data/README.md +304 -0
  14. data/Rakefile +13 -0
  15. data/bin/ci +10 -0
  16. data/bin/console +15 -0
  17. data/bin/setup +8 -0
  18. data/bin/test-minimal-support +7 -0
  19. data/bin/testunit +8 -0
  20. data/bootsnap.gemspec +46 -0
  21. data/dev.yml +10 -0
  22. data/ext/bootsnap/bootsnap.c +829 -0
  23. data/ext/bootsnap/bootsnap.h +6 -0
  24. data/ext/bootsnap/extconf.rb +19 -0
  25. data/lib/bootsnap.rb +48 -0
  26. data/lib/bootsnap/bundler.rb +15 -0
  27. data/lib/bootsnap/compile_cache.rb +43 -0
  28. data/lib/bootsnap/compile_cache/iseq.rb +73 -0
  29. data/lib/bootsnap/compile_cache/yaml.rb +63 -0
  30. data/lib/bootsnap/explicit_require.rb +50 -0
  31. data/lib/bootsnap/load_path_cache.rb +78 -0
  32. data/lib/bootsnap/load_path_cache/cache.rb +208 -0
  33. data/lib/bootsnap/load_path_cache/change_observer.rb +63 -0
  34. data/lib/bootsnap/load_path_cache/core_ext/active_support.rb +107 -0
  35. data/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb +93 -0
  36. data/lib/bootsnap/load_path_cache/core_ext/loaded_features.rb +18 -0
  37. data/lib/bootsnap/load_path_cache/loaded_features_index.rb +148 -0
  38. data/lib/bootsnap/load_path_cache/path.rb +114 -0
  39. data/lib/bootsnap/load_path_cache/path_scanner.rb +50 -0
  40. data/lib/bootsnap/load_path_cache/realpath_cache.rb +32 -0
  41. data/lib/bootsnap/load_path_cache/store.rb +90 -0
  42. data/lib/bootsnap/setup.rb +39 -0
  43. data/lib/bootsnap/version.rb +4 -0
  44. data/shipit.rubygems.yml +0 -0
  45. metadata +174 -0
@@ -0,0 +1,6 @@
1
+ #ifndef BOOTSNAP_H
2
+ #define BOOTSNAP_H 1
3
+
4
+ /* doesn't expose anything */
5
+
6
+ #endif /* BOOTSNAP_H */
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+ require("mkmf")
3
+ $CFLAGS << ' -O3 '
4
+ $CFLAGS << ' -std=c99'
5
+
6
+ # ruby.h has some -Wpedantic fails in some cases
7
+ # (e.g. https://github.com/Shopify/bootsnap/issues/15)
8
+ unless ['0', '', nil].include?(ENV['BOOTSNAP_PEDANTIC'])
9
+ $CFLAGS << ' -Wall'
10
+ $CFLAGS << ' -Werror'
11
+ $CFLAGS << ' -Wextra'
12
+ $CFLAGS << ' -Wpedantic'
13
+
14
+ $CFLAGS << ' -Wno-unused-parameter' # VALUE self has to be there but we don't care what it is.
15
+ $CFLAGS << ' -Wno-keyword-macro' # hiding return
16
+ $CFLAGS << ' -Wno-gcc-compat' # ruby.h 2.6.0 on macos 10.14, dunno
17
+ end
18
+
19
+ create_makefile("bootsnap/bootsnap")
@@ -0,0 +1,48 @@
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')
6
+
7
+ module Bootsnap
8
+ InvalidConfiguration = Class.new(StandardError)
9
+
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
35
+ )
36
+ end
37
+
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",
43
+ )
44
+ else
45
+ RubyVM::InstructionSequence.compile_option = { trace_instruction: false }
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+ module Bootsnap
3
+ extend(self)
4
+
5
+ def bundler?
6
+ return false unless defined?(::Bundler)
7
+
8
+ # Bundler environment variable
9
+ %w(BUNDLE_BIN_PATH BUNDLE_GEMFILE).each do |current|
10
+ return true if ENV.key?(current)
11
+ end
12
+
13
+ false
14
+ end
15
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+ module Bootsnap
3
+ module CompileCache
4
+ Error = Class.new(StandardError)
5
+ PermissionError = Class.new(Error)
6
+
7
+ def self.setup(cache_dir:, iseq:, yaml:)
8
+ if iseq
9
+ if supported?
10
+ require_relative('compile_cache/iseq')
11
+ Bootsnap::CompileCache::ISeq.install!(cache_dir)
12
+ elsif $VERBOSE
13
+ warn("[bootsnap/setup] bytecode caching is not supported on this implementation of Ruby")
14
+ end
15
+ end
16
+
17
+ if yaml
18
+ if supported?
19
+ require_relative('compile_cache/yaml')
20
+ Bootsnap::CompileCache::YAML.install!(cache_dir)
21
+ elsif $VERBOSE
22
+ warn("[bootsnap/setup] YAML parsing caching is not supported on this implementation of Ruby")
23
+ end
24
+ end
25
+ end
26
+
27
+ def self.permission_error(path)
28
+ cpath = Bootsnap::CompileCache::ISeq.cache_dir
29
+ raise(
30
+ PermissionError,
31
+ "bootsnap doesn't have permission to write cache entries in '#{cpath}' " \
32
+ "(or, less likely, doesn't have permission to read '#{path}')",
33
+ )
34
+ end
35
+
36
+ def self.supported?
37
+ # only enable on 'ruby' (MRI), POSIX (darwin, linux, *bsd), and >= 2.3.0
38
+ RUBY_ENGINE == 'ruby' &&
39
+ RUBY_PLATFORM =~ /darwin|linux|bsd/ &&
40
+ Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("2.3.0")
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+ require('bootsnap/bootsnap')
3
+ require('zlib')
4
+
5
+ module Bootsnap
6
+ module CompileCache
7
+ module ISeq
8
+ class << self
9
+ attr_accessor(:cache_dir)
10
+ end
11
+
12
+ def self.input_to_storage(_, path)
13
+ RubyVM::InstructionSequence.compile_file(path).to_binary
14
+ rescue SyntaxError
15
+ raise(Uncompilable, 'syntax error')
16
+ end
17
+
18
+ def self.storage_to_output(binary)
19
+ RubyVM::InstructionSequence.load_from_binary(binary)
20
+ rescue RuntimeError => e
21
+ if e.message == 'broken binary format'
22
+ STDERR.puts("[Bootsnap::CompileCache] warning: rejecting broken binary")
23
+ nil
24
+ else
25
+ raise
26
+ end
27
+ end
28
+
29
+ def self.input_to_output(_)
30
+ nil # ruby handles this
31
+ end
32
+
33
+ module InstructionSequenceMixin
34
+ def load_iseq(path)
35
+ # Having coverage enabled prevents iseq dumping/loading.
36
+ return nil if defined?(Coverage) && Bootsnap::CompileCache::Native.coverage_running?
37
+
38
+ Bootsnap::CompileCache::Native.fetch(
39
+ Bootsnap::CompileCache::ISeq.cache_dir,
40
+ path.to_s,
41
+ Bootsnap::CompileCache::ISeq
42
+ )
43
+ rescue Errno::EACCES
44
+ Bootsnap::CompileCache.permission_error(path)
45
+ rescue RuntimeError => e
46
+ if e.message =~ /unmatched platform/
47
+ puts("unmatched platform for file #{path}")
48
+ end
49
+ raise
50
+ end
51
+
52
+ def compile_option=(hash)
53
+ super(hash)
54
+ Bootsnap::CompileCache::ISeq.compile_option_updated
55
+ end
56
+ end
57
+
58
+ def self.compile_option_updated
59
+ option = RubyVM::InstructionSequence.compile_option
60
+ crc = Zlib.crc32(option.inspect)
61
+ Bootsnap::CompileCache::Native.compile_option_crc32 = crc
62
+ end
63
+
64
+ def self.install!(cache_dir)
65
+ Bootsnap::CompileCache::ISeq.cache_dir = cache_dir
66
+ Bootsnap::CompileCache::ISeq.compile_option_updated
67
+ class << RubyVM::InstructionSequence
68
+ prepend(InstructionSequenceMixin)
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+ require('bootsnap/bootsnap')
3
+
4
+ module Bootsnap
5
+ module CompileCache
6
+ module YAML
7
+ class << self
8
+ attr_accessor(:msgpack_factory)
9
+ end
10
+
11
+ def self.input_to_storage(contents, _)
12
+ raise(Uncompilable) if contents.index("!ruby/object")
13
+ obj = ::YAML.load(contents)
14
+ msgpack_factory.packer.write(obj).to_s
15
+ rescue NoMethodError, RangeError
16
+ # if the object included things that we can't serialize, fall back to
17
+ # Marshal. It's a bit slower, but can encode anything yaml can.
18
+ # NoMethodError is unexpected types; RangeError is Bignums
19
+ Marshal.dump(obj)
20
+ end
21
+
22
+ def self.storage_to_output(data)
23
+ # This could have a meaning in messagepack, and we're being a little lazy
24
+ # about it. -- but a leading 0x04 would indicate the contents of the YAML
25
+ # is a positive integer, which is rare, to say the least.
26
+ if data[0] == 0x04.chr && data[1] == 0x08.chr
27
+ Marshal.load(data)
28
+ else
29
+ msgpack_factory.unpacker.feed(data).read
30
+ end
31
+ end
32
+
33
+ def self.input_to_output(data)
34
+ ::YAML.load(data)
35
+ end
36
+
37
+ def self.install!(cache_dir)
38
+ require('yaml')
39
+ require('msgpack')
40
+
41
+ # MessagePack serializes symbols as strings by default.
42
+ # We want them to roundtrip cleanly, so we use a custom factory.
43
+ # see: https://github.com/msgpack/msgpack-ruby/pull/122
44
+ factory = MessagePack::Factory.new
45
+ factory.register_type(0x00, Symbol)
46
+ Bootsnap::CompileCache::YAML.msgpack_factory = factory
47
+
48
+ klass = class << ::YAML; self; end
49
+ klass.send(:define_method, :load_file) do |path|
50
+ begin
51
+ Bootsnap::CompileCache::Native.fetch(
52
+ cache_dir,
53
+ path,
54
+ Bootsnap::CompileCache::YAML
55
+ )
56
+ rescue Errno::EACCES
57
+ Bootsnap::CompileCache.permission_error(path)
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+ module Bootsnap
3
+ module ExplicitRequire
4
+ ARCHDIR = RbConfig::CONFIG['archdir']
5
+ RUBYLIBDIR = RbConfig::CONFIG['rubylibdir']
6
+ DLEXT = RbConfig::CONFIG['DLEXT']
7
+
8
+ def self.from_self(feature)
9
+ require_relative("../#{feature}")
10
+ end
11
+
12
+ def self.from_rubylibdir(feature)
13
+ require(File.join(RUBYLIBDIR, "#{feature}.rb"))
14
+ end
15
+
16
+ def self.from_archdir(feature)
17
+ require(File.join(ARCHDIR, "#{feature}.#{DLEXT}"))
18
+ end
19
+
20
+ # Given a set of gems, run a block with the LOAD_PATH narrowed to include
21
+ # only core ruby source paths and these gems -- that is, roughly,
22
+ # temporarily remove all gems not listed in this call from the LOAD_PATH.
23
+ #
24
+ # This is useful before bootsnap is fully-initialized to load gems that it
25
+ # depends on, without forcing full LOAD_PATH traversals.
26
+ def self.with_gems(*gems)
27
+ orig = $LOAD_PATH.dup
28
+ $LOAD_PATH.clear
29
+ gems.each do |gem|
30
+ pat = %r{
31
+ /
32
+ (gems|extensions/[^/]+/[^/]+) # "gems" or "extensions/x64_64-darwin16/2.3.0"
33
+ /
34
+ #{Regexp.escape(gem)}-(\h{12}|(\d+\.)) # msgpack-1.2.3 or msgpack-1234567890ab
35
+ }x
36
+ $LOAD_PATH.concat(orig.grep(pat))
37
+ end
38
+ $LOAD_PATH << ARCHDIR
39
+ $LOAD_PATH << RUBYLIBDIR
40
+ begin
41
+ yield
42
+ rescue LoadError
43
+ $LOAD_PATH.replace(orig)
44
+ yield
45
+ end
46
+ ensure
47
+ $LOAD_PATH.replace(orig)
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bootsnap
4
+ module LoadPathCache
5
+ ReturnFalse = Class.new(StandardError)
6
+ FallbackScan = Class.new(StandardError)
7
+
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
16
+
17
+ DL_EXTENSIONS = ::RbConfig::CONFIG
18
+ .values_at('DLEXT', 'DLEXT2')
19
+ .reject { |ext| !ext || ext.empty? }
20
+ .map { |ext| ".#{ext}" }
21
+ .freeze
22
+ DLEXT = DL_EXTENSIONS[0]
23
+ # This is nil on linux and darwin, but I think it's '.o' on some other
24
+ # platform. I'm not really sure which, but it seems better to replicate
25
+ # ruby's semantics as faithfully as possible.
26
+ DLEXT2 = DL_EXTENSIONS[1]
27
+
28
+ CACHED_EXTENSIONS = DLEXT2 ? [DOT_RB, DLEXT, DLEXT2] : [DOT_RB, DLEXT]
29
+
30
+ class << self
31
+ attr_reader(:load_path_cache, :autoload_paths_cache,
32
+ :loaded_features_index, :realpath_cache)
33
+
34
+ def setup(cache_path:, development_mode:, active_support: true)
35
+ unless supported?
36
+ warn("[bootsnap/setup] Load path caching is not supported on this implementation of Ruby") if $VERBOSE
37
+ return
38
+ end
39
+
40
+ store = Store.new(cache_path)
41
+
42
+ @loaded_features_index = LoadedFeaturesIndex.new
43
+ @realpath_cache = RealpathCache.new
44
+
45
+ @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')
48
+
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
60
+ end
61
+
62
+ def supported?
63
+ RUBY_ENGINE == 'ruby' &&
64
+ RUBY_PLATFORM =~ /darwin|linux|bsd/
65
+ end
66
+ end
67
+ end
68
+ end
69
+
70
+ 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')
78
+ end
@@ -0,0 +1,208 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative('../explicit_require')
4
+
5
+ module Bootsnap
6
+ module LoadPathCache
7
+ class Cache
8
+ AGE_THRESHOLD = 30 # seconds
9
+
10
+ def initialize(store, path_obj, development_mode: false)
11
+ @development_mode = development_mode
12
+ @store = store
13
+ @mutex = defined?(::Mutex) ? ::Mutex.new : ::Thread::Mutex.new # TODO: Remove once Ruby 2.2 support is dropped.
14
+ @path_obj = path_obj.map! { |f| File.exist?(f) ? File.realpath(f) : f }
15
+ @has_relative_paths = nil
16
+ reinitialize
17
+ end
18
+
19
+ # What is the path item that contains the dir as child?
20
+ # e.g. given "/a/b/c/d" exists, and the path is ["/a/b"], load_dir("c/d")
21
+ # is "/a/b".
22
+ def load_dir(dir)
23
+ reinitialize if stale?
24
+ @mutex.synchronize { @dirs[dir] }
25
+ end
26
+
27
+ # { 'enumerator' => nil, 'enumerator.so' => nil, ... }
28
+ BUILTIN_FEATURES = $LOADED_FEATURES.each_with_object({}) do |feat, features|
29
+ # Builtin features are of the form 'enumerator.so'.
30
+ # All others include paths.
31
+ next unless feat.size < 20 && !feat.include?('/')
32
+
33
+ base = File.basename(feat, '.*') # enumerator.so -> enumerator
34
+ ext = File.extname(feat) # .so
35
+
36
+ features[feat] = nil # enumerator.so
37
+ features[base] = nil # enumerator
38
+
39
+ next unless [DOT_SO, *DL_EXTENSIONS].include?(ext)
40
+ DL_EXTENSIONS.each do |dl_ext|
41
+ features["#{base}#{dl_ext}"] = nil # enumerator.bundle
42
+ end
43
+ end.freeze
44
+
45
+ # Try to resolve this feature to an absolute path without traversing the
46
+ # loadpath.
47
+ def find(feature)
48
+ reinitialize if (@has_relative_paths && dir_changed?) || stale?
49
+ feature = feature.to_s
50
+ return feature if absolute_path?(feature)
51
+ return expand_path(feature) if feature.start_with?('./')
52
+ @mutex.synchronize do
53
+ x = search_index(feature)
54
+ return x if x
55
+
56
+ # Ruby has some built-in features that require lies about.
57
+ # For example, 'enumerator' is built in. If you require it, ruby
58
+ # returns false as if it were already loaded; however, there is no
59
+ # file to find on disk. We've pre-built a list of these, and we
60
+ # return false if any of them is loaded.
61
+ raise(LoadPathCache::ReturnFalse, '', []) if BUILTIN_FEATURES.key?(feature)
62
+
63
+ # The feature wasn't found on our preliminary search through the index.
64
+ # We resolve this differently depending on what the extension was.
65
+ case File.extname(feature)
66
+ # If the extension was one of the ones we explicitly cache (.rb and the
67
+ # native dynamic extension, e.g. .bundle or .so), we know it was a
68
+ # failure and there's nothing more we can do to find the file.
69
+ # no extension, .rb, (.bundle or .so)
70
+ when '', *CACHED_EXTENSIONS
71
+ nil
72
+ # Ruby allows specifying native extensions as '.so' even when DLEXT
73
+ # is '.bundle'. This is where we handle that case.
74
+ when DOT_SO
75
+ x = search_index(feature[0..-4] + DLEXT)
76
+ return x if x
77
+ if DLEXT2
78
+ x = search_index(feature[0..-4] + DLEXT2)
79
+ return x if x
80
+ end
81
+ else
82
+ # other, unknown extension. For example, `.rake`. Since we haven't
83
+ # cached these, we legitimately need to run the load path search.
84
+ raise(LoadPathCache::FallbackScan, '', [])
85
+ end
86
+ end
87
+
88
+ # In development mode, we don't want to confidently return failures for
89
+ # cases where the file doesn't appear to be on the load path. We should
90
+ # be able to detect newly-created files without rebooting the
91
+ # application.
92
+ raise(LoadPathCache::FallbackScan, '', []) if @development_mode
93
+ end
94
+
95
+ if RbConfig::CONFIG['host_os'] =~ /mswin|mingw|cygwin/
96
+ def absolute_path?(path)
97
+ path[1] == ':'
98
+ end
99
+ else
100
+ def absolute_path?(path)
101
+ path.start_with?(SLASH)
102
+ end
103
+ end
104
+
105
+ def unshift_paths(sender, *paths)
106
+ return unless sender == @path_obj
107
+ @mutex.synchronize { unshift_paths_locked(*paths) }
108
+ end
109
+
110
+ def push_paths(sender, *paths)
111
+ return unless sender == @path_obj
112
+ @mutex.synchronize { push_paths_locked(*paths) }
113
+ end
114
+
115
+ def reinitialize(path_obj = @path_obj)
116
+ @mutex.synchronize do
117
+ @path_obj = path_obj
118
+ ChangeObserver.register(self, @path_obj)
119
+ @index = {}
120
+ @dirs = {}
121
+ @generated_at = now
122
+ push_paths_locked(*@path_obj)
123
+ end
124
+ end
125
+
126
+ private
127
+
128
+ def dir_changed?
129
+ @prev_dir ||= Dir.pwd
130
+ if @prev_dir == Dir.pwd
131
+ false
132
+ else
133
+ @prev_dir = Dir.pwd
134
+ true
135
+ end
136
+ end
137
+
138
+ def push_paths_locked(*paths)
139
+ @store.transaction do
140
+ paths.map(&:to_s).each do |path|
141
+ p = Path.new(path)
142
+ @has_relative_paths = true if p.relative?
143
+ next if p.non_directory?
144
+ expanded_path = p.expanded_path
145
+ entries, dirs = p.entries_and_dirs(@store)
146
+ # push -> low precedence -> set only if unset
147
+ dirs.each { |dir| @dirs[dir] ||= path }
148
+ entries.each { |rel| @index[rel] ||= expanded_path }
149
+ end
150
+ end
151
+ end
152
+
153
+ def unshift_paths_locked(*paths)
154
+ @store.transaction do
155
+ paths.map(&:to_s).reverse_each do |path|
156
+ p = Path.new(path)
157
+ next if p.non_directory?
158
+ expanded_path = p.expanded_path
159
+ entries, dirs = p.entries_and_dirs(@store)
160
+ # unshift -> high precedence -> unconditional set
161
+ dirs.each { |dir| @dirs[dir] = path }
162
+ entries.each { |rel| @index[rel] = expanded_path }
163
+ end
164
+ end
165
+ end
166
+
167
+ def expand_path(feature)
168
+ maybe_append_extension(File.expand_path(feature))
169
+ end
170
+
171
+ def stale?
172
+ @development_mode && @generated_at + AGE_THRESHOLD < now
173
+ end
174
+
175
+ def now
176
+ Process.clock_gettime(Process::CLOCK_MONOTONIC).to_i
177
+ end
178
+
179
+ if DLEXT2
180
+ def search_index(f)
181
+ try_index(f + DOT_RB) || try_index(f + DLEXT) || try_index(f + DLEXT2) || try_index(f)
182
+ end
183
+
184
+ def maybe_append_extension(f)
185
+ try_ext(f + DOT_RB) || try_ext(f + DLEXT) || try_ext(f + DLEXT2) || f
186
+ end
187
+ else
188
+ def search_index(f)
189
+ try_index(f + DOT_RB) || try_index(f + DLEXT) || try_index(f)
190
+ end
191
+
192
+ def maybe_append_extension(f)
193
+ try_ext(f + DOT_RB) || try_ext(f + DLEXT) || f
194
+ end
195
+ end
196
+
197
+ def try_index(f)
198
+ if (p = @index[f])
199
+ p + '/' + f
200
+ end
201
+ end
202
+
203
+ def try_ext(f)
204
+ f if File.exist?(f)
205
+ end
206
+ end
207
+ end
208
+ end