Homework 3: Make Your Makefile
Now it's time to make your own Makefile! The Makefile will contain all the compilation and linking steps to make an executable called our-movie. You and your partner (together or individually) should complete the written questions in Part 1 before working on this part.
In the asciimation directory, you'll find quite a number of files. Specifically, you'll find:
boundingbox.cppandboundingbox.hpp: The implementation and header files for theBoundingBoxclass.boundingbox-test.cpp: A test program for theBoundingBoxclass.sprite.cppandsprite.hpp: The implementation and header files for theSpriteclass.sprite-test.cpp: A test program for theSpriteclass.display.cppanddisplay.hpp: The implementation and header files for theDisplayclass.display-test.cpp: A test program for theDisplayclass.asciimation.cppandasciimation.hpp: The implementation and header files for theAsciimationclass.numsprites.hpp: A header file containing a constant indicating the number of sprites in the movie.our-movie.cpp: The main program that runs the movie.Makefile: A partially completed Makefile that you will finish.spriteImages/: A directory containing text files with ASCII art images for sprites.
Overall, there are four executables we can build (three test programs and the final movie program).
Reminder: Compiling by Hand
If we wanted to build each of the executables by hand, we would need to run the following commands to build boundingbox-test:
clang++ -c -g -std=c++17 -pedantic -Wall -Wextra boundingbox.cpp
clang++ -c -g -std=c++17 -pedantic -Wall -Wextra boundingbox-test.cpp
clang++ -o boundingbox-test boundingbox-test.o boundingbox.o -ltestinglogger
(You can try pasting the above commands into your terminal in the asciimation directory to see that they work and build the boundingbox-test executable.)
Similarly, we would run the following commands to build sprite-test (which will generate a slew of warning and error messages because the Sprite class isn't fully implemented yet):
clang++ -c -g -std=c++17 -pedantic -Wall -Wextra boundingbox.cpp
clang++ -c -g -std=c++17 -pedantic -Wall -Wextra sprite.cpp
clang++ -c -g -std=c++17 -pedantic -Wall -Wextra sprite-test.cpp
clang++ -o sprite-test sprite-test.o sprite.o boundingbox.o -ltestinglogger
We can build the display-test executable with:
clang++ -c -g -std=c++17 -pedantic -Wall -Wextra display.cpp
clang++ -c -g -std=c++17 -pedantic -Wall -Wextra display-test.cpp
clang++ -o display-test display-test.o display.o -lncurses
(Those commands will work, because the Display class is fully implemented.)
Finally, we can compile the our-movie executable with:
clang++ -c -g -std=c++17 -pedantic -Wall -Wextra boundingbox.cpp
clang++ -c -g -std=c++17 -pedantic -Wall -Wextra sprite.cpp
clang++ -c -g -std=c++17 -pedantic -Wall -Wextra display.cpp
clang++ -c -g -std=c++17 -pedantic -Wall -Wextra asciimation.cpp
clang++ -c -g -std=c++17 -pedantic -Wall -Wextra our-movie.cpp
clang++ -o our-movie our-movie.o asciimation.o display.o sprite.o boundingbox.o -lncurses
(These commands will also work, but the executable won't do much since many things are not yet implemented.)
Hay! A bunch of the lines are repeated! That seems wasteful.
Yes, if we pasted in the commands to build each executable separately, we'd be recompiling the same files over and over again. For example,
boundingbox.cppis compiled three times—once for each executable that uses it.
Okay, so we just put all the commands in a file and eliminate the duplicates, right?
Unfortunately, that method always rebuilds everything, even if we were only working on one executable or one of the component files.
For example, if we change
boundingbox.cpp, we only need to recompile that one file and then relink the three executables that use it. We don't need to recompilesprite.cpp,display.cpp,asciimation.cpp, orour-movie.cppat all.
On the other hand, if we change
sprite.hpp, we need to recompile everything that includes it.
We need a smarter way to only compile what needs to be compiled, and only when it needs to be compiled.
How am I ever going to remember what depends on what?
One way would be to keep detailed notes about which files depend on each other…
For example:
boundingbox.cppusesboundingbox.hppboundingbox-test.cppusesboundingbox.hppsprite.cppusessprite.hppandboundingbox.hppsprite-test.cppusessprite.hppandboundingbox.hppdisplay.cppusesdisplay.hppdisplay-test.cppusesdisplay.hppasciimation.cppusesasciimation.hpp,display.hpp,sprite.hpp,boundingbox.hpp, andnumsprites.hppour-movie.cppusesasciimation.hpp,display.hpp,sprite.hpp,boundingbox.hpp, andnumsprites.hpp
and
boundingbox-testneeds to be relinked ifboundingbox-test.oorboundingbox.ochangessprite-testneeds to be relinked ifsprite-test.oorsprite.oorboundingbox.ochangesdisplay-testneeds to be relinked ifdisplay-test.oordisplay.ochangesour-movieneeds to be relinked ifour-movie.oorasciimation.oorsprite.oordisplay.oorboundingbox.ochanges
But keeping track of all these details, remembering all the necessary commands and arguments, and running just the ones needed when they're needed is tedious and error prone.
Which is why, all the way back in 1976, Bell Labs' Stuart Feldman created the first version of the build system
make.
More old stuff? Meh.
The make program reads a specification of what files depend on other files, and what commands to run to build each file. It then automatically determines what needs to be rebuilt based on which files have changed, and runs the appropriate commands to rebuild them.
The specification is written in a file called a “makefile” (but by convention, named Makefile with a capital 'M'). Once you've (correctly!) written a Makefile, you can simply type make in the terminal, and make will take care of everything for you.
An Initial Makefile
We've made a start on a nice Makefile for you in the asciimation directory. It contains some comments and a few example targets to get you started. (Specifically, it is able to build boundingbox-test and display-test, but it does not yet build sprite-test or our-movie.)
Your job is to fill in the rest of the Makefile so that it correctly builds all four executables, recompiling only what needs to be recompiled when you've made changes to one or more of the source or header files.
You can test it out by running one of the following commands in the terminal while in the asciimation directory:
make cleanto delete all the.ofiles and executables.make boundingbox-testto build theboundingbox-testexecutable.make display-testto build thedisplay-testexecutable.make allmakes bothboundingbox-testanddisplay-test.makebuilds the first target in theMakefile, which isallin this case.
I typed
makeand it saidNothing to be done for 'all'.What's that about?
That means that
makechecked the timestamps on all the files and determined that everything is already up to date, so nothing needs to be done. If you change one of the source files, then runmakeagain, it should recompile the necessary files and relink the necessary executables.
What if I want to force it to recompile everything?
You can run
make cleanto delete all the.ofiles and executables, and then runmaketo rebuild everything from scratch.
The make program shows you the commands it runs, but it doesn't tell you why it's doing what it's doing. When you're debugging your Makefile, it can be handy to know what make is thinking. We've provided the cs70-make program, which does what make does, but also explains what it's doing and why. So if you're confused about why something isn't being rebuilt when you expected it to be, you can use cs70-make instead of plain make to get more insight.
For example, if we changed boundingbox.cpp and then ran make boundingbox-test, make would say something like the following, just listing the commands it ran:
clang++ -c -g -std=c++17 -pedantic -Wall -Wextra boundingbox-test.cpp
clang++ -c -g -std=c++17 -pedantic -Wall -Wextra boundingbox.cpp
clang++ -o boundingbox-test boundingbox-test.o boundingbox.o -ltestinglogger
But if we ran cs70-make boundingbox-test, we would get an explanation of why it decided to recompile each file:
- Making boundingbox-test
- Found rule for boundingbox-test, prereqs: boundingbox-test.o boundingbox.o
- Making boundingbox-test.o
- Found rule for boundingbox-test.o, prereqs: boundingbox-test.cpp boundingbox.hpp
- Making boundingbox-test.cpp
+ Okay, no rule found, but file exists
- Making boundingbox.hpp
+ Okay, no rule found, but file exists
+ Target boundingbox-test.o doesn't need to be rebuilt (no newer prereqs)
- Making boundingbox.o
- Found rule for boundingbox.o, prereqs: boundingbox.cpp boundingbox.hpp
- Making boundingbox.cpp
+ Okay, no rule found, but file exists
- Making boundingbox.hpp (skipped, already made!)
- Target boundingbox.o must be rebuilt because these prereqs are newer: boundingbox.cpp
- Building: boundingbox.o
- Running: clang++ -c -g -std=c++17 -pedantic -Wall -Wextra boundingbox.cpp
+ Made boundingbox.o!
- Target boundingbox-test must be rebuilt because these prereqs are newer: boundingbox.o
- Building: boundingbox-test
- Running: clang++ -o boundingbox-test boundingbox-test.o boundingbox.o -ltestinglogger
+ Made boundingbox-test!
Your Task: Finish Your Makefile and Use It
Read Over the Existing Makefile
Because we usually only make a Makefile once (tweaking it slightly as we add files to the project), people don't get a lot of practice writing them, and much of the time they'll start a new project by copying an existing Makefile and modifying it. For that reason, it's more important than ever to have a clear, nicely organized Makefile that is easy to read and understand and reminds people of all the little make details they might forget and uses all the little tricks that make it easy to maintain the file as the project grows.
Thus the provided Makefile uses two (optional) make features:
- Named variables to avoid repeating yourself. For example, the compiler and compiler flags are defined as variables at the top of the file, so if you want to change them, you only have to change them in one place.
- Automatic variables such as
$@and$<to avoid repeating yourself in the build commands.
The file contains comments explaining these features, so read through it carefully to understand how it works.
But you haven't used even more advanced features like implicit rules or suffix rules…
No, we haven't. We could use those features, but this
Makefiletries to strike a balance between being easy to read and understand while still being concise and avoiding unnecessary repetition.
Does
cs70-makework with those features?
Yes.
cs70-makesupports implicit rule and suffix rules, but not other advanced GNU Make features like pattern rules or functions which wouldn't add much value for our use cases.
Finish the Makefile
There are three areas with TODO comments in the Makefile that you'll need to fill in to complete it. Specifically, you need to
- Add rules to build
sprite.oandsprite-test.o,asciimation.o, andour-movie.o. - Add rules to build
sprite-testandour-movie. - Add
sprite-testandour-movieto themTARGETSvariable, which will cause them to be listed both as prerequisites of thealltarget and to be deleted by thecleantarget.
Warning
Don't list .cpp or .hpp files in the TARGETS variable, as they are not executables and, most importantly, you really don't want make clean to delete them!
Meh. This looks like a lot of work.
It's not too bad. We listed the dependencies above to help you out, and you can see the pattern from the existing rules for
boundingbox.o,boundingbox-test.o, anddisplay.o.
And there's a cool trick in the hints section below that can make it even easier.
Meh. I never have time to read hints.
Test Your Makefile
Once you've written your Makefile, you should be able to run cs70-make or make in the asciimation directory to generate any needed .o files, and to link the .o files to make the three executables: boundingbox-test, display-test, and our-movie. (Remember that building sprite-test will give you lots of warnings and errors, so you might not want to put it in TARGETS yet.)
Meh. The programs don't do anything.
boundingbox-testfails andour-moviejust shows a blank screen with a?on it.
You didn't think you'd be done with the assignment after just making a
Makefile, did you?
Helpful Hints
It's just called Makefile
By default, the make program looks for a file called Makefile. If you wanted to have lots of make files, you could use the extension .mak, but, by default, the name is just Makefile with no file extension. And yes, it's a capital M at the front.
Test Your Makefile (Use cs70-make)
Once you write your Makefile, you should be able to successfully run cs70-make or make to generate any needed .o files, and to link the .o files together into the executables.
Remember that the
cs70-makeprogram explains everything it's doing and why, which can be helpful for checking that yourMakefileis working properly.
cs70-makeis also more likely to give an error when you've made a mistake in yourMakefile. Sometimes regularmakeassumes that you're an expert and you did something weird on purpose.
Indent with Tabs (in Makefiles)
The syntax for Makefiles requires that the line below the target and dependencies begins with a literal tab character followed by the command it should run. Check the example Makefile you were given for examples. Note that in the bottom bar in VS Code there is a convenient way to switch between using tabs/spaces for a given file. (If you have the Makefile support that VS Code offers to install, it will automatically set the indentation character to a tab in Makefiles.)
Headers that Include Headers
Remember that a header file can include another header file. If a .cpp file includes a .hpp file that includes another .hpp file, it depends on both of them! That is, if either header file changes, the .cpp file should be recompiled to stay up-to-date.
Fancy Makefile Tricks Are Optional
You don't have to use the fancy tricks we've used in the provided Makefile (or any of the terrifying shortcuts you might find on the internet)—if your Makefile doesn't use any tricks fancier than the example provided for segfaultGenerator, that's fine.
An Amazing Trick for Finding Dependencies
It's nice that you listed all the dependencies above, but it feels like figuring this out for another project would be a lot of work, because you'd have to follow the whole chain of includes.
Yes, it can be tedious to figure out all the dependencies by hand. But there's a really cool trick you can use to make it much easier.
Ooh! A trick!
Although it's good to understand how to do things by hand, the compiler also has to figure out everything that gets #included when it compiles a source file. So people asked the compiler developers if they could help us out by having the compiler show what files a given source file depends on. And they said, “Sure!” So now you can ask the compiler to generate a list of dependencies for you.
If you pass the -MM flag with clang++, rather than compiling your code, it will instead automatically generate a list of dependencies for a given source file. For example, if you run the following command in the terminal while in the asciimation directory,
clang++ -MM boundingbox.cpp
it will output a list of all the header files that boundingbox.cpp depends on, which you can then use to update your Makefile accordingly.
(When logged in, completion status appears here.)