Dev Notes

Software Development Resources by David Egan.

Notes on GNU Make


C, C++, Make
David Egan

The GNU make utility is a tool that maintains groups of programmes make can determine which pieces of a large programme need to be compiled and/or recompiled, and generally take care of compilation and recompilation in an efficient way.

You can use make with any programming language whose compiler can be run from the command line. Beyond compiling programmes, make can describe any task that involves updating files automatically when other files change.

The Makefile

The Makefile is used to describe relationships between files in the programme, and the commands necessary for updating files.

In C programmes, the executable file is updated (“linked”) from “object” files, which are in turn made by compiling source files.

Once you’ve written a suitable Makefile, you just need to run make in the programme root directory to perform all necessary recompilations.

If you don’t specify a Makefile with make -f <makefile>, make will look for the makefiles:

  1. GNUmakefile
  2. makefile
  3. Makefile

in that order. It is recommended to call the makefile Makefile.

The makefile contains one or more rules that specify how a target is produced from prerequisite files. make updates a target if the prerequisite files that it depends upon have been modified since the target was last changed, or if the target does not exist.

Rules

A make rule is comprised of a target specification and it’s prerequisite files, all on one line and with the target separated from the prerequisites by a colon.

After the space-separated list of prerequisites, the rule has a newline and a list of commands that should be executed required to rebuild the target from the prerequisites.

Commands may be added over multiple lines, but each line that contains a command must start with a TAB character.

Example Makefile

The following makefile has 3 targets: myProgramme, moduleOne & moduleTwo:

myProgramme: moduleOne.o moduleTwo.o
	gcc -o myProgramme moduleOne.o moduleTwo.o

moduleOne.o: moduleOne.c moduleOne.h standaloneHeader.h
	gcc -std=gnu=gnu99 -pedantic -Wall -c moduleOne.c

moduleTwo.o: moduleTwo.c moduleTwo.h standaloneHeader.h
	gcc -std=gnu=gnu99 -pedantic -Wall -c moduleTwo.c

Special Make Variables

Inside an action, special variables are available for matching filenames:

  • $@: The full target name of the current target
  • $?: Dependencies that are newer than the current target
  • $*: Returns text corresponding to % in current target
  • $<: Returns the name of the first dependency
  • $^: Name of all dependencies, space separated

This allows your makefile to be more concise:

Example Makefile 2

Example C++ makefile for the following project structure:

  • main.cpp: includes grade.h & util.h
  • util.cpp: includes util.h
  • util.h: contains declarations
  • grade.cpp: includes grade.h
  • grade.h: contains declarations
# Set a variable for flags to avoid repetition
CXXFLAGS = -W -Wall -pedantic -std=c++17 -g
CXX = g++ ${CXXFLAGS}
NAME = grades

# The target has the dependencies util.o, main.o & grade.o
$(NAME): util.o main.o grade.o
	$(CXX) -o $@ $^

# For any .c file, build an object file by applying the actions on the following line.
# This is a pattern rule, a type of implicit rule. Specifies one target and one dependency.
# Causes one invocation of $(CXX) for each target.
%.o: %.c
	$(CXX) -c $<

# util.c and grade.c include header files. Their object files are generated by the implicit
# rule above, but the following lines specify additional dependencies:
util.o: util.h
grade.o: grade.h

# Remove the executable and all object files
.PHONY: clean
clean:
	-rm $(NAME) *.o

Note that the pattern rule (%.o: %.c) involves matching file names - the % can match any non-empty substring, whereas the characters match only themselves. The rule says: “Compile .c files into corresponding .o files”.

In this example, main.o, util.o and grade.o will be made from main.c, util.c and grade.c respectively.

Variables for Implicit Rules

  • CFLAGS: Extra flags to give to the C compiler.
  • CPPFLAGS: Extra flags to give to the C preprocessor and programs that use it (the C and Fortran compilers).
  • CXX: Program for compiling C++ programs, default g++.
  • CXXFLAGS: Extra flags to give to the C++ compiler.

Set Variables

Assign variables within a Makefile.

In a Makefile, a variable is a name assigned to a value, which is a string of text. Values are substituted into targets, prerequisites, commands etc.

# Set a variable to hold the absolute path of the Makefile
# Getting `firstword` strips off any includes
ABS_PATH := $(dir $(realpath $(firstword $(MAKEFILE_LIST))))

# Alternatively:
ABS_PATH := $(shell pwd)  

See this article for a more complete description.

Variables can be either recursively expanded (defined using the = operator) or simply expanded (defined using the := operator).

See this article for more information.

Example: C

# Set common parameters
CC = gcc
CFLAGS = -std=gnu99 -W -Wall -pedantic -g

# Specify necessary targets/tasks
all: maxSeq.o test-subseq.o test-subseq

# Compile maxSeq.c into maxSeq.o
maxSeq.o: maxSeq.c
        $(CC) $(CFLAGS) -c maxSeq.c

# Compile test-subseq.c into test-subseq.o
test-subseq.o: test-subseq.c
        $(CC) $(CFLAGS) -c test-subseq.c

# Link object files test-subseq.o and maxSeq.o into an executable test-subseq
test-subseq: test-subseq.o maxSeq.o
        $(CC) $(CFLAGS) -o test-subseq test-subseq.o maxSeq.o

clean:
        rm maxSeq.o test-subseq.o

Example: C++ Makefile to Create Multiple Executables

In this example, each *.cpp file is compiled and linked into a distinct executable:

# Makefile to compile and link all C++ files in the current directory, such that each C++
# file results in a unique executable.
#
# This is useful for experimentation, testing and small-scale projects.
# 
# Copyright (c) David Egan 2020
# SPDX-License-Identifier: GPL-2.0-or-later

WARNINGS = -pedantic -Wall -Wfatal-errors -Wextra -Wno-unused-parameter -Wno-unused-variable
BIN_DIR = bin
OBJS_DIR = $(BIN_DIR)/objs
CXXFLAGS = $(WARNINGS) -std=c++17 -g
LDFLAGS =
CXX = g++ ${CXXFLAGS}

# Handle all .cpp files together - allows more to be added to the project without
# changing the Makefile
SRCS = $(wildcard *.cpp)

# String substitution to generate appropriate names for executable files
EXECUTABLE_FILES = $(SRCS:%.cpp=$(BIN_DIR)/%)
# Note that the following is an alternative way of performing string substitution:
# EXECUTABLE_FILES = $(patsubst %.cpp,$(BIN_DIR)/%,$(SRCS))

# Output for debugging purposes
$(info EXECUTABLE_FILES = "$(EXECUTABLE_FILES)")

# There should be a corresponding object file for each .cpp file
OBJECT_FILES = $(SRCS:%.cpp=$(OBJS_DIR)/%.o)

.PHONY: all clean
all: $(EXECUTABLE_FILES)

# This pattern is triggered by the `all` rule. This is a builtin rule
# that builds executables from object files.
$(BIN_DIR)/%: $(OBJS_DIR)/%.o
	@$(CXX) $(LDFLAGS) -o $@ $^

# Builtin rule to generate object files from .cpp files
$(OBJS_DIR)/%.o: %.cpp
	$(info Building object file for "$(<)")
	@mkdir -p $(@D)
	@$(CXX) $(CXXFLAGS) -o $@ -c $<

# Additional dependencies
$(OBJS_DIR)/selection-sort.o: util.h
$(OBJS_DIR)/merge-sort.o: util.h

clean:
	rm $(OBJS_DIR)/*.o
	rm -f $(BIN_DIR)/*

Project Structure:


.
├── bin
│   ├── merge-sort
│   ├── objs
│   │   ├── merge-sort.o
│   │   └── selection-sort.o
│   └── selection-sort
├── Makefile
├── merge-sort.cpp
├── selection-sort.cpp
└── util.h

2 directories, 8 files

Output Variables

The internal info function allows you to output variables to stdout:

$(info EXECUTABLE_FILES = "$(EXECUTABLE_FILES)")
# outputs the value of `EXECUTABLE_FILES`

String Substitution

$(patsubst pattern,replacement,text) finds whitespace separated words in text that match pattern and replace these with replacement. The pattern may contain a % character, which acts as a wildcard matching any number of characters within a word - escape literal % characters in a pattern with a \.

Example:

# For each "word" in $(SRCS) remove .cpp and prepend a directory path
EXECUTABLE_FILES = $(patsubst %.cpp,$(BIN_DIR)/%,$(SRCS))

You can use substitution references to get the same effect:

EXECUTABLE_FILES = $(SRCS:%.cpp=$(BIN_DIR)/%)

Substitution references substitute the value of a variable with the specified alterations, in the form:

$(VAR:a=b)

This means take the value of VAR and replace every a at the end of a word with b. Wildcards can also be used, as shown in the example above.

References


comments powered by Disqus