debrew: formula debugging for homebrew

A new feature for easing the pain of working with complex formulas, or
formulas for large packages. When running brew in debug mode (-d), if an
exception propagates outside the formula's install method, you now get a menu
which lets you return to the point where the exception was raised and perfom
several useful actions, such as:
- printing a backtrace
- entering IRB to examine the context and test ruby code
- entering the debugger (if ruby-debug is available)
- entering a shell
- ignoring the exception or proceeding with the raise as normal

Signed-off-by: Max Howell <mxcl@me.com>

* Fixed conflict in build.rb.
* Removed old debug handling in Formula.brew.

Closes Homebrew/homebrew#10435.
This commit is contained in:
Camillo Lugaresi 2012-02-21 01:14:02 -06:00 committed by Max Howell
parent f6091b1c85
commit 18dbe47f9f
3 changed files with 178 additions and 15 deletions

View File

@ -13,6 +13,7 @@ at_exit do
end
require 'global'
require 'debrew' if ARGV.debug?
def main
# The main Homebrew process expects to eventually see EOF on the error
@ -42,6 +43,7 @@ def main
install(Formula.factory($0))
rescue Exception => e
unless error_pipe.nil?
e.continuation = nil if ARGV.debug?
Marshal.dump(e, error_pipe)
error_pipe.close
exit! 1
@ -130,7 +132,16 @@ def install f
interactive_shell f
else
f.prefix.mkpath
f.install
begin
f.install
rescue Exception => e
if ARGV.debug?
debrew e, f
else
raise e
end
end
# Find and link metafiles
FORMULA_META_FILES.each do |filename|

163
Library/Homebrew/debrew.rb Normal file
View File

@ -0,0 +1,163 @@
require 'irb'
begin
require 'continuation' # needed on 1.9
rescue LoadError
end
class Menu
attr_accessor :prompt
attr_accessor :entries
def initialize
@entries = []
end
def choice(name, &action)
entries << { :name => name, :action => action }
end
end
def choose
menu = Menu.new
yield menu
choice = nil
while choice.nil?
menu.entries.each_with_index do |entry, i|
puts "#{i+1}. #{entry[:name]}"
end
puts menu.prompt unless menu.prompt.nil?
reply = $stdin.gets.chomp
i = reply.to_i
if i > 0
choice = menu.entries[i-1]
else
possible = menu.entries.find_all {|e| e[:name].to_s.start_with? reply }
case possible.size
when 0 then puts "No such option"
when 1 then choice = possible.first
else puts "Multiple options match: #{possible.map{|e| e[:name]}.join(' ')}"
end
end
end
choice[:action].call
end
module IRB
@setup_done = false
def IRB.start_within(binding)
unless @setup_done
# make IRB ignore our command line arguments
saved_args = ARGV.shift(ARGV.size)
IRB.setup(nil)
ARGV.concat(saved_args)
@setup_done = true
end
workspace = WorkSpace.new(binding)
irb = Irb.new(workspace)
@CONF[:IRB_RC].call(irb.context) if @CONF[:IRB_RC]
@CONF[:MAIN_CONTEXT] = irb.context
trap("SIGINT") do
irb.signal_handle
end
begin
catch(:IRB_EXIT) do
irb.eval_input
end
ensure
irb_at_exit
end
end
end
class Exception
attr_accessor :continuation
def restart(&block)
continuation.call block
end
end
def has_debugger?
begin
require 'rubygems'
require 'ruby-debug'
true
rescue LoadError
false
end
end
def debrew(exception, formula=nil)
puts "#{exception.backtrace.first}"
puts "#{Tty.red}#{exception.class.to_s}#{Tty.reset}: #{exception.to_s}"
begin
again = false
choose do |menu|
menu.prompt = "Choose an action:"
menu.choice(:raise) { original_raise exception }
menu.choice(:ignore) { exception.restart }
menu.choice(:backtrace) { puts exception.backtrace; again = true }
menu.choice(:debug) do
puts "When you exit the debugger, execution will continue."
exception.restart { debugger }
end if has_debugger?
menu.choice(:irb) do
puts "When you exit this IRB session, execution will continue."
exception.restart do
# we need to capture the binding after returning from raise
set_trace_func proc { |event, file, line, id, binding, classname|
if event == 'return'
set_trace_func nil
IRB.start_within(binding)
end
}
end
end
menu.choice(:shell) do
puts "When you exit this shell, you will return to the menu."
interactive_shell formula
again=true
end
end
end while again
end
module RaisePlus
alias :original_raise :raise
def raise(*args)
exception = case
when args.size == 0 then ($!.nil? ? RuntimeError.exception : $!)
when (args.size == 1 and args[0].is_a?(String)) then RuntimeError.exception(args[0])
else args[0].exception(args[1]) # this does the right thing if args[1] is missing
end
# passing something other than a String or Exception is illegal, but if someone does it anyway,
# that object won't have backtrace or continuation methods. in that case, let's pass it on to
# the original raise, which will reject it
return super exception unless exception.is_a?(Exception)
# keep original backtrace if reraising
exception.set_backtrace(args.size >= 3 ? args[2] : caller) if exception.backtrace.nil?
blk = callcc do |cc|
exception.continuation = cc
super exception
end
blk.call unless blk.nil?
end
alias :fail :raise
end
class Object
include RaisePlus
end

View File

@ -224,21 +224,10 @@ class Formula
# so load any deps before this point! And exit asap afterwards
yield self
rescue RuntimeError, SystemCallError => e
if not ARGV.debug?
%w(config.log CMakeCache.txt).each do |fn|
(HOMEBREW_LOGS/name).install(fn) if File.file?(fn)
end
raise
%w(config.log CMakeCache.txt).each do |fn|
(HOMEBREW_LOGS/name).install(fn) if File.file?(fn)
end
onoe e.inspect
puts e.backtrace unless e.kind_of? BuildError
ohai "Rescuing build..."
puts "When you exit this shell Homebrew will attempt to finalise the installation."
puts "If nothing is installed or the shell exits with a non-zero error code,"
puts "Homebrew will abort. The installation prefix is:"
puts prefix
interactive_shell self
raise
end
end
end