Website development
22 December 2008

GNU Make может больше чем ты думаешь

Как только исходники проекта надо распространять, то возникает необходимость использовать систему сборке, вместо того что нагенерила любимая IDE. В мире unix (с подачи gnu) традиционно используется autotools, ему есть отличные альтернативы в виде cmake или scons. Но почему-то ядро Linux собирается при помощи GNU Make, а вся FreeBSD включая порты при помощи BSD Make. WTF?

Однажды намучившись с autotools, я решил провести эксперимент — насколько можно перелопатить Makefile, чтобы обеспечить себе более-менее удобную сборку.



Т.к. современные make сильно отличаются, я использовал GNU Make, т.к. он родной для моей основной системы — GNU/Linux.
И так, первую приятную вещь, что я обнаружил, так это отсутствие необходимости писать для каждого файлика правило. Т.е. вместо повторения для каждого файла блоков вроде:
foo.o: foo.c
	$(CC) -c foo.c

Можно просто, для конкретного бинарика указать список зависимых объектов и он их соберёт:
main_executable: foo.o bar.o test.o main.o
    $(CC) $^ -o $@ $(LDFLAGS)

Что за $^ и $@? Это автоматические переменные, здесь $@ раскрывается в имя цели (т.е. main_executable), а $^ в список всех зависимостей. Подробное описание всех автоматических переменных тут.

Вторая проблема которую я хотел решить это генерация зависимостей. Один файлик может зависеть от другого, а он от системного заголовка и т.д. Если прописывать их руками то отпадает всякий смысл в предыдущем открытии.
Как выяснилось, gcc умеет разбирать исходник и выдавать список заголовков от которых он зависит, делается это при помощи ключа -M: gcc -M foo.c. Вывод команды в формате Makefile, следовательно он может быть сохранён в файл и подключён в Makefile. Большинство build систем также использую gcc -M. Классически, генерация зависимостей запихивалась куда-нибудь в таргет: depends, но в мануале нашёлся способ обновлять все зависимости автоматом.
В совете используется один список объектов, но что если целей несколько? Я слишком ленив чтобы руками писать для каждой! По этому я решил организовывать Makefile по такой схеме:
* Есть список targets, содержащий все цели (бинарики). Правило для его сборки прописывается вручную.
* Для кажой цели есть список её объектов: цель_obj = foo.o bar.o и т.д.
* Есть цель all которая проходит по всем целям.
Исходя из этого, необходимо взять все цели, преобразовать их имена в вид цель_obj, из них уже вытащить объекты, переименовать их суффиксы в .dep (или в .d), и потом только подключить. Звучит сложно? Строчка которая делает это ещё более запутана: deps = $(foreach o,$(targets:=_obj),$($(o):%.o=.deps/%.dep)). Я даже не буду пытаться объяснить ньюансы, скажу лишь что она генерит список зависимостей для каждого объекта, в виде .deps/foo.dep. В директорию .deps только чтобы не гадить в директории с сорцами.

В заключение пример мэйкфайла:
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


P.S. В данном случае есть ещё куча недочётов, но опыт работы с GNU Make, дал мне понять, что при желании, на ней можно сделать полноценную билд систему. Возможно в будущем, напишу и выложу более generic библиотеку мэйкфайлов.

+25
5.6k 44
Comments 58
Top of the day