Bozo
Bozo is a build system written in Ruby. It is designed to be rigid yet extensible.
Steps
There is a fixed set of steps, they are currently:
- Clean
 - Resolve dependencies
 - Prepare
 - Compile
 - Test
 - Package
 - Publish
 
The steps are sequential but you can run up to any of them. For example bozo compile executes the clean, dependencies, prepare and compile steps
whereas bozo dependencies only executes the clean and dependencies
steps.
Bozo is a framework that provides a skeleton which can be populated by custom runners and hooks. Bozo itself provides no runners or hooks a reference project for runners and hooks can be found in the bozo-scripts project.
Each step allows several runners to execute, for example you may run RSpec
unit tests followed by Cucumber integration tests within the scope of the
test step. Each step, along with the entire build, also expose pre- and
post-step hooks.
Configuration
A bozo build is configured through a single Ruby file, by convention this
should be bozorc.rb located at the root of your project.
Bozo makes use of a VERSION file in the root directory of the project. Versions can be specified in whatever format is required but a string in the format [major].[minor].[point] is generally expected.
Conventions
Dependency resolvers must be defined within the Bozo::DependencyResolvers
module, project preparers must be defined within the Bozo::Preparers
module, compilers must be defined within the Bozo::Compilers module, test
runners must be defined within the Bozo::TestRunners module, packagers must
be defined within the Bozo::Packagers, publishers must be defined within
the Bozo::Publishers module and hooks, regardless of the steps they relate
to, must be defined within the Bozo::Hooks module.
Runners are specified by convention with the relevant module being inspected
for a matching class definition. For example, the configuration compile_with :msbuild will resolve to the class definition Bozo::Compilers::Msbuild.
The symbol provided will be converted to Pascal Case prior to resolution. For
example, the configuration pre_compile :common_assembly_info will resolve
to the class definition Bozo::Hooks::CommonAssemblyInfo.
Each runner and hook must provide a parameterless constructor and Bozo will invoke that constructor before registering and passing the instance to any block provided as part of the configuration. A runner or hook should be able to run with the default configuration whenever possible with customizations being provided through the block:
test_with :nunit do |n|          # Creates and registers a new Bozo::TestRunners::Nunit instance
  n.project 'Project.Tests'      # Adds additonal configuration to the instance
endIf there are several runners for the same step then they will be executed in the order the are specified within the configuration.
As soon as one runner or hook raises an error through either failing to execute a command successfully or some custom condition then the build is aborted.
Configuration example
The exact syntax is still a work in progress though the concepts will remain the same.
require 'bozo_scripts'            # Makes custom runners and hooks available
prepare :common_assembly_info     # Defines that the common assembly info should be prepared for the project
compile_with :msbuild             # Defines that the project should be compiled with the `msbuild` compiler
test_with :nunit do |n|           # Defines that the project should be tested with the `nunit` test runner
  n.project 'Project.Tests'       # Runner specific configuration - in this case defining the assemblies to run
end
package_with :nuget do |p|        # Defines that the project should be packaged with `nuget`
  p.project 'Project'             # Runner specific configuration - in this case the projects to package
  p.project 'Project.Testing'
end
resolve_dependencies_with :nuget  # Defines that project dependencies should be resolved with `nuget`
with_hook :git_commit_hashes      # Defines that the `git_commit_hashes` hook should be executed with the build
with_hook :timing                 # Defines that the `timing` hook should be executed with the build
build_tools_location '//SERVER/network/path' # Defines the location build tools can be copied fromCreating step runners and hooks
Both step runners and hooks have their nuances which are covered in their
dedicated sections. However, both are extended by the Bozo::Runner module
that makes a collection of methods available to them.
build_configuration
Returns the Bozo::Configuration of the build.
build_server?
Returns true when the build is being run with the --build-server switch,
otherwise false.
This is a shortcut for global_params[:build_server].
pre_release?
Returns true when the build is being run with the --pre-release switch,
otherwise false.
This is a shortcut for params[:pre_release].
env
Returns the hash of environment variables. Initially populated by calling
ENV.to_hash this may be added to by runners and hooks to enable lightweight
communication and to cache the result of expensive calls.
environment
Returns the name of the environment that the build is running in, eg.
'development'.
This is a shortcut for global_params[:environment].
execute_command(tool, args)
Executes a command line tool.
Raises a Bozo::ExecutionError if the command responds with a non-zero exit
code.
Parameters
- tool [Symbol] A friendly identifier for the tool
 - args [Array] An array of arguments making up the command to execute
 
global_params
Returns the hash of global parameters passed to bozo. All key symbols are
converted from the CLI style of :'multi-word' to :multi_word to be more
idiomatic for Ruby.
log_debug(msg)
Records an debug log message.
Parameters
- msg [String] The message to log
 
log_fatal(msg)
Records an fatal log message.
Parameters
- msg [String] The message to log
 
log_info(msg)
Records an info log message.
Parameters
- msg [String] The message to log
 
log_warn(msg)
Records an warn log message.
Parameters
- msg [String] The message to log
 
params
Returns the hash of command parameters passed to bozo. All key symbols are
converted from the CLI style of :'multi-word' to :multi_word to be more
idiomatic for Ruby.
version
Returns the version of the build.
This is a shortcut for build_configuration.version.
Creating step runners
The structure of all runners is the same. They must be defined within the
appropriate module, dependency resolvers in the Bozo::DependencyResolvers
module, project preparers must be defined within the Bozo::Preparers
module, compilers in theBozo::Compilers module, test runners in the
Bozo::TestRunners module, packagers in the Bozo::Packagers module and
publishers in the Bozo::Publishers module. They must have a parameterless
constructor and they must expose an #execute method which will be invoked
when they should execute whatever task they are meant to perform. They can
optionally define a #required_tools method which returns the name of any
build tools it requires that cannot be retrieved through dependency
resolvers, for example a dependency resolving executable such as nuget.exe.
When executing a command line executable they should use the
execute_command(tool, args) method so that the command will be logged in if
the correct format and if executable completes with an error exit code the
build will be aborted. They should also use the log_info(msg) and
log_debug(msg) methods to ensure their output is formatted correctly and
the verbosity of the messages can be controlled centrally.
The runner will be passed back to the configuration code via an optional
block so if further configuration of the runner is possible, or required,
this should be exposed through public methods on the runner. If required
configuration is omitted then a Bozo::ConfigurationError with a message
explaining the problem and how to rectify it should be raised when the
#execute method of the runner is called.
Registration
Runners are registered through step-specific methods:
- 
dependency_resolver(identifier, &block)registers dependency resolvers - 
prepare(identifier, &block)registers project preparers - 
compile_with(identifier, &block)registers compilers - 
test_with(identifier, &block)registers test runners - 
package_with(identifier, &block)registers packagers - 
publish_with(identifier, &block)registers publishers 
Example
Here is an example of a 'compiler' that logs "Hello, <name>!" where name is
configured from the optional block and a Bozo::ConfigurationError is raised
if no name has been configured:
module Bozo::Compilers
  class HelloWorld
    def name(name)
      @name = name
    end
    def execute
      raise Bozo::ConfigurationError.new('You must specify a name to say "Hello" to') if @name.nil?
      log_info "Hello, #{@name}!"
    end
  end
endThis compiler would be added to your build via the configuration:
compile_with :hello_world do |hw|
  hw.name 'Bozo'
