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      — очистить
Поддержать проект