Mike Slinn
Mike Slinn

Debugging Jekyll Plugins with an IDE

Published 2022-02-21. Last modified 2022-04-17.
Time to read: 8 minutes.

This page is part of the jekyll collection, categorized under Jekyll, Ruby, Visual Studio Code.

This article is out of date. The Rebornix Ruby debugger is not used any more. The Ruby debugger setup is described here. The rest of the information is valid.

This article is the last in a three-part series. The first article describes how to install and use the jekyll_bootstrap5_tabs plugin. The second article describes how the Jekyll plugin was constructed. The plugin is built and published as an open-source Ruby gem. This article is dedicated to demonstrating a straightforward way of debugging Jekyll plugins, which are always written in Ruby.

Many open-source projects fall far short of their true potential because no-one bothers to tell the story, completely and thoughtfully, in depth. Hopefully this article will in some small way improve Jekyll’s circumstance in the F/OSS world.

The Jekyll documentation provides absolutely no commentary on how to debug plugins. Perhaps the Jekyll developers felt the information would be obvious to sufficiently experienced programmers. Because so many debugging possibilities exist, I did not find it obvious.

These Instructions Work Everywhere

The instructions in this article should work on every OS that Jekyll runs on, using Visual Studio Code, RubyMine, IntelliJ IDEA, or any other IDE that supports normal Ruby debugging.

Most problems with debugging Jekyll plugins are related to the plugin’s need to receive parameters from Jekyll. We do not need to dig into Jekyll itself to get this sorted out; all we need to do is to find where the gem you want to debug was installed, and set breakpoints. The information that the IDE will then present to you will far exceed what the Jekyll plugin documentation provides.

Prerequisites

The Setting Up a Ruby Development Environment article describes the necessary preparations.

Plugins Run In the Jekyll Address Space

The most important thing to know about debugging Jekyll plugins is that they run in the same address space as Jekyll itself. That means to debug a plugin you must actually debug the Jekyll process, and it will load the most recently created version of your plugin.

Orientation

For the purposes of this article, I assume that you have a Jekyll plugin that you want to debug. It does not matter if you wrote the plugin or not. All that matters is that the plugin was installed properly. Throughout this article, I will refer to this gem as “the subject gem”. I wrote this article to figure out how to debug my subject gem, which is jekyll_bootstrap5_tabs, so you will see references to that gem in this article whenever I discuss what needs to be done with your subject gem.

Move to your Jekyll project directory, this is where we will work. For me, that meant:

Shell
$ cd $jekyll_bootstrap5_tabs

If you are curious where the environment variable above was defined, when my Bash shells start, they source $work/.evars. The following lines are within:

Shell
export work=/var/work
export jekyll=$work/jekyll
export jekyll_flexible_include_plugin=$jekyll/jekyll-flexible-include-plugin
export jekyll_bootstrap5_tabs=$jekyll/jekyll_bootstrap5_tabs
export jekyll_template=$sites/jekyll_template

That is part of a directory structure I maintain across several machines.

Jekyll Launcher

Jekyll is provided as a Ruby gem. When the Jekyll gem is installed, it also creates a launcher at /usr/local/bin/jekyll, which is actually a small Ruby program that loads the gem.

/usr/local/bin/jekyll
#!/usr/bin/env ruby2.7
#
# This file was generated by RubyGems.
#
# The application 'jekyll' is installed as part of a gem, and
# this file is here to facilitate running it.
#

require 'rubygems'

version = ">= 0.a"

str = ARGV.first
if str
  str = str.b[/\A_(.*)_\z/, 1]
  if str and Gem::Version.correct?(str)
    version = str
    ARGV.shift
  end
end

if Gem.respond_to?(:activate_bin_path)
  load Gem.activate_bin_path('jekyll', 'jekyll', version)
else
  gem "jekyll", version
  load Gem.bin_path("jekyll", "jekyll", version)
end

Jekyll is best debugged when launched via the normal means, that is, via /usr/local/bin/jekyll. The launcher figures out where the Jekyll gem resides, and loads it.

🙏

If you set a breakpoint on the installed plugin's source code, execution will halt when the plugin is loaded or invoked. You will be able to see the call stack and the variables at each level of the call stack. This is super helpful!

Locating the Subject Gem

You need to know where the subject gem was installed to set breakpoints in it. From within a Jekyll project that uses the subject gem, discover the location of the Jekyll entry points within the gem as follows:

Shell
$ bundle info jekyll_bootstrap5_tabs
  * jekyll_bootstrap5_tabs (1.1.0)
        Summary: Jekyll plugin for Bootstrap 5 tabs
        Homepage: https://mslinn.com/blog/2022/02/13/jekyll-gem.html
        Source Code: https://github.com/mslinn/jekyll_bootstrap5_tabs
        Path: /home/mslinn/gems/gems/jekyll_bootstrap5_tabs-1.1.0 

