How to build a ruby development environment

The beta release date for project Feijoa was moved up 4 weeks. So that gives Kaya and the project Feijoa team only 6 more weeks to complete the beta. C01t, Nick, and Vijay, from the JAM team will pitch to help meet the new deadline. The programming language for project Feijoa is ruby. Since C01t has no experience with ruby, Kaya is his buddy for the first week on the project. “I’ll show you how the project is organized and how the ruby development environment works”, Kaya tells C01t. “Then you can take the rest of the day to setup and poke around the code.”

“Like all other teams here at Guava Orchards, we have fully Docker-ized our build and development environment”, explains Kaya. ruby projects use rake as the build utility instead of make. However, since rake requires ruby to run, project Feijoa has a Makefile wrapper to execute rake tasks inside the devbox container. C01t decides to try out this magical wrapper. So he types make -- -T in the terminal. As a result, the rake -T command executes inside the container and displays all tasks define in the Rakefile. This is awesome. C01t can run unit-tests and lint tools on his changes without having to install ruby natively.

“We use RSpec to write all our tests, simplecov to collect code coverage numbers, and RuboCop for linting and coding style enforcement”, Kaya informs C01t. First C01t gets a quick overview of the tools by reading the “Getting started guide” for project Feijoa. Then he checks out the configuration files at root of the project. C01t notices that .simplecov sets the minimum coverage threshold to 95%. In addition, he spots that .rspec randomizes specs execution order. Finally, he discovers that .rubocop.yml disables the BlockLenght cop for spec files.

“Another important tidbit of information for you to know is, that we use unbuilt gems to organize the codebase of project Feijoa into loosely coupled components”, Kaya tells him. Each top level folder is a component. Therefore, the file and folder structure inside a top level folder looks like a typical gem folder. There is a *.gemspec file and a Gemfile. There is a lib folder, a spec, and sometimes a bin folder.


x/
   .rspec
   .rubocop.yml
   .simplecov
   Gemfile
   Gemfile.lock
   Makefile
   Rakefile
   chairman/
      lib/
      spec/
      chairman.gemspec
      Gemfile
   dev/
      ...
   housekeeper/
      bin/
      lib/
      spec/
      housekeeper.gemspec
      Gemfile
   ...

The ruby development environment

Let us explore how create a ruby development environment for a project using un-built gems. The code samples are representative of what the files would look like for project Feijoa.

Dockerize

Above all we want to enable development of a ruby project like Guava Orchards project Feijoa without requiring a native installation of ruby. Therefore, we aim execute all specs, lint-ing tools and the service or app inside Docker containers. To create the scripts that build and run the containers we will follow the steps in our post: The case of the missing development environment. Therefore, we create the same structure under the dev folder.


dev
  docker
     docker-compose.yml
     devbox
       Dockerfile
       ...

However, this time we use ruby:latest as the base image instead of python:3.6.

rake via make

Because, every ruby development environment should employ rake as its build utility, we have to provide an easy way to execute rake tasks. Running rake requires ruby. Hence all tasks must run in the container. On the other hand, running make only needs the binary to be in a folder on your path. Therefore, we built a simple Makefile executes rake tasks inside of the devbox container instead of its own targets.


DEVBOX_CONTAINER=x_devbox

ifdef NO_DOCKER
  CMD = $(1)
else
  CMD = docker exec -t $(DEVBOX_CONTAINER) bash -c "cd /src && $(1)" 
endif

.DEFAULT:
  $(call CMD, bundle)
  $(call CMD, rake $@)

.PHONY: bundle
bundle:
  $(call CMD, bundle)

The value assigned to DEVBOX_CONTAINER is the name of the container started by the docker-compose.yaml file you created in the dev folder. Furthermore, the devbox container must mount the root of the project folder under /src.

So now if you have a rake task called unit-test, you can execute it via the command make unit-test. (If you copy your make binary and rename it to rake you can run the command as rake unit-test.) Furthermore, you can pass flags to rake by inserting -- before the flags you want passed on. So to see all rake task you would run make -- -T.

Finally, you define rake tasks as you normally would. You can implement them directly in the Rakefile or in *.rake files.

Un-built gems

To effectively work with the un-built gems we create a Gemfile in the root of the project and add all components as dependent gems. We also need to add the test only dependencies of the components explicitly into the root Gemfile. Therefore, the Gemfile in the root folder of project Feijoa would look something like:


source "http://rubygems.org"

group :development, :test do
  path "." do
    gem "chairman"
    gem "housekeeper"
  end

  gem "rake"
  gem "rspec"
  ...
end

Next we define a couple of rake tasks to run specs and lint tools from the root folder. For RuboCop we can use the task defined in the gem:


require "rubocop/rake_task"

desc "Default RuboCop task"
RuboCop::RakeTask.new(:rubocop)

Since all specs are implemented in folders nested inside the component folders, running those specs from the root is a tad more tricky. However, we can still make use of the task implementation from the RSpec gem:


require "rspec/core/rake_task"

components = %i[chairman housekeeper]
namespace :unittest do
  components.each do |component|
    desc "RSpec task for #{component}"
    RSpec::Core::RakeTask.new(component) do |t|
      test_dir = Rake.application.original_dir
      t.rspec_opts = [
        "-I#{test_dir}/#{component}/spec",
        "-I#{test_dir}/#{component}/spec/support",
        "-I#{test_dir}/#{component}/lib",
        "{test_dir}/#{component}",
      ]
    end
  end
  
  # other tasks in namespace unittest
end

Finally, with the tasks in place configuring RuboCop, RSpec, and simplecov is straight forward. We can just drop the .rubocop.yml, .rspec, and .simplecov files into the root folder.

Congratulations. You should now have a functioning ruby development environment.