A closer look at bake: a tool that makes building C/C++ code effortless

By Sander Mertens

It has been two weeks since I introduced bake to the world (https://github.com/SanderMertens/bake). The release sparked lively discussions on reddit and HackerNews. Because there is only so much that can be explained in a comment thread, I wrote this blogpost to lay out the main reasons why users love bake, and why I created it in the first place.

JSON Configuration. The first thing you’ll notice in a bake project is the project.json file. Here is an example project with three dependencies:

{
"id": "apple.pie",
"type": "application",
"value": {
"use": ["apple", "sugar", "cinnamon"]
}
}

Picking a configuration language was not easy. There is no commonly used language for build tools, and discussions on what is the best language can get, well, heated. I ultimately decided on JSON, not because I’m a big fan of the language, but because:

  • everyone knows how to read & write it
  • it is easy to parse by any programming language
  • I did not want to invent a new language (CMake) or use a full programming language (Premake, Rake)
  • it has been successfully used for a similar use case by NPM

People have expressed concern about how JSON can get unwieldy for complex projects. Bake however has a mechanism that keeps your project configurations from getting complex: configuration encapsulation.

With configuration encapsulation, a complex configuration like this:

{
"id": "apple.pie",
"type": "application",
"lang.c": {
"${os linux}": {
"lib": ["some_lib"],
"libpath": ["/opt/some_lib/lib"],
"include": ["/opt/some_lib/include"]
}
}
}

Can be turned into this:

{
"id": "apple.pie",
"type": "application",
"value": {
"use": ["some_lib"]
}
}

For more information on how configuration encapsulation works, see: https://github.com/SanderMertens/bake#can-i-link-with-non-bake-libraries

Project discovery. The second reason people love bake, is because it recursively discovers projects in the directory where it is invoked, and build them in the correct dependency order. This makes it really easy to share multiple projects with other developers. All they have to do is call bake:

$ ls
apple apple-pie cinnamon sugar
$ bake
build 'apple' in 'apple'
build 'cinnamon' in 'cinnamon'
build 'sugar' in 'sugar'
build 'apple.pie' in 'apple-pie'

Even if a project has dependencies that are not discovered by bake in the current directory, bake can still find the project if it was built before. This is because projects are built to the bake environment. This is a location on your hard drive (by default$HOME/bake) where bake stores all built packages.

Bake lets you easily locate a project in the bake environment:

$ bake locate apple.pie
A apple.pie => ~/bake/x64-darwin-debug/bin/apple_pie

You can also easily inspect the packages available in the bake environment:

$ bake list
P apple => BAKE_TARGET
A apple.pie => BAKE_TARGET
P bake.lang.c => BAKE_HOME
P bake.lang.cpp => BAKE_HOME
P bake.util => BAKE_HOME
P cinnamon => BAKE_TARGET
P sugar => BAKE_TARGET

Or uninstall a package from the environment:

$ bake uninstall apple.pie
uninstalled 'apple.pie'

The great thing about the bake environment is that it is all local, so you do not need root privileges to install packages before they can be discovered.

Interactive building. When you quickly want to see the results of code changes on, for example, a web page, going back and forth between the code editor, command line, and browser can be annoying. Instead, bake lets you run projects in interactive mode, which automatically rebuilds a project when you’ve made a change to the code:

$ bake run apple.pie --interactive
ready 'apple.pie'
run 'apple.pie' [169] (~/apple-pie/bin/x64-darwin-debug/apple_pie)
detected change in file ~/apple-pie/src/main.c'
build 'apple.pie' in '~/apple-pie'
restart 'apple.pie' (1x)
run 'apple.pie' [248] (~/apple-pie/bin/x64-darwin-debug/apple_pie)

Currently interactive mode is only used as a development tool. We have some exciting plans for it though, where it can be put to use as a simple service runner that automatically restarts when a dependency is updated!

Automatic header file inclusion. For large projects with lots of dependencies, it can be a pain to manually include all the headers of dependencies, and keep track of where they are. This gets even more tedious when code is refactored and project names are changing, or projects are split up or combined.

Bake users don’t have to think about including the right header files. Based on the dependency list in your project.json it can automatically generate a list of #include statements for your project. It stores this list in a file called bake_config.h which you can then simply include in your project. It seems like a small thing, but it makes it really hard to go back once you’re used to it.

This is what the include section looks like for the apple.pie project:

/* Headers of public dependencies */
#include <apple>
#include <sugar>
#include <cinnamon>

You’ll note that the include statements use the logical package names of the dependency projects. This is due to how the way include files are stored in the bake environment, and even works for nested packages, like hello.world

#include <hello.world>

The only thing your project needs to do to take advantage of this feature, is have a “main” header file with the short name of your project. For example, the apple.pie project should have a file include/pie.h. If your project does not have one, bake will simply ignore it for the include list in bake_config.h.

Git integration. Git is one of the most important technologies in a developer toolset, and bake integrates seamlessly into the git workflow. First of all, bake automatically initializes new bake projects as git repositories:

$ bake init apple.pie
Created new application with id 'apple.pie'
$ cd apple-pie
$ git status
On branch master
No commits yet
Untracked files:
(use "git add <file>..." to include in what will be committed)
include/
project.json
src/
nothing added to commit but untracked files present (use "git add" to track)

This lets you immediately use the project with git, like importing it into the GitHub desktop tool, or keeping track of changes using commits.

Secondly, bake makes it really easy to clone bake repositories, and their dependencies. All you have to do is provide the git URL to bake, and it will clone and build the entire project:

$ bake clone SanderMertens/apple-pie 
clone 'https://github.com/SanderMertens/apple-pie'
clone 'https://github.com/SanderMertens/apple'
clone 'https://github.com/SanderMertens/sugar'
clone 'https://github.com/SanderMertens/cinnamon'
build 'apple' in 'apple'
build 'cinnamon' in 'cinnamon'
build 'sugar' in 'sugar'
build 'apple.pie' in 'apple-pie'

Currently this only works when the dependencies of your project can be found under the same git URL. Bake does not rely on its own server infrastructure, and we would like to keep it that way. Having said that, we are looking at mechanisms to easily allow for cross-URL dependencies.

Lastly, bake automates the creation of git tags whenever you release a new version of your project. If you want to release a new minor version, just do:

$ bake publish minor
This command creates a new tag. Commit all your changes before continuing!
Proceed? [y/N] y
[master (root-commit) b4ceef9] Published version 0.1.0"
1 file changed, 11 insertions(+)
create mode 100644 project.json
OK Committed version '0.1.0'
OK Created tag 'V0.1.0'
OK Published apple.pie:0.1.0

Environment configuration. Often when working with 3rd party frameworks, you may need to set environment variables before you can build or run the code. Having to do this repeatedly every time you instantiate a new terminal is annoying.

Bake lets you add environment variables once to a bake environment configuration. Those environment variables will then be applied every time you do a bake build, or run a process with bake. To export an environment variable to bake, simply do:

bake export MYVAR=some_value

This exports the environment variable MYVAR to the default environment. To see what is in the default environment, you can just do:

bake env
MYVAR=some_value
...

Bake lets you manage different environments at the same time. To only export an environment variable to the “bakery” environment, do:

bake export HAS_PIES=true --env bakery

If you want to export the environment variables from the bakery environment to your terminal, you can do that with the export command:

export $(bake env --env bakery)

This is only a small taste of what bake can do, and we plan to add even more exciting features like test framework integration, doxygen integration, code coverage and build dashboard integration.

Hopefully this gave you a better idea of what bake does, why people like it and why we built it. While there is a plethora of build tools out there, we felt like there wasn’t one that provided the level of user experience for C/C++ that tools like NPM and cargo offer. With bake, we got a little closer to that goal.

Thanks for reading! If you want to check out bake, the GitHub repository is here: https://github.com/SanderMertens/bake. If you like what you see, consider giving it a star, or better yet, help us make bake better by contributing issues and pull requests!