Project organisation and Makefiles (11)

It's good to know the C language, but it's much better if you also have a great organisation for your project. It permits to easily retrieve your file, to follow the usual methods known by the other programmers that might see your work, and Makefiles will give you more power on the compilation.

Let's transform the structure of this project :

hello_world/
    main.c
    foo.c
    foo.h

To this :

hello_world/
    src/
        main.c
        foo.c
    include/
        foo.h

We separate the code files into the src/ folder (means sources), and the header files into the include/ folder.

Include compiler flag

Let's compile our project : gcc src/main.c src/foo.c -o program. But, we get this error :

src/main.c:3:10: fatal error: foo.h: No such file or directory
    3 | #include "foo.h"
      |          ^~~~~~~
compilation terminated.
src/foo.c:1:10: fatal error: foo.h: No such file or directory
    1 | #include "foo.h"
      |          ^~~~~~~
compilation terminated.

That's because the "foo.h" file is not located in the src/ folder.

We could include "include/foo.h", but this is too annoying. So, we will use a new compiler flag to easily locate the header files !

gcc src/main.c src/foo.c -I include/ -o program

Then, it works. The "foo.h" has been located in the include/ folder.

Makefiles

The compilation part is annoying, we have to add every new C file as a gcc argument.

A program called make permits to easily compile all the files of our projects.

We have to create a file named Makefile at the project's root :

hello_world/
    src/
        main.c
        foo.c
    include/
        foo.h
    Makefile

A make file looks like a bash script but with some specificities :

CC = gcc # Specifies the C Compiler we are going to use
CC_FLAGS = -I include/

SRC = $(wildcard src/*.c) # Creates a variable `SRC` with all the files in the "src/" folder
OBJ = $(SRC:.c=.o) # Creates a variable `OBJ` with all the C files of `SRC` with the ".o" extension instead of ".c"

BIN = program

build : $(BIN)

$(BIN) : $(OBJ)
    $(CC) $^ -o $@ $(CC_FLAGS)

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

Run make build in your command shell.

gcc  src/foo.c -c -o src/foo.o -I include/
gcc  src/main.c -c -o src/main.o -I include/
gcc  src/foo.o src/main.o -o program -I include/

All the C files were compiled into object files, then these object files were compiled into one binary file.

I can't understand this make script... looks so weird !

In a Makefile, we can create variables, declare targets and run some system commands.

  • Make variables

      <ID> = <value>
    
  • Make targets and system commands

      <target> : <?prerequisites>
          <?system commands>
    

    A target can require other targets to work. The "build" target needs the "$(BIN)" target done, and the $(BIN) target needs the $(OBJ) target done.

      build : $(BIN)
    
      $(BIN) : $(OBJ)
          ...
    

    System commands are working exactly the same as BASH commands. There are some variables we can use in them.

Make variables

In target's code, we can use special variables.

  • $@ is the target's identifier
  • $< is the first prerequisite
  • $^ is all the prerequisites

This script :

program : main.c foo.c
    gcc $^ -o $@

Becomes for make :

program : main.c foo.c
    gcc main.c foo.c -o program

All files in a row !

We use this :

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

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

The same target compiles each C files into an object file of the same name. It uses the same system command.

To make this target happening, we have to require $(OBJ) as a prerequisite of another target, like there :

$(BIN) : $(OBJ)

Clean the generated files

clean :
    rm -rf src/*.o $(BIN)

Generated files should never be published with the project source code because they are not the same on every platforms.