Skip to content

Instantly share code, notes, and snippets.

Last active June 4, 2024 03:56
Show Gist options
  • Save isaacs/62a2d1825d04437c6f08 to your computer and use it in GitHub Desktop.
Save isaacs/62a2d1825d04437c6f08 to your computer and use it in GitHub Desktop.
# Hello, and welcome to makefile basics.
# You will learn why `make` is so great, and why, despite its "weird" syntax,
# it is actually a highly expressive, efficient, and powerful way to build
# programs.
# Once you're done here, go to
# to learn SOOOO much more.
# To do stuff with make, you type `make` in a directory that has a file called
# "Makefile". You can also type `make -f <makefile>` to use a different
# filename.
# A Makefile is a collection of rules. Each rule is a recipe to do a specific
# thing, sort of like a grunt task or an npm package.json script.
# A rule looks like this:
# <target>: <prerequisites...>
# <commands>
# The "target" is required. The prerequisites are optional, and the commands
# are also optional, but you have to have one or the other.
# Type "make" and see what happens:
@# todo: have this actually run some kind of tutorial wizard?
@echo "Please read the 'Makefile' file to go through this tutorial"
# By default, the first target is run if you don't specify one. So, in this
# dir, typing "make" is the same as typing "make tutorial"
# By default, make prints out the command before it runs it, so you can see
# what it's doing. This is a departure from the "success should be silent"
# UNIX dogma, but without that default, it'd be very difficult to see what
# build logs etc are actually doing.
# To suppress the output, we've added @ signs before each line, above.
# Each line of the command list is run as a separate invocation of the shell.
# So, if you set a variable, it won't be available in the next line! To see
# this in action, try running `make var-lost`
export foo=bar
echo "foo=[$$foo]"
# Notice that we have to use a double-$ in the command line. That is because
# each line of a makefile is parsed first using the makefile syntax, and THEN
# the result is passed to the shell.
# Let's try running both of the commands in the *same* shell invocation, by
# escaping the \n character. Run `make var-kept` and note the difference.
export foo=bar; \
echo "foo=[$$foo]"
# Now let's try making something that depends on something else. In this case,
# we're going to create a file called "result.txt" which depends on
# "source.txt".
result.txt: source.txt
@echo "building result.txt from source.txt"
cp source.txt result.txt
# When we type `make result.txt`, we get an error!
# $ make result.txt
# make: *** No rule to make target `source.txt', needed by `result.txt'. Stop.
# The problem here is that we've told make to create result.txt from
# source.txt, but we haven't told it how to get source.txt, and the file is
# not in our tree right now.
# Un-comment the next ruleset to fix the problem.
# @echo "building source.txt"
# echo "this is the source" > source.txt
# Run `make result.txt` and you'll see it first creates source.txt, and then
# copies it to result.txt. Try running `make result.txt` again, and you'll see
# that nothing happens! That's because the dependency, source.txt, hasn't
# changed, so there's no need to re-build result.txt.
# Run `touch source.txt`, or edit the file, and you'll see that
# `make result.txt` re-builds the file.
# Let's say that we were working on a project with 100 .c files, and each of
# those .c files we wanted to turn into a corresponding .o file, and then link
# all the .o files into a binary. (This is effectively the same if you have
# 100 .styl files to turn into .css files, and then link together into a big
# single concatenated main.min.css file.)
# It would be SUPER TEDIOUS to create a rule for each one of those. Luckily,
# make makes this easy for us. We can create one generic rule that handles
# any files matching a specific pattern, and declare that we're going to
# transform it into the corresponding file of a different pattern.
# Within the ruleset, we can use some special syntax to refer to the input
# file and the output file. Here are the special variables:
# $@ The file that is being made right now by this rule (aka the "target")
# You can remember this because it's like the "$@" list in a
# shell script. @ is like a letter "a" for "arguments.
# When you type "make foo", then "foo" is the argument.
# $< The input file (that is, the first prerequisite in the list)
# You can remember this becasue the < is like a file input
# pipe in bash. `head <foo.txt` is using the contents of
# foo.txt as the input. Also the < points INto the $
# $^ This is the list of ALL input files, not just the first one.
# You can remember it because it's like $<, but turned up a notch.
# If a file shows up more than once in the input list for some reason,
# it's still only going to show one time in $^.
# $? All the input files that are newer than the target
# It's like a question. "Wait, why are you doing this? What
# files changed to make this necessary?"
# $$ A literal $ character inside of the rules section
# More dollar signs equals more cash money equals dollar sign.
# $* The "stem" part that matched in the rule definition's % bit
# You can remember this because in make rules, % is like * on
# the shell, so $* is telling you what matched the pattern.
# You can also use the special syntax $(@D) and $(@F) to refer to
# just the dir and file portions of $@, respectively. $(<D) and
# $(<F) work the same way on the $< variable. You can do the D/F
# trick on any variable that looks like a filename.
# There are a few other special variables, and we can define our own
# as well. Most of the other special variables, you'll never use, so
# don't worry about them.
# So, our rule for result.txt could've been written like this
# instead:
result-using-var.txt: source.txt
@echo "buildling result-using-var.txt using the $$< and $$@ vars"
cp $< $@
# Let's say that we had 100 source files, that we want to convert
# into 100 result files. Rather than list them out one by one in the
# makefile, we can use a bit of shell scripting to generate them, and
# save them in a variable.
# Note that make uses := for assignment instead of =
# I don't know why that is. The sooner you accept that this isn't
# bash/sh, the better.
# Also, usually you'd use `$(wildcard src/*.txt)` instead, since
# probably the files would already exist in your project. Since this
# is a tutorial, though we're going to generate them using make.
# This will execute the shell program to generate a list of files.
srcfiles := $(shell echo src/{00..99}.txt)
# How do we make a text file in the src dir?
# We define the filename using a "stem" with the % as a placeholder.
# What this means is "any file named src/*.txt", and it puts whatever
# matches the "%" bit into the $* variable.
@# First things first, create the dir if it doesn't exist.
@# Prepend with @ because srsly who cares about dir creation
@[ -d src ] || mkdir src
@# then, we just echo some data into the file
@# The $* expands to the "stem" bit matched by %
@# So, we get a bunch of files with numeric names, containing their number
echo $* > $@
# Try running `make src/00.txt` and `make src/01.txt` now.
# To not have to run make for each file, we define a "phony" target that
# depends on all of the srcfiles, and has no other rules. It's good
# practice to define your phony rules in a .PHONY declaration in the file.
# (See the .PHONY entry at the very bottom of this file.)
# Running `make source` will make ALL of the files in the src/ dir. Before
# it can make any of them, it'll first make the src/ dir itself. Then
# it'll copy the "stem" value (that is, the number in the filename matched
# by the %) into the file, like the rule says above.
# Try typing "make source" to make all this happen.
source: $(srcfiles)
# So, to make a dest file, let's copy a source file into its destination.
# Also, it has to create the destination folder first.
# The destination of any dest/*.txt file is the src/*.txt file with
# the matching stem. You could just as easily say that %.css depends
# on %.styl
dest/%.txt: src/%.txt
@[ -d dest ] || mkdir dest
cp $< $@
# So, this is great and all, but we don't want to type `make dest/#.txt`
# 100 times!
# Let's create a "phony" target that depends on all the destination files.
# We can use the built-in pattern substitution "patsubst" so we don't have
# to re-build the list. This patsubst function uses the same "stem"
# concept explained above.
destfiles := $(patsubst src/%.txt,dest/%.txt,$(srcfiles))
destination: $(destfiles)
# Since "destination" isn't an actual filename, we define that as a .PHONY
# as well (see below). This way, Make won't bother itself checking to see
# if the file named "destination" exists if we have something that depends
# on it later.
# Let's say that all of these dest files should be gathered up into a
# proper compiled program. Since this is a tutorial, we'll use the
# venerable feline compiler called "cat", which is included in every
# posix system because cats are wonderful and a core part of UNIX.
kitty: $(destfiles)
@# Remember, $< is the input file, but $^ is ALL the input files.
@# Cat them into the kitty.
cat $^ > kitty
# Note what's happening here:
# kitty -> (all of the dest files)
# Then, each destfile depends on a corresponding srcfile
# If you `make kitty` again, it'll say "kitty is up to date"
# Let's update just ONE of the source files, and see what happens
# Run this: touch src/25.txt; make kitty
# Note that it is smart enough to re-build JUST the single destfile that
# corresponds to the 25.txt file, and then concats them all to kitty. It
# *doesn't* re-generate EVERY source file, and then EVERY dest file,
# every time
# It's good practice to have a `test` target, because people will come to
# your project, and if there's a Makefile, then they'll expect `make test`
# to do something.
# We can't test the kitty unless it exists, so we have to depend on that.
test: kitty
@echo "miao" && echo "tests all pass!"
# Last but not least, `make clean` should always remove all of the stuff
# that your makefile created, so that we can remove bad stuff if anything
# gets corrupted or otherwise screwed up.
rm -rf *.txt src dest kitty
# What happens if there's an error!? Let's say you're building stuff, and
# one of the commands fails. Make will abort and refuse to proceed if any
# of the commands exits with a non-zero error code.
# To demonstrate this, we'll use the `false` program, which just exits with
# a code of 1 and does nothing else.
$(MAKE) kitty # The special var $(MAKE) means "the make currently in use"
false # <-- this will fail
echo "should not get here"
.PHONY: source destination clean test badkitty
Copy link

