Skip to content
This repository has been archived by the owner on Mar 23, 2023. It is now read-only.

Add New Language

cyphactor edited this page Nov 20, 2014 · 8 revisions

This document covers the necessary steps to take to add support for a new language to BoxCI. We will cover this in sections with respect to each of BoxCI commands.

In this document we will go through the process of adding support for the Ruby language. This is broken out into the following sections:

  1. Requirements for boxci init
  2. Requirements for boxci build
  3. Requirements for boxci test
  4. Overview of Result

Requirements for boxci init

In order for boxci init to work with a particular language we need to get the language to be registered as a supported language. This is a two step process:

  1. Create Language Class Skeleton
  2. Require the Language Skeleton
  3. Verify Language is registered

Create Language Class Skeleton

The first step in the process is to define the skeleton of the Boxci::Languages::Ruby class. This should exist in a file located at lib/boxci/languages/ruby.rb and would have the following content as a starting skeleton. Note: If you were adding support for Java it would exist in lib/boxci/languages/java.rb and would have a class name of Java.

module Boxci
  module Languages
    class Ruby < ::Boxci::Language

      no_commands do
        def default_script
          raise "Needs to be implemented"
        end

        def generate_starter_puppet_manifest
          raise "Needs to be implemented"
        end
      end
    end
  end
end

Require the Language Skeleton

Next you need to edit lib/boxci/boxci.rb and add the following line so that BoxCI will properly load your new language class. This should be added below the require 'boxci/language'

require 'boxci/languages/ruby'

Verify Language is Registered

You can verify that the Language is properly registered by running the following command:

bundle exec bin/boxci help init

If all is good you should now see your language appear in the Supported Languages: call out toward the end of the help message. This would look something like the following:

Usage:
  boxci init [-p PROVIDER] LANGUAGE

Options:
  -p, [--provider=PROVIDER]  
                             # Default: virtualbox

Description:
  `boxci init [-p PROVIDER] LANGUAGE` will create a .boxci directory in your user's home directory, create a provider 
  specific config (ex: ~/.boxci/providers/virtualbox.yml), set the default provider in (~/.boxci/global_config.yml), 
  and create a project config (.boxci.yml) in the current working directory.

  LANGUAGE is required, it is the language of your project, see supported languages below.

  --provider (-p) is optional. If omitted, it will use your configured default provider (virtualbox). If you don't have 
  a default provider configured it will default to 'virtualbox'.

  Supported Languages: ruby
  Supported Providers: virtualbox, aws, openstack

Requirements of boxci build

In order for boxci build to work with a particular language we need two things.

  1. Implement the generate_starter_puppet_manifest method in the language class
  2. Verify boxci build properly generates the starter puppet manifest

Implement generate_starter_puppet_manifest method

Implementing this in the lib/boxci/languages/ruby.rb file would look as follows.

module Boxci
  module Languages
    class Ruby < ::Boxci::Language

      no_commands do
        def default_script
          raise "Needs to be implemented"
        end

        def generate_starter_puppet_manifest
          @project_config = Boxci.project_config
          if @project_config.rbenv
            inside Boxci.project_path do
              run "git submodule add -f git@github.com:alup/puppet-rbenv.git puppet/modules/rbenv"
              run "git submodule update --init"
            end
          end
          template "templates/languages/#{Boxci.project_config.language}/main.pp", File.join(Boxci.project_path, "puppet/manifests/main.pp")
        end
      end
    end
  end
end

Based on the implementation of the generate_starter_puppet_manifest method defined above. You can see that it also depends on a language specific template existing at lib/boxci/templates/languages/ruby/main.pp. This template should be whatever you want the initial puppet manifest to be. In the case of ruby we want it to be the following.

exec { "apt-update":
  command => "/usr/bin/apt-get update"
}

Exec["apt-update"] -> Package <| |>

package {
  "rbenv":
  ensure => present,
  require => Exec['apt-update'],
}

rbenv::install { $vagrant_user:
  home => $vagrant_home,
  require => [Package['rbenv']],
}
<% if @project_config.rbenv %>
<% @project_config.rbenv.each do |ruby_version| -%>

rbenv::compile { "$vagrant_user/<%= ruby_version -%>":
  user => $vagrant_user,
  home => $vagrant_home,
  ruby => '<%= ruby_version -%>',
  require => Rbenv::Install["$vagrant_user"],
}
<% end -%>
<% end %>

Verify boxci build properly generates the starter puppet manifest

You can verify boxci build properly generates the starter puppet manifest by first boxci init a project with your new language and then running boxci build and seeing if it produced the starter puppet manifest the way you wanted. Note: It is also important that you test the resulting puppet manifest and make sure that it works as you want.

Requirements of boxci test

In order to properly support the boxci test command and all its functionality a number of things need to happen.

  1. Implement required default_script method
  2. Optionally implement before_permutation_switch method
  3. Optionally implement after_permutation_switch method
  4. Verify boxci test works properly

Implement required default_script method

At this point to support boxci test functionality you need to implement the default_script method in the language class. This method is responsible for returning a string representation of what the default script hook command should be. Note: For more info on the script hook please refer to BoxCI - Build Lifecycle.

Implementing this in the lib/boxci/languages/ruby.rb file would look as follows.

module Boxci
  module Languages
    class Ruby < ::Boxci::Language

      no_commands do
        def default_script
          %q{bundle exec rake}
        end

        def generate_starter_puppet_manifest
          @project_config = Boxci.project_config
          if @project_config.rbenv
            inside Boxci.project_path do
              run "git submodule add -f git@github.com:alup/puppet-rbenv.git puppet/modules/rbenv"
              run "git submodule update --init"
            end
          end
          template "templates/languages/#{Boxci.project_config.language}/main.pp", File.join(Boxci.project_path, "puppet/manifests/main.pp")
        end
      end
    end
  end
end

Optionally implement before_permutation_switch method

Optionally a language can also define commands that should happen before a permutation switch in the test runner. This would be the case for example if you had projects that specify specific of versions of the languages, BoxCI - Choose runtime versions.

Implementing this in the lib/boxci/languages/ruby.rb file would look as follows.

module Boxci
  module Languages
    class Ruby < ::Boxci::Language

      no_commands do
        def default_script
          %q{bundle exec rake}
        end

        def before_permutation_switch
          %q{export RAILS_ENV=test}
        end

        def generate_starter_puppet_manifest
          @project_config = Boxci.project_config
          if @project_config.rbenv
            inside Boxci.project_path do
              run "git submodule add -f git@github.com:alup/puppet-rbenv.git puppet/modules/rbenv"
              run "git submodule update --init"
            end
          end
          template "templates/languages/#{Boxci.project_config.language}/main.pp", File.join(Boxci.project_path, "puppet/manifests/main.pp")
        end
      end
    end
  end
end

In the above example in the before_permutation_switch I have set the RAILS_ENV environment variable to a value of test. I did this because in the case of Rails apps we specifically want to make sure that we are in the test environment. In the case of non-Rails ruby projects having RAILS_ENV set does not hurt anything.

Optionally implement after_permutation_switch method

Optionally a language can also define commands that should happen after a permutation switch in the test runner. This would be the case for example if you had projects that specify specific versions of the languages, BoxCI - Choose runtime versions.

Implementing this in the lib/boxci/languages/ruby.rb file would look as follows.

module Boxci
  module Languages
    class Ruby < ::Boxci::Language

      no_commands do
        def default_script
          %q{bundle exec rake}
        end

        def before_permutation_switch
          %q{export RAILS_ENV=test}
        end

        def after_permutation_switch
          %q{gem install bundler && bundle install --without production}
        end

        def generate_starter_puppet_manifest
          @project_config = Boxci.project_config
          if @project_config.rbenv
            inside Boxci.project_path do
              run "git submodule add -f git@github.com:alup/puppet-rbenv.git puppet/modules/rbenv"
              run "git submodule update --init"
            end
          end
          template "templates/languages/#{Boxci.project_config.language}/main.pp", File.join(Boxci.project_path, "puppet/manifests/main.pp")
        end
      end
    end
  end
end

In the above example in the after_permutation_switch I have it making sure that Bundler is installed and install the projects dependencies using Bundler. I did this because I am making an opinionated decision here that anyone developing Ruby or Ruby on Rails apps that is using BoxCI will be using Bundler.

Verify boxci test works properly

At this point you should have a fully functioning language integrated into BoxCI and you should be able to test it through the boxci init, boxci build and now with the boxci test command to make sure everything is working.

Overview of Result

If you have made it this far then you should have a properly functioning language added to BoxCI ready to make pull request to get it added to be peer reviewed and added to the mainline repository. To recap the actual files we created and their end resulting content/changes we have the following.

lib/boxci/languages/ruby.rb

module Boxci
  module Languages
    class Ruby < ::Boxci::Language

      no_commands do
        def default_script
          %q{bundle exec rake}
        end

        def before_permutation_switch
          %q{export RAILS_ENV=test}
        end

        def after_permutation_switch
          %q{gem install bundler && bundle install --without production}
        end

        def generate_starter_puppet_manifest
          @project_config = Boxci.project_config
          if @project_config.rbenv
            inside Boxci.project_path do
              run "git submodule add -f git@github.com:alup/puppet-rbenv.git puppet/modules/rbenv"
              run "git submodule update --init"
            end
          end
          template "templates/languages/#{Boxci.project_config.language}/main.pp", File.join(Boxci.project_path, "puppet/manifests/main.pp")
        end
      end
    end
  end
end

lib/boxci.rb

require 'boxci/languages/ruby'

lib/boxci/templates/languages/ruby/main.pp

exec { "apt-update":
  command => "/usr/bin/apt-get update"
}

Exec["apt-update"] -> Package <| |>

package {
  "rbenv":
  ensure => present,
  require => Exec['apt-update'],
}

rbenv::install { $vagrant_user:
  home => $vagrant_home,
  require => [Package['rbenv']],
}
<% if @project_config.rbenv %>
<% @project_config.rbenv.each do |ruby_version| -%>

rbenv::compile { "$vagrant_user/<%= ruby_version -%>":
  user => $vagrant_user,
  home => $vagrant_home,
  ruby => '<%= ruby_version -%>',
  require => Rbenv::Install["$vagrant_user"],
}
<% end -%>
<% end %>

Thats it. Not much code at the end of the day.