Me wearing ridiculous goggles

Simple Cobble Example

This will walk you through creating a small “hello world” program that builds with Cobble.

This tutorial assumes:

  1. That you are comfortable in C or willing to pretend.
  2. That you are on a Unix-like system with gcc installed.

Get Cobble

To use Cobble, you will need Cobble (clearly), but also Ninja.

For the rest of this tutorial, we’ll refer to the directory containing Cobble as ${C} for short.

Hello, Cobble!

Create a new empty directory to hold your project. For the rest of this tutorial, we’ll refer to this directory as hello-cobble, but you can name it anything you like.

hello-cobble is your project root. When Cobble talks about the path to a software component, it will be relative to this root.

Let’s write a little program. Place the following code in a file called hello.c in the project root:

#include <stdio.h>

int main() {
  puts("Hello, Cobble!");
}

This program is so simple that you don’t really need a build system. But this is a tutorial.

Making a package: the BUILD file

Let’s turn our project root into a Cobble package by adding a file called BUILD. This file will explain how to compile hello.c.

Fill in the file as follows:

c_binary('hello',
  environment = 'default',
  sources = [ 'hello.c' ],
)

Notice that this is valid Python code.

Line by line:

  • c_binary creates a new target that will compile code into a binary (an executable program) using the C Production Model.

  • 'hello' is the name of this target. The name will be used if we need to reference this target from other targets. It will also be the name of the binary on disk. By convention, all Cobble targets take a name as their first parameter.

  • environment specifies the environment (toolchain, etc.) that will be used to build this binary. We’ll create the ‘default’ environment in the next step.

  • sources provides a list of paths to source files. The paths are relative to the package root — that is, the directory with BUILD in it. (The comma at the end of this line is not mandatory, but putting it there makes it easier to rearrange lines when targets become more complex.)

For more specifics, see the C Syntax Reference.

Making a project: the BUILD.conf file

Cobble needs a file in the project root called BUILD.conf. This file tells Cobble that the directory is a Cobble project (i.e. the path was not misspelled) and provides project-wide settings.

Create a BUILD.conf file. We’ll add stuff to it over the next couple steps.

First, remember that Cobble doesn’t include any language support out of the box. So we need to install support for C programs:

install('cobble.target.c')

Now, we need an initial environment. Only one detail in the environment is important for our little program: where to find the C compiler. Cobble’s C plugin uses an environment key cc for this.

We’ll name our initial environment default, but there’s nothing special about this name. You could name it Fred Rogers if you prefer.

environment('default', contents = {
  'cc': 'gcc',
})

Finally, we need to tell Cobble where to look to find our BUILD file. We’ll use a “double-slash path” to designate the project root, wherever it may be on your filesystem:

seed('//')

Directory Structure Review

To be clear, you should now have a directory containing three files:

hello-cobble/
  BUILD.conf
  BUILD
  hello.c

Building

First, you must create a second empty directory to contain build output. You can put this anywhere. For the purposes of this tutorial, put it inside your hello-cobble project root and call it build.

When creating a new build directory, we must first initialize it. From your project root:

$ cd build
$ ${C}/cobble init ..

The init subcommand to Cobble initializes a build directory. Its only required argument is a path to the project root. Here we use a relative path; this is a good idea. init can take other options too; use -h on any Cobble command to get a usage message.

init will return silently after a fraction of a second. Your build directory will now contain a file called build.ninja. Read through it if you like, it’s short. There’s also a symlink pointing to the copy of Cobble you used to initialize the build directory, for convenience.

Now, from the same build directory, you can build the code:

$ ./cobble build
[2/2] SYMLINK latest/hello

The second line is Ninja’s way of saying that:

  • It completed 2 tasks of 2 total, and
  • The last task completed was producing a SYMLINK called latest/hello.

Your fresh executable binary is inside latest/. It’s a symbolic link into the env/ directory, where Cobble keeps track of things by environment. The (optionally installed) Unix command tree can help visualize the directory structure; if you don’t have tree, you can use find . instead.

Try your program out:

$ latest/hello
Hello, Cobble!

Adding a Library

A program consisting of a single source file is easy. Let’s complicate things a little.

Imagine that your job is a little weird, and you’re going to have to make a bunch of different programs that say “Hello, Cobble!” You want to factor out the printing code into a reusable component.

Let’s call this component say_hi. We could define it in the same BUILD file as hello, but maybe you’d like to include its code in other programs without copy-pasting, using a git submodule (for example). To do this, it’s often useful to put code in a subdirectory.

Create a new subdirectory called say_hi inside your project root:

hello-cobble/
  BUILD.conf
  BUILD
  hello.c
  say_hi/
    (empty for now)

Inside this directory, create a C file to implement your new must-have library. Call it print.c. It looks a lot like hello.c.

#include <stdio.h>

void say_hi() {
  puts("Hello, cobble!");
}

Now create say_hi/BUILD:

c_library('say_hi',
  sources = [ 'print.c' ],
)

This looks a lot like the c_binary stanza in our first BUILD file, except that it’s missing the environment part. This is because libraries pick up their environment from the targets that use them.

If we want to use this code from hello we need a header file. Create say_hi/print.h:

#ifndef _say_hi_print_h_
#define _say_hi_print_h_

void say_hi();

#endif  // _say_hi_print_h_

Cobble will try to use ar to archive our library, so we need to modify the default environment in BUILD.conf to tell Cobble where ar lives. Add this line just under the definition of cc:

    'ar': 'ar',

Finally, modify hello.c in the project root to use the library. It should read as follows:

#include "say_hi/print.h"

int main() {
  say_hi();
}

A build failure

Go into your build directory and run your build. You’ll get something like the following:

$ ./cobble build
[2/2] LINK env/dc7b2208e241c51a1de2ffd1e29b69b2304aef3d/hello
FAILED: gcc  -o env/dc7b2208e241c51a1de2ffd1e29b69b2304aef3d/hello env/a72cd19a9c089c71d1c37113edc34ad9930c979c/hello.c.o 
env/a72cd19a9c089c71d1c37113edc34ad9930c979c/hello.c.o: In function `main':
hello.c:(.text+0xa): undefined reference to `say_hi'
collect2: error: ld returned 1 exit status
ninja: build stopped: subcommand failed.

The linker can’t figure out where say_hi is defined. This is because we didn’t tell it how to do so.

Adding a dependency

We have to indicate that hello depends on our say_hi library. Edit the top-level BUILD file so that it reads:

c_binary('hello',
  environment = 'default',
  sources = [ 'hello.c' ],
  deps = [ '//say_hi' ],    # <-- new line
)

We’ve added //say_hi — the path to our library’s package — to hello’s deps (dependencies). Try the build again:

$ ./cobble build
[3/3] SYMLINK latest/hello

$ latest/hello
Hello, cobble!

Hooray!

But wait…

Remember in the first step, when we added this line to BUILD.conf?

seed('//')

We said this was to help Cobble find BUILD files. But we didn’t add our say_hi library to the seed line. How did Cobble find it?

The seed line just gets Cobble started. It will automatically spider out using deps information to collect the set of packages required to build seeded targets. So by adding //say_hi to the deps of hello, we’ve told Cobble everything it needs to know.

More Cliffle

By Topic