Skip to content

Instantly share code, notes, and snippets.

@tapyu
Last active June 5, 2024 14:14
Show Gist options
  • Save tapyu/38de27bb576d8d01074b1c3062a1eafa to your computer and use it in GitHub Desktop.
Save tapyu/38de27bb576d8d01074b1c3062a1eafa to your computer and use it in GitHub Desktop.
Makefile cheat sheet

Makefile and Dependency tree instructions

# 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
# http://www.gnu.org/software/make/manual/make.html
# 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:
tutorial:
@# 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`
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.
var-kept:
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.
#
#source.txt:
# @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.
src/%.txt:
@# 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"
#
# NOW TIME FOR MAGIC!
#
# 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.
clean:
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.
badkitty:
$(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
Syntax Description Example

Special characters

@ Command prefix: suppress printing the command on the terminal. @rm -f *.out *.aux *.alg *.acr
| dependency prefix: check whether the following dependencies exist, but do not trigger them. If they don't exist, however, it is triggered.. compile: | default_preamble.tex
- Command prefix: error will be printed but ignored, and make will continue to run -false
% Serves as a variety of wildcard-like functionalities within Makefile, such as search-and-replace patterns, rule terget-dependency pattern, etc. To expand to system files (i.e., outside Makefile), use * with wildcard instead. dest/%.txt: src/%.txt

Usual variable names

CC Program for compiling C programs; default is cc
CFLAGS Extra flags to give to the C compiler
CPPFLAGS Extra flags to give to the C preprocessor
CXX Program for compiling C++ programs; default is g++
CXXFLAGS Extra flags to give to the C++ compiler
LDFLAGS Extra flags to give to compilers when they are supposed to invoke the linker
SRCS Source files (.c/.cpp).
OBJS Object files (.o).
EXEC Executable file name, that is, the final output.
SHELL Set the shell interpreter. Default is /bin/sh

Variable expansion

$(X), ${X}, $X Expand to the Makefile variable VARIABLE. If it is not found, return shell variable instead. $(X) is peferred, while $X is a bad practice as it only works single-character variables. echo $(SRCS)
$(X:pattern=replace) Variable expansion with pattern substitution/replacement. OBJS = $(SRCS:.c=.o)
$${FOO} Expand to the shell variable FOO echo let me see it $${PWD}
$@ Expand to the target(s), that is, the left-hand side of the rule
$@ Expand to all the targets, that is, the left-hand side of the rule.
$^ Expand to the dependencies, that is, the right-hand side of the rule.
$< Expand to the first the dependency.
$? List of dependencies (prerequisites) that are newer than the target.

Makefile functions

$(shell command), $$(command) Shell command substitution. $(shell command) is peferred. echo $(shell ls) is that good?
$(or element1, element2) Makefile or conditional. It is useful both for handling Makefile varables and $(shell command) commands. $(or $(shell { ls -1 lecture_*.tex | head -n 1; } 2> /dev/null), $(basename $(shell pwd)))
$(or $(MY_VARIABLE), default_value)
$(addprefix FOO,BAR) Prefix each word in a list with a specified prefix. $(addprefix $(BINDIR)/,$(BINARIES))
$(addsuffix FOO,BAR) Suffix each word in a list with a specified prefix.
$(foreach DIR,$(DIRS),someting Iterate over $(DIRS) variable. CFILES=$(foreach DIR,$(SRCDIRS),$(wildcard $(DIR)/*.c))
$(wildcard foo/*.bar) Uses the wildcard *. It searches your filesystem for matching filenames. You should always wrap it in the wildcard function. $(wildcard $(DIR)/*.c)
$(patsubst match-it,replace-by-it,from-it) Pattern-based substitution. $(patsubst %.c,$(BUILDDIR)/%.o,$(notdir $(CFILES)))
$(filter get-it,from-it) Filter function. $(filter %tip.o,$(OBJECTS))
$(filter-out filter-it-out,from-it) Filter out function. $(filter-out %tip.o,$(OBJECTS))
$(word extract-it,from-it) Extracts a specific word from a space-separated list of words.
$(wordlist start,,end,text) Extracts a range of words from a space-separated list of words.
$(words list) Counts the number of words in a list.
$(firstword list) Extracts the first word from a space-separated list of words.
$(lastword list) Extracts the last word from a space-separated list of words.
$(strip list) Removes leading and trailing whitespace characters from a string.
$(dir path) Extracts the directory portion from a path. $(dir src/foo.c hacks) yields src/ ./
$(notdir path) Extracts the name portion from a path. $(notdir src/foo.c hacks lecture_3/figs/DLL_EL) yields foo.c hacks DLL_EL
$(basename path) Extracts the file name without the extension from a path. $(basename src/foo.c src-1.0/bar hacks) yields src/foo src-1.0/bar hacks

Assignments

= Simple Variable Assignment (AKA lazy or deferred evaluation), that is, it is evaluated everytime the variable is encountered in the code. FOO = $(BAR) BAR = hello all: echo $(FOO) # prints "hello"
:= Immediate Variable Assignment. Use it when you want the variable to be set to the value at the time of assignment, without being affected by any changes made later in the Makefile. FOO := $(BAR) BAR = hello all: echo $(FOO)# prints nothing
?= Conditional assignment, that is, it assigns a value to a variable only if it does not have a value.
+= Appending.

Useful information

Generic syntax:

target: dependencies
    actions
  • The rule is triggered if the target file doesn't exist in the current directory or if one or more dependencies have been updated more recently than the target file. If you only modify the target file and not its dependencies, the rule isn't triggered as long as the target file still exists.
  • If one dependence file is missing, the Makefile tries to find a rule whose target is this missing file. If it is found, that rule is triggered fisrt, and after the initial rule is triggered too.
  • A rule without dependencies is only triggered if the target file doesn't exist anymore.
  • If you pass no rule as an argument to the make command on terminal, it will trigger the very first rule. It is a convention to call the first rule all:.
  • You may want to define targets that do not correspond to actual files. Instead, it perform actions such as cleaning up a build directory or running tests. These are called "phony" targets.
  • You must use a tab to make the indentation, or it will prompt an error otherwise
  • * searches your filesystem for matching filenames. I suggest that you always wrap it in the wildcard function:
thing_wrong := *.o  # Don't do this! '*' will not get expanded
one: $(thing_wrong) # Fails, because $(thing_wrong) is the string "*.o"
two: *.o            # Don't do this! Stays as *.o if there are no files that match this pattern :(
thing_right := $(wildcard *.o) # that is right! You must always use $(wildcard )
three: $(thing_right)          # Works as you would expect!
four: $(wildcard *.o)          # Same as rule three
  • When there are multiple targets in a rule, the commands will be run for each target. E.g.,
f1.o f2.o:
	echo $@
# Equivalent to:
# f1.o:
#	 echo f1.o
# f2.o:
#	 echo f2.o
  • You can pass variables to Makefile, from the command line, e.g., make FIRS_VAR=Myke SECOND_VAR=Joanne.
  • In Makefile, each line is considered a separate command, and they are executed in their own subshell. If you want to run multiple commands in the same subshell, you need to concatenate them using ; or &&. You can use \ to scape the break line and make things more organized:
target1:
	cd /path/to/A/path/to/B/ ; \ # first the cd command is run
	  lualatex main.tex          # then the lualatex is run

or

target2:
	command1 && \
	  command2    # command1 and command2 are run simultaneously
  • Makefile is ridiculously stupid to work with varible's filenames that contains whitespace in it. It is useless to single or double quote the filename as Make ignores them in every situation. Moreover, all make functions use whitespace as delimiters, and disregard quote characters. So, as general rule, you must not use filenames with whitespaces and you must not quote Make variables, you can even get some weird workaround to handle whitespaces, but the solution is as stupid as Makefile itself:
NULL :=
S := $(NULL) $(NULL) # whitespace

path := /some/path here # this is meant to be "/some/path here"
xpath := $(subst $S,^,$(path)) # match whitespace, replace by ^, from $(path)

var := $(xpath) another_file.txt # now you can have "/some/path here" and another_file.txt
all:
        @echo $(words $(path)) # 2
	@echo $(words $(var)) # 2
	@echo $(var) # /some/path^here labas
	@echo $(subst ^,$S,$(var)) # /some/path here labas
  • Some shell features such as prefix and suffix substitution (${parameter/#foo/bar} and ${parameter/%foo/bar}) are not supported by /bin/sh (only in Makefile?), which is the default shell interpreter in Makefile. To use those features, you need to change the shell interpreter, i.e., SHELL:=/bin/bash.

Static pattern rules

objects = foo.o bar.o all.o
all: $(objects)

# These files compile via implicit rules
# Syntax:
# targets ...: target-pattern: prereq-patterns ...
# Example:
foo.o: %.o: %.c
# foo.o sets the **stem** to "foo".
# It then replaces the '%' in prereq-patterns with that stem, yielding "foo.c"
# you can do all for all target files at once:
# foo.o bar.o all.o: %.o: %.c
# or simply
$(objects): %.o: %.c
# Either of these two forms are equivalent to these three commands:
# foo.o: foo.c
# bar.o: bar.c
# all.o: all.c

all.c:
	echo "int main() { return 0; }" > all.c

%.c:
	touch $@

clean:
	rm -f *.c *.o all
  • The filter function can be used in static pattern rules to match the correct files.
obj_files = foo.result bar.o lose.o
src_files = foo.raw bar.c lose.c

all: $(obj_files)
# Note: PHONY is important here. Without it, implicit rules will try to build the executable "all", since the prereqs are ".o" files.
.PHONY: all 

# Ex 1: filter %.o files from $(obj_files), the dependency is %.c
$(filter %.o,$(obj_files)): %.o: %.c
	echo "Ex1. target: $@ prereq: $<"

# Ex 2: filter %.result files from $(obj_files), the dependency is %.raw
$(filter %.result,$(obj_files)): %.result: %.raw
	echo "Ex2. target: $@ prereq: $<" 

%.c %.raw:
	touch $@

clean:
	rm -f $(src_files
# % expands to the same stem
%.o : %.c
	$(CC) -c $(CFLAGS) $(CPPFLAGS) $< -o $@

Main .PHONY targets:

  • all: target
    • It is actually just a dummy rule
    • The all target has no command
    • It is actually just a dummy target whose dependencies are the targets from rules at the tip of the dependency tree (DT). For instance, if a project has two files at the tip of the DT, all must have these two files. Otherwise, the Makefile will not be able to verify all the files at the end of the DT.
  • clean: target
    • It is actually just a dummy rule
    • it does not have dependencies
    • It aims to remove intermediates file that was created through the process
    • It is never called unless you explicitly type make clean
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment