bootsnap 1.4.5 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +46 -0
- data/README.md +17 -3
- data/exe/bootsnap +5 -0
- data/ext/bootsnap/bootsnap.c +183 -65
- data/ext/bootsnap/extconf.rb +1 -0
- data/lib/bootsnap/bundler.rb +1 -0
- data/lib/bootsnap/cli/worker_pool.rb +131 -0
- data/lib/bootsnap/cli.rb +246 -0
- data/lib/bootsnap/compile_cache/iseq.rb +22 -7
- data/lib/bootsnap/compile_cache/yaml.rb +89 -39
- data/lib/bootsnap/compile_cache.rb +3 -2
- data/lib/bootsnap/explicit_require.rb +1 -0
- data/lib/bootsnap/load_path_cache/cache.rb +8 -8
- data/lib/bootsnap/load_path_cache/change_observer.rb +2 -1
- data/lib/bootsnap/load_path_cache/core_ext/active_support.rb +1 -0
- data/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb +18 -5
- data/lib/bootsnap/load_path_cache/core_ext/loaded_features.rb +1 -0
- data/lib/bootsnap/load_path_cache/loaded_features_index.rb +33 -10
- data/lib/bootsnap/load_path_cache/path.rb +3 -2
- data/lib/bootsnap/load_path_cache/path_scanner.rb +39 -26
- data/lib/bootsnap/load_path_cache/realpath_cache.rb +5 -5
- data/lib/bootsnap/load_path_cache/store.rb +6 -5
- data/lib/bootsnap/load_path_cache.rb +1 -1
- data/lib/bootsnap/setup.rb +1 -0
- data/lib/bootsnap/version.rb +2 -1
- data/lib/bootsnap.rb +4 -2
- metadata +15 -28
- data/.github/CODEOWNERS +0 -2
- data/.github/probots.yml +0 -2
- data/.gitignore +0 -17
- data/.rubocop.yml +0 -20
- data/.travis.yml +0 -21
- data/CODE_OF_CONDUCT.md +0 -74
- data/CONTRIBUTING.md +0 -21
- data/Gemfile +0 -8
- data/README.jp.md +0 -231
- data/Rakefile +0 -12
- data/bin/ci +0 -10
- data/bin/console +0 -14
- data/bin/setup +0 -8
- data/bin/test-minimal-support +0 -7
- data/bin/testunit +0 -8
- data/bootsnap.gemspec +0 -45
- data/dev.yml +0 -10
- data/shipit.rubygems.yml +0 -0
| @@ -0,0 +1,131 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Bootsnap
         | 
| 4 | 
            +
              class CLI
         | 
| 5 | 
            +
                class WorkerPool
         | 
| 6 | 
            +
                  class << self
         | 
| 7 | 
            +
                    def create(size:, jobs:)
         | 
| 8 | 
            +
                      if size > 0 && Process.respond_to?(:fork)
         | 
| 9 | 
            +
                        new(size: size, jobs: jobs)
         | 
| 10 | 
            +
                      else
         | 
| 11 | 
            +
                        Inline.new(jobs: jobs)
         | 
| 12 | 
            +
                      end
         | 
| 13 | 
            +
                    end
         | 
| 14 | 
            +
                  end
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                  class Inline
         | 
| 17 | 
            +
                    def initialize(jobs: {})
         | 
| 18 | 
            +
                      @jobs = jobs
         | 
| 19 | 
            +
                    end
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                    def push(job, *args)
         | 
| 22 | 
            +
                      @jobs.fetch(job).call(*args)
         | 
| 23 | 
            +
                      nil
         | 
| 24 | 
            +
                    end
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                    def spawn
         | 
| 27 | 
            +
                      # noop
         | 
| 28 | 
            +
                    end
         | 
| 29 | 
            +
             | 
| 30 | 
            +
                    def shutdown
         | 
| 31 | 
            +
                      # noop
         | 
| 32 | 
            +
                    end
         | 
| 33 | 
            +
                  end
         | 
| 34 | 
            +
             | 
| 35 | 
            +
                  class Worker
         | 
| 36 | 
            +
                    attr_reader :to_io, :pid
         | 
| 37 | 
            +
             | 
| 38 | 
            +
                    def initialize(jobs)
         | 
| 39 | 
            +
                      @jobs = jobs
         | 
| 40 | 
            +
                      @pipe_out, @to_io = IO.pipe
         | 
| 41 | 
            +
                      @pid = nil
         | 
| 42 | 
            +
                    end
         | 
| 43 | 
            +
             | 
| 44 | 
            +
                    def write(message, block: true)
         | 
| 45 | 
            +
                      payload = Marshal.dump(message)
         | 
| 46 | 
            +
                      if block
         | 
| 47 | 
            +
                        to_io.write(payload)
         | 
| 48 | 
            +
                        true
         | 
| 49 | 
            +
                      else
         | 
| 50 | 
            +
                        to_io.write_nonblock(payload, exception: false) != :wait_writable
         | 
| 51 | 
            +
                      end
         | 
| 52 | 
            +
                    end
         | 
| 53 | 
            +
             | 
| 54 | 
            +
                    def close
         | 
| 55 | 
            +
                      to_io.close
         | 
| 56 | 
            +
                    end
         | 
| 57 | 
            +
             | 
| 58 | 
            +
                    def work_loop
         | 
| 59 | 
            +
                      loop do
         | 
| 60 | 
            +
                        job, *args = Marshal.load(@pipe_out)
         | 
| 61 | 
            +
                        return if job == :exit
         | 
| 62 | 
            +
                        @jobs.fetch(job).call(*args)
         | 
