bootsnap 1.8.1 → 1.10.3
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +78 -3
- data/LICENSE.txt +1 -1
- data/README.md +2 -2
- data/exe/bootsnap +1 -1
- data/ext/bootsnap/bootsnap.c +39 -37
- data/ext/bootsnap/extconf.rb +13 -11
- data/lib/bootsnap/bundler.rb +1 -0
- data/lib/bootsnap/cli/worker_pool.rb +1 -0
- data/lib/bootsnap/cli.rb +78 -43
- data/lib/bootsnap/compile_cache/iseq.rb +42 -12
- data/lib/bootsnap/compile_cache/json.rb +88 -0
- data/lib/bootsnap/compile_cache/yaml.rb +264 -77
- data/lib/bootsnap/compile_cache.rb +22 -7
- data/lib/bootsnap/explicit_require.rb +4 -3
- data/lib/bootsnap/load_path_cache/cache.rb +57 -41
- data/lib/bootsnap/load_path_cache/change_observer.rb +2 -0
- data/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb +33 -65
- data/lib/bootsnap/load_path_cache/core_ext/loaded_features.rb +1 -0
- data/lib/bootsnap/load_path_cache/loaded_features_index.rb +32 -21
- data/lib/bootsnap/load_path_cache/path.rb +5 -3
- data/lib/bootsnap/load_path_cache/path_scanner.rb +6 -5
- data/lib/bootsnap/load_path_cache/realpath_cache.rb +1 -0
- data/lib/bootsnap/load_path_cache/store.rb +24 -7
- data/lib/bootsnap/load_path_cache.rb +16 -22
- data/lib/bootsnap/setup.rb +2 -1
- data/lib/bootsnap/version.rb +2 -1
- data/lib/bootsnap.rb +103 -89
- metadata +6 -75
| @@ -1,6 +1,6 @@ | |
| 1 1 | 
             
            # frozen_string_literal: true
         | 
