Лаборатория кодера

Справочник · Код и сборка

Make: Makefile для проекта

Цели, зависимости и правила, переменные и автоматические переменные, .PHONY, шаблонные правила и самодокументирующийся Makefile, который запускает тесты, сборку и линтеры одной командой.

Обновлено · 7 мин чтения

Make придумали в 1976 году для сборки программ на C, но он по-прежнему удобен как единая точка входа в проект: make test, make lint, make run работают одинаково на любой машине и в CI. Ниже GNU Make, который стоит в Linux и ставится в macOS.

Правило

Makefile состоит из правил:

make
цель: зависимости
	команды

Make смотрит на время изменения файлов: если цель — файл и он новее всех своих зависимостей, команды не выполняются. Поэтому повторный make после успешной сборки ничего не делает.

make
app: main.o utils.o
	cc -o app main.o utils.o

main.o: main.c utils.h
	cc -c main.c

utils.o: utils.c utils.h
	cc -c utils.c

После правки utils.h пересоберутся оба объектных файла и программа, а после правки main.c только main.o и app.

Без аргументов make выполняет первую цель в файле. Её принято делать целью по умолчанию: all или help.

.PHONY: цели, которые не файлы

Большинство целей в современных проектах — это команды, а не файлы: test, lint, clean. Их объявляют в .PHONY, иначе если в папке случайно появится файл test, Make решит, что цель уже готова, и ничего не запустит.

make
.PHONY: test lint clean

test:
	pytest -q

lint:
	ruff check .

clean:
	rm -rf build dist .pytest_cache

Переменные

make
PYTHON  ?= python3
SRC     := src
VERSION := $(shell git describe --tags --always)
FILES    = $(wildcard $(SRC)/*.py)

run:
	$(PYTHON) -m app --version $(VERSION)
ОператорКогда вычисляется значение
=при каждом использовании, лениво; может ссылаться на переменные, заданные ниже
:=один раз, в момент присваивания
?=только если переменная ещё не задана, например окружением
+=дописать к значению через пробел

Переменную можно переопределить при запуске: make run PYTHON=python3.12. Значения из командной строки сильнее присваиваний в файле, а переменные окружения слабее, если в файле не использован ?=.

Знак доллара в командах для оболочки удваивается: $$HOME, $$(date +%F). Одинарный $ Make считает своей переменной.

Автоматические переменные

ПеременнаяЗначение
$@имя цели
$<первая зависимость
$^все зависимости без повторов
$?зависимости новее цели
$*основа имени в шаблонном правиле, то, что совпало с %
$(@D)каталог цели
make
build/%.html: content/%.md
	@mkdir -p $(@D)
	pandoc $< -o $@

Это шаблонное правило: для любого build/имя.html Make найдёт content/имя.md. Осталось задать список целей:

make
PAGES := $(patsubst content/%.md,build/%.html,$(wildcard content/*.md))

.PHONY: site
site: $(PAGES)

Полезные функции

ФункцияПримерРезультат
wildcard$(wildcard src/*.c)список файлов по шаблону
patsubst$(patsubst %.c,%.o,a.c b.c)a.o b.o
сокращённая замена$(SRCS:.c=.o)то же для переменной
subst$(subst -,_,a-b-c)a_b_c
notdir / dir$(notdir src/a.c)a.c / src/
basename$(basename a.tar.gz)a.tar
addprefix$(addprefix build/,a b)build/a build/b
filter / filter-out$(filter %.c,a.c b.h)a.c
shell$(shell date +%F)вывод команды
if$(if $(CI),--no-color,)условная подстановка

Префиксы команд

ПрефиксЧто делает
@не печатать саму команду перед выполнением
-не останавливаться, если команда завершилась с ошибкой
+выполнить даже при make -n

Каждая строка команды запускается в отдельной оболочке. Поэтому cd на одной строке не влияет на следующую:

make
# не работает: второй строкой мы снова в корне проекта
wrong:
	cd frontend
	npm run build

# работает
right:
	cd frontend && npm run build

Если нужно, чтобы весь рецепт шёл в одной оболочке, в начале файла пишут .ONESHELL:. Там же полезно включить строгий режим bash, как в обычных скриптах:

make
SHELL := bash
.SHELLFLAGS := -eu -o pipefail -c

Самодокументирующийся Makefile

Рабочий шаблон для проекта на Python с Docker. Комментарий после ## превращается в справку.

make
.DEFAULT_GOAL := help
SHELL := bash
.SHELLFLAGS := -eu -o pipefail -c

PYTHON ?= python3
IMAGE  := myapp
TAG    := $(shell git describe --tags --always --dirty)

.PHONY: help install test lint fmt run image clean

help: ## показать эту справку
	@grep -E '^[a-zA-Z_-]+:.*?## ' $(MAKEFILE_LIST) | \
	  awk 'BEGIN {FS = ":.*?## "}; {printf "  \033[36m%-10s\033[0m %s\n", $$1, $$2}'

install: .venv/done ## поставить зависимости

.venv/done: requirements.txt
	$(PYTHON) -m venv .venv
	.venv/bin/pip install -r requirements.txt
	touch $@

test: install ## запустить тесты
	.venv/bin/pytest -q

lint: install ## проверить стиль
	.venv/bin/ruff check .

fmt: install ## отформатировать код
	.venv/bin/ruff format .

run: install ## запустить локально
	.venv/bin/python -m app

image: ## собрать Docker-образ
	docker build -t $(IMAGE):$(TAG) .

clean: ## удалить временные файлы
	rm -rf .venv .pytest_cache build
text
$ make
  help       показать эту справку
  install    поставить зависимости
  test       запустить тесты
  ...

Обратите внимание на цель .venv/done. Это файл-метка: он появляется после установки зависимостей, и Make повторит установку только тогда, когда requirements.txt станет новее метки. Так make test не тратит время на pip install при каждом запуске. Про сборку образов есть отдельная шпаргалка по Docker.

Условия и включения

make
ifeq ($(OS),Windows_NT)
  EXE := .exe
else
  EXE :=
endif

ifdef CI
  TEST_FLAGS += --no-header -p no:cacheprovider
endif

-include .env.mk     # подключить, если файл есть; минус — не ругаться, если нет

Ключи запуска

КомандаЧто делает
makeпервая цель или .DEFAULT_GOAL
make test lintнесколько целей по порядку
make -n targetпоказать команды, ничего не выполняя
make -B targetпересобрать всё, не глядя на даты
make -j8выполнять до восьми независимых рецептов параллельно
make -C dirперейти в каталог и выполнить там
make -kпродолжать другие цели после ошибки
make -pвывести все правила и переменные, включая встроенные
make VAR=valueпереопределить переменную

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

Частые ошибки

  • missing separator — пробелы вместо табуляции в начале команды.
  • No rule to make target 'x', needed by 'y' — зависимости нет как файла и нет правила, которое её создаёт. Часто это опечатка в имени файла.
  • Nothing to be done for 'test' — цель не объявлена в .PHONY, а в каталоге есть файл или папка с таким именем.
  • make: *** [Makefile:12: test] Error 1 — упала команда в строке 12; сама ошибка напечатана выше.
  • Переменная оболочки пустая — забыли удвоить $.