
"Переносимая кодовая база - это плацдарм для будущих разработок."
Пролог
GNU Make - это консольная утилита, которая запускает другие консольные утилиты в желаемой последовательности. Только и всего. Конфигом для утилиты make является текстовый файл-скрипт хранящийся в файле по имени Makefile. Скрипт - это программа для интерпретатора. Поэтому утилиту GNU Make называют системой сборки. Можно сказать, что Make - это интерпретатор языка make.
Прелесть утилиты make в том, что ей абсолютно всё равно с каким языком программирования работать. Более того, утилите make всё равно с какие утилиты вызывать.
Утилита make всеядная. Понимаете?
С математической точки зрения, make делает топологическую сортировку ориентированного графа. Makefile скрипт прописывает список смежности вершин. Далее запускается команда make all, и утилита проходит все вершины в порядке ориентированного графа. По сути makefile определяет конвейер вызова утилит для метаморфоза файлов из одного расширения в другое расширение прямо на жестком диске PC.
При помощи make можно даже автоматически синтезировать инструкции по сборке пассажирских авиалайнеров или домов из миллионов деталей. Достаточно просто в make файле атомарно указать, что надо соединить с чем. Затем вызвать make all и у вас появится текстовый файл с логом корректной инструкций сборки этого, условно, самолёта. Корректная инструкция в том плане, что у вас не будет такой ситуации, что очередная деталь упирается в узкий проём и её не вставить, из-за чего надо опять разбирать, скажем, пол двигателя.
Всё то же самое происходит и в сборке компьютерных программ. Нет смысла генерировать bin файл, когда нет elf файла. Нет смысла генерировать elf файл, когда нет obj файла.
И тому подобное.
Достоинство make в том, что он пере собирает только те файлы , которые были изменены.
Времена последней модификации, читаются от файловой системы. То есть в make с самого начала в 1976 были заложены элементы контроля версий.
Синтаксис и cемантика
Признаком комментария является символ решетка #. Пустые строки и строки, начинающиеся с #, игнорируются.
В make есть переменные, функции и файлы. Всё как в настоящем языке программирования.
Как и в любом скрипте make позволяет создавать переменные. Например CFLAGS. Получить доступ к переменной можно заключив ее в круглые скобки.
$(error CFLAGS=$(CFLAGS))
Заложены и сокращенные, специальные переменные
специальные переменные | пояснение |
$@ | Имя цели обрабатываемого правила |
$^ | Список всех зависимостей обрабатываемого правила |
$< | Имя первой зависимости обрабатываемого правила |
$* | Основа целевого имени файла. Основа — это обычно имя файла без суффикса. |
$? | Цепочке имен файлов, которые оказались более новыми, чем целевой |
$+ | Подобно $^, это имена всех зависимостей, разделенных пробелами, за исключением того, |
$% | Элемент имени файла спецификации члена архива. |
Как организовать скрипты сборки GNU Make?
Любой GNU Make скрипт начинается с Makefile. У каждой сборки есть свой Makefile.
По-хорошему Makefile должен быть очень маленьким. Все общие скрипты должны быть вынесены за скобки в отдельные *.mk файлы. Да, GNU Make поддерживает свой собственный препроцессор. Поэтому вы будете часто встречать ключевое слово include. Это нормально.
# Makefile MK_PATH:=$(dir $(realpath $(lastword $(MAKEFILE_LIST)))) WORKSPACE_LOC:=$(MK_PATH)../../ WORKSPACE_LOC:= $(realpath $(WORKSPACE_LOC)) MK_PATH:= $(realpath $(MK_PATH)) #@echo $(error MK_PATH=$(MK_PATH)) INCDIR += -I$(MK_PATH) INCDIR += -I$(WORKSPACE_LOC) DEPENDENCIES_GRAPHVIZ=Y TARGET=boardname_configname_gcc_m include $(MK_PATH)/config.mk ifeq ($(CLI),Y) include $(MK_PATH)/cli_config.mk endif ifeq ($(DIAG),Y) include $(MK_PATH)/diag_config.mk endif ifeq ($(TEST),Y) include $(MK_PATH)/test_config.mk endif include $(WORKSPACE_LOC)/make_scripts/code_base.mk include $(WORKSPACE_LOC)/make_scripts/rules.mk
Тут функция realpath вычисляет абсолютный путь учитывая арифметику над путями. При сборке из make по сути все сборки в репозитории отличаются только одним лишь файликом.
Это config.mk. Вот так он выглядит.
# config.mk CLI=Y DEBUG=Y DEPENDENCIES_GRAPHVIZ=Y GENERIC=Y GPIO=Y LED_MONO=Y ............. TIME=Y UART2=Y UNIT_TEST=Y YTM32B1ME05G0MLQ=Y YTM32B1M_EVB_0144_REV_B=Y
Внутри config.mk декларативно перечисляется из каких программных компонентов
должна состоять данная программа. Конфиг для диагностики (diag_config.mk).
# diag_config.mk $(info Add Diag) DIAG=Y LOG_DIAG=Y ifeq ($(ALLOCATOR),Y) ALLOCATOR_DIAG=Y endif ifeq ($(CORTEX_M33),Y) CORTEX_M33_DIAG=Y endif ........ ifeq ($(TIMER),Y) TIMER_DIAG=Y endif ifeq ($(WATCHDOG),Y) WATCHDOG_DIAG=Y endif
Конфиг для CLI cli_config.mk
# cli_config.mk $(info CLI_CONFIG_MK_INC=$(CLI_CONFIG_MK_INC) ) ifneq ($(CLI_CONFIG_MK_INC),Y) CLI_CONFIG_MK_INC=Y CLI_CMD_HISTORY=Y CLI=Y ifeq ($(BUTTON),Y) BUTTON_COMMANDS=Y endif ifeq ($(NVIC),Y) NVIC_COMMANDS=Y endif ....... ifeq ($(UART),Y) UART_COMMANDS=Y endif ifeq ($(UNIT_TEST),Y) UNIT_TEST_COMMANDS=Y endif ifeq ($(WATCHDOG),Y) WATCHDOG_COMMANDS=Y endif endif
У каждого программного компонента свой отдельный make скрипт сборки. Имеет расширение *.mk. Все они будут выглядеть структурно одинаково. Отличие только в одном ключевом слове. Вот, например button.mk
# button.mk $(info BUTTON_MK_INC=$(BUTTON_MK_INC)) ifneq ($(BUTTON_MK_INC),Y) BUTTON_MK_INC=Y BUTTON_DIR = $(SENSITIVITY_DIR)/button #@echo $(error BUTTON_DIR=$(BUTTON_DIR)) INCDIR += -I$(BUTTON_DIR) SOURCES_C += $(BUTTON_DIR)/button_drv.c BUTTON=Y OPT += -DHAS_BUTTON OPT += -DHAS_BUTTON_PROC ifeq ($(BUTTON_DIAG),Y) OPT += -DHAS_BUTTON_DIAG SOURCES_C += $(BUTTON_DIR)/button_diag.c endif ifeq ($(CLI),Y) ifeq ($(BUTTON_COMMANDS),Y) OPT += -DHAS_BUTTON_COMMANDS SOURCES_C += $(BUTTON_DIR)/button_commands.c endif endif endif
Это скрипт с правилами сборки проекта (rules.mk). Его прелесть в том, что он общий для всех 300 сборок в репозитории! Обратите внимание на ключевое слово vpath. Оно позволяет перенаправлять объектные файлы в папку build и, тем самым, не засорять репозиторий временными объектными файлами.
# rules.mk CSTANDARD = -std=c99 #CSTANDARD = -std=c11 mkfile_path := $(abspath $(lastword $(MAKEFILE_LIST))) $(info mkfile_path:$(mkfile_path) ) MK_PATH := $(subst /cygdrive/c/,C:/, $(MK_PATH)) $(info MK_PATH=$(MK_PATH)) BUILD_DIR=build EXTRA_TARGETS= INCDIR := $(subst /cygdrive/c/,C:/, $(INCDIR)) SOURCES_TOTAL_C += $(SOURCES_C) SOURCES_TOTAL_C += $(SOURCES_CONFIGURATION_C) SOURCES_TOTAL_C += $(SOURCES_THIRD_PARTY_C) SOURCES_TOTAL_C := $(subst /cygdrive/c/,C:/, $(SOURCES_TOTAL_C)) SOURCES_ASM := $(subst /cygdrive/c/,C:/, $(SOURCES_ASM)) LIBS := $(subst /cygdrive/c/,C:/, $(LIBS)) LDSCRIPT := $(subst /cygdrive/c/,C:/, $(LDSCRIPT)) WORKSPACE_LOC := $(realpath $(WORKSPACE_LOC)) WORKSPACE_LOC := $(subst /cygdrive/c/,C:/, $(WORKSPACE_LOC)) include $(WORKSPACE_LOC)/make_scripts/toolchain.mk AS_DEFS = AS_INCLUDES = MICROPROCESSOR += $(CPU) MICROPROCESSOR += $(FPU) MICROPROCESSOR += $(FLOAT-ABI) include $(WORKSPACE_LOC)/make_scripts/compiler_options.mk include $(WORKSPACE_LOC)/make_scripts/linker_options.mk ASFLAGS += $(MCU) ASFLAGS += $(AS_DEFS) ASFLAGS += $(AS_INCLUDES) ASFLAGS += $(OPT) ASFLAGS += $(COMPILE_OPT) ASFLAGS += -Wall ASFLAGS +=-fdata-sections ASFLAGS += -ffunction-sections CPP_FLAGS += $(CSTANDARD) $(INCDIR) $(OPT) $(COMPILE_OPT) EXTRA_TARGETS += generate_definitions ARTIFACTS += $(BUILD_DIR)/$(TARGET).bin ARTIFACTS += $(BUILD_DIR)/$(TARGET).hex ARTIFACTS += $(BUILD_DIR)/$(TARGET).elf .PHONY: all all: $(EXTRA_TARGETS) $(ARTIFACTS) .PHONY: generate_definitions generate_definitions: $(info GenerateDefinitions...) $(PREPROCESSOR_TOOL) $(CPP_FLAGS) $(WORKSPACE_LOC)/empty_source.c -dM -E> c_defines_generated.h $(SORTER_TOOL) -u c_defines_generated.h -o c_defines_generated.h OBJECTS = $(addprefix $(BUILD_DIR)/,$(notdir $(SOURCES_TOTAL_C:.c=.o))) vpath %.c $(sort $(dir $(SOURCES_TOTAL_C))) # list of ASM program objects OBJECTS += $(addprefix $(BUILD_DIR)/,$(notdir $(SOURCES_ASM:.S=.o))) vpath %.S $(sort $(dir $(SOURCES_ASM))) TOTAL_FILES := $(words $(OBJECTS)) $(info TOTAL_FILES:$(TOTAL_FILES) ) $(BUILD_DIR)/%.o: %.c Makefile | $(BUILD_DIR) $(eval CURRENT_CNT=$(shell echo $$(($(CURRENT_CNT)+1)))) @echo Compiling $(CURRENT_CNT)/$(TOTAL_FILES) $@ @$(CC) -c -MD $(CFLAGS) -Wa,-a,-ad,-alms=$(BUILD_DIR)/$(notdir $(<:.c=.lst)) $< -o $@ $(BUILD_DIR)/%.o: %.S Makefile | $(BUILD_DIR) $(AS) -c $(CFLAGS) $< -o $@ $(BUILD_DIR)/$(TARGET).elf: $(OBJECTS) Makefile $(CC) $(OBJECTS) $(LDFLAGS) -o $@ $(SZ) $@ $(BUILD_DIR)/%.hex: $(BUILD_DIR)/%.elf | $(BUILD_DIR) $(HEX) $< $@ $(BUILD_DIR)/%.bin: $(BUILD_DIR)/%.elf | $(BUILD_DIR) $(BIN) $< $@ $(BUILD_DIR): mkdir -p $@ .PHONY: clean clean: -rm -fR $(BUILD_DIR) # dependencies -include $(wildcard $(BUILD_DIR)/*.d)
Как можно заметить, в зависимости от значения переменных окружения, make скрипт добавит в сборку те или иные программные компоненты. Каждый программный компонент оформляется как .mk файл. Внутри .mk файла происходит добавление файлов исходников в переменную окружения SOURCES_C и происходит добавление макро определений в переменную окружения OPT.
ifneq ($(CODE_BASE_MK),Y) CODE_BASE_MK=Y include $(WORKSPACE_LOC)/make_scripts/code_base_preconfig.mk include $(WORKSPACE_LOC)/make_scripts/verify_build.mk INCDIR += -I$(WORKSPACE_LOC) $(info WORKSPACE_LOC=$(WORKSPACE_LOC)) GIT_SHA := $(shell git rev-parse --short HEAD) OPT += -DGIT_SHA=0x0$(GIT_SHA) ifeq ($(DEBUG),Y) OPT += -DHAS_DEBUG endif ifeq ($(MBR),Y) include $(WORKSPACE_LOC)/make_scripts/mbr.mk endif ifeq ($(GENERIC),Y) include $(WORKSPACE_LOC)/make_scripts/generic.mk endif ifeq ($(MICROCONTROLLER),Y) FIRMWARE=Y include $(WORKSPACE_LOC)/microcontroller/microcontroller.mk endif ifeq ($(BOARD),Y) include $(WORKSPACE_LOC)/boards/boards.mk endif ifeq ($(PROTOTYPE),Y) include $(WORKSPACE_LOC)/prototypes/prototypes.mk endif ifeq ($(X86),Y) SUPER_CYCLE=Y OPT += -DX86 OPT += -DHAS_X86 FLOAT_UTILS=Y endif ifeq ($(X86_64),Y) SUPER_CYCLE=Y #@echo $(error stop) OPT += -DX86_64 OPT += -DHAS_X86_64 endif ifeq ($(THIRD_PARTY),Y) include $(WORKSPACE_LOC)/third_party/third_party.mk endif ifeq ($(CORE),Y) include $(WORKSPACE_LOC)/core/core.mk endif ifeq ($(APPLICATIONS),Y) include $(WORKSPACE_LOC)/applications/applications.mk endif ifeq ($(MCAL),Y) include $(WORKSPACE_LOC)/mcal/mcal.mk endif ifeq ($(ADT),Y) include $(WORKSPACE_LOC)/adt/adt.mk endif ifeq ($(CONNECTIVITY),Y) include $(WORKSPACE_LOC)/connectivity/connectivity.mk endif ifeq ($(CONTROL),Y) include $(WORKSPACE_LOC)/control/control.mk endif ifeq ($(COMPONENTS),Y) include $(WORKSPACE_LOC)/components/components.mk endif ifeq ($(COMPUTING),Y) include $(WORKSPACE_LOC)/computing/computing.mk endif include $(WORKSPACE_LOC)/compiler/compiler.mk ifeq ($(SENSITIVITY),Y) include $(WORKSPACE_LOC)/sensitivity/sensitivity.mk endif ifeq ($(STORAGE),Y) include $(WORKSPACE_LOC)/storage/storage.mk endif ifeq ($(SECURITY),Y) include $(WORKSPACE_LOC)/security/security.mk endif ifeq ($(ASICS),Y) include $(WORKSPACE_LOC)/asics/asics.mk endif ifeq ($(MISCELLANEOUS),Y) include $(WORKSPACE_LOC)/miscellaneous/miscellaneous.mk endif ifeq ($(UNIT_TEST),Y) include $(WORKSPACE_LOC)/unit_tests/unit_test.mk endif SOURCES_C += $(WORKSPACE_LOC)/main.c endif
Сборка прошивок - это всегда кросс-компиляция. Поэтому надо в make скрипте явно указать каким именно компилятором мы будем собирать исходные тексты программ (сорцы).
PYTHON_BIN=python.exe ifeq ($(ARM), Y) PREFIX = arm-none-eabi- endif ifeq ($(RISC_V), Y) PREFIX = riscv-none-elf- endif # binaries #GCC_PATH="C:/Program Files (x86)/GNU Arm Embedded Toolchain/10 2021.10/bin" $(info GCC_PATH=$(GCC_PATH)) # The gcc compiler bin path can be either defined in make command via GCC_PATH # variable (> make GCC_PATH=xxx) # either it can be added to the PATH environment variable. ifdef GCC_PATH $(info WithPath) PREPROCESSOR_TOOL =$(GCC_PATH)/$(PREFIX)cpp CC = $(GCC_PATH)/$(PREFIX)gcc CPP = $(GCC_PATH)/$(PREFIX)g++ AS = $(GCC_PATH)/$(PREFIX)gcc -x assembler-with-cpp CP = $(GCC_PATH)/$(PREFIX)objcopy SZ = $(GCC_PATH)/$(PREFIX)size else $(info WithOutPath) PREPROCESSOR_TOOL = $(PREFIX)cpp CC = $(PREFIX)gcc CPP = $(PREFIX)g++ AS = $(PREFIX)gcc -x assembler-with-cpp CP = $(PREFIX)objcopy SZ = $(PREFIX)size endif HEX = $(CP) -O ihex BIN = $(CP) -O binary -S
Компилятор - это консольная утилита. Как и у любой консольной утилиты в gcc есть опции командной строки (ключи). Вот типичный пучок опций для сборки прошивок на MCU (compiler_options.mk). Что значит каждая опция компилятора можно посмотреть в официальном документе Using the GNU Compiler Collection.
# compiler_options.mk COMPILE_OPT += -Wall COMPILE_OPT += -fdata-sections COMPILE_OPT += -ffunction-sections COMPILE_OPT += -Werror=address COMPILE_OPT += -Werror=switch COMPILE_OPT += -Werror=array-bounds=1 COMPILE_OPT += -Werror=comment COMPILE_OPT += -Werror=div-by-zero COMPILE_OPT += -Werror=duplicated-cond COMPILE_OPT += -Werror=shift-negative-value COMPILE_OPT += -Werror=duplicate-decl-specifier COMPILE_OPT += -Werror=enum-compare COMPILE_OPT += -Werror=uninitialized COMPILE_OPT += -Werror=empty-body COMPILE_OPT += -Werror=unused-but-set-parameter COMPILE_OPT += -Werror=unused-but-set-variable COMPILE_OPT += -Werror=float-equal COMPILE_OPT += -Werror=logical-op COMPILE_OPT += -Werror=implicit-int COMPILE_OPT += -Werror=implicit-function-declaration COMPILE_OPT += -Werror=incompatible-pointer-types COMPILE_OPT += -Werror=int-conversion COMPILE_OPT += -Werror=old-style-declaration COMPILE_OPT += -Werror=maybe-uninitialized COMPILE_OPT += -Werror=redundant-decls COMPILE_OPT += -Werror=sizeof-pointer-div COMPILE_OPT += -Werror=misleading-indentation COMPILE_OPT += -Werror=missing-declarations COMPILE_OPT += -Werror=missing-parameter-type COMPILE_OPT += -Werror=overflow COMPILE_OPT += -Werror=parentheses COMPILE_OPT += -Werror=pointer-sign COMPILE_OPT += -Werror=return-type COMPILE_OPT += -Werror=shift-count-overflow COMPILE_OPT += -Werror=strict-prototypes COMPILE_OPT += -Werror=unused-but-set-variable COMPILE_OPT += -Werror=unused-function COMPILE_OPT += -Werror=unused-variable COMPILE_OPT += -Werror=type-limits COMPILE_OPT += -Werror=override-init COMPILE_OPT += -Werror=duplicate-decl-specifier COMPILE_OPT += -Werror=int-conversion COMPILE_OPT += -Wno-stringop-truncation COMPILE_OPT += -Wno-format-truncation COMPILE_OPT += -Wno-restrict COMPILE_OPT += -Wno-format COMPILE_OPT += -Wno-cpp #TODO temp COMPILE_OPT += -Wno-discarded-qualifiers COMPILE_OPT += -Wmissing-prototypes COMPILE_OPT += -Werror=traditional COMPILE_OPT += -Werror=missing-prototypes COMPILE_OPT += -fdce COMPILE_OPT += -fdse COMPILE_OPT += -fmessage-length=0 COMPILE_OPT += -fsigned-char COMPILE_OPT += -fno-common COMPILE_OPT += -fstack-usage COMPILE_OPT += -fzero-initialized-in-bss COMPILE_OPT += -finline-small-functions COMPILE_OPT += -Wmissing-field-initializers COMPILE_OPT += -Werror=missing-field-initializers COMPILE_OPT += -Werror=unused-but-set-variable COMPILE_OPT += -Werror=implicit-function-declaration COMPILE_OPT += -Werror=unused-variable COMPILE_OPT += -Wformat-overflow=1 # Generate dependency information COMPILE_OPT += -MMD -MP -MF"$(@:%.o=%.d)" ifeq ($(DEBUG), Y) COMPILE_OPT += -O0 COMPILE_OPT += -g3 else COMPILE_OPT += -Os endif COMPILE_OPT += $(CSTANDARD) COMPILE_OPT += $(MICROPROCESSOR) COMPILE_OPT += $(INCDIR)
Подобно компилятору, ключи надо передавать и компоновщику. Вот типичный пучок опций для настройки компоновщика (linker_options.mk). Что значит каждая опция компоновщика можно посмотреть в доке The GNU linker.
LINKER_FLAGS += -Xlinker --gc-sections LINKER_FLAGS += -Xlinker --print-memory-usage ifeq ($(LIBC_NANO), Y) LINKER_FLAGS += --specs=nano.specs endif ifeq ($(LIBC_RDIMON), Y) LINKER_FLAGS += --specs=rdimon.specs endif ifeq ($(LIBC), Y) LIBS += -lc endif ifeq ($(MATH_LIB), Y) LIBS += -lm endif LIBDIR += LDFLAGS += -t LDFLAGS += $(MICROPROCESSOR) LDFLAGS += -T$(LDSCRIPT) LDFLAGS += $(LIBDIR) LDFLAGS += $(LIBS) LDFLAGS += -Wl,--cref LDFLAGS += -Wl,--gc-sections LDFLAGS += -Wl,-Map=$(BUILD_DIR)/$(TARGET).map LDFLAGS += $(LINKER_FLAGS)
В переменную окружения LDFLAGS через ключ -T передается скрипт компоновщику. Он сообщает компоновщику, как организовать код из входных объектов.
Структура сборки
При сборке из самоcтоятельно написанных скриптов make у вас для каждой прошивки (сборки) в папке projects будет папка, в которой будет лежать вот эти файлы. По факту достаточно только двух файлов: Makefile и config.mk. Остальное - это уже специфика прошивки и настройки для вашего текстового редактора.
№ | название файла или папки | обязательно | пояснение |
1 | Makefile | 1 | make файл для данной сборки |
2 | build_from_make.bat | скрипт который просто выpывает make all | |
3 | 1 | конфигурация для функционала прошивки | |
4 | diag_config.mk | конфигурация для диагностики | |
5 | cli_config.mk | конфигурация UART-CLI консоли | |
6 | flash_hex.bat | скрипт для вызова make flash | |
7 | test_config.mk | конфигурация для модульных тестов | |
8 | sys_config.h | конфиг для выбора электронной платы | |
9 | version.h | версия прошивки | |
10 | build | 1 | папка куда будут попадать сгенерированные артефакты (*.elf, *.hex, *.bin, .map) и .o файлы |
11 | .cproject | Настройка Eclipse, как системы сборки | |
12 | .project | Настройка Eclipse, как текстового редактора |
Итоги
Вот теперь и Вы обладаете минимальными необходимыми навыками и сноровкой для ручного написания make скриптов, полноценной работы с системой сборки GNU Make и можете учить этому других.
Как видите, в GNU Make нет, ровным счетом, ничего сложного.
Помимо сборки сорцов make позволит Вам достаточно легко сделать много других полезностей:
№ | Задача |
16 | показывать progress bar процесса сборки прошивки |
1 | |
2 | |
3 | запустить всяческие кодогенераторы (для любителей кодогенераторов) |
4 | |
5 | |
6 | |
7 | |
8 | |
9 | |
11 | |
12 | |
13 | |
14 | |
15 | и многое другое, всё, что Вашей душе угодно |
У сборок из скриптов самое главное достоинство - это простота масштабирования. В этом вся соль. Немного усилий в начале зато потом существенный выигрыш в производительности.

Ссылки
# | Название | URL |
1 | Инструкция к GNU make на русском | |
2 | GNU make | |
3 | Магия makefile на простых примерах | https://microsin.net/programming/arm/learning-makefile-with-simple-examples.html |
4 | Перевод Makefile Mini HOWTO | |
5 | Everything You Never Wanted To Know About Linker Script | |
6 | Пример Makefile | |
7 | Эффективное использование GNU Make | |
8 | Обновление Прошивки из Make Скрипта | |
9 | Настройка ToolChain(а) для Win10++С+Makefile+ARM Cortex-Mx+GDB | |
12 | Как собрать Си программу в OS Windows | |
10 | GNU Make может больше чем ты думаешь | |
11 | Почему важно собирать код из скриптов |