| 63 | 
            +
                      end
         | 
| 64 | 
            +
                    rescue IOError
         | 
| 65 | 
            +
                      nil
         | 
| 66 | 
            +
                    end
         | 
| 67 | 
            +
             | 
| 68 | 
            +
                    def spawn
         | 
| 69 | 
            +
                      @pid = Process.fork do
         | 
| 70 | 
            +
                        to_io.close
         | 
| 71 | 
            +
                        work_loop
         | 
| 72 | 
            +
                        exit!(0)
         | 
| 73 | 
            +
                      end
         | 
| 74 | 
            +
                      @pipe_out.close
         | 
| 75 | 
            +
                      true
         | 
| 76 | 
            +
                    end
         | 
| 77 | 
            +
                  end
         | 
| 78 | 
            +
             | 
| 79 | 
            +
                  def initialize(size:, jobs: {})
         | 
| 80 | 
            +
                    @size = size
         | 
| 81 | 
            +
                    @jobs = jobs
         | 
| 82 | 
            +
                    @queue = Queue.new
         | 
| 83 | 
            +
                    @pids = []
         | 
| 84 | 
            +
                  end
         | 
| 85 | 
            +
             | 
| 86 | 
            +
                  def spawn
         | 
| 87 | 
            +
                    @workers = @size.times.map { Worker.new(@jobs) }
         | 
| 88 | 
            +
                    @workers.each(&:spawn)
         | 
| 89 | 
            +
                    @dispatcher_thread = Thread.new { dispatch_loop }
         | 
| 90 | 
            +
                    @dispatcher_thread.abort_on_exception = true
         | 
| 91 | 
            +
                    true
         | 
| 92 | 
            +
                  end
         | 
| 93 | 
            +
             | 
| 94 | 
            +
                  def dispatch_loop
         | 
| 95 | 
            +
                    loop do
         | 
| 96 | 
            +
                      case job = @queue.pop
         | 
| 97 | 
            +
                      when nil
         | 
| 98 | 
            +
                        @workers.each do |worker|
         | 
| 99 | 
            +
                          worker.write([:exit])
         | 
| 100 | 
            +
                          worker.close
         | 
| 101 | 
            +
                        end
         | 
| 102 | 
            +
                        return true
         | 
| 103 | 
            +
                      else
         | 
| 104 | 
            +
                        unless @workers.sample.write(job, block: false)
         | 
| 105 | 
            +
                          free_worker.write(job)
         | 
| 106 | 
            +
                        end
         | 
| 107 | 
            +
                      end
         | 
| 108 | 
            +
                    end
         | 
| 109 | 
            +
                  end
         | 
| 110 | 
            +
             | 
| 111 | 
            +
                  def free_worker
         | 
| 112 | 
            +
                    IO.select(nil, @workers)[1].sample
         | 
| 113 | 
            +
                  end
         | 
| 114 | 
            +
             | 
| 115 | 
            +
                  def push(*args)
         | 
| 116 | 
            +
                    @queue.push(args)
         | 
| 117 | 
            +
                    nil
         | 
| 118 | 
            +
                  end
         | 
| 119 | 
            +
             | 
| 120 | 
            +
                  def shutdown
         | 
| 121 | 
            +
                    @queue.close
         | 
| 122 | 
            +
                    @dispatcher_thread.join
         | 
| 123 | 
            +
                    @workers.each do |worker|
         | 
| 124 | 
            +
                      _pid, status = Process.wait2(worker.pid)
         | 
| 125 | 
            +
                      return status.exitstatus unless status.success?
         | 
| 126 | 
            +
                    end
         | 
| 127 | 
            +
                    nil
         | 
| 128 | 
            +
                  end
         | 
| 129 | 
            +
                end
         | 
| 130 | 
            +
              end
         | 
| 131 | 
            +
            end
         | 
    
        data/lib/bootsnap/cli.rb
    ADDED
    
    | @@ -0,0 +1,246 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require 'bootsnap'
         | 
| 4 | 
            +
            require 'bootsnap/cli/worker_pool'
         | 
| 5 | 
            +
            require 'optparse'
         | 
| 6 | 
            +
            require 'fileutils'
         | 
| 7 | 
            +
            require 'etc'
         | 
| 8 | 
            +
             | 
| 9 | 
            +
            module Bootsnap
         | 
| 10 | 
            +
              class CLI
         | 
| 11 | 
            +
                unless Regexp.method_defined?(:match?)
         | 
| 12 | 
            +
                  module RegexpMatchBackport
         | 
| 13 | 
            +
                    refine Regexp do
         | 
| 14 | 
            +
                      def match?(string)
         | 
| 15 | 
            +
                        !!match(string)
         | 
| 16 | 
            +
                      end
         | 
| 17 | 
            +
                    end
         | 
| 18 | 
            +
                  end
         | 
| 19 | 
            +
                  using RegexpMatchBackport
         | 
| 20 | 
            +
                end
         | 
| 21 | 
            +
             | 
| 22 | 
            +
                attr_reader :cache_dir, :argv
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                attr_accessor :compile_gemfile, :exclude, :verbose, :iseq, :yaml, :jobs
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                def initialize(argv)
         | 
| 27 | 
            +
                  @argv = argv
         | 
| 28 | 
            +
                  self.cache_dir = ENV.fetch('BOOTSNAP_CACHE_DIR', 'tmp/cache')
         | 
| 29 | 
            +
                  self.compile_gemfile = false
         | 
| 30 | 
            +
                  self.exclude = nil
         | 
| 31 | 
            +
                  self.verbose = false
         | 
| 32 | 
            +
                  self.jobs = Etc.nprocessors
         | 
| 33 | 
            +
                  self.iseq = true
         | 
| 34 | 
            +
                  self.yaml = true
         | 
| 35 | 
            +
                end
         | 
| 36 | 
            +
             | 
| 37 | 
            +
                def precompile_command(*sources)
         | 
| 38 | 
            +
                  require 'bootsnap/compile_cache/iseq'
         | 
| 39 | 
            +
                  require 'bootsnap/compile_cache/yaml'
         | 
| 40 | 
            +
             | 
| 41 | 
            +
                  fix_default_encoding do
         | 
| 42 | 
            +
                    Bootsnap::CompileCache::ISeq.cache_dir = self.cache_dir
         | 
| 43 | 
            +
                    Bootsnap::CompileCache::YAML.init!
         | 
| 44 | 
            +
                    Bootsnap::CompileCache::YAML.cache_dir = self.cache_dir
         | 
| 45 | 
            +
             | 
| 46 | 
            +
                    @work_pool = WorkerPool.create(size: jobs, jobs: {
         | 
| 47 | 
            +
                      ruby: method(:precompile_ruby),
         | 
| 48 | 
            +
                      yaml: method(:precompile_yaml),
         | 
| 49 | 
            +
                    })
         | 
| 50 | 
            +
                    @work_pool.spawn
         | 
| 51 | 
            +
             | 
| 52 | 
            +
                    main_sources = sources.map { |d| File.expand_path(d) }
         | 
| 53 | 
            +
                    precompile_ruby_files(main_sources)
         | 
| 54 | 
            +
                    precompile_yaml_files(main_sources)
         | 
| 55 | 
            +
             | 
| 56 | 
            +
                    if compile_gemfile
         | 
| 57 | 
            +
                      # Some gems embed their tests, they're very unlikely to be loaded, so not worth precompiling.
         | 
| 58 | 
            +
                      gem_exclude = Regexp.union([exclude, '/spec/', '/test/'].compact)
         | 
| 59 | 
            +
                      precompile_ruby_files($LOAD_PATH.map { |d| File.expand_path(d) }, exclude: gem_exclude)
         | 
| 60 | 
            +
             | 
| 61 | 
            +
                      # Gems that include YAML files usually don't put them in `lib/`.
         | 
| 62 | 
            +
                      # So we look at the gem root.
         | 
