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:
parent
f6091b1c85
commit
18dbe47f9f
@ -13,6 +13,7 @@ at_exit do
|
|||||||
end
|
end
|
||||||
|
|
||||||
require 'global'
|
require 'global'
|
||||||
|
require 'debrew' if ARGV.debug?
|
||||||
|
|
||||||
def main
|
def main
|
||||||
# The main Homebrew process expects to eventually see EOF on the error
|
# The main Homebrew process expects to eventually see EOF on the error
|
||||||
@ -42,6 +43,7 @@ def main
|
|||||||
install(Formula.factory($0))
|
install(Formula.factory($0))
|
||||||
rescue Exception => e
|
rescue Exception => e
|
||||||
unless error_pipe.nil?
|
unless error_pipe.nil?
|
||||||
|
e.continuation = nil if ARGV.debug?
|
||||||
Marshal.dump(e, error_pipe)
|
Marshal.dump(e, error_pipe)
|
||||||
error_pipe.close
|
error_pipe.close
|
||||||
exit! 1
|
exit! 1
|
||||||
@ -130,7 +132,16 @@ def install f
|
|||||||
interactive_shell f
|
interactive_shell f
|
||||||
else
|
else
|
||||||
f.prefix.mkpath
|
f.prefix.mkpath
|
||||||
|
|
||||||
|
begin
|
||||||
f.install
|
f.install
|
||||||
|
rescue Exception => e
|
||||||
|
if ARGV.debug?
|
||||||
|
debrew e, f
|
||||||
|
else
|
||||||
|
raise e
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
# Find and link metafiles
|
# Find and link metafiles
|
||||||
FORMULA_META_FILES.each do |filename|
|
FORMULA_META_FILES.each do |filename|
|
||||||
|
|||||||
163
Library/Homebrew/debrew.rb
Normal file
163
Library/Homebrew/debrew.rb
Normal 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
|
||||||
@ -224,22 +224,11 @@ class Formula
|
|||||||
# so load any deps before this point! And exit asap afterwards
|
# so load any deps before this point! And exit asap afterwards
|
||||||
yield self
|
yield self
|
||||||
rescue RuntimeError, SystemCallError => e
|
rescue RuntimeError, SystemCallError => e
|
||||||
if not ARGV.debug?
|
|
||||||
%w(config.log CMakeCache.txt).each do |fn|
|
%w(config.log CMakeCache.txt).each do |fn|
|
||||||
(HOMEBREW_LOGS/name).install(fn) if File.file?(fn)
|
(HOMEBREW_LOGS/name).install(fn) if File.file?(fn)
|
||||||
end
|
end
|
||||||
raise
|
raise
|
||||||
end
|
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
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user