Makefile для сборки PHP проектов с использованием Composer, Docker и Git

Наверняка многие слышали, а возможно даже и пользовались командой make в Linux. Чаще всего это выглядит как такая последовательность команд:

make && make install && make clean

Как правило это нужно для сборки из исходников программ написанных на Си или Си++. Однако, кто сказал, что с помощью Makefile (так называется конфигурационный файл для команды make) нельзя собирать билды для PHP проектов? А никто не говорил, потому что можно!

Только представьте, что можно установить PHP-проект на чистый сервер с помощью одной команды:

make install

И через минуту ваше приложение будет полностью готово к работе! Вам этого мало? А что насчёт такого же удобного обновления одной командой:

make update

Основы make и Makefile

Утилита make по-умолчанию установлена в большинство современных Linux-дистрибутивов, поэтому проблем с её использованием не возникнет. И чтобы начать её использовать нужно создать файл с именем Makefile, в котором описать последовательности bash-команд. Его синтаксис банален до невозможности:

target: [dependency [dependency [dependency ... [dependency]]]]
	commands
	commands
#	commands

Где target собственно и есть требуемое действие, например, install или update. А dependency — зависимость, которая должна существовать в виде файла или директории на диске, либо успешное выполнение другого target. На следующих строках начиная с символа табуляции (пробелы не подойдут) можно перечислить команды, которые будут выполнены при вызове цели. Цель может не содержать команд, но при этом содержать зависимости.

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

Можно закоментировать команду поставив перед ней знак решётки — #.

Все команды выполняются в контексте текущей директории. Если в одной из строк сделать cd some/directory, то на следующей строке директория вернётся к изначальной, поэтому часть можно увидеть в Makefile подобное содержимое:

	cd $(build) && something
	cd $(build) && another
	cd $(build) && something

Если вызвать команду make без указания target, то будет запущен самый первый найденный target в файле.

В команду make можно передавать аргументы, которые следуют после target в формате arg=value another=value. При этом в Makefile можно указать значения по-умолчанию для аргументов, а также описать константы, которые не могут быть перезаписаны пользователем. Регистр имени аргумента важен: arg и ARG будут сохранены в две независимые переменные! Также можно объявить многострочный аргумент:

define parameters
parameters:
    database_host: mariadb
    database_port: 3306
endef

И при желании переопределить его при выполнении команды: make parameters=new. Внутри команд переменные можно подставлять таким образом:

	ls ${argument}
	ls $(argumen)

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

ifeq ($(test), test)
        echo 'this is test'
else ifeq (${test}, stable)
        echo 'this is stable'
else
        echo 'this is unknown'
endif

Более сложный пример:

ifeq ($(test), test)
check:
        echo 'this is test'
else ifeq (${test}, stable)
check^
        echo 'this is stable'
else
check:
        echo 'this is unknown'
endif
tests: check
        echo $(test)

Собственно всех этих знаний должно хватить для написания достаточно сложных сценариев.

Пример Makefile для сборки PHP приложения в Docker

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

build = beta
EXECFLAGS = -u app
ENVFLAGS = --env SYMFONY_ENV=prod --env SYMFONY_DEBUG=0
BUILDAPP = $(build)/app
ifeq (${build}, beta)
ports:
	cd $(build) && sed -i -e "s/- 700:80/- 701:80/" -e "s/- 710:80/- 711:80/" docker-compose.yml
else ifeq (${build}, stable)
ports:
	cd $(build) && sed -i -e "s/- 700:80/- 700:80/" -e "s/- 710:80/- 710:80/" docker-compose.yml
else
ports:
	cd $(build) && sed -i -e "s/- 700:80/- 702:80/" -e "s/- 710:80/- 712:80/" docker-compose.yml
endif
define parameters
parameters:
    database_host: mariadb
    database_port: 3306
endef
export parameters
swagger-codegen:
	wget http://central.maven.org/maven2/io/swagger/swagger-codegen-cli/2.3.1/swagger-codegen-cli-2.3.1.jar
clone:
	git clone -v --progress --branch $(build) $(repository) $(build)
	cd $(build) && git checkout $(build)
allow-butbucket:
	cd $(build) && docker-compose exec $(EXECFLAGS) php bash -c 'ssh-keyscan -H bitbucket.org >> ~/.ssh/known_hosts'
configure: ports
	cd $(build) && sed -i -e "s/hostname: php/hostname: $(build)-php/" \
	-e "s/hostname: nginx/hostname: $(build)-nginx/" \
	-e "s/hostname: mariadb/hostname: $(build)-mariadb/" \
	-e "s/hostname: redis/hostname: $(build)-redis/" \
	-e "s/hostname: swaggerui/hostname: $(build)-swaggerui/" \
	docker-compose.yml
up: clone configure
	cd $(build) && docker-compose up --build -d
chmod:
	chown 1000:1000 -R $(build)/app
	chmod 755 -R $(build)/app
install: up chmod allow-butbucket
	chmod 777 /tmp/composer/
	chown 1000:1000 -R $(build)/storage/php/var/www/app/current/var/
	cd $(build) && docker-compose exec $(EXECFLAGS) php mkdir -p var/tmp
	cd $(build) && docker-compose exec $(EXECFLAGS) php bash -c "echo '$$parameters' > app/config/parameters.yml"
	cd $(build) && docker-compose exec $(EXECFLAGS) $(ENVFLAGS) php composer install --no-dev
	cd $(build) && docker-compose exec $(EXECFLAGS) php bin/console doctrine:schema:create --env=prod
#	cd $(build) && docker-compose exec $(EXECFLAGS) php bin/console cache:clear --env=prod --no-debug
reload-fpm:
	cd $(build) && docker-compose exec php kill -USR2 1
reload-nginx:
	cd $(build) && docker-compose exec nginx nginx -s reload
restart-nginx:
	cd $(build) && docker-compose exec nginx nginx -s restart
docker-rebuild:
	cd $(build) && docker-compose up --build -d
docker-recreate:
	cd $(build) && docker-compose up --force-recreate -d $(services)
reset:
	cd $(build) && git fetch
	cd $(build) && git reset --hard origin/$(build)
composer-install:
	cd $(build) && docker-compose exec $(EXECFLAGS) $(ENVFLAGS) php composer install --no-dev
schema-update:
	cd $(build) && docker-compose exec $(EXECFLAGS) $(ENVFLAGS) php bin/console doctrine:schema:update --force
update: reset configure chmod docker-rebuild allow-butbucket hotfix composer-install schema-update reload-fpm
down:
	cd $(build) && docker-compose down
uninstall: down
	rm -rf $(build)
cron:
	cd $(build) && docker-compose exec -T $(EXECFLAGS) $(ENVFLAGS) php bin/console somecommand

Часто возникающие ошибки с make и Makefile

make: *** No rule to make target `target’. Stop.

Скорее всего target с таким именем не существует в Makefile.

make: `target’ is up to date.

Этот target не будет выполнен, так как уже существует файл или директория с тем же именем что и target.

make: Nothing to be done for `target’.

Такой target не существует, однако существует файл или директория с таким именем.

make: *** [target] Error 127

Какая-то из команд в target завершилась с ошибкой 127.

Makefile:34: *** missing separator (did you mean TAB instead of 8 spaces?). Stop.

Проверьте, у вас где-то пробелы вместо табов перед описанием bash-команд.

Makefile:22: *** missing separator. Stop.

В Makefile объявлены bash-команды без отступов.

Makefile:27: *** recipe commences before first target. Stop.

В Makefile объявлены команды раньше, чем объявлен первый target.

make: Circular tests <- tests dependency dropped.

Обнаружены циклически зависимости которые были пропущены.

make: *** No rule to make target `target’, needed by `target’. Stop.

Выполняемый target зависит от другого target, который не объявлен в Makefile.