As soon as the source code of the project should be distributed, it becomes necessary to use the build system instead of generating the favorite IDE. In the world of unix (with the filing of gnu), autotools are traditionally used, there are excellent alternatives in the form of cmake or scons. But for some reason, the Linux kernel is built using GNU Make, and the whole FreeBSD including ports using BSD Make. WTF?
Once having had enough time with autotools, I decided to conduct an experiment on how much you can shovel a Makefile to ensure a more or less convenient build.
')
Because modern make are very different, I used GNU Make, because It is native to my main system, GNU / Linux.
And so, the first pleasant thing that I discovered is the absence of the need to write a rule for each file. Those. instead of repeating for each file blocks like:
foo.o: foo.c
$ (CC) -c foo.c
You can simply specify a list of dependent objects for a specific binar and it will collect them:
main_executable: foo.o bar.o test.o main.o
$ (Cc) $ ^ -o $ @ $ (LDFLAGS)
What the $ ^ and $ @? These are automatic variables, here $ @ is expanded to the name of the target (ie, main_executable), and $ ^ to the list of all dependencies. Detailed description of all automatic variables
here .
The second problem I wanted to solve is the generation of dependencies. One file may depend on another, and it may depend on the system header, etc. If you prescribe them with your hands, then there is no point in the previous discovery.
As it turned out, gcc is able to parse the source and produce a list of headers on which it depends, this is done with the -M key: gcc -M foo.c. The output of the command is in the Makefile format, therefore it can be saved to a file and connected to the Makefile. Most build systems also use gcc -M. Classically, dependency generation was stuffed somewhere in the target: depends, but in the
manual there was a way to update all dependencies automatically.
The council uses one list of objects, but what if there are several goals? I'm too lazy to write with my hands for everyone! That's why I decided to organize a Makefile according to this scheme:
* There is a list of targets containing all targets (binarics). The rule for its assembly is written manually.
* For each goal there is a list of its objects: target_obj = foo.o bar.o, etc.
* There is an all purpose that passes through all the goals.
Proceeding from this, it is necessary to take all the targets, convert their names to the target_obj type, pull out the objects from them, rename their suffixes to .dep (or .d), and then only connect. Sounds hard? The line that makes it even more confusing:
deps = $ (foreach o, $ (targets: = _ obj), $ ($ (o):%. O = .deps /%. Dep)) . I will not even try to explain the nuances, I will say only that it generates a list of dependencies for each object, in the form of .deps / foo.dep. In the directory .deps just to not crap in the directory with sorts.
Finally, a makefile example:
CFLAGS = -Wall-pipe -ggdb
compiler_obj = compiler.o string_util.o tokenizer.o btree.o memory.o ast.o
vm_obj = vm.o
targets = compiler vm
all: $ (targets)
.deps /%. dep:% .c
@mkdir -p .deps
@set -e; rm -f $ @; \
$ (CC) -M $ (CFLAGS) $ <> $ @; \
sed -i 's, \ ($ * \) \. o [:] *, \ 1.o $ @:, g' $ @;
deps = $ (foreach o, $ (targets: = _ obj), $ ($ (o):%. o = .deps /%. dep))
-include $ (deps)
echo-deps:
@echo $ (deps)
compiler: $ (compiler_obj)
@ $ (CC) $ ^ -o $ @ $ (LDFLAGS)
vm: $ (vm_obj)
@ $ (CC) $ ^ -o $ @ $ (LDFLAGS)
clean:
rm -f * .o .deps / *. dep $ (targets)
ctags:
@ctags * .c * .h
PS In this case, there are still a lot of shortcomings, but the experience with GNU Make made me understand that if you wish, you can make a full-fledged build system on it. Perhaps in the future, I will write and post a more generic makefile library.