Еще один makefile

MakefileДоброго времени суток!
Сколько уже про makefile говорено и писано — и все равно хочется отчебучить что-нибудь свое! Видят мои глаза отсутствие идеала, а пальцы стремятся это дело исправить.
Так что внесу и я свои 5 центов в это дело — опишу я идеальный makefile!
P. S. Все найденные ошибки, реализованные пожелания и изменения я буду здесь выкладывать. Так что здесь будет всегда актуальное состояние. Ну… так я планирую…


Постановка задачи

Душа поэта жаждет нечто особенное:

  • понятный — с нормальным набором комментариев, чтобы из-за каждой строчки не нужно было бежать в справочник;
  • переносимый — чтобы его легко можно было переносить куда угодно — на новый проект, новую платформу или другой язык программирования;
  • редактируемый — из предыдущего пожелания прямо следует возможность его легкой модификации;
  • доступный для отладки — а то вечно плодят кучу непонятных переменных, а просмотреть их — то ли ключи какие-то хитрые нужны, то ли поллитры;
  • широко варьируемый — вот, скажем, я люблю, чтобы объектные файлы были .obj, а зависимости — .dep (а не стандартные .o и .d), или же я хочу, чтобы объектные файлы были в одной директории, файлы зависимости — в другой;
  • UPD: DEBUG- и RELEASE-версии — это абсолютно необходимо для приложений для компьютера, но не факт что нужно для микроконтроллеров… Но, тем не менее, это тоже иногда нужно. Так что должна быть возможность разделять параметры для разных вариантов компилирования.
Вот такую красотку я хочу!
Сказано — сделано. Встречайте!

ВНИМАНИЕ!!! Если Вы будете экспериментировать со структурой, файлами — обязательно делайте резервные копии! Тут один неверный символ может все рассыпать очень неочевидным способом!

Вкратце о makefile-ах

Пару слов о makefile-ах — читайте в Интернете! Материалов не то, чтобы масса, а просто океан!
Лично мне больше всего помогло изучение описания, одного варианта и другого.
Если читать не хочется, то опишу классическую процедуру сборки проекта с помощью makefile:
  1. создали файлы зависимостей, чтобы знать при каких изменениях что надо пересобрать;
  2. создали объектные файлы из исходников;
  3. создали двоичный файл из объектных;
  4. создали всевозможные вспомогательные файлы из двоичного;
  5. узнали размер результирующего файла;
  6. прошили устройство;
  7. стерли все временные файлы, если нужно.

Возможности

Мой makefile имеет следующие функции (или «цели», если в терминах makefile):
  • build — цель по умолчанию. Сборка;
  • rebuild — стирает все временные файлы, создает файлы зависимостей, делает компиляцию, сборку, прошивка;
  • all — файлы зависимостей, сборка, прошивка;
  • dep — файлы зависимостей;
  • prog — прошивка;
  • clean — очистка временных файлов;
  • vars — список значений всех значимых переменных;
  • disasm — вывод ассемблерного сгенерированного кода.

UPD: Также можно указать режим компилирования — DEBUG или RELEASE. Для первого варианта сборка запускается make… MODE=DEBUG, для второго — make… MODE=RELEASE ("..." — это цели и прочие параметры).
Если надо добавить что-нибудь еще — пишите, буду добавлять и исправлять.

Структура файлов

Я решил не мелочиться и один makefile расплодить на целых 5:
  • makefile — основной makefile, используемый средой программирования;
  • makefile.operat — перечень конкретных действий, все настройки в других файлах;
  • makefile.filelist — список файлов и путей текущего проекта;
  • makefile.args — ключи и прочие параметры компиляции;
  • makefile.vars — вывод всех существенных переменных.

Последовательность работы следующая:
  1. прописать в среде программирования ключи для работы (очистка — make clean, сборка — make all, переделка — make rebuild);
  2. один раз для проекта настроить параметры (makefile.args);
  3. по ходу дела корректировать перечень файлов (makefile.filelist).
