Руководство по make

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

В большинстве проектов, будь то разработка ПО, написание книги или публикация записи в блоге, в какой-то момент приходится генерировать «конечные продукты» из вручную написанных исходных файлов, то есть осуществлять сборку.

Суть автоматической сборки состоит в том, чтобы взять за основу генерацию какого-то одного элемента, вычленить правила, по которым она происходит, и применить их к большему количеству элементов такого же типа, обновляя только изменяемые параметры.

Это может быть сделано (и часто делается) под определенную задачу с помощью кастомных скриптов, но мы в данной статье рассмотрим, как можно удобно автоматизировать сборку, используя утилиту make и её встроенные правила.

Почему стоит использовать утилиту make

  • она работает;
  • легко настраивается как для новых, так и для существующих проектов;
  • в большинстве ОС она предустановлена, если нет — её легко скачать;
  • она крошечная и содержит мало зависимостей;
  • make-файлы всё-таки могут быть короткими, ёмкими и красивыми;
  • она не использует загадочные папки типа working или resource;
  • да и вообще темной магией не занимается — всё на виду.

Создадим файл и назовем его makefile или Makefile. Содержание стандартного make-файла можно описать так: «если любой из файлов-пререквизитов был изменен, то целевой файл должен быть обновлен». Суть make в том, что нам нужно по определенным правилам произвести какие-то действия с пререквизитами, чтобы получить некую цель.

Правила, которые и составляют основу make-файла, по сути являются командами и могут быть сколь угодно сложными, а также могут содержать в себе вызов других инструментов, таких как компиляторы и парсеры.

Базовый синтаксис для определения цели (в файле makefile):

цель: реквизит1 реквизит2 ...
команда1
команда2
...
<пустая строка>

Важно Индентация производится с помощью табуляции, а не пробелов.

В командах описывается, что make должна сделать, чтобы создать целевой файл. Они исполняются, когда мы делаем запрос на создание или обновление целевого файла и make заключает, что пререквизиты изменились или целевой файл еще не существует.

Часто нам нужно обрабатывать несколько файлов одного вида по одним и тем же правилам. Например, при создании страниц HTML на основе размеченного текста. Это делается с помощью шаблонных правил, наличие которых является отличительной чертой make в сравнении с обычной сборкой через командную строку.

Шаблонные правила работают на основе сопоставления расширений файлов. Например, make знает, как создавать объектные файлы *.o из исходных C-файлов *.c, компилируя их и передавая компилятору флаг -c. В make есть несколько встроенных шаблонных правил, самые известные из которых используются для компиляции кода на C и C++.

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

В большинстве случаев мы можем даже опустить пререквизиты: внутренние правила make подразумевают, что для того, чтобы, например, собрать somefile.o по принципу Исходник на C → Объектный файл, нам нужен somefile.c.

Будьте аккуратны: когда вы предлагаете make свой список команд, она будет ориентироваться только на ваш код и в данном случае не будет искать шаблонные правила для сборки цели.

Вызываем make

Запустим make в текущей директории:

make

Если make-файл в ней уже есть, будет создана (собрана) первая цель, которую make сможет найти. Если make-файла нет (или он есть, но в нем нет целей), make об этом сообщит.

Чтобы обратиться к конкретной цели, запустите:

make [цель]

Здесь цель — это название цели (без квадратных скобок).

make может догадаться, что делать, используя встроенные правила, без дополнительной информации от пользователя. Попробуйте создать файл test.c в пустой папке и запустить make. test.make скомпилирует test.c, используя встроенное шаблонное правило, и соберет цель с названием test. Это сработает даже через несколько шагов генерации промежуточных файлов.

Специальные цели

В большинстве make-файлов можно найти цели, называемые специальными. Вот самые распространенные:

  • all — собрать весь проект целиком;
  • clean — удалить все сгенерированные артефакты;
  • install — установить сгенерированные файлы в систему;
  • release или dist — для подготовки дистрибутивов (модули и тарболы).

Они не обязательно должны присутствовать в make-файле, но большинство сборочных процессов странно представить без хотя бы первых трех.

При сборке этих целей не будут созданы файлы с именами, например, all или clean. make обычно решает, запускать ли какие-либо процессы, основываясь на данных о том, нужно ли изменять целевой файл. Создание файлов с подобными именами не даст make произвести никаких изменений с целями.

Для предотвращения этого GNU make позволяет помечать такие цели как «фиктивные» (phony), чтобы запускать их в любом случае. Сделать это можно, добавив необходимые цели в качестве пререквизитов во внутреннюю цель .PHONY следующим образом:

.PHONY: all clean run

Цель all обычно просто содержит главный исполняемый файл в пререквизите — иногда с дополнительными командами для пост-обработки генерируемых файлов. Так как мы в большинстве случаев хотим, чтобы цель all исполнялась по умолчанию, она должна стоять первой в файле.

Для цели clean стоит добавить список команд, удаляющих все генерируемые файлы. Это может быть легко сделано с использованием переменных, о которых мы поговорим в следующем разделе.

Переменные и функции

Как и в большинстве языков программирования, make-файлы можно сделать более читабельными, используя переменные. make импортирует свою среду, позволяя нам определять переменные внутри файла через внешние источники.

Основные операции

Определять переменные и ссылаться на них можно следующим образом:

NAME = value
FOO = baz
bar = $(FOO) frob

Ссылаться на переменные можно через $(NAME) или ${NAME}. Если опустить скобки, make сочтет за имя переменной только первый символ. Присоединение осуществляется при помощи оператора +=. Можно также задать условные переменные с помощью ?= (если им еще не присвоены значения).

Наконец, большинство реализаций make позволяют нам задать выходную переменную с помощью оператора != при порождении одного подпроцесса за операцию.

Передача аргументов встроенным шаблонным правилам

Ранее мы видели некоторые шаблонные правила в действии, правда, во всех случаях они запускали одни и те же базовые команды. Конечно, требования к сборке не всегда одинаковы, и инструменты могут запрашивать разные флаги для разных задач.

Например, какому-то ПО нужно обеспечить соединение с разными библиотеками, или заданные оптимизации различаются для сборки отладки и итоговой сборки.

Поэтому встроенные правила в make включают в себя несколько распространённых переменных в важных местах команд. Мы можем установить их по желанию из make-файла или внешней среды.

Это позволяет, например, запускать один и тот же make-файл с разными компиляторами, снабдив make необходимым именем бинарного файла для выполнения. Так задаётся переменная среды компилятора C:

CC=clang make

Вот некоторые из самых известных переменных, которые вы могли видеть, если когда-нибудь заглядывали в make-файл:

  • $(CC) / $(CXX) — бинарные файлы для компиляторов C и C++, которые make использует для сборки;
  • $(CFLAGS) / $(CXXFLAGS) — флаги, передаваемые компиляторам;
  • $(LDLIBS) — присоединяемые библиотеки.

Программные переменные

make хранит некоторые распространённые программы в переменных. В основном это делается для того, чтобы при необходимости их можно было перезаписать.

Самая важная из них — $(MAKE), которая должна использоваться при рекурсивном вызове make из make-файла. Она принимает во внимание аргументы командной строки из исходного вызова.

В цели clean, главная задача которой — удаление файлов, безопаснее использовать переменную $(RM) вместо прямого вызова rm.

Функции нескольких переменных

