Changelog:
- 18 Jan 2023: add description of setting runtime search paths
- 18 Jan 2023: avoid indicating that most Makefiles must define particular common variables
- 24 Jan 2023: talk about LDLIBS/LIBS in addition to LDFLAGS; link to ninja build system
1 The make
tool
make
is a tool that reads a file (called a makefile,
named Makefile
by default) and decides what commands it
need to run to update your project. To avoid redundant commands when
only a small part of the project has been updated, make
checks file modifications times when it determines which commands to
generate. A well-constructed makefile can simplify building and running
code on many platforms.
1.1 Rules
The key component of a makefile is a rule, consisting of three parts:
- A target, the name of a file that will be created or updated by the rule.
- A list of dependencies (or
prerequisites
): if the target is missing or is older than at least one dependency, the system commands will be run. - A list of system commands (or the
recipe
): shell commands that should result in (re)making the target
The target must begin a line (i.e., must not be indented) and end in a colon. Anything after that colon is a dependency. Indented lines that follow (which must be indented with a single tab character, not spaces) are systems commands.
The following rule will build hello.o
if either
hello.c
or hello.h
is newer than
hello.o
. Otherwise, it will do nothing.
hello.o: hello.c hello.h
clang -c hello.c
When you run make
it reads Makefile
and
executes the first rule it finds. If you want to run a
different rule, you can give its target as an argument to
make
.
Consider the following Makefile
:
hello.o: hello.c hello.h
clang -c hello.c
bye.o: bye.c bye.h
clang -c bye.c
Running make
will ensure hello.o
is up to
date. Running make bye.o
will ensure bye.o
is
up to date.
If a dependency of an executed rule is the target of another rule, that other rule will be executed first.
Consider the following Makefile
:
runme: hello.o bye.o main.c main.h
clang main.c hello.o bye.o -o runme
hello.o: hello.c hello.h
clang -c hello.c
bye.o: bye.c bye.h
clang -c bye.c
Running make
will ensure runme
is up to
date; since runme
’s dependencies include
hello.o
and bye.o
, both of those rules will
also be executed.
1.2 Macros
Makefiles routinely use variables (which they call macros) to separate out the list of files from the rules that make them, as well as to allow easy swapping out of different compilers, compilation flags, etc.
Variables are defined with NAME := meaning
syntax. Traditionally, variable names are in all-caps. Variables are
used by placing them in parentheses, preceded by a dollar sign:
$(NAME)
.
Some variables names that are very common to find in Makefiles include:
Name | example | notes |
---|---|---|
CC | clang | The C compiler to use |
CFLAGS | -O2 -g | Compile-time flags for the C compiler |
LDFLAGS | -static | Link-time flags for the compiler (typically placed before list of object files on command line) |
LIBS or LDLIBS | -lm | Link-time flags representing libraries for the compiler (typically placed after list of object files on command line) |
CXX | clang++ | The C++ compiler to use |
CXXFLAGS | -O2 -g | Compile-time flags for the C++ compiler |
Even if you do not need linker flags or libraries, it’s still common
to define a blank LDFLAGS :=
or LIBS :=
and
use $(LDFLAGS)
or $(LIBS)
in all linking
locations so that if you later realize you need to link to an external
library (like the math library, -lm
), you can easily do
so.
1.3 Targets that aren’t files
Two of these we will find useful:
You can have a (set of) targets that do not represent files, commonly
including all
and clean
. These are specified
by having the line .PHONY: all clean
, and then using
all
and clean
like regular targets. You
should always have your first rule be named all
and have it do the main
task (i.e., building the program or
library you are providing). You should always have a rule named
clean
that removes all files that are built by
make
.
1.4 Pattern rules
You will often want a few pattern rules
using a few
automatic variables
. There are a lot of things you
could learn about these, but a simple version will often
suffice:
- Use
%.ending
as a target.%
is a wild-card and can represent any name. - Use
%.other_ending
as a dependency.%
has the same meaning it did in the target. - Use
$<
in the system commands as the name of the first dependency. - Use
$@
in the system commands as the name of the target.
Many makefiles will include a command like
%.o: %.c %.h config.h
$(CC) -c $(CFLAGS) $< -o $@
In other words,
Part | Meaning |
---|---|
%.o |
To make any .o file |
: %.c |
from a C file of the same name |
%.h config.h |
(with its .h file and the file
config.h as extra dependencies) |
$(CC) |
use our C compiler |
$(CFLAGS) |
with our compile-time flags |
$< |
to build that .c file |
-o $@ |
and name the result the name of our target. |
2 A bit about
bash
Recall that each line of bash
begins with a program and
is followed by any number of arguments. bash
also has
various control constructs and special syntax that can be used where
programs are expected, like for
and x=
, which
we will not cover in this class. It can also redirect input and output
using |
, >
, <
,
>>
, 2>
, and a few others.
Because new-lines are important to bash
, but sometimes
line breaks help reading, you can end a bash
line with
\
to say I’m not really done with this line.
Thus
echo 1 echo 2 \
\
echo \
3
echo 4echo 5 \
echo 6echo 7
is equivalent to
echo 1 echo 2 echo 3 echo 4
echo 5 echo 6
echo 7
You can also combine lines; a ;
is (almost) the same as
a new-line to bash
.
3 Multi-file project design
It is possible to have a compiler read every file involved in a project and from them create one big binary. However, that is generally undesirable, as it means both that the build process takes time proportional to the total project size and that the resulting binary can get quite large.
3.1 Glossary
- Header File
-
A file (
.h
for C;.h
,.hpp
,.H
or.hh
for C++) that provides- the signatures of functions, but not their definitions
- the names and types of global variables, but not where in memory they go
typedef
s and#define
s
- Library
- A collection of related implementations, generally provided as both a library file and a header file.
- Library File
- Either a shared library file or a static library file.
- Object File
-
A file (
.o
on most systems,.obj
on Windows) that contains assembled binary code in the target ISA, with metadata to allow the linker to place it in various locations in memory depending on what other files it is linked with. - Shared Library
-
A file (
.so
on Linux,.dll
on Windows,.dylib
on OS X) that contains one more more object files together with metadata needed to connect them into a running code system by the loader. This load-time linkage means that (a) multiple running programs can share the same copy of the shared library, saving memory; and (b) the library can be updated independently of the programs that use it, ideally allowing modular updates.Because they are linked by the loader each time the program is run, your OS needs to (a) have a copy of the library files and (b) know where they are in order to run a program that uses shared libraries.
- Standard Library
-
A library to which every program is linked by default. Virtually every language has a standard library, which does much to define the character of the language.
The C language’s standard library is typically called
libc
and is defined by the C standard (see https://en.wikipedia.org/wiki/C_standard_library); there are several implementations of it, butglibc
is probably the most pervasive on Linux. - Static Library
-
A file (
.a
on most Unix-like systems,.lib
on Windows) that contains one more more object files together with metadata needed to connect them into a running code system by the linker. This compile-time linkage means that each executable has its own copy of the library stored internally to the file, without external dependencies. - Source File
-
A file (
.c
for C,.cpp
,.C
, or.cc
for C++) that provides- the implementation of functions
- the declaration and implementation of provided helper functions
- the memory in which to store global variables
3.2 Common patterns
- A project
-
consists of several source files, header files, and dependencies
- header file
-
Contains the declaration of functions and shared global variables, but
not their definitions. May also contain various
typedef
s. To be#include
d by any file that wants to use those functions, etc. - source file
- Contains the definitions of closely related functions and global variables. May also declare helper functions that are not exported in a header file.
- dependency
- A separate project that creates a library used by this project
Projects generally are designed to either create a library or a program, but not both. This lab is an exception, having both a library and a program using it in one project to keep the lab short enough to be manageable.
- Creating a shared library
-
Compile each
.c
file into a.o
file; use the-fPIC
compiler flag (position independent code
) so the resulting code can be shared by multiple applications.Use the
ld
tool to combine the.o
files. Since this is tricky to get right, your compiler (gcc
orclang
) will have a simpler interface that callsld
under the hood, generally likeclang -shared \ \ all.o your.o object.o files.o -o libyourname.so
Note that loaders often assume that shared library names begin with
lib
and end with.so
, so you should abide by that pattern.
- Creating a static library
-
Compile each
.c
file into a.o
file.Use the
ar
tool to turn a collection of.o
files into an.a
file, generally likear rcs \ \ libyouname.a all.o your.o object.o files.o
Note that linkers often assume that static library names begin with
lib
and end with.a
, so you should abide by that pattern.
Alternatively,
make
provides special syntax for making a library:- Use the library name as the target
- Use the library name followed by an object file in parentheses as the dependencies
- Use the command
ranlib
in the system command
libname.a: libname.a(your.o) libname.a(object.o) libname.a(files.o) ranlib libname.a
- Creating a program
-
Compile each
.c
file into a.o
file.Link your
.o
files and.a
files together, with references to any needed.so
files.The tool that actually does this is
ld
, but you should use your compiler’s wrapper around that instead. The compiler needs to be told all your.o
files explicitly, as well as thelibrary path
(directories where library files are located; specified with-L
) andlibraries
(the part of library names betweenlib
and.
; specified with-l
)For example, the following code
clang \ \ all.o your.o object.o files.o -L../mylib -L./ \ -lflub -lflux -lflibber \ -o runme
will create a program named
runme
by combiningall.o
,your.o
,object.o
, andfiles.o
withlibflub.so
orlibflub.a
(whichever it can find),libflux.so
orlibflux.a
(whichever it can find), andlibflibber.so
orlibflibber.a
(whichever it can find), looking for each library in the directories../mylib/
and./
as well as the system-wide library path (on Linux, usually/lib
,/usr/lib/
and additional directories specified by/etc/ld.so.conf
).Make sure any needed
.so
files can be found at runtime (if they aren’t in a standard location).The
-L
option only affects where the shared libraries are found while building the executable. So, for shared libraries, some additional steps may be needed the library will also be found when the program is done. On Linux, some ways of doing this include setting theLD_LIBRARY_PATH
environment variable or including a runtime library search path in the executable using a link-time option like-Wl,-rpath=/path/to/mylib
.
4 Some additional references
On make in particular:
- The manual for
GNU make (the version of
make
on the department machines and most Linux systems). - The POSIX specification for make.
On build systems:
- Some selected other build systems for C/C++ (when make isn’t enough, maybe):
On linking and libraries:
- Ulrich Drepper,
How To Write Shared Libraries
(2011) (also provides an explanation of how dynamic linking works on Linux)