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
end
If 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 from
Creating 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
end
This compiler would be added to your build via the configuration:
compile_with :hello_world do |hw|
hw.name 'Bozo'
end
Creating 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
end
Which 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
end
Tools 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'
end
In 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 from
This 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