aboutsummaryrefslogtreecommitdiffstats
path: root/Library/Homebrew/debrew.rb
diff options
context:
space:
mode:
authorCamillo Lugaresi2012-02-21 01:14:02 -0600
committerMax Howell2012-10-28 11:39:02 -0400
commit18dbe47f9f356ad68cdc16327487f7a7d44a4ca2 (patch)
tree7e609a29b8fa40588713f145d7db7e73e4bce987 /Library/Homebrew/debrew.rb
parentf6091b1c85ca63c592234251bfc018d7c4f95d64 (diff)
downloadbrew-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.rb163
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