endCreating hooks
The structure of all hooks is the same. The must be defined within the
Bozo::Hooks module and they must have a parameterless constructor. They can
optionally define a #required_tools method which returns the name of any
build tools it requires that cannot be retrieved through dependency resolvers,
for example a dependency resolving executable such as nuget.exe.
When executing a command line executable they should use the
execute_command(tool, args) method so that the command will be logged in if
the correct format and if executable completes with an error exit code the
build will be aborted. They should also use the log_info(msg) and
log_debug(msg) methods to ensure their output is formatted correctly and the
verbosity of the messages can be controlled centrally.
The hook will be passed back to the configuration code via an optional block
so if further configuration of the hook is possible, or required, this should
be exposed through public methods on the hook. If required configuration is
omitted then a Bozo::ConfigurationError with a message explaining the
problem and how to rectify it should be raised when a hook method is called.
A hook can be called several times. In order to hook around a step all that is required is that an appropriately named method is defined within the class. For example, this hook logs a message both before and after the compile step is run:
module Bozo::Hooks
  class CompilingMessages
    def pre_compile
      log_info 'About to compile'
    end
    def post_compile
      log_info 'Finished compiling'
    end
  end
endWhich steps the hook wants to execute on is determined by checking the
response to the #respond_to? method so if you wish to use #method_missing
to add functionality you need to ensure that the response to #respond_to?
reflects that.
Registration
As hook instances can listen to one or more pre- or post-stage hooks there are multiple ways to register a hook. However, they are all functionally identical and are just aliases to the same method so that your configuration can read more clearly.
The registration methods are:
- 
with_hook(identifier, &block)(recommended when hooking several stages) pre_build(identifier, &block)post_build(identifier, &block)pre_clean(identifier, &block)post_clean(identifier, &block)pre_dependencies(identifier, &block)post_dependencies(identifier, &block)pre_prepare(identifier, &block)post_prepare(identifier, &block)pre_compile(identifier, &block)post_compile(identifier, &block)pre_test(identifier, &block)post_test(identifier, &block)pre_package(identifier, &block)post_package(identifier, &block)pre_publish(identifier, &block)post_publish(identifier, &block)
Failed hooks exist that are called when a stage fails, in these cases the
relevant post hook is not called.
failed_build(identifier, &block)failed_clean(identifier, &block)failed_dependencies(identifier, &block)failed_prepare(identifier, &block)failed_compile(identifier, &block)failed_test(identifier, &block)failed_package(identifier, &block)failed_publish(identifier, &block)
Build tools
Build tools are usually executables that you need to perform a task that are not available via some other means.
For example, at Zopa we use in Nuget to resolve our .NET dependencies. This is a chicken and egg situation in that you can't use a dependency management system like Nuget until you've got a copy of the Nuget executable you can call. The build tools function aims to resolve this loop of cyclical dependency.
Your build tools are resolved as the first part of the "resolve dependencies" step. When possible you should use real package management systems to retrieve dependencies rather than using the build tools function.
Specifying required build tools
All the runners and hooks you create can optionally specify a required_tools
method which returns the name of one or more required build tools:
module Bozo::DependencyResolvers
  class Nuget
    def required_tools
      :nuget # or for many [:nuget, :open_wrap]
    end
  end
endTools that aren't required_tools of another runner can be specified using the following:
required_tool :nuget
required_tool :tool_with_configuration do |n|
  n.tool_version '1.0'
endIn the case of the configuration required_tool method only the Bozo::Tools module is used.
How it works
There are two ways which tools may be resolved when required by another module, either
from the build_tools_location or via a Bozo::Tool module class. A class in the Bozo::Tool module
takes priority over the build_tools_location, if Bozo fails to find a class with the same name then
the build_tools_location is used.
Within the example configuration there is a single line:
build_tools_location '//SERVER/network/path' # Defines the location build tools can be copied fromThis specifies the location that build tools should be retrieved from. This
location is then joined with the name of the build tool to find the directory
that must be copied into the ./build/tools directory. For example with a
build_tools_location of //SERVER/network/path along with a required
build tool called :nuget will result in the directory
//SERVER/network/path/nuget being copied to ./build/tools/nuget directory.
By knowing the contents of this directory you can then invoke the executables
contained within it:
module Bozo::DependencyResolvers
  class Nuget
    def execute
      execute_command :nuget, File.join('build', 'tools', 'nuget', 'NuGet.exe')
    end
  end
end