MAKEFILES, лучшие практики. Часть 1

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

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


Перевод подготовлен в преддверии старта курса "Программист С".

Источник: https://habr.com/ru/company/otus/blog/597993/


Интересные статьи

Интересные статьи

Или о том, как я обманываю читателей
Это третья часть из серии публикаций (первая и вторая), посвящённых разработке умных пайеток - электромеханических цветовоспроизводящих устройств для умной одежды. Сегодня расскажу о новой конструкции...
В предыдущих двух частях мы научились анализировать нашу камеру и подбирать оптимальную экспозицию исходя из математических принципов.Теперь мы бы хотели эти два умения объединить и получить некую дор...
Хакатонами и конференциями в сфере разработки никого не удивить. Тем не менее, новые технологии разрабатывают и представители других отраслей, например, строительства. Это направление сей...
Продолжаем серию «C++, копаем в глубь». Цель этой серии — рассказать максимально подробно о разных особенностях языка, возможно довольно специальных. Эта статья посвящена перегрузке операторов....