Table of Contents
The first and most well-known build tool is make. Stuart Feldman authored make in 1976 in Bell Labs. The idea of make is that while build can be done using a shell script, a shell script executes all build commands uncondionally. In a small project consisting of a single C language source file, this is not a problem. However, larger projects can consist of multiple C source files, for example Linux 6.17-rc7 contains 35778 source files, not counting header files. If in a large project a small change is made, it should affect only on the code that is dependent on it. For example, if a C source file is changed, only that source file should be recompiled and any .a archive, shared library or executable binary dependent on it should be re-linked.
A simple Makefile could be as follows:
.PHONY: all all: myprog CC=cc CFLAGS=-O1 myprog: myprog.c $(CC) $(CFLAGS) -o $@ $^
Here $@ refers to the target name, wheras $^ contains all dependencies. Instead of $^, the first dependency can be referred to with $<.
In this case, myprog is re-built only if nonexistent, or if its timestamp is before the modification timestamp of myprog.c.
A problem immediately appears. The file myprog does not depend on Makefile. So if CFLAGS is modified, myprog won't be rebuilt. The answer to this generally is two-fold.
Firstly, a target clean is added to clean everything:
.PHONY: all clean all: myprog CC=cc CFLAGS=-O1 clean: rm -f myprog myprog: myprog.c $(CC) $(CFLAGS) -o $@ $^
Then if CFLAGS is modified, the user can run "make clean" and then "make". This demonstrates that the all-important "make clean" must be executed very often with make-based build systems. This also demonstrates that it's all too easy to forget something from "make clean". For example, in this simple Makefile, if a new target secondprog is added, it needs to be added to its rule, but also to "make clean" rule which is often forgotten.
Another possibility is adding dependency to Makefile, but this requires some changes so that it's not given to CC as an argument:
.PHONY: all clean all: myprog CC=cc CFLAGS=-O1 clean: rm -f myprog myprog: myprog.c Makefile $(CC) $(CFLAGS) -o $@ $(filter %.c,$^)
Note that filter is a GNU make extension, and may not be available in other implementations of make.
However, with both approaches, the "solution" is to clean or re-build everything if the Makefile is changed. This is a problem in cases where the Makefile is huge, but only one command to build one target is modified. Both "solutions" in this case recompile everything, not just the target that had a change in its command.
In larger projects, it is generally preferred to not compile binary from C sources directly, but instead create object files that are then linked:
.PHONY: all clean all: myprog CC=cc CFLAGS=-O1 clean: rm -f myprog rm -f myprog.o mylib.o myprog: myprog.o mylib.o Makefile $(CC) $(CFLAGS) -o $@ $(filter %.o,$^) myprog.o: myprog.c Makefile $(CC) $(CFLAGS) -c -o $@ $< mylib.o: mylib.c Makefile $(CC) $(CFLAGS) -c -o $@ $<
Now there are two unclean approaches in this Makefile. First is that the information about what object files are there is repeated to three locations: the rule of the object file, the linking rule, and the "make clean" phony rule. The second is that if there are N object files, the command to build them is repeated N times. These can be solved with some GNU make extensions:
.PHONY: all clean all: myprog CC=cc CFLAGS=-O1 OBJS=myprog.o mylib.o clean: rm -f myprog rm -f $(OBJS) myprog: $(OBJS) Makefile $(CC) $(CFLAGS) -o $@ $(filter %.o,$^) $(OBJS): %.o: %.c Makefile $(CC) $(CFLAGS) -c -o $@ $<
Now the list of object files is specified only once, and the rule to build an object from a source is common to all object files. Note that already at this stage, the standard POSIX make is not enough, an GNU extensions have to be used.
However, this more complex Makefile suffers from the fact that compiling myprog.c into myprog.o most likely requires inclusion of a header file, mylib.h. So myprog.o depends on mylib.h. This can be specified as an additional dependency in the Makefile:
.PHONY: all clean all: myprog CC=cc CFLAGS=-O1 OBJS=myprog.o mylib.o clean: rm -f myprog rm -f $(OBJS) myprog: $(OBJS) Makefile $(CC) $(CFLAGS) -o $@ $(filter %.o,$^) $(OBJS): %.o: %.c Makefile $(CC) $(CFLAGS) -c -o $@ $< myprog.o: mylib.h
But however, this approach does not scale. Every time a C file is modified and a new header file is added, it must be documented in the Makefile. Worse, a header file can include other headers, and all of them should be documented in the Makefile. Clearly, something better is needed.
Fortunately, most C compilers support outputting dependencies, which can be imported:
.PHONY: all clean all: myprog CC=cc CFLAGS=-O1 OBJS=myprog.o mylib.o DEPS=$(patsubst %.o,%.d,$(OBJS)) clean: rm -f myprog rm -f $(OBJS) $(DEPS) myprog: $(OBJS) Makefile $(CC) $(CFLAGS) -o $@ $(filter %.o,$^) $(OBJS): %.o: %.c %.d Makefile $(CC) $(CFLAGS) -c -o $@ $< $(DEPS): %.d: %.c Makefile $(CC) $(CFLAGS) -MM -o $@ $< -include $(DEPS)
So the C compiler then creates the files myprog.d mylib.d that both myprog.o and mylib.o depend on mylib.h. This is fully automatic. Before a build has been made, the .d dependency files don't exist, but then everything must be initially compiled anyway. In this case, -MM means omit system header files. With -M only, system header file dependencies would be stored in the .d files.
Also notice that instead of include, you use -include. This is because the dependency files don't exist the first time make is run.
Is the Makefile now good enough? No, because if mylib.h is renamed to mylibpublic.h, then the .d files contain reference to the old mylib.h and make notices it can't find that file. So an empty phony rule is needed to affect make in such a manner that it doesn't exit when finding a reference to a nonexistent header file. Fortunately, the -M option has -MP which adds the phony rules and now "make clean" is not necessary if a header is renamed:
.PHONY: all clean all: myprog CC=cc CFLAGS=-O1 OBJS=myprog.o mylib.o DEPS=$(patsubst %.o,%.d,$(OBJS)) clean: rm -f myprog rm -f $(OBJS) $(DEPS) myprog: $(OBJS) Makefile $(CC) $(CFLAGS) -o $@ $(filter %.o,$^) $(OBJS): %.o: %.c %.d Makefile $(CC) $(CFLAGS) -c -o $@ $< $(DEPS): %.d: %.c Makefile $(CC) $(CFLAGS) -MM -MP -o $@ $< -include $(DEPS)
However, this is not all that needs to be done. The .d files aren't updated if a header file changes, only the .o files are updated. Fortunately, most C compilers have an option to solve this: -MT:
.PHONY: all clean all: myprog CC=cc CFLAGS=-O1 OBJS=myprog.o mylib.o DEPS=$(patsubst %.o,%.d,$(OBJS)) clean: rm -f myprog rm -f $(OBJS) $(DEPS) myprog: $(OBJS) Makefile $(CC) $(CFLAGS) -o $@ $(filter %.o,$^) $(OBJS): %.o: %.c %.d Makefile $(CC) $(CFLAGS) -c -o $@ $< $(DEPS): %.d: %.c Makefile $(CC) $(CFLAGS) -MM -MP -MT $*.d -MT $*.o -o $@ $< -include $(DEPS)
Now the .d files finally work just fine. However, there were a lot of difficulties in the road to make the ultimate dependency file mechanism.