The jekyll_bootstrap5_tabs gem is located at /home/mslinn/gems/gems/jekyll_bootstrap5_tabs-1.1.0.

Now we need to find the entry point for the plugin, which is where Jekyll invokes the functionality of the plugin. If we search for Liquid:: we'll find the source files containing the entry points.

Shell
$ grep -rl 'Liquid::' /home/mslinn/gems/gems/jekyll_bootstrap5_tabs-1.1.0/*
/home/mslinn/gems/gems/jekyll_bootstrap5_tabs-1.1.0/lib/jekyll_bootstrap5_tabs.rb 

Now we know we need to open /home/mslinn/gems/gems/jekyll_bootstrap5_tabs-1.1.0/lib/jekyll_bootstrap5_tabs.rb in Visual Studio to set breakpoints.

Here is a one-line command that will tell you that same information:

Shell
$ grep -rl 'Liquid::' "$( bundle info jekyll_bootstrap5_tabs | \
  grep Path | awk '{print $2}' )"
/home/mslinn/gems/gems/jekyll_bootstrap5_tabs-1.1.0/lib/jekyll_bootstrap5_tabs.rb 

Launching vs. Attaching

For programs that you built, you would start the debug server; it would launch the program and provide normal Ruby debugging features. Your IDE (Visual Studio Code, RubyMine or IDEA) attaches to the debug server. Most IDEs simplify this process by allowing you to provide a run configuration for launching a Ruby program / gem; the IDE takes care of the plumbing.

Because the Jekyll launch process is complex, we will start it up and then attach to its process instead.

Debugging using Visual Studio Code and Docker on Debian Distros

Documentation on how to set up Jekyll for debugging using Visual Studio Code has been available for Debian-derived Linux distros since February 2020. However, the docs are thin and are not applicable to many circumstances. Following the instructions causes Docker and other dependencies to be installed on your machine.

I don't see any benefit in running Docker to debug Jekyll. Examining the Dockerfile reveals its essence, however: two well-known gems are installed that provide a debug server ruby-debug-ide and debase (GitHub).

.devcontainer Files

You can skip ahead to Container-Free Debugging. I just provide the information in this section for completeness, but it is not necessary for understanding anything relevant to this article.

Here are the files included in Jekyll for debugging via Docker, in the .devcontainer directory: devcontainer.json and Dockerfile.

.devcontainer/devcontainer.json
// For format details, see https://aka.ms/vscode-remote/devcontainer.json or this file's README at:
// https://github.com/microsoft/vscode-dev-containers/tree/v0.101.1/containers/ruby-2
{
	"name": "Ruby 2",
	"dockerFile": "Dockerfile",

	// Set *default* container specific settings.json values on container create.
	"settings": {
		"terminal.integrated.shell.linux": "/bin/bash"
	},

	// Add the IDs of extensions you want installed when the container is created.
	"extensions": [
		"rebornix.Ruby"
	]

	// Use 'forwardPorts' to make a list of ports inside the container available locally.
	// "forwardPorts": [],

	// Use 'postCreateCommand' to run commands after the container is created.
	"postCreateCommand": "bundle install",

	// Uncomment to connect as a non-root user. See https://aka.ms/vscode-remote/containers/non-root.
	// "remoteUser": "vscode"

}

The following Dockerfile is specific to Debian Linux and related distros that support apt. Sorry, Mac users!

.devcontainer/Dockerfile
#-------------------------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See https://go.microsoft.com/fwlink/?linkid=2090316 for license information.
#-------------------------------------------------------------------------------------------------------------
FROM ruby:2
# Avoid warnings by switching to noninteractive ENV DEBIAN_FRONTEND=noninteractive
# This Dockerfile adds a non-root user with sudo access. Use the "remoteUser" # property in devcontainer.json to use it. On Linux, the container user's GID/UIDs # will be updated to match your local UID/GID (when using the dockerFile property). # See https://aka.ms/vscode-remote/containers/non-root-user for details. ARG USERNAME=vscode ARG USER_UID=1000 ARG USER_GID=$USER_UID
# Configure apt and install packages RUN apt-get update \ && apt-get -y install --no-install-recommends apt-utils dialog locales 2>&1 \ # Verify git, process tools installed && apt-get -y install git openssh-client iproute2 procps lsb-release \ # # Install ruby-debug-ide and debase && gem install ruby-debug-ide \ && gem install debase \ # # Install node.js && apt-get -y install curl software-properties-common \ && curl -sL https://deb.nodesource.com/setup_13.x | bash - \ && apt-get -y install nodejs \ # # Create a non-root user to use if preferred - see https://aka.ms/vscode-remote/containers/non-root-user. && groupadd --gid $USER_GID $USERNAME \ && useradd -s /bin/bash --uid $USER_UID --gid $USER_GID -m $USERNAME \ # [Optional] Add sudo support for the non-root user && apt-get install -y sudo \ && echo $USERNAME ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/$USERNAME\ && chmod 0440 /etc/sudoers.d/$USERNAME \ # # Clean up && apt-get autoremove -y \ && apt-get clean -y \ && rm -rf /var/lib/apt/lists/*
# Set the locale RUN sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen && \ dpkg-reconfigure --frontend=noninteractive locales && \ update-locale LANG=en_US.UTF-8
ENV LANG en_US.UTF-8
# Switch back to dialog for any ad-hoc use of apt-get ENV DEBIAN_FRONTEND=dialog

Container-Free Debugging

These instructions are applicable for all platforms that Jekyll runs on.

Debugging Components

A diagram should help us understand how the different components of a Ruby debugging extension work for Visual Studio Code. I added Ruby components to a diagram provided in Introducing Logpoints and auto-attach from the Visual Studio Code documentation.

You can see three components in the above diagram:

  1. The rebornix.Ruby Visual Studio Code plugin, which provides debugging, lint, Semantic highlighting, and Intellisense support for Ruby.
  2. The ruby-debug-ide gem, which communicates between Visual Studio and Ruby debuggers.
  3. The debase gem, which is the actual Ruby debugger.

Install Debugging Gems

Install the two Ruby gems required for debugging. The debase-ruby_core_source transitive dependency was automatically installed as well.

Shell
$ gem install debase ruby-debug-ide
Fetching debase-0.2.4.1.gem
Fetching debase-ruby_core_source-0.10.14.gem
Successfully installed debase-ruby_core_source-0.10.14
Building native extensions. This could take a while...
Successfully installed debase-0.2.4.1
Parsing documentation for debase-ruby_core_source-0.10.14
Installing ri documentation for debase-ruby_core_source-0.10.14
Parsing documentation for debase-0.2.4.1
Installing ri documentation for debase-0.2.4.1
Done installing documentation for debase-ruby_core_source, debase after 4 seconds
Fetching ruby-debug-ide-0.7.3.gem
Building native extensions. This could take a while...
Successfully installed ruby-debug-ide-0.7.3
Parsing documentation for ruby-debug-ide-0.7.3
Installing ri documentation for ruby-debug-ide-0.7.3
Done installing documentation for ruby-debug-ide after 0 seconds
3 gems installed 

Lets find out the names of the commands provided by the ruby-debug-ide gem:

Shell
$ gem specification ruby-debug-ide executables
---
- rdebug-ide
- gdb_wrapper 

Caveat

Looking at the source code for the Ruby plugin it is apparent that functional breakpoints are not implemented. This is not mentioned in the documentation.

Setup Debugging

This is where having your software development projects on an SSD with an M.2 interface, such as NVMe, would really make a difference in your productivity.

Now it is time to launch Jekyll from the rdebug-ide debug server. Debug clients such as Visual Studio Code will be able to control the debug session via port 1234.

At first, I specified all of my normal Jekyll options for local usage, as shown below. They probably impacted the responsiveness of the debug session, but are workable nonetheless.

Shell
$ bundle exec rdebug-ide \
  --host 0.0.0.0 \
  --port 1234 \
  --dispatcher-port 26162 \
  -- \
    /usr/local/bin/jekyll serve \
      --livereload_port 35721 \
      --force_polling \
      --host 0.0.0.0 \
      --port 4001 \
      --future \
      --incremental \
      --livereload \
      --drafts \
      --unpublished
Fast Debugger (ruby-debug-ide 0.7.3, debase 0.2.4.1, file filtering is supported) listens on 0.0.0.0:1234 

Jekyll does not run any slower under the Ruby debugger.

Above you see 2-stage launches expressed as single command lines. The backslashes are line continuation characters, which is why 'one command line' actually spans a dozen or so lines.

  1. Stage 1: debugger parameters
  2. Stage 2: Jekyll parameters

I took a screen shot of the above command line and added a few comments:

You could make the above command line into a script, or a Visual Studio Code task. Here it is as a task:

.vscode/tasks.json
{
  // See https://go.microsoft.com/fwlink/?LinkId=733558
  // for the documentation about the tasks.json format
  "version": "2.0.0",
  "tasks": [
      {
          "label": "start-debug",
          "command": "/usr/local/bin/bundle",
          "args": [
             "exec", "rdebug-ide",
                "--host", "0.0.0.0",
                "--port", "1234",
                "--dispatcher-port", "26162",
                "--",
                "/usr/local/bin/jekyll", "serve",
                    "--livereload_port", "35721",
                    "--force_polling",
                    "--host", "0.0.0.0",
                    "--incremental",
                    "--livereload",
                    "--port", "4001",
                    "--future",
                    "--drafts",
                    "--unpublished",
          ],
          "isBackground": true,
          "presentation": {
              "panel": "new"
          },
          "problemMatcher": {
              "owner": "custom",
              "pattern": {
                  "regexp": "____"
              },
              "background": {
                  "activeOnStart": true,
                  "beginsPattern": "____",
                  "endsPattern": "Fast Debugger"
              }
          }
      }
  ]
}
This is one of those times where I wish JSON had a comment facility.

I made the following run configuration for Visual Studio Code to attach to the process resulting from running the above. Note that this is not the entire contents of .vscode/launch.json, just the one run configuration.

.vscode/launch.json Entry
{
  "cwd": "${workspaceRoot}",
  "name": "Attach rdebug-ide",
  "request": "attach",
  "remoteHost": "localhost",
  "remotePort": "1234",
  "remoteWorkspaceRoot": "/",
  "restart": true,
  "showDebuggerOutput": true,
  "stopOnEntry": true,
  "type": "Ruby",
},

I did not have success when I attempted to run the task automatically when starting the debug session. I am unsure what the problem was. If you are so inclined, you could enable that feature by adding the following to the launch.json entry above. I thoughtfully provided a trailing comma for you to copy:

Extra entry in .vscode/launch.json
"preLaunchTask": "start-debug",

Step By Step

To establish a debugging session:

  1. Launch Jekyll as shown above (bundle exec rdebug-ide …), either on the command line or as a Visual Studio Code task.
  2. Set a breakpoint in Visual Studio Code (I will discuss this next).
  3. Attach the Visual Studio debug client using the run configuration called Attach rdebug-ide.

Breakpoint Types

Visual Studio Code supports several types of breakpoints. This article discusses only two types: location-based breakpoints and function breakpoints. The links in this paragraph explain how breakpoints work in detail; I won't repeat that information here.

However, the rebornix.Ruby Visual Studio plugin does not support function breakpoints.

The overhead of breakpoints can be reduced.

PDB Files

Visual Studio Code uses PDB files to associate source code line numbers and variable names with a program being debugged. However, I am unaware of any mechanism to create a PDB file for Ruby code. Dear reader, if you know how to do this, please contact me.

This means that attaching to a Ruby process, or debugging a gem that was launched, will not automatically cause the relevant source file to open up at the line containing the breakpoint when hit. Instead, you will have to look at the top item in the Call stack pane, where you will see the fully qualified name of the source file and the line number where execution has been paused. You will then have to open that file up in the editor manually.

Execution Break - Ta-Da!

When VS Code hits a breakpoint, the call stack and variables are shown, but the editor does not display the line that the debugger has stopped at. You might not realize that this happened. After all this work (I only publish the happy path), when this just happened as it should for me the first time, I did not recognize it. If a source edit pane opened up with the breakpoint highlighted I would have noticed. Unfortunately, that does not work (yet!).

Now for the ta-da! moment. If you are not a programmer, this will go over with a thud. Since you made it this far, you are either a masochist, or a programmer.

😁

Take a good long look at the variables in the following screenshot. You can see what has been passed from Jekyll to the plugin easily here. This information is really hard to come by any other way.

Context.@environments

context.instance_variable_get('@scopes'), an array of hashes, has only one entry: 'nowMillis': '1645386226'. That was a Liquid variable that I had set in the page that contained a reference to the Bootstrap 5 tabs plugin.

self

The debugger paused at the start of TabsBlock#render. The variable self has type JeklyyBootstrap5Tabs::TabsBlock, and it looks like this:

The Jekyll tag was: {% tabs test pretty %}:

  • self.@markup is a string with value test pretty (with a space at the end).
  • self.@pretty_print is true.
  • self.@tab_name is a string with value test.
  • self.tag_name is a string with value tabs.

About the Author

I, Mike Slinn, have been working with Ruby for a long time now. Back in 2005, I was the product marketing manager at CodeGear (the company was formerly known as Borland) for their 3rd Rail IDE. 3rd Rail supported Ruby and Ruby on Rails at launch.

In 2006, I co-chaired the Silicon Valley Ruby Conference on behalf of the SD Forum in Silicon Valley. As you can see, I have the t-shirt. I was the sole chairman of the 2007 Silicon Valley Ruby Conference.

Several court cases have come my way over the years in my capacity as a software expert witness. The court cases featured questions about IP misappropriation for Ruby on Rails programs. You can read about my experience as a software expert if that interests you.

I currently enjoy writing Jekyll plugins in Ruby for this website and others, as well as Ruby utilities.



* indicates a required field.

Please select the following to receive Mike Slinn’s newsletter:

You can unsubscribe at any time by clicking the link in the footer of emails.

Mike Slinn uses Mailchimp as his marketing platform. By clicking below to subscribe, you acknowledge that your information will be transferred to Mailchimp for processing. Learn more about Mailchimp’s privacy practices.