| 2 2 |  | 
| 3 | 
            -
            require_relative( | 
| 3 | 
            +
            require_relative("../explicit_require")
         | 
| 4 4 |  | 
| 5 5 | 
             
            module Bootsnap
         | 
| 6 6 | 
             
              module LoadPathCache
         | 
| @@ -28,15 +28,16 @@ module Bootsnap | |
| 28 28 | 
             
                  BUILTIN_FEATURES = $LOADED_FEATURES.each_with_object({}) do |feat, features|
         | 
| 29 29 | 
             
                    # Builtin features are of the form 'enumerator.so'.
         | 
| 30 30 | 
             
                    # All others include paths.
         | 
| 31 | 
            -
                    next unless feat.size < 20 && !feat.include?( | 
| 31 | 
            +
                    next unless feat.size < 20 && !feat.include?("/")
         | 
| 32 32 |  | 
| 33 | 
            -
                    base = File.basename(feat,  | 
| 33 | 
            +
                    base = File.basename(feat, ".*") # enumerator.so -> enumerator
         | 
| 34 34 | 
             
                    ext  = File.extname(feat) # .so
         | 
| 35 35 |  | 
| 36 36 | 
             
                    features[feat] = nil # enumerator.so
         | 
| 37 37 | 
             
                    features[base] = nil # enumerator
         | 
| 38 38 |  | 
| 39 39 | 
             
                    next unless [DOT_SO, *DL_EXTENSIONS].include?(ext)
         | 
| 40 | 
            +
             | 
| 40 41 | 
             
                    DL_EXTENSIONS.each do |dl_ext|
         | 
| 41 42 | 
             
                      features["#{base}#{dl_ext}"] = nil # enumerator.bundle
         | 
| 42 43 | 
             
                    end
         | 
| @@ -44,21 +45,27 @@ module Bootsnap | |
| 44 45 |  | 
| 45 46 | 
             
                  # Try to resolve this feature to an absolute path without traversing the
         | 
| 46 47 | 
             
                  # loadpath.
         | 
| 47 | 
            -
                  def find(feature)
         | 
| 48 | 
            +
                  def find(feature, try_extensions: true)
         | 
| 48 49 | 
             
                    reinitialize if (@has_relative_paths && dir_changed?) || stale?
         | 
| 49 50 | 
             
                    feature = feature.to_s.freeze
         | 
| 50 | 
            -
             | 
| 51 | 
            -
                    return  | 
| 51 | 
            +
             | 
| 52 | 
            +
                    return feature if Bootsnap.absolute_path?(feature)
         | 
| 53 | 
            +
             | 
| 54 | 
            +
                    if feature.start_with?("./", "../")
         | 
| 55 | 
            +
                      return try_extensions ? expand_path(feature) : File.expand_path(feature).freeze
         | 
| 56 | 
            +
                    end
         | 
| 57 | 
            +
             | 
| 52 58 | 
             
                    @mutex.synchronize do
         | 
| 53 | 
            -
                      x = search_index(feature)
         | 
| 59 | 
            +
                      x = search_index(feature, try_extensions: try_extensions)
         | 
| 54 60 | 
             
                      return x if x
         | 
| 61 | 
            +
                      return unless try_extensions
         | 
| 55 62 |  | 
| 56 63 | 
             
                      # Ruby has some built-in features that require lies about.
         | 
| 57 64 | 
             
                      # For example, 'enumerator' is built in. If you require it, ruby
         | 
| 58 65 | 
             
                      # returns false as if it were already loaded; however, there is no
         | 
| 59 66 | 
             
                      # file to find on disk. We've pre-built a list of these, and we
         | 
| 60 67 | 
             
                      # return false if any of them is loaded.
         | 
| 61 | 
            -
                       | 
| 68 | 
            +
                      return false if BUILTIN_FEATURES.key?(feature)
         | 
| 62 69 |  | 
| 63 70 | 
             
                      # The feature wasn't found on our preliminary search through the index.
         | 
| 64 71 | 
             
                      # We resolve this differently depending on what the extension was.
         | 
| @@ -67,13 +74,14 @@ module Bootsnap | |
| 67 74 | 
             
                      # native dynamic extension, e.g. .bundle or .so), we know it was a
         | 
| 68 75 | 
             
                      # failure and there's nothing more we can do to find the file.
         | 
| 69 76 | 
             
                      # no extension, .rb, (.bundle or .so)
         | 
| 70 | 
            -
                      when  | 
| 77 | 
            +
                      when "", *CACHED_EXTENSIONS
         | 
| 71 78 | 
             
                        nil
         | 
| 72 79 | 
             
                      # Ruby allows specifying native extensions as '.so' even when DLEXT
         | 
| 73 80 | 
             
                      # is '.bundle'. This is where we handle that case.
         | 
| 74 81 | 
             
                      when DOT_SO
         | 
| 75 82 | 
             
                        x = search_index(feature[0..-4] + DLEXT)
         | 
| 76 83 | 
             
                        return x if x
         | 
| 84 | 
            +
             | 
| 77 85 | 
             
                        if DLEXT2
         | 
| 78 86 | 
             
                          x = search_index(feature[0..-4] + DLEXT2)
         | 
| 79 87 | 
             
                          return x if x
         | 
| @@ -81,7 +89,7 @@ module Bootsnap | |
| 81 89 | 
             
                      else
         | 
| 82 90 | 
             
                        # other, unknown extension. For example, `.rake`. Since we haven't
         | 
| 83 91 | 
             
                        # cached these, we legitimately need to run the load path search.
         | 
| 84 | 
            -
                         | 
| 92 | 
            +
                        return FALLBACK_SCAN
         | 
| 85 93 | 
             
                      end
         | 
| 86 94 | 
             
                    end
         | 
| 87 95 |  | 
| @@ -89,26 +97,18 @@ module Bootsnap | |
| 89 97 | 
             
                    # cases where the file doesn't appear to be on the load path. We should
         | 
| 90 98 | 
             
                    # be able to detect newly-created files without rebooting the
         | 
| 91 99 | 
             
                    # application.
         | 
| 92 | 
            -
                     | 
| 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
         | 
| 100 | 
            +
                    return FALLBACK_SCAN if @development_mode
         | 
| 103 101 | 
             
                  end
         | 
| 104 102 |  | 
| 105 103 | 
             
                  def unshift_paths(sender, *paths)
         | 
| 106 104 | 
             
                    return unless sender == @path_obj
         | 
| 105 | 
            +
             | 
| 107 106 | 
             
                    @mutex.synchronize { unshift_paths_locked(*paths) }
         | 
| 108 107 | 
             
                  end
         | 
| 109 108 |  | 
| 110 109 | 
             
                  def push_paths(sender, *paths)
         | 
| 111 110 | 
             
                    return unless sender == @path_obj
         | 
| 111 | 
            +
             | 
| 112 112 | 
             
                    @mutex.synchronize { push_paths_locked(*paths) }
         | 
| 113 113 | 
             
                  end
         | 
| 114 114 |  | 
| @@ -141,6 +141,7 @@ module Bootsnap | |
| 141 141 | 
             
                        p = Path.new(path)
         | 
| 142 142 | 
             
                        @has_relative_paths = true if p.relative?
         | 
| 143 143 | 
             
                        next if p.non_directory?
         | 
| 144 | 
            +
             | 
| 144 145 | 
             
                        expanded_path = p.expanded_path
         | 
| 145 146 | 
             
                        entries, dirs = p.entries_and_dirs(@store)
         | 
| 146 147 | 
             
                        # push -> low precedence -> set only if unset
         | 
| @@ -155,6 +156,7 @@ module Bootsnap | |
| 155 156 | 
             
                      paths.map(&:to_s).reverse_each do |path|
         | 
| 156 157 | 
             
                        p = Path.new(path)
         | 
| 157 158 | 
             
                        next if p.non_directory?
         | 
| 159 | 
            +
             | 
| 158 160 | 
             
                        expanded_path = p.expanded_path
         | 
| 159 161 | 
             
                        entries, dirs = p.entries_and_dirs(@store)
         | 
| 160 162 | 
             
                        # unshift -> high precedence -> unconditional set
         | 
| @@ -177,48 +179,62 @@ module Bootsnap | |
| 177 179 | 
             
                  end
         | 
| 178 180 |  | 
| 179 181 | 
             
                  if DLEXT2
         | 
| 180 | 
            -
                    def search_index( | 
| 181 | 
            -
                       | 
| 182 | 
            +
                    def search_index(feature, try_extensions: true)
         | 
| 183 | 
            +
                      if try_extensions
         | 
| 184 | 
            +
                        try_index(feature + DOT_RB) ||
         | 
| 185 | 
            +
                          try_index(feature + DLEXT) ||
         | 
| 186 | 
            +
                          try_index(feature + DLEXT2) ||
         | 
| 187 | 
            +
                          try_index(feature)
         | 
| 188 | 
            +
                      else
         | 
| 189 | 
            +
                        try_index(feature)
         | 
| 190 | 
            +
                      end
         | 
| 182 191 | 
             
                    end
         | 
| 183 192 |  | 
| 184 | 
            -
                    def maybe_append_extension( | 
| 185 | 
            -
                      try_ext( | 
| 193 | 
            +
                    def maybe_append_extension(feature)
         | 
| 194 | 
            +
                      try_ext(feature + DOT_RB) ||
         | 
| 195 | 
            +
                        try_ext(feature + DLEXT) ||
         | 
| 196 | 
            +
                        try_ext(feature + DLEXT2) ||
         | 
| 197 | 
            +
                        feature
         | 
| 186 198 | 
             
                    end
         | 
| 187 199 | 
             
                  else
         | 
| 188 | 
            -
                    def search_index( | 
| 189 | 
            -
                       | 
| 200 | 
            +
                    def search_index(feature, try_extensions: true)
         | 
| 201 | 
            +
                      if try_extensions
         | 
| 202 | 
            +
                        try_index(feature + DOT_RB) || try_index(feature + DLEXT) || try_index(feature)
         | 
| 203 | 
            +
                      else
         | 
| 204 | 
            +
                        try_index(feature)
         | 
| 205 | 
            +
                      end
         | 
| 190 206 | 
             
                    end
         | 
| 191 207 |  | 
| 192 | 
            -
                    def maybe_append_extension( | 
| 193 | 
            -
                      try_ext( | 
| 208 | 
            +
                    def maybe_append_extension(feature)
         | 
| 209 | 
            +
                      try_ext(feature + DOT_RB) || try_ext(feature + DLEXT) || feature
         | 
| 194 210 | 
             
                    end
         | 
| 195 211 | 
             
                  end
         | 
| 196 212 |  | 
| 197 213 | 
             
                  s = rand.to_s.force_encoding(Encoding::US_ASCII).freeze
         | 
| 198 214 | 
             
                  if s.respond_to?(:-@)
         | 
| 199 | 
            -
                    if (-s).equal?(s) && (-s.dup).equal?(s) || RUBY_VERSION >=  | 
| 200 | 
            -
                      def try_index( | 
| 201 | 
            -
                        if ( | 
| 202 | 
            -
                          - | 
| 215 | 
            +
                    if (-s).equal?(s) && (-s.dup).equal?(s) || RUBY_VERSION >= "2.7"
         | 
| 216 | 
            +
                      def try_index(feature)
         | 
| 217 | 
            +
                        if (path = @index[feature])
         | 
| 218 | 
            +
                          -File.join(path, feature).freeze
         | 
| 203 219 | 
             
                        end
         | 
| 204 220 | 
             
                      end
         | 
| 205 221 | 
             
                    else
         | 
| 206 | 
            -
                      def try_index( | 
| 207 | 
            -
                        if ( | 
| 208 | 
            -
                          -File.join( | 
| 222 | 
            +
                      def try_index(feature)
         | 
| 223 | 
            +
                        if (path = @index[feature])
         | 
| 224 | 
            +
                          -File.join(path, feature).untaint
         | 
| 209 225 | 
             
                        end
         | 
| 210 226 | 
             
                      end
         | 
| 211 227 | 
             
                    end
         | 
| 212 228 | 
             
                  else
         | 
| 213 | 
            -
                    def try_index( | 
| 214 | 
            -
                      if ( | 
| 215 | 
            -
                        File.join( | 
| 229 | 
            +
                    def try_index(feature)
         | 
| 230 | 
            +
                      if (path = @index[feature])
         | 
| 231 | 
            +
                        File.join(path, feature)
         | 
| 216 232 | 
             
                      end
         | 
| 217 233 | 
             
                    end
         | 
| 218 234 | 
             
                  end
         | 
| 219 235 |  | 
| 220 | 
            -
                  def try_ext( | 
| 221 | 
            -
                     | 
| 236 | 
            +
                  def try_ext(feature)
         | 
| 237 | 
            +
                    feature if File.exist?(feature)
         | 
| 222 238 | 
             
                  end
         | 
| 223 239 | 
             
                end
         | 
| 224 240 | 
             
              end
         | 
| @@ -1,4 +1,5 @@ | |
| 1 1 | 
             
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 2 3 | 
             
            module Bootsnap
         | 
| 3 4 | 
             
              module LoadPathCache
         | 
| 4 5 | 
             
                module ChangeObserver
         | 
| @@ -57,6 +58,7 @@ module Bootsnap | |
| 57 58 |  | 
| 58 59 | 
             
                  def self.register(observer, arr)
         | 
| 59 60 | 
             
                    return if arr.frozen? # can't register observer, but no need to.
         | 
| 61 | 
            +
             | 
| 60 62 | 
             
                    arr.instance_variable_set(:@lpc_observer, observer)
         | 
| 61 63 | 
             
                    arr.extend(ArrayMixin)
         | 
| 62 64 | 
             
                  end
         | 
| @@ -1,47 +1,35 @@ | |
| 1 1 | 
             
            # frozen_string_literal: true
         | 
| 2 | 
            -
            module Bootsnap
         | 
| 3 | 
            -
              module LoadPathCache
         | 
| 4 | 
            -
                module CoreExt
         | 
| 5 | 
            -
                  def self.make_load_error(path)
         | 
| 6 | 
            -
                    err = LoadError.new(+"cannot load such file -- #{path}")
         | 
| 7 | 
            -
                    err.instance_variable_set(Bootsnap::LoadPathCache::ERROR_TAG_IVAR, true)
         | 
| 8 | 
            -
                    err.define_singleton_method(:path) { path }
         | 
| 9 | 
            -
                    err
         | 
| 10 | 
            -
                  end
         | 
| 11 | 
            -
                end
         | 
| 12 | 
            -
              end
         | 
| 13 | 
            -
            end
         | 
| 14 2 |  | 
| 15 3 | 
             
            module Kernel
         | 
| 16 | 
            -
              module_function | 
| 4 | 
            +
              module_function
         | 
| 17 5 |  | 
| 18 6 | 
             
              alias_method(:require_without_bootsnap, :require)
         | 
| 19 7 |  | 
| 20 | 
            -
              # Note that require registers to $LOADED_FEATURES while load does not.
         | 
| 21 | 
            -
              def require_with_bootsnap_lfi(path, resolved = nil)
         | 
| 22 | 
            -
                Bootsnap::LoadPathCache.loaded_features_index.register(path, resolved) do
         | 
| 23 | 
            -
                  require_without_bootsnap(resolved || path)
         | 
| 24 | 
            -
                end
         | 
| 25 | 
            -
              end
         | 
| 26 | 
            -
             | 
| 27 8 | 
             
              def require(path)
         | 
| 28 | 
            -
                 | 
| 9 | 
            +
                string_path = path.to_s
         | 
| 10 | 
            +
                return false if Bootsnap::LoadPathCache.loaded_features_index.key?(string_path)
         | 
| 29 11 |  | 
| 30 | 
            -
                 | 
| 31 | 
            -
             | 
| 32 | 
            -
             | 
| 33 | 
            -
             | 
| 34 | 
            -
             | 
| 35 | 
            -
             | 
| 36 | 
            -
             | 
| 37 | 
            -
             | 
| 38 | 
            -
             | 
| 39 | 
            -
             | 
| 40 | 
            -
             | 
| 41 | 
            -
             | 
| 42 | 
            -
             | 
| 43 | 
            -
             | 
| 44 | 
            -
                   | 
| 12 | 
            +
                resolved = Bootsnap::LoadPathCache.load_path_cache.find(string_path)
         | 
| 13 | 
            +
                if Bootsnap::LoadPathCache::FALLBACK_SCAN.equal?(resolved)
         | 
| 14 | 
            +
                  if (cursor = Bootsnap::LoadPathCache.loaded_features_index.cursor(string_path))
         | 
| 15 | 
            +
                    ret = require_without_bootsnap(path)
         | 
| 16 | 
            +
                    resolved = Bootsnap::LoadPathCache.loaded_features_index.identify(string_path, cursor)
         | 
| 17 | 
            +
                    Bootsnap::LoadPathCache.loaded_features_index.register(string_path, resolved)
         | 
| 18 | 
            +
                    return ret
         | 
| 19 | 
            +
                  else
         | 
| 20 | 
            +
                    return require_without_bootsnap(path)
         | 
| 21 | 
            +
                  end
         | 
| 22 | 
            +
                elsif false == resolved
         | 
| 23 | 
            +
                  return false
         | 
| 24 | 
            +
                elsif resolved.nil?
         | 
| 25 | 
            +
                  error = LoadError.new(+"cannot load such file -- #{path}")
         | 
| 26 | 
            +
                  error.instance_variable_set(:@path, path)
         | 
| 27 | 
            +
                  raise error
         | 
| 28 | 
            +
                else
         | 
| 29 | 
            +
                  # Note that require registers to $LOADED_FEATURES while load does not.
         | 
| 30 | 
            +
                  ret = require_without_bootsnap(resolved)
         | 
| 31 | 
            +
                  Bootsnap::LoadPathCache.loaded_features_index.register(string_path, resolved)
         | 
| 32 | 
            +
                  return ret
         | 
| 45 33 | 
             
                end
         | 
| 46 34 | 
             
              end
         | 
| 47 35 |  | 
| @@ -56,25 +44,9 @@ module Kernel | |
| 56 44 |  | 
| 57 45 | 
             
              alias_method(:load_without_bootsnap, :load)
         | 
| 58 46 | 
             
              def load(path, wrap = false)
         | 
| 59 | 
            -
                if (resolved = Bootsnap::LoadPathCache.load_path_cache.find(path))
         | 
| 60 | 
            -
                   | 
| 61 | 
            -
                 | 
| 62 | 
            -
             | 
| 63 | 
            -
                # load also allows relative paths from pwd even when not in $:
         | 
| 64 | 
            -
                if File.exist?(relative = File.expand_path(path).freeze)
         | 
| 65 | 
            -
                  return load_without_bootsnap(relative, wrap)
         | 
| 66 | 
            -
                end
         | 
| 67 | 
            -
             | 
| 68 | 
            -
                raise(Bootsnap::LoadPathCache::CoreExt.make_load_error(path))
         | 
| 69 | 
            -
              rescue LoadError => e
         | 
| 70 | 
            -
                e.instance_variable_set(Bootsnap::LoadPathCache::ERROR_TAG_IVAR, true)
         | 
| 71 | 
            -
                raise(e)
         | 
| 72 | 
            -
              rescue Bootsnap::LoadPathCache::ReturnFalse
         | 
| 73 | 
            -
                false
         | 
| 74 | 
            -
              rescue Bootsnap::LoadPathCache::FallbackScan
         | 
| 75 | 
            -
                fallback = true
         | 
| 76 | 
            -
              ensure
         | 
| 77 | 
            -
                if fallback
         | 
| 47 | 
            +
                if (resolved = Bootsnap::LoadPathCache.load_path_cache.find(path, try_extensions: false))
         | 
| 48 | 
            +
                  load_without_bootsnap(resolved, wrap)
         | 
| 49 | 
            +
                else
         | 
| 78 50 | 
             
                  load_without_bootsnap(path, wrap)
         | 
| 79 51 | 
             
                end
         | 
| 80 52 | 
             
              end
         | 
| @@ -90,17 +62,13 @@ class Module | |
| 90 62 | 
             
                # The challenge is that we don't control the point at which the entry gets
         | 
| 91 63 | 
             
                # added to $LOADED_FEATURES and won't be able to hook that modification
         | 
| 92 64 | 
             
                # since it's done in C-land.
         | 
| 93 | 
            -
                 | 
| 94 | 
            -
             | 
| 95 | 
            -
                e.instance_variable_set(Bootsnap::LoadPathCache::ERROR_TAG_IVAR, true)
         | 
| 96 | 
            -
                raise(e)
         | 
| 97 | 
            -
              rescue Bootsnap::LoadPathCache::ReturnFalse
         | 
| 98 | 
            -
                false
         | 
| 99 | 
            -
              rescue Bootsnap::LoadPathCache::FallbackScan
         | 
| 100 | 
            -
                fallback = true
         | 
| 101 | 
            -
              ensure
         | 
| 102 | 
            -
                if fallback
         | 
| 65 | 
            +
                resolved = Bootsnap::LoadPathCache.load_path_cache.find(path)
         | 
| 66 | 
            +
                if Bootsnap::LoadPathCache::FALLBACK_SCAN.equal?(resolved)
         | 
| 103 67 | 
             
                  autoload_without_bootsnap(const, path)
         | 
| 68 | 
            +
                elsif resolved == false
         | 
| 69 | 
            +
                  return false
         | 
| 70 | 
            +
                else
         | 
| 71 | 
            +
                  autoload_without_bootsnap(const, resolved || path)
         | 
| 104 72 | 
             
                end
         | 
| 105 73 | 
             
              end
         | 
| 106 74 | 
             
            end
         | 
| @@ -29,14 +29,15 @@ module Bootsnap | |
| 29 29 | 
             
                    @mutex = Mutex.new
         | 
| 30 30 |  | 
| 31 31 | 
             
                    # In theory the user could mutate $LOADED_FEATURES and invalidate our
         | 
| 32 | 
            -
                    # cache. If this ever comes up in practice  | 
| 33 | 
            -
                    # enterprising reader, feels inclined to solve this problem  | 
| 32 | 
            +
                    # cache. If this ever comes up in practice - or if you, the
         | 
| 33 | 
            +
                    # enterprising reader, feels inclined to solve this problem - we could
         | 
| 34 34 | 
             
                    # parallel the work done with ChangeObserver on $LOAD_PATH to mirror
         | 
| 35 35 | 
             
                    # updates to our @lfi.
         | 
| 36 36 | 
             
                    $LOADED_FEATURES.each do |feat|
         | 
| 37 37 | 
             
                      hash = feat.hash
         | 
| 38 38 | 
             
                      $LOAD_PATH.each do |lpe|
         | 
| 39 39 | 
             
                        next unless feat.start_with?(lpe)
         | 
| 40 | 
            +
             | 
| 40 41 | 
             
                        # /a/b/lib/my/foo.rb
         | 
| 41 42 | 
             
                        #          ^^^^^^^^^
         | 
| 42 43 | 
             
                        short = feat[(lpe.length + 1)..-1]
         | 
| @@ -68,11 +69,30 @@ module Bootsnap | |
| 68 69 | 
             
                    @mutex.synchronize { @lfi.key?(feature) }
         | 
| 69 70 | 
             
                  end
         | 
| 70 71 |  | 
| 72 | 
            +
                  def cursor(short)
         | 
| 73 | 
            +
                    unless Bootsnap.absolute_path?(short.to_s)
         | 
| 74 | 
            +
                      $LOADED_FEATURES.size
         | 
| 75 | 
            +
                    end
         | 
| 76 | 
            +
                  end
         | 
| 77 | 
            +
             | 
| 78 | 
            +
                  def identify(short, cursor)
         | 
| 79 | 
            +
                    $LOADED_FEATURES[cursor..-1].detect do |feat|
         | 
| 80 | 
            +
                      offset = 0
         | 
| 81 | 
            +
                      while (offset = feat.index(short, offset))
         | 
| 82 | 
            +
                        if feat.index(".", offset + 1) && !feat.index("/", offset + 2)
         | 
| 83 | 
            +
                          break true
         | 
| 84 | 
            +
                        else
         | 
| 85 | 
            +
                          offset += 1
         | 
| 86 | 
            +
                        end
         | 
| 87 | 
            +
                      end
         | 
| 88 | 
            +
                    end
         | 
| 89 | 
            +
                  end
         | 
| 90 | 
            +
             | 
| 71 91 | 
             
                  # There is a relatively uncommon case where we could miss adding an
         | 
| 72 92 | 
             
                  # entry:
         | 
| 73 93 | 
             
                  #
         | 
| 74 94 | 
             
                  # If the user asked for e.g. `require 'bundler'`, and we went through the
         | 
| 75 | 
            -
                  # ` | 
| 95 | 
            +
                  # `FALLBACK_SCAN` pathway in `kernel_require.rb` and therefore did not
         | 
| 76 96 | 
             
                  # pass `long` (the full expanded absolute path), then we did are not able
         | 
| 77 97 | 
             
                  # to confidently add the `bundler.rb` form to @lfi.
         | 
| 78 98 | 
             
                  #
         | 
| @@ -82,15 +102,8 @@ module Bootsnap | |
| 82 102 | 
             
                  #    not quite right; or
         | 
| 83 103 | 
             
                  # 2. Inspect $LOADED_FEATURES upon return from yield to find the matching
         | 
| 84 104 | 
             
                  #    entry.
         | 
| 85 | 
            -
                  def register(short, long | 
| 86 | 
            -
                    if  | 
| 87 | 
            -
                      pat = %r{/#{Regexp.escape(short)}(\.[^/]+)?$}
         | 
| 88 | 
            -
                      len = $LOADED_FEATURES.size
         | 
| 89 | 
            -
                      ret = yield
         | 
| 90 | 
            -
                      long = $LOADED_FEATURES[len..-1].detect { |feat| feat =~ pat }
         | 
| 91 | 
            -
                    else
         | 
| 92 | 
            -
                      ret = yield
         | 
| 93 | 
            -
                    end
         | 
| 105 | 
            +
                  def register(short, long)
         | 
| 106 | 
            +
                    return if Bootsnap.absolute_path?(short)
         | 
| 94 107 |  | 
| 95 108 | 
             
                    hash = long.hash
         | 
| 96 109 |  | 
| @@ -109,13 +122,11 @@ module Bootsnap | |
| 109 122 | 
             
                      @lfi[short] = hash
         | 
| 110 123 | 
             
                      (@lfi[altname] = hash) if altname
         | 
| 111 124 | 
             
                    end
         | 
| 112 | 
            -
             | 
| 113 | 
            -
                    ret
         | 
| 114 125 | 
             
                  end
         | 
| 115 126 |  | 
| 116 127 | 
             
                  private
         | 
| 117 128 |  | 
| 118 | 
            -
                  STRIP_EXTENSION = /\.[^.] | 
| 129 | 
            +
                  STRIP_EXTENSION = /\.[^.]*?$/.freeze
         | 
| 119 130 | 
             
                  private_constant(:STRIP_EXTENSION)
         | 
| 120 131 |  | 
| 121 132 | 
             
                  # Might Ruby automatically search for this extension if
         | 
| @@ -132,15 +143,15 @@ module Bootsnap | |
| 132 143 | 
             
                  # with calling a Ruby file 'x.dylib.rb' and then requiring it as 'x.dylib'.)
         | 
| 133 144 | 
             
                  #
         | 
| 134 145 | 
             
                  # See <https://ruby-doc.org/core-2.6.4/Kernel.html#method-i-require>.
         | 
| 135 | 
            -
                  def extension_elidable?( | 
| 136 | 
            -
                     | 
| 146 | 
            +
                  def extension_elidable?(feature)
         | 
| 147 | 
            +
                    feature.to_s.end_with?(".rb", ".so", ".o", ".dll", ".dylib")
         | 
| 137 148 | 
             
                  end
         | 
| 138 149 |  | 
| 139 | 
            -
                  def strip_extension_if_elidable( | 
| 140 | 
            -
                    if extension_elidable?( | 
| 141 | 
            -
                       | 
| 150 | 
            +
                  def strip_extension_if_elidable(feature)
         | 
| 151 | 
            +
                    if extension_elidable?(feature)
         | 
| 152 | 
            +
                      feature.sub(STRIP_EXTENSION, "")
         | 
| 142 153 | 
             
                    else
         | 
| 143 | 
            -
                       | 
| 154 | 
            +
                      feature
         | 
| 144 155 | 
             
                    end
         | 
| 145 156 | 
             
                  end
         | 
| 146 157 | 
             
                end
         | 
| @@ -1,5 +1,6 @@ | |
| 1 1 | 
             
            # frozen_string_literal: true
         | 
| 2 | 
            -
             | 
| 2 | 
            +
             | 
| 3 | 
            +
            require_relative("path_scanner")
         | 
| 3 4 |  | 
| 4 5 | 
             
            module Bootsnap
         | 
| 5 6 | 
             
              module LoadPathCache
         | 
| @@ -43,6 +44,7 @@ module Bootsnap | |
| 43 44 | 
             
                      # set to zero anyway, just in case we change the stability heuristics.
         | 
| 44 45 | 
             
                      _, entries, dirs = store.get(expanded_path)
         | 
| 45 46 | 
             
                      return [entries, dirs] if entries # cache hit
         | 
| 47 | 
            +
             | 
| 46 48 | 
             
                      entries, dirs = scan!
         | 
| 47 49 | 
             
                      store.set(expanded_path, [0, entries, dirs])
         | 
| 48 50 | 
             
                      return [entries, dirs]
         | 
| @@ -93,8 +95,8 @@ module Bootsnap | |
| 93 95 |  | 
| 94 96 | 
             
                  # Built-in ruby lib stuff doesn't change, but things can occasionally be
         | 
| 95 97 | 
             
                  # installed into sitedir, which generally lives under libdir.
         | 
| 96 | 
            -
                  RUBY_LIBDIR  = RbConfig::CONFIG[ | 
| 97 | 
            -
                  RUBY_SITEDIR = RbConfig::CONFIG[ | 
| 98 | 
            +
                  RUBY_LIBDIR  = RbConfig::CONFIG["libdir"]
         | 
| 99 | 
            +
                  RUBY_SITEDIR = RbConfig::CONFIG["sitedir"]
         | 
| 98 100 |  | 
| 99 101 | 
             
                  def stability
         | 
| 100 102 | 
             
                    @stability ||= begin
         | 
| @@ -1,18 +1,18 @@ | |
| 1 1 | 
             
            # frozen_string_literal: true
         | 
| 2 2 |  | 
| 3 | 
            -
            require_relative( | 
| 3 | 
            +
            require_relative("../explicit_require")
         | 
| 4 4 |  | 
| 5 5 | 
             
            module Bootsnap
         | 
| 6 6 | 
             
              module LoadPathCache
         | 
| 7 7 | 
             
                module PathScanner
         | 
| 8 8 | 
             
                  REQUIRABLE_EXTENSIONS = [DOT_RB] + DL_EXTENSIONS
         | 
| 9 9 | 
             
                  NORMALIZE_NATIVE_EXTENSIONS = !DL_EXTENSIONS.include?(LoadPathCache::DOT_SO)
         | 
| 10 | 
            -
                  ALTERNATIVE_NATIVE_EXTENSIONS_PATTERN = /\.(o|bundle|dylib)\z | 
| 10 | 
            +
                  ALTERNATIVE_NATIVE_EXTENSIONS_PATTERN = /\.(o|bundle|dylib)\z/.freeze
         | 
| 11 11 |  | 
| 12 12 | 
             
                  BUNDLE_PATH = if Bootsnap.bundler?
         | 
| 13 13 | 
             
                    (Bundler.bundle_path.cleanpath.to_s << LoadPathCache::SLASH).freeze
         | 
| 14 14 | 
             
                  else
         | 
| 15 | 
            -
                     | 
| 15 | 
            +
                    ""
         | 
| 16 16 | 
             
                  end
         | 
| 17 17 |  | 
| 18 18 | 
             
                  class << self
         | 
| @@ -44,7 +44,8 @@ module Bootsnap | |
| 44 44 |  | 
| 45 45 | 
             
                    def walk(absolute_dir_path, relative_dir_path, &block)
         | 
| 46 46 | 
             
                      Dir.foreach(absolute_dir_path) do |name|
         | 
| 47 | 
            -
                        next if name.start_with?( | 
| 47 | 
            +
                        next if name.start_with?(".")
         | 
| 48 | 
            +
             | 
| 48 49 | 
             
                        relative_path = relative_dir_path ? File.join(relative_dir_path, name) : name
         | 
| 49 50 |  | 
| 50 51 | 
             
                        absolute_path = "#{absolute_dir_path}/#{name}"
         | 
| @@ -58,7 +59,7 @@ module Bootsnap | |
| 58 59 | 
             
                      end
         | 
| 59 60 | 
             
                    end
         | 
| 60 61 |  | 
| 61 | 
            -
                    if RUBY_VERSION >=  | 
| 62 | 
            +
                    if RUBY_VERSION >= "3.1"
         | 
| 62 63 | 
             
                      def os_path(path)
         | 
| 63 64 | 
             
                        path.freeze
         | 
| 64 65 | 
             
                      end
         | 
| @@ -1,12 +1,15 @@ | |
| 1 1 | 
             
            # frozen_string_literal: true
         | 
| 2 | 
            -
            require_relative('../explicit_require')
         | 
| 3 2 |  | 
| 4 | 
            -
             | 
| 5 | 
            -
             | 
| 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 |  | 
| @@ -23,6 +26,7 @@ module Bootsnap | |
| 23 26 |  | 
| 24 27 | 
             
                  def fetch(key)
         | 
| 25 28 | 
             
                    raise(SetOutsideTransactionNotAllowed) unless @txn_mutex.owned?
         | 
| 29 | 
            +
             | 
| 26 30 | 
             
                    v = get(key)
         | 
| 27 31 | 
             
                    unless v
         | 
| 28 32 | 
             
                      @dirty = true
         | 
| @@ -34,6 +38,7 @@ module Bootsnap | |
| 34 38 |  | 
| 35 39 | 
             
                  def set(key, value)
         | 
| 36 40 | 
             
                    raise(SetOutsideTransactionNotAllowed) unless @txn_mutex.owned?
         | 
| 41 | 
            +
             | 
| 37 42 | 
             
                    if value != @data[key]
         | 
| 38 43 | 
             
                      @dirty = true
         | 
| 39 44 | 
             
                      @data[key] = value
         | 
| @@ -42,6 +47,7 @@ module Bootsnap | |
| 42 47 |  | 
| 43 48 | 
             
                  def transaction
         | 
| 44 49 | 
             
                    raise(NestedTransactionError) if @txn_mutex.owned?
         | 
| 50 | 
            +
             | 
| 45 51 | 
             
                    @txn_mutex.synchronize do
         | 
| 46 52 | 
             
                      begin
         | 
| 47 53 | 
             
                        yield
         | 
| @@ -62,15 +68,20 @@ module Bootsnap | |
| 62 68 |  | 
| 63 69 | 
             
                  def load_data
         | 
| 64 70 | 
             
                    @data = begin
         | 
| 65 | 
            -
                      File.open(@store_path, encoding: Encoding::BINARY) do |io|
         | 
| 71 | 
            +
                      data = File.open(@store_path, encoding: Encoding::BINARY) do |io|
         | 
| 66 72 | 
             
                        MessagePack.load(io)
         | 
| 67 73 | 
             
                      end
         | 
| 74 | 
            +
                      if data.is_a?(Hash) && data[VERSION_KEY] == CURRENT_VERSION
         | 
| 75 | 
            +
                        data
         | 
| 76 | 
            +
                      else
         | 
| 77 | 
            +
                        default_data
         | 
| 78 | 
            +
                      end
         | 
| 68 79 | 
             
                    # handle malformed data due to upgrade incompatibility
         | 
| 69 80 | 
             
                    rescue Errno::ENOENT, MessagePack::MalformedFormatError, MessagePack::UnknownExtTypeError, EOFError
         | 
| 70 | 
            -
                       | 
| 81 | 
            +
                      default_data
         | 
| 71 82 | 
             
                    rescue ArgumentError => error
         | 
| 72 83 | 
             
                      if error.message =~ /negative array size/
         | 
| 73 | 
            -
                         | 
| 84 | 
            +
                        default_data
         | 
| 74 85 | 
             
                      else
         | 
| 75 86 | 
             
                        raise
         | 
| 76 87 | 
             
                      end
         | 
| @@ -78,9 +89,11 @@ module Bootsnap | |
| 78 89 | 
             
                  end
         | 
| 79 90 |  | 
| 80 91 | 
             
                  def dump_data
         | 
| 92 | 
            +
                    require "fileutils" unless defined? FileUtils
         | 
| 93 | 
            +
             | 
| 81 94 | 
             
                    # Change contents atomically so other processes can't get invalid
         | 
| 82 95 | 
             
                    # caches if they read at an inopportune time.
         | 
| 83 | 
            -
                    tmp = "#{@store_path}.#{Process.pid}.#{(rand *  | 
| 96 | 
            +
                    tmp = "#{@store_path}.#{Process.pid}.#{(rand * 100_000).to_i}.tmp"
         | 
| 84 97 | 
             
                    FileUtils.mkpath(File.dirname(tmp))
         | 
| 85 98 | 
             
                    exclusive_write = File::Constants::CREAT | File::Constants::EXCL | File::Constants::WRONLY
         | 
| 86 99 | 
             
                    # `encoding:` looks redundant wrt `binwrite`, but necessary on windows
         | 
| @@ -93,6 +106,10 @@ module Bootsnap | |
| 93 106 | 
             
                    retry
         | 
| 94 107 | 
             
                  rescue SystemCallError
         | 
| 95 108 | 
             
                  end
         | 
| 109 | 
            +
             | 
| 110 | 
            +
                  def default_data
         | 
| 111 | 
            +
                    {VERSION_KEY => CURRENT_VERSION}
         | 
| 112 | 
            +
                  end
         | 
| 96 113 | 
             
                end
         | 
| 97 114 | 
             
              end
         | 
| 98 115 | 
             
            end
         |