Skip to content
Michael Builov edited this page Jul 13, 2017 · 14 revisions

clean-build

Простая, быстрая, нерекурсивная, платформонезависимая, шаблонная, расширяемая система сборки, полагающая только на возможности Gnu Make.

В отличие от генераторов систем сборок, таких как CMake, Meson, clean-build представляет собой набор платформо-независимых макросов, при сборке раскрывающихся в непосредственный вызов утилит командной строки - компиляторов, линкеров и др., с возможностью сохранения генерируемых команд в shell-скирпт или batch-файл для сборки уже без участия clean-build, либо для отладки сборки.

clean-build не является заменой системам конфигурирования проектов, таким как Autotools или pkg-config.

Конфигурирование проекта, как то установка и настройка компиляторов, devel-версий внешних библиотек и т.д. должно быть выполнено до запуска сборки.

Задача clean-build - максимально быстро выполнить сборку проекта, аналогично минималистичной системе сборки Ninja.

Однако, в отличие от Ninja, имеющей очень ограниченный язык сценариев сборки и обычно используемой совместно с генератором сценариев сборки, clean-build самодостаточен - возможностей языка Gnu Make достаточно для написания довольно сложных сценариев сборки.

Использование шаблонов позволяет создавать сценарии clean-build краткими, понятными, не теряя при этом возможность изменять любой параметр вызываемой утилиты.

clean-build хорошо подходит для осуществления периодических сборок при кросс-платформеной разработке проекта.

Содержание

Принципы построения

  • Декларативное описание сценариев сборки

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

    Также есть возможность создания произвольных шаблонов и правил сборки, используя функционал языка Gnu Make и примеры шаблонов clean-build.

  • Нерекурсивность выполнения сборки

    Структура большого проекта обычно состоит из нескольких модулей, у каждого из которых есть своя под-директория проекта и свой сценарий сборки, расположенный в этой под-директории.

    В рекурсивных системах сборки, порядок сборки модулей определяется порядком рекурсивного обхода их под-директориий.

    В clean-build также осуществляется рекурсивный обход под-директориий проекта - для построения единого графа зависимостей целей между собой.

    Затем сборка осуществляется в порядке, определяемом только зависимостями целей между собой.

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

  • Использование только нативных утилит сборочной платформы

    Система сборки минималистична, помимо стандартных утилит сборочной платформы, необходим только Gnu Make и, опционально, sed (например Gnu Sed).

    Gnu Make и sed доступны в нативном виде для множества платформ, включая изначально поддерживаемые clean-build ОС Windows, Linux и Solaris.

Основные характеристики

  • нерекурсивность выполнения сборки
  • массивное распараллеливание построения целей, практически не ограниченное масштабирование по количеству процессоров сборочной платформы
  • модульный, расширяемый дизайн
  • платформо-независимые сценарии сборки - через одинаковый для всех ОС набор шаблонов (при их платформо-зависимой реализации)
  • многоплатформенность, использование только нативных утилит сборочной платформы (для сборки на WINDOWS не нужен слой эмуляции UNIX)
  • возможность указания зависимости выполнения сценариев сборки друг от друга - для задания порядка сборки подсистем проекта
  • поддержка кросс-компиляции (в рамках одного типа ОС, но различной архитектуры центрального процессора сборочной и целевой машин)
  • встроенная поддержка сборки целей из исходных текстов на языках: C, C++, Java, Scala, Wix
  • поддержка предкомпилированных заголовочных файлов для языков C/C++
  • поддержка автоматической генерации в процессе сборки описаний зависимостей исходных текстов от заголовочных файлов (для C/C++)
  • поддержка групповой компиляции нескольких файлов за один вызов компилятора (для cl.exe на WINDOWS для C/C++)
  • встроенные средства для отладки, проверки и трассировки сценариев сборки
  • предопределённые шаблоны для сборки динамических и статических библиотек, исполняемых файлов, модулей ядра и архивов java
  • поддержка генерации нескольких целей за один вызов заданной команды
  • простое создание batch-файла или shell-скрипта для сборки без использования Gnu Make: make V=1 > build.bat
  • предсказуемость сборок, независимость от переменных окружения
  • возможность переопределения любого макроса clean-build без редактирования сценариев сборки
  • поддержка генерации конфигурационного файла с параметрами сборки - для повторяемых сборок
  • красивый вывод информации о процессе сборки
  • скорость: время проверки необходимости пересборки изменений проекта из 3000 файлов при отсутствии изменений - менее 1 секунды

Установка

Достаточно скопировать clean-build в локальную директорию, например в C:\tools (для Windows) или ${HOME} (для Unix-like ОС).

Запуск сборки

Сборка осуществляется вызовом интерпретатора Gnu Make, обычно в директории проекта, где находится корневой Makefile.

Система сборки сама сообщит о неопределённых параметрах сборки, полный список которых определяется проектом, например:

  • путь к clean-build (обычно через определение переменной MTOP)
  • TARGET - тип сборки (например DEBUG или RELEASE)
  • OS - тип операционной системы сборочной платфорvы (например WINXX, LINUX или SOLARIS)
  • CPU - архитектура процессора исполняемых файлов (например x86 или x86_64).

Параметры сборки, за некоторым исключением (возможно, переменной MTOP), должны быть заданы либо в командной строке, либо в конфигурационном Makefile.

Замечание. Переменные, заданные в строке запуска make, имеют наивысший приоритет и переопределяют переменные, оределённые в Makefile'ах.

Определение параметров сборки через переменные окружения, обычно, не используется (из-за сложности контроля за переменными окружения - все переменные окружения по умолчанию становятся переменными Makefile).

Перед началом сборки, все переменные окружения устанавливаются в предзаданные значения или очищаются, чтобы сборка не зависела от значений переменных окружения.

Пример проекта

Рассмотрим пример проекта, идущего в составе clean-build, расположенного в директории examples/hello.

Проект состоит из следующих файлов:

  • Makefile
  • project.mk
  • hello.mk
  • hello.c
  • readme.txt

Описание файлов проекта

  1. Makefile

Корневой Makefile проекта, обычно служит для подключения суб-модулей проекта.

