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.
- checksums.yaml +7 -0
- data/.github/CODEOWNERS +2 -0
- data/.github/probots.yml +2 -0
- data/.gitignore +17 -0
- data/.rubocop.yml +20 -0
- data/.travis.yml +21 -0
- data/CHANGELOG.md +122 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/CONTRIBUTING.md +21 -0
- data/Gemfile +9 -0
- data/LICENSE.txt +21 -0
- data/README.jp.md +231 -0
- data/README.md +304 -0
- data/Rakefile +13 -0
- data/bin/ci +10 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/bin/test-minimal-support +7 -0
- data/bin/testunit +8 -0
- data/bootsnap.gemspec +46 -0
- data/dev.yml +10 -0
- data/ext/bootsnap/bootsnap.c +829 -0
- data/ext/bootsnap/bootsnap.h +6 -0
- data/ext/bootsnap/extconf.rb +19 -0
- data/lib/bootsnap.rb +48 -0
- data/lib/bootsnap/bundler.rb +15 -0
- data/lib/bootsnap/compile_cache.rb +43 -0
- data/lib/bootsnap/compile_cache/iseq.rb +73 -0
- data/lib/bootsnap/compile_cache/yaml.rb +63 -0
- data/lib/bootsnap/explicit_require.rb +50 -0
- data/lib/bootsnap/load_path_cache.rb +78 -0
- data/lib/bootsnap/load_path_cache/cache.rb +208 -0
- data/lib/bootsnap/load_path_cache/change_observer.rb +63 -0
- data/lib/bootsnap/load_path_cache/core_ext/active_support.rb +107 -0
- data/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb +93 -0
- data/lib/bootsnap/load_path_cache/core_ext/loaded_features.rb +18 -0
- data/lib/bootsnap/load_path_cache/loaded_features_index.rb +148 -0
- data/lib/bootsnap/load_path_cache/path.rb +114 -0
- data/lib/bootsnap/load_path_cache/path_scanner.rb +50 -0
- data/lib/bootsnap/load_path_cache/realpath_cache.rb +32 -0
- data/lib/bootsnap/load_path_cache/store.rb +90 -0
- data/lib/bootsnap/setup.rb +39 -0
- data/lib/bootsnap/version.rb +4 -0
- data/shipit.rubygems.yml +0 -0
- metadata +174 -0
| @@ -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")
         | 
    
        data/lib/bootsnap.rb
    ADDED
    
    | @@ -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
         |