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 | |
| 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')
| -rwxr-xr-x | Library/Homebrew/build.rb | 13 | ||||
| -rw-r--r-- | Library/Homebrew/debrew.rb | 163 | ||||
| -rw-r--r-- | Library/Homebrew/formula.rb | 17 | 
3 files changed, 178 insertions, 15 deletions
| diff --git a/Library/Homebrew/build.rb b/Library/Homebrew/build.rb index fe39a709d..85ff88705 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 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 diff --git a/Library/Homebrew/formula.rb b/Library/Homebrew/formula.rb index 4339c471d..76d1d271f 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 | 
