- Что такое Makefile и как начать его использовать
- Введение
- Что такое make и Makefile
- Синтаксис Makefile
- Продвинутое использование
- Фальшивая цель
- Последовательный запуск команд и игнорирование ошибок
- Переменные
- Заключение
- Дополнительные материалы
- Отладка Makefile /часть 1/
- Отладочные возможности make
- Опции командной строки
- —just-print
- —print-data-base
- —warn-undefined-variables
- —debug опция
- Пишем код, удобный для отладки
- Хорошие практики кодирования
- Защитное программирование
Что такое Makefile и как начать его использовать
Гайд по основам make и Makefile для использования в собственных проектах.
Содержание
Введение
В жизни многих разработчиков найдётся история про первый рабочий день с новым проектом. После клонирования основного репозитория проекта наступает этап, когда приходится вводить множество команд с определёнными флагами и в заданной последовательности. Без описания команд, в большинстве случаев, невозможно понять что происходит, например:
Эти команды являются лишь частью того, что необходимо выполнить при разворачивании проекта. В приведённом примере видно, что команды сами по себе длинные, содержат много флагов, а значит, их трудно не только запомнить, но и вводить вручную. Постоянно вести документацию становится сложнее с ростом проекта, она неизбежно устаревает, а порог входа для новичков становится выше, ведь уже никто не в состоянии вспомнить всех деталей проекта. Некоторые такие команды необходимо использовать каждый день, и даже не один раз в день.
Со временем становится понятно, что нужен инструмент, способный объединить в себе подобные команды, предоставить к ним удобные шорткаты (более короткие и простые команды) и обеспечить самодокументацию проекта. Именно таким инструментом стал Makefile и утилита make . Этот гайд расскажет, как использование этих инструментов позволит свести процесс разворачивания проекта к нескольким коротким и понятным командам:
Что такое make и Makefile
Makefile — это файл, который хранится вместе с кодом в репозитории. Его обычно помещают в корень проекта. Он выступает и как документация, и как исполняемый код. Мейкфайл скрывает за собой детали реализации и раскладывает “по полочкам” команды, а утилита make запускает их из того мейкфайла, который находится в текущей директории.
Изначально make предназначалась для автоматизации сборки исполняемых программ и библиотек из исходного кода. Она поставлялась по умолчанию в большинство *nix дистрибутивов, что и привело к её широкому распространению и повсеместному использованию. Позже оказалось что данный инструмент удобно использовать и при разработке любых других проектов, потому что процесс в большинстве своём сводится к тем же задачам — автоматизация и сборка приложений.
Применение мейка в проектах стало стандартом для многих разработчиков, включая крупные проекты. Примеры мейкфайла можно найти у таких проектов, как Kubernetes, Babel, Ansible и, конечно же, повсеместно на Хекслете.
Синтаксис Makefile
make запускает цели из Makefile, которые состоят из команд:
Но недостаточно просто начать использовать мейкфайл в проекте. Чтобы получить эффект от его внедрения, понадобится поработать над разделением команд на цели, а целям дать семантически подходящие имена. Поначалу, перенос команд в Makefile может привести к свалке всех команд в одну цель с «размытым» названием:
Здесь происходит сразу несколько действий: создание файла с переменными окружения, подготовка базы данных, генерация ключей, установка зависимостей и запуск проекта. Это невозможно понять из комментариев и названия цели, поэтому будет правильно разделить эти независимые команды на разные цели:
Теперь, когда команды разбиты на цели, можно отдельно установить зависимости командой make install или запустить приложение через make start . Но остальные цели нужны только при первом разворачивании проекта и выполнять их нужно в определённой последовательности. Говоря языком мейкфайла, цель имеет пререквизиты:
Задачи будут выполняться только в указанной последовательности и только в случае успеха предыдущей задачи. Значит, можно добавить цель setup , чтобы объединить в себе все необходимые действия:
Теперь развернуть и запустить проект достаточно двумя командами:
Благодаря проделанной работе Makefile, команды проекта вместе с флагами сведены в Makefile. Он обеспечивает правильный порядок выполнения и не важно, какие при этом задействованы языки и технологии.
Продвинутое использование
Фальшивая цель
Использование make в проекте однажды может привести к появлению ошибки make: is up to date. , хотя всё написано правильно. Зачастую, её появление связано с наличием каталога или файла, совпадающего с именем цели. Например:
Как уже говорилось ранее, изначально make предназначалась для сборок из исходного кода. Поэтому она ищет каталог или файл с указанным именем, и пытается собрать из него проект. Чтобы изменить это поведение, необходимо в конце мейкфайла добавить .PHONY указатель на цель:
Последовательный запуск команд и игнорирование ошибок
Запуск команд можно производить по одной: make setup , make start , make test или указывать цепочкой через пробел: make setup start test . Последний способ работает как зависимость между задачами, но без описания её в мейкфайле. Сложности могут возникнуть, если одна из команд возвращает ошибку, которую нужно игнорировать. В примерах ранее такой командой было создание .env-файла при разворачивании проекта:
Самый простой (но не единственный) способ «заглушить» ошибку — это сделать логическое ИЛИ прямо в мейкфайле:
Добавлять такие хаки стоит с осторожностью, чтобы не «выстрелить себе в ногу» в более сложных случаях.
Переменные
Зачастую в команды подставляют параметры для конфигурации, указания путей, переменные окружения и make тоже позволяет этим управлять. Переменные можно прописать прямо в команде внутри мейкфайла и передавать их при вызове:
Переменные могут быть необязательными и содержать значение по умолчанию. Обычно их объявляют в начале мейкфайла.
Некоторые переменные в Makefile имеют названия отличные от системных. Например, $PWD называется $CURDIR в мейкфайле:
Заключение
В рамках данного гайда было рассказано об основных возможностях Makefile и утилиты make . Более плотное знакомство с данным инструментом откроет множество других его полезных возможностей: условия, циклы, подключение файлов. В компаниях, где имеется множество проектов, написанных разными командами в разное время, мейкфайл станет отличным подспорьем в стандартизации типовых команд: setup start test deploy . .
Возможность описывать в мейкфале последовательно многострочные команды позволяет использовать его как «универсальный клей» между менеджерами языков и другими утилитами. Широкая распространённость этого инструмента и общая простота позволяют внедрить его в свой проект достаточно легко, без необходимости доработок. Но мейкфайл может быть по-настоящему большим и сложным, это можно увидеть на примере реальных проектов:
Дополнительные материалы
- Руководство по современному Make — «выжимка» из документации на русском языке;
- Утилита make: полезный универсальный инструмент программиста — видео-версия данного гайда.
Мейкфайлы, использованные при составлении гайда:
Отладка Makefile /часть 1/
Отладка makefile — это что-то из черной магии. К несчастью, не существует такой вещи как makefile отладчик, чтобы изучить ход выполнения конкретного правила или как разворачивается переменная. Большую часть отладки можно выполнить с помощью обычных print’ов и проверкой makefile. Конечно, GNU make немного помогает своими встроенными методами и опциями командной строки. Один из лучших методов отладки makefile это добавить отладочные перехваты (hooks) и использовать техники безопасного программирования, на которые можно будет опереться, когда дела пойдут совсем плохо. Далее представлено несколько основных техник отладки и практик безопасного программирования, которые будут, на мой взгляд, наиболее полезными.
Отладочные возможности make
Очень полезная для отладки не работающего makefile — функция warning . Так как функция warning разворачивается в пустую строку, ее можно использовать везде в makefile: на верхнем уровне, в имени цели, в списке зависимостей и в командных скриптах. Это позволяет печатать значения переменных там, где это наиболее подходит для их проверки. Например:
Заметим, что выполнение функции warning следует нормальному ходу алгоритма make для немедленных и отложенных вычислений. Также, присваивание к BAZ содержит warning и сообщение не печатается, пока BAZ не будет развернут в списке зависимостей.
Возможность вставки warning вызов куда угодно делает его очень полезным инструментом отладки.
Опции командной строки
Есть три очень полезные опции командной строки для отладки: —just-print (-n) , —print-data-base (-p) и —warn-undefined-variables .
—just-print
Первое испытание новой цели в makefile — это вызвать make с опцией —just-print (-n) . Будучи вызванным с этой опцией make прочитает makefile и напечатает каждую команду, которую в обычном режиме он бы выполнил для обновления цели. Для удобства, GNU make также выведет команды помеченные собачкой ( @ ) — заглушающим модификатором.
Опция предназначена для полного подавления выполнения всех команд. Однако, есть исключения. В то время как make не выполняет командные скрипты целей, он выполнит вызовы shell функции, которые будут в немедленном контексте выполнения. Для примера:
Смысл переменной _MKDIRS в инициировании создания нужных директорий. Если выполнить это с —just-print опцией, команда оболочки будет выполнена как обычно в момент чтения makefile. Затем, make выведет (без исполнения) каждую команду компиляции необходимую для обновления списка файлов $(objects) .
—print-data-base
Еще одна опция, которую нужно использовать почаще. С ней, после обычного «прогона» makefile, make выдаст в конце его внутреннюю базу данных. Данные сгруппированы по группам: переменные, директории, неявные правила, переменные шаблонов, файлы (явные правила) и vpath путь поиска. Давайте остановимся на этих секциях подробнее.
Секция Variables выводит список переменных с описательным комментарием:
Авто-переменные не выводятся, но выводятся другие, зависящие от них, полезные переменные, такие как $( . В комментарии пишется вывод функции origin (см. make manual). Если переменная определена в файле, будет выведено имя файла и номер строки объявления. Простые и рекурсивные переменные определяются по оператору присваивания. Вычисленное значение простых переменных также печатается в правой части выражения.
Следующий раздел Directories более полезен разработчикам make , а не пользователям make . Представляет собой список папок просмотренных make, включая SCCS и RCS под-папки, которые могут быть, но обычно отсутствуют. Для каждой папки выводится детали реализации: номер устройства, inode и статистика по совпадениям шаблонов файлов.
Секция Implicit rules содержит все встроенные и пользовательские шаблоны в базе данных make . Опять же, для тех правил, которые определены в файле, выводится имя файла и строка определения:
Изучение этой секции даст быстрое понимание разнообразия и структуры встроенных правил make . Конечно, не все встроенные правила реализованы как шаблонные. Если нужное правило не находится, его еще можно поискать в разделе Files, где выводятся суффиксные правила в старом стиле.
Следующая секция это каталог зависящих от конкретного шаблона переменных определенных в makefile. Описание этих переменных — это их значения, которые будут подставлены во время выполнения при выполнении соответствующих им правил. Например, для переменной определенной как:
Далее следует Files секция, которая выводит все явные и суффикс- правила, которые относятся к конкретным файлам:
Промежуточные файлы и суффикс-правила обозначены как «Not a target»; остальное — цели. Каждый файл включает комментарии, показывающие как make будет обрабатывать это правило. У файлов, которые найдены через обычный vpath поиск, показан найденный путь до них.
Последняя секция называется VPATH Search Paths и перечисляет значение VPATH и все vpath шаблоны.
Для makefile‘ов, которые обильно используют пользовательские функции и eval для создания сложных переменных и правил, исследование этого вывода — единственный путь проверить, что разворачивание макросов дает ожидаемые значения.
—warn-undefined-variables
Эта опция заставляет make выводить предупреждение при вычислении неопределенной переменной. Так как неопределенные переменные вычисляются в пустую строку, зачастую, опечатки остаются необнаруженными долгое время. Проблема с этим ключом в том, что многие встроенные правила используют неопределенные переменные, которые нужны для перехвата пользовательских значений. Поэтому запуск make с этой опцией неизбежно выведет много предупреждений, которые не являются ошибками и не связаны с makefile‘ом пользователя, Например:
Тем не менее, эта опция может быть крайне полезна в поиске ошибок такого типа.
—debug опция
Когда нужно узнать как make анализирует твой граф зависимостей, используй —debug опцию. Она предоставляет самую детальную доступную информацию без запуска отладчика. Есть пять опций вывода отладки и один модификатор: basic , verbose , implicit , jobs , all , и makefile , соответственно.
Если опция указана в форме —debug , используется basic — краткий вывод для отладки. Если опция указана в виде -d , используется all . Для выбора других комбинаций можно использовать список разделенный запятыми: —debug=option1,option2 , где option может быть одно из следующих значений (на самом деле, make смотрит только на первую букву):
basic
Наименьшая детализированность. Когда включена, make выводит каждую цель, которая нуждается в обновлении и статус действия обновления.
verbose
Эта опция включает basic вывод и дополнительную информацию о проанализированных файлах и о зависимостях, которые не нуждаются в обновлении.
implicit
Эта опция включает basic вывод и дополнительную информацию о неявных правилах, просмотренных в поиске нужного для каждой выполняемой цели.
jobs
Эта опция выводит детали о запущенных make ‘ом подпроцессах. Она не включает basic вывод.
all
Включает все выше перечисленное и является значением по умолчанию для -d опции.
makefile
Обычно, отладочная информация включается после того, как все makefileы будут обновлены. Обновление включает в себя и обновление всех импортированных файлов, таких как файлы со списками зависимостей. С этим модификатором make выведет выбранную опциями информацию в ходе обновления makefile‘ов и импортированных файлов. Он включает basic информацию, а также включается при указании all опции.
Пишем код, удобный для отладки
Как мы видим, средств отладки для makefile не так уж и много, вывод внутреннего состояния и несколько функций отчетов. Когда дела обстоят именно так, мы сами вынуждены делать наши makefileы такими, чтобы и минимизировать шанс ошибок и иметь инструментарий для отладки этих ошибок.
Правила в этом разделе, в несколько произвольном виде, основаны на практиках кодирования, защитном программировании и техник отладки. Тогда как определенные правила, такие как проверка статуса выхода команд, можно поместить как в хорошие практики кодирования так и в защитное программирование, эти три категории отражают общую шкалу. Кодируй хорошо свои makefile, но без излишеств в срезании углов. Используй защитное программирование, чтобы защитить makefile от неожиданных событий и изменчивого внешнего окружения. В конце концов, когда баги все-таки будут возникать, используй каждый приём, доступный тебе, чтобы их размозжить.
Хорошие практики кодирования
По моему опыту, большая часть программистов не рассматривает написание makefile’ов как программирование, и, из-за этого, не уделяют такое же внимание, как при разработке на C++ или Java. Но ведь язык make это полный по Тьюрингу декларативный язык! И если надежность и легкость сопровождения твоей системы сборки важна, пиши ее тщательно и используй все доступные тебе лучшие практики.
Один из важнейших аспектов в программировании безотказных makefile это проверка статуса выхода команд. Конечно, make проверяет для простых команд это дело сам, но makefile часто содержит составные команды, которые могут не сработать молча:
При выполнении этот makefile не прервется с ошибкой, тогда как ошибка несомненно произойдет:
И далее, выражение подстановки не найдет никаких файлов .c , и молча вернет выражение подстановки. Ой. Способ по-лучше, это использовать возможности командной оболочки по проверке и предотвращению ошибок:
Теперь ошибка cd правильно передастся make , команда echo никогда не исполнится и make прервётся со статусом ошибки. В дополнение, после установки nullglob опции bash а подстановка вернет пустую строку если не будут найдены такие файлы. (Конечно, в твоем конкретном случае могут быть другие предпочтения.)
Другой хорошей практикой является форматирование своего кода для максимальной читабельности. Большая часть makefile’ов плохо отформатированы, и как следствие, их трудно читать. Что тут кажется легче прочитать?
Если ты как все люди, то тебе будет труднее прочитать первое, точку с запятой сложно найти, и количество операндов сосчитать еще сложнее. И это не шутки. Огромный процент синтаксических ошибок, которые встречаются в командных скриптах будут из-за пропущенных точек с запятой, символов экранирования, других разделителей, таких как разделитель программного конвейера или логические операторы.
Замечательно, что не все пропущенные разделители дадут ошибку. Для справки, ни одна из следующих ошибок не даст синтаксической ошибки оболочки окружения:
С форматированием команд для читабельности такого рода ошибки станет легко ловить. Делай отступы при работе в пользовательских функциях. Если же пробелы станут проблемой при работе макроса, форматирование можно обернуть через вызов strip функции. Форматируй длинные списки значений по одному на строку. Добавь комментарий перед каждой целью, дай краткое пояснение, и задокументируй параметры.
Следующей хорошей практикой является использование переменных для хранения общих значений. Как и в любой программе, неограниченное использование буквальных значений создает дублицирование кода и приводит к проблемам поддержки и багам. Еще одно большое преимущество переменных — make умеет их выводить в ходе работы в целях отладки. Ниже в разделе «способы отладки» будет приведена полезная команда.
Защитное программирование
Защитный код это код, который выполняется когда одно из твоих предположений или ожиданий неверно — if проверяет то, что всегда истинно, assert функция никогда не упадет, и конечно, значимость такого кода в том, что внезапно (обычно когда меньше всего этого ждешь), он выполняется и дает предупреждение или ошибку, или ты включаешь трассировку кода, чтобы увидеть внутреннюю работу make .
Валидация — это отличный пример защитного программирования. Следующий код проверяет что текущая запущенная версия make — 3.80:
Для приложений Java полезно включить проверку файлов в CLASSPATH .
Код валидации также может просто проверять что что-то истинно.
Другой отличный пример защитного программирования это использование assert функций, например таких:
Небольшое количество assert вызовов в makefile будет дешевым и эффективным путём обнаружения пропавших переменных или переменных с опечатками в названии, а также и других ожиданий.
Также можно определить пару функций для трассировки разворачивания пользовательских функций:
Можно добавить вызовы этих макросов в свою функцию и оставить их выключенными до тех пор, пока они не потребуются для отладки. Для включения их нужно установить debug_trace в любое не пустое значение:
И еще одно средство защитного программирования это легкое и простое выключение действия @ командного модификатора, если использовать его через переменную, а не буквально:
Используя эту технику можно увидеть ход выполнения заглушенных команд просто переопределив переменную в командной строке: