lib/bootsnap/compile_cache/iseq.rb



# frozen_string_literal: true
require('bootsnap/bootsnap')
require('zlib')

module Bootsnap
  module CompileCache
    module ISeq
      class << self
        attr_accessor(:cache_dir)
      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
            raise(Uncompilable, 'syntax error')
          end

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

      def self.storage_to_output(binary, _args)
        RubyVM::InstructionSequence.load_from_binary(binary)
      rescue RuntimeError => e
        if e.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, cache_dir: ISeq.cache_dir)
        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 => e
          if e.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