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 это установка самой программы и связанных с ней данных. Это самая сложная часть, потому что существует множество типов данных, и каждый из них может быть установлен в разных местах.
Перевод подготовлен в преддверии старта курса "Программист С".