Skip to content

Instantly share code, notes, and snippets.

@x-yuri
Last active October 23, 2019 21:14
Show Gist options
  • Select an option

  • Save x-yuri/2f58e58365542e1a3b8663132e9d357a to your computer and use it in GitHub Desktop.

Select an option

Save x-yuri/2f58e58365542e1a3b8663132e9d357a to your computer and use it in GitHub Desktop.
How do I debug bundler?

You might have run, or might run in the future (or never run for that mater) into the following issue. Let's do bundle init, create 1.rb:

require 'rubygems'
require 'bundler'
Bundler.setup(:default)

Then add require 'byebug'; debugger to the beginning of the Bundler.setup method, and run it with ruby 1.rb. That gives us:

Traceback (most recent call last):
        10920: from 1.rb:7:in `<main>'
        10919: from /home/yuri/.gem/ruby/2.6.3/gems/bundler-2.0.2/lib/bundler.rb:106:in `setup'
        10918: from /home/yuri/.gem/ruby/2.6.3/gems/bundler-2.0.2/lib/bundler.rb:127:in `require'
        10917: from /home/yuri/.gem/ruby/2.6.3/gems/bundler-2.0.2/lib/bundler.rb:106:in `setup'
        10916: from /home/yuri/.gem/ruby/2.6.3/gems/bundler-2.0.2/lib/bundler.rb:127:in `require'
        10915: from /home/yuri/.gem/ruby/2.6.3/gems/bundler-2.0.2/lib/bundler.rb:106:in `setup'
        10914: from /home/yuri/.gem/ruby/2.6.3/gems/bundler-2.0.2/lib/bundler.rb:127:in `require'
        10913: from /home/yuri/.gem/ruby/2.6.3/gems/bundler-2.0.2/lib/bundler.rb:106:in `setup'
         ... 10908 levels...
            4: from /home/yuri/.gem/ruby/2.6.3/gems/bundler-2.0.2/lib/bundler.rb:127:in `require'
            3: from /home/yuri/.gem/ruby/2.6.3/gems/bundler-2.0.2/lib/bundler.rb:106:in `setup'
            2: from /home/yuri/.gem/ruby/2.6.3/gems/bundler-2.0.2/lib/bundler.rb:127:in `require'
            1: from /home/yuri/.gem/ruby/2.6.3/gems/bundler-2.0.2/lib/bundler.rb:106:in `setup'
/home/yuri/.gem/ruby/2.6.3/gems/bundler-2.0.2/lib/bundler.rb:127:in `require': stack level too deep (SystemStackError)

So we see that Bundler module has require method that was called in place of the usual one we expected. Let's execute it within the top-level context then:

TOPLEVEL_BINDING.eval("require 'byebug'"); debugger

But Bundler.require somehow gets called again:

Traceback (most recent call last):
        11232: from 1.rb:7:in `<main>'
        11231: from /home/yuri/.gem/ruby/2.6.3/gems/bundler-2.0.2/lib/bundler.rb:109:in `setup'
        11230: from /home/yuri/.gem/ruby/2.6.3/gems/byebug-11.0.1/lib/byebug/attacher.rb:36:in `byebug'
        11229: from /home/yuri/.gem/ruby/2.6.3/gems/bundler-2.0.2/lib/bundler.rb:130:in `require'
        11228: from /home/yuri/.gem/ruby/2.6.3/gems/bundler-2.0.2/lib/bundler.rb:109:in `setup'
        11227: from /home/yuri/.gem/ruby/2.6.3/gems/byebug-11.0.1/lib/byebug/attacher.rb:36:in `byebug'
        11226: from /home/yuri/.gem/ruby/2.6.3/gems/bundler-2.0.2/lib/bundler.rb:130:in `require'
        11225: from /home/yuri/.gem/ruby/2.6.3/gems/bundler-2.0.2/lib/bundler.rb:109:in `setup'
         ... 11220 levels...
            4: from /home/yuri/.gem/ruby/2.6.3/gems/bundler-2.0.2/lib/bundler.rb:107:in `eval'
            3: from <main>:in `<main>'
            2: from /home/yuri/.rubies/ruby-2.6.3/lib/ruby/2.6.0/rubygems/core_ext/kernel_require.rb:36:in `require'
            1: from /home/yuri/.rubies/ruby-2.6.3/lib/ruby/2.6.0/rubygems/core_ext/kernel_require.rb:36:in `puts'
/home/yuri/.rubies/ruby-2.6.3/lib/ruby/2.6.0/rubygems/core_ext/kernel_require.rb:36:in `puts': stack level too deep (SystemStackError)

To understand that we've got to know a little about how byebug works. byebug.rb is not particularly big:

require_relative "byebug/attacher"

byebug/attacher.rb:

...
module Kernel
  def byebug
    require "byebug/core"
    Byebug.attach unless Byebug.mode == :off
  end
  alias debugger byebug
...

So, calling debugger from Bundler.setup leads to require 'byebug/core'. And the thing is, self is Bundler when the last statement gets invoked.

Let's invoke the debugger this way then:

TOPLEVEL_BINDING.eval("require 'byebug'; debugger")

This time everything goes as planned:

[107, 116] in /home/yuri/.gem/ruby/2.6.3/gems/bundler-2.0.2/lib/bundler.rb
   107: # TOPLEVEL_BINDING.eval("require 'byebug'")
   108: puts '-' * 10
   109: # debugger
   110: TOPLEVEL_BINDING.eval("require 'byebug'; debugger")
   111:       # Return if all groups are already loaded
=> 112:       return @setup if defined?(@setup) && @setup
   113: 
   114:       definition.validate_runtime!
   115: 
   116:       SharedHelpers.print_major_deprecations!
(byebug) c

But that's not all. The funny thing about bundler is that it cleans up $LOAD_PATH at some point. $LOAD_PATH to ruby is what PATH is to your average shell. It's a list of paths relative to which ruby resolves arguments to require. So, let's leave the statement we added at the beginning of Bundler.setup method, and add our customary TOPLEVEL_BINDING.eval("require 'byebug'; debugger") after the following line. That results into:

Traceback (most recent call last):
        6: from 1.rb:7:in `<main>'
        5: from /home/yuri/.gem/ruby/2.6.3/gems/bundler-2.0.2/lib/bundler.rb:122:in `setup'
        4: from /home/yuri/.gem/ruby/2.6.3/gems/bundler-2.0.2/lib/bundler/runtime.rb:21:in `setup'
        3: from /home/yuri/.gem/ruby/2.6.3/gems/bundler-2.0.2/lib/bundler/runtime.rb:21:in `eval'
        2: from <main>:in `<main>'
        1: from /home/yuri/.rubies/ruby-2.6.3/lib/ruby/2.6.0/rubygems/core_ext/kernel_require.rb:56:in `require'
/home/yuri/.rubies/ruby-2.6.3/lib/ruby/2.6.0/rubygems/core_ext/kernel_require.rb:56:in `require': cannot load such file -- byebug (LoadError)
        7: from 1.rb:7:in `<main>'
        6: from /home/yuri/.gem/ruby/2.6.3/gems/bundler-2.0.2/lib/bundler.rb:122:in `setup'
        5: from /home/yuri/.gem/ruby/2.6.3/gems/bundler-2.0.2/lib/bundler/runtime.rb:21:in `setup'
        4: from /home/yuri/.gem/ruby/2.6.3/gems/bundler-2.0.2/lib/bundler/runtime.rb:21:in `eval'
        3: from <main>:in `<main>'
        2: from /home/yuri/.rubies/ruby-2.6.3/lib/ruby/2.6.0/rubygems/core_ext/kernel_require.rb:34:in `require'
        1: from /home/yuri/.rubies/ruby-2.6.3/lib/ruby/2.6.0/rubygems/core_ext/kernel_require.rb:132: in `rescue in require'
