Skip to main content
Compilation Toolchain
Compilation Toolchain
IOT
2h

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 -c commands
  • And 1 gcc -o command are required to build the executable
  • (Hint: it’s actually way more than that!)

Anatomy of Make

  • Requires a file named Makefile
  • The Makefile contains 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

VariableDescription
$@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)
Note

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