# 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. Compile 4. Test 5. Package 6. Publish The steps are sequential but you can run up to any of them. For example `bozo compile` executes the `clean`, `dependencies` 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](https://github.com/zopaUK/bozo-scripts/). 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, 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: ```ruby 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. ```ruby require 'bozo_scripts' # Makes custom runners and hooks available pre_compile :common_assembly_info # Adds a hook at the pre-compile stage 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]`. ### 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, compilers in the`Bozo::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 * `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, !"` where name is configured from the optional block and a `Bozo::ConfigurationError` is raised if no name has been configured: ```ruby 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: ```ruby 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: ```ruby 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_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)` ## 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 resolve 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: ```ruby module Bozo::DependencyResolvers class Nuget def required_tools :nuget # or for many [:nuget, :open_wrap] end end end ``` ### How it works Within the example configuration there is a single line: ```ruby 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: ```ruby module Bozo::DependencyResolvers class Nuget def execute execute_command :nuget, File.join('build', 'tools', 'nuget', 'NuGet.exe') end end end ```