Как стать автором
Обновить

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

Время на прочтение3 мин
Количество просмотров6.7K
Как только исходники проекта надо распространять, то возникает необходимость использовать систему сборке, вместо того что нагенерила любимая 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
Комментарии60

Публикации

Истории

Ближайшие события

Weekend Offer в AliExpress
Дата20 – 21 апреля
Время10:00 – 20:00
Место
Онлайн
Конференция «Я.Железо»
Дата18 мая
Время14:00 – 23:59
Место
МоскваОнлайн