Skip to content
This repository is currently being migrated. It's locked while the migration is in progress.

Latest commit

 

History

History
444 lines (314 loc) · 14.8 KB

README.md

File metadata and controls

444 lines (314 loc) · 14.8 KB

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:

  1. Clean
  2. Resolve dependencies
  3. Prepare
  4. Compile
  5. Test
  6. Package
  7. 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