| 63 | 
            +
                      gem_pattern = %r{^#{Regexp.escape(Bundler.bundle_path.to_s)}/?(?:bundler/)?gem\/[^/]+}
         | 
| 64 | 
            +
                      gem_paths = $LOAD_PATH.map { |p| p[gem_pattern] }.compact.uniq
         | 
| 65 | 
            +
                      precompile_yaml_files(gem_paths, exclude: gem_exclude)
         | 
| 66 | 
            +
                    end
         | 
| 67 | 
            +
             | 
| 68 | 
            +
                    if exitstatus = @work_pool.shutdown
         | 
| 69 | 
            +
                      exit(exitstatus)
         | 
| 70 | 
            +
                    end
         | 
| 71 | 
            +
                  end
         | 
| 72 | 
            +
                  0
         | 
| 73 | 
            +
                end
         | 
| 74 | 
            +
             | 
| 75 | 
            +
                dir_sort = begin
         | 
| 76 | 
            +
                  Dir[__FILE__, sort: false]
         | 
| 77 | 
            +
                  true
         | 
| 78 | 
            +
                rescue ArgumentError, TypeError
         | 
| 79 | 
            +
                  false
         | 
| 80 | 
            +
                end
         | 
| 81 | 
            +
             | 
| 82 | 
            +
                if dir_sort
         | 
| 83 | 
            +
                  def list_files(path, pattern)
         | 
| 84 | 
            +
                    if File.directory?(path)
         | 
| 85 | 
            +
                      Dir[File.join(path,  pattern), sort: false]
         | 
| 86 | 
            +
                    elsif File.exist?(path)
         | 
| 87 | 
            +
                      [path]
         | 
| 88 | 
            +
                    else
         | 
| 89 | 
            +
                      []
         | 
| 90 | 
            +
                    end
         | 
| 91 | 
            +
                  end
         | 
| 92 | 
            +
                else
         | 
| 93 | 
            +
                  def list_files(path, pattern)
         | 
| 94 | 
            +
                    if File.directory?(path)
         | 
| 95 | 
            +
                      Dir[File.join(path,  pattern)]
         | 
| 96 | 
            +
                    elsif File.exist?(path)
         | 
| 97 | 
            +
                      [path]
         | 
| 98 | 
            +
                    else
         | 
| 99 | 
            +
                      []
         | 
| 100 | 
            +
                    end
         | 
| 101 | 
            +
                  end
         | 
| 102 | 
            +
                end
         | 
| 103 | 
            +
             | 
| 104 | 
            +
                def run
         | 
| 105 | 
            +
                  parser.parse!(argv)
         | 
| 106 | 
            +
                  command = argv.shift
         | 
| 107 | 
            +
                  method = "#{command}_command"
         | 
| 108 | 
            +
                  if respond_to?(method)
         | 
| 109 | 
            +
                    public_send(method, *argv)
         | 
| 110 | 
            +
                  else
         | 
| 111 | 
            +
                    invalid_usage!("Unknown command: #{command}")
         | 
| 112 | 
            +
                  end
         | 
| 113 | 
            +
                end
         | 
| 114 | 
            +
             | 
| 115 | 
            +
                private
         | 
| 116 | 
            +
             | 
| 117 | 
            +
                def precompile_yaml_files(load_paths, exclude: self.exclude)
         | 
| 118 | 
            +
                  return unless yaml
         | 
| 119 | 
            +
             | 
| 120 | 
            +
                  load_paths.each do |path|
         | 
| 121 | 
            +
                    if !exclude || !exclude.match?(path)
         | 
| 122 | 
            +
                      list_files(path, '**/*.{yml,yaml}').each do |yaml_file|
         | 
| 123 | 
            +
                        # We ignore hidden files to not match the various .ci.yml files
         | 
| 124 | 
            +
                        if !yaml_file.include?('/.') && (!exclude || !exclude.match?(yaml_file))
         | 
| 125 | 
            +
                          @work_pool.push(:yaml, yaml_file)
         | 
| 126 | 
            +
                        end
         | 
| 127 | 
            +
                      end
         | 
| 128 | 
            +
                    end
         | 
| 129 | 
            +
                  end
         | 
| 130 | 
            +
                end
         | 
| 131 | 
            +
             | 
| 132 | 
            +
                def precompile_yaml(*yaml_files)
         | 
| 133 | 
            +
                  Array(yaml_files).each do |yaml_file|
         | 
| 134 | 
            +
                    if CompileCache::YAML.precompile(yaml_file, cache_dir: cache_dir)
         | 
| 135 | 
            +
                      STDERR.puts(yaml_file) if verbose
         | 
| 136 | 
            +
                    end
         | 
| 137 | 
            +
                  end
         | 
| 138 | 
            +
                end
         | 
| 139 | 
            +
             | 
| 140 | 
            +
                def precompile_ruby_files(load_paths, exclude: self.exclude)
         | 
| 141 | 
            +
                  return unless iseq
         | 
| 142 | 
            +
             | 
| 143 | 
            +
                  load_paths.each do |path|
         | 
| 144 | 
            +
                    if !exclude || !exclude.match?(path)
         | 
| 145 | 
            +
                      list_files(path, '**/*.rb').each do |ruby_file|
         | 
| 146 | 
            +
                        if !exclude || !exclude.match?(ruby_file)
         | 
| 147 | 
            +
                          @work_pool.push(:ruby, ruby_file)
         | 
| 148 | 
            +
                        end
         | 
| 149 | 
            +
                      end
         | 
| 150 | 
            +
                    end
         | 
| 151 | 
            +
                  end
         | 
| 152 | 
            +
                end
         | 
| 153 | 
            +
             | 
| 154 | 
            +
                def precompile_ruby(*ruby_files)
         | 
| 155 | 
            +
                  Array(ruby_files).each do |ruby_file|
         | 
| 156 | 
            +
                    if CompileCache::ISeq.precompile(ruby_file, cache_dir: cache_dir)
         | 
| 157 | 
            +
                      STDERR.puts(ruby_file) if verbose
         | 
| 158 | 
            +
                    end
         | 
| 159 | 
            +
                  end
         | 
| 160 | 
            +
                end
         | 
| 161 | 
            +
             | 
| 162 | 
            +
                def fix_default_encoding
         | 
| 163 | 
            +
                  if Encoding.default_external == Encoding::US_ASCII
         | 
| 164 | 
            +
                    Encoding.default_external = Encoding::UTF_8
         | 
| 165 | 
            +
                    begin
         | 
| 166 | 
            +
                      yield
         | 
| 167 | 
            +
                    ensure
         | 
| 168 | 
            +
                      Encoding.default_external = Encoding::US_ASCII
         | 
| 169 | 
            +
                    end
         | 
| 170 | 
            +
                  else
         | 
| 171 | 
            +
                    yield
         | 
| 172 | 
            +
                  end
         | 
| 173 | 
            +
                end
         | 
| 174 | 
            +
             | 
| 175 | 
            +
                def invalid_usage!(message)
         | 
| 176 | 
            +
                  STDERR.puts message
         | 
| 177 | 
            +
                  STDERR.puts
         | 
| 178 | 
            +
                  STDERR.puts parser
         | 
| 179 | 
            +
                  1
         | 
| 180 | 
            +
                end
         | 
| 181 | 
            +
             | 
| 182 | 
            +
                def cache_dir=(dir)
         | 
| 183 | 
            +
                  @cache_dir = File.expand_path(File.join(dir, 'bootsnap/compile-cache'))
         | 
| 184 | 
            +
                end
         | 
| 185 | 
            +
             | 
| 186 | 
            +
                def exclude_pattern(pattern)
         | 
| 187 | 
            +
                  (@exclude_patterns ||= []) << Regexp.new(pattern)
         | 
| 188 | 
            +
                  self.exclude = Regexp.union(@exclude_patterns)
         | 
| 189 | 
            +
                end
         | 
| 190 | 
            +
             | 
| 191 | 
            +
                def parser
         | 
| 192 | 
            +
                  @parser ||= OptionParser.new do |opts|
         | 
| 193 | 
            +
                    opts.banner = "Usage: bootsnap COMMAND [ARGS]"
         | 
| 194 | 
            +
                    opts.separator ""
         | 
| 195 | 
            +
                    opts.separator "GLOBAL OPTIONS"
         | 
| 196 | 
            +
                    opts.separator ""
         | 
| 197 | 
            +
             | 
| 198 | 
            +
                    help = <<~EOS
         | 
| 199 | 
            +
                      Path to the bootsnap cache directory. Defaults to tmp/cache
         | 
| 200 | 
            +
                    EOS
         | 
| 201 | 
            +
                    opts.on('--cache-dir DIR', help.strip) do |dir|
         | 
| 202 | 
            +
                      self.cache_dir = dir
         | 
| 203 | 
            +
                    end
         | 
| 204 | 
            +
             | 
| 205 | 
            +
                    help = <<~EOS
         | 
| 206 | 
            +
                      Print precompiled paths.
         | 
| 207 | 
            +
                    EOS
         | 
| 208 | 
            +
                    opts.on('--verbose', '-v', help.strip) do
         | 
| 209 | 
            +
                      self.verbose = true
         | 
| 210 | 
            +
                    end
         | 
| 211 | 
            +
             | 
| 212 | 
            +
                    help = <<~EOS
         | 
| 213 | 
            +
                      Number of workers to use. Default to number of processors, set to 0 to disable multi-processing.
         | 
| 214 | 
            +
                    EOS
         | 
| 215 | 
            +
                    opts.on('--jobs JOBS', '-j', help.strip) do |jobs|
         | 
| 216 | 
            +
                      self.jobs = Integer(jobs)
         | 
| 217 | 
            +
                    end
         | 
| 218 | 
            +
             | 
| 219 | 
            +
                    opts.separator ""
         | 
| 220 | 
            +
                    opts.separator "COMMANDS"
         | 
| 221 | 
            +
                    opts.separator ""
         | 
| 222 | 
            +
                    opts.separator "    precompile [DIRECTORIES...]: Precompile all .rb files in the passed directories"
         | 
| 223 | 
            +
             | 
| 224 | 
            +
                    help = <<~EOS
         | 
| 225 | 
            +
                      Precompile the gems in Gemfile
         | 
| 226 | 
            +
                    EOS
         | 
| 227 | 
            +
                    opts.on('--gemfile', help) { self.compile_gemfile = true }
         | 
| 228 | 
            +
             | 
| 229 | 
            +
                    help = <<~EOS
         | 
| 230 | 
            +
                      Path pattern to not precompile. e.g. --exclude 'aws-sdk|google-api'
         | 
| 231 | 
            +
                    EOS
         | 
| 232 | 
            +
                    opts.on('--exclude PATTERN', help) { |pattern| exclude_pattern(pattern) }
         | 
| 233 | 
            +
             | 
| 234 | 
            +
                    help = <<~EOS
         | 
| 235 | 
            +
                      Disable ISeq (.rb) precompilation.
         | 
| 236 | 
            +
                    EOS
         | 
| 237 | 
            +
                    opts.on('--no-iseq', help) { self.iseq = false }
         | 
| 238 | 
            +
             | 
| 239 | 
            +
                    help = <<~EOS
         | 
| 240 | 
            +
                      Disable YAML precompilation.
         | 
| 241 | 
            +
                    EOS
         | 
| 242 | 
            +
                    opts.on('--no-yaml', help) { self.yaml = false }
         | 
| 243 | 
            +
                  end
         | 
| 244 | 
            +
                end
         | 
| 245 | 
            +
              end
         | 
| 246 | 
            +
            end
         | 
| @@ -1,3 +1,4 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 1 2 | 
             
            require('bootsnap/bootsnap')
         | 
| 2 3 | 
             
            require('zlib')
         | 
| 3 4 |  | 
| @@ -14,7 +15,7 @@ module Bootsnap | |
| 14 15 | 
             
                    raise(Uncompilable, 'syntax error')
         | 
| 15 16 | 
             
                  end
         | 
| 16 17 |  | 
| 17 | 
            -
                  def self.storage_to_output(binary)
         | 
| 18 | 
            +
                  def self.storage_to_output(binary, _args)
         | 
| 18 19 | 
             
                    RubyVM::InstructionSequence.load_from_binary(binary)
         | 
| 19 20 | 
             
                  rescue RuntimeError => e
         | 
| 20 21 | 
             
                    if e.message == 'broken binary format'
         | 
| @@ -25,7 +26,24 @@ module Bootsnap | |
| 25 26 | 
             
                    end
         | 
| 26 27 | 
             
                  end
         | 
| 27 28 |  | 
| 28 | 
            -
                  def self. | 
| 29 | 
            +
                  def self.fetch(path, cache_dir: ISeq.cache_dir)
         | 
| 30 | 
            +
                    Bootsnap::CompileCache::Native.fetch(
         | 
| 31 | 
            +
                      cache_dir,
         | 
| 32 | 
            +
                      path.to_s,
         | 
| 33 | 
            +
                      Bootsnap::CompileCache::ISeq,
         | 
| 34 | 
            +
                      nil,
         | 
| 35 | 
            +
                    )
         | 
| 36 | 
            +
                  end
         | 
| 37 | 
            +
             | 
| 38 | 
            +
                  def self.precompile(path, cache_dir: ISeq.cache_dir)
         | 
| 39 | 
            +
                    Bootsnap::CompileCache::Native.precompile(
         | 
| 40 | 
            +
                      cache_dir,
         | 
| 41 | 
            +
                      path.to_s,
         | 
| 42 | 
            +
                      Bootsnap::CompileCache::ISeq,
         | 
| 43 | 
            +
                    )
         | 
| 44 | 
            +
                  end
         | 
| 45 | 
            +
             | 
| 46 | 
            +
                  def self.input_to_output(_data, _kwargs)
         | 
| 29 47 | 
             
                    nil # ruby handles this
         | 
| 30 48 | 
             
                  end
         | 
| 31 49 |  | 
| @@ -34,11 +52,7 @@ module Bootsnap | |
| 34 52 | 
             
                      # Having coverage enabled prevents iseq dumping/loading.
         | 
| 35 53 | 
             
                      return nil if defined?(Coverage) && Bootsnap::CompileCache::Native.coverage_running?
         | 
| 36 54 |  | 
| 37 | 
            -
                      Bootsnap::CompileCache:: | 
| 38 | 
            -
                        Bootsnap::CompileCache::ISeq.cache_dir,
         | 
| 39 | 
            -
                        path.to_s,
         | 
| 40 | 
            -
                        Bootsnap::CompileCache::ISeq
         | 
| 41 | 
            -
                      )
         | 
