bootsnap 1.4.6

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 (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