LEARN X · ЗА 12 МИН
Make
Make (Makefile) за 12 минут: правила, переменные, автопеременные, .PHONY, шаблонные правила, функции, условия и типичный Makefile для C-проекта.
Make — это инструмент автоматизации сборки. Вы описываете в файле Makefile цели и зависимости между файлами, а make сам решает, что нужно пересобрать. Этот тур умещает весь инструмент на одной странице через плотно закомментированный код.
1. Что такое Make и базовое правило
Makefile состоит из правил. Каждое правило: цель, её зависимости и рецепт (команды).
# Комментарии начинаются с # и идут до конца строки.
# Базовая структура правила:
#
# цель: зависимости
# рецепт (команда shell)
#
# ВАЖНО: рецепт ОБЯЗАТЕЛЬНО начинается с символа ТАБУЛЯЦИИ, не пробелов!
# Если поставить пробелы — make выдаст: "missing separator".
hello: # цель "hello", зависимостей нет
echo "Привет!" # ← здесь TAB, затем команда
# Цель обычно = имя файла, который нужно создать.
# Зависимости = файлы, из которых цель собирается.
program: main.o # чтобы собрать program, нужен main.o
gcc -o program main.o # рецепт: команда сборки (с TAB)
2. Запуск
Запускается командой make в каталоге с Makefile.
# make — выполнит ПЕРВУЮ цель в файле (цель по умолчанию).
# make hello — выполнит конкретную цель "hello".
# make program — соберёт цель "program" и все её зависимости.
# make -f other.mk — использовать файл other.mk вместо Makefile.
# make -n — "сухой прогон": показать команды, но не выполнять.
# make -j4 — параллельная сборка в 4 потока.
# Концепция целей:
# make строит ГРАФ зависимостей и обходит его. Чтобы выполнить цель,
# он сначала рекурсивно обрабатывает все её зависимости.
# Цель пересобирается, только если она устарела (см. секцию 9).
all: program # принято: первая цель называется "all" —
@echo "Готово" # @ в начале команды прячет её эхо в выводе
3. Переменные
# Есть несколько операторов присваивания:
CC = gcc # = рекурсивное: значение раскрывается при ИСПОЛЬЗОВАНИИ
CFLAGS := -Wall -O2 # := немедленное: раскрывается СЕЙЧАС, при объявлении
DEBUG ?= 0 # ?= присвоить, ТОЛЬКО если переменная ещё не задана
CFLAGS += -g # += дописать в конец (теперь "-Wall -O2 -g")
# Использование переменной: $(ИМЯ) или ${ИМЯ}
build:
$(CC) $(CFLAGS) -o app main.c # подставится: gcc -Wall -O2 -g -o app main.c
# Разница = и :=
# A = $(B) — B подставится в момент, когда используется A (лениво)
# A := $(B) — B подставится прямо здесь, текущим значением
# Переменную можно переопределить из командной строки:
# make build CC=clang DEBUG=1
4. Автоматические переменные
Внутри рецепта доступны спецпеременные, которые make подставляет автоматически.
# $@ — имя текущей цели
# $< — ПЕРВАЯ зависимость
# $^ — ВСЕ зависимости (без дубликатов), через пробел
# $? — только те зависимости, что НОВЕЕ цели (изменились)
program: main.o utils.o
gcc -o $@ $^ # $@ = program, $^ = main.o utils.o
# раскроется в: gcc -o program main.o utils.o
main.o: main.c
gcc -c $< -o $@ # $< = main.c, $@ = main.o
# раскроется в: gcc -c main.c -o main.o
# $? удобно для архивов: пересобрать только изменённые файлы
lib.a: a.o b.o c.o
ar r $@ $? # добавит в архив только обновлённые .o
5. Фальшивые цели (.PHONY)
Цель, за которой нет файла (например clean), нужно объявить фальшивой.
# Проблема: если в каталоге случайно появится файл с именем "clean",
# make решит, что цель "clean" уже собрана, и НЕ выполнит рецепт.
# Решение: .PHONY перечисляет цели, которые НЕ являются файлами.
.PHONY: all clean test install
all: program # собрать всё
clean:
rm -f *.o program # удалить артефакты сборки
test:
./run_tests.sh # запустить тесты
# Теперь make clean выполнится ВСЕГДА, даже если есть файл ./clean.
# Бонус: .PHONY-цели чуть быстрее (make не проверяет дату файла).
6. Шаблонные правила
Символ % — это шаблон, который ловит общую часть имени. Не нужно писать правило для каждого файла.
# Одно правило для ВСЕХ .c → .o:
# % — это "основа" имени (stem). %.o ловит foo.o, bar.o и т.д.,
# а %.c подставляет ту же основу: foo.c, bar.c.
%.o: %.c
gcc -c $< -o $@ # $< = foo.c, $@ = foo.o
# wildcard разворачивает маску в список реальных файлов:
SOURCES := $(wildcard *.c) # все .c в каталоге: main.c utils.c
# patsubst меняет суффиксы по шаблону: получаем список .o из .c
OBJECTS := $(patsubst %.c,%.o,$(SOURCES)) # main.o utils.o
app: $(OBJECTS)
gcc -o $@ $^ # благодаря шаблонному правилу каждый .o соберётся сам
7. Функции
Функции вызываются как $(имя аргументы).
# $(wildcard маска) — список существующих файлов по маске
SRC := $(wildcard src/*.c) # src/a.c src/b.c
# $(patsubst шаблон,замена,текст) — замена по шаблону с %
OBJ := $(patsubst src/%.c,build/%.o,$(SRC)) # build/a.o build/b.o
# $(shell команда) — выполнить shell-команду и взять её вывод
DATE := $(shell date +%Y-%m-%d) # текущая дата в переменную
GIT := $(shell git rev-parse --short HEAD) # хеш коммита
# $(foreach перем,список,выражение) — цикл по списку
DIRS := src lib test
ALL := $(foreach d,$(DIRS),$(d)/main.c) # src/main.c lib/main.c test/main.c
# Ещё полезные: $(subst из,в,текст), $(filter шаблон,список),
# $(dir путь), $(notdir путь), $(basename файл)
info:
@echo "Сборка $(DATE) на коммите $(GIT)"
8. Условия
# ifeq / ifneq — сравнение значений
DEBUG ?= 0
ifeq ($(DEBUG),1)
CFLAGS := -g -O0 # отладочная сборка
else
CFLAGS := -O2 # оптимизированная сборка
endif
# ifdef / ifndef — проверка, ОПРЕДЕЛЕНА ли переменная
ifdef VERBOSE
Q := # ничего: команды видны
else
Q := @ # @: прятать эхо команд
endif
build:
$(Q)gcc $(CFLAGS) -o app main.c
# Запуск: make build DEBUG=1 VERBOSE=1
# Условия вычисляются при ЧТЕНИИ Makefile, до выполнения целей.
9. Зависимости и инкрементальная сборка
Главная идея Make: пересобирать только то, что устарело.
# Как make решает, нужно ли пересобирать цель:
#
# 1. Смотрит на ВРЕМЯ ИЗМЕНЕНИЯ (mtime) файла цели и её зависимостей.
# 2. Если какая-то зависимость НОВЕЕ цели — цель устарела, пересобираем.
# 3. Если цели-файла нет вообще — собираем.
# 4. Если цель новее всех зависимостей — make говорит "up to date", пропускает.
#
# Это и есть инкрементальная сборка: при изменении одного main.c
# пересоберётся только main.o и финальный бинарь, остальное не тронется.
app: main.o utils.o # app зависит от двух объектников
gcc -o $@ $^
main.o: main.c defs.h # ВАЖНО: указывайте и заголовки в зависимостях!
gcc -c $< -o $@ # иначе правка defs.h не вызовет пересборку
utils.o: utils.c defs.h
gcc -c $< -o $@
# Меняем defs.h → make видит, что .o старее → пересоберёт оба .o и app.
# Меняем только utils.c → пересоберётся лишь utils.o и app.
10. Типичный Makefile
Собираем всё изученное в реальный Makefile для C-проекта.
# ===== Настройки =====
CC := gcc
CFLAGS := -Wall -Wextra -O2
TARGET := myapp # имя итогового бинарника
PREFIX ?= /usr/local # куда устанавливать (можно переопределить)
SRC := $(wildcard src/*.c) # все исходники
OBJ := $(patsubst src/%.c,build/%.o,$(SRC)) # объектные файлы в build/
# ===== Цели =====
.PHONY: all build test install clean
all: build # цель по умолчанию
build: $(TARGET) # собрать приложение
$(TARGET): $(OBJ)
$(CC) $(CFLAGS) -o $@ $^ # слинковать все .o в бинарь
# Шаблонное правило: каждый build/имя.o из src/имя.c
build/%.o: src/%.c
@mkdir -p build # создать каталог build при нужде
$(CC) $(CFLAGS) -c $< -o $@
test: build # тесты зависят от готовой сборки
./$(TARGET) --selftest
install: build # установка в систему
install -d $(PREFIX)/bin
install -m 755 $(TARGET) $(PREFIX)/bin/
clean: # убрать артефакты
rm -rf build $(TARGET)
# Использование:
# make — собрать
# make test — собрать и протестировать
# make install PREFIX=~/.local
# make clean — очистить