CS 70

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 and boundingbox.hpp: The implementation and header files for the BoundingBox class.
  • boundingbox-test.cpp: A test program for the BoundingBox class.
  • sprite.cpp and sprite.hpp: The implementation and header files for the Sprite class.
  • sprite-test.cpp: A test program for the Sprite class.
  • display.cpp and display.hpp: The implementation and header files for the Display class.
  • display-test.cpp: A test program for the Display class.
  • asciimation.cpp and asciimation.hpp: The implementation and header files for the Asciimation 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.)

  • Horse speaking

    Hay! A bunch of the lines are repeated! That seems wasteful.

  • LHS Cow speaking

    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.

  • Duck speaking

    Okay, so we just put all the commands in a file and eliminate the duplicates, right?

  • LHS Cow speaking

    Unfortunately, that method always rebuilds everything, even if we were only working on one executable or one of the component files.

  • RHS Cow speaking

    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 recompile sprite.cpp, display.cpp, asciimation.cpp, or our-movie.cpp at all.

  • LHS Cow speaking

    On the other hand, if we change sprite.hpp, we need to recompile everything that includes it.

  • RHS Cow speaking

    We need a smarter way to only compile what needs to be compiled, and only when it needs to be compiled.

  • Hedgehog speaking

    How am I ever going to remember what depends on what?

  • LHS Cow speaking

    One way would be to keep detailed notes about which files depend on each other…

For example:

  • boundingbox.cpp uses boundingbox.hpp
  • boundingbox-test.cpp uses boundingbox.hpp
  • sprite.cpp uses sprite.hpp and boundingbox.hpp
  • sprite-test.cpp uses sprite.hpp and boundingbox.hpp
  • display.cpp uses display.hpp
  • display-test.cpp uses display.hpp
  • asciimation.cpp uses asciimation.hpp, display.hpp, sprite.hpp, boundingbox.hpp, and numsprites.hpp
  • our-movie.cpp uses asciimation.hpp, display.hpp, sprite.hpp, boundingbox.hpp, and numsprites.hpp

and

  • boundingbox-test needs to be relinked if boundingbox-test.o or boundingbox.o changes
  • sprite-test needs to be relinked if sprite-test.o or sprite.o or boundingbox.o changes
  • display-test needs to be relinked if display-test.o or display.o changes
  • our-movie needs to be relinked if our-movie.o or asciimation.o or sprite.o or display.o or boundingbox.o changes
  • RHS Cow speaking

    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.

  • LHS Cow speaking

    Which is why, all the way back in 1976, Bell Labs' Stuart Feldman created the first version of the build system make.

  • Goat speaking

    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 the boundingbox-test executable.
  • make display-test to build the display-test executable.
  • make all makes both boundingbox-test and display-test.
  • make builds the first target in the Makefile, which is all in this case.
  • Dog speaking

    I typed make and it said Nothing to be done for 'all'. What's that about?

  • LHS Cow speaking

    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 run make again, it should recompile the necessary files and relink the necessary executables.

  • Cat speaking

    What if I want to force it to recompile everything?

  • LHS Cow speaking

    You can run make clean to delete all the .o files and executables, and then run make 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.

  • Rabbit speaking

    But you haven't used even more advanced features like implicit rules or suffix rules…

  • LHS Cow speaking

    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.

  • Duck speaking

    Does cs70-make work with those features?

  • LHS Cow speaking

    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 and sprite-test.o, asciimation.o, and our-movie.o.
  • Add rules to build sprite-test and our-movie.
  • Add sprite-test and our-movie to them TARGETS variable, which will cause them to be listed both as prerequisites of the all target and to be deleted by the clean 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!

  • Goat speaking

    Meh. This looks like a lot of work.

  • LHS Cow speaking

    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, and display.o.

  • RHS Cow speaking

    And there's a cool trick in the hints section below that can make it even easier.

  • Goat speaking

    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.)

  • Goat speaking

    Meh. The programs don't do anything. boundingbox-test fails and our-movie just shows a blank screen with a ? on it.

  • LHS Cow speaking

    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.

  • RHS Cow speaking

    Remember that the cs70-make program explains everything it's doing and why, which can be helpful for checking that your Makefile is working properly.

  • LHS Cow speaking

    cs70-make is also more likely to give an error when you've made a mistake in your Makefile. Sometimes regular make assumes 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

  • Cat speaking

    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.

  • LHS Cow speaking

    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.

  • Dog speaking

    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 -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.

To Complete This Part of the Assignment…

You'll know you're done with this part of the assignment when you've done all of the following:

(When logged in, completion status appears here.)