Разберём его содержимое построчно.

  1. Первой строкой подключается project.mk - конфигурационный Makefile проекта.

    include $(dir $(lastword $(MAKEFILE_LIST)))project.mk

    здесь: $(MAKEFILE_LIST) - список всех подключённых Makefile'ов до выполнения этой строки.

    функция lastword возвращает из этого списка последнее слово - имя текущего Makefile

    функция dir - возвращает директорию текущего Makefile, в которой находится project.mk

    Замечание:

    • project.mk` должен быть подключён всегда первым, вначале любого Makefile проекта
    • подключение project.mk включает необходимые определения для выполнения данного Makefile индивидуально, напрямую (через ключ -f), а не через запуск сборки от корня проекта
  2. строка

    include $(MTOP)/parallel.mk

    добавляет определения макросов clean-build, используемых для подключения в сборку суб-модулей проекта.

    Здесь $(MTOP) - путь к clean-build, определённый в project.mk.

  3. следующей строкой проверяется, что цель сборки - не distclean.

    ifneq (distclean,$(MAKECMDGOALS))

    Цель distclean предопределена в clean-build, служит для удаления всех результатов сборки.

    При выполнении этой цели просто удаляется директория $(BUILD) - директория результатов сборки, путь к которой задаётся в project.mk.

    Обработка суб-модулей проекта при выполении distclean не требуется.

  4. строка

    TO_MAKE := hello.mk

    определяет произвольную переменную TO_MAKE (имя выбрано произвольно), в которую записывается имя Makefile'а суб-модуля.

  5. если целевая платформа - Windows, то в сборку добавляется предопределённый в clean-build суб-модуль version.

    ifeq (WINXX,$(OS))
    TO_MAKE += $(MTOP)/exts/version
    endif

    Подключение этого модуля нужно для генерации стандартного ресурсного файла с информацией о дате/версии/производителе и др. данных, включаемых в создаваемые исполняемые файлы и динамические библиотеки.

  6. предпоследняя строка

    $(call PROCESS_SUBMAKES,$(TO_MAKE))
    • вызов макроса clean-build PROCESS_SUBMAKES, служащего для обработки суб-модулей проекта, в данном случае перечисленных в переменной TO_MAKE.
  7. финальная строка

    endif

    завершает условное выражение ifneq (distclean,$(MAKECMDGOALS)).

  1. project.mk

Конфигурационный Makefile проекта, определяет переменные проекта, как необходимые для clean-build, так и специфичные для данного проекта.

Разберём его содержимое.

  1. Проверяем, определена ли переменная TOP - корневая директория проекта.

    ifneq (override,$(origin TOP))

    Эта переменная заведена для удобства, например для указания путей к исходным файлам проекта.

    Переменная TOP определяется автоматически, в project.mk, поэтому должна иметь атрибут override - переопределённая переменная в файле.

    Если переменная TOP уже определена, значит project.mk уже был обработан и дальнейшие определения пропускаются.

  2. Иначе, определяем переменную TOP.

    override TOP := $(abspath $(dir $(lastword $(MAKEFILE_LIST))))

    Зная, что project.mk находится в корневой директории проекта, просто присваиваем TOP значение абсолютного пути к директории текущего Makefile.

    Замечание: TOP определяется с аттрибутом override, чтобы указать, что это определение имеет высший приоритет над (случайным) определением TOP в командной строке.

  3. Опционально, задаём версию clean-build, требуемую для сборки данного проекта.

    CLEAN_BUILD_REQUIRED_VERSION := 0.6.3

    Эта переменная, если определена, проверяется clean-build.

    Если проект требует неподдерживаемую версию clean-build, то сборка завершится с ошибкой.

  4. Единственная обязательная переменная, требуемая clean-build - это путь к директории для создаваемых в процессе сборки файлов.

    BUILD := $(TOP)/build

    Замечание: BUILD можно переопределить, если задать значение этой переменной в командной строке (или в специальном Makefile переопределений, речь о котором идёт ниже).

  5. Задаём переменные, необходимые для генерации стандартного ресурсного файла, подключаемого в сборку исполняемого файла данного примера.

    PRODUCT_VER      := 1.0.0
    PRODUCT_NAMES_H  := product_names.h
    VENDOR_NAME      := Michael M. Builov
    PRODUCT_NAME     := Sample app
    VENDOR_COPYRIGHT := Copyright (C) 2015-2017 Michael M. Builov, https://github.com/mbuilov/clean-build

    PRODUCT_VER: версия продукта, clean-build определяет её как 0.0.1. Однако переменные, определённые в Makefile'ах до включения определений clean-build переопределяют значения clean-build. Так как project.mk включается первым в любом Makefile проекта, то итоговым значением PRODUCT_VER будет 1.0.0. PRODUCT_NAMES_H,

  6. Определяем переменную, используемую в Makefile'ах для ссылки на clean-build.

    MTOP := $(abspath $(TOP)/../..)

    Замечание: поскольку директория данного тестового проекта расположена внутри clean-build, переменную MTOP можно определить автоматически, относительно $(TOP).

Структура проекта

Для работы clean-build должна быть определена переменная TOP - абсолютный путь к корневой директории проекта.

Переменная TOP может быть определена автоматически, как показано выше на примере файла top.mk, расположенного в корневой директории проекта.

Также, в корневой директории проекта обычно располагаются:

  • корневой Makefile, содержащий список модулей проекта
  • опциональная конфигурационная директория make.

Предполагается, что конфигурационная директория make содержит файл project.mk, включаемый в систему сборки непосредственно перед определением стандартных макросов clean-build.

Система сборки clean-build позволяет переопределить в project.mk практически все стандартные макросы и шаблоны сборки.

Ещё директория make может содержать настройки для подключения внешних модулей - через поддиректории use.

Пример структуры проекта (полный пример см. в clean-build/examples/hello):

/opt/hello/
/opt/hello/hello.mk
/opt/hello/Makefile
/opt/hello/make
/opt/hello/make/project.mk
/opt/hello/make/SOLARIS/use/socket.mk
/opt/hello/make/LINUX/use/pthread.mk
/opt/hello/make/WINXX/use/iphlpapi.mk
/opt/hello/hello.c

Замечания:

  • путь к project.mk можно задать в переменной PROJECT (по умолчанию PROJECT=$(TOP)/make/project.mk)
  • путь к поддиректории use задаются через переменную PROJECT_USE_DIR (по умолчанию PROJECT_USE_DIR=$(TOP)/make/$(OS)/use)

Примеры правил сборки

Сборка статической библиотеки

include $(MTOP)/c.mk
LIB     := my_lib
SRC     := my_source1.c my_source2.c
INCLUDE := my_include1 my_include2
$(DEFINE_TARGETS)

Первой строкой указывается включение файла clean-build $(MTOP)/c.mk - добавление шаблонов сборки целей на языках C, C++.

Далее указываются параметры шаблона:

  • LIB - выбирает шаблон сборки статической библиотеки и задаёт её имя (расширение файла будет добавлено автоматически)
  • SRC - список файлов исходных текстов библиотеки
  • INCLUDE - список дополнительных директорий для поиска #include-файлов
  • $(DEFINE_TARGETS) - применение выбранных шаблонов сборки

Создание сборочной утилиты

Сборочная улитита может быть запущена во время сборки, например для генерации исходных текстов.

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

TOOL_MODE := 1
include $(MTOP)/c.mk
EXE := my_tool
SRC := my_tool_source.c
$(DEFINE_TARGETS)

Выставление переменной TOOL_MODE в непустое значение (которое будет очищено в $(DEFINE_TARGETS)) настраивает шаблоны сборки на создание утилит сборки.

Тем самым, следующее включение $(MTOP)/c.mk переопределит архитектуру и пути системы сборки для создаваемых файлов на архитектуру и пути для утилит сборки.

Параметры шаблона:

  • EXE - выбирает шаблон сборки исполняемого файла и задаёт его имя (расширение файла будет добавлено автоматически)
  • SRC - список файлов исходных текстов утилиты
  • $(DEFINE_TARGETS) - применение выбранных шаблонов сборки

Генерация произвольных файлов

Для генерации произвольных файлов нужно определить правила их генерации и добавить генерируемые файлы в сборку - через вызов макроса clean-build ADD_GENERATED.

include $(MTOP)/defs.mk
MY_GENERATED := $(GEN_DIR)/my_dir/my_file.txt
$(call ADD_GENERATED,$(MY_GENERATED))
$(MY_GENERATED): MY_OPTIONS := my_options
$(MY_GENERATED): $(call GET_TOOL,my_tool) $(VPREFIX)my_source.txt
    $(call ospath,$<) $(MY_OPTIONS) -o $(call ospath,$(word 2,$^))
$(DEFINE_TARGETS)

Разберём пример построчно.

  1. Включение

    $(MTOP)/defs.mk

    добавляет описание стандартных функций и определений clean-build.

    Вообще, включение любого шаблона clean-build (например упоминаемый ранее $(MTOP)/c.mk) также включает и $(MTOP)/defs.mk.

  2. Строка

    MY_GENERATED := $(GEN_DIR)/my_dir/my_file.txt

    задаёт имя генерируемого файла.

    Имя переменной MY_GENERATED выбрано произвольно, однако возможны коллизии с предопределёнными именами clean-build.

    Найти коллизии в именах переменных можно запустив сборку в режиме check makefiles - определением переменной C в отличное от нуля значение: make C=1.

    GEN_DIR - путь к директории для генерируемых в процессе сборки файлов.

    Система сбоки позволяет создавать файлы только в директориях $(GEN_DIR) (исходные тексты), $(LIB_DIR) (библиотеки), $(BIN_DIR) (исполняемые файлы) или $(OBJ_DIR) (объектные файлы).

  3. Вызов

    $(call ADD_GENERATED,$(MY_GENERATED))

    добавляет генерируемые файлы (т.е. $(GEN_DIR)/my_dir/my_file.txt) в сборку:

    • директория $(GEN_DIR)/my_dir будет создана автоматически перед вызовом правила генерации файлов
    • при выполнении make clean сгенерённые файлы будут удалены (за исключением директорий)
  4. строка

    $(MY_GENERATED): MY_OPTIONS := my_options

    определяет target-specific (связанную с целью) переменную MY_OPTIONS со значением my_options.

    В системе сборки clean-build важно использовать по-возможности тоько target-specific переменные - из-за нерекурсивности системы сборки.

    После того, как очередной Makefile разобран и определены правила сборки целей, может быть включён и разобран следующий Makefile, который может переопределить переменные из первого Makefile.

    Во время выполнения правил первого Makefile глобальные переменные могут иметь уже другие значения, нежели во время определения правил.

    Поэтому переменные нужно привязывать к цели сборки - имена целей сборки всегда уникальны, что контролируется самим Gnu Make.

  5. следующие две строки определяют правило сборки файла $(MY_GENERATED)

    $(MY_GENERATED): $(call GET_TOOL,my_tool) $(VPREFIX)my_source.txt
        $(call ospath,$<) $(MY_OPTIONS) -o $(call ospath,$(word 2,$^))
    • Первая строка задаёт зависимости $(MY_GENERATED) от утилиты сборки my_tool и файла my_source.txt.

      Раскрытие параметризованного макроса GET_TOOL через $(call GET_TOOL,my_tool) даёт путь к утилите сборки my_tool (пример создания my_tool приведён выше).

      Указание $(VPREFIX)my_source.txt задаёт зависимость генерируемого файла от my_source.txt, расположенного в той же директории, что и данный Makefile.

      В переменной $(VPREFIX) содержится относительный путь от директории, в которой запущен make к директории, в которой расположен целевой Makefile.

      Замечание:

      • В приведённых выше примерах сборки статической библиотеки и утилиты сборки переменная $(VPREFIX) не указывалась перед именами файлов исходных текстов.

      $(VPREFIX) добавляется автоматически шаблонами сборки к путям файлов исходных текстов, если пути не абсолютные (т.е. не указаны относительно корня проекта $(TOP)).

      Здесь же задаётся произвольное правило сборки - все зависимости нужно указывать явно.

    • Вторая строка (начинающаяся с символа табуляции) определяет метод генерации файла.

      Здесь:

          $(call ospath,$<) $(MY_OPTIONS) -o $(call ospath,$(word 2,$^))

      $< - автоматическая переменная - имя первой зависимости цели, в данном случае это путь, возвращаемый $(call GET_TOOL,my_tool).

      Вызовом $(call ospath,$<) путь к утилите my_tool преобразуется в путь к фалу, характерный для операционной системы сборочной машины (например преобразуется C:/a/b -> C:\a\b).

      $(MY_OPTIONS) - target-specific переменная - раскрывается в my_options.

      $^ - автоматическая переменная - список всех зависимостей цели в порядке их перечисления.

      $(word 2,$^) - имя второй зависимости цели, в данном случае это $(VPREFIX)my_source.txt.

      Замечание:

      • VPREFIX не была объявлена как target-specific переменная, поэтому нельзя использовать выражение $(VPREFIX)my_source.txt в теле правила, т.к. VPREFIX может быть изменена в следующих Makefile'ах, включенных после задания данного правила построения цели.

      $(call ospath,$(word 2,$^)) - преобразование пути к my_source.txt в путь, характерный для операционной системы сборочной машины.

  6. $(DEFINE_TARGETS) - применение выбранных шаблонов сборки.

    В данном случае, т.к. никаких предопределённых шаблонов сборки указано не было, то $(DEFINE_TARGETS) не делает ничего, кроме включения завершающего файла clean-build $(MTOP)/all.mk, если текущий Makefile был обработан последним.

    Завершающий $(MTOP)/all.mk содержит определение цели по-умолчанию all, для выполнения которой нужно выполнить все заданные цели.

    Замечание:

    • Вместе с определением правила генерации произвольного файла можно было выбрать и шаблон сборки, скажем библиотеки (включением $(MTOP)/c.mk вместо $(MTOP)/defs.mk и заданием переменной LIB), тогда вызов $(DEFINE_TARGETS) также приведёт к сборке библиотеки.

Генерация нескольких файлов одной командой

Правило генерации нескольких файлов одной командой в Gnu Make можно задать с помошью определения шаблонного правила (pattern-rule).

Имена генерируемыех файлов, при этом, должны содержать некоторую общую часть - шаблон - например общий путь к родительской директории.

Рассмотрим пример генерации файлов $(GEN_DIR)/t1, $(GEN_DIR)/t2 и $(GEN_DIR)/t3, зависящих от файла prereq.txt с помощью команды $(my_gen_cmd):

include $(MTOP)/defs.mk

define my_gen_cmd
$(if $(VERBOSE),,@)$(call ECHO,1) > $(call ospath,$(word 1,$1))
$(if $(VERBOSE),,@)$(call ECHO,2) > $(call ospath,$(word 2,$1))
$(if $(VERBOSE),,@)$(call ECHO,3) > $(call ospath,$(word 3,$1))
endef

MY_GENERATED := $(GEN_DIR)/t1 $(GEN_DIR)/t2 $(GEN_DIR)/t3
$(call ADD_GENERATED,$(MY_GENERATED))

$(MY_GENERATED): GENERATED := $(MY_GENERATED)
%/t1 %/t2 %/t3: $(VPREFIX)prereq.txt
    $(call SUP,MGEN,$(GENERATED))$(call my_gen_cmd,$(GENERATED))

$(DEFINE_TARGETS)

В общем случае, при таком подходе есть несколько неудобных моментов:

  • во-первых, нужно выделять шаблон из имён файлов (правда, если использовать общий путь в качестве шаблона, то это - не проблема)
  • во-вторых, имена создаваемых файлов, для их использования в теле правила, нужно запоминать в target-specific-переменной, т.к. автоматическая переменная $@ будет содержать имя только первого создаваемого файла (в примере - $(GEN_DIR)/t1)
  • и в-третьих, создаётся шаблонное правило, которое будет проверяться для каждого файла, для которого явно не определено правило создания, что негативно повлияет на скорость сборки.

Для решения этих проблем и упрощения генерации нескольких файлов одной командой, в clean-build определён макрос MULTI_TARGET.

include $(MTOP)/defs.mk

define my_gen_cmd
$(if $(VERBOSE),,@)$(call ECHO,1) > $(call ospath,$(word 1,$1))
$(if $(VERBOSE),,@)$(call ECHO,2) > $(call ospath,$(word 2,$1))
$(if $(VERBOSE),,@)$(call ECHO,3) > $(call ospath,$(word 3,$1))
endef

MY_GENERATED := $(GEN_DIR)/t1 $(GEN_DIR)/t2 $(GEN_DIR)/t3

$(call MULTI_TARGET,$(MY_GENERATED),prereq.txt,$$(call my_gen_cmd,$(MY_GENERATED)))

$(DEFINE_TARGETS)

Макрос MULTI_TARGET, определённый в подключаемом $(MTOP)/defs.mk, принимает 3 аргумента:

  1. список генерируемых файлов - $(MY_GENERATED)
  2. список зависимостей генерируемых файлов - prereq.txt
  3. правило генерации файлов - $$(call my_gen_cmd,$(MY_GENERATED))

Перед вызовом макроса MULTI_TARGET, вычисляется каждый из передаваемых ему аргументов.

Так как правило генерации файлов, передаваемое 3-им аргументом, будет раскрыто дважды - первый раз перед вызовом MULTI_TARGET, второй - во время выполнения правила, то вызов $$(call my_gen_cmd,$(MY_GENERATED)) экранирован двойным $$.

Однако список файлов, передаваемый в my_gen_cmd раскрывается сразу - в момент вызова MULTI_TARGET

Значение переменной VPREFIX необходимо использовать сразу, до включения следующего Makefile, в котором VPREFIX может быть изменена. Или можно сохранить $(VPREFIX) в target-specific переменной, чтобы использовать это значение во время выполнения правила.

Переменная MY_OPTIONS не объявлена как target-specific, в отличие от предыдущего примера, т.к. используется сразу, в момент определения правила сборки, а не в момент выполнения этого правила.

  1. Объединение Makefile'ов в группу

    Пример группового Makefile:

    MDEPS := my_gen.mk
    TO_MAKE := tool1.mk lib1/my_lib.mk $(TOP)/dir1/dir2/my_exe_dir
    include $(MTOP)/parallel.mk

    Группа Makefile'ов указывается в переменной TO_MAKE, значение которой интерпретируется в подключаемом $(MTOP)/parallel.mk.

    Makefile'ы указываются как имена файлов с расширением .mk или директорий, путь к которым может быть либо относительным от текущего Makefile, либо абсолютным - от корня проекта в переменной $(TOP). Если элемент списка $(TO_MAKE) не заканчивается на .mk, то предполагается, что это директория, в которой ищется файл с именем Makefile.

    В переменной MDEPS можно указать список Makefile'ов, от которых зависят цели текущего Makefile (или группы Makefile'ов, как в данном примере). Эти зависимости будут наследоваться всеми Makefile'ами, подключаемыми групповым Makefile.

    Правила перечисления Makefile'ов в переменной MDEPS те же, что и для переменной TO_MAKE.

    Цели, указанные в Makefile'ах tool1.mk, lib1/my_lib.mk и $(TOP)/dir1/dir2/my_exe_dir/Makefile будут выполнены только после того, как будут выполнены цели Makefile'ов из $(MDEPS).

    Если Makefile из $(MDEPS) не был подключён к сборке в процессе определения целей, то он игнорируется. Таким образом, возможно выполнение (пере-)сборки модуля в его поддиректории, без необходимости пересобирать внешние зависимости, как в следующем примере:

    top_dir/Makefile:

    TO_MAKE := gen module
    include $(MTOP)/parallel.mk

    top_dir/module/Makefile:

    MDEPS := ../gen
    ....
    $(DEFINE_TARGETS)

    Вызов make clean && make в директории top_dir/module пересобирёт только цели из этой директории, не трогая цели в top_dir/gen.

  2. Собираем что-то уникальное, например выполняем тесты

    Пример:

    include $(MTOP)/defs.mk
    MDEPS := build_tests.mk
    TESTS := rb_test c_base64_test
    $(call ADD_GENERATED,$(addprefix $(GEN_DIR)/tests/,$(addsuffix .tsmt,$(TESTS))))
    define TEST_RULE
    $(newline)
    $(GEN_DIR)/tests/$t.tsmt: $(BIN_DIR)/$t$(EXE_SUFFIX)
    	$$(call SUP,TEST,$$<)$$< > $(GEN_DIR)/tests/$t.out
    	$(if $(VERBOSE),,@)$$(call TOUCH,$$@)
    endef
    $(eval $(foreach t,$(TESTS),$(TEST_RULE)))
    $(DEFINE_TARGETS)

    Здесь:

    1. MDEPS := build_tests.mk Задаёт что цели, указанные в build_tests.mk (например сборка тестируемых исполняемых файлов) должны быть выполнены до целей текущего Makefile.

    2. TESTS := rb_test c_base64_test Список тестируемых исполняемых файлов.

    3. $(call ADD_GENERATED,$(addprefix $(GEN_DIR)/tests/,$(addsuffix .tsmt,$(TESTS)))) В результате запуска тестов будут сгенерены файлы rb_test.tsmt и c_base64_test.tsmt в директории $(GEN_DIR)/tests.

    4. define TEST_RULE Начало определения шаблона генерации правила запуска теста.

    5. $(newline) Тело определяемого шаблона TEST_RULE начинается со строки, следующей за define TEST_RULE. Пусть первым символом шаблона будет перевод строки (переменная newline определена в $(MTOP)/defs.mk). Это позволит начинать определение очередного правила с новой строки - при раскрытии шаблона в цикле foreach.

    6. шаблонное правило c параметром шаблона $t

      $(GEN_DIR)/tests/$t.tsmt: $(BIN_DIR)/$t$(EXE_SUFFIX)
      	$$(call SUP,TEST,$$<)$$< > $(GEN_DIR)/tests/$t.out
      	$(if $(VERBOSE),,@)$$(call TOUCH,$$@)

      определяет, что результат теста $t.tsmt в директории $(GEN_DIR)/tests будет зависеть от даты создания тестируемого исполняемого файла $t$(EXE_SUFFIX) в директории $(BIN_DIR).

      Для выполнения теста сначала будет вызван исполняемый файл $(BIN_DIR)/$t$(EXE_SUFFIX) (указываемый через автоматическую перемсенную $$<), а stdout этого вызова будет перенаправлен с помощью > в файл $(GEN_DIR)/tests/$t.out. Вызов $$(call SUP,TEST,$$<) служит для информационной печати на экране выполняемой операции или команды (если make вызван с параметром V=1). Следующей строка -- вызов$$(call TOUCH,$$@) -- вторая команда правила генерации файла $(GEN_DIR)/tests/$t.tsmt (указываемого через автоматическую переменную $$@). Макрос TOUCH определён в $(MTOP)/defs.mk через подключение файла определений, специфичного для текущей сборочной платформы. Конструкция $(if $(VERBOSE),,@) -- даёт пустое значение в verbose режиме (если make вызван с параметром V=1) и @ (директива make -- скрыть выполняемую команду) в обычном режиме.

      Соответственно, в обычном режиме, при выполнении правила шаблона make напечатает что-то вроде:

      TEST   /home/user/procject/bin/LINUX-x86_64-x86_64-DEBUG/rb_test
      

      А в verbose-режиме:

      /home/user/proj/bin/LINUX-x86_64-x86_64-DEBUG/rb_test > /home/user/proj/gen/LINUX-x86_64-x86_64-DEBUG/rb_test.out
      touch /home/user/proj/gen/LINUX-x86_64-x86_64-DEBUG/rb_test.tsmt
      

      Замечания:

      • команды правила должны начинаться с символа табуляции
      • тест правил будет интерпретирован дважды, один раз при раскрытии шаблона, второй -- в момент выполнения правил. Поэтому раскрытие некоторых макросов в правилах отложено до второго раза - путём экранирования символа $ двойным $$.
    7. endef Конец шаблона TEST_RULE.

      Замечание: тело шаблона не включает перевод строки перед endef.

    8. $(eval $(foreach t,$(TESTS),$(TEST_RULE))) Для каждого $t из списка $(TESTS) раскрывается шаблон TEST_RULE, затем полученный список правил интерпретируется функцией eval как если бы эти правила были заданы в тексте Makefile.

      Небольшое замечание: функция foreach разделяет результаты раскрытия шаблона TEST_RULE пробелами, поэтому в конце последней строки раскрытого шаблона (после $$(call TOUCH,$$@)) будет добавлен пробел (добавление которого не играет никакой роли). А из-за того, что первым символом шаблона является $(newline), функции eval для интерпретации на вход поступят правила, начинающиеся с новой строки.

    9. $(DEFINE_TARGETS) -- раскрывает макрос исполнения заданных шаблонов сборки. В данном случае, т.к. никаких предопределённых шаблонов сборки указано не было, то $(DEFINE_TARGETS) не делает ничего, кроме включения завершающего $(MTOP)/all.mk, если текущий Makefile был обработан последним.

е и разбор всех Makefile'ов проекта, затем построение единого дерева зависимостей целей, затем (параллельное) выполнение целей. Единое дерево зависимостей позволяет максимально распараллелить выполнение целей проекта, утилизируя все доступные ресурсы сборочной машины.

  • Использование только нативных утилит сборочной платформы

    Система сборки минималистична, полагается только на возможности Gnu Make и, опционально, sed (например Gnu Sed). Данные утилиты доступны в нативном виде для всех поддерживаемых платформ: Windows, Linux, Solaris.

Основные характеристики

  • нерекурсивное выполнение сборки
  • массивное распараллеливание выполнения целей, практически не ограниченное масштабирование по количеству процессоров
  • модульный, расширяемый дизайн
  • платформо-независимые целевые сценарии сборки
  • многоплатформенность, использование только нативных утилит сборочной платформы (для сборки на WINDOWS не нужен слой эмуляции UNIX)
  • поддержка указания зависимости выполнения сценариев сборки друг от друга
  • поддержка кросс-компиляции
  • встроенная поддержка сборки целей из исходных текстов на языках: C, C++, Java, Scala, Wix
  • поддержка предкомпилированных заголовочных файлов для языков C/C++
  • поддержка генерации в процессе сборки описаний зависимостей исходных текстов от заголовочных файлов (для C/C++)
  • поддержка групповой компиляции нескольких файлов за один вызов компилятора (на WINDOWS для C/C++)
  • встроенные средства для отладки, проверки и трассировки сценариев сборки
  • предопределённые шаблоны для сборки динамических и статических библиотек, исполняемых фалов, модулей ядра и архивов java
  • поддержка произвольных правил генерации нескольких целей за один вызов заданной команды
  • возможность генерации простого batch-файла или shell-скрипта для сборки с нуля без использования Gnu Make: make V=1 > build.bat
  • красивый вывод информации о процессе сборки

Установка

Достаточно скопировать clean-build в директорию без пробелов в пути, например в C:\tools (для Windows) или /usr/home/user (для Unix-like ОС).

Запуск сборки

Сборка осуществляется обычным вызовом интерпретатора Gnu Make, примеры:

  • make distclean

  • make -j4

  • make -f dir/Makefile check или make -C dir check

  • вызов из bash (например под cygwin) с заданием параметров сборки:

    $ make MTOP=/cygdrive/c/User/clean-build OS=LINUX CPU=x86_64 COMPILER_TYPE=GCC6 NO_STACK_PROTECTOR=1 NO_SANITIZE=1 NO_COVERAGE=1 DLL_PREFIX= DLL_SUFFIX=.dll MEMSTACK_INCLUDE=/cygdrive/c/User/memstack MEMSTACK_LIBDIR='/cygdrive/c/User/memstack/lib/$(TARGET_TRIPLET)' CMN_HEADERS_INCLUDE=/cygdrive/c/User/cmn_headers GTPARSER_INCLUDE=/cygdrive/c/User/gtparser GTPARSER_LIBDIR='/cygdrive/c/User/gtparser/lib/$(TARGET_TRIPLET)'
  • вызов из командного итерпретатора cmd.exe с заданием параметров сборки:

    C:\User\bitbridge>C:\tools\gnumake-4.2.1-x64.exe MTOP=c:\User\clean-build SED=c:\tools\sed-4.4-x64.exe OS=WINXX CPU=x86 OSVARIANT=WIN7 VS="d:\Program Files (x86)\Microsoft Visual Studio\2017\Community\VC\Tools\MSVC\14.10.25017" WDK="c:\Program Files (x86)\Windows Kits\10" WDK_TARGET="10.0.14393.0" MEMSTACK_INCLUDE=C:\User\memstack MEMSTACK_LIBDIR=C:\User\memstack\lib\$(TARGET_TRIPLET) MEMSTACK_DLLDIR=C:\User\memstack\bin\$(TARGET_TRIPLET) CMN_HEADERS_INCLUDE=C:\User\cmn_headers GTPARSER_INCLUDE=C:\User\gtparser GTPARSER_LIBDIR=C:\User\gtparser\lib\$(TARGET_TRIPLET)

Примеры правил сборки

Сборка статической библиотеки

include $(MTOP)/c.mk
LIB     := my_lib
SRC     := my_source1.c my_source2.c
INCLUDE := my_include1 my_include2
$(DEFINE_TARGETS)

Первой строкой указывается включение файла $(MTOP)/c.mk -- добавление шаблонов сборки целей на языках C, C++.

Далее указываются параметры шаблона:

  • LIB -- выбирает шаблон сборки статической библиотеки и задаёт её имя (расширение будет добавлено автоматически)
  • SRC -- список файлов исходных текстов библиотеки
  • INCLUDE -- список дополнительных директорий для поиска #include-файлов
  • $(DEFINE_TARGETS) -- раскрывает макрос исполнения выбранных шаблонов сборки

Создание сборочной утилиты

Сборочная улитита может быть запущена во время сборки, например для генерации исходных текстов.

В случае кросс-компиляции, архитектура сборочной улититы соответствует архитектуре сборочной машины и может отличаться от архитектуры собираемых исполняемых файлов.

TOOL_MODE := 1
include $(MTOP)/c.mk
EXE := my_tool
SRC := my_tool_source.c
$(DEFINE_TARGETS)

Выставление переменной TOOL_MODE в непустое значение (которое будет очищено в $(DEFINE_TARGETS)) настраивает шаблоны сборки на создание утилит сборки.

Тем самым, следующее включение $(MTOP)/c.mk переопределит пути системы сборки для создаваемых файлов на пути для утилит сборки.

Параметры шаблона:

  • EXE -- выбирает шаблон сборки исполняемого файла и задаёт его имя (расширение будет добавлено автоматически)
  • SRC -- список файлов исходных текстов исполняемого файла
  • $(DEFINE_TARGETS) -- раскрывает макрос исполнения заданных шаблонов сборки.

Генерация произвольных файлов

Для генерации произвольных файлов нужно определить правила их генерации и добавить генерируемые файлы в сборку через вызов макроса ADD_GENERATED.

include $(MTOP)/defs.mk
MY_GENERATED := $(GEN_DIR)/my_dir/my_file.txt
$(call ADD_GENERATED,$(MY_GENERATED))
$(MY_GENERATED): MY_OPTIONS := my_options
$(MY_GENERATED): $(call GET_TOOL,my_tool) $(VPREFIX)my_source.txt
	$(call ospath,$<) $(MY_OPTIONS) -o $(call ospath,$(word 2,$^))
$(DEFINE_TARGETS)

Разберём пример построчно.

  1. Включение

    $(MTOP)/defs.mk

    добавляет описание стандартных функций и определений.

    Вообще, включение любого набора шаблонов clean-build (например упоминаемый ранее $(MTOP)/c.mk) также включает и $(MTOP)/defs.mk.

  2. Строка

    MY_GENERATED := $(GEN_DIR)/my_dir/my_file.txt

    задаёт имя генерируемого файла.

    Имя переменной MY_GENERATED выбрано произвольно, однако возможны коллизии с предопределёнными именами clean-build.

    Найти коллизии в именах переменных поможет запуск make C=1 (режим check makefiles -- проверка скриптов сборки одновременно со сборкой).

    GEN_DIR -- путь к директории для генерируемых в процессе сборки файлов.

    Система сбоки позволяет генерить файлы только в директориях $(GEN_DIR), $(LIB_DIR), $(BIN_DIR) или $(OBJ_DIR).

  3. Вызов

    $(call ADD_GENERATED,$(MY_GENERATED))

    добавляет генерируемые файлы в сборку:

    • директория $(GET_DIR)/my_dir будет создана автоматически перед вызовом правила генерации файлов
    • при выполнении make clean сгенерённые файлы будут удалены
  4. строка

    $(MY_GENERATED): MY_OPTIONS := my_options

    определяет target-specific переменную MY_OPTIONS со значением my_options.

    В системе сборки clean-build важно использовать по-возможности тоько target-specific переменные -- из-за нерекурсивности системы сборки.

    После того, как очередной Makefile разобран и определены правила сборки целей, может быть включён и разобран следующий Makefile, который может переопределить переменные из первого Makefile.

    Поэтому переменные нужно привязывать к цели сборки -- цели сборки всегда уникальны, что контролируется самим Gnu Make.

  5. следующие две строки определяют правило сборки файла $(MY_GENERATED)

    $(MY_GENERATED): $(call GET_TOOL,my_tool) $(VPREFIX)my_source.txt
    	$(call ospath,$<) $(MY_OPTIONS) -o $(call ospath,$(word 2,$^))
    • Первая строка задаёт зависимости $(MY_GENERATED) от утилиты сборки и файла my_source.txt.

    Раскрытие параметризованного макроса GET_TOOL через $(call GET_TOOL,my_tool) даёт путь к утилите сборки my_tool (пример создания my_tool приведён выше).

    Указание $(VPREFIX)my_source.txt задаёт зависимость генерируемого файла от my_source.txt, расположенного в той же директории, что и Makefile.

    В переменной $(VPREFIX) содержится относительный путь от директории, в которой запущен make к директории, в которой расположен целевой Makefile.

    Замечание.

    В приведённых выше примерах сборки статической библиотеки и утилиты переменная $(VPREFIX) не указывалась перед именами исходных файлов.

    $(VPREFIX) добавляется автоматически шаблонами сборки, если путь не абсолютный (т.е. не указан относительно корня проекта $(TOP)).

    Здесь же задаётся произвольное правило сборки -- все зависимости нужно указывать явно.

    • Вторая строка (начинающаяся с символа табуляции) определят метод генерации файла.

    $< -- автоматическая переменная -- имя первой зависимости цели, в данном случае это путь, возвращаемый $(call GET_TOOL,my_tool).

    Вызовом $(call ospath,$<) путь к утилите my_tool преобразуется в путь к фалу, характерный для операционной системы сборочной машины (например преобразуется C:/a/b -> C:\a\b).

    $(MY_OPTIONS) -- target-specific переменная -- раскрывается в my_options.

    $^ -- автоматическая переменная -- список всех зависимостей цели в порядке их перечисления.

    $(word 2,$^) -- имя второй зависимости цели, в данном случае это $(VPREFIX)my_source.txt.

    Замечание.

    VPREFIX не была объявлена как target-specific переменная, поэтому нельзя использовать выражение $(VPREFIX)my_source.txt в теле правила -- VPREFIX может быть изменена в следующих Makefile'ах, включенных после задания данного правила построения цели.

    $(call ospath,$(word 2,$^)) -- преобразование пути к my_source.txt в путь, характерный для операционной системы сборочной машины.

  6. $(DEFINE_TARGETS) -- раскрывает макрос исполнения заданных шаблонов сборки.

    В данном случае, т.к. никаких предопределённых шаблонов сборки указано не было, то $(DEFINE_TARGETS) не делает ничего, кроме включения завершающего $(MTOP)/all.mk, если текущий Makefile был обработан последним.

    Завершающий $(MTOP)/all.mk содержит определение цели по-умолчанию all, для выполнения которой нужно выполнить все определённые правила.

    Замечание.

    Вместе с заданием правила генерации произвольного файла можно было задать шаблон сборки, скажем библиотеки (включением $(MTOP)/c.mk вместо $(MTOP)/defs.mk), тогда вызов $(DEFINE_TARGETS) также приведёт к сборке библиотеки.

  7. Генерация нескольких файлов из одного исходного файла

    В Gnu Make нет специальных средств для решения проблемы с генерацией одним вызовом нескольких файлов -- правило построения цели в Gnu Make предполагает, что целевой файл всегда один. А иногда нужно, чтобы правило генерации нескольких целей выполнялось единожды при необходимости обновления любой из целей.

    Для решения этой проблемы, в clean-build определён макрос MULTI_TARGET.

    include $(MTOP)/defs.mk
    MY_OPTIONS := my_options
    MY_GENERATED := $(addprefix $(GEN_DIR)/my_dir/,my_file1.txt my_file2.txt)
    $(call MULTI_TARGET,$(MY_GENERATED),$(call GET_TOOL,my_tool) $(VPREFIX)my_source.txt,$$(call \
     ospath,$$<) $(MY_OPTIONS) -o $(VPREFIX)my_source.txt)
    $(DEFINE_TARGETS)

    Макрос MULTI_TARGET, определённый в $(MTOP)/defs.mk, принимает 3 аргумента:

    1. список генерируемых файлов -- $(MY_GENERATED)
    2. список зависимостей генерируемых файлов -- $(call GET_TOOL,my_tool) $(VPREFIX)my_source.txt
    3. правило генерации файлов -- $$(call ospath,$$<) $(MY_OPTIONS) -o $(VPREFIX)my_source.txt

    Перед вызовом макроса MULTI_TARGET, вычисляется каждый из передаваемых ему аргументов. Так как правило генерации файлов, передаваемое 3-им аргументом, будет раскрыто дважды -- первый раз перед вызовом MULTI_TARGET, второй -- во время выполнения правила, то обращение к $(ospath) и переменной $< экранировано двойным $$.

    Значение переменной VPREFIX необходимо использовать сразу, до включения следующего Makefile, в котором VPREFIX может быть изменена. Или можно сохранить $(VPREFIX) в target-specific переменной, чтобы использовать это значение во время выполнения правила.

    Переменная MY_OPTIONS не объявлена как target-specific, в отличие от предыдущего примера, т.к. используется сразу, в момент определения правила сборки, а не в момент выполнения этого правила.

  8. Объединение Makefile'ов в группу

    Пример группового Makefile:

    MDEPS := my_gen.mk
    TO_MAKE := tool1.mk lib1/my_lib.mk $(TOP)/dir1/dir2/my_exe_dir
    include $(MTOP)/parallel.mk

    Группа Makefile'ов указывается в переменной TO_MAKE, значение которой интерпретируется в подключаемом $(MTOP)/parallel.mk.

    Makefile'ы указываются как имена файлов с расширением .mk или директорий, путь к которым может быть либо относительным от текущего Makefile, либо абсолютным - от корня проекта в переменной $(TOP). Если элемент списка $(TO_MAKE) не заканчивается на .mk, то предполагается, что это директория, в которой ищется файл с именем Makefile.

    В переменной MDEPS можно указать список Makefile'ов, от которых зависят цели текущего Makefile (или группы Makefile'ов, как в данном примере). Эти зависимости будут наследоваться всеми Makefile'ами, подключаемыми групповым Makefile.

    Правила перечисления Makefile'ов в переменной MDEPS те же, что и для переменной TO_MAKE.

    Цели, указанные в Makefile'ах tool1.mk, lib1/my_lib.mk и $(TOP)/dir1/dir2/my_exe_dir/Makefile будут выполнены только после того, как будут выполнены цели Makefile'ов из $(MDEPS).

    Если Makefile из $(MDEPS) не был подключён к сборке в процессе определения целей, то он игнорируется. Таким образом, возможно выполнение (пере-)сборки модуля в его поддиректории, без необходимости пересобирать внешние зависимости, как в следующем примере:

    top_dir/Makefile:

    TO_MAKE := gen module
    include $(MTOP)/parallel.mk

    top_dir/module/Makefile:

    MDEPS := ../gen
    ....
    $(DEFINE_TARGETS)

    Вызов make clean && make в директории top_dir/module пересобирёт только цели из этой директории, не трогая цели в top_dir/gen.

  9. Собираем что-то уникальное, например выполняем тесты

    Пример:

    include $(MTOP)/defs.mk
    MDEPS := build_tests.mk
    TESTS := rb_test c_base64_test
    $(call ADD_GENERATED,$(addprefix $(GEN_DIR)/tests/,$(addsuffix .tsmt,$(TESTS))))
    define TEST_RULE
    $(newline)
    $(GEN_DIR)/tests/$t.tsmt: $(BIN_DIR)/$t$(EXE_SUFFIX)
    	$$(call SUP,TEST,$$<)$$< > $(GEN_DIR)/tests/$t.out
    	$(if $(VERBOSE),,@)$$(call TOUCH,$$@)
    endef
    $(eval $(foreach t,$(TESTS),$(TEST_RULE)))
    $(DEFINE_TARGETS)

    Здесь:

    1. MDEPS := build_tests.mk Задаёт что цели, указанные в build_tests.mk (например сборка тестируемых исполняемых файлов) должны быть выполнены до целей текущего Makefile.

    2. TESTS := rb_test c_base64_test Список тестируемых исполняемых файлов.

    3. $(call ADD_GENERATED,$(addprefix $(GEN_DIR)/tests/,$(addsuffix .tsmt,$(TESTS)))) В результате запуска тестов будут сгенерены файлы rb_test.tsmt и c_base64_test.tsmt в директории $(GEN_DIR)/tests.

    4. define TEST_RULE Начало определения шаблона генерации правила запуска теста.

    5. $(newline) Тело определяемого шаблона TEST_RULE начинается со строки, следующей за define TEST_RULE. Пусть первым символом шаблона будет перевод строки (переменная newline определена в $(MTOP)/defs.mk). Это позволит начинать определение очередного правила с новой строки - при раскрытии шаблона в цикле foreach.

    6. шаблонное правило c параметром шаблона $t

      $(GEN_DIR)/tests/$t.tsmt: $(BIN_DIR)/$t$(EXE_SUFFIX)
      	$$(call SUP,TEST,$$<)$$< > $(GEN_DIR)/tests/$t.out
      	$(if $(VERBOSE),,@)$$(call TOUCH,$$@)

      определяет, что результат теста $t.tsmt в директории $(GEN_DIR)/tests будет зависеть от даты создания тестируемого исполняемого файла $t$(EXE_SUFFIX) в директории $(BIN_DIR).

      Для выполнения теста сначала будет вызван исполняемый файл $(BIN_DIR)/$t$(EXE_SUFFIX) (указываемый через автоматическую перемсенную $$<), а stdout этого вызова будет перенаправлен с помощью > в файл $(GEN_DIR)/tests/$t.out. Вызов $$(call SUP,TEST,$$<) служит для информационной печати на экране выполняемой операции или команды (если make вызван с параметром V=1). Следующей строка -- вызов$$(call TOUCH,$$@) -- вторая команда правила генерации файла $(GEN_DIR)/tests/$t.tsmt (указываемого через автоматическую переменную $$@). Макрос TOUCH определён в $(MTOP)/defs.mk через подключение файла определений, специфичного для текущей сборочной платформы. Конструкция $(if $(VERBOSE),,@) -- даёт пустое значение в verbose режиме (если make вызван с параметром V=1) и @ (директива make -- скрыть выполняемую команду) в обычном режиме.

      Соответственно, в обычном режиме, при выполнении правила шаблона make напечатает что-то вроде:

      TEST   /home/user/procject/bin/LINUX-x86_64-x86_64-DEBUG/rb_test
      

      А в verbose-режиме:

      /home/user/proj/bin/LINUX-x86_64-x86_64-DEBUG/rb_test > /home/user/proj/gen/LINUX-x86_64-x86_64-DEBUG/rb_test.out
      touch /home/user/proj/gen/LINUX-x86_64-x86_64-DEBUG/rb_test.tsmt
      

      Замечания:

      • команды правила должны начинаться с символа табуляции
      • тест правил будет интерпретирован дважды, один раз при раскрытии шаблона, второй -- в момент выполнения правил. Поэтому раскрытие некоторых макросов в правилах отложено до второго раза - путём экранирования символа $ двойным $$.
    7. endef Конец шаблона TEST_RULE.

      Замечание: тело шаблона не включает перевод строки перед endef.

    8. $(eval $(foreach t,$(TESTS),$(TEST_RULE))) Для каждого $t из списка $(TESTS) раскрывается шаблон TEST_RULE, затем полученный список правил интерпретируется функцией eval как если бы эти правила были заданы в тексте Makefile.

      Небольшое замечание: функция foreach разделяет результаты раскрытия шаблона TEST_RULE пробелами, поэтому в конце последней строки раскрытого шаблона (после $$(call TOUCH,$$@)) будет добавлен пробел (добавление которого не играет никакой роли). А из-за того, что первым символом шаблона является $(newline), функции eval для интерпретации на вход поступят правила, начинающиеся с новой строки.

    9. $(DEFINE_TARGETS) -- раскрывает макрос исполнения заданных шаблонов сборки. В данном случае, т.к. никаких предопределённых шаблонов сборки указано не было, то $(DEFINE_TARGETS) не делает ничего, кроме включения завершающего $(MTOP)/all.mk, если текущий Makefile был обработан последним.