Если что-то работает не так, как вы этого хотели — вызывайте make vars и изучайте значения переменных (список большой, поэтому лучше делать что-то вроде «make vars > vars.txt»). Остальные файлы менять не нужно.

makefile
В этом файле прописаны команды и реакция на них. Первая цель в списке (в моем случае build) является целью по умолчанию.
Вы можете тут менять текстовое название (то, что перед двоеточием), выбирать свою цель по умолчанию.

makefile.filelist
Здесь у нас название цели TARGET (имя файла с финальной прошивкой) и все пути. Тут также указывается платформа MCU.
Поддерживаются различные директории для исходных файлов, объектных, зависимостей и всех остальных (впрочем, при желании все можно настроить на одну директорию). В основе лежит TEMP_PATH — директория для временных файлов. Лично я люблю все файлы, кроме исходников и финальной прошивки, записывать во временную директорию, которая при старте операционной системы очищается. Поэтому я использую переменную среды TEMP. Объектные файлы OBJ_PATH хранятся в одном месте, файлы зависимостей DEP_PATH — в другом. В этих директориях создается структура директорий, соответствующая исходным файлов. Это позволяет использовать одинаковые имена файлов в разных директориях.
Далее идут пути к результирующим файлам. Названия говорят сами за себя, поэтому смысла их подробно расписывать тут нет.
LIBS — используемые библиотеки.
FILELIST_C, FILELIST_CPP, FILELIST_ASM — перечень исходных файлов, разделенных пробелами (можно и с новой строки, тогда FILELIST_C += ...). Обратите внимание — далее для построения пути используется путь к корневой директории проекта SRC_PATH, поэтому список файлов описывается относительно корневой директории проекта.
DIR_H — директории с заголовками (.H-файлами).

makefile.args
Тут все достаточно очевидно:
  • расширения для файлов — DEP, OBJ;
  • параметры компиляции C (C_FLAGS), C++ (CPP_FLAGS) и ассемблера (ASM_FLAGS);
  • параметры сборщика — LINKER_SCRIPT, LINK_FLAGS;
  • настройки программатора — PRG_FLAGS;
  • define-ы по умолчанию — DEFINE;
  • пути к инструментам GNU (UTILS_DIR), базовая часть имени (UTILS). У меня arm-none-eabi-g++, arm-none-eabi-gcc, arm-none-eabi-objcopy,… — значит, UTILS = $(UTILS_DIR)arm-none-eabi-;
  • специфичный момент — создание директорий (MD). Учитывая, что я буду создавать новую структуру директорий, мне нужна команда, создающая их. При этом она должна работать молча, а не ругаться, когда я создаю уже существующую директорию. В Windows для этих целей используется MKDIR -p. В Unix — не знаю, подскажите, допишу;
  • обработка вывода сообщений об ошибках — ERR_VIEW (он закомментирован). Это еще один специфичный момент. Дело в том, что я использую Microsoft Visual Studio, у которого в сообщениях об ошибках путь к файлу и строка с ошибкой не соответствуют стандарту GNU. Поэтому используется утилитка gnu2msdev, которая это дело исправляет. Подробнее можно почитать тут, файл я прикрепил (gnu2msdev.cpp)- его необходимо откомпилировать для своей ОС и поместить в директорию, которая прописана в PATH.

UPD: для параметров компиляции, сборщика и define-ов можно указывать различные варианты для версий DEBUG / не-DEBUG и RELEASE / не-RELEASE. Например, параметры компиляции для C-файлов (C_FLAGS). В конце их перечисления есть такие строки:
ifeq ($(MODE), DEBUG)
	C_FLAGS +=				# на случай DEBUG-версии
else
	C_FLAGS +=				# на случай не DEBUG-версии
endif
ifeq ($(MODE), RELEASE)
	C_FLAGS +=				# на случай RELEASE-версии
else
	C_FLAGS +=				# на случай не RELEASE-версии
endif

