Makefile’ы широко используются для создания билдов огромного множества проектов на самых разных языках, но проекты на C/C++ составляют большинство из них. Если вы разрабатываете или тестируете программное обеспечение, вероятность того, что вы их встретите, очень высока. В этой статье мы рассмотрим некоторые распространенные ошибки при работе с Makefile’ами, а также расскажем о лучших практиках и поддержке кросс-компиляции. Что вам понадобится: хорошее понимание, что из себя представляет Makefile, иерархия каталогов UNIX и процесс компиляции. Стандартный POSIX синтаксис Make часто расширяется в различных реализациях. В этой статье мы используем GNU Make (gmake) и его расширения. Каждый диалект Make при запуске ищет конкретный файл и, в случае, если не найдет свой родной, будет использовать стандартный Makefile; например, для GNU Make это будет файл GNUMakefile. При использовании диалекта поверх POSIX-интерфейса рекомендуется называть Makefile соответственно; так будет понятно, какая это реализация. В большинстве систем Linux и OSX make - это симлинк на gmake. Проверить версию make в вашей системе можно, запустив следующее: Для удобства в этой статье в дальнейшем мы будем называть GNUMakefile и gmake - Makefile и make соответственно. В этой статье мы используем два способа (из пяти возможных) для определения переменной в Makefile. Краткий обзор на них есть на StackOverflow: Ленивое (отложенное) определение Стандартный способ определения переменной - значения в ней рекурсивно разворачиваются только когда переменная используется, а не в момент, когда она объявляется. VARIABLE = value Немедленное определение Определение переменной с простым заполнением значений внутри - значения внутри него разворачиваются во время объявления. VARIABLE := value Определение, если отсутствует значение Setting of a variable only if it doesn’t have a value Переменная определяется, только если ей еще не присвоено значение. CC ?= gcc LD ?= gcc Чаще всего можно с уверенностью предположить, что для CC и LDD были присвоены правильные значения, но не повредит присваивать их с помощью оператора ?= только в том случае, если они еще не были присвоены в среде. При использовании оператора присваивания = переопределяет значения CC и LDD из среды; это означает, что мы выбираем компилятор по умолчанию, и его нельзя изменить без редактирования Makefile или добавления этих переменных в командную строку. Это, как правило, приводит к двум проблемам: Пользователь определил CC=clang в среде, но gcc будет использоваться в любом случае, даже если он не был установлен. Среда кросс-компиляции определила CC как ссылку на фактический компилятор целевой архитектуры, например, arm-pc-linux-cc, но gcc хоста также будет использоваться. Если пользователь подтвердил наличие этих проблем (т.е. компиляция не удалась, потому что gcc не установлен), он может добавить CC=clang к вызову make: $ make CC=clang Это решение работает (clang компилирует исходные коды), независимо от того, какие операторы (= или ?=) использует Makefile, но это добавляет нагрузку на пользователя: чтение Makefile необходимо для проверки переменных, которые нужно добавить в командную строку. Здесь также могут быть ошибки, потому что легко пропустить одно присвоение переменной в большом проекте, в то время как предположение, что среда уже содержит правильные значения, гораздо смелее. По этим причинам это решение считается неоптимальным, особенно для обслуживания пакетов, и поэтому использовать его не рекомендуется. Утилита make также использует переменные, которые определены неявными правилами5, и кроме этих переменных некоторые определяют дополнительные флаги билдов: CFLAGS: флаги для компилятора C. CXXFLAGS: флаги для компилятора C++. CPPFLAGS: для флагов препроцессора для компиляторов C/C++ и Fortran. Примечание: существует переменная с именем CCFLAGS, которое используют некоторые проекты; она определяет дополнительные флаги для обоих C/C++ компиляторов. Эта переменная не определяется неявными правилами, по возможности избегайте ее. Примечание 2: системы сборки обычно следуют неявным правилам make как для именования переменных, так и для присвоения им значений. Другими словами, определение CFLAGS означает определение дополнительных флагов для компилятора C независимо от используемой вами системы сборки. Обычно мы добавляем к компилятору параметры, специфичные для приложения, например версию языка (хотим ли мы использовать c89 или c99?). Затем пользователь добавляет свои собственные CFLAGS/CXXFLAGS, чтобы включить возможность отладки и добавить оптимизации; добавлять эти определяемые пользователем флаги очень важно. У нас может возникнуть соблазн сделать следующее: CFLAGS = -ansi -std=99 Но это сбросит CFLAGS среды, которые могут содержать определенное пользователем значение. Вместо этого рекомендуется сделать: CFLAGS := ${CFLAGS} -ansi -std=99 Примечание. Немедленное определение (:=) используется, потому что Ленивое определение (=) результирует в рекурсивном цикле. Или, если у вас длинный CFLAGS: CFLAGS += -ansi -std=99 В примере выше мы добавляем наши значения в CFLAGS среды; если он не определен, он будет развернут пустым. Полученный CFLAGS по-прежнему будет рекурсивно развернутой переменной. Мы можем немного оптимизировать это, преобразовав CFLAGS из рекурсивной переменной в простую. CFLAGS := ${CFLAGS} CFLAGS += -ansi -std=99 Чтобы добавить библиотеки в программу, флаги gcc необходимы как для компиляции, так и для компоновки. Вы можете добавить значения по умолчанию при добавлении библиотек, например, расположение заголовков по умолчанию, с помощью /usr/include/; если вы используете этот подход, используйте это значение внутри переменной, которая может быть переопределена из среды (?= set). Предположим, мы хотим включить и связать нашу программу с OpenSSL, широко используемой библиотекой для криптографии и протоколов TLS/SSL; мы бы интуитивно добавили -I/usr/include/openssl в наши CFLAGS/CXXFLAGS. Это может быть сработать для большинства систем Linux, но у пользователя MacOS будут заголовки OpenSSL в /usr/local/include/openssl, ломающие компиляцию. То же самое касается и кросс-компиляции. Вместо этого следует сделать следующее: Хотя этот подход результирует в успешной компиляции, при необходимости заменяя значения, он достаточно трудоемок и подвержен ошибкам. Лучший способ подключить внешние библиотеки - это использовать pkg-config. pkg-config - это cli-инструмент, который предоставляет правильные параметры компилятора при включении библиотек; он широко используется как в Makefile’ах, так и в различных системах сборки, таких как CMake и meson. Давайте перепишем предыдущий фрагмент кода, используя pkg-config: Обратите внимание, что исполняемый файл pkg-config может быть переопределен из среды, опять же для поддержки кросс-компиляции. Немедленное определение следует использовать с LIBS, чтобы избежать создания pkg-config при каждом вычислении переменной. Спасибо u/dima55 за подсказку. Другие исполняемые файлы, часто используемые при компиляции: ar, ranlib и as; не следует вызывать напрямую, лучше сохраняйте их имена в переменных и используйте вместо них эти переменные. Из документации make: Последняя часть Makefile это установка самой программы и связанных с ней данных. Это самая сложная часть, потому что существует множество типов данных, и каждый из них может быть установлен в разных местах. Перевод подготовлен в преддверии старта курса "Программист С". Редактор