For the last decade I worked on several more or less complex projects. I often had problems with their installation. Sometimes, the project was not documented or the existing documentation was not up to date. I had to run commands but I did not understand all of them. When I got errors it was hard to understand what happened. That was not always simple.
During my last projects, I used makefile to provide an abstraction to simplify their installation and hide complexity. It let me install projects with a single command and provided a minimal set of targets which made my daily work easier.
How does makefile work?
A makefile is a set of “rules” that looks like:
target: prerequisites prerequisites
recipe
recipe
...
A target is the name of a file (or a folder) that is generated by make
. It could also be the name of an action to perform but you need to declare it as PHONY.
A prerequisite are the files (or folders) needed to create the target, you can see them as target dependencies.
A recipe are all actions executed when the target is run. Caution: you need to indent all recipes using a “real” tab character otherwise you will get errors.
.env:
cp .env.dist .env
If the .env
file does not exist, the recipe will be carried out, but if it does exist the recipe won’t be executed #magic.
.PHONY: cache
cache: .env
bin/console c:c
The cache
target does not target a file. Declaring this target as PHONY allows having a file named cache
in the same directory as the Makefile.
Note: If the .env
file does not exist the .env
target will be performed before the cache
target.
Let’s take an example
For instance, a project orchestrated by docker-compose having:
- a front application written in JS using React framework,
- a back application written in PHP using Symfony framework,
- a PostgreSQL database managed by Doctrine DBAL and Doctrine migration
Caution: I will only speak about projects setup for dev purposes. I won’t talk about making docker images ready for production.
Do you speak French ? Tired of the same old CRUD applications, struggling with your framework, or feeling the pressure of production releases? It’s time to take your career to the next level.
Discover the power of Hexagonal Architecture and DDD to build robust and sustainable Symfony applications. Join me and kickstart your journey toward mastering advanced development techniques.
Let’s start installing the project dependencies. The following targets install project dependencies if they have not been downloaded yet. They can guess if they are outdated to upgrade them thanks to target prerequisites. Another interesting thing is that nothing will be done if dependencies are already installed and up to date.
# Front
web/yarn.lock: web/package.json
docker-compose run --rm node yarn install
web/node_modules: web/yarn.lock
docker-compose run --rm node yarn install --frozen-lockfile
docker-compose run --rm node yarn check --integrity
# back
api/composer.lock: api/composer.json
docker-compose run --rm fpm composer update
api/vendor: api/composer.lock
docker-compose run --rm fpm composer install
Then, we need to “up” all docker-compose services: the web server, the PHP process manager, the datatable, and the node to run the front application in development mode.
.PHONY: up
up:
docker-compose up -d
docker-compose ps
Name Command State Ports
---------------------------------------------------------------------------------------------------------------------------------------------
my-maps-node docker-entrypoint.sh yarn ... Up
my-maps-web nginx -g daemon off; Up 80/tcp
my-maps_database_1 docker-entrypoint.sh postgres Up 5432/tcp
my-maps_fpm_1 php-fpm -F Up
The last thing to do is to create the database schema using Doctrine migration.
.PHONY: db-migration
db-migration: api/vendor
docker-compose run --rm fpm bin/console doctrine:migrations:migrate --no-interaction
Tip: To ease my daily work, I like introducing other targets like db
target that resets database quickly or fixture
to load some data fixtures.
.PHONY: db
db: api/vendor
docker-compose run --rm fpm bin/console doctrine:database:drop --force --no-interaction
docker-compose run --rm fpm bin/console doctrine:database:create --no-interaction
.PHONY: fixtures
fixtures: api/vendor
docker-compose run --rm fpm bin/console project:fixtures:load
Now, we have all atomic targets to set up all parts of the application. Another interesting thing with make, is that it can run several targets within a single command make up api/vendor web/node_modules
.This is pretty useful to create scenarios. For instance, to set up and run the project I only need to run the following command:
make up api/vendor web/node_modules db db-migration fixtures
But it works with everything:
make db db-migration fixtures
make api/vendor web/node_modules
To make your day-to-day basis, you can introduce targets that run those scenarios.
# It builds and runs the application
.PHONY: app-dev
app-dev: up api/vendor web/node_modules db db-migration fixtures
# It resets the database
.PHONY: db-reset
db-reset: db db-migration fixtures
# It resets the database
.PHONY: dependencies
dependencies: api/vendor web/node_modules
Tip: I advise you to introduce the minimum set of targets into the makefile. Keep it simple! Don’t forget that everyone uses it! To let developers have their custom targets you may want to include custom makefiles using include
. Don’t forget to add an entry into .ignore
to avoid committing those files. Now, developers can create their own makefile with their personal targets.
One last word
Makefile is only a solution, this is not THE solution. The important thing to keep in mind is that you should be able to easily run your project to reduce the developer’s mental load and let them focus on the right thing. It will improve the day-to-day basis and make newcomers’ onboarding easier.
Thanks to my proofreader @LaureBrosseau .