GNU make определяет некоторые очень полезные методы, большинство которых работает со словами, то есть с разделенными пробелами строками символов. Это облегчает работу с переменными, содержащими в себе списки файлов.

Функции вызываются таким же образом, как вызывалась бы переменная с таким же именем, но с добавлением аргументов перед закрывающей скобкой.

Вот некоторые наиболее интересные методы:

  • $(wildcard шаблон) возвращает список с названиями файлов, соответствующих шаблону, которые в том числе могут представлять собой относительный путь. Список внутри разделен с помощью пробелов, что проблематично для работы с файлами, содержащими пробелы в названии. Лучше всего избегать таких файлов при работе с make. Шаблон может содержать универсальный символ *;
  • $(patsubst шаблон поиска, шаблон замены, список слов) заменяет все слова в списке, которые соответствуют шаблону поиска в соответствии с шаблоном замены. Оба шаблона используют % в качестве символа;
  • $(filter-out шаблон поиска, список слов) возвращает список всех слов, отфильтрованных по шаблону поиска;
  • $(notdir список слов) возвращает список слов, где имя каждой записи сокращается до основного (то есть если имя содержит название директории, то оно отфильтровывается);
  • $(shell команда) запускает команду в подпроцессоре и перехватывает стандартный вывод подобно оператору !=. Оболочка для выполнения команды определяется переменной $(SHELL).

Подробное описание функций можно найти в официальной документации.

Продвинутое использование переменных

Отсылки к переменным можно делать в любом контексте внутри make-файла. Можно даже соорудить имя исполняемого файла внутри списка команд с помощью соединения нескольких переменных. Это позволяет использовать переменные в качестве целей или пререквизитов и создавать простые конструкции типа:

OBJECTS = $(patsubst %.c,%.o,$(wildcard *.c))
all: $(OBJECTS)

Данный make-файл создает список всех исходных C-файлов в директории, заменяет суффикс .c на .o, используя функцию $(patsubst ...), и потом использует этот список файлов в качестве пререквизитов к цели all. При запуске make станет собирать цель all, потому что она определена первой. Так как цель зависит от нескольких объектных файлов, которые могут ещё не существовать или должны быть обновлены, а make знает, как их сделать из исходных C-файлов, все запрашиваемые файлы также будут собраны.

Это становится мощным инструментом в сочетании с шаблонными правилами — мы можем автоматически собирать пререквизиты, которые косвенно используются в качестве новых целей.

Замена суффиксов

Для совместимости с другими реализациями make обеспечивает альтернативный синтаксис при вызове функции $(patsubst ...), называемый «ссылка с заменой» и позволяющий заменить некоторые суффиксы в списке слов на другие.

Make-файл из предыдущего примера можно преобразовать следующим образом:

FILES != echo *.c
OBJS = $(FILES:.c=.o)
all: $(OBJS)

Важно Вместо функции $(wildcard ...) используется оператор !=.

Целезависимые переменные

Это ещё одна интересная особенность, использование которой ведёт к сокращению кода: она позволяет устанавливать переменным разные значения в зависимости от текущей цели.

FOO = bar
target1: FOO = frob
target2: FOO += baz

В этом примере мы бы установили $(FOO) значение bar глобально, значение frob для цели один и значение bar baz для цели два.

Можно использовать любые необходимые операторы присваивания, что позволяет, например, создавать цели с разными наборами флагов для компилятора, просто присвоив переменной $(CFLAGS) разные значения.

Интеграция с внешними процессами

В связи с тем, что дистрибутивы Linux в большинстве случаев поставляются с системой управления пакетами и многие пакеты создаются из проектов, использующих make для сборки, были приняты некоторые общие правила использования переменных.

В основном они касаются установки целей проекта, так как бинарные пакеты часто состоят из результатов запуска make install, упакованных в распространённом формате. Важно запомнить следующие переменные:

  • $(DESTDIR) должна быть пустой по умолчанию и никогда не должна задаваться из make-файла (режим чтения). Используется составителями пакета для вставки пути доступа перед устанавливаемыми файлами;
  • $(PREFIX) — значение этой переменной в вашем make-файле должно соответствовать /usr/local или другому заданному вами пути. Она позволяет пользователю пакета задать желаемую директорию для установки. Задавайте значение этой переменной, только если оно не было передано окружением (используя оператор ?=).

Определение шаблонных правил

Синтаксис для написания шаблонных правил во многом похож на обычный синтаксис целей. Основное отличие состоит в использовании шаблонного символа % в основе цели (её имени до расширения), который и будет применяться для поиска соответствий.

Шаблон также может быть использован в списке пререквизитов, где он будет заменён на основу для формирования неявно определённых пререквизитов. (Список пререквизитов в случае чего можно расширить и явно определёнными.)

Также можно перезаписать любое из встроенных правил с помощью определения правила с такой же целью и пререквизитами.

Так как шаблонное правило должно быть написано таким образом, чтобы отличаться от реальных имён файлов, с которыми оно вызывается, make предоставляет специальные переменные для определения шаблонных правил.

Динамические переменные

Все имена динамических переменных состоят из одного знака, поэтому скобки для ссылки на них не нужны. Таких переменных довольно много, рассмотрим наиболее необходимые:

  • $@ — полное название цели;
  • $< — имя первого пререквизита (в том числе косвенно сгенерированного поиском по шаблону);
  • $^ — разделенный пробелами список всех пререквизитов (версия GNU).

Шаблонное правило, конвертирующее размеченные файлы в HTML с использованием markdown, получает такой вид:

%.htm : %.md 
    markdown $^ > $@

Итоги

Ранее мы разобрали некоторые из наиболее трудных аспектов в контексте make-файлов. Перед вами относительно сложный, но тем не менее полезный make-файл, использующийся для статей на сайте автора:

.PHONY: all clean
ARTICLES = $(patsubst %.md,%.htm,$(wildcard *.md))

%.htm : %.md index.htm
./generate_article.py $&lt; &gt; $@

all: $(ARTICLES)

clean:
$(RM) $(ARTICLES)

Скрипт generate_article.py реализует минималистичный шаблонизатор, используя index.htm в качестве базы для вставки HTML, сгенерированного из входных файлов. Присутствие шаблонизатора в пререквизитах шаблонного правила обеспечивает, что изменения в шаблонизаторе вызовут изменения всех файлов, относящихся к статье.

Для дальнейшего изучения make рекомендуем ознакомиться с официальным руководством.

По материалам статьи «Make Files Not War»

Содержание

Introduction
Установка
Проверить версию
Для чего используются Makefiles
Формат
.PHONY:
Посмотреть цели Make-файла
Пример из C++
Переменные
Docker из Makefile
Параметризация Make
BUILD_ID
USER_ID
Альтернативы
$$: Вызов bash команд (например whoami)
: Игнорировать ошибки
Цель из других целей
Несколько make-файлов в одной директории
Связанные статьи

Установить make

sudo apt install make

или для rpm

sudo yum install make

Так как make входит в состав build-essentials можно установить вместе с этим пакетом

sudo apt install build-essentials

Проверить версию make

/usr/bin/make —version

GNU Make 4.2.1
Built for x86_64-pc-linux-gnu
Copyright (C) 1988-2016 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.

Для чего используются Makefiles

Make-файлы используются, чтобы помочь решить, какие части большой
программы должны быть перекомпилированы.

В подавляющем большинстве случаев компилируются файлы

C

или

C++
.

Другие языки обычно имеют свои собственные инструменты, которые служат той же цели, что и Make.

Его можно использовать и за пределами программ, когда вам нужна серия инструкций для запуска
в зависимости от того, какие файлы изменились.

В этой статье вы узнаете про использование компиляции C/C++.

Вот пример графика зависимостей, который вы можете построить с помощью Make.

Если какие-либо зависимости файла изменятся, то файл будет перекомпилирован:

Граф зависимостей для компиляции изображение с сайта www.andreyolegovich.ru

Граф зависимостей


wikipedia.org

Формат

Makefile состоит из правил (rules).
Первым указывается название цели (target), затем зависимости (prerequisites)
и действие (recipe — набор действий/команд), которое нужно выполнить.

Зависимости нужны не всегда и указываются по необходимости. Для простоты на первом этапе можно
думать о зависимостях как о файлах, которые нужно проверить: если ни один не изменился — заново
компилировать не нужно.

Отступы по умолчанию нужно ставить табуляцией. Если хотите поменять на другой символ — задайте
.RECIPEPREFIX

target: prerequisites
recipe

На русский обычно переводят так

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

Типичное применение: какая-то зависимость изменилась → выполнятеся действие в результате которого
создаётся таргет файл.

output: main.o message.o
g++ main.o message.o -o output

clean:
rm *.o output

Как и в статье

Configure, make, install

в примере выше используются стандартные цели (target)

Про опции -o и -c
читайте статью

«Компиляция в C++

Опция Назначение
-c Указывает компилятору не делать линковку и создавать .o файлы для каждого исходника
-o filename Меняет название output файла со стадартного на указанный
-S Directs the compiler to produce an assembly source file but not to assemble the program.

Дополнительная информация (на

английском
):

gnu.org: Rule-Introduction

Если файл вам не нужен, например, вы просто хотите выполнить какие-то команды — можно
использовать .PHONY

.PHONY

.PHONY: site

site:
echo "HeiHei.ru"

Если теперь выполнить

make site

echo «HeiHei.ru»

HeiHei.ru

Удалите site из первой строки, а всё остальное не трогайте

make site

echo «HeiHei.ru»

HeiHei.ru

Вроде бы ничего не изменилось, но теперь создайте файл

site

рядом с

Makefile

touch site

make site

make: ‘site’ is up to date.

Так как таргет теперь реальный — make не нашёл изменений и ничего не сделал. Из-за такого простого
совпадения имени цели (target) и какого-то файла в директории может перестать работать скрипт.

Для защиты от таких неприятностей и применяют PHONY

Также PHONY удобен тем, что можно перечислить все цели в самом начале файла.

Если не злоупотреблять этой возможностью — можно улучшить читаемость кода, особенно в небольших файлах.

Посмотреть цели Make-файла

Если вы создали Make-файл с большим количеством PHONY целей и забыли название нужно — не обязательно продираться через весь файл

Чтобы получить списко всех целей воспользуйтесь

grep

и выполните

cat GNUmakefile | grep PHONY:

Пример из C++

Рассмотрим пример из статьи о

заголовочных файлах .h

Есть три файла

ls

Functions.cpp Functions.h Main.cpp


Main.cpp

#include <iostream>
#include "Functions.h"

int main() {

double b = add(1.3, 4.5);
cout << "1.3 + 4.5 is " << b << "n";

return 0;
}


Functions.cpp

double add(double x, double y)
{
return x + y;
}


Functions.h

#pragma once

double add(double x, double y);

Если один из этих файлов изменился — нужно перекомпилировать проект. Для начала будем пользоваться командой

g++ -o output Main.cpp Functions.cpp

Эта команда сначала вызывает компиляцию, затем линковку

Создайте

Makefile

и откройте его в текстовом редакторе. Например, в

Vim

touch Makefile

vi Makefile


Makefile

будет выглядеть следующим образом

output: Main.cpp Functions.cpp Functions.h
g++ -o output Main.cpp Functions.cpp

Теперь для компиляции достаточно выполнить

make output

Или просто

make

В результате появится исполняемый файл

output

В этот пример можно добавить ещё два шага: отдельно следить за компиляцией и убираться после работы.

Если вам не понятно что происходит в этом файле — изучите статью

«Компиляция в C++

.PHONY: clean

output: Main.o Functions.o
g++ Main.o Functions.o -o output

Main.o: Main.cpp
g++ -c Main.cpp

Functions.o: Functions.cpp
g++ -c Functions.cpp

clean:
rm *.o output

To запустить скрипт, достаточно выполнить

make

g++ -c Main.cpp

g++ -c Functions.cpp

g++ -o output Main.o Functions.o

Если нужно скомпилировать Main execute

make Main.o

g++ -c Main.cpp

ls

Появится файл
Main.o
но не появятся остальные (Functions.o, output)

Functions.cpp Functions.h Main.cpp Main.o Makefile

На примере команды make Main.o можно понять почему в Make-файлах используется термин цели (target)

make

Main.o

говорит — создай файл

Main.o

а инструкция в Makefile определяет правило по которому это нужно сделать.

Если теперь выполнить make

Main.o

не будет перекомпилироваться. Будут выполнены только последние два шага.

g++ -c Functions.cpp

g++ -o output Main.o Functions.o

Выполните make если ещё не выполняли и не делайте после этого clean

Добавим ещё одну функцию в наш проект. Нужно указать её в файлах Functions.*

Вызывать пока не будет, поэтому

Main.cpp

остаётся без изменений


Functions.cpp

bool test(bool x)
{
return x;
}


Functions.h

bool test(bool x);

make

g++ -c Functions.cpp
g++ -o output Main.o Functions.o

Обратите внимание:

Main.cpp

не был перекомпилирован так как в нём нет изменений.

Таже посмотрите на время изменения файла

output

оно должно измениться.

Не вносите никаких изменений в файлы и execute

make

make: ‘output’ is up to date.

Перекомпиляция не нужна и поэтому не выполнена

Переменные

Подробнее про переменные в Makefile читайте в статье

Работа с переменными в GNUmakefile

В этом примере вы можете увидеть как названия файлов сохранены в переменную для сокращения кода.

.PHONY: clean

objects = Main.o Functions.o

output: $(objects)
g++ -o output $(objects)

Main.o: Main.cpp
g++ -c Main.cpp

Functions.o: Functions.cpp
g++ -c Functions.cpp

clean:
rm *.o output

Запустить Docker container из Makefile

.PHONY: docker

docker:
docker-compose -f docker/dev/docker-compose.yml build

Параметризация Make

?= позволяет переменным быть перезаписанными на существующие переменные окружения

:= перезаписывает значение переменной

PROJECT_NAME ?= myproject
ORG_NAME ?= heihei
REPO_NAME ?= myproject

#Filenames
DEV_COMPOSE_FILE := docker/dev/docker-compose.yml
REL_COMPOSE_FILE := docker/release/docker-compose.yml

.PHONY: test release

test:
docker-compose -f $(DEV_COMPOSE_FILE) build
docker-compose -f $(DEV_COMPOSE_FILE) up agent
docker-compose -f $(DEV_COMPOSE_FILE) up test

release:
docker-compose -f $(REL_COMPOSE_FILE) build
docker-compose -f $(REL_COMPOSE_FILE) up agent
docker-compose -f $(REL_COMPOSE_FILE) run --rm app manage.py collectstatic --noinput
docker-compose -f $(REL_COMPOSE_FILE) run --rm app manage.py migrate --noinput
docker-compose -f $(REL_COMPOSE_FILE) up test

clean:
docker-compose -f $(DEV_COMPOSE_FILE) kill
docker-compose -f $(DEV_COMPOSE_FILE) rm -f
docker-compose -f $(REL_COMPOSE_FILE) kill
docker-compose -f $(DEV_COMPOSE_FILE) rm -f

BUILD_ID

To добавить переменным уникальности используют BUILD_ID

# Docker Compose Project Names
REL_PROJECT := $(PROJECT_NAME)$(BUILD_ID)
DEV_PROJECT := $(REL_PROJECT)dev

USER_ID

To получить ID пользователя запустившего GNUmakefile

USER_ID = $(shell id -u ${USER})

Какие альтернативы Make существуют

Популярными альтернативными системами сборки C/C++ являются
SCons, CMake, Bazel и Ninja. Некоторые редакторы кода, такие как

Microsoft Visual Studio

, имеют свои собственные встроенные инструменты сборки.

Для

Java

есть Ant,

Maven

и Gradle.

Другие языки, такие как

Go

и Rust, имеют свои собственные инструменты сборки.

Интерпретируемые языки, такие как

Python
,

Ruby

и

JavaScript

, не требуют аналога для создания файлов.

Цель Makefile состоит в том, чтобы скомпилировать любые файлы, которые
должны быть скомпилированы, основываясь на том, какие файлы изменились.

Но когда файлы на интерпретируемых языках меняются, ничего не нужно перекомпилировать.

При запуске программы используется самая последняя версия файла.

Что означает cc -c

cc это C compiler

Существует несколько общедоступных компиляторов C

В этой статье использовался

gcc

-c это опция, которую разбирали

здесь

whoami

В обычном

Bash скрипте

достаточно написать $(whoami) и это будет равносильно подстановке вывода whoami

В Make файле это может не получиться. Есть два варианта решить проблему

`whoami`

И

$$(whoami)

Игнорировать ошибки

Если какая-то команда выполнена с ошибкой выполнение сценария прерывается.

Рассмотрим пример

RPM_DIR=/home/$$(whoami)/rpms/

