CS 70

Automating the Build Process

Before You Start…

Get together with your partner and clone this GitHub repository.

The activity is split into multiple parts. After certain parts we will ask you to pause and reflect a bit on what you did in that part. Please do your best to NOT scroll to the next section until you've finished reflecting (because the next section may contain "spoilers" for the reflection)!

Review the C++ Code

The repository contains a simple C++ program that consists of three source files:

  • cow.cpp and cow.hpp — code that defines a Cow class
  • main.cpp — a small program that uses the Cow 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?

  • Duck speaking

    I know! Let's make a shell script to do it for us!

  • LHS Cow speaking

    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. Then, reflect on the following question:

Does re-running python3 build-all.py rebuild everything? Does it need to rebuild everything?

Once you're done reflecting, scroll down to proceed to the next part.







Review rebuild.py

  • Cat speaking

    Since we're using Python, we could make the program smarter about what to build.

  • Dog speaking

    Yeah! We could make it only rebuild what it needs to.

  • LHS Cow speaking

    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 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. Then, reflect on the following question:

Are you satisfied with this as a general solution? If we were to add more .cpp files to the project, how much extra code would we need to write to make rebuild.py work properly?

When you're ready to move on, run

rm *.o main

Then scroll down to proceed to the next part.







Try running make.py

  • Goat speaking

    Meh. This seems a bit hard-coded to our particular program. I want to do less work.

  • Cat speaking

    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?

  • Dog speaking

    Yeah, like a data file that says, “to build this file, you need these files, and you run this command”.

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.

  • LHS Cow speaking

    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.

What do you think will happen when you build clean?

To check you answer, run:

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 commands are indented by a single TAB character (not spaces!). This file format is super simple to make it easy for a program to read it. Before contining, briefly reflect on the following question:

What similarities do you see between the makefile format and the buildRules specification from make.py? Any interesting differences?

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

  • Dog speaking

    So I should run make clean every time I want to build stuff, right?

  • LHS Cow speaking

    No! That would require rebuilding everything each time, which throws away the time savings we get by only rebuilding things that have changed.

  • RHS Cow speaking

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

  • Pig speaking

    This is getting pretty fancy. Are there even MORE features?

  • LHS Cow speaking

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

  • Horse speaking

    Hay! If I do that, all my rules will look the same!

  • LHS Cow speaking

    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.

  • Rabbit speaking

    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 GNU make, every version of make (e.g., BSD make) supports suffix rules, so they're more portable.

  • LHS Cow speaking

    Note that cs70-make supports suffix rules, but not pattern rules. Your makefiles in CS 70 must work with cs70-make, so you can only go so far with advanced make 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.)