My First Ruby Gem Part 2: A Look Under the Hood

Sunny Beatteay
12 min readFeb 24, 2017

--

To recap from Part 1, we have initialized our gem project with the bundle gem command. We now have a new git project and a working directory.

➜  rubytutor git:(master) ✗ tree .├── Gemfile
├── LICENSE.txt
├── README.md
├── Rakefile
├── bin
│ ├── console
│ └── setup
├── lib
│ ├── rubytutor
│ │ └── version.rb
│ └── rubytutor.rb
├── rubytutor.gemspec
└── test
├── rubytutor_test.rb
└── test_helper.rb
4 directories, 11 files

If you’re new to gem building, this is a lot to comprehend. Let’s go through it step-by-step.

Gemfile

Anyone familiar with Bundler will be familiar with a Gemfile. It’s a Ruby file that uses a Domain Specific Language (DSL). This is where we list the Ruby version we want the program to use, all the gems our program will need to run on, as well as the source of those of gems (most often this will be rubygems.org).

However, as you will see below, this Gemfile is different:

source 'https://rubygems.org'# Specify your gem's dependencies in rubytutor.gemspecgemspec

On top, we can see that we are listing our gem’s source as 'rubygems.org'. Below that, where we normally list our gem dependencies, we see a message saying to specify our gems in the gemspec file. Let’s go look at that.

rubytutor.gemspec

Here’s what Bundler gave us:

# coding: utf-8lib = File.expand_path('../lib', __FILE__)
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
require 'rubytutor/version'
Gem::Specification.new do |spec|spec.name = "rubytutor"
spec.version = Rubytutor::VERSION
spec.authors = ["Sun-Li Beatteay"]
spec.email = ["not disclosed"]
spec.summary = %q{TODO: Write a short summary, because Rubygems requires one.}
spec.description = %q{TODO: Write a longer description or delete this line.}
spec.homepage = "TODO: Put your gem's website or public repo URL here."
spec.license = "MIT"
# Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'# to allow pushing to a single host or delete this section to allow pushing to any host.if spec.respond_to?(:metadata)
spec.metadata['allowed_push_host'] = "TODO: Set to 'http://mygemserver.com'"
else
raise "RubyGems 2.0 or newer is required to protect against " \
"public gem pushes."
end
spec.files = `git ls-files -z`.split("\x0").reject do |f|
f.match(%r{^(test|spec|features)/})
end
spec.bindir = "exe"
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
spec.require_paths = ["lib"]
spec.add_development_dependency "bundler", "~> 1.14"
spec.add_development_dependency "rake", "~> 10.0"
spec.add_development_dependency "minitest", "~> 5.0"
end

This file appears fairly robust, so let’s dissect it.

  1. At the very top, we are requiring all the necessary files from our gem and requiring the file that houses our gem’s version number.
  2. Next are our gem’s specifications. Notice that the name, version, authors and email are already filled out for us.
  3. We then have the summary and description, which are required by rubygems.org. If you try to build your gem without filling in this areas you will get an error. If you have a public repo where you store your gem’s files, I would suggest using that for your homepage.
  4. Below this is a message asking us to specify either a single server we will push our gem to, or to delete that whole bit of logic if we want to push to any server. If you want your gem to be open sourced, you’re okay to just delete that whole piece. However, if you’re making a private gem that is to be used for a company, you probably have a dedicated private server.

I will cover this section later on:

spec.files         = `git ls-files -z`.split("\x0").reject do |f|
f.match(%r{^(test|spec|features)/})
end
spec.bindir = "exe"
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
spec.require_paths = ["lib"]

So we can skip it for now.

At the bottom of the gemspec, we will see where we can add gem dependencies using their DSL. The reason why Bundler tells you to include gem dependencies in the gemspec instead of the Gemfile is because they display this metadata on rubygems.org.

While there are other reasons why we want to put our gem dependencies in the gemspec, they don’t pertain to us right now. They deal with long-term resilience and the fact that gems are less picky about exact versions of dependencies than applications are.

However, some gems, like Sinatra and Rails, will require a robust Gemfile because they need to ensure that the development environment and the production environment stay consistent.

The majority of the time we’re safe listing our gem dependencies in the gemspec and including a single line gemspec in our Gemfile. If you want to read more about the differences between Gemfile and gemspec, you can read about it here.

LICENSE.txt and README.md

These are fairly self explanatory but are still important to know.

The LICENSE.txt contains the license of our gem. Most often this will be the MIT license which is the standard for open source material. Again, if you are creating a private gem, this will not apply.

Here’s the license in full:

The MIT License (MIT)Copyright (c) 2017 Sun-Li BeatteayPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

As we can see, the MIT license essentially grants anyone access to use, modify and change the gems we create. All gems also come “as is”, meaning we, the creators, are not on the hook if any bugs in our code cause someone else’s software to crash.

The README.md is very important document for any gem and any piece of software in general. This is where we explain what our gem is, how to install it, how to use it, how to contribute to it, etc.

Bundler gives us some boiler plate text that you will need to fill in on your own:

# RubytutorWelcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/rubytutor`. To experiment with that code, run `bin/console` for an interactive prompt.TODO: Delete this and the text above, and describe your gem## InstallationAdd this line to your application's Gemfile:```rubygem 'rubytutor'```And then execute:$ bundleOr install it yourself as:$ gem install rubytutor## UsageTODO: Write usage instructions here## DevelopmentAfter checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).## ContributingBug reports and pull requests are welcome on GitHub at https://github.com/Sunny/rubytutor.## LicenseThe gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).

Documentation is very important. If we want anyone to use our gem at all, we need a well written and legible README.md. The writeup doesn’t have to be long or complex if our gem is fairly simple. Check RubyTutor’s or Bundler’s README as examples.

Rakefile

The Rakefile has an immense number of capabilities and is an incredibly flexible tool that is meant to automate redundant tasks — such as building, testing, updating, and releasing the gem. Like the gemspec and Gemfile, it uses a DSL.

Bundler gives us a fairly simple Rakefile:

require "bundler/gem_tasks"
require "rake/testtask"
Rake::TestTask.new(:test) do |t|
t.libs << "test"
t.libs << "lib"
t.test_files = FileList['test/**/*_test.rb']
end
task :default => :test

This simplicity is deceiving because the require "bundler/gem_tasks" at the top has pre-loaded several built in tasks to our Rakefile. To see them all, we can run bundle exec rake -T in our console. (Note: you must first complete the Specifications section of your gemspec file or you will get an error)

This is what we see:

rake build            # Build rubytutor-0.1.0.gem into the pkg 
directory
rake clean # Remove any temporary productsrake clobber # Remove any generated filesrake install # Build and install rubytutor-0.1.0.gem into
system gems
rake install:local # Build and install rubytutor-0.1.0.gem into
system gems withou...
rake release[remote] # Create tag v0.1.0 and build and push
rubytutor-0.1.0.gem to R...
rake test # Run tests

I’ll highlight a couple handy ones we will use often.

  • bundle exec rake test: This will run the tests. It finds those tests in the test folder, so make sure not to move/rename your test directory or else you’ll have to change it in your Rakefile. Always make sure your tests pass before pushing to RubyGems.
  • bundle exec rake install: This will build and install our gem onto our local machine so we can test it out for ourselves. We will probably do this a lot, so keep this command in mind.
  • bundle exec rake clobber: This rake goes hand-in-hand with the previous one. Having our gem installed and built on our local machine can cause an error saying:
ERROR: While executing gem … (Gem::InvalidSpecificationException)
gem-name-0.1.0 contains itself (gem-name-0.1.0.gem), check your files list

If you see this, run bundle exec rake clobber and it will remove the gem from your machine and you can go along with your work.

  • bundle exec rake release: This is an all-in-one release command that will push our gem to both our git repo and to the RubyGems server(if you’re using RubyGems). A couple notes about this, make sure that when you’re releasing a new version to update the version number. Also, you must commit your work first and your work must be up-to-date with what is in the remote git repo you’re pushing to.

While these are the tasks that come pre-loaded, we are more than welcome to create our own. If you’re a Launch School student and want to learn more about the Rakefile, check out the Packaging Code into a Project course in 130 Ruby Foundations. If you have never heard of Launch School, you can learn more about the Rakefile here:

The “bin” directory

The bin directory contains what are known as “binstubs” or “binaries”. These are essentially wrappers that provide a layer security for the original executables like rake, rails, and rspec that many Rubyists use when writing gems.

Why the need for this layer of security?

The Gemfile makes sure that we are using the correct version of Ruby. However, when we call gem executables, such a rake or rspec, nothing ensures that we’re using the correct version of that gem. This is why we need to prepend those commands with bundle exec.

However, continuously writing bundle exec can be a pain. Binstubs give us a way of shortening that command without losing that safety net. To create a binstub for a specific command, run bundle binstubs [gemname].

Since we’ll probably use rake a lot, we can run bundle binstubs rake. If we check our bin directory, we should see a new rake file.

#!/usr/bin/env ruby
# frozen_string_literal: true
#
# This file was generated by Bundler.
#
# The application 'rake' is installed as part of a gem, and
# this file is here to facilitate running it.
#
require "pathname"
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
Pathname.new(__FILE__).realpath)
require "rubygems"
require "bundler/setup"
load Gem.bin_path("rake", "rake")

We can see that this is written in Ruby and it’s setting up the rake environment. It’s requiring the file path from the Gemfile, which will lead to our gemspec and our gem dependencies. It is also requiring bundle/setup, a feature given to us by Bundler that makes it so our program can only use the gems we have listed in our Gemfile/gemspec and not any other gems we may have on our computer.

Finally, load Gem.bin_path("rake", "rake") is retrieving the actual rake executable file. This rake binstub was merely the wrapper for the original executable.

Since we’re here, let’s see what other binstubs Bundler has given us.

  • console: This is a handy command that opens up IRB with our gem pre-installed so we can play around with it.
  • setup: This is an open-ended executable that allows us to automate any setup that our gem requires. It comes with bundle install but we add more to it. Just keep it in mind this file uses the bash syntax.

As a final note: when we create a binstub, it gets rid of the need to being our commands with bundle exec. However, we now need to prepend our commands with bin/.

Example: bin/rake, bin/console. If we want to take it a step further and just write the executable itself, we can add bin/ to the $PATH variable like so:

export PATH="./bin:$PATH"

We just added our current working directory’s bin folder as the first path in the executable $PATH lookup. Meaning that when we call rake or console, it will look to our gem’s bin folder first for those files.

The nice thing about this is if you’re working on multiple gems/projects that have a bin folder with executables, this will work for those binstubs as well.

This change to $PATH is only temporary. If you exit out of your current Terminal/Console session, the changes will be reversed.

One thing to be aware of is that if we’re working in a shared environment where multiple users have “write” access, the previous $PATH manipulation is a security risk. To mitigate the risk, use this command instead:

export PATH="$PWD/bin:$PATH"

This will provide the absolute path to the bin in your current gem.

(To learn more about binstubs, check Understanding Binstubs)

OPTIONAL: the “exe” folder

Related to the bin directory is the exe directory. Bundler does not give it to us with the bundle gem command, but it’s good to know about. The exe directory is where we will put any executable files that we want shipped with our gem.

If you do not intend on creating an executable file, then you can ignore the rest of this section. It is by no means required of you and many gems do not have them.

Keep in mind that these executable files are different from binstubs. Binstubs are used in development or to help other developers contributing to our gem. Executables will be commands that users of the gem can run in the command line.

For example, if we go back to the GitHub for Bundler, we can see that they have a bin directory that has binstubs for rubocop and pry; these are common gems used in development. If we check their exe folder, we see their famous bundle and bundler executables.

Keep in mind that we should not name our executable directory anything else but exe. It’s standard practice and what Bundler will be looking for.

It says so right in our gemspec:

spec.bindir        = "exe"
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }

For those of us who want to write an executable but are not sure how, that’s beyond the scope of this article. However, the RubyGems Guide is a good place to start.

The “lib” directory

lib, short for library, is where we house the actual logic that makes our gem, our gem. Standard practice says that we have an .rb file with the name of our gem, as well as a directory of the same name.

lib
├── rubytutor
│ └── version.rb
└── rubytutor.rb

For me, the rubytutor.rb file is the main file and the file that will be uploaded to the users computer. The dir rubytutor will contain any other relevant files that pertain the main file.

As you can see from the file tree, we already have a file in the rubytutor dir, version.rb. This contains the current version number of our gem. In this case, 0.1.0.

Every time we release a new version of your gem, we should update this accordingly. Or better yet, add it to the rake release task.

The “test” directory

Last, but not least, the test files. No gem should be released without an extensive test suite.

To give you an example, my gem code is only 121 lines long, but I have 235 lines of test code with 23 tests and 133 assertions. This may be a bit overkill, but better safe than sorry.

The tests you use are up to you. I know RSpec is very popular, but I’m a fan of Minitest. The first time you run bundle gem (name) it will ask you about your test framework preferences and it will save that preference for future gems as well.

The reason for this is that Bundler creates some initial tests for us.

test_helper.rb

$LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
require 'rubytutor'
require 'minitest/autorun'

rubytutor_test.rb

require 'test_helper'class RubytutorTest < Minitest::Test
def test_that_it_has_a_version_number
refute_nil ::Rubytutor::VERSION
end
def test_it_does_something_useful
assert false
end
end

test_helper.rb is where we load the files and gems we’ll use for testing while rubytutor_test.rb is where the actual tests will be written. Notice that it requires test_helper at the top of rubytutor_test.rb.

That’s it for our guided tour through the gem files. I hope you now have a basic understanding of what’s going on under the hood and are ready to start building something. Continue onto Part 3 where I describe some lessons I learned while building RubyTutor.

--

--