Introduction to Cook

Welcome to the introductory blog post about Cook—a new build system. You are probably asking yourself: Why do we need yet another build system? As of now, Wikipedia lists close to 50 candidates.

Let’s start with one simple observation: All build systems suck. While some may suck less, there is not a single one that’s really great. And let’s be honest: Cook won’t change this. There are just too many aspects about building software, which makes building that one tool that fits all purposes very difficult.

Note: Cook is currently in version 0.2.0—which means a lot of things still need some work. Contributors are very welcome! Please help in making this project a great build-system. Or just play a little bit around with the project using the examples. Everything is appreciated.

Motivation

Cook was created to address two specific problems, which are somehow related:

Don’t get me wrong: CMake and Bazel are really doing a good job. They are quite powerful, have support for project generation for many IDEs, integrated packaging and testing or scalability. It just happens that their goals do not align well with some people’s needs.

Cook currently has 21 source files with a total size of 0.073MB—including rules for C++, GIMP, LaTeX (currently not that good), LibreOffice and other miscellaneous rules. Extending it by writing a custom one is really easy, but let’s first take a look at how to use the built-in rules.

Hello World

Using Cook to compile a new C++ project is quite easy.

// File: hello.cpp
#include <iostream>

int main() {
    std::cout << "Hello world!" << std::endl;
}

The build-script looks like this:

# File: BUILD.py
from cook import cpp

cpp.executable(
    name='hello',
    sources=['hello.cpp']
)

We are importing the cpp package here since we want to build a c++ executable.

$ cook
[  0%] Compile main.cpp
[ 50%] Link build/hello
[100%] Done.

$ ./build/hello
Hello world!

Note: This works Linux and Windows. MacOS is currently untested but it should work as well.

Advanced Examples

Putting source files belonging to a certain module or library in their own shared folder is a common pattern. This structure is nicely represented using glob patterns, such as:

from cook import cpp, core

cpp.static_library(
    name='foo',
    sources=core.glob('*.cpp')
)

Sometimes you want to enable or disable certain features of your application during compilation, for example when having a lite and professional version. Cook allows declaring options of various types which can be set upon invocation. Also note that we are linking with some Boost libraries easily.

from cook import cpp, core

lite = core.option('lite', help='build lite version instead of professional')

cpp.executable(
    name='app',
    sources=core.glob('*.cpp'),
    define={
        'LITE': lite
    },
    links=['boost_regex', 'boost_utils']
)

Building the lite version is now performed using cook lite=1, while the professional version will be built as usual: cook. You can also list all available options:

$ cook --options
name       type  default    help
----------------------------------------------------------------------
lite       bool  True       build lite version instead of professional

Custom Rules

Here is a little snippet showing a rule which replaces occurrences of a string with another.

from cook import core

@core.rule
def replace(source, destination, mapping):
    source = core.resolve(source)
    destination = core.build(destination)

    yield core.publish(
        inputs=[source],
        message='Processing {}'.format(source),
        outputs=[destination],
        check=mapping
    )

    with open(source) as file:
        content = file.read()
    for key, value in mapping.items():
        content = content.replace(key, value)
    with open(destination, 'w') as file:
        file.write(content)

year = core.option('year', int, 1979, 'overwrite the year of publication')

replace(
    source='foo.txt',
    destination='out.txt',
    mapping={
        'meaning of life': 42,
        'author': 'Douglas Adams',
        'year': year
    }
)

Just a few words on how this works: core.resolve and core.build interpret the following path relative to the currently evaluated build-script respectively the build directory. The yield statement pauses execution of the rule—it will continue if and when the system decides it should. The mapping is passed using the check keyword argument. This will make the system look at the mapping and detect if it changed after the last run, because then it must be redone.

If you want to know more about creating custom rules, make sure to check out the docs.

Some Additional Features

Closing Words

We hope to have given you a brief overview about the project. This was in no way an article covering the whole spectrum of things to be said, but if we caught your interest, then you can take a look at the GitHub repository. While not all parts are great from a coding-style point of view yet, it is hopefully sufficiently polished to get you started. Feel free to open an issue or maybe even upload a pull request. Your help is very much appreciated.

If you need more information about Cook, make sure to read more about its features.