Makefile’ы широко используются для создания билдов огромного множества проектов на самых разных языках, но проекты на C/C++ составляют большинство из них. Если вы разрабатываете или тестируете программное обеспечение, вероятность того, что вы их встретите, очень высока.
В этой статье мы рассмотрим некоторые распространенные ошибки при работе с Makefile’ами, а также расскажем о лучших практиках и поддержке кросс-компиляции.
Что вам понадобится: хорошее понимание, что из себя представляет Makefile, иерархия каталогов UNIX и процесс компиляции.
Версии Make
Стандартный POSIX синтаксис Make часто расширяется в различных реализациях. В этой статье мы используем GNU Make (gmake) и его расширения.
Каждый диалект Make при запуске ищет конкретный файл и, в случае, если не найдет свой родной, будет использовать стандартный Makefile; например, для GNU Make это будет файл GNUMakefile. При использовании диалекта поверх POSIX-интерфейса рекомендуется называть Makefile соответственно; так будет понятно, какая это реализация.
В большинстве систем Linux и OSX make - это симлинк на gmake. Проверить версию make в вашей системе можно, запустив следующее:
$ make --version
GNU Make 4.2.1
Built for x86_64-pc-linux-gnu
Copyright (C) 1988-2016 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Для удобства в этой статье в дальнейшем мы будем называть 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
, ломающие компиляцию. То же самое касается и кросс-компиляции.
Вместо этого следует сделать следующее:
OPENSSL_INCLUDE ?= -I/usr/include/openssl
OPENSSL_LIBS ?= -lssl -lcrypto
CFLAGS ?= -O2 -pipe
CFLAGS += $(OPENSSL_INCLUDE)
LIBS := $(OPENSSL_LIBS)
Хотя этот подход результирует в успешной компиляции, при необходимости заменяя значения, он достаточно трудоемок и подвержен ошибкам. Лучший способ подключить внешние библиотеки - это использовать pkg-config.
pkg-config
pkg-config
- это cli-инструмент, который предоставляет правильные параметры компилятора при включении библиотек; он широко используется как в Makefile’ах, так и в различных системах сборки, таких как CMake и meson.
Давайте перепишем предыдущий фрагмент кода, используя pkg-config
:
PKG_CONFIG ?= pkg-config
CFLAGS ?= -O2 -pipe
CFLAGS += -std=99
CFLAGS += $(shell ${PKG_CONFIG} --cflags openssl)
LIBS := $(shell ${PKG_CONFIG} --libs openssl)
Обратите внимание, что исполняемый файл pkg-config может быть переопределен из среды, опять же для поддержки кросс-компиляции.
Немедленное определение следует использовать с LIBS, чтобы избежать создания pkg-config при каждом вычислении переменной. Спасибо u/dima55 за подсказку.
Остальное
Другие исполняемые файлы, часто используемые при компиляции: ar, ranlib и as; не следует вызывать напрямую, лучше сохраняйте их имена в переменных и используйте вместо них эти переменные.
AR ?= ar
RANLIB ?= ranlib
AS ?= as
Из документации make:
The precise recipe is ${AS} ${ASFLAGS}.
Установка
Последняя часть Makefile это установка самой программы и связанных с ней данных. Это самая сложная часть, потому что существует множество типов данных, и каждый из них может быть установлен в разных местах.
Перевод подготовлен в преддверии старта курса "Программист С".
Комментарии (13)
includedlibrary
27.12.2021 01:30+1Какой-то корявый перевод.
Примечание. Немедленное определение (:=) используется, потому что Ленивое определение (=) результирует в рекурсивном цикле.
Note: The Immediate set (
:=
) is used because the Lazy set (=
) would result in a recursive loop.«result in» в данном случае переводится, как «приводит к». И эта ошибка по всему переводу проскакивает
ilammy
27.12.2021 04:37+7Чёт как-то мало практики. Выше уже сказали, что для программ написание мейкфайлов вручную уже не очень модно. Для проектов на Си потому, что большинство проектов на Си — это большие проекты на Си, что несёт свои сложности. Для других языков — потому что там есть система сборки, а мейкфайлы нужны максимум как удобный диспетчер для скриптов.
Если брать именно Си, то где другие интересные вопросы?
- как отключить бесполезные встроенные правила make
- когда пользоваться
@
,-
,+
- отслеживание зависимостей через
-MP
и-MD
- «стандартные» цели:
all
,check
,clean
, etc. - что делать при установке библиотек кроме
install
- как не забывать реализовать
uninstall
- заклинания для самодокументируемого
make help
- делать или не делать рекурсивные вызовы
make
Часть 1
График постинга горит, чтоль? Или время переводчика в преддверии нового года дорого? Исходная статья не такая уж и большая. Судя по кускам непереведённого текста, всё же график.
unsignedchar
27.12.2021 08:45+3Какой то странный перевод.
Пользователь определил CC=clang в среде, но gcc будет использоваться в любом случае, даже если он не был установлен.
это на каком языке? Как пользоваться неустановленным gcc?
jogick
27.12.2021 13:53Какое громкое название для такой статьи. Это даже переводом толком не назовёшь, а уж где тут "практики" не понял, не говоря уже о лучших.
vershov
Самая распространенная ошибка при использовании Makefile - это не пользоваться cmake для генерации Makefile.
DistortNeo
Да. Makefile — слишком низкоуровенвый инструмент, чтобы лазить в него руками. Тем не менее, базовые сценарии его использования все равно надо понимать.
Bonio
Makefile используют не только для сборки сишных программ. Для сборки всяких докер образов используется, просто для часто используемых наборов комманд для проекта.
unsignedchar
Это фактически init скрипт, переписанный с bash на make. Ничего из make power там нет.
Bonio
Фактически да. Но такий сценарий их использования тоже есть и используется довольно часто. Это удобнее, чем bash скрипты.
unsignedchar
Bash: хуже чем bash быть не может
Make: поддержите моё пиво..
help: ## This help. @awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST)
Bonio
Подкол засчитан, сам так делаю )
Нет, серьезно, Makefile удобнее и лаконичнее для списка часто используемых наборов комманд.
technic93
Самая распространенная ошибка при использовании cmake это генерировать Makefile вместо ninja.
IGR2014
CMake + Ninja = мощь