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.cpp
andboundingbox.hpp
: The implementation and header files for theBoundingBox
class.boundingbox-test.cpp
: A test program for theBoundingBox
class.sprite.cpp
andsprite.hpp
: The implementation and header files for theSprite
class.sprite-test.cpp
: A test program for theSprite
class.display.cpp
anddisplay.hpp
: The implementation and header files for theDisplay
class.display-test.cpp
: A test program for theDisplay
class.asciimation.cpp
andasciimation.hpp
: The implementation and header files for theAsciimation
class.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.cpp
is 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.cpp
at 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.cpp
usesboundingbox.hpp
boundingbox-test.cpp
usesboundingbox.hpp
sprite.cpp
usessprite.hpp
andboundingbox.hpp
sprite-test.cpp
usessprite.hpp
andboundingbox.hpp
display.cpp
usesdisplay.hpp
display-test.cpp
usesdisplay.hpp
asciimation.cpp
usesasciimation.hpp
,display.hpp
,sprite.hpp
,boundingbox.hpp
, andnumsprites.hpp
our-movie.cpp
usesasciimation.hpp
,display.hpp
,sprite.hpp
,boundingbox.hpp
, andnumsprites.hpp
and
boundingbox-test
needs to be relinked ifboundingbox-test.o
orboundingbox.o
changessprite-test
needs to be relinked ifsprite-test.o
orsprite.o
orboundingbox.o
changesdisplay-test
needs to be relinked ifdisplay-test.o
ordisplay.o
changesour-movie
needs to be relinked ifour-movie.o
orasciimation.o
orsprite.o
ordisplay.o
orboundingbox.o
changes
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 clean
to delete all the.o
files and executables.make boundingbox-test
to build theboundingbox-test
executable.make display-test
to build thedisplay-test
executable.make all
makes bothboundingbox-test
anddisplay-test
.make
builds the first target in theMakefile
, which isall
in this case.
I typed
make
and it saidNothing to be done for 'all'.
What's that about?That means that
make
checked 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 runmake
again, 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 clean
to delete all the.o
files and executables, and then runmake
to 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
Makefile
tries to strike a balance between being easy to read and understand while still being concise and avoiding unnecessary repetition.Does
cs70-make
work with those features?Yes.
cs70-make
supports 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.o
andsprite-test.o
,asciimation.o
, andour-movie.o
. - Add rules to build
sprite-test
andour-movie
. - Add
sprite-test
andour-movie
to themTARGETS
variable, which will cause them to be listed both as prerequisites of theall
target and to be deleted by theclean
target.
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-test
fails andour-movie
just 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-make
program explains everything it's doing and why, which can be helpful for checking that yourMakefile
is working properly.cs70-make
is also more likely to give an error when you've made a mistake in yourMakefile
. Sometimes regularmake
assumes that you're an expert and you did something weird on purpose.
Indent with Tabs (in Makefile
s)
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 #include
d 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 -M
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++ -M 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.)