JiaqiL commented Jan 4, 2018

Great! As @nucleartide said, should consider add SHELL=/bin/bash at the first line.

Copy link

nhasbun commented May 14, 2018

Pretty useful guide.
Better than many I found online and everything packed on a single gist.

Copy link

This is awesome!

Copy link

Great! Thanks

Copy link

Thanks for this. It's really great.

Copy link

Great tutorial! Thank you!

I don't why(maybe different version of make, environment, etc), but this command

srcfiles := $(shell echo src/{00..99}.txt)

didn't produce the needed sequence, so

->make source
echo {00..99} > src/{00..99}.txt

So I just rewrited it to

srcfiles := $(shell for i in `seq 0 100`; do echo "src/$$i.txt"; done)```

It's just the shell that is using make, you must specify which to use, at the beginning of the file, by setting the attribute

SHELL = /bin/bash

More info:

Copy link

I'm just starting to learn about makefiles and this is the very best ref/tutorial I have found to get me going very quickly. I also enjoy your sense of humor coming through, make a dry subject so much more enjoyable.

Being so new, I have what I'm sure is a real dumb question but I don't know so I'll ask, on line 170 is:

@[ -d src ] || mkdir src

what does [-d src] do? From the context I can tell that it's checking to see if the src directory is there but is this a shell script thing or a make thing? do the brackets [] tell make to execute a command? is -d a make thing or a linux shell thing ?

So sorry for my complete ignorance on this but googling for -d or [-d] or [-d src] is not helping me.

Thanks! (and Thanks for this great gist!)
... Ed

Copy link

Another question, if I run:

make dest/hello.txt

And hello.txt doesn't already exist in the src folder it does the following:

> $ make dest/hello.txt
echo hello > src/hello.txt
cp src/hello.txt dest/hello.txt
rm src/hello.txt

I am trying to understand the rm src/hello.txt and why that happened. I'm guessing it is a make thing because I couldn't see anything in the recipe that would do the rm src/hello.txt

Thanks again for your help understanding this!
... Ed

Copy link

isaacs commented Oct 5, 2019

what does [-d src] do?

Type this on a terminal:

man bash | less

You'll see this:

       Conditional  expressions  are  used  by  the [[ compound command and the test and [ builtin commands to test file attributes and perform string and
       arithmetic comparisons.  Expressions are formed from the following unary or binary primaries.  If any file argument to one of the primaries  is  of
       the  form  /dev/fd/n,  then  file  descriptor  n  is  checked.   If the file argument to one of the primaries is one of /dev/stdin, /dev/stdout, or
       /dev/stderr, file descriptor 0, 1, or 2, respectively, is checked.

       Unless otherwise specified, primaries that operate on files follow symbolic links and operate on the target of  the  link,  rather  than  the  link

       When used with [[, the < and > operators sort lexicographically using the current locale.  The test command sorts using ASCII ordering.

       -a file
              True if file exists.
       -b file
              True if file exists and is a block special file.
       -c file
              True if file exists and is a character special file.
       -d file
              True if file exists and is a directory.

In other words, [ -d src ] is a command that returns success (ie, exit status code of 0) if src is a directory. The || says "if the previous command failed, run the next one".

So, it's a shell thing, not a make thing.

See this line which defines how any file in src should get made.

dest/hello.txt depends on a corresponding file in src of the same basename. So, make dest/hello.txt requires src/hello.txt, which doesn't exist, but there's a recipe for that, so it runs the src/hello.txt recipe first.

Copy link

isaacs commented Oct 5, 2019

I recommend taking some time to read through man bash at least once in your life. It won't all stick on the first read, and that's fine, but a few useful things probably will click into place.

Copy link

Perfect! Thanks! I wasn't even sure how to search for it. Much appreciated.

Copy link

Excecptionally helpful, I can't tell you how much I appreciated finding this. Thank you thank you thank you thank you

Copy link

da895 commented May 5, 2021

Great tutorial! Thank you!

I don't why(maybe different version of make, environment, etc), but this command

srcfiles := $(shell echo src/{00..99}.txt)

didn't produce the needed sequence, so

->make source
echo {00..99} > src/{00..99}.txt

So I just rewrited it to

srcfiles := $(shell for i in `seq 0 100`; do echo "src/$$i.txt"; done)```

you can try to add "SHELL=bash" at first line

Copy link

mouuii commented Aug 25, 2021

thanks , it's useful for me

Copy link

Why I "make kitty" failed?
error as followed:
akefile:21: recipe for target 'src/{00..99}.txt' failed
make: *** [src/{00..99}.txt] Error 127

Copy link

A comprehensive and easy to use C++ Makefile example can also be found here:

Copy link

Can Makefile be used for python packaging for publishing to PyPi?

Copy link

uberbaud commented May 31, 2023

If there is no makefile, you can still use make. For instance if you have a file test.c and run make test, make will generate an executable using variables CFLAGS, LDFLAGS, and LDLIBS. This is done using the input and output file suffixes and built in inference rules. You can see those and more using make -p.

Copy link

isaacs commented Jun 1, 2023

@uberbaud oh wow, I did not know that :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment