Getting Started

It is easiest to start using tmake with an existing C or C++ project. Consider the following simple project:

$ ls
api.c
api.h
test-api.c

api.c and api.h provide an implementation of a simple API and test-api.c provides unit testing of that API. We want to build a library (libapi.a), compile test-api.c, link it against the library and run it. While it is easy to write a build specification for this project, let's make use of tmake --genie to create a starting build definition.

$ tmake --genie
Created build.spec
Not already in a project, so creating project.spec

tmake --genie notices that some C sources and header files exist and that no existing build files exist. So it creates a trivial, empty project.spec. And the following build.spec

$ cat build.spec
# This is a sample build.spec file generated by tmake --genie
# Edit it as required

# Any non-private headers should be published
#PublishIncludes api.h

# If this library is only used by local executables, remove --publish
Lib --publish api api.c

Executable --test test-api test-api.c

Note that tmake --genie has identified that api.c is library source and needs to be built into a library. The current directory name is used as the default library name, thus "api".

Next we see that tmake --genie noticed that test-api.c has main() and begins with "test" so is likely a test. Therefore it builds the executable with "--test".

Finally we see that api.h may need to be a published as a public header file, but this is commented out. We can uncomment this. And removing the comments at the top, here is our final build.spec

$ cat build.spec
PublishIncludes api.h

Lib --publish api api.c

Executable --test test-api test-api.c

One of the hallmarks of tmake is the compact build descriptions with no boiler plate, quoting or other noise. Let's build it.

$ tmake 
    Publish      publish/include/api.h
    Cc           test-api.o
    Cc           api.o
    Ar           libapi.a
    Link         test-api
Built 5 of 6 target(s) in 0.26 seconds

By default, tmake builds the all target of the current directory. Executable and Lib targets are automatically added as dependencies of the all target, so libapi.a and test-api are built by default. Unit tests are not run by default, but we can run them now.

$ tmake test
    Test         objdir/test-api

No output means the test succeeded. Now what has been produced?

$ ls -F
api.c
api.h
build.spec
objdir/
project.spec
test-api.c

We see that everything is built into the objdir directory (this is the default) leaving the source directory unchanged. Like all modern build systems it is important to keep the build artifacts and the sources separate. Let's look at how the outputs were arranged.

$ tree objdir
objdir
├── api.o
├── libapi.a
├── publish
│   └── include
│       └── api.h
├── test-api
└── test-api.o

2 directories, 5 files

We can see that apart from the objdir/publish directory, everything mirrors the source directory, except with the objdir directory. The publish directory acts as a common staging area for include files and libraries that are shared between directories so that we can change the source location easily without affecting other parts of the project. We can also see the detailed commands that tmake ran for the build. First let's clean out the build.

$ tmake clean
    Clean        .

And build again with -v

$ tmake -v test
gcc -Iobjdir/publish/include -I. -Iobjdir   -c test-api.c -o objdir/test-api.o
gcc -Iobjdir/publish/include -I. -Iobjdir   -c api.c -o objdir/api.o
ar cr objdir/libapi.a objdir/api.o
ranlib objdir/libapi.a
gcc   -o objdir/test-api objdir/test-api.o objdir/libapi.a
objdir/test-api >@stdout
Built 5 of 6 target(s) in 0.20 seconds

Now we can see the commands that were used. So what if we want to do a debug build? We can add some flags to the C compiler with CFlags

$ cat build.spec
CFlags -g -O0
PublishIncludes api.h

Lib --publish api api.c

Executable --test test-api test-api.c

By adding CFlags at the top of the file, these flags apply to any subsequent rules in this build.spec Now if we build again, tmake notices that the build commands have change and so runs the appropriate rules.

$ tmake 
    Cc           test-api.o
    Cc           api.o
    Ar           libapi.a
    Link         test-api
Built 4 of 6 target(s) in 0.15 seconds

Now let's dig into the rules that are driving the build. We are using the high level build commands from the built-in rulebase.default in our build specification. Taking a look at one of these rules:

$ tmake --commands=Executable
Executable --test --chdir --nobuild --publish --no|strip --nofork --install=<dir> target sources...

Builds an executable from one or more source files. For each source, the appropriate ObjectRule is used
to generate the object file, and these files are linked (along with any libraries) to create the executable.

Bound variables: CCLD (from Linker), LD_FLAGS (from LinkFlags), PROJLIBS and PROJDEPS (from UseLibs)
                 LOCAL_LIBS (from ArchiveLib), SYSLIBS (from UseSystemLibs)
Used variables: LDFLAGS

The following options are supported:
  --test              Marks the executable as a test (see Test)
  --nofork            With --test, uses Test --nofork
  --chdir             With --test, uses Test --chdir
  --nobuild           Don't make this executable dependent on phony 'all' (which is otherwise the default)
  --publish           Publishes the executable (see PublishBin)
  --install=<dir>     Install the executable to the given directory (see InstallFile)
  --strip[=full|dynamic|none]  Sets the strip type on installation. --strip means full. omitted means none. (see InstallFile)

We can see that Executable takes a number of options and parameters that determine how the low level target rules are created. Let's use tmake --targets and tmake --find to see what it generated. First let's see all the final (non-phony) targets that tmake knows about.

$ tmake --targets
api.o
libapi.a
publish/include/api.h
publish/lib/libapi.a
test-api
test-api.o

These are all the targets that can be built in objdir/. So let's examine the test-api target:

$ tmake --find=test-api
-- test-api ---------------------------------------------------
@build.spec:6
test-api: test-api.o libapi.a
local=.
  var CCLD=gcc
  var LD_FLAGS=
  var PROJLIBS=
  var SYSLIBS=
	run $CCLD $LD_FLAGS $LDFLAGS -o $target $inputs $PROJLIBS $SYSLIBS
Note: 1 non-exact matches also exist, use --findall to show them

Here we can see that Executable has generated a low level target rule to build test-api, with dependencies on test-api.o and libapi.a and the command line:

run $CCLD $LD_FLAGS $LDFLAGS -o $target $inputs $PROJLIBS $SYSLIBS

Here, $target becomes test-api and $inputs becomes test-api.o. The variables CCLD, LD_FLAGS, PROJLIBS and SYSLIBS are "bound" variables. This means that the target rule captures the values of these variables when the rule is defined. By contrast, LDFLAGS is an "unbound" variable. It is a global variable that has the same value across all rules.

Now imagine that we would like to restructure our project to move api.c and api.h into their own subdirectory, lib/, and test-api.c into it's own directory, tests. How would this affect the build rules? First let's move the sources.

$ mkdir lib tests
$ mv api.c api.h lib
$ mv test-api.c tests

Now let's create lib/build.spec

$ cat lib/build.spec
PublishIncludes api.h
Lib --publish api api.c

This is just the relevant lines moved to lib/. Similarly for tests/build.spec. Here the only change we need to make here is that tests needs to be told to use libapi. Previously this was automatically handled because the library was defined earlier in the build.spec. So:

$ cat tests/build.spec
UseLibs api
Executable --test test-api test-api.c

We now have:

$ tree
.
├── build.spec
├── lib
│   ├── api.c
│   ├── api.h
│   └── build.spec
├── objdir
│   ├── api.o
│   ├── libapi.a
│   ├── publish
│   │   └── include
│   │       └── api.h
│   ├── test-api
│   └── test-api.o
├── project.spec
└── tests
    ├── build.spec
    └── test-api.c

5 directories, 12 files

Now let's build:

$ tmake 
    Clean        removing 4 orphan target(s)
    Publish      publish/include/api.h
    Cc           lib/api.o
    Cc           tests/test-api.o
    Ar           lib/libapi.a
    Publish      publish/lib/libapi.a
    Link         tests/test-api
Built 6 of 6 target(s) in 0.18 seconds

And the outputs were created:

$ tree objdir
objdir
├── lib
│   ├── api.o
│   └── libapi.a
├── publish
│   ├── include
│   │   └── api.h
│   └── lib
│       └── libapi.a
└── tests
    ├── test-api
    └── test-api.o

5 directories, 6 files

Notice how we didn't need to do a clean or remove any existing files. tmake noticed that the previous targets, test-api, test-api.o, libapi.a and api.o were no longer targets and so these outputs were automatically removed. In their place were build tests/test-api.o, tests/test-api, lib/libapi.a and lib/api.o

Also notice that now publish/lib/libapi.a has been created. In order for test-api to find the library, it is published into the known location, publish/lib directory. This is what the --publish option to Lib did.

Now a couple of other things we can do. Imagine we are working on test-api.c. Let's cd into the tests directory.

$ cd tests

And now we simulate changing api.c

$ touch ../lib/api.c

Let's rebuild. What will happen?

$ tmake test
    Cc           lib/api.o
tmake: Entering directory `/private/tmp/tmakedocs/getting-started/api'
    Ar           lib/libapi.a
    Publish      publish/lib/libapi.a
    Link         tests/test-api
    Test         objdir/tests/test-api
Built 4 of 6 target(s) in 0.20 seconds

tmake notices that lib/api.a is required by test-api and lib/api.a depends on lib/api.c, so that is automatically recompiled, followed by recreating the library and finally tests-api is relinked.

By default, tmake will build the all target in the current directory, but if this depends on targets in other directories, those will be automatically built as required.

We can also use one of the various debug flags to follow along and understand why tmake is building each target. If we had done the following and used tmake -dg we would see.

$ touch ../lib/api.c
$ tmake -dg
   13ms [g] tests/all --> tests/test-api --> <lib>api --> lib/libapi.a --> lib/api.o (changed lib/api.c)
    Cc           lib/api.o
tmake: Entering directory `/private/tmp/tmakedocs/getting-started/api'
   29ms [g] tests/all --> tests/test-api --> <lib>api --> lib/libapi.a (depend lib/api.o changed lib/api.o)
    Ar           lib/libapi.a
   69ms [g] tests/all --> tests/test-api --> <lib>api (depend lib/libapi.a changed lib/libapi.a)
    Publish      publish/lib/libapi.a
   70ms [g] tests/all --> tests/test-api (depend <lib>api changed <lib>api)
    Link         tests/test-api
Built 4 of 6 target(s) in 0.14 seconds

Initially we see that the default target is tests/all, and by following dependencies, there is ultimately a dependency on lib/api.o via the chain:

 tests/all --> tests/test-api --> <lib>api --> lib/libapi.a --> lib/api.o

lib/api.o depends on lib/api.c which has changed, and so lib/api.o is rebuilt.

This process contains where now lib/libapi.a depends on lib/api.o which has changed and so it is rebuilt. This continues until the initial target is finally built.

An alternative approach is to use tmake -dG. Instead of only showing the reasons why targets were build, this shows each target as it is attempted via the dependency tree.

$ touch ../lib/api.c
$ tmake -dG
   12ms [G] build tests/all
   12ms     [G] build tests/test-api
   12ms         [G] build <lib>api
   12ms             [G] build lib/libapi.a
   12ms                 [G] build lib/api.o
   12ms                     [G] build lib/api.c
   13ms                     [G] build publish/include/api.h
   13ms                         [G] build lib/api.h
    Cc           lib/api.o
tmake: Entering directory `/private/tmp/tmakedocs/getting-started/api'
   15ms         [G] build tests/test-api.o
   15ms             [G] build tests/test-api.c
   30ms [G] build tests/all
   30ms     [G] build tests/test-api
   30ms         [G] build <lib>api
   30ms             [G] build lib/libapi.a
    Ar           lib/libapi.a
   69ms [G] build tests/all
   69ms     [G] build tests/test-api
   69ms         [G] build <lib>api
    Publish      publish/lib/libapi.a
    Link         tests/test-api
  142ms [G] build tests/all
Built 4 of 6 target(s) in 0.14 seconds

Let's also take a look at a phony target. Targets line all and clean are phony targets. They are like "actions" that can have dependencies but don't have any corresponding outputs. Test targets are an example of phony targets.

First let's look at the local test target.

$ tmake --find=test
-- tests/test -------------------------------------------------
@/Volumes/src/tmake/rulebase.default:345, tests/build.spec:2
tests/test [phony]: tests/test#1
local=tests
Note: 10 non-exact matches also exist, use --findall to show them

We see that tests/test is a phony target that depends on test/tests#1. Let's look at that one.

$ tmake --find=test#1
-- tests/test#1 -----------------------------------------------
@tests/build.spec:2
tests/test#1 [phony slow]: tests/test-api 
local=tests
  var testcommand=objdir/tests/test-api
  var SRCDIR=tests
  var publishlib=/private/tmp/tmakedocs/getting-started/api/objdir/publish/lib
  var LD_LIBRARY_PATH=
	setup-test-env $publishlib $LD_LIBRARY_PATH
	run-test-command $testcommand

This is the test target that was generated by Executable --test. We can see that it depends on tests/test-api (the executable) and has the bound variable testcommand=objdir/tests/test-api. Thus the command that will be run is:

run-test-command objdir/tests/test-api

This is part of the default rulebase (rulebase.default). We can find out more about this command with:

$ tmake --ref

And searching for run-test-command we see:

 run-test-command testcommand
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~

 This is a helper for running test commands. It is used by the Test rule. The
 environment should first be set up with setup-test-env Then SRCDIR is exported
 to the environment stdout, stderr and errok are checked from the caller to

In fact, Executable --test prog prog.c is shorthand for:

 Executable prog prog.c
 Test prog

As a final note, let's see how publish libraries are handled.

$ tmake --find=libapi.a
-- lib/libapi.a -----------------------------------------------
@lib/build.spec:2
lib/libapi.a: lib/api.o
local=lib
	file delete $target
	run $AR $ARFLAGS $target {*}[expand-objects $inputs]
	run $RANLIB $target

-- publish/lib/libapi.a ---------------------------------------
@lib/build.spec:2
publish/lib/libapi.a: lib/libapi.a
alias=<lib>api
local=lib
	file delete $target
	file-link $target $inputs

Here we see that there are two rules associated with libapi.a The first creates lib/libapi.a from lib/api.o

Secondly, publish/lib/libapi.a depends on lib/libapi.a and this rule creates a symlink to the library. Also note that the library as the "alias" api. This alias means that the UseLibs command can add a dependency on api and this will automatically resolve to publish/lib/libapi.a which in turn depends on lib/libapi.a

Using this approach with aliases makes it easy to change between shared and archive libraries along with some other features. These will be covered in a later topic on aliases.

The results of the search are