diff --git a/Library/Homebrew/build.rb b/Library/Homebrew/build.rb index fe39a709d1..85ff88705b 100755 --- a/Library/Homebrew/build.rb +++ b/Library/Homebrew/build.rb @@ -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| diff --git a/Library/Homebrew/debrew.rb b/Library/Homebrew/debrew.rb new file mode 100644 index 0000000000..93fabfc52b --- /dev/null +++ b/Library/Homebrew/debrew.rb @@ -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 diff --git a/Library/Homebrew/formula.rb b/Library/Homebrew/formula.rb index 4339c471d9..76d1d271f2 100644 --- a/Library/Homebrew/formula.rb +++ b/Library/Homebrew/formula.rb @@ -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