Build Automation with Makefile
Compilation (Practical)
Objectives
If a task is repetitive, automate it!
- Compilation is very repetitive
- Typing compilation commands with large projects:
- Is very tedious
- Is non optimal
Modular Programming
Build easy to read / easy to maintain code by:
- Splitting features in files…
- Located in folders or modules that share the same functionalities (like I/O, Rendering, Network, Users…)
This allows you to control dependencies between modules and build libraries.
Example: Quake3 Arena (1999)
$ cloc .
600 text files.
591 unique files.
53 files ignored.
-------------------------------------------
Language files blank comment code
-------------------------------------------
C 328 37249 58865 192435
C/C++ Header 143 6285 7688 23982
C++ 8 725 724 2438
make 3 214 230 1792
Assembly 9 267 406 1224
Bourne Shell 8 61 48 305
[...]
-------------------------------------------
SUM: 547 47047 71726 248147
-------------------------------------------
$ tree -d
.
├── botlib
├── bspc
├── cgame
├── client
├── game
├── jpeg-6
├── macosx
├── null
├── q3_ui
├── qcommon
├── renderer
├── server
├── splines
├── ui
├── unix
└── win32
Modular Programming - How To
- Place declarations of reusable resources (functions, variables, structures) in header files (.h)
- Place definitions of reusable resources and implementation details in source files (.c)
Example
inout.h
#define TAILLE_MAX 256
#define INTENSITE_MAX 255
#define LARGEUR 800
#define HAUTEUR 600
int chargeImage(const char *nom_fichier,
unsigned char tableau[HAUTEUR][LARGEUR]);
int sauvegardeImage(const char *nom_fichier,
unsigned char tableau[HAUTEUR][LARGEUR]);
inout.c
/*
* Projet de Programmation Structuree
* Departement IMA - 3ieme Annee
* Polytech'Lille - 2010/2011
* By Jeremie Dequidt
*/
#include "inout.h"
#define DEBUG true
/*********************************************************
* Base functions : input/output of pictures
*********************************************************
*/
/*
* Load a PGM picture into a char vect
* - Parameters :
* nom_fichier: The input file
* vecteur: char pointer where the pixels are stored
* - Return value : 1 if everything went well, 0 otherwise
*/
int chargeImage(const char *nom_fichier,
unsigned char vecteur[HAUTEUR][LARGEUR])
{
/* Variable Declaration */
FILE *fp;
unsigned int type;
int imax, l, h, debug;
unsigned char buffer[TAILLE_MAX];
// [...]
}
/*
\brief Fonction to save a picture
\param nom_fichier output file where we want to save
\param type output type (grey / color)
\param tableau char array (= where to find pixels)
\return 1 if everything went well, 0 otherwise
*/
int sauvegardeImage(const char *nom_fichier,
unsigned char vecteur[HAUTEUR][LARGEUR])
{
FILE *fp;
/* File opening */
[...]
}
main.c
#include <stdio.h>
#include <stdlib.h>
#include "inout.h"
//------------FONCTIONS--------------------
//image en negatif
void negatif(unsigned char imagea[HAUTEUR][LARGEUR]) {
for(int i=0; i< HAUTEUR; i++) {
for (int j = 0; j < LARGEUR; j++) {
imagea[i][j] = 255 - imagea[i][j]; // assign inverse of pixel intensity
}
}
}
//[...]
int main()
{
unsigned char image[HAUTEUR][LARGEUR], image2[HAUTEUR][LARGEUR];
int charge = chargeImage("../images/royalPalaceMadrid.pgm", image);
int charge2 = chargeImage("../images/fog-1535201_800.pgm", image2);
// [...]
return 0;
}
Error: Multiple Inclusion
Remember the pre-processing step in compilation!
// a.h
const int g_variable = 50;
// b.h
#include "a.h"
int func()
{
return g_variable;
}
// main.c
#include "a.h"
#include "b.h"
int main()
{
int var = g_variable;
return var + func();
}
This causes an error:
$ gcc main.c
In file included from main.c:2:
In file included from ./b.h:1:
./a.h:1:11: error: redefinition of 'g_variable'
const int g_variable = 50;
^
main.c:1:10: note: './a.h' included multiple times,
additional include site here
#include "a.h"
^
The preprocessed file shows g_variable defined twice:
# 1 "main.c"
# 1 "./a.h" 1
const int g_variable = 50;
# 2 "main.c" 2
# 1 "./b.h" 1
# 1 "./a.h" 1
const int g_variable = 50;
# 2 "./b.h" 2
// ...
Error: Cyclic Inclusion
// c.h
#include "d.h"
int g_var = 10;
int temp_c()
{
return temp_d() + g_var;
}
// d.h
#include "c.h"
int temp_d()
{
return temp_c() + g_var;
}
// main.c
#include "c.h"
int main()
{
return temp_c();
}
Solution: Header Guard
#ifndef __HEADER_NAME__
#define __HEADER_NAME__
// file content
#endif
Automatic Compilation: Makefile
From Q3 Example
With 328 .c files:
- At least 328
gcc -ccommands - And 1
gcc -ocommand are required to build the executable - (Hint: it’s actually way more than that!)
Anatomy of Make
- Requires a file named
Makefile - The
Makefilecontains comments, variables, and rules to build the program
Makefile: Example
# Makefile for project S5
TARGET= project
CFLAGS=-g -W -Wall -Wextra
LDFLAGS=-lm
default: $(TARGET)
inout.o: inout.c inout.h
gcc $(CFLAGS) -c inout.c
main.o: main.c inout.h
gcc $(CFLAGS) -c main.c
$(TARGET): main.o inout.o
gcc $(LDFLAGS) main.o inout.o -o $(TARGET)
.PHONY: clean
clean:
rm -f *.o
rm -f $(TARGET)
Anatomy of Rules
target: list of dependencies
[TAB]command(s) to build the target
Makefile Usage
make
is equivalent to:
make -f Makefile default
(assumes that the default target exists)
Internal Variables
| Variable | Description |
|---|---|
$@ | the target |
$* | the target without extension |
$< | the first dependency |
$^ | all the dependencies |
Makefile with Internal Variables
inout.o: inout.c inout.h
gcc $(CFLAGS) -c $<
main.o: main.c inout.h
gcc $(CFLAGS) -c $<
$(TARGET): main.o inout.o
gcc $(LDFLAGS) $^ -o $@
Inference Rules
To avoid similar rules, use inference rules:
.c.o:
gcc $(CFLAGS) -c $<
This defines how .c files are built into object files.
Makefile with Inference Rules
TARGET= project
CFLAGS=-g -W -Wall -Wextra
LDFLAGS=-lm
default: $(TARGET)
.c.o:
gcc $(CFLAGS) -c $<
$(TARGET): main.o inout.o
gcc $(LDFLAGS) $^ -o $@
.PHONY: clean
clean:
rm -f *.o
rm -f $(TARGET)
With inference rules, the dependency on headers is lost.
Automatic Dependency Generation
$ gcc -MMD -c ld_main.c
$ cat ld_main.d
ld_main.o: ld_main.c
For each source file (.c), a corresponding .d file is generated that contains dependencies.
Makefile with Dependencies
TARGET= project
CFLAGS=-g -W -Wall -Wextra -MMD
LDFLAGS=-lm
DEPS=main.d inout.d
default: $(TARGET)
.c.o:
gcc $(CFLAGS) -c $<
$(TARGET): main.o inout.o
gcc $(LDFLAGS) $^ -o $@
-include $(DEPS)
.PHONY: clean
clean:
rm -f *.o
rm -f $(TARGET)
Generic Makefile
Automatic list of files:
SRC=$(wildcard *.c)
Every source file (.c) in the current folder.
DEPS=$(SRC:.c=.d)
Generates a list of filenames where .c extension is replaced by .d extension.
Generic Makefile - Complete
TARGET= project
CFLAGS=-g -W -Wall -Wextra -MMD
LDFLAGS=-lm
SRC=$(wildcard *.c)
DEPS=$(SRC:.c=.d)
OBJ=$(SRC:.c=.o)
default: $(TARGET)
.c.o:
gcc $(CFLAGS) -c $<
$(TARGET): $(OBJ)
gcc $(LDFLAGS) $^ -o $@
-include $(DEPS)
.PHONY: clean
clean:
rm -f *.o
rm -f $(TARGET)
Adding Options
DEBUG=yes #no
ifeq ($(DEBUG),yes)
CFLAGS=-W -Wall -ansi -g -DDEBUG
else
CFLAGS=-O3
endif