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"
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