Automating the Build Process
Getting Started
Get together with your partner and clone this GitHub repository.
Review the C++ Code
cd into the build-automation-exercise folder, where you'll find a number of files. For now, let's just look at the three files
cow.cpp— code that implements aCowclasscow.hpp— code that defines aCowclassmain.cpp— a small program that uses theCowclass
At this point you're pretty used to typing the commands to compile and link C++ programs. But it's tiresome to have to remember all the right commands and options 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
We can build the program by running the included build-all.py file, with
python3 ./build-all.py
When it's finished, run
./main
to confirm that the executable works.
Modify the Code
Next, try modifying one of the C++ source files (e.g., add a cout statement somewhere) and then rebuild the executable by running build-all.py again.
rebuild.py
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.
We're going to throw away build-all.py and replace it with rebuild.py, which is a smarter build script that will build all the parts of a program if it hasn't been built before, but that will only recompile the files that have changed on future runs.
Look over the code for rebuild.py and run it by typing
python3 ./rebuild.py
As above, run the executable to make sure it works properly.
Modify the Code
Experiment by editing one of the C++ source files and confirm that rebuild.py recompiles and relinks only the parts that have changed.
Remove the Executable
Now try removing the main executable the script built with
rm main
and then run rebuild.py again and verify that it only does the linking step to create the executable file without recompiling any of the other code.
make.py
Meh. So we have to write a separate Python program to build every C++ program we write? I thought we were trying 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”.
The problem with rebuild.py is that it's very much hard-coded to this particular program—its understanding of what files depend on other files and what commands it needs to run are baked into the Python code.
In contrast, make.py is a script that generalizes the build process. It lets us define the dependencies in a compact form that the script can use to decide what to do.
buildRules{}
The most important part of make.py is the definition of the buildRules dictionary, which encodes the build process for each component of our program.
# Unlike build.py, now we've got DATA describing our specific project, and
# generic code that understands that data. For each file, we have two bits
# of information, the prereqs for the file (a list of files it depends on),
# and the command to run.
#
# The dictionary below maps FILENAME -> (LIST_OF_PREREQS, BUILD_COMMAND)
buildRules = {
"main": (('main.o', 'cow.o'),
"clang++ -std=c++17 -o main main.o cow.o"),
"cow.o": (('cow.cpp', 'cow.hpp'),
"clang++ -Wall -Wextra -pedantic -c -std=c++17 cow.cpp"),
"main.o": (('main.cpp', 'cow.hpp'),
"clang++ -Wall -Wextra -pedantic -c -std=c++17 main.cpp"),
"clean": ((),
"rm -f *.o main"),
}
Each key in the dictionary corresponds to the name of a file we want to produce, and the value specifies both what files we need before we can produce our key file and the compilation or linker command to run to produce the file from those prerequisites.
make
Next, read over the make function:
# --- The build algorithm, a recursive function called make ---
def make(file):
iprint(f'- Making {file}', +1) # +1 => Indent subsequent output
if file not in buildRules:
# If a file doesn't have a rule, it's fine if it already exists
if doesntExist(file):
error(f"Couldn't make '{file}' (no rule, and doesn't it exist)!")
iprint(f' + Okay, no rule found, but file exists!', -1)
return
# Okay, the file does have a rule, get the prereqs and command to run
(prereqs, cmd) = buildRules[file]
iprint(f'- Found rule for {file}, prereqs: {" ".join(prereqs)}')
# Before going further, make sure all prereqs are up to date
# before checking this file (recursively run make on prereqs)
for prereq in prereqs:
make(prereq)
# Now that prereqs are up to date, see if we need to build this file
mustBuildReason = None
if doesntExist(file):
mustBuildReason = "it doesn't exist"
else:
for prereq in prereqs:
if isNewer(prereq, file):
mustBuildReason = "prereqs are newer"
# If we do need to build the file (and have a command to run), do so.
if mustBuildReason is not None:
iprint(f'- Target {file} must be built because {mustBuildReason}.')
if cmd is not None:
iprint(f'- Building: {file}')
run(cmd)
iprint(f'+ Made {file}')
else:
iprint(f"+ Target {file} doesn't need to be rebuilt (no newer prereqs)")
# Set indentation back to what it was before and return
iprint(None, -1)
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 the
makefunction uses the data inbuildRulesto decide what to build and how to build it.
Your key takeaway is that
makeis a recursive function that builds the prerequisites of a file before building the file itself.
Run make.py
Run make.py with
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 one of the C++ source files and confirm that make.py rebuilds exactly what needs to be rebuilt and no more.
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.
To check your answer, run
python3 ./make.py clean
Hay! It deleted the object files and the executable, but where's the
cleanfile?
Hmm. I suppose that calling the key a “file” is a bit misleading.
A more general term would be to call it a “target”—the thing we're aiming to get..
Target does work better, but in this case,
cleanis what's called a “phony target”. There aren't any prerequisites listed, so the make algorithm will always run the code to “build” this target if it's asked to do so.
But nothing depends on
clean, so it only runs if we specifically tell it to run.
Exactly.
Meh. So now we have to copy this script and edit the dictionary for every program we write?
Well…
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). make reads a separate file (known as a “makefile”) to get the same data that was in buildRules, so you don't have to copy a script around and modify it, and the makefile format is a bit easier to type.
Check out example1.mak; it's basically the same data, just in a different form:
# This is a simple Makefile, it exactly mirrors the data we saw in make.py
#
# Use it by running:
# cs70-make -f example1.mak
# or
# cs70-make -f example1.mak main
#
# (or specify any other target from the the file)
main: main.o cow.o
clang++ -g -std=c++17 -o main main.o cow.o
cow.o: cow.cpp cow.hpp
clang++ -g -Wall -Wextra -pedantic -c -std=c++17 cow.cpp
main.o: main.cpp cow.hpp
clang++ -g -Wall -Wextra -pedantic -c -std=c++17 main.cpp
clean:
rm -f *.o main
target :prerequisites commands
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 cleanevery 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 cleanif 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-makesupports suffix rules, but not pattern rules. Your makefiles in CS 70 must work withcs70-make, so you can only go so far with advancedmakefeatures.
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.
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.)