Справочник · Код и сборка
Make: Makefile для проекта
Цели, зависимости и правила, переменные и автоматические переменные, .PHONY, шаблонные правила и самодокументирующийся Makefile, который запускает тесты, сборку и линтеры одной командой.
Make придумали в 1976 году для сборки программ на C, но он по-прежнему удобен как единая точка входа в проект: make test, make lint, make run работают одинаково на любой машине и в CI. Ниже GNU Make, который стоит в Linux и ставится в macOS.
Правило
Makefile состоит из правил:
цель: зависимости
команды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 решит, что цель уже готова, и ничего не запустит.
.PHONY: test lint clean
test:
pytest -q
lint:
ruff check .
clean:
rm -rf build dist .pytest_cacheПеременные
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) | каталог цели |
build/%.html: content/%.md
@mkdir -p $(@D)
pandoc $< -o $@Это шаблонное правило: для любого build/имя.html Make найдёт content/имя.md. Осталось задать список целей:
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 на одной строке не влияет на следующую:
# не работает: второй строкой мы снова в корне проекта
wrong:
cd frontend
npm run build
# работает
right:
cd frontend && npm run buildЕсли нужно, чтобы весь рецепт шёл в одной оболочке, в начале файла пишут .ONESHELL:. Там же полезно включить строгий режим bash, как в обычных скриптах:
SHELL := bash
.SHELLFLAGS := -eu -o pipefail -cСамодокументирующийся Makefile
Рабочий шаблон для проекта на Python с Docker. Комментарий после ## превращается в справку.
.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$ make
help показать эту справку
install поставить зависимости
test запустить тесты
...Обратите внимание на цель .venv/done. Это файл-метка: он появляется после установки зависимостей, и Make повторит установку только тогда, когда requirements.txt станет новее метки. Так make test не тратит время на pip install при каждом запуске. Про сборку образов есть отдельная шпаргалка по Docker.
Условия и включения
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; сама ошибка напечатана выше.- Переменная оболочки пустая — забыли удвоить
$.