
"Переносимая кодовая база - это плацдарм для будущих разработок."
Пролог
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
\end{lstlisting}
Конфиг для 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 скрипте явно указать каким именно компилятором мы будем собирать исходные тексты программ (сорцы).
PREFIX = arm-none-eabi-
$(info GCC_PATH=$(GCC_PATH))
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 позволит Вам достаточно легко сделать много других полезностей:
№ | Задача |
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 | |
10 | GNU Make может больше чем ты думаешь | |
11 | Почему важно собирать код из скриптов |