В строке 2 вы указываете (добавляете к имеющимся параметрам) то, что нужно только для DEBUG-версии, в 4-ой — то, что нужно не для DEBUG-версии. Аналогично 6-ая, 8-ая строки — RELEASE-версия.
Аналогичные настройки есть и для прочих параметров.

makefile.vars
Тут все элементарно — выводим значения переменных.

makefile.operat
А вот здесь уже все весьма нетривиально… Чтобы все это понять, рекомендую изучать руководства. Я опишу сложные места.

ALL_INCLUDE = $(addprefix -I, $(INCLUDE))

Здесь ко всем INCLUDE-ам дописывается спереди -I, как это требуется для ключей компилятора. Аналогично DEFINE, LIBS.

OBJ_C = $(addprefix $(OBJ_PATH), $(patsubst %.c, %.$(OBJ), $(SRC_C)))

Исходные данные — перечень С-файлов SRC_C. Нам надо из него получить список OBJ-файлов, которые будут находиться в директории OBJ_PATH (и глубже, если того требует структура директорий проекта). Значит, я должен 1) в перечне файлов поменять расширения (*.C на *.$(OBJ), где $(OBJ) — это значение переменной OBJ) командой patsubst, 2) добавить перед всеми файлами путь к объектным файлам OBJ_PATH командой addprefix.
Аналогично делается для файлов зависимостей.

Давайте подробно разберем процедуру построения файлов зависимостей (например, C:\TEMP\DIR\file.dep для С-файла C:\SOURCE\DIR\file.c):
$(DEP_C): $(DEP_PATH)%.$(DEP): $(SRC_PATH)%.c
	@echo dep: $< ...
	@$(MD) $(dir $@)
	@$(CC) $(C_FLAGS) $(ALL_INCLUDE) $(ALL_DEFINE) -M -MT $(patsubst $(DEP_PATH)%.$(DEP), $(OBJ_PATH)%.$(OBJ), $@) -MF $@ $<

Первая строка: мы рассматривает что нужно для создания файлов зависимостей, которые перечислены в DEP_C. По классике эта строка записывается так:
$(DEP_C): %.d: %.c

'%' — это общая часть в обеих строках. Т. е. для каждого файла из DEP_C *.d необходимо указать соответствующий *.c. Если он не менялся с последнего запуска, то выполнять действия не надо.
Я пошел на усложнения.
Во-первых, я даю возможность менять расширение файла зависимостей, и текущее расширение записано в DEP. Следовательно, правило усложняется:
$(DEP_C): %.$(DEP): %.c

Во-вторых, файлы зависимостей и исходные файлы могут храниться в разных директориях:
$(DEP_C): $(DEP_PATH)%.$(DEP): $(SRC_PATH)%.c

Тут от имени из DEP_C отбрасывается путь к файлам зависимостей DEP_PATH, и из оставшейся часть создается имя .c-файла, у которого отброшено SRC_PATH.
В нашем случае: C:\TEMP\DIR\file.dep: C:\TEMP\DIR\file.dep: C:\SOURCE\DIR\file.c. Жирным выделено то, что совпадает.

В следующей строке мы на экран выводим имя файла зависимости (в нашем примере «dep: DIR\file.с ...»).

Далее вызывается создание директории из файла-цели (C:\TEMP\DIR\file.dep). Для этих целей используется команда из переменной MD. Для нее берется путь к файлу (C:\TEMP\DIR), для чего используется функция dir.

Последней строчкой вызывается компилятор. Здесь куча переменных: CC — команда вызова компилятора, C_FLAGS — все флаги С, ALL_INCLUDE — все INCLUDE-ы, ALL_DEFINE — все DEFINE-ы, -M — создание файла зависимостей, -MF $@ — путь к файлу зависимостей ($@ — файл цели, C:\TEMP\DIR\file.dep), $< — имя C-файла (C:\SOURCE\DIR\file.c).

Пару слов про ключ -MT. Этот ключ заменяет в создаваемом файле имя объектного файла. Если этого ключа не указать, что для нашего примера файл зависимостей начнется так:
DIR/file.o:

Нам же надо указать полный путь к объектному файлу и заменить расширение файла. Это мы делаем следующим кодом:
$(patsubst $(DEP_PATH)%.$(DEP), $(OBJ_PATH)%.$(OBJ), $@)

Функция $(patsubst A, B, C) ищет в C шаблон A и заменяет его на B.

Ну вот, в общем-то, и все. Дальше там уже ничего интересного.

Присылайте ваши пожелания, результаты испытаний. Может, я что-то написал неправильно? Или что-то добавить бы? Буду здесь вносить исправления, модификации — статью просто так не брошу :-)! Так что подписывайтесь на изменения.

Успехов!

P. S.Хочу поучаствовать в конкурсе, поэтому добавляю: Спонсоры
  • +3
  • 25 марта 2014, 14:57
  • PICC
  • 1
Файлы в топике: makefile.zip

Комментарии (10)

RSS свернуть / развернуть
В ответе на этот комментарий я буду описывать все изменения.
0
  • avatar
  • PICC
  • 25 марта 2014, 14:58
Исправлена цель по умолчанию. Теперь это build.
0
Сделан ассемблерный листинг кода программы. С ключами я не очень уверен (переменная DISASM_FLAGS)…
0
Добавил разделение на DEBUG- и RELEASE- версии, а также исправил пару мелких обнаруженных ошибок. Исправлен прикрепленный файл!
0
У меня в билд скриптах помимо сборки основного приложения есть:
— автоматический поиск и проверка подходящего установленного тулчейна;
— сборка ПО для тествого стенда, который включает ПО для ПК и/или ПО для дополнительной тестовой остнастки;
— сборка и выполнение модульных тестов;
— сборка bootloader-а, если он нужен;
— автоматическая генерация версии прошивки;
— подсчет и запись контрольной суммы прошивки для самопроверки;
— может собираться несколько версий прошивки с разными параметрами;
— продукты сборки вместе со скриптами для прошивки упаковываются в архив, который потом передается регулировщикам для прошивки серийных изделий.
Сейчас я использую систему сборки scons. Да, она в отличии от make требует установленного Python. Да, она не очень быстра на больших проектах (как говорят). Но возможность в билд скриптах использовать полноценный скриптовый язык программирования многого.
Всё это, конечно, можно сделать и в make, но ограмное количество знаков препинания в makefile-ах на меня наводит тоску. Сколько их не писал, не читал, а без справочника не могу.
0
А зачем список файлов руками указывать? Не проще ли написать
FILELIST_CSRC = $(wildcard [_a-zA-Z]*.cpp)?
0
А если у Вас файлы в разных поддиректориях находятся? А если у Вас не один уровень вложенности?
А если Вам нужны НЕ ВСЕ файлы? Во FreeRTOS есть взаимоисключающие вещи.
0
rebuild — цель по умолчанию. Стирает все временные файлы, создает файлы зависимостей, делает компиляцию, сборку, прошивка;

Здесь очень спорный момент. Идеология make позволяет проводить «обработку» только тех файлов которые были модифицированы позже, чем зависимые от них. Грубо говоря:

При проверке цели test1.o сравниваются дата и время изменения файла test1.o и test1.c. Если файл test1.o не существует или файл test1.c был изменены позднее чем test1.o то будет выполнена команда gcc -с test1.c -o test1.o.

Это одна из основных «фичей» данного инструмента. Для небольших проектов не особо важна разница между полной пересборкой и «инкрементальной» пересборкой. Но в больших проектах это очень ощутимо. Для большого проекта время полной пересборки может измерятся десятками минут. А время «инкрементальной» переборки — секундами.

Не совсем понятно, почему вы выбрали целью по умолчанию полную пересборку.
0
Ну-у, я тут еще не очень определился… В общем-то, это легко переопределяется. Надо будет дописать как это переделывается в моем случае.
0
Согласен, исправил
0
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.