lib/bootsnap/compile_cache/iseq.rb



# frozen_string_literal: true

require("bootsnap/bootsnap")
require("zlib")

module Bootsnap
  module CompileCache
    module ISeq
      class << self
        attr_reader(:cache_dir)

        def cache_dir=(cache_dir)
          @cache_dir = cache_dir.end_with?("/") ? "#{cache_dir}iseq" : "#{cache_dir}-iseq"
        end
      end

      has_ruby_bug_18250 = begin # https://bugs.ruby-lang.org/issues/18250
        if defined? RubyVM::InstructionSequence
          RubyVM::InstructionSequence.compile("def foo(*); ->{ super }; end; def foo(**); ->{ super }; end").to_binary
        end
        false
      rescue TypeError
        true
      end

      if has_ruby_bug_18250
        def self.input_to_storage(_, path)
          iseq = begin
            RubyVM::InstructionSequence.compile_file(path)
          rescue SyntaxError
            return UNCOMPILABLE # syntax error
          end

          begin
            iseq.to_binary
          rescue TypeError
            return UNCOMPILABLE # ruby bug #18250
          end
        end
      else
        def self.input_to_storage(_, path)
          RubyVM::InstructionSequence.compile_file(path).to_binary
        rescue SyntaxError
          return UNCOMPILABLE # syntax error
        end
      end

      def self.storage_to_output(binary, _args)
        RubyVM::InstructionSequence.load_from_binary(binary)
      rescue RuntimeError => error
        if error.message == "broken binary format"
          STDERR.puts("[Bootsnap::CompileCache] warning: rejecting broken binary")
          nil
        else
          raise
        end
      end

      def self.fetch(path, cache_dir: ISeq.cache_dir)
        Bootsnap::CompileCache::Native.fetch(
          cache_dir,
          path.to_s,
          Bootsnap::CompileCache::ISeq,
          nil,
        )
      end

      def self.precompile(path)
        Bootsnap::CompileCache::Native.precompile(
          cache_dir,
          path.to_s,
          Bootsnap::CompileCache::ISeq,
        )
      end

      def self.input_to_output(_data, _kwargs)
        nil # ruby handles this
      end

      module InstructionSequenceMixin
        def load_iseq(path)
          # Having coverage enabled prevents iseq dumping/loading.
          return nil if defined?(Coverage) && Bootsnap::CompileCache::Native.coverage_running?

          Bootsnap::CompileCache::ISeq.fetch(path.to_s)
        rescue Errno::EACCES
          Bootsnap::CompileCache.permission_error(path)
        rescue RuntimeError => error
          if error.message =~ /unmatched platform/
            puts("unmatched platform for file #{path}")
          end
          raise
        end

        def compile_option=(hash)
          super(hash)
          Bootsnap::CompileCache::ISeq.compile_option_updated
        end
      end

      def self.compile_option_updated
        option = RubyVM::InstructionSequence.compile_option
        crc = Zlib.crc32(option.inspect)
        Bootsnap::CompileCache::Native.compile_option_crc32 = crc
      end
      compile_option_updated

      def self.install!(cache_dir)
        Bootsnap::CompileCache::ISeq.cache_dir = cache_dir
        Bootsnap::CompileCache::ISeq.compile_option_updated
        class << RubyVM::InstructionSequence
          prepend(InstructionSequenceMixin)
        end
      end
    end
  end
end