This blog post is part of the series I just made up called “I’m not really sure, but I should know how this thing works if I’m a developer”. That’s trademarked, just so you know.
So recently I was playing around with some Linux and on a couple of occasions while surfing Stack Overflow, I saw a few examples of people solving problems by using make
. I thought to myself “Wow, all these smart ass programmers here sure sound like they know what they are talking about. Maybe I should look into what make
does and how I can use it”. <— [Edit: I kinda regret this] Let’s try to understand what make
is, how to write a Makefile
and how useful can it be.
The super high level explanation
Make
is a utility in Unix (on top of which MacOS and Linux are built). This utility is made to start the execution of a makefile / Makefile, (naming is dependant on your system, but you get the gist). What is a Makefile you might ask? A Makefile is a file that has some commands inside. When you are inside the same directory you have your Makefile saved, you can run make
and the commands in the file will be executed.
Let’s make a very simple example.
mkdir make-playground
cd make-playground/
touch Makefile
We’ve created a directory and an empty Makefile
inside. Cool. Let’s edit the Makefile
and add something to it we can execute.
complicated_example:
echo "This is a very complicated example"
Nice one. Remember what we said earlier? If we are in the same directory and we run make
in the terminal, the commands inside the Makefile
will be executed. Let’s give that a try.
make
And we get the following output:
echo "This is a very complicated example"
This is a very complicated example
Let’s break this down:
complicated_example is like we’re defining a function in any programming language. This is called the target. The prerequisites or the dependencies follow the target. We have not yet defined any prerequisites in our example. The echo "This is a very complicated example"
command is called the recipe. The recipe uses the prerequisites to make a target. The target, prerequisites and the recipe all together make a rule.
This is a bit mindfucky to read, I know, so let’s try to visualise it with some pseudo code:
target: prerequisites
<TAB> recipe
A target could be a binary file that depends on prerequisites (source files). A prerequisite can also be a target that depends on other dependencies. Yeah I know, I am also confused as fuck. Let’s try out another visualisation:
final_target: sub_target final_target.c
Recipe_to_create_final_target
sub_target: sub_target.c
Recipe_to_create_sub_target
It is not necessary for the target to be a file; it could be just a name for the recipe, as in our example. Apparently these are called “phony targets.”
Ok, so enough with the mindfucky stuff, let’s go back to the comfy simple “Hello world” style example from above. Remember the output we got when we ran make
? Don’t worry about scrolling up to find it, here it is:
echo "This is a very complicated example"
This is a very complicated example
As you can see, we saw the command (recipe) itself too. I don’t want to see the recipe, I want to see the recipe in action. Let’s surpress that.
Let’s edit our Makefile
:
complicated_example:
@echo "This is a very complicated example"
We surpress echoing the actual command by using @
. Now if we run make
we should see the following output:
This is a very complicated example
Let’s expand our Makefile
to do more than just echo something back to us:
complicated_example:
echo "This is a very complicated example"
file_creator:
@echo "Creating a bunch of files"
touch {1..10}.txt
file_destroyer:
@echo "Begone you mortal files!"
rm **.txt
Alright, nice, so we have a couple of targets now that will first create 10 files ({n1..n2}
is how you make an array in bash), and then if I need to tell you what our file_destroyer
does, maybe you should be reading a different genre of blogs. Seriously though.
Let’s run it!
make
Hmmm. I don’t see any files created. I don’t see the echo either. I only see the first target’s output, what is happening?
So by default, when you run make
, the very first rule in the Makefile
gets executed, and that’s pretty much it. A bit anticlimactic, right? Let’s see how we can fix this behaviour, but before that, let’s sneak in some more learnings:
We can specify the default command when we run make
like so:
.DEFAULT_GOAL := file_creator
Add this to the top of the Makefile
and run make
.
.DEFAULT_GOAL
is pretty self-explanatory. We say what’s the default target we want to run when we call make
. But most of the time, you’ll actualy be running multiple commands. Or not, I’m not your dad, you can do whatever you want. (Seriously though, in reality, you’ll run multiple commands). Let’s learn how to tell the Makefile
what to run:
all: complicated_example file_creator
complicated_example:
echo "This is a very complicated example"
file_creator:
@echo "Creating a bunch of files"
touch {1..10}.txt
file_destroyer:
@echo "Begone you mortal files!"
rm **.txt
Now, when we run make, we should see the following output (+10 freshly created empty .txt files):
This is a very complicated example
Creating a bunch of files
touch {1..2}.txt
Great! Now we know how to tell the Makefile
which targets to execute by default!
We can also call specific targets with the following syntax: make file_destroyer
:
Begone you mortal files!
rm **.txt
Makefiles can get as simple and as complicated as your usecase. After spending a couple of days researching, I found out that that are entire books written on make
and that using it to compile c programs is just the tip of the iceberg. There are patterns, variables, functions, and it can get real serious real fast.
I’ll try and leave you with a practical example that can utilise our base knowledge we’ve gained. Let’s say we are making a lil Flask app that uses Docker, and we are too lazy to type docker-compose up
docker-compose down
, etc.
Let’s see how the Makefile for that will look like:
serve:
docker-compose up -d myservice
stop:
docker-compose down
test:
docker-compose run myservice pytest
seed:
docker-compose run myservice python3/bin/manage.py seed_db
Stick this Makefile in your base directory, and now you can simplify your flow by just running make <target>
! We have a couple of examples here that will run our app, stop it, run some tests or seed our local database.
I hope this gave you a base understanding of how make
works and that you can implement it in your life to make some tasks easier. Or you might go ahead and start compiling binary files to C programs. The sky is the limit.
Until next time!