/home/yuri/.rubies/ruby-2.6.3/lib/ruby/2.6.0/rubygems/core_ext/kernel_require.rb:132:in `require': cannot load such file -- byebug (LoadError)

The output is a bit funny, but one can argue that ruby tries to require byebug, fails because it's not in the $LOAD_PATH, tries to activate it, and requires it again hoping that corresponding gem's lib dir was added to $LOAD_PATH.

A little stepaside here to explain things. Activating a gem is choosing a version of the gem, and adding its lib dir to $LOAD_PATH. Every time you require the first file from an installed gem, it goes through this require—rescue—activate—require routine. The second file gets required right away, since corresponding gem's lib dir is already in $LOAD_PATH.

But the thing is we've already required—and hence activated—byebug gem before. After which bundler removed its dir from $LOAD_PATH. So, now it's marked as activated, and as such rubygems doesn't add it to $LOAD_PATH again. Therefore, when it comes to requiring the file again, it fails.

Okay, let's not require byebug twice:

Traceback (most recent call last):
        7: from 1.rb:7:in `<main>'
        6: from /home/yuri/.gem/ruby/2.6.3/gems/bundler-2.0.2/lib/bundler.rb:122:in `setup'
        5: from /home/yuri/.gem/ruby/2.6.3/gems/bundler-2.0.2/lib/bundler/runtime.rb:21:in `setup'
        4: from /home/yuri/.gem/ruby/2.6.3/gems/bundler-2.0.2/lib/bundler/runtime.rb:21:in `eval'
        3: from <main>:in `<main>'
        2: from /home/yuri/.gem/ruby/2.6.3/gems/byebug-11.0.1/lib/byebug/attacher.rb:36:in `byebug'
        1: from /home/yuri/.rubies/ruby-2.6.3/lib/ruby/2.6.0/rubygems/core_ext/kernel_require.rb:56:in `require'
/home/yuri/.rubies/ruby-2.6.3/lib/ruby/2.6.0/rubygems/core_ext/kernel_require.rb:56:in `require': cannot load such file -- byebug/core (LoadError)
        8: from 1.rb:7:in `<main>'
        7: from /home/yuri/.gem/ruby/2.6.3/gems/bundler-2.0.2/lib/bundler.rb:122:in `setup'
        6: from /home/yuri/.gem/ruby/2.6.3/gems/bundler-2.0.2/lib/bundler/runtime.rb:21:in `setup'
        5: from /home/yuri/.gem/ruby/2.6.3/gems/bundler-2.0.2/lib/bundler/runtime.rb:21:in `eval'
        4: from <main>:in `<main>'
        3: from /home/yuri/.gem/ruby/2.6.3/gems/byebug-11.0.1/lib/byebug/attacher.rb:36:in `byebug'
        2: from /home/yuri/.rubies/ruby-2.6.3/lib/ruby/2.6.0/rubygems/core_ext/kernel_require.rb:34:in `require'
        1: from /home/yuri/.rubies/ruby-2.6.3/lib/ruby/2.6.0/rubygems/core_ext/kernel_require.rb:136: in `rescue in require'
/home/yuri/.rubies/ruby-2.6.3/lib/ruby/2.6.0/rubygems/core_ext/kernel_require.rb:136:in `require': cannot load such file -- byebug/core (LoadError)

We've moved on a bit, but now the issue is with Kernel.byebug requiring byebug/core. So, let's add byebug back. In my case that would be:

clean_load_path
$LOAD_PATH.unshift(
  "/home/yuri/.gem/ruby/2.6.3/gems/byebug-11.0.1/lib",
  "/home/yuri/.gem/ruby/2.6.3/extensions/x86_64-linux/2.6.0-static/byebug-11.0.1")
TOPLEVEL_BINDING.eval("debugger")

And that worked. But couldn't we just add byebug to the Gemfile? Well, that would help to some extent. Meaning, with byebug in the Gemfile bundler would add byebug's lib dir to $LOAD_PATH at some point. But that leaves the following zone uncovered. To be able to put debugger between those lines, We've got to add byebug to $LOAD_PATH.

Happy debugging :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment