diff options
| author | Camillo Lugaresi | 2012-02-21 01:14:02 -0600 | 
|---|---|---|
| committer | Max Howell | 2012-10-28 11:39:02 -0400 | 
| commit | 18dbe47f9f356ad68cdc16327487f7a7d44a4ca2 (patch) | |
| tree | 7e609a29b8fa40588713f145d7db7e73e4bce987 /Library/Homebrew/debrew.rb | |
| parent | f6091b1c85ca63c592234251bfc018d7c4f95d64 (diff) | |
| download | brew-18dbe47f9f356ad68cdc16327487f7a7d44a4ca2.tar.bz2 | |
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.
Diffstat (limited to 'Library/Homebrew/debrew.rb')
| -rw-r--r-- | Library/Homebrew/debrew.rb | 163 | 
1 files changed, 163 insertions, 0 deletions
| diff --git a/Library/Homebrew/debrew.rb b/Library/Homebrew/debrew.rb new file mode 100644 index 000000000..93fabfc52 --- /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 | 