.PHONY: clean-repo
clean-repo:
@sudo rm $(RPM_DIR)release/*
@sudo rm $(RPM_DIR)master/*

Если в …release/ пусто, то удалять в …master/ make уже не будет.

Вместо этого появится ошибка:

sudo rm /home/$(whoami)/rpms/release/*
rm: cannot remove ‘/home/andrei/rpms/release/*’: No such file or directory
make: *** [clean-repo] Error 1

Избежать этой проблемы можно поставив — перед командой

RPM_DIR=/home/$$(whoami)/rpms/

.PHONY: clean-repo
clean-repo:
@-sudo rm $(RPM_DIR)release/*
@-sudo rm $(RPM_DIR)master/*

[andrei@localhost ~]$ make clean-repo

rm: cannot remove ‘/home/andrei/rpms/release/*’: No such file or directory

make: [clean-repo] Error 1 (ignored)

make жалуется, но переходит ко второй команде и чистит директорию.

Цель из других целей

Если нужно запустить несколько целей сразу, можно вызывать из новой цели

all-targets: target1 target2 target3

Несколько make-файлов в одной директории

Если в одной директории находится два и более make-файлов с совпадающими целями, вызывать
из нужного файла помогает опция -f

Пример проекта

make
├── GNUmakefile.beget
└── GNUmakefile.heihei

# GNUmakefile.beget
.PHONY: url
url:
echo «https://beget.com»

# GNUmakefile.heihei
.PHONY: url
url:
echo «https://heihei.ru»

make -f GNUmakefile.beget url

echo «https://beget.com»
https://beget.com

make -f GNUmakefile.heihei url

echo «https://heihei.ru»
https://heihei.ru

Похожие статьи

make
Основы make
PHONY
CURDIR
shell
wget + make
Переменные в Make файлах
ifeq: Условные операторы
filter
-c: Компиляция
Linux
Bash
C
C++
C++ Header файлы
Configure make install
DevOps
Docker
OpenBSD
Errors make

Содержание

Назначение, история, варианты

Утилита make предназначена для автоматизации сборки проектов. Если какие-либо файлы проекта могут быть сгенерированы из других файлов, утилита позволяет выполнить процесс построения наиболее оптимальным способом, по возможности минимизируя количество обрабатываемых файлов.

Исторически утилита предназначалась для сборки проектов на языке C в операционной системе Unix, однако может быть использоваться для работы с любыми проектами. Первая версия системы была создана в 1977 году.

На сегодняшний день наиболее распространены три варианта утилиты, объединенные общими принципами работы, но отличающиеся синтаксисом языка и возможностями:

  • GNU make — самый распространенный и функциональный вариант

  • BSD make (pmake) — используется в проектах BSD, по функциональности примерно соответствует GNU make

  • nmake (Microsoft make) — работает под Windows, малофункционален, только базовые принципы make.

Мы работаем с GNU make. На BSD системах (в частности, FreeBSD, он может быть доступен как gmake, на Linux — просто make).

Основные принципы

Утилита make работает по правилам (rules), записанным в специальном конфигурационном файле. Правила определяют цели (targets), завимости между целями и набор команд для выполнения каждой цели.

Цели могут соответствовать определенным файлам. Кроме того, цели могут не соответствовать ни одному файлу и использоваться для группировки других целей или определенной последовательности команд. Такие цели называются phony targets.

Каждая цель может зависеть от выполнения других целей. Выполнение цели требует предварительного выполнения других целей, от которых она зависит.

В случае зависимости между целями, соответствующими файлам, цель выполняется только в том случае, если файлы, от которых она зависит, новее, чем файл, соответствующий цели. Это позволяет перегенерировать только файлы, зависящие от измененных файлов, и не выполнять потенциально долгий процесс пересборки всех файлов проекта.

Таким образом, makefile определяет граф зависимостей, по которому утилита make выполняет ту или иную цель, по возможности минимизируя количество операций сборки.

Запуск make

Несмотря на то, что для make можно указать произвольный файл правил, как правило используют стандартное имя Makefile. Поддерживаются также несколько альтернативных имен по умолчанию, но имеет смысл использовать наиболее распространенное.

Соответственно, команда

$ make

будет использовать файл Makefile, находящийся в текущем каталоге.

При запуске make можно указать цель, которая будет выполнена. Если цель не указана, используется цель по умолчанию, которая либо указана в файле правил явно, либо неявно используется первая определенная цель.

Явное указание цели выполняется инструкцией DEFAULT_GOAL в Makefile:

.DEFAULT_GOAL: all

Например, команда

$ make clean

вызовет обработку цели clean файла Makefile, находящегося в текущем каталоге.

Можно указать сразу несколько целей.

Выполнение целей может быть настроено с использованием переменных (о которых ниже). При запуске make можно указать значения переменных:

$ make build PREFIX=/usr/local

Значение переменной PREFIX будет доступно в правилах Makefile и может быть использовано при сборке.

Команда поддерживает также ряд дополнительных опций, из которых наиболее важные следующие:

  • -f — позволяет явно указать файл правил

  • -C — переходит в указанный каталог перед выполнением, может быть, например, использована для запуска make из внешнего каталога по отношению к каталогу проекта

  • -B — отключает проверку времени зависимых целей и принудительно выполняет их полностью

Базовый синтаксис make

Основная конструкция, используемая в файлах make, выглядит следующим образом:

target: dep1 dep2 ...
  command1
  command2
  ...

Здесь:

  • target — цель

  • dep1, dep2 — цели, от которых зависит цель target

  • command1, command2 — команды, выполняемые для достижения цели target

Например:

style.css: src/less/app.less
  lessc src/less/app.less > style.css

Этот фрагмент определяет, что файл style.css зависит от файла src/less/app.less и для его сборки необходимо выполнить команду lessc src/less/app.less > style.css. Перегенерация файла style.css будет выполняться только в случае,если файл src/less/app.less новее, чем файл style.css (до тех пор, пока при запуске make не будет указан ключ -B).

Перед каждой командой внутри описания цели должен присутствовать символ табуляции. В принципе, это настраивается, но лучше использовать общепринятые соглашения. Если вместо табуляции используются пробелы, make работать не будет.

В качестве команд обработки целей используются команды shell. Текст команды выводится, для того, чтобы он не выводился, команду необходимо начать с символа @.

Каждая команда запускается в отдельном интерпретаторе shell, таким образом, команды не связаны друг с другом. Иначе говоря, одна строка команды — один shell. Это поведение может быть переопределено с помощью специальной цели .ONESHELL.

Если команду (или список зависимостей) необходимо записать в несколько строк, используют символ переноса .

PHONY targets

Цели, не соответствующие файлам, и предназначенные для выполнения набора команд или группировки завимостей, декларируются следующим образом:

.PHONY: clean
clean:
  rm *.o temp

Деклараций .PHONY может быть несколько, обычно определяют одну и прописывают туда все соответствующие цели.

В нашем примере вызов make clean приведет к выполнению цели clean, которая безусловно выполнит удаление временных файлов.

В случае, если у phony target есть зависимость в виде другой phony target, то зависимость выполняется перед зависящей целью. Таким образом, мы получаем механизм, напоминающий подпрограммы. Например, мы можем определить цель all, собирающую все файлы проекта, и отдельные цели css, js и php, собирающие отдельной css-файлы, js-файлы и обрабатывающие php файлы.

Соответственно, в Makefile мы можем написать:

.PHONY: all css js php
all: css js php
css: www/style.css
  ... тут команды
js:  www/js/app.js
  ... тут еще команды
php:
  ... тут снова команды

В результате мы можем использовать make all для пересборки всех файлов и, скажем, make css для пересборки только CSS-файлов.

Переменные

В make-файле можно использовать переменные, хотя правильнее сказать, что можно использовать макросы.

Переменные определяются присваиванием в makefile или могут быть переданы извне.

Переменные — это макроопределения, причем вычисление переменной всегда выполняется в самый последний момент перед подстановкой. Макросы могут использовать везде в тексте makefile.

СС=gcc
IDIR=../include
CFLAGS=-I$(IDIR)
DEPS=hellomake.o hellofunc.p
NAME=hellomake
 
$(NAME): $(DEPS)
  $(CC) -o $(NAME) $(DEPS)

Подстановка выполняется конструкцией $(VAR) в отличие от shell, где используется $VAR.

Если в shell команде используется shell-переменная, необходимо квотить знак $, дублируя его, например:

printhome:
  echo $$HOME

Помимо макропеременных существуют и более традиционные, в которых значение устанавливается сразу. Для работы с ними используется оператор :=. В наших условиях достаточно использовать обычные переменные.

Часто требуется определить переменную только в том случае, если она еще не была определена. Для этого используется оператор ?=:

MYSQL_CHARSET ?= UTF8

Соответственно, если мы вызовем

make create-database 

будет использована кодировка UTF8, а в случае

make create-database MYSQL_CHARSET=CP1251

будет использована CP1251.

Если переменная содержит несколько строк, можно использовать синтаксис define:

define MYSQL_APP_CONF_TEMPLATE
  [mysql]
  host=$(MYSQL_SERVER)
  port=$(MYSQL_PORT)
  user=$(MYSQL_USER)
  password='$(MYSQL_PASSWORD)'
endef

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

Make поддерживает набор автоматических переменных, облегчающих написание правил. Например, переменная $@ соответствую текущей цели (то, что слева от :), а переменная $^ — списку зависимостей (то, что справа от :). Таким образом, например, можно написать:

www/js/script.js: src/js/jquery.js src/js/plugin1.js src/js/plugin2.js
  cat $^ > $@

В результате www/js/script.js будет результатом объединения трех js-файлов.

Полный список таких переменных приведен в документации, для нас наиболее интересны:

  • $@ — имя цели

  • $< — имя первой зависимости

  • $? — имена всех зависимостей, которые новее чем цель

  • $^ — имена всех зависимостей цели

С полным списком можно ознакомиться в документации: Automatic Variables.

Условное выполнение

В Makefile можно использовать условные выражения. Опять же, мы говорим о макрообработке make, соответственно, условные выражения работают на уровне makefile, а не на уровне команд. Обычно условные выражения используются для определения тех или иных целей в зависимости от значения переменных. Например:

ifdef $(APP)
  setup:
        ...
else
  setup:
    @echo "Error, applications is not defined"
endif

В качестве условий можно проверять определенность переменной, а также ее значение:

foo: $(objects)
  ifeq ($(CC),gcc)
    $(CC) -o foo $(objects) $(libs_for_gcc)
  else
    $(CC) -o foo $(objects) $(normal_libs)
  endif

Полностью с возможностями условных выражений можно ознакомиться в документации: Conditional syntax.

Шаблонные правила

Шаблонные правила (pattern rules) позволяют указать правило преобразования одних файлов в другие на основании зависимостей между их именами. Например, мы можем указать правило для получения объектного файла из файла на языке C:

%.o: %.c
  $(CC) $< -o $@

Обратите внимание на использование переменной %<, которая в таких правилах используется для получения имени исходного файла.

Шаблоны не обязаны ограничиваться расширениями файлов. Если исходные и выходные файлы соответствуют друг другу и в их именах есть какая-либо зависимость, можно использовать pattern rule.

Включение других файлов make

Файл make может подключить другие файлы make оператором include:

include make1.mk make2.mk

Таким образом, из файлов можно строить модульную систему, часто имеет смысл выполнять include внутри условного оператора.

Функции

Make определяет большой набор функций, которые могут быть использованы в переменных (макросах). Вызов функции выполняется конструкцией:

$(function arg1,arg2,...)

Функции позволяют обрабатывать строки, имена файлов, организовывать циклы по набору значений, организовывать условный вызов других функций.

Несколько примеров из hive. Получаем текущее время (обратите внимание на использование :=:

HIVE_TIME := $(shell date +%Y/%m/%d %H:%M:%S)

Включение файла container.mk только в случае, если он существует:

include $(shell find $(HIVE_ETC_DIR) -name container.mk)

Формирование имени MySQL базы по имени проекта:

MYSQL_DB ?= $(subst -,_,$(HIVE_NAME)_$(APP))

Добавление префиксов и суффиксов к именам файлов

$(addprefix src/less/,$(addsuffix .less, app ui catalog))

Подробнее о функциях можно прочитать в документации Functions.

Собственные функции

Можно создавать собственные параметризованные функции путем определения переменных, содержащих специальные переменные $1, $2, …, соответствующие переданным аргументам. Вызов пользовательской функции производится специальным макросом call:

$(call variable,param,param,...)

Очень тупой пример:

hive_red = "33[1;31m$133[0m"
define hive_error
  (echo $(call hive_red,error: $1) && exit 1)
endef

Теперь можно написать:

ifndef HIVE_URI
    @$(call hive_error,undefined container domain)
endif

Рекурсивный make

Помимо включения другого файла make, Makefile может выполнить другой файл make в виде отдельного make-процесса.

Это достаточно частая ситуация в случае модульных проектов. Например, для каждой из компонент проекта (сервер, клиент, библиотеки) предусмотрен свой Makefile, при этом есть основной Makefile, задающий набор общих настроек и вызывающий Makefile каждого подпроекта.

Вызов make из Makefile часто называют submake. Для вызова используется переменная $(MAKE):

libraries:
  cd libs && $(MAKE)

Значение этой переменной соответствует пути к программе make, обрабатывающей текущий файл.

Для передачи переменных в вызываемый таким образом файл их необходимо явно экспортировать:

export PREFIX=/some/path
subsystem:
  cd subsystem && $(MAKE)

Значение переменной PREFIX будет доступно в subsystem/Makefile.

Параллельный make

Make умеет распараллеливать выполнение правил. Для этого используется опция -j, позволяющая указать количество используемых параллельных процессов. Помимо ускорения процесса сборки эта особенность иногда позволяет реализовывать на коленке очень простые сценарии обработки, подразумевающие многозадачность. Например, можно реализовать простой, но работоспособный менеджер очередей на make, используя файлы для хранения заданий и каталоги для хранения файлов заданий на разных стадиях (ожидание, выполнение, результат).

Параллельное выполнение может быть запрещено с помощью специальной цели .NOTPARALLEL.

Специальные цели

Make поддерживает набор целей, которые могут быть использованы как модификаторы для последующих целей либо как служебные инструкции. Вот некоторые из таких специальных целей:

  • .PHONY — определяет набор phony targets;

  • .DEFAULT — определяет цель, которая будет вызываться, если не найдена зависимая цель, необходимая для выполнения другой цели;

  • .IGNORE — указывает цели, для которых необходимо игнорировать ошибки при выполнении цели;

  • .SILENT — определяет набор целей, для которых необходимо подавлять вывод команд, выполняющих цель;

  • .NOTPARALLEL — запрещает параллельное выполнение makefile;

  • .ONESHELL — выполняет набор команд цели в одном процессе shell.

Варианты использования

Чаще всего о make говорят в контексте сборки программ на C/C++, в конце концов, для этого он изначально предназначался. Однако, make ­— гораздо более универсальный инструмент. Записывая makefile, мы декларативно описываем определенное состояние отношений между файлами, которое каждый запуск make будет стараться поддерживать. Декларативный характер определения состояния очень удобен, в случае использования какого-либо императивного языка (например, shell) нам приходилось бы выполнять большое количество различных проверок, получая на выходе сложный и запутанный код.

Кроме того, использование зависимостей между phony targets, позволяющии, по сути, декларативно описывать некий (ограниченный) конечный автомат, может быть полезно для написания различных административных сценариев. Используя make в качестве каркаса для выполнения различных shell-команд, мы получаем по сути некий базовый framework для shell с готовым пользовательским интерфейсом (вызов make + передача переменных), встроенными средствами отслеживания зависимостей, параллельного выполнения, макроопределениями и т.д.

Поэтому базовое знание make позволяет в ряде случаев решить проблему пусть и не самым красивым, но зато быстрым и достаточно надежным способом.

MAKE(1) User Commands MAKE(1)

NAME

make — GNU Make utility to maintain groups of programs

SYNOPSIS

make [OPTION]… [TARGET]…

DESCRIPTION

The make utility will determine automatically which pieces
of a large program need to be recompiled, and issue the commands to
recompile them. The manual describes the GNU implementation of make,
which was written by Richard Stallman and Roland McGrath, and is currently
maintained by Paul Smith. Our examples show C programs, since they are very
common, but you can use make with any programming language whose
compiler can be run with a shell command. In fact, make is not
limited to programs. You can use it to describe any task where some files
must be updated automatically from others whenever the others change.

To prepare to use make, you must write a file called the
makefile that describes the relationships among files in your
program, and provides commands for updating each file. In a program,
typically the executable file is updated from object files, which are in
turn made by compiling source files.

Once a suitable makefile exists, each time you change some source
files, this simple shell command:

make

suffices to perform all necessary recompilations. The make
program uses the makefile description and the last-modification times of the
files to decide which of the files need to be updated. For each of those
files, it issues the commands recorded in the makefile.

make executes commands in the makefile to update one
or more targets, where target is typically a program. If no
-f option is present, make will look for the makefiles
GNUmakefile, makefile, and Makefile, in that order.

Normally you should call your makefile either makefile or
Makefile. (We recommend Makefile because it appears
prominently near the beginning of a directory listing, right near other
important files such as README.) The first name checked,
GNUmakefile, is not recommended for most makefiles. You should use
this name if you have a makefile that is specific to GNU Make, and will not
be understood by other versions of make. If makefile is ‘-‘,
the standard input is read.

make updates a target if it depends on prerequisite files
that have been modified since the target was last modified, or if the target
does not exist.

OPTIONS

-b, -m
These options are ignored for compatibility with other versions of
make.
-B,
—always-make
Unconditionally make all targets.
-C dir,
—directory=dir
Change to directory dir before reading the makefiles or doing
anything else. If multiple -C options are specified, each is
interpreted relative to the previous one: -C / -C etc is
equivalent to -C /etc. This is typically used with recursive
invocations of make.
-d
Print debugging information in addition to normal processing. The
debugging information says which files are being considered for remaking,
which file-times are being compared and with what results, which files
actually need to be remade, which implicit rules are considered and which
are applied—everything interesting about how make decides what to
do.
—debug[=FLAGS]
Print debugging information in addition to normal processing. If the
FLAGS are omitted, then the behavior is the same as if -d
was specified. FLAGS may be any or all of the following names,
comma- or space-separated. Only the first character is significant: the
rest may be omitted: all for all debugging output (same as using
-d), basic for basic debugging, verbose for more
verbose basic debugging, implicit for showing implicit rule search
operations, jobs for details on invocation of commands,
makefile for debugging while remaking makefiles, print shows
all recipes that are run even if they are silent, and why shows the
reason make decided to rebuild each target. Use none to
disable all previous debugging flags.
-e,
—environment-overrides
Give variables taken from the environment precedence over variables from
makefiles.
-E string,
—eval string
Interpret string using the eval function, before parsing any
makefiles.
-f file,
—file=file, —makefile=FILE
Use file as a makefile.
-i,
—ignore-errors
Ignore all errors in commands executed to remake files.
-I dir,
—include-dir=dir
Specifies a directory dir to search for included makefiles. If
several -I options are used to specify several directories, the
directories are searched in the order specified. Unlike the arguments to
other flags of make, directories given with -I flags may
come directly after the flag: -Idir is allowed, as well as
-I dir. This syntax is allowed for compatibility with the C
preprocessor’s -I flag.
-j [jobs],
—jobs[=jobs]
Specifies the number of jobs (commands) to run simultaneously. If
there is more than one -j option, the last one is effective. If the
-j option is given without an argument, make will not limit
the number of jobs that can run simultaneously.
—jobserver-style=style
The style of jobserver to use. The style may be one of fifo,
pipe, or sem (Windows only).
-k,
—keep-going
Continue as much as possible after an error. While the target that failed,
and those that depend on it, cannot be remade, the other dependencies of
these targets can be processed all the same.
-l [load],
—load-average[=load]
Specifies that no new jobs (commands) should be started if there are
others jobs running and the load average is at least load (a
floating-point number). With no argument, removes a previous load
limit.
-L,
—check-symlink-times
Use the latest mtime between symlinks and target.
-n, —just-print,
—dry-run, —recon
Print the commands that would be executed, but do not execute them (except
in certain circumstances).
-o file,
—old-file=file, —assume-old=file
Do not remake the file file even if it is older than its
dependencies, and do not remake anything on account of changes in
file. Essentially the file is treated as very old and its rules are
ignored.
-O[type],
—output-sync[=type]
When running multiple jobs in parallel with -j, ensure the output
of each job is collected together rather than interspersed with output
from other jobs. If type is not specified or is target the
output from the entire recipe for each target is grouped together. If
type is line the output from each command line within a
recipe is grouped together. If type is recurse output from
an entire recursive make is grouped together. If type is
none output synchronization is disabled.
-p,
—print-data-base
Print the data base (rules and variable values) that results from reading
the makefiles; then execute as usual or as otherwise specified. This also
prints the version information given by the -v switch (see below).
To print the data base without trying to remake any files, use make -p
-f/dev/null
.
-q,
—question
«Question mode». Do not run any commands, or print anything; just return
an exit status that is zero if the specified targets are already up to
date, nonzero otherwise.
-r,
—no-builtin-rules
Eliminate use of the built-in implicit rules. Also clear out the default
list of suffixes for suffix rules.
-R,
—no-builtin-variables
Don’t define any built-in variables.
-s, —silent,
—quiet
Silent operation; do not print the commands as they are executed.
—no-silent
Cancel the effect of the -s option.
-S, —no-keep-going,
—stop
Cancel the effect of the -k option.
-t, —touch
Touch files (mark them up to date without really changing them) instead of
running their commands. This is used to pretend that the commands were
done, in order to fool future invocations of make.
—trace
Information about the disposition of each target is printed (why the
target is being rebuilt and what commands are run to rebuild it).
-v, —version
Print the version of the make program plus a copyright, a list of
authors and a notice that there is no warranty.
-w,
—print-directory
Print a message containing the working directory before and after other
processing. This may be useful for tracking down errors from complicated
nests of recursive make commands.
—no-print-directory
Turn off -w, even if it was turned on implicitly.
—shuffle[=MODE]
Enable shuffling of goal and prerequisite ordering. MODE is one of
none to disable shuffle mode, random to shuffle
prerequisites in random order, reverse to consider prerequisites in
reverse order, or an integer <seed> which enables
random mode with a specific seed value. If MODE is
omitted the default is random.
-W file,
—what-if=file, —new-file=file,
—assume-new=file
Pretend that the target file has just been modified. When used with
the -n flag, this shows you what would happen if you were to modify
that file. Without -n, it is almost the same as running a
touch command on the given file before running make, except
that the modification time is changed only in the imagination of
make.
—warn-undefined-variables
Warn when an undefined variable is referenced.

EXIT STATUS

GNU Make exits with a status of zero if all makefiles were
successfully parsed and no targets that were built failed. A status of one
will be returned if the -q flag was used and make determines
that a target needs to be rebuilt. A status of two will be returned if any
errors were encountered.

SEE ALSO

The full documentation for make is maintained as a Texinfo
manual. If the info and make programs are properly installed
at your site, the command

info make

should give you access to the complete manual.

BUGS

See the chapter «Problems and Bugs» in The GNU Make
Manual
.

This manual page contributed by Dennis Morse of Stanford
University. Further updates contributed by Mike Frysinger. It has been
reworked by Roland McGrath. Maintained by Paul Smith.

COPYRIGHT

Copyright © 1992-1993, 1996-2023 Free Software Foundation,
Inc. This file is part of GNU Make.

GNU Make is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by the
Free Software Foundation; either version 3 of the License, or (at your
option) any later version.

GNU Make is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
more details.

You should have received a copy of the GNU General Public License
along with this program. If not, see
https://www.gnu.org/licenses/.

  • Введение
  • Что такое make и Makefile
  • Продвинутое использование
  • Заключение

Введение

В жизни многих разработчиков найдётся история про первый рабочий день с новым проектом. После клонирования основного репозитория проекта наступает этап, когда приходится вводить множество команд с определёнными флагами и в заданной последовательности. Без описания команд, в большинстве случаев, невозможно понять что происходит, например:

# Bash
touch ~/.bash_history
ufw allow 3035/tcp || echo 'cant configure ufw'
ufw allow http || echo 'cant configure ufw'
docker run 
  -v /root/:/root/ 
  -v /etc:/etc 
  -v /var/run/docker.sock:/var/run/docker.sock 
  -v /var/tmp:/var/tmp 
  -v /tmp:/tmp 
  -v $PWD:/app 
  --network host 
  -w /app 
  --env-file .env 
  ansible ansible-playbook ansible/development.yml -i ansible/development --limit=localhost -vv
grep -qxF 'fs.inotify.max_user_watches=524288' /etc/sysctl.conf || echo fs.inotify.max_user_watches=524288 | tee -a /etc/sysctl.conf || echo 'cant set max_user_watches' && sysctl -p
sudo systemctl daemon-reload && sudo systemctl restart docker

Эти команды являются лишь частью того, что необходимо выполнить при разворачивании проекта. В приведённом примере видно, что команды сами по себе длинные, содержат много флагов, а значит, их трудно не только запомнить, но и вводить вручную. Постоянно вести документацию становится сложнее с ростом проекта, она неизбежно устаревает, а порог входа для новичков становится выше, ведь уже никто не в состоянии вспомнить всех деталей проекта. Некоторые такие команды необходимо использовать каждый день, и даже не один раз в день.

Со временем становится понятно, что нужен инструмент, способный объединить в себе подобные команды, предоставить к ним удобные шорткаты (более короткие и простые команды) и обеспечить самодокументацию проекта. Именно таким инструментом стал Makefile и утилита make. Этот гайд расскажет, как использование этих инструментов позволит свести процесс разворачивания проекта к нескольким коротким и понятным командам:

# Bash
make setup
make start
make test

Что такое make и Makefile

Makefile — это файл, который хранится вместе с кодом в репозитории. Его обычно помещают в корень проекта. Он выступает и как документация, и как исполняемый код. Мейкфайл скрывает за собой детали реализации и раскладывает «по полочкам» команды, а утилита make запускает их из того мейкфайла, который находится в текущей директории.

Изначально make предназначалась для автоматизации сборки исполняемых программ и библиотек из исходного кода. Она поставлялась по умолчанию в большинство *nix дистрибутивов, что и привело к её широкому распространению и повсеместному использованию. Позже оказалось что данный инструмент удобно использовать и при разработке любых других проектов, потому что процесс в большинстве своём сводится к тем же задачам — автоматизация и сборка приложений.

Применение мейка в проектах стало стандартом для многих разработчиков, включая крупные проекты. Примеры мейкфайла можно найти у таких проектов, как Kubernetes, Babel, Ansible и, конечно же, повсеместно на Хекслете.

Синтаксис Makefile

make запускает цели из Makefile, которые состоят из команд:

# Makefile
цель1: # имя цели, поддерживается kebab-case и snake_case
	команда1 # для отступа используется табуляция, это важная деталь 
	команда2 # команды будут выполняться последовательно и только в случае успеха предыдущей

Но недостаточно просто начать использовать мейкфайл в проекте. Чтобы получить эффект от его внедрения, понадобится поработать над разделением команд на цели, а целям дать семантически подходящие имена. Поначалу, перенос команд в Makefile может привести к свалке всех команд в одну цель с «размытым» названием:

# Makefile
up: # разворачивание и запуск
	cp -n .env.example .env
	touch database/database.sqlite
	composer install
	npm install
	php artisan key:generate
	php artisan migrate --seed
	heroku local -f Procfile.dev # запуск проекта

Здесь происходит сразу несколько действий: создание файла с переменными окружения, подготовка базы данных, генерация ключей, установка зависимостей и запуск проекта. Это невозможно понять из комментариев и названия цели, поэтому будет правильно разделить эти независимые команды на разные цели:

# Makefile
env-prepare: # создать .env-файл для секретов
	cp -n .env.example .env

sqlite-prepare: # подготовить локальную БД
	touch database/database.sqlite

install: # установить зависимости
	composer install
	npm install

key: # сгенерировать ключи
	php artisan key:generate

db-prepare: # загрузить данные в БД
	php artisan migrate --seed

start: # запустить приложение
	heroku local -f Procfile.dev

Теперь, когда команды разбиты на цели, можно отдельно установить зависимости командой make install или запустить приложение через make start. Но остальные цели нужны только при первом разворачивании проекта и выполнять их нужно в определённой последовательности. Говоря языком мейкфайла, цель имеет пререквизиты:

# Makefile
цель1: цель2 # такой синтаксис указывает на зависимость задач — цель1 зависит от цель2
	команда2 # команда2 выполнится только в случае успеха команды из цель2

цель2:
	команда1

Задачи будут выполняться только в указанной последовательности и только в случае успеха предыдущей задачи. Значит, можно добавить цель setup, чтобы объединить в себе все необходимые действия:

# Makefile
setup: env-prepare sqlite-prepare install key db-prepare # можно ссылаться на цели, описанные ниже

env-prepare:
	cp -n .env.example .env

sqlite-prepare:
	touch database/database.sqlite

install:
	composer install
	npm install

key:
	php artisan key:generate

db-prepare:
	php artisan migrate --seed

start:
	heroku local -f Procfile.dev

Теперь развернуть и запустить проект достаточно двумя командами:

# Bash
make setup # выполнит последовательно: env-prepare sqlite-prepare install key db-prepare
make start

Благодаря проделанной работе Makefile, команды проекта вместе с флагами сведены в Makefile. Он обеспечивает правильный порядок выполнения и не важно, какие при этом задействованы языки и технологии.

Продвинутое использование

Фальшивая цель

Использование make в проекте однажды может привести к появлению ошибки make: <имя-цели> is up to date., хотя всё написано правильно. Зачастую, её появление связано с наличием каталога или файла, совпадающего с именем цели. Например:

# Makefile
test: # цель в мейкфайле
	php artisan test
# Bash
$ ls
Makefile
test # в файловой системе находится каталог с именем, как у цели в мейкфайле

$ make test # попытка запустить тесты
make: `test` is up to date.

Как уже говорилось ранее, изначально make предназначалась для сборок из исходного кода. Поэтому она ищет каталог или файл с указанным именем, и пытается собрать из него проект. Чтобы изменить это поведение, необходимо в конце мейкфайла добавить .PHONY указатель на цель:

# Makefile
test:
	php artisan test

.PHONY: test
# Bash
$ make test
✓ All tests passed!

Последовательный запуск команд и игнорирование ошибок

Запуск команд можно производить по одной: make setup, make start, make test или указывать цепочкой через пробел: make setup start test. Последний способ работает как зависимость между задачами, но без описания её в мейкфайле. Сложности могут возникнуть, если одна из команд возвращает ошибку, которую нужно игнорировать. В примерах ранее такой командой было создание .env-файла при разворачивании проекта:

# Makefile
env-prepare:
	cp -n .env.example .env # если файл уже создан, то повторный запуск этой команды вернёт ошибку

Самый простой (но не единственный) способ «заглушить» ошибку — это сделать логическое ИЛИ прямо в мейкфайле:

# Makefile
env-prepare:
	cp -n .env.example .env || true # теперь любой исход выполнения команды будет считаться успешным

Добавлять такие хаки стоит с осторожностью, чтобы не «выстрелить себе в ногу» в более сложных случаях.

Переменные

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

# Makefile
say:
	echo "Hello, $(HELLO)!"
# Bash
$ make say HELLO=World
echo "Hello, World!"
Hello, World!

$ make say HELLO=Kitty
echo "Hello, Kitty!"
Hello, Kitty!

Переменные могут быть необязательными и содержать значение по умолчанию. Обычно их объявляют в начале мейкфайла.

# Makefile
HELLO?=World # знак вопроса указывает, что переменная опциональна. Значение после присвоения можно не указывать.

say:
	echo "Hello, $(HELLO)!"
# Bash
$ make say
echo "Hello, World!"
Hello, World!

$ make say HELLO=Kitty
echo "Hello, Kitty!"
Hello, Kitty!

Некоторые переменные в Makefile имеют названия отличные от системных. Например, $PWD называется $CURDIR в мейкфайле:

# Makefile
project-env-generate:
	docker run --rm -e RUNNER_PLAYBOOK=ansible/development.yml 
		-v $(CURDIR)/ansible/development:/runner/inventory  # $(CURDIR) - то же самое, что $PWD в терминале
		-v $(CURDIR):/runner/project 
		ansible/ansible-runner

Заключение

В рамках данного гайда было рассказано об основных возможностях Makefile и утилиты make. Более плотное знакомство с данным инструментом откроет множество других его полезных возможностей: условия, циклы, подключение файлов. В компаниях, где имеется множество проектов, написанных разными командами в разное время, мейкфайл станет отличным подспорьем в стандартизации типовых команд: setup start test deploy ....

Возможность описывать в мейкфале последовательно многострочные команды позволяет использовать его как «универсальный клей» между менеджерами языков и другими утилитами. Широкая распространённость этого инструмента и общая простота позволяют внедрить его в свой проект достаточно легко, без необходимости доработок. Но мейкфайл может быть по-настоящему большим и сложным, это можно увидеть на примере реальных проектов:

  • Codebattle
  • Babel
  • Kubernetes

Дополнительные материалы

  • Утилита make: полезный универсальный инструмент программиста — видео-версия данного гайда.

Мейкфайлы, использованные при составлении гайда:

  • Hexlet SICP
  • Hexlet Basics

Понравилась статья? Поделить с друзьями:
  • Как вернуть 13 процентов за обучение в автошколе пошаговая инструкция
  • Крка рус руководство
  • Инструкция к духовому шкафу bosch hbf534ebor
  • Ballu home platinum series конвектор инструкция по применению
  • Сбер здоровье руководство