lib/bootsnap.rb



# frozen_string_literal: true

require_relative('bootsnap/version')
require_relative('bootsnap/bundler')
require_relative('bootsnap/load_path_cache')
require_relative('bootsnap/compile_cache')

module Bootsnap
  InvalidConfiguration = Class.new(StandardError)

  class << self
    attr_reader :logger
  end

  def self.log!
    self.logger = $stderr.method(:puts)
  end

  def self.logger=(logger)
    @logger = logger
    if logger.respond_to?(:debug)
      self.instrumentation = ->(event, path) { @logger.debug("[Bootsnap] #{event} #{path}") }
    else
      self.instrumentation = ->(event, path) { @logger.call("[Bootsnap] #{event} #{path}") }
    end
  end

  def self.instrumentation=(callback)
    @instrumentation = callback
    if respond_to?(:instrumentation_enabled=, true)
      self.instrumentation_enabled = !!callback
    end
  end

  def self._instrument(event, path)
    @instrumentation.call(event, path)
  end

  def self.setup(
    cache_dir:,
    development_mode: true,
    load_path_cache: true,
    autoload_paths_cache: nil,
    disable_trace: nil,
    compile_cache_iseq: true,
    compile_cache_yaml: true,
    compile_cache_json: true
  )
    unless autoload_paths_cache.nil?
      warn "[DEPRECATED] Bootsnap's `autoload_paths_cache:` option is deprecated and will be removed. " \
        "If you use Zeitwerk this option is useless, and if you are still using the classic autoloader " \
        "upgrading is recommended."
    end

    unless disable_trace.nil?
      warn "[DEPRECATED] Bootsnap's `disable_trace:` option is deprecated and will be removed. " \
        "If you use Ruby 2.5 or newer this option is useless, if not upgrading is recommended."
    end

    if compile_cache_iseq && !iseq_cache_supported?
      warn "Ruby 2.5 has a bug that break code tracing when code is loaded from cache. It is recommened " \
        "to turn `compile_cache_iseq` off on Ruby 2.5"
    end

    Bootsnap::LoadPathCache.setup(
      cache_path:       cache_dir + '/bootsnap/load-path-cache',
      development_mode: development_mode,
    ) if load_path_cache

    Bootsnap::CompileCache.setup(
      cache_dir: cache_dir + '/bootsnap/compile-cache',
      iseq: compile_cache_iseq,
      yaml: compile_cache_yaml,
      json: compile_cache_json,
    )
  end

  def self.iseq_cache_supported?
    return @iseq_cache_supported if defined? @iseq_cache_supported

    ruby_version = Gem::Version.new(RUBY_VERSION)
    @iseq_cache_supported = ruby_version < Gem::Version.new('2.5.0') || ruby_version >= Gem::Version.new('2.6.0')
  end

  def self.default_setup
    env = ENV['RAILS_ENV'] || ENV['RACK_ENV'] || ENV['ENV']
    development_mode = ['', nil, 'development'].include?(env)

    unless ENV['DISABLE_BOOTSNAP']
      cache_dir = ENV['BOOTSNAP_CACHE_DIR']
      unless cache_dir
        config_dir_frame = caller.detect do |line|
          line.include?('/config/')
        end

        unless config_dir_frame
          $stderr.puts("[bootsnap/setup] couldn't infer cache directory! Either:")
          $stderr.puts("[bootsnap/setup]   1. require bootsnap/setup from your application's config directory; or")
          $stderr.puts("[bootsnap/setup]   2. Define the environment variable BOOTSNAP_CACHE_DIR")

          raise("couldn't infer bootsnap cache directory")
        end

        path = config_dir_frame.split(/:\d+:/).first
        path = File.dirname(path) until File.basename(path) == 'config'
        app_root = File.dirname(path)

        cache_dir = File.join(app_root, 'tmp', 'cache')
      end


      setup(
        cache_dir:            cache_dir,
        development_mode:     development_mode,
        load_path_cache:      !ENV['DISABLE_BOOTSNAP_LOAD_PATH_CACHE'],
        compile_cache_iseq:   !ENV['DISABLE_BOOTSNAP_COMPILE_CACHE'] && iseq_cache_supported?,
        compile_cache_yaml:   !ENV['DISABLE_BOOTSNAP_COMPILE_CACHE'],
        compile_cache_json:   !ENV['DISABLE_BOOTSNAP_COMPILE_CACHE'],
      )

      if ENV['BOOTSNAP_LOG']
        log!
      end
    end
  end
end