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``: .. code-block:: c /* main.c */ #include #include "mod1.h" int main() { printf("%s from main\n", GREETING); mod1_greeting(); return 0; } .. code-block:: c /* mod1.c */ #include #include "mod1.h" void mod1_greeting() { printf("%s from mod1\n", GREETING); } .. code-block:: c /* mod1.h */ #define GREETING "hello" extern void mod1_greeting(); To build it with JCons we could create the following file: .. code-block:: python # 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: .. code-block:: python # 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 :py:meth:`Cons.command` can handle this situation: .. code-block:: python 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 :py:meth:`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: .. code-block:: python # construct.py e = Cons() e.program("prog", "main.cpp", "mod1.c") .. FILE: /* main.cpp */ extern "C" int status ; int main() { return status; } .. FILE: /* mod1.c */ int status = 123; .. COMMAND: REMOVE .jcons 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``: .. code-block:: python # 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: .. code-block:: c /* main.c */ #include 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: 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: .. code-block:: python # construct.py e = Cons(BUILD_TOP = "build/release") e.program("prog", "main.c", "lib/mod1.c") .. FILE: /* main.c */ extern int status ; int main() { return status; } .. FILE: /* lib/mod1.c */ int status = 123; .. COMMAND: REMOVE .jcons :: $ 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: .. code-block:: python # 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") .. CALL: save_file construct.py 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: .. code-block:: python # 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: .. code-block:: python # 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. :py:meth:`Cons.program` or :py:meth:`Cons.static_library`), are affected by the ``BUILD_*`` variables. :py:meth:`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 :py:class:`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: .. code-block:: python # 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 :py:class:`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. :py:meth:`Cons.command`, :py:meth:`Cons.object`, :py:meth:`Cons.objects`, :py:meth:`Cons.static_library`, :py:meth:`Cons.program`). The following example will demonstrate: .. code-block:: python 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 :py:class:`Cons` class can take optional named arguments. For example: .. code-block:: python 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_): .. code-block:: python # construct.py e = Cons() e.program("prog", "main.c", "util/util.a") Cons.include("util/conscript.py") .. code-block:: python # util/conscript.py e = Cons() e.static_library("util", "util.c") .. FILE: /* main.c */ extern int status; int main() { return status; } .. FILE: /* util/util.c */ int status = 17; 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. .. include:: pointers.txt