Skip to content

Instantly share code, notes, and snippets.

@isaacs
Last active October 28, 2024 14:40
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
# 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
@isaacs
Copy link
Author

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