Last update: 2013-09-12

Developer Guide

This documentation is intended for people who want to develop new packages for Bee. A package is made of tasks and eggs. Tasks may be compared to functions that run in build files. Eggs generate new projects interactively with the user and are similar to Rails Scaffoldings.

Template Project

The fast way to start developing a new package is to run the package egg with following command line:

bee -t package

This will ask you the package name:

$ b -t package
--------------------------------------------------------------------- welcome --
This script will create a project for a bee package with sample task (in
'lib' directory) and template (in 'egg' directory). A unit test for the
task is generated in 'test' directory. Generated build file has a target
'test' to run unit tests, a target 'gem' to generate a gem archive, a
'clean' target to clean generated files and a 'install' target to build
and install gem.
---------------------------------------------------------------------- prompt --
Please answer following questions to generate the project:
What is the project's name? [bee_hello]:

Here, accept default name (bee_hello) pressing ENTER. A new directory named bee_hello is created in current directory that contains the generated project. Sample task is generated in file lib/bee_task_hello.rb and is the source for following task sample. Sample egg is generated in the egg subdirectory.

Writing Bee Tasks

If you think that your preferred piece of Ruby code you run in Bee is of interest for other users, you should probably make it a Bee task. This is a way to make it easy to run and distribute to other users.

Let's say you decide to write a Bee task from your greeting script. This must be written in a script named bee_task_hello.rb, where hello will be the package of your task. This naming scheme that starts with bee_task followed by the package name is necessary so that Bee is able to locate and load this script when a task with package hello is called in a build file.

This package will contain a single task named hello that will greet a user which name is passed as argument to the task. You could write this script as follows:

require 'bee_task_package'

module Bee
  
  module Task
  
    # Package for Hello tasks.
    class Hello < Package
    
      # Sample hello task that prints greeting message on console.
      # 
      # - who: who to greet.
      # 
      # Example
      # 
      #  - hello.hello: "World"
      def hello(who)
        puts "Hello #{who}!"
      end

    end

  end

end

Your code must be embedded in a class Hello (because of the name you choose for your package) in Bee::Task module. This class must inherit Bee::Task::Package to be recognized as a Bee task.

In this class, you will declare a method named hello, the named of the task you want to expose. It will take the name you want to say hello to. These methods that code a given task must always take a single parameter that is the object resulting from the YAML parsing of task parameters. Thus, this parameter may be a string, a list, a hash or any other YAML type, depending on your task parameters. These parameters are processed before calling these methods so that property references are replaced with their values, but its up to you to check that parameter types are correct and output an appropriate error message if this is not the case. Note that you may code more than one task in a given package.

You may call this task with following build file:

- properties:
    who: World

- target: hello
  script:
  - hello.hello: "#{who}"

Which will output, as expected:

$ bee hello
----------------------------------------------------------------------- hello --
Hello World!
OK

Of course, you must put the Ruby script for your task somewhere in your Ruby path so that it can be found. The best way to distribute a Bee task is probably building a Ruby Gem. This is done by the build file generated by package egg, running target gem. This build file is also able to run unit tests in the generated test/tc_bee_task_hello.rb by running target test.

Tasks are self documented. To get help about a task foo.bar, simply type bee -k foo.bar and you get this task documentation on the terminal. This works also for tasks you write. Simply write this documentation as the method help. Ruby will parse this comment and print it on the standard output. This help should always describe your task parameters and provide a short sample, as in the example we've seen above. Install generated gem for the template project, typing bee install in the template project directory. When typing bee -k hello.hello, you get the following help page:

$ bee -k hello.hello
----------------------------------------------------------------- hello.hello --
Sample hello task that prints greeting message on console.

- who: who to greet.

Example

 - hello.hello: "World"

This feature is quite convenient for people who write build files and thus tasks coders should take care of properly documenting their tasks.

Writing Bee Eggs

An egg is a script that the user launches to generate interactively a new project. This is very handy to quickly have a running project to start working with. We've seen above that there is an egg to generate a template project for a Bee packages.

In a nutshell, eggs are Bee build files that ask the user for pieces of information, such as the project name, and generate the template in a subdirectory.

Let's say we have just discovered the power of Ruby Gems and want to distribute our scripts packaged as ready to install gems. We could write an egg to generate template projects to start with.

The first thing to do is to write a sample project that we will package and customize in our egg. This sample project, named hello might look like this:

hello
 |
 +- build.yml
 |
 +- bin
 |   |
 |   +- hello
 |
 +- lib
     |
     +- hello.rb

Our script is hello.rb in the lib directory:

#!/usr/bin/env ruby

# Greet a given person.
# - who: the person to greey.
def hello(who)
  return "Hello #{who}!"
end

# command line help
HELP = 'hello [-h] who ...
-h      To print this help screen
who     The person(s) to greet'

# parse command line arguments
require 'getoptlong'
opts = GetoptLong.new(['--help', '-h', GetoptLong::NO_ARGUMENT])
begin
  opts.each do |opt, arg|
    case opt
    when '--help'
      puts HELP
      exit
    end
  end
rescue
  puts HELP
  exit
end
for who in ARGV
  puts hello(who)
end

To start this script as a gem, we need a launching script named hello in bin directory. This script might look like this one:

#!/usr/bin/env ruby
# Launching script.

$:.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))

load 'hello.rb'

Now we need a build file to package our gem. This build file has targets to generate the gem archive, install and uninstall it and to clean generated files. The following one will do the job:

- build: hello
  description: "Project to generate a gem for a Ruby script"
  default: all

- properties:
    name: hello
    version: 0.0.1
    build: build
    gem_spec: "#{build}/gem_spec"
    gem_erb: |
      require 'rubygems'
      remove_const(:SPEC) if defined?(SPEC)
      SPEC = Gem::Specification.new do |spec|
        spec.name = '<%= name %>'
        spec.version = '0.0.1'
        spec.platform = 'ruby'
        spec.summary = 'summary'
        spec.author = 'author'
        spec.email = 'email'
        spec.homepage = 'homepage'
        spec.rubyforge_project = 'rubyforge project'
        spec.require_path = 'lib'
        spec.files = Dir.glob('{bin,lib}/*')
        spec.has_rdoc = true
        spec.executables = ['<%= name %>']
      end

- target: gem
  description: "Generate gem archive"
  script:
  - mkdir: :build
  - erb:
      source: :gem_erb
      dest:   :gem_spec
  - gem: :gem_spec
  - mv:
      src:  "*.gem"
      dest: :build

- target: install
  depends: gem
  description: Install generated gem
  script:
  - "sudo gem install #{build}/#{name}-#{version}.gem"

- target: uninstall
  description: Uninstall gem
  script:
  - "sudo gem uninstall -x #{name}"

- target: clean
  description: Clean generated files
  script:
  - rmdir: :build

- target: all
  depends: [clean, gem, install]
  description: Generate and install the gem

Now that we have a sample project, we must turn it into a template, which means add parameters to customize it. Our script will stay the same except that we will replace the file name with the project's name along with an .rb extension while generating the target project. The launching script must be customized so that the name of the file to load matches the script name. We can do so making this file an ERB file (Embedded Ruby, that is a file containing Ruby expressions embedded in <%= expression %> tags). This ERB might be:

#!/usr/bin/env ruby
# Launching script.

$:.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))

load '<%= project_name %>.rb'

While processing this file, the expression <%= project_name %> will be replaced with the value of project_name Ruby variable, which in turn will have the value of property project_name of our build file.

Our build file must be customized the same way:

- build: <%= project_name %>
  description: "Project to generate a gem for a Ruby script"
  default: all

- properties:
    name: <%= project_name %>
    version: 0.0.1
    build: build
    gem_spec: "#{build}/gem_spec"
    gem_erb: |
      require 'rubygems'
      remove_const(:SPEC) if defined?(SPEC)
      SPEC = Gem::Specification.new do |spec|
        spec.name = '<%= name %>'
        spec.version = '0.0.1'
        spec.platform = 'ruby'
        spec.summary = 'summary'
        spec.author = 'author'
        spec.email = 'email'
        spec.homepage = 'homepage'
        spec.rubyforge_project = 'rubyforge project'
        spec.require_path = 'lib'
        spec.files = Dir.glob('{bin,lib}/*')
        spec.has_rdoc = true
        spec.executables = ['<%= name %>']
      end

- target: gem
  description: "Generate gem archive"
  script:
  - mkdir: :build
  - erb:
      source: :gem_erb
      dest:   :gem_spec
  - gem: :gem_spec
  - mv:
      src:  "*.gem"
      dest: :build

- target: install
  depends: gem
  description: Install generated gem
  script:
  - "sudo gem install #{build}/#{name}-#{version}.gem"

- target: uninstall
  description: Uninstall gem
  script:
  - "sudo gem uninstall -x #{name}"

- target: clean
  description: Clean generated files
  script:
  - rmdir: :build

- target: all
  depends: [clean, gem, install]
  description: Generate and install the gem

Now we have all necessary pieces and just have to write a build file to put them together. We will name this build file after the template name (which suggests what it does). In this example, we'll name it hello. Thus this build file will be named hello.yml and we will put all associated file within a directory named hello. All egg files live in a directory named egg in the package directory. This convention must be implemented so that Bee is able to find a given package in a gem named bee_<package> and the build file to generate the template in a directory egg/<name>.yml. Thus, the structure of the directory of our example package is the following:

bee_<package>
 |
 +- egg
     |
     +- hello.yml
     |
     +- hello
         |
         +- build.erb
         |
         +- launcher.erb
         |
         +- script.rb

The build file to generate the template might be:

- build: hello
  default: all
  description: "Generate a project for a Ruby script"

- properties:
    project_name: hello
    description: |
      This script will create a project for a Ruby script that might be
      distributed as a gem. Generated build file has a single target gem
      to generate the distribution gem.

- target: welcome
  description: "Print information message"
  script:
  - print: :description

- target: prompt
  depends: welcome
  description: "Prompt for project information"
  script:
  - print: "Please answer following questions to generate the project:"
  - prompt:
      message: "What is the project's name?"
      default: :project_name
      property: project_name

- target: generate
  depends: prompt
  description: "Generate project"
  script:
  - print: "Generating project..."
  - rb: |
      error "A directory named '#{project_name}' already exists, aborting" if
        File.exists?("#{here}/#{project_name}")
      name = "<%= name %>"
  - mkdir: "#{here}/#{project_name}"
  - mkdir: "#{here}/#{project_name}/bin"
  - mkdir: "#{here}/#{project_name}/lib"
  - erb:
      src:  "#{base}/hello/build.erb"
      dest: "#{here}/#{project_name}/build.yml"
  - cp:
      includes: "#{base}/hello/script.rb"
      dest:     "#{here}/#{project_name}/lib/#{project_name}.rb"
  - erb:
      src:  "#{base}/hello/launcher.erb"
      dest: "#{here}/#{project_name}/bin/#{project_name}"

- target: customization
  depends: generate
  description: "Print information about project customization"
  script:
  - print: |
      Project has been generated in directory '#{project_name}'. Type 'bee -b'
      to print information about generated build file. Enjoy!

- target: all
  depends: [welcome, prompt, generate, customization]

This build file defines two properties: project_name and description which are quiet self descriptive. Property description is used to describe egg in command line help, thus it should always be defined so that users may obtain a description of the purpose of your eggs.

First target, named welcome, simply prints the description of the egg. Second target, named prompt, will ask the user for the project's name. Target generate will generate template project, copying the script and processig the launching script and the build file using ERB, which will inject project's name into these scripts. Note that before generating template project we check that it's directory doesn't already exists to prevent an accident, overwriting an existing directory. Last target, named customization prints a closing message to the user.

The best way to test our egg is to go in the egg directory and launch the build file typing bee -f hello.yml, to start our egg's build file. This would produce:

$ bee -f hello.yml 
--------------------------------------------------------------------- welcome --
This script will create a project for a Ruby script that might be
distributed as a gem. Generated build file has a single target gem
to generate the distribution gem.
---------------------------------------------------------------------- prompt --
Please answer following questions to generate the project:
What is the project's name? [hello]:
test
-------------------------------------------------------------------- generate --
Generating project...
Creating directory 'test'
Creating directory 'test/bin'
Creating directory 'test/lib'
Processing ERB 'hello/build.erb'
Copying 1 file(s) to 'test/lib/test.rb'
Processing ERB 'hello/launcher.erb'
--------------------------------------------------------------- customization --
Project has been generated in directory 'test'. Type 'bee -b'
to print information about generated build file. Enjoy!
------------------------------------------------------------------------- all --
OK

As a result, we have a brand new customized template project in directory test (the name for the generated template we entered on prompt). This output is the one your user will see running your egg typing bee -t hello.hello.

Now that you have written your package hello, you can distribute it as a Ruby Gem. To generate the gem, type bee gem in your package directory. If your gem is distributed using RubyForge, your users can install this package with:

sudo gem install bee_hello

Else, you can distribute this gem and your users can install it locally typing:

sudo gem install --local build/bee_hello-0.0.1.gem

Then, they will be able to use your new tasks in their build files and generate template projects typing bee -t hello.hello.

Enjoy!