This chapter will show you how to use MakeKit to configure and build a C language project.
Programs are built using the mk_program
function, which requires
at a minimum a list of source files and a program name. The program name is passed
as the PROGRAM
parameter, and the list of sources files is passed
as the SOURCES
parameter. Consider Example 3.1, “Using mk_program
”
for a simple "Hello, World" program hello
, it's source file
main.c
, and the MakeKitBuild
file to build it.
Example 3.1. Using mk_program
main.c
#include <stdio.h> int main(int argc, char** argv) { printf("Hello, world!\n"); }
MakeKitBuild
make() { mk_program \ PROGRAM=hello \ SOURCES="main.c" }
By default, the program will be installed in the usual binary directory
(e.g. /usr/local/bin
). The install location
can be changed with the INSTALLDIR
parameter to
mk_program
.
A C project with a complex layout will typically need to look for headers
from multiple directories within the source tree. Using the
INCLUDEDIRS
parameter to mk_program
,
you can specify a list of directories where the compiler should look for
header files. Adding a header file include/values.h
to our small example project and modifying MakeKitBuild
to find it yields Example 3.2, “Using INCLUDEDIRS
”.
Example 3.2. Using INCLUDEDIRS
include/values.h
#define MESSAGE "Hello, World!"
main.c
#include <stdio.h> #include "values.h" int main(int argc, char** argv) { printf("%s\n", MESSAGE); }
MakeKitBuild
make() { mk_program \ PROGRAM=hello \ SOURCES="main.c" \ INCLUDEDIRS="include" }
To link your program to other libraries, specify them as a list
via the LIBDEPS
parameter. Each library should be
specified exactly as you would with -l
to the C
compiler (without the lib
prefix). See
Example 3.3, “Using LIBDEPS
”, which links to libm
to use the sqrt()
function.
Example 3.3. Using LIBDEPS
main.c
#include <stdio.h> #include <math.h> #include "values.h" int main(int argc, char** argv) { printf("%s\n", MESSAGE); printf("The square root of 2.0 is %f\n", sqrt(2.0)); }
MakeKitBuild
make() { mk_program \ PROGRAM=hello \ SOURCES="main.c" \ INCLUDEDIRS="include" \ LIBDEPS="m" }
MakeKit allows you to build two types of libraries:
A shared library is a collection of address-relocatable code which is combined
with the program that links against it at startup by the system dynamic linker.
Shared libraries are built with the mk_library
function.
A dynamically loadable object is a blob of code which is explicitly loaded
and executed by a program through functions like dlopen()
.
Dynamically loadable objects are typically used to implement plugins. Although
on many UNIX systems dynamically loadable objects and
shared libraries are the same type of file, some systems distinguish between
the two. For example, Mac OS X distinguishes between
.dylib
(shared libraries) and .bundle
or .so
(dynamically loadable objects). To build a dynamically
loadable object, use the mk_dlo
function, which takes nearly
identical parameters to mk_library
.
As with building programs, building a library requires a library name, passed as the
LIB
parameter, and a list of sources, passed as the
SOURCES
parameter. The library name should be specified
without a filename extension or the lib
prefix. Consider
Example 3.4, “Using mk_library
”, which expands on previous examples by building a
small library and linking it into the program hello
.
Example 3.4. Using mk_library
include/example.h
external unsigned long factorial(unsigned long i);
main.c
#include <stdio.h> #include <math.h> #include "values.h" #include "example.h" int main(int argc, char** argv) { printf("%s\n", MESSAGE); printf("The square root of 2.0 is %f\n", sqrt(2.0)); printf("5! is %lu\n", factorial(5)); }
example.c
#include "example.h" unsigned long factorial(unsigned long i) { return i == 1 ? 1 : i * factorial(i - 1); }
MakeKitBuild
make() { mk_library \ LIB=example \ SOURCES="example.c" \ INCLUDEDIRS="include" mk_program \ PROGRAM=hello \ SOURCES="main.c" \ INCLUDEDIRS="include" \ LIBDEPS="m example" }
As you can see, mk_library
accepts many of the same parameters
as mk_program
, including INCLUDEDIRS
.
As with programs, libraries can specify dependencies on other shared libraries
with LIBDEPS
.
Our new library was linked into hello
the same way as the
system library libm
. Note that the order of the two function
invocations in MakeKitBuild
is significant.
Because mk_program
was called after
mk_library
, it knows that libexample
is a library we build ourselves rather than pre-existing on the system, and it adds
appropriate dependencies to the generated Makefile
.
When a library or program grows large enough, it is often
prudent to break its source code up across multiple
subdirectories to enhance organizational clarity and separation
of concerns. Rather than maintaining a long list of source
files from disparate subdirectories in your invocation of
mk_library
or
mk_program
, you can use the
mk_group
function to group together a set
of source files and compiler flags into a logical unit which can
then be added in to your program or library. This feature is an
analogue of "convenience libraries" available in
GNU libtool.
Imagine that the currently diminuitive libexample
is destined
to become a vast utility library with both math and string manipulation functions.
In anticipation of this, we break it into two subdirectories, math
and string
. We will build each subdirectory as an object
group and then combine the two into libexample
using the
GROUPS
parameter to mk_library
.
This yields Example 3.5, “Using mk_group
”.
Example 3.5. Using mk_group
include/example.h
/* Math functions */ external unsigned long factorial(unsigned long i); /* String functions */ external void reverse_string(char* str);
math/factorial.c
#include "example.h" unsigned long factorial(unsigned long i) { return i == 1 ? 1 : i * factorial(i - 1); }
math/MakeKitBuild
make() { mk_group \ GROUP=math \ SOURCES="*.c" \ INCLUDEDIRS="../include" }
string/reverse.c
#include <string.h> #include "example.h" unsigned void reverse_string(char* str) { char* front = NULL; char* back = NULL; char tmp = 0; for (front = str, back = str + strlen(str) - 1; back > front; front++, back--) { tmp = *front; *front = *back; *back = tmp; } }
string/MakeKitBuild
make() { mk_group \ GROUP=string \ SOURCES="*.c" \ INCLUDEDIRS="../include" }
main.c
#include <stdio.h> #include <math.h> #include "values.h" #include "example.h" int main(int argc, char** argv) { char reverse[] = MESSAGE; reverse_string(reverse); printf("%s\n", MESSAGE); printf("Reversed: %s\n", reverse); printf("The square root of 2.0 is %f\n", sqrt(2.0)); printf("5! is %lu\n", factorial(5)); }
MakeKitBuild
SUBDIRS="math string" make() { mk_library \ LIB=example \ GROUPS="math/math string/string" mk_program \ PROGRAM=hello \ SOURCES="main.c" \ INCLUDEDIRS="include" \ LIBDEPS="m example" }
The mk_group
function accepts nearly the
same set of optional parameters as
mk_library
; in fact, you can even use
GROUPDEPS
to combine object groups
together hierachically.
This particular example also demonstrates a useful shortcut enabled by keeping source
files neatly separated into subdirectories: filename patterns. The invocations of
mk_group
in the math
and string
subdirectories can include all C source files with *.c
. In general,
MakeKit supports filename patterns whenever specifying
source files.
configure
is run. Files that are created by a
build rule will not be included.
Pre-compiled headers help reduce build times for large projects by avoiding parsing the same header files multiple times. C++ templates in particular tend to result in large header files, and pre-compiling them can be a substantial win for build times. MakeKit makes it easy to use pre-compiled headers with both gcc and clang.
The easiest way to use a pre-compiled header is by passing
PCH=
to
header
mk_program
,
mk_library
,
mk_dlo
, or
mk_group
. The
pre-compiled header will be included before all other headers
via the -include
parameter to the
compiler. If header
is also included
by source files, it should use a standard include guard.
Pre-compiled headers are highly sensitive to language (C or C++) and various compiler settings (e.g. PIC versus non-PIC code). Using this method will ensure that the pre-compiled header is built with compatible settings and for the correct language, but does not allow sharing the result between build targets.
You can use mk_pch
to
produce an explicit pre-compiled header. Capture the value
of result
and pass it as
PCH
to subsequent build targets.
It is up to you to ensure that it is built with compatible
settings. The following non-exhaustive list of parameters
can affect the result:
PIC
Must be no
(the default) for
programs and static libraries, and yes
for libraries and dynamically-loadable objects.
COMPILER
Since many header extensions are ambiguous, you should
manually specify c
or c++
to determine which compiler should be used to generate
the pre-compiled header.
Most projects of any significant complexity will need to determine how
and what source code to build based on the attributes of the host operating
system, available libraries, and so forth. To this end,
MakeKit provides an array of functions to use in
configure()
within your MakeKitBuild
files.
The idioms used to configure projects with MakeKit are very similar to autoconf's. This was an intentional design decision to ease migration.
Most configuration tests result in #define
statements being written
into a config header which you can include in your source files to make use of the test
results. You can set the current config header with mk_config_header
.
You can add #define
statements to the current config header with
mk_define
:
Example 3.6. Using mk_config_header
configure() { # Place #defines in include/config.h relative # to location of MakeKitBuild in source tree mk_config_header "include/config.h" # Define FOOBAR to 1 mk_define FOOBAR 1 # Define BUILD_TIME to current time as a string # using UNIX 'date' program mk_define BUILD_TIME "\"`date`\"" }
Most configuration checks do one or more of the following:
configure
The names of variables and defines are usually based on the item in question (e.g. the name of the header or library), with a few transformations:
*
(asterisk) characters are converted to P
.
These rules are the same as those used in autoconf. Hereafter, a
name derived by these rules will be referred to as a varname
.
Most of the configuration checks have two forms, singular and plural. The plural
form takes a list of items and performs the above steps for each item, and is the
most common and convenient way of performing a check. The singular form takes
only a single item has none of the side effects of the plural form. Instead, it
sets the variable result
to the result of the test, and
returns 0 (logical true in shell script) if the test succeeded, and 1 (logical false)
otherwise. This form is useful for writing more elaborate custom tests.
Many of the tests have 3 possible results: internal
,
external
, and no
. If the item being
checked for is provided by the current project -- for example, checking for
a library which is built in a preceding source directory -- the test will
trivially succeed with internal
. If the item was identified
as being provided by the operating system or build environment, such as a system
header, the result will be external
. If the item was not
found, the result will be no
.
To check for the presence of a header on the system, use
mk_check_headers
. This function takes
a list of header names and tests for the presence and usability
of each one by attempting to compile a small program that includes
it. For each header, it will do the following:
HAVE_varname
to the result of the test.
HAVE_varname
Example 3.7. Using mk_check_headers
configure() { mk_config_header "include/config.h" # Check for some basic header files mk_check_headers stdio.h stdlib.h string.h does-not-exist.h # On a reasonable system, the following variables will be set: # # HAVE_STDIO_H=external # HAVE_STDLIB_H=external # HAVE_STRING_H=external # HAVE_DOES_NOT_EXIST_H=no # # The following defines will be written into config.h: # # #define HAVE_STDIO_H # #define HAVE_STDLIB_H # #define HAVE_STRING_H }
If these side effects are undesirable, you can use the singular form
mk_check_header
.
To check for the presence of a library, use mk_check_libraries
.
For each library name specified, it will test for its presence and usability by
attempting to compile and link a test program against it. For each library
it will do the following:
HAVE_LIB_varname
to the result of the test.
LIB_varname
with the name of the library exactly as passed to mk_check_libraries
.
Otherwise, the variable will be set as the empty string. This is useful for conditionally
including the library in a LIBDEPS=
list to mk_program
and friends.
HAVE_LIB_varname
.
Library names should be specified the same way as when linking to them with
mk_program
, with no lib
prefix or
file extension.
Example 3.8. Using mk_check_headers
configure() { mk_config_header "include/config.h" # Check for some common UNIX libraries mk_check_libraries m pthread does-not-exist # On a typical Linux system, the following variables will be set: # # HAVE_LIB_M=external # HAVE_LIB_PTHREAD=external # HAVE_LIB_DOES_NOT_EXIST=no # LIB_M=m # LIB_PTHREAD=pthread # LIB_DOES_NOT_EXIST= # # The following defines will be written into config.h: # # #define HAVE_LIB_M # #define HAVE_LIB_PTHREAD }
If these side effects are undesirable, you can use the singular form
mk_check_library
.
mk_check_functions
allows you to test for the availability
of a list of functions by attempting to compile and link test programs that
reference them. For each function it will then do the following:
HAVE_varname
to the result of the test. The possible results are yes
and no
.
HAVE_varname
.
HAVE_DECL_varname
to 1. Otherwise, define it to 0.
Unlike autoconf, this check ensures that a declaration
of the function was available in addition to the function's symbol being
present in a library. You can use HEADERDEPS=
and
LIBDEPS=
parameters to mk_check_functions
to specify header files and libraries that are necessary for the function
to be found.
Each function can be specified as a simple name, or as a full function prototype in C syntax. In the second case, MakeKit will verify that the function exists and has a compatible prototype. Bear in mind that the resulting variable/define name will reflect any whitespace in your prototype.
Example 3.9. Using mk_check_functions
configure() { mk_config_header "include/config.h" # Check for some functions in libm mk_check_functions \ HEADERDEPS="math.h" \ LIBDEPS="m" \ sin cos "double sqrt(double)" does_not_exist # On a typical UNIX system, the following variables will be set: # # HAVE_SIN=yes # HAVE_COS=yes # HAVE_DOUBLE_SQRT__DOUBLE_=yes # HAVE_DOES_NOT_EXIST=no # # The following defines will be written into config.h: # # #define HAVE_SIN # #define HAVE_COS # #define HAVE_DOUBLE_SQRT__DOUBLE_ # #define HAVE_DECL_SIN 1 # #define HAVE_DECL_COS 1 # #define HAVE_DECL_DOUBLE_SQRT__DOUBLE_ 1 # #define HAVE_DECL_DOES_NOT_EXIST 0 }
If these side effects are undesirable, you can use the singular form
mk_check_function
.
mk_check_types
allows you to test for the availability
of a list of types by attempting to compile test programs that reference them.
For each type it will then do the following:
HAVE_varname
to the result of the test. The possible results are yes
and no
.
HAVE_varname
.
You can use a HEADERDEPS=
parameter to
mk_check_types
to specify header
files necessary to find the type definition.
Example 3.10. Using mk_check_types
configure() { mk_config_header "include/config.h" # Check for some types in stdlib.h and inttypes.h mk_check_types \ HEADERDEPS="stdlib.h inttypes.h" \ size_t int64_t uint64_t does_not_exist_t # On a typical UNIX system, the following variables will be set: # # HAVE_SIZE_T=yes # HAVE_INT64_T=yes # HAVE_UINT64_T=yes # HAVE_DOES_NOT_EXIST_T=no # # The following defines will be written into config.h: # # #define HAVE_SIZE_T # #define HAVE_INT64_T # #define HAVE_UINT64_T }
If these side effects are undesirable, you can use the singular form
mk_check_type
.
mk_check_sizeofs
allows you to test for the
sizes of a list of types. For each type it will then do the following:
SIZEOF_varname
to the result of the test.
SIZEOF_varname
to the result of the test.
You can use a HEADERDEPS=
parameter to
mk_check_types
to specify header
files necessary to find the type definitions. Note that
if this function fails to determine the size of a type
it will abort configuration, so you might want to first check
that the types exist with mk_check_types
.
Example 3.11. Using mk_check_sizeofs
configure() { mk_config_header "include/config.h" # Check for sizes of some types mk_check_types \ HEADERDEPS="stdlib.h inttypes.h" \ size_t int64_t "void*" # On a typical 32-bit UNIX system, the following variables will be set: # # SIZEOF_SIZE_T=4 # SIZEOF_INT64_T=8 # SIZEOF_VOIDP=4 # # The following defines will be written into config.h: # # #define SIZEOF_SIZE_T 4 # #define SIZEOF_INT64_T 8 # #define SIZEOF_VOIDP 4 }
If these side effects are undesirable, you can use the singular form
mk_check_sizeof
.