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.cWhen 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.cRunning 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.cRunning 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
%.endingas a target.%is a wild-card and can represent any name. - Use
%.other_endingas 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 4
echo 5 \
echo 6
echo 7is equivalent to
echo 1 echo 2 echo 3 echo 4
echo 5 echo 6
echo 7You 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 (
.hfor C;.h,.hpp,.Hor.hhfor 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
typedefs and#defines
- 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 (
.oon most systems,.objon 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 (
.soon Linux,.dllon Windows,.dylibon 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
libcand is defined by the C standard (see https://en.wikipedia.org/wiki/C_standard_library); there are several implementations of it, butglibcis probably the most pervasive on Linux. - Static Library
-
A file (
.aon most Unix-like systems,.libon 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 (
.cfor C,.cpp,.C, or.ccfor 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
typedefs. To be#included 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
.cfile into a.ofile; use the-fPICcompiler flag (position independent code
) so the resulting code can be shared by multiple applications.Use the
ldtool to combine the.ofiles. Since this is tricky to get right, your compiler (gccorclang) will have a simpler interface that callsldunder the hood, generally likeclang -shared \ all.o your.o object.o files.o \ -o libyourname.soNote that loaders often assume that shared library names begin with
liband end with.so, so you should abide by that pattern.
- Creating a static library
-
Compile each
.cfile into a.ofile.Use the
artool to turn a collection of.ofiles into an.afile, generally likear rcs \ libyouname.a \ all.o your.o object.o files.oNote that linkers often assume that static library names begin with
liband end with.a, so you should abide by that pattern.
Alternatively,
makeprovides 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
ranlibin 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
.cfile into a.ofile.Link your
.ofiles and.afiles together, with references to any needed.sofiles.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.ofiles explicitly, as well as thelibrary path
(directories where library files are located; specified with-L) andlibraries
(the part of library names betweenliband.; specified with-l)For example, the following code
clang \ all.o your.o object.o files.o \ -L../mylib -L./ \ -lflub -lflux -lflibber \ -o runmewill create a program named
runmeby combiningall.o,your.o,object.o, andfiles.owithlibflub.soorlibflub.a(whichever it can find),libflux.soorlibflux.a(whichever it can find), andlibflibber.soorlibflibber.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
.sofiles can be found at runtime (if they aren’t in a standard location).The
-Loption 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_PATHenvironment 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
makeon 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)