| 55 | 
            +
                      Bootsnap::CompileCache::ISeq.fetch(path.to_s)
         | 
| 42 56 | 
             
                    rescue Errno::EACCES
         | 
| 43 57 | 
             
                      Bootsnap::CompileCache.permission_error(path)
         | 
| 44 58 | 
             
                    rescue RuntimeError => e
         | 
| @@ -59,6 +73,7 @@ module Bootsnap | |
| 59 73 | 
             
                    crc = Zlib.crc32(option.inspect)
         | 
| 60 74 | 
             
                    Bootsnap::CompileCache::Native.compile_option_crc32 = crc
         | 
| 61 75 | 
             
                  end
         | 
| 76 | 
            +
                  compile_option_updated
         | 
| 62 77 |  | 
| 63 78 | 
             
                  def self.install!(cache_dir)
         | 
| 64 79 | 
             
                    Bootsnap::CompileCache::ISeq.cache_dir = cache_dir
         | 
| @@ -1,61 +1,111 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 1 2 | 
             
            require('bootsnap/bootsnap')
         | 
| 2 3 |  | 
| 3 4 | 
             
            module Bootsnap
         | 
| 4 5 | 
             
              module CompileCache
         | 
| 5 6 | 
             
                module YAML
         | 
| 6 7 | 
             
                  class << self
         | 
