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)


  1. vershov
    27.12.2021 00:23
    +5

    Самая распространенная ошибка при использовании Makefile - это не пользоваться cmake для генерации Makefile.


    1. DistortNeo
      27.12.2021 00:29
      +1

      Да. Makefile — слишком низкоуровенвый инструмент, чтобы лазить в него руками. Тем не менее, базовые сценарии его использования все равно надо понимать.


      1. Bonio
        27.12.2021 12:35

        Makefile используют не только для сборки сишных программ. Для сборки всяких докер образов используется, просто для часто используемых наборов комманд для проекта.


        1. unsignedchar
          27.12.2021 13:13

          Для сборки всяких докер образов используется

          Это фактически init скрипт, переписанный с bash на make. Ничего из make power там нет.


          1. Bonio
            27.12.2021 13:26

            Фактически да. Но такий сценарий их использования тоже есть и используется довольно часто. Это удобнее, чем bash скрипты.


            1. unsignedchar
              27.12.2021 14:31
              +1

              Это удобнее, чем bash скрипты.

              Bash: хуже чем bash быть не может

              Make: поддержите моё пиво..

              help: ## This help. @awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST)


              1. Bonio
                27.12.2021 14:49

                Подкол засчитан, сам так делаю )
                Нет, серьезно, Makefile удобнее и лаконичнее для списка часто используемых наборов комманд.


    1. technic93
      27.12.2021 02:21
      +2

      Самая распространенная ошибка при использовании cmake это генерировать Makefile вместо ninja.


    1. IGR2014
      27.12.2021 20:22

      CMake + Ninja = мощь


  1. 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» в данном случае переводится, как «приводит к». И эта ошибка по всему переводу проскакивает


  1. ilammy
    27.12.2021 04:37
    +7

    Чёт как-то мало практики. Выше уже сказали, что для программ написание мейкфайлов вручную уже не очень модно. Для проектов на Си потому, что большинство проектов на Си — это большие проекты на Си, что несёт свои сложности. Для других языков — потому что там есть система сборки, а мейкфайлы нужны максимум как удобный диспетчер для скриптов.


    Если брать именно Си, то где другие интересные вопросы?


    • как отключить бесполезные встроенные правила make
    • когда пользоваться @, -, +
    • отслеживание зависимостей через -MP и -MD
    • «стандартные» цели: all, check, clean, etc.
    • что делать при установке библиотек кроме install
    • как не забывать реализовать uninstall
    • заклинания для самодокументируемого make help
    • делать или не делать рекурсивные вызовы make

    Часть 1

    График постинга горит, чтоль? Или время переводчика в преддверии нового года дорого? Исходная статья не такая уж и большая. Судя по кускам непереведённого текста, всё же график.


  1. unsignedchar
    27.12.2021 08:45
    +3

    Какой то странный перевод.

    Пользователь определил CC=clang в среде, но gcc будет использоваться в любом случае, даже если он не был установлен.

    это на каком языке? Как пользоваться неустановленным gcc?


  1. jogick
    27.12.2021 13:53

    Какое громкое название для такой статьи. Это даже переводом толком не назовёшь, а уж где тут "практики" не понял, не говоря уже о лучших.