MYF

[RA] Ch37.1 Build Tool: make

Reading Assignment: All of Programming Ch37.1 Build Tool: make

Useful Resources:

For large projects, we typically want to recompile only the files that have actually changed, or those that depend on a file that has change(e.g. those that include a header file which has changed). We could try to track this manually and only recompile those files. That’s why we introduce make to manage the task for us.

The input to make is a Makefile, which contians one or more rules that specify how to produce a target from its prerequisites. For, example:

1
2
rectangle: rectangle.c
gcc -o rectangle -pedantic -std=gnu99 -Wall -Werror rectangle.c

In this example, rectangle in the first line is the target specification, rectangle.c is the lists of prerequisites, in this case, this file is the only one prerequisite, if you have many, it should be separated with a single space. In the second line, there is the command required to rebuild that target from the prerequisites. The commands may appear over multiple lines, however, each line must begin with a TAB character.

When you run make, you can specify a particular target to build, it will automatically build the first target if you have many. To build the target, make will first check if is up-to-date. Then make checks if the target needs to be (re)built. If any dependency is new that the target file, then the target is out-of-date, and must be rebuilt.

Variables

You could put the compiler options in a variable, and use that variable in each of the relevant rules.

1
2
3
4
5
6
7
CFLAGS=-std=gnu99 -pedantic -Wall
myProgram:oneFile.o anotherFile.o
gcc -o myProgram oneFile.o anotherFile.o
oneFile.o:oneFile.c oneHeader.h someHeader.h
gcc $(CFLAGS) -c oneFile.c
anotherFile.o:anotherFile.c anotherHeader.h someHeader.h
gcc $(CFLAGS) -c anotherFile.c

Clean

It is a target intended to remove the compiled program, all object files, all editor backups, and any other files that you might consider to be clustery.

Usage:

  • force the entire program to be rebuilt
  • clean up the directory

We might add a clean target to our Makefile as follows:

1
2
3
.PHONY: clean
clean:
rm -f myProgram *.o *.c~ *.h~

Note that the .PHONY: clean tells make that clean is a phony target, which means we don’t actually expect it to create a file called clean, nor would we want to consider it up to date and skip its commands if a file called “clean” already existed. If we want other phony targets, we would list them all as if they were prerequisites of the .PHONY target.

Generic Rules

In make, we can write generic ruls. We can build (something).o from (something).c.

1
2
3
4
5
6
7
8
CFLAGS=-std=gnu99 -pedantic -Wall
myProgram:oneFile.o anotherFile.o
gcc -o myProgram oneFile.o anotherFile.o
%.o:%.c
gcc $(CFLAGS) -c $<
.PHONY:clean
clean:
rm -f myProgram *.o *.c~ *.h~

It specifies how to make a file ending with .o from a file of the same name, except with .c instead of .o. We have to use the special built-in variable $<, which make will set to the name of the first prerequisite of the rule.

However, there is a significant problem here. We have made it so that our object files no longer depend on the relevant header files. If we were to change a header file, then make might not rebuild all of the relevant object files. We could make every object file depend on every header file, however, it will rebuild everything we need to when we change a header file, because we would rebuild every object file, even if we only need to rebuild a few.

We can fix this by adding extra dependency information to our Makefile:

1
2
3
4
5
6
7
8
9
10
11
# This fixed the problem
CFLAGS=-std=gnu99 -pedantic -Wall
myProgram:oneFile.o anotherFile.o
gcc -o myProgram oneFile.o anotherFile.o
%.o:%.c
gcc $(CFLAGS) -c $<
.PHONY:clean
clean:
rm -f myProgram *.o *.c~ *.h~
oneFile.o: oneHeader.h someHeader.h
anotherFile.o: anotherHeader.h someHeader.h

Managing all of the dependency information by hand would be tedious and error-prone, and we also have to figure out every file which is transitively included by each source file, and keep the information up to date as the code changes. Therefore, makedepend which will eidt Makefile to put all of this information at the end comes. See man makedepend page for more details.

Built-in Generic Rules

Some generic rules are so common that they are built into make, and we do not even have to write them. We can see all of the rules(including both those that are built-in and those that are specified by the Makefile) by using make -p. Doing so also builds the default target as usual. If we want to avoid building anything, we can do make -p -f /dev/null to use the special file /dev/null as our Makefile.

Built-in Functions

We can use some of make‘s built-in functions to automatically compute the set of .c files in the current directory and then to generate the list of target object files from that list. The syntax of function calls in make is $(functionName, arge1, arg2, arg3). We can use the $(wildcard pattern) function to generate the list of .c files in the current directory: SRC = $(wildcard *.c). Then we can use the $(patsubst pattern, replacement *.c). Then we can use the $(patsubst pattern, replacement, text) function to repalce the .c endings with .o endings: OBJS = $(patsubst %.c, %.o, $(SRCS)). Once we have done this, we can use $(SRCS) and $(OBJS) in our Makefile.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# Multiple .o file(Using the pattern substitution)
CC=gcc
CFLAGS=-std=gnu99 -pedantic -Wall
SRCS=$(wildcard *.c)
OBJS=$(patstubst %.c, %.o, $(SRCS))
myProgram: $(OBJS)
gcc -o $@ $(OBJS)
.PHONY: clean depend
clean:
rm -f myProgram *.o *.c~ *.h~
depend:
makedepend $(SRCS)
anotherFile.o: anotherHeader.h someHeader.h
oneFile.o: oneHeader.h someHeader.h

Parallelizing Compilation

If you give make the -j option, it requsts that it run as many tasks in parallel as it can. On large projects, this many make a significant difference in how long a build takes.

1
-j8 # runs up to 8 tasks in parallel

…And Much More

In fact, you can use make for pretty much any task that you can describe in terms of creating targets from the prereuisites that they depend on. For most such tasks, you can put the parallelization capabilities of make to good use to speed up the task.