| 7 | 
            -
                    attr_accessor(:msgpack_factory)
         | 
| 8 | 
            -
                  end
         | 
| 8 | 
            +
                    attr_accessor(:msgpack_factory, :cache_dir, :supported_options)
         | 
| 9 9 |  | 
| 10 | 
            -
             | 
| 11 | 
            -
             | 
| 12 | 
            -
             | 
| 13 | 
            -
             | 
| 14 | 
            -
             | 
| 15 | 
            -
             | 
| 16 | 
            -
             | 
| 17 | 
            -
                     | 
| 18 | 
            -
                    Marshal.dump(obj)
         | 
| 19 | 
            -
                  end
         | 
| 10 | 
            +
                    def input_to_storage(contents, _)
         | 
| 11 | 
            +
                      raise(Uncompilable) if contents.index("!ruby/object")
         | 
| 12 | 
            +
                      obj = ::YAML.load(contents)
         | 
| 13 | 
            +
                      msgpack_factory.dump(obj)
         | 
| 14 | 
            +
                    rescue NoMethodError, RangeError
         | 
| 15 | 
            +
                      # The object included things that we can't serialize
         | 
| 16 | 
            +
                      raise(Uncompilable)
         | 
| 17 | 
            +
                    end
         | 
| 20 18 |  | 
| 21 | 
            -
             | 
| 22 | 
            -
             | 
| 23 | 
            -
             | 
| 24 | 
            -
             | 
| 25 | 
            -
             | 
| 26 | 
            -
                      Marshal.load(data)
         | 
| 27 | 
            -
                    else
         | 
| 28 | 
            -
                      msgpack_factory.unpacker.feed(data).read
         | 
| 19 | 
            +
                    def storage_to_output(data, kwargs)
         | 
| 20 | 
            +
                      if kwargs && kwargs.key?(:symbolize_names)
         | 
| 21 | 
            +
                        kwargs[:symbolize_keys] = kwargs.delete(:symbolize_names)
         | 
| 22 | 
            +
                      end
         | 
| 23 | 
            +
                      msgpack_factory.load(data, kwargs)
         | 
| 29 24 | 
             
                    end
         | 
| 30 | 
            -
                  end
         | 
| 31 25 |  | 
| 32 | 
            -
             | 
| 33 | 
            -
             | 
| 34 | 
            -
             | 
| 26 | 
            +
                    def input_to_output(data, kwargs)
         | 
| 27 | 
            +
                      ::YAML.load(data, **(kwargs || {}))
         | 
| 28 | 
            +
                    end
         | 
| 29 | 
            +
             | 
| 30 | 
            +
                    def precompile(path, cache_dir: YAML.cache_dir)
         | 
| 31 | 
            +
                      Bootsnap::CompileCache::Native.precompile(
         | 
| 32 | 
            +
                        cache_dir,
         | 
| 33 | 
            +
                        path.to_s,
         | 
| 34 | 
            +
                        Bootsnap::CompileCache::YAML,
         | 
| 35 | 
            +
                      )
         | 
| 36 | 
            +
                    end
         | 
| 37 | 
            +
             | 
| 38 | 
            +
                    def install!(cache_dir)
         | 
| 39 | 
            +
                      self.cache_dir = cache_dir
         | 
| 40 | 
            +
                      init!
         | 
| 41 | 
            +
                      ::YAML.singleton_class.prepend(Patch)
         | 
| 42 | 
            +
                    end
         | 
| 35 43 |  | 
| 36 | 
            -
             | 
| 37 | 
            -
             | 
| 38 | 
            -
             | 
| 44 | 
            +
                    def init!
         | 
| 45 | 
            +
                      require('yaml')
         | 
| 46 | 
            +
                      require('msgpack')
         | 
| 47 | 
            +
                      require('date')
         | 
| 39 48 |  | 
| 40 | 
            -
             | 
