Files
brew/Library/Homebrew/debrew.rb
T
Mike McQuaid 0c2baca69e ignorable: replace callcc with Fiber
- Ruby 4.0's `continuation` warns `callcc is obsolete; use Fiber
  instead` whenever it is required, which happens on every `brew`
  command that loads a formula from source.
- Run `Ignorable.hook_raise` blocks in a `Fiber`: `raise` now pauses
  at the raise site and asks an `on_ignorable` callback whether to
  resume (`:ignore`) or raise there as usual, replacing the rescue
  plus continuation jump and `Ignorable::ExceptionMixin#ignore`.
- `Debrew` menus and `Formulary` `ignore_errors` decisions now happen
  before the stack unwinds, so `ensure` blocks only run when an
  exception is actually raised.
- Only require `ignorable` when `Formulary` uses `ignore_errors` and
  drop the obsolete `brew verify-undefined` `Warnings` guard.

Fixes https://github.com/Homebrew/brew/issues/23384
2026-08-01 14:28:57 +01:00

150 lines
3.8 KiB
Ruby

# typed: strict
# frozen_string_literal: true
require "ignorable"
# Helper module for debugging formulae.
module Debrew
# Module for allowing to debug formulae.
module Formula
sig { void }
def install
Debrew.debrew { super }
end
sig { void }
def patch
Debrew.debrew { super }
end
sig {
# TODO: replace `returns(BasicObject)` with `void` after dropping `return false` handling in test
returns(BasicObject)
}
def test
Debrew.debrew { super }
end
end
# Module for displaying a debugging menu.
class Menu
class Entry < T::Struct
const :name, String
const :action, T.proc.void
end
sig { returns(T.nilable(String)) }
attr_accessor :prompt
sig { returns(T::Array[Entry]) }
attr_accessor :entries
sig { void }
def initialize
@entries = T.let([], T::Array[Entry])
end
sig { params(name: Symbol, action: T.proc.void).void }
def choice(name, &action)
entries << Entry.new(name: name.to_s, action:)
end
sig { params(_block: T.proc.params(menu: Menu).void).void }
def self.choose(&_block)
menu = new
yield menu
choice = T.let(nil, T.nilable(Entry))
while choice.nil?
menu.entries.each_with_index { |e, i| puts "#{i + 1}. #{e.name}" }
print menu.prompt unless menu.prompt.nil?
input = $stdin.gets || exit
input.chomp!
i = input.to_i
if i.positive?
choice = menu.entries[i - 1]
else
possible = menu.entries.select { |e| e.name.start_with?(input) }
case possible.size
when 0 then puts "No such option"
when 1 then choice = possible.first
else puts "Multiple options match: #{possible.map(&:name).join(" ")}"
end
end
end
choice.action.call
end
end
@mutex = T.let(nil, T.nilable(Mutex))
@debugged_exceptions = T.let(Set.new, T::Set[Exception])
class << self
sig { returns(T::Set[Exception]) }
attr_reader :debugged_exceptions
sig { returns(T::Boolean) }
def active? = !@mutex.nil?
end
sig {
type_parameters(:U)
.params(block: T.proc.returns(T.type_parameter(:U)))
.returns(T.type_parameter(:U))
}
def self.debrew(&block)
@mutex = Mutex.new
Ignorable.hook_raise(on_ignorable: ->(e) { e.is_a?(SystemExit) ? :raise : debug(e) }, &block)
ensure
@mutex = nil
end
sig { params(exception: Exception).returns(Symbol) }
def self.debug(exception)
raise(exception) if !active? || !debugged_exceptions.add?(exception) || !@mutex&.try_lock
begin
puts exception.backtrace&.first
puts Formatter.error(exception, label: exception.class.name)
loop do
Menu.choose do |menu|
menu.prompt = "Choose an action: "
menu.choice(:raise) { raise(exception) }
menu.choice(:ignore) { return :ignore } if exception.is_a?(Ignorable::ExceptionMixin)
menu.choice(:backtrace) { puts exception.backtrace }
if exception.is_a?(Ignorable::ExceptionMixin)
menu.choice(:irb) do
puts "When you exit this IRB session, execution will continue."
set_trace_func proc { |event, _, _, id, binding, klass|
if klass == Object && id == :raise && event == "return"
set_trace_func(nil)
@mutex.synchronize do
require "debrew/irb"
IRB.start_within(binding)
end
end
}
return :ignore
end
end
menu.choice(:shell) do
puts "When you exit this shell, you will return to the menu."
interactive_shell
end
end
end
ensure
@mutex.unlock
end
end
end