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.