| 41 | 
            -
             | 
| 42 | 
            -
             | 
| 43 | 
            -
             | 
| 44 | 
            -
             | 
| 45 | 
            -
             | 
| 49 | 
            +
                      # MessagePack serializes symbols as strings by default.
         | 
| 50 | 
            +
                      # We want them to roundtrip cleanly, so we use a custom factory.
         | 
| 51 | 
            +
                      # see: https://github.com/msgpack/msgpack-ruby/pull/122
         | 
| 52 | 
            +
                      factory = MessagePack::Factory.new
         | 
| 53 | 
            +
                      factory.register_type(0x00, Symbol)
         | 
| 54 | 
            +
                      factory.register_type(
         | 
| 55 | 
            +
                        MessagePack::Timestamp::TYPE, # or just -1
         | 
| 56 | 
            +
                        Time,
         | 
| 57 | 
            +
                        packer: MessagePack::Time::Packer,
         | 
| 58 | 
            +
                        unpacker: MessagePack::Time::Unpacker
         | 
| 59 | 
            +
                      )
         | 
| 60 | 
            +
             | 
| 61 | 
            +
                      marshal_fallback = {
         | 
| 62 | 
            +
                        packer: ->(value) { Marshal.dump(value) },
         | 
| 63 | 
            +
                        unpacker: ->(payload) { Marshal.load(payload) },
         | 
| 64 | 
            +
                      }
         | 
| 65 | 
            +
                      {
         | 
| 66 | 
            +
                        Date => 0x01,
         | 
| 67 | 
            +
                        Regexp => 0x02,
         | 
| 68 | 
            +
                      }.each do |type, code|
         | 
| 69 | 
            +
                        factory.register_type(code, type, marshal_fallback)
         | 
| 70 | 
            +
                      end
         | 
| 71 | 
            +
             | 
| 72 | 
            +
                      self.msgpack_factory = factory
         | 
| 73 | 
            +
             | 
| 74 | 
            +
                      self.supported_options = []
         | 
| 75 | 
            +
                      params = ::YAML.method(:load).parameters
         | 
| 76 | 
            +
                      if params.include?([:key, :symbolize_names])
         | 
| 77 | 
            +
                        self.supported_options << :symbolize_names
         | 
| 78 | 
            +
                      end
         | 
| 79 | 
            +
                      if params.include?([:key, :freeze])
         | 
| 80 | 
            +
                        if factory.load(factory.dump('yaml'), freeze: true).frozen?
         | 
| 81 | 
            +
                          self.supported_options << :freeze
         | 
| 82 | 
            +
                        end
         | 
| 83 | 
            +
                      end
         | 
| 84 | 
            +
                      self.supported_options.freeze
         | 
| 85 | 
            +
                    end
         | 
| 86 | 
            +
                  end
         | 
| 87 | 
            +
             | 
| 88 | 
            +
                  module Patch
         | 
| 89 | 
            +
                    def load_file(path, *args)
         | 
| 90 | 
            +
                      return super if args.size > 1
         | 
| 91 | 
            +
                      if kwargs = args.first
         | 
| 92 | 
            +
                        return super unless kwargs.is_a?(Hash)
         | 
| 93 | 
            +
                        return super unless (kwargs.keys - ::Bootsnap::CompileCache::YAML.supported_options).empty?
         | 
| 94 | 
            +
                      end
         | 
| 46 95 |  | 
| 47 | 
            -
                    klass = class << ::YAML; self; end
         | 
| 48 | 
            -
                    klass.send(:define_method, :load_file) do |path|
         | 
| 49 96 | 
             
                      begin
         | 
| 50 | 
            -
                        Bootsnap::CompileCache::Native.fetch(
         | 
| 51 | 
            -
                          cache_dir,
         | 
| 97 | 
            +
                        ::Bootsnap::CompileCache::Native.fetch(
         | 
| 98 | 
            +
                          Bootsnap::CompileCache::YAML.cache_dir,
         | 
| 52 99 | 
             
                          path,
         | 
| 53 | 
            -
                          Bootsnap::CompileCache::YAML
         | 
| 100 | 
            +
                          ::Bootsnap::CompileCache::YAML,
         | 
| 101 | 
            +
                          kwargs,
         | 
| 54 102 | 
             
                        )
         | 
| 55 103 | 
             
                      rescue Errno::EACCES
         | 
| 56 | 
            -
                        Bootsnap::CompileCache.permission_error(path)
         | 
| 104 | 
            +
                        ::Bootsnap::CompileCache.permission_error(path)
         | 
| 57 105 | 
             
                      end
         | 
| 58 106 | 
             
                    end
         | 
| 107 | 
            +
             | 
| 108 | 
            +
                    ruby2_keywords :load_file if respond_to?(:ruby2_keywords, true)
         | 
| 59 109 | 
             
                  end
         | 
| 60 110 | 
             
                end
         | 
| 61 111 | 
             
              end
         | 
| @@ -1,3 +1,4 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 1 2 | 
             
            module Bootsnap
         | 
| 2 3 | 
             
              module CompileCache
         | 
| 3 4 | 
             
                Error           = Class.new(StandardError)
         | 
| @@ -33,9 +34,9 @@ module Bootsnap | |
| 33 34 | 
             
                end
         | 
| 34 35 |  | 
| 35 36 | 
             
                def self.supported?
         | 
| 36 | 
            -
                  # only enable on 'ruby' (MRI), POSIX (darwin, linux, *bsd), and >= 2.3.0
         | 
| 37 | 
            +
                  # only enable on 'ruby' (MRI), POSIX (darwin, linux, *bsd), Windows (RubyInstaller2) and >= 2.3.0
         | 
| 37 38 | 
             
                  RUBY_ENGINE == 'ruby' &&
         | 
| 38 | 
            -
                  RUBY_PLATFORM =~ /darwin|linux|bsd/ &&
         | 
| 39 | 
            +
                  RUBY_PLATFORM =~ /darwin|linux|bsd|mswin|mingw|cygwin/ &&
         | 
| 39 40 | 
             
                  Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("2.3.0")
         | 
| 40 41 | 
             
                end
         | 
| 41 42 | 
             
              end
         | 
| @@ -46,7 +46,7 @@ module Bootsnap | |
| 46 46 | 
             
                  # loadpath.
         | 
| 47 47 | 
             
                  def find(feature)
         | 
| 48 48 | 
             
                    reinitialize if (@has_relative_paths && dir_changed?) || stale?
         | 
| 49 | 
            -
                    feature = feature.to_s
         | 
| 49 | 
            +
                    feature = feature.to_s.freeze
         | 
| 50 50 | 
             
                    return feature if absolute_path?(feature)
         | 
| 51 51 | 
             
                    return expand_path(feature) if feature.start_with?('./')
         | 
| 52 52 | 
             
                    @mutex.synchronize do
         | 
| @@ -67,7 +67,7 @@ module Bootsnap | |
| 67 67 | 
             
                      # native dynamic extension, e.g. .bundle or .so), we know it was a
         | 
| 68 68 | 
             
                      # failure and there's nothing more we can do to find the file.
         | 
| 69 69 | 
             
                      # no extension, .rb, (.bundle or .so)
         | 
| 70 | 
            -
                      when '', *CACHED_EXTENSIONS | 
| 70 | 
            +
                      when '', *CACHED_EXTENSIONS
         | 
| 71 71 | 
             
                        nil
         | 
| 72 72 | 
             
                      # Ruby allows specifying native extensions as '.so' even when DLEXT
         | 
| 73 73 | 
             
                      # is '.bundle'. This is where we handle that case.
         | 
| @@ -144,7 +144,7 @@ module Bootsnap | |
| 144 144 | 
             
                        expanded_path = p.expanded_path
         | 
| 145 145 | 
             
                        entries, dirs = p.entries_and_dirs(@store)
         | 
| 146 146 | 
             
                        # push -> low precedence -> set only if unset
         | 
| 147 | 
            -
                        dirs.each    { |dir| @dirs[dir] | 
| 147 | 
            +
                        dirs.each    { |dir| @dirs[dir] ||= path }
         | 
| 148 148 | 
             
                        entries.each { |rel| @index[rel] ||= expanded_path }
         | 
| 149 149 | 
             
                      end
         | 
| 150 150 | 
             
                    end
         | 
| @@ -178,25 +178,25 @@ module Bootsnap | |
| 178 178 |  | 
| 179 179 | 
             
                  if DLEXT2
         | 
| 180 180 | 
             
                    def search_index(f)
         | 
| 181 | 
            -
                      try_index(f | 
| 181 | 
            +
                      try_index("#{f}#{DOT_RB}") || try_index("#{f}#{DLEXT}") || try_index("#{f}#{DLEXT2}") || try_index(f)
         | 
| 182 182 | 
             
                    end
         | 
| 183 183 |  | 
| 184 184 | 
             
                    def maybe_append_extension(f)
         | 
| 185 | 
            -
                      try_ext(f | 
| 185 | 
            +
                      try_ext("#{f}#{DOT_RB}") || try_ext("#{f}#{DLEXT}") || try_ext("#{f}#{DLEXT2}") || f
         | 
| 186 186 | 
             
                    end
         | 
| 187 187 | 
             
                  else
         | 
| 188 188 | 
             
                    def search_index(f)
         | 
| 189 | 
            -
                      try_index(f | 
| 189 | 
            +
                      try_index("#{f}#{DOT_RB}") || try_index("#{f}#{DLEXT}") || try_index(f)
         | 
| 190 190 | 
             
                    end
         | 
| 191 191 |  | 
| 192 192 | 
             
                    def maybe_append_extension(f)
         | 
| 193 | 
            -
                      try_ext(f | 
| 193 | 
            +
                      try_ext("#{f}#{DOT_RB}") || try_ext("#{f}#{DLEXT}") || f
         | 
| 194 194 | 
             
                    end
         | 
| 195 195 | 
             
                  end
         | 
| 196 196 |  | 
| 197 197 | 
             
                  def try_index(f)
         | 
| 198 198 | 
             
                    if (p = @index[f])
         | 
| 199 | 
            -
                      p | 
| 199 | 
            +
                      "#{p}/#{f}"
         | 
| 200 200 | 
             
                    end
         | 
| 201 201 | 
             
                  end
         | 
| 202 202 |  | 
| @@ -1,3 +1,4 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 1 2 | 
             
            module Bootsnap
         | 
| 2 3 | 
             
              module LoadPathCache
         | 
| 3 4 | 
             
                module ChangeObserver
         | 
| @@ -25,7 +26,7 @@ module Bootsnap | |
| 25 26 | 
             
                      super
         | 
| 26 27 | 
             
                    end
         | 
| 27 28 |  | 
| 28 | 
            -
                    # uniq! keeps the first  | 
| 29 | 
            +
                    # uniq! keeps the first occurrence of each path, otherwise preserving
         | 
| 29 30 | 
             
                    # order, preserving the effective load path
         | 
| 30 31 | 
             
                    def uniq!(*args)
         | 
| 31 32 | 
             
                      ret = super
         |