Building Code: make and Makefiles#

Today#

  • We’ll continue working in the class 3 assignment.

Some more on git#

Ignoring files#

git is a version management tool, also known as “source control”. This is an important part: It is meant to keep track of the source files in your project, not the generated / compiled binary code that is generated as your code is built. The general rule is, if you used an editor to create a given file (like a C++ source file, or a script), it should be added to the repository. If the file was generated by running a command, it generally should not. In particular, executables and object files should not be added – they are big, change all the time, are specific to the system that you are working on, and may well not work for someone else (or you yourself) working on a different machine.

Ideally, compiled files etc should be put into a separate directory to avoid confusion in the first place, and we’ll get there. But in any case, one can tell git that certain files should be “ignored”, so that they aren’t accidentally being added. This is done by using a .gitignore file. The .gitignore file itself should be added to the repository.

Pull requests#

When working by yourself, you can just commit changes directly to the main branch of your repository. Or, you might do work in a “feature” or “bug fix” branch locally, then merge it into the main branch. In order for changes to be reflected in your github repo, you push those latest changes in the main branch to github, using git push <repo> <branch>, or using the “Sync” button in VS Code. (“Sync” will not just push changes from your local repo to github, but also pull any changes from github to your local repo.)

However, when working in a team, it’s often better to use “pull requests” to merge changes into the main branch. This allows for code review, discussion, and automated testing before changes are merged. Even when working alone, using pull requests can be a good practice to ensure that you review your own changes before they become part of the main codebase.

In order to create a pull request, you generally push your changes to a separate branch on github, then use the github web interface to create a pull request from that branch into the main branch. You can then review the changes, discuss them, and finally merge them into main.

I set up the Github classroom assignments in this class to automatically create a “Feedback” pull request for you once you push your changes back to github. You can use that pull request to document your work, and I can use it to provide feedback. Basically, we’re using a general github feature (pull requests) to implement a simple workflow for submitting and reviewing homework assignments. The analogy between what happens in a classroom setting and in a real-world software development project only goes so far, though – it maps well for submitting work and getting feedback, but in a real-world project, the goal in the end is to actually merge the pull request into the main code base, which wouldn’t work well here, in particular since there will be many students working on the same assignment, doing similar things, and merging all of that into a single code base would be a mess of conflicts.

Compiler Explorer#

For anyone who’s interested: Compiler Explorer

make and Makefiles#

The build script works perfectly fine, but for large projects it’s not a good solution, since even if we just make one little change in one file, the whole project will be recompiled, which is slow. Instead we’ll learn about using the make command, which is the traditional way of building code, and still the most commonly used build tool, though it may be hiding under additional layers.

You generally call make <target>, and the make command will then do whatever is needed to create the <target> – if it can figure out what to do. Normally, one writes a file called Makefile in the current directory, which contains rules that tells make what command to run to do the job of creating a target. For example:

hello: hello.o factorial.o greeting.o
  gcc hello.o factorial.o greeting.o -o hello

The first thing, listed before the colon, is the name of the target. After the colon follows a list of prerequisites, also called dependencies – these need to exist and be up-to-date before the target can be built. Finally, in the next line is the command that will actually turn the prerequisites and create the target. Note that there needs to be a TAB in front of the command, spaces won’t work.

So, generally:

TARGET: PREREQ1 PREREQ2 ...
  COMMAND

At this point, the Makefile knows how to create “hello” from the object (.o) files, but we haven’t told it how to make the object files, ie., by asking the compiler to compile the source files:

hello: hello.o factorial.o greeting.o
  gcc hello.o factorial.o greeting.o -o hello

hello.o: hello.c
  gcc -c hello.c

greeting.o: greeting.c
  gcc -c greeting.c
[...]

Your turn#

Write a Makefile like the above (it needs to be completed!), and run

[kai@mpro] $ make
[...]

Run make repeatedly and pay attention to what commands it does / does not rerun. Edit a file (you can just make a trivial change, like adding a space or a comment), and run make again. Observe which commands get executed.

Commit this step to your git repository.

Variables#

The main ingredient in Makefiles are rules like the above – but we can also use variables. For example, we can assign gcc to the variable CC, and then use that instead, so that when you want to change to a different compiler later, e.g., clang, you only have to change gcc -> clang in one place. CC is the standad name for the C compiler, and we’ll also use CFLAGS for additional flags to the C compiler, like -Wall, which turns on lots of often useful warnings. (For C++, the corresponding variables are CXX and CXXFLAGS.)

# for C
CC = gcc
CFLAGS = -Wall
# for C++
CXX = g++
CXXFLAGS = -Wall

hello: hello.o factorial.o greeting.o
  $(CC) hello.o factorial.o greeting.o -o hello

hello.o: hello.c
  $(CC) $(CFLAGS) -c hello.c

[...]

Variables are assigned using, e.g., CC = gcc, that means the variable CC now has the value gcc. When using a variable, you have to put $(...) around it, e.g., $(CC) will be replaced by gcc if that’s the value we set previously.

Special variables#

There are a number of special variables that can be used in Makefile rules:

  • $@ target

  • $< (first) prerequisite

  • $^ all prerequisites

We can use these so we don’t have to put redundant information into the command part of a rule:

CC = gcc
CFLAGS = -Wall

hello: hello.o factorial.o greeting.o
  $(CC) $^ -o $@

hello.o: hello.c
  $(CC) $(CFLAGS) -c $<

[...]

Wildcard rules#

Now we notice that the last three rules are pretty much the same: something.o depends on something.c, and the command is also the same. Make allows wildcard rules using % that we can use to write a general compile .c -> .o rule:

CC = gcc
CFLAGS = -Wall

hello: hello.o factorial.o greeting.o
  $(CC) $^ -o $@

%.o: %.c
  $(CC) $(CFLAGS) -c $<

As it turns out, make actually already knows a lot of standard rules, For example, it also knows how to link multiple object files into an executable – but we still need to list the prerequisites so that make knows what object files to link together. So we can skip that rule. It also knows how to compile .c files into .o files using the C compiler, or how to compile .cpp files into .o files using the C++ compiler. rather short Makefile. Unfortunately, our C++ files have the .cxx extension which make does not know about by default, so we have to add that rule when switching to C++ (or we could use the .cpp extension).

CC = gcc
CFLAGS = -Wall

hello: hello.o factorial.o greeting.o

Your turn#

Try out the simplified version of the Makefile above.

Header files#

As we’ve seen, C++ uses header files, just like C. Fortran77 on the other hand doesn’t have a concept of header files, and it doesn’t need / want functions (or subroutines) to be declared before using them. However, that means it’s easily possible to call a function with the wrong kind or the wrong number of arguments, and the compiler is fine with that. Unfortunately, if you run the code, it’s highly unlikely that it works right, and fairly likely that it’ll just crash without giving much of a clue as to what’s wrong.

Fortran90 (and C++20) supports “modules”, which basically create something like a header file automatically as they get compiled. This helps in ensuring that one uses functions correctly. It does cause complications when using make/Makefiles, though, and there are better tools out there (like cmake, as we’ll see.)

Header file dependencies#

Back to the Makefile: Things are pretty good already, but there’s one problem: make doesn’t know anything about header files, unless we tell it about the dependencies explicitly. Next time we’ll see how some tools can help out with that, but for now we’re stuck listing dependencies on header files explicitly, which is not so much fun…

CC = gcc
CLAGS = -Wall

hello: hello.o factorial.o greeting.o

hello.o: hello.h
greeting.o: hello.h
factorial.o: hello.h

More on make, and some of its limitations#

make and Makefiles are a standard tool that has been around for a long time and can be found on pretty much any operating system. As often the case for things that have been around for a while, there are different versions of the make command around, and while the basic feature set is always the same and compatible between versions, specific versions of make, like, e.g. the GNU make, support additional features that might be useful, but if using those, one should be aware that a non-GNU make might choke on such a Makefile.

As usual for these kind of basic tools, there is a wealth of information available on the internet that goes into a lot more depth, for example the GNU make manual.

There are a number of limitations to the make utility, in particular its lack of handling of dependencies on header files, and the fact that it cannot automatically adapt to the specifics of a given machine. For example, I hardcoded my compiler to be gcc, so that same Makefile wouldn’t work on another system where the compiler might be called pgcc or clang.

Homework#

  • Finish the work from class 3.

  • Finish the “your turn” exercises from above

    Update: It is okay to do the first “Your turn” (creating a Makefile and trying it), and then skip to the last “Your turn” (the simplified Makefile), though of course it won’t hurt to go through the intermediate steps, too.

  • Keep a log of what you’ve been doing and add any comments you might have.

  • As you do your work, make commits to keep track of your work.

  • Submit your work by syncing (pushing) your changes back to github. With VS Code, you can use the “Sync” button in the version management tab. On the command line, you can just say git push.

  • You can put your “report” bits directly into the “Feedback” pull request that’ll be created once you push your changes back to github.

    Here is an example of how the “report” might look like: UNH-HPC-2026/class-3-kai#1