A tour of JCons

Basics

Suppose we have a program consisting of two modules main.c and mod1.c, and a header file mod1.h:

/* main.c */
#include <stdio.h>
#include "mod1.h"
int main() {
    printf("%s from main\n", GREETING);
    mod1_greeting();
    return 0;
}
/* mod1.c */
#include <stdio.h>
#include "mod1.h"
void mod1_greeting() {
    printf("%s from mod1\n", GREETING);
}
/* mod1.h */
#define GREETING "hello"
extern void mod1_greeting();

To build it with JCons we could create the following file:

# construct.py
e = Cons()
e.program("prog", "main.c", "mod1.c")

When JCons is invoked, the program prog will be built in the following manner:

$ jcons
gcc -c main.c -o main.o
gcc -c mod1.c -o mod1.o
gcc -o prog main.o mod1.o
$ jcons
jcons: up-to-date: .
$ ./prog
hello from main
hello from mod1

The second time everything is “up to date”, so nothing is done. If any of the involved files changes, JCons will detect that an re-build the necessary files:

$ echo "void a_change() {}" >> main.c     # change source file
$ jcons
gcc -c main.c -o main.o
gcc -o prog main.o mod1.o

$ rm main.o                               # remove object file
$ jcons
gcc -c main.c -o main.o

$ echo GARBAGE > mod1.o                   # destroy content of object file
$ jcons
gcc -c mod1.c -o mod1.o

$ rm prog                                 # remove program
$ jcons
gcc -o prog main.o mod1.o

In some of the examples above only an object file is rebuild. Since the newly created object file has the same content as before, no re-linking is needed. For users of Make this might seem odd, but this is one of the features of tools like JCons. JCons tracks changes to the contents of the files. The only function of timestamps is as an indicator of change (a file might have changed if its timestamp has changed). If we only change the timestamps of files, nothing will be re-built:

$ touch main.c
$ jcons
jcons: up-to-date: .

$ touch mod1.o
$ jcons
jcons: up-to-date: .

$ touch prog
$ jcons
jcons: up-to-date: .

The module mod1.c has a header file mod1.h included by the C file itself and by main.c. JCons will detect changes to that header file:

$ jcons                                   # up-to-date before change
jcons: up-to-date: .

$ perl -i -pe 's/hello/goodbye/' mod1.h   # change header file
$ jcons
gcc -c main.c -o main.o
gcc -c mod1.c -o mod1.o
gcc -o prog main.o mod1.o
$ ./prog
goodbye from main
goodbye from mod1

This works because JCons looks for #include lines in the source files and automatically adds dependencies for the files it finds. If we would like to build the program in a debug flavor, we need to change the construct.py file:

# construct.py
e = Cons(CFLAGS = "-DDEBUG -g", LINKFLAGS = "-g")
e.program("prog", "main.c", "mod1.c")
$ jcons
gcc -DDEBUG -g -c main.c -o main.o
gcc -DDEBUG -g -c mod1.c -o mod1.o
gcc -o prog -g main.o mod1.o

As can be seen, the configuration variable CFLAGS affects how a C program is built. Variables like CFLAGS can also be set temporarily on the command line:

$ jcons CFLAGS="-O2"                     # command line override
gcc -O2 -c main.c -o main.o
gcc -O2 -c mod1.c -o mod1.o
gcc -o prog -g main.o mod1.o

$ jcons CFLAGS="-Wall"                   # another override
gcc -Wall -c main.c -o main.o
gcc -Wall -c mod1.c -o mod1.o
gcc -o prog -g main.o mod1.o

$ jcons CFLAGS="-Wall"                   # same setting again
jcons: up-to-date: .

$ jcons                                  # back to normal
gcc -DDEBUG -g -c main.c -o main.o
gcc -DDEBUG -g -c mod1.c -o mod1.o
gcc -o prog -g main.o mod1.o

As can be seen in the examples, a change in command line triggers a rebuild. JCons includes the command line in the dependency calculation. JCons knows which files are “generated files” and can remove them if asked to do that (with the -r option):

$ jcons -r                               # remove generated files
Removed main.o
Removed mod1.o
Removed prog

It is also possible to force a full rebuild:

$ jcons                                  # normal build
gcc -DDEBUG -g -c main.c -o main.o
gcc -DDEBUG -g -c mod1.c -o mod1.o
gcc -o prog -g main.o mod1.o

$ jcons                                  # nothing to do
jcons: up-to-date: .

$ jcons  --always-make                   # forced build
gcc -DDEBUG -g -c main.c -o main.o
gcc -DDEBUG -g -c mod1.c -o mod1.o
gcc -o prog -g main.o mod1.o

Several output files

A command can have several output files. The method Cons.command() can handle this situation:

e = Cons()
e.command(["parse.tab.c", "parse.tab.h"], "parse.y", "bison -d parse.y")

Here the output argument is an array of files produced by the command. If any of those files doesn’t exit or is out-of-date the command has to be run. Both the input- and output-argument to Cons.command() can be arrays instead of single values. JCons understands how to deal with those cases too.

C++ programs

If the some of the source files had been C++ rather than C files, the construct.py would look almost the same:

# construct.py
e = Cons()
e.program("prog", "main.cpp", "mod1.c")

When running JCons we then get:

$ jcons
g++ -c main.cpp -o main.o
gcc -c mod1.c -o mod1.o
g++ -o prog main.o mod1.o

Note that g++ is used to compile the C++ file, and also used when linking.

Include files

The include path to a C/C++ compiler is typically given by -I options on the command line. For JCons to be able to calculate the #include dependencies, the directories have to be specified via a variable CPPPATH:

# construct.py
e = Cons(CPPPATH = ["dir1", "dir2"])
e.program("prog", "main.c")

When JCons is executed the values in CPPPATH are translated into -I `` options on the command line. Suppose the file ``main.c looks like:

/* main.c */
#include <main.h>
int main() { return EXIT_CODE; }

Running JCons we get:

$ mkdir -p dir1 dir2
$ echo "#define EXIT_CODE 2" > dir2/main.h
$ jcons
gcc -I dir1 -I dir2 -c main.c -o main.o
gcc -o prog main.o

$ echo "#define EXIT_CODE 1" > dir1/main.h
$ jcons
gcc -I dir1 -I dir2 -c main.c -o main.o
gcc -o prog main.o

The second time JCons realizes that the main.h located in dir1 is going to be used, and recompiles main.c.

Libraries

TODO: write this section ....

Variant builds

Often a program should be built in several “flavours”, e.g. a debug and and a release version. Then the object files and executables need to be stored in different places for each flavour. JCons makes it easy to change where the output is placed in several ways:

  1. in a separate build directory tree (by using BUILD_TOP)
  2. in separate sub-directories (by using BUILD_SUBDIR)
  3. with different filename suffixes (by using BUILD_SUFFIX)

If BUILD_TOP is used, we get:

# construct.py
e = Cons(BUILD_TOP = "build/release")
e.program("prog", "main.c", "lib/mod1.c")
$ jcons
gcc -c lib/mod1.c -o build/release/lib/mod1.o
gcc -c main.c -o build/release/main.o
gcc -o build/release/prog build/release/main.o build/release/lib/mod1.o

To build both a release and a debug version, we can use normal Python scripting:

# construct.py
flavors = [
    ["release", "-O2 -DNDEBUG"],
    ["debug",   "-DDEBUG"],
]
for flavor, cflags in flavors:
    e = Cons(BUILD_TOP = "build/" + flavor, CFLAGS = cflags)
    e.program("prog", "main.c", "lib/mod1.c")

With this file the build would look like:

$ jcons --always-make
gcc -DDEBUG -c lib/mod1.c -o build/debug/lib/mod1.o
gcc -DDEBUG -c main.c -o build/debug/main.o
gcc -o build/debug/prog build/debug/main.o build/debug/lib/mod1.o
gcc -O2 -DNDEBUG -c lib/mod1.c -o build/release/lib/mod1.o
gcc -O2 -DNDEBUG -c main.c -o build/release/main.o
gcc -o build/release/prog build/release/main.o build/release/lib/mod1.o

If BUILD_SUBDIR was used instead of BUILD_TOP, we would get:

# construct.py
e = Cons(BUILD_SUBDIR = "release")
e.program("prog", "main.c", "lib/mod1.c")
$ jcons
gcc -c lib/mod1.c -o lib/release/mod1.o
gcc -c main.c -o release/main.o
gcc -o release/prog release/main.o lib/release/mod1.o

or if BUILD_SUFFIX was used:

# construct.py
e = Cons(BUILD_SUFFIX = "release")
e.program("prog", "main.c", "lib/mod1.c")
$ jcons
gcc -c lib/mod1.c -o lib/mod1-release.o
gcc -c main.c -o main-release.o
gcc -o prog-release main-release.o lib/mod1-release.o

Note that only methods producing object files or executables (e.g. Cons.program() or Cons.static_library()), are affected by the BUILD_* variables. Cons.command() does not look at those variables.

Cache of build results

JCons can cache the generated files in a special directory. If the same file is about to built again later, JCons can replace the actual command with a copy operation from the cache directory. This is much faster, and avoids needlessly re-executing the same command with the same input several times.

TODO: add example

The construct.py file

JCons reads a file construct.py (or another file specified with an -f option). This file is a normal Python file where methods of the Cons class can be called. The purpose of the file is to tell JCons what there is to build (i.e. help build the directed acyclic graph (DAG) describing the dependencies). It is of course possible (but pointless) to do something entirely different in the script, but then JCons would not know what to do:

# construct.py
print("hello world")         # pointless use of JCons
exit(0)
$ jcons
hello world

The first thing to do in a construct.py file is to create an object of the Cons class. Then methods can be called on that object to tell JCons what there is to build. There are different methods for different needs (e.g. Cons.command(), Cons.object(), Cons.objects(), Cons.static_library(), Cons.program()). The following example will demonstrate:

e = Cons()

# C program with two modules
e.program("foo", "foo.c", "bar.c")

# generic command
e.command("bar.pdf", "bar.ps", "ps2pdf %INPUT %OUTPUT")

# a library
e.static_library("mylib", "x.cpp", "y.cpp", "z.cpp")

The constructor of the Cons class can take optional named arguments. For example:

e1 = Cons(CFLAGS = "-g -Wall")
e1.program("foo", "foo1.cpp", "foo2.c")

e2 = Cons(CFLAGS = "-O2 -Wall", CXXFLAGS = "-O2")
e2.program("bar", "bar1.cpp", "bar2.c")

conscript.py files

A larger application will be spread over a number of directories. Each directory may produce a program or a library used by some program. To handle this situation, the main construct.py file can include other subsidiary files (typically called conscript.py in the tradition from Cons):

# construct.py
e = Cons()
e.program("prog", "main.c", "util/util.a")
Cons.include("util/conscript.py")
# util/conscript.py
e = Cons()
e.static_library("util", "util.c")

With these files we get:

$ jcons
gcc -c main.c -o main.o
gcc -c util/util.c -o util/util.o
ar rc util/util.a util/util.o
gcc -o prog main.o util/util.a

JCons maintains one global DAG of all dependencies. All included conscript.py file contribute to this dependency graph.

Table Of Contents

Previous topic

Introduction

Next topic

Reference