Automating the Build Process
Before You Start…
Form a group of about three or four people.
Then do the following:
- Pick a lab machine to work on. Make sure that you're all able to see the screens and potentially take over the keyboard.
- Find out your group number. Prof. Melissa will hand out a piece of paper with your group number on it.
- Have one person in the group open Visual Studio Code, and sign into the course server.
-
Choose the
option and select and navigate to either/cs70/fall2025/lab/build-tools/sect1/group
(for section 1), orN /cs70/fall2025/lab/build-tools/sect2/group
(for section 2).N
(Replace
with your actual group number, and note the leading slash; this code is not in your own directory.)N
Review the C++ Code
The directory contains a simple C++ program that consists of three source files:
cow.cpp
andcow.hpp
— code that defines aCow
classmain.cpp
— a small program that uses theCow
class
At this point you're pretty used to typing the commands to compile and link C++ programs. But it might feel tiresome to have to remember all the commands and type them in every time you want to build the program. Wouldn't it be nice if we could automate this process?
I know! Let's make a shell script to do it for us!
That's a good idea, but not everyone knows shell scripting. But everyone should know Python, so we'll use Python to automate the build process.
Review build-all.py
Build the code by running
python3 build-all.py
Confirm that the executable works by running ./main
.
Next, try modifying one of the C++ source files (e.g., add a cout
statement somewhere) and re-running python3 build-all.py
. Does it rebuild everything? Does it need to rebuild everything?
Since we're using Python, we could make the program smarter about what to build.
Yeah! We could make it only rebuild what it needs to.
Let's do that next.
Review rebuild.py
We're going to throw away build-all.py
and replace it with rebuild.py
, which is a smarter build script that only rebuilds what is necessary (but can also build everything for the first time, too).
Look over the code for rebuild.py
and run it by typing
python3 rebuild.py
Experiment with editing one of the C++ source files and confirm that it rebuilds exactly what is necessary. Try also just removing main
(with rm main
) and confirm it just does the linking step.)
When you're ready to move on, run
rm *.o main
Meh. This seems a bit hard-coded to our particular program. I want to do less work.
Hmm… Maybe we could have a separate file that's just a specification of what depends on what, and what to run to build each file?
Yeah, like a data file that says, “to build this file, you need these files, and you run this command”.
Try running make.py
A problem with rebuild.py
is that it's very much hard-coded to this particular program—what files depend on other files and what commands to run are baked into the code.
So make.py
is a generalization of the build process, which turns the dependencies into data. Look at the code and find the definition of buildRules
. It's a dictionary that specifies both what the prerequisite files are for a given file (i.e., what things it needs to compile) and the compilation command to run for that file.
The most important part of make.py
is the definition of buildRules
. Look it over and make sure you understand what it is saying. (There is a block comment above it that explains the format, but if you're unsure, ask!)
Next, read over the make
function. It's a little long, but it should be fairly self explanatory.
Don't spend a ton of time trying to understand every line of code. The important thing is to understand the overall structure and how it uses the data in
buildRules
to decide what to build and how to build it. The key thing is that it's a recursive function that builds the prerequisites of a file before building the file itself.
Then run
python3 make.py
make.py
is rather chatty about what it is doing and why. Read through the output that it produced explaining its actions. Then, as you did with rebuild.py
, you may want to try modifying a C++ source file and confirming that make.py
rebuilds exactly what needs to be rebuilt.
The specification in buildRules
also includes a rule for how to build a file called clean
—have a go at figuring out what will happen when you run it. Check your answer by running
python3 make.py clean
Is it good or bad that the file called clean
is never actually created by this rule? (FWIW, this is known as a “phony” target; the make algorithm always thinks it needs to do what's necessary to build this target.)
Getting to Know Makefiles — example1.mak
In 1976, after commiserating with his colleagues about the complexity of building programs, Stuart Feldman at Bell Labs wrote the first version of a C program called make
that does what our make.py
script does, with a few enhancements. It reads a file (known as a “makefile”) to get the same data that was in buildRules
. Check out example1.mak
; it's basically the same data, just in a different (and easier to type!) format.
Notice that the format is
target :prerequisites commands
where the
We're using cs70-make
here because it tells you why it's doing something in addition to what it's doing. You can use make
instead, but you'll only see the commands it runs.
You can see how a makefile works by running
cs70-make -f example1.mak
You can also just make a specific target; for example,
cs70-make -f example1.mak clean
runs the clean
target (which deletes files that were created when make
ran on some other target).
So I should run
make clean
every time I want to build stuff, right?No! That would require rebuilding everything each time, which throws away the time savings we get by only rebuilding things that have changed.
You only want to run
make clean
if the system seems to be very confused about what to build, or maybe at the end of a session so that you can see what files you've changed or added more easily when checking them in, or at the end of a project so you can share a copy of your code without going through GitHub.
Feel free to experiment with changing files and making sure that cs70-make
only rebuilds what is necessary.
Normally, Your Makefile Would Be Named Makefile
For almost any project you work on, the default name for makefiles is Makefile
(note the capital “M”). make
(and cs70-make
) will expect to find Makefile
if run without the -f
option.
We're using the -f
option here because we want to be able to use more than one makefile to demonstrate different sets of features that make
supports.
More make
features — example2.mak
Look over example2.mak
and try it out. This makefile adds macros (a.k.a. variables) to avoid having to copy and paste the same text in multiple places.
In the makefile, we can define a macro like this:
CXXFLAGS = -g -Wall -std=c++17
The variable CXXFLAGS
is now bound to the text -g -Wall -std=c++17
. Then, elsewhere in the makefile (basically anywhere), we can use $(CXXFLAGS)
to substitute that text. Now if we want to change the compilation flags, we only have to change them in one place.
We've also added another phony target called all
. This commonly provided target is most useful when we have several executables (or other components, like documentation) that we want to build as we can list all our executables as prerequisites for all
.
If you don't specify a target on the command line, make
defaults to building the all
target's dependencies.
This is getting pretty fancy. Are there even MORE features?
Yes, but these are totally optional. Some people love them and others find they make their head spin. Take a look and you can decide how you feel about them.
Advanced make
features — example3.mak
Look over example3.mak
and try it out. It uses some even more advanced make
features that allow us to type even less code.
Automatic Variables
When writing the build commands for a rule, we often want to refer to the target and prerequisites of that rule. make
provides automatic variables for this purpose:
$@
— the target of the rule (the@
sign looks a bit like a target)$<
— the first prerequisite of the rule (the<
points to the left towards the first prerequisite)$^
— all the prerequisites of the rule (the^
points up to all the prerequisites listed above)
Thus, instead of writing
cow.o: cow.cpp cow.hpp $(CXX) $(CXXFLAGS) -c cow.cpp
we can write
cow.o: cow.cpp cow.hpp $(CXX) $(CXXFLAGS) -c $<
meaning “compile the first prerequisite to make the target”.
Similarly, for
main: main.o cow.o $(CXX) $(CXXFLAGS) -o main main.o cow.o
we can write
main: main.o cow.o $(CXX) $(CXXFLAGS) -o $@ $^
meaning “link all the prerequisites to make the target”.
Hay! If I do that, all my rules will look the same!
That's where suffix rules come in.
Suffix Rules
In a suffix rule, we say “here's how to transform a .xxx
file into a .yyy
file”. For example, we can say “here's how to transform a .cpp
file into a .o
file”:
.cpp.o: $(CXX) $(CXXFLAGS) -c $<
This special kind of rule says “if no one gave you any build commands for building a particular .o
file from a .cpp
file, use these commands”.
So you can just define a single suffix rule for .cpp
to .o
and then you only need to write the dependencies for each .o
file, as you can see in example3.mak
.
Actually, suffix rules are a bit old-fashioned. Today, most people use GNU
make
, which introduced more advanced pattern rules that are more flexible. But while pattern rules require GNUmake
, every version ofmake
(e.g., BSDmake
) supports suffix rules, so they're more portable.Note that
cs70-make
supports suffix rules, but not pattern rules. Your makefiles in CS 70 must work withcs70-make
, so you can only go so far with advancedmake
features.
A Handy Trick
Try running
clang++ -std=c++17 -MM *.cpp
Do you see how this output could be handy when creating a makefile?
Want More Information or Another Perspective?
The Working with Makefiles help page provides a summary of the key aspects of Makefiles, using a different program as the running example.
If you find other helpful resources, please share them with the class on Piazza!
Other Build Systems
There are other build systems besides make
, although make
(and, in particular, GNU make
) is probably the most commonly used one, especially for free/open source software, and especially on Unix/Unix-like systems.
Other languages also have their own build systems. But make
is the inspiration for them all.
(When logged in, completion status appears here.)