Post

Makefiles

Makefiles

This post is an automatic translation from French. You can read the original version here.

The stream from our friend Imil from last Saturday was dedicated to good old make, and it was a chance to review the basics of Makefiles. Nothing too complicated, but it feels good and I still learned a few things along the way! Here are some personal notes, which might be useful to others besides myself.

The basics

A makefile is mainly composed of two types of blocks:

  • Variable declarations

    1
    
    VARIABLE = value
    
  • Recipe descriptions for generating files

    1
    2
    3
    
    target: dependency1 dependency2 dependency3 ... dependencyN
    (tab) command line
    (tab) command line
    

Warning: It must be a tab, not spaces!

For example:

CC = gcc

monprog: main.o bidule.o
	${CC} -o monprog main.o bidule.o

main.o: main.c
	${CC} -c main.c

bidule.o: bidule.c
	${CC} -c bidule.c

You can clearly see the structure of the “recipes”: to build the target main.o, for example, you need main.c and you apply the command lines in order. Here, it’s easy, there’s only one (gcc -c main.c).

By the way, you can see the use of a variable. Here, curly braces are used. I didn’t know this, but you can also use parentheses! Moreover, if you don’t use any, the $ will only apply to the first character… :-(

Invoking make

To execute one of the recipes, you invoke make with the target you want to build as an argument. For example, to build main.o:

1
make main.o

If necessary, the recipes for the dependencies will also be executed:

$make monprog
 cc -c bidule.c
 cc -c main.c
 cc -o monprog main.o bidule.o
$

That’s pretty much the whole point of the thing!

To determine whether a recipe needs to be executed or not, make relies on file modification times.

By default, make launches the first target in the Makefile when run without arguments. Therefore, it’s best to place the recipe generating the executable first.

Writing generic rules

You don’t need to write rules for absolutely every file… Especially when those rules are exactly the same. So you have the option to create generic rules.

CC = gcc
OBJ = main.o bidule.o fonk.o

all: projet

projet: $(OBJ)
	$(CC) -o projet $(OBJ)

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

The last rule here is a generic rule. It will be applied for building any .o file.

Another syntax for writing a generic rule:

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

Which can be read as “To transform a .c into a .o”… And as a result, the order is reversed: the dependency first, the target second!

As you can see above, there are special variables that allow you to write generic rules in a… well, generic way. There are (at least) five of them, but the first three seem the most useful to me:

  • $@ : The target of the rule
  • $< : The first dependency
  • $^ : The dependencies
  • $? : The dependencies newer than the target
  • $* : The target name, without suffix

While we’re at it, it’s also possible to silence a command line and not display it. To do this, simply prefix the line with @. For example:

CC = gcc
OBJ = main.o bidule.o fonk.o

all: projet

projet: $(OBJ)
	@$(CC) -o projet $(OBJ)

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

Implicit rules

The make tool applies, in addition to the compilation rules described in the Makefile, a set of default rules. For the GNU version of make, you can find them here:

Catalog of Built-In Rules

If you want to take advantage of these rules, it’s best to follow good habits when choosing variable names. For C, you would use:

  • ${CC} for the compiler name
  • ${CFLAGS} for compilation options
  • ${LDFLAGS} for linking options

There are many more: for C++, Lex, Yacc, Fortran, etc. You can find the list here:

Implicit Variables

The .PHONY target

The .PHONY target is a special target that lets make know that certain rules don’t result in the creation of a file, and therefore it shouldn’t rely on the modification date of the dependencies for those rules. You would use it, for example, for a “clean” target that removes temporary files, or a “mrproper” target that really cleans up.

build:
	gcc -o foo foo.c

.PHONY: clean

clean:
	rm -f *.o

A few useful things

Assignment with ?=

If you assign a variable using the = operator, the old content of that variable is erased. With ?=, you can assign a value only if the variable hasn’t already been declared previously. For example:

CC ?= gcc

test: main.c
	${CC} -o test main.c

This is useful, for example, when this Makefile can be called from another Makefile.

Overriding a variable from the command line

When a variable is defined in the Makefile, you can override it by specifying it on the command line. For example:

CC = gcc

test: main.c
	${CC} -o test main.c
$make CC=echo
echo -o test main.c
-o test main.c

As you can see, the ${CC} variable is overridden by the command line. Contrary to what I thought, whether it was assigned with = or ?= doesn’t matter: the command line takes priority (and when you think about it, that makes perfect sense!)

Conditionals

You can also use conditionals… Handy!

DEBUG = 1

build:
	ifeq ($(DEBUG), 1)
	gcc -Wall -Werror -o foo foo.c
	else
	gcc -o foo foo.c
	endif

Wildcards

To avoid long lists of files to type by hand, and especially to maintain and update:

SRC= $(wildcard *.c)
OBJ= $(SRC:.c=.o)

BSD style… Wow!

Honestly, this is classy! Granted, I don’t think it’s ultra portable, but it’s worth a look:

PROG = fin

.include <bsd.prog.mk>

It’ll be hard to get more concise than that!!!

This post is licensed under CC BY 4.0 by the author.