make
make
is a build automation tool that creates files from other files by reading build instructions from a file called Makefile
. Make is widely used for compiling software as well as other software engineering operations.
make # find and run Makefile in the current directory
make -f ./path/to/Makefile # define a custom path to your Makefile
By default, Makefile
creates "targets" from "prerequisites". Targets are sometimes called files or file targets. Prerequisites are also sometimes called dependencies or components.
# pseudo example, rest of the examples are actually working Makefiles,
# if the component a or b has changed, run the commands to create "my-target"
my-target: my-dependency-a my-dependency-b
my-command
my-command
make
requires you to use TABS for command indentation. You could customize this behavior or hack around this with ;
this but it's better to go with the default.
make
will run only the first target by default. If target is a filename, it can try to figure out if it's up-to-date to avoid rebuilding, but it is also common to use "verbs" as build target like clean
or install
.
make
reads prerequisite modified times. A target is considered out of date if it does not exist or if it is older than any of the prerequisites (by comparison of last-modification times).
- If the target is missing, build the target.
- If any of the prerequisites are missing, build them if they are in the
Makefile
. - If any of the prerequisites is newer than the target, rebuild the target.
hello.txt:
echo "Hello" > hello.txt
world.txt:
echo "World" > world.txt
make
ls
# => hello.txt Makefile
cat hello.txt
# => Hello
make world.txt
cat world.txt
# => World
make world.txt
# => make: 'world.txt' is up to date.
If your make
target doesn't produce a file, consider marking it as .PHONY
This will make sure it is always ran when triggered.
hello.txt:
echo "Hello" > hello.txt
world.txt:
echo "World" > world.txt
.PHONY: clean
clean:
(test -e hello.txt && rm hello.txt) || true
(test -e world.txt && rm world.txt) || true
make clean
ls
# => Makefile
Targets can require other targets as dependencies. It's common to have your first target to be all:
to act as a reasonable default, or to just print out some help message.
.PHONY: all
all: hello.txt world.txt
hello.txt:
echo "Hello" > hello.txt
world.txt:
echo "World" > world.txt
.PHONY: clean
clean:
(test -e hello.txt && rm hello.txt) || true
(test -e world.txt && rm world.txt) || true
make
ls
# => hello.txt Makefile world.txt
Targets can require files as dependencies, even if they weren't created by make
.
.PHONY: all
all: greeting.txt
greeting.txt: hello.txt world.txt symbol.txt
cat hello.txt world.txt symbol.txt > greeting.txt
hello.txt:
echo -n "Hello" > hello.txt
world.txt:
echo -n "World" > world.txt
.PHONY: clean
clean:
(test -e hello.txt && rm hello.txt) || true
(test -e world.txt && rm world.txt) || true
(test -e greeting.txt && rm greeting.txt) || true
make
# make: *** No rule to make target 'symbol.txt', needed by 'greeting.txt'. Stop.
echo -n "!" > symbol.txt
make
cat greeting.txt
# => HelloWorld!
Makefile
can utilize environmental variables.
FILENAME=greeting.txt
.PHONY: all
all: $(FILENAME)
$(FILENAME): hello.txt world.txt symbol.txt
cat hello.txt world.txt symbol.txt > $(FILENAME)
hello.txt:
echo -n "Hello" > hello.txt
world.txt:
echo -n "World" > world.txt
.PHONY: clean
clean:
(test -e hello.txt && rm hello.txt) || true
(test -e world.txt && rm world.txt) || true
(test -e $(FILENAME) && rm $(FILENAME)) || true
make
# => cat hello.txt world.txt symbol.txt > greeting.txt
make
# => make: Nothing to be done for 'all'.
# normally make doesn't read from environmental variables
FILENAME=new.txt make
# => make: Nothing to be done for 'all'.
# but if you specify -e to make, it will use them
FILENAME=new.txt make -e
# => cat hello.txt world.txt symbol.txt > new.txt
# note that you can also assign the variable manually by giving it as an argument
make FILENAME=new.txt
make FILENAME=new.txt clean
make clean
make
also has simply expanded variables that are defined with :=
. It contains their values as of the time this variable was defined and usage is substituted verbatim. They are used to make Makefile
s more manageable and behave much like environmental variables.
FILENAME=greeting.txt
hello-file := hello.txt
world-file := world.txt
symbol-file := symbol.txt
.PHONY: all
all: $(FILENAME)
$(FILENAME): $(hello-file) $(world-file) $(symbol-file)
cat $(hello-file) $(world-file) $(symbol-file) > $(FILENAME)
$(hello-file):
echo -n "Hello" > $(hello-file)
$(world-file):
echo -n "World" > $(world-file)
.PHONY: clean
clean:
(test -e $(hello-file) && rm $(hello-file)) || true
(test -e $(world-file) && rm $(world-file)) || true
(test -e $(FILENAME) && rm $(FILENAME)) || true
echo -n "?" > other-symbol.txt
make symbol-file=other-symbol.txt
cat greeting.txt
# => HelloWorld?
Magic variables:
out.o: src.c src.h
$@ # "out.o" (the target)
$< # "src.c" (the first prerequisite)
$^ # "src.c src.h" (all of the prerequisites)
%.o: %.c
$* # the stem of the match (e.g. "foo" in "foo.c")
also:
$+ # dependency (all, with duplication)
$? # dependency (new ones)
$| # dependency (order-only dependencies)
$(@D) # target directory
Directories can be prerequisites too. A directory are considered modified if a files is added, modified, removed or renamed on it's root, so it's not recursive. Quite frequently you want to make directory prerequisites as order-only as discussed in the next section.
You can make some prerequisites order-only with |
separator. In a simple sense, this removes the last-updated check but does still check for existence. Usually used with directories that we don't want to recreate after any modification of the files included.
.PHONY: all
all: greetings/hello.txt greetings/hi.txt
# without the `|`s (order-only), the greetings target
# would be built each time the the directory
# gets modified (files added, modified, removed or
# renamed at directory root, or a new subdirectory)
greetings/hello.txt: | greetings
echo "Hello" > $@
greetings/hi.txt: | greetings
echo "Hi" > $@
greetings:
mkdir greetings
make
# mkdir greetings
# echo "Hello" > greetings/hello.txt
# echo "Hi" > greetings/hi.txt
make
# make: Nothing to be done for 'all'.
echo "Aloha" > greetings/aloha.txt
make
# make: Nothing to be done for 'all'.
Command prefixes:
- Ignore errors.
@ Don't print command, useful e.g. when actually printing something.
+ Run even if in "don't execute" mode.
You can also add the following statements in the header to upgrade your make
-life:
MAKEFLAGS ...
= crash on undefined variables and don't use special suffix handlingSHELL := bash
= always usebash
, regardless where/bin/sh
points to.SHELLFLAGS ...
= usebash
in strict mode e.g. crash if using undefined variable.ONESHELL:
= use one shell for all commands of a target, which is more logical.DELETE_ON_ERROR:
= if a command fails, the target is deleted.RECIPEPREFIX:
= change the default, error prone\t
indentation to something else
MAKEFLAGS += --warn-undefined-variables
MAKEFLAGS += --no-builtin-rules
SHELL := bash
.SHELLFLAGS := -eu -o pipefail -c
.ONESHELL:
.DELETE_ON_ERROR:
.RECIPEPREFIX = >
.PHONY: example
example:
> FILENAME=greeting.txt
> echo "Hello World!" > $$FILENAME
> cat $$FILENAME
> rm $$FILENAME
> echo "Bye!"
Consider using output files to point to generated external entities if possible.
# will only build a new image if any files under src/ have changed
docker-image: $(shell find src/ -type f)
TAG="example.com/my-app:$$(date +%s)"
docker build --tag="$${TAG}
echo "$${TAG}" > docker-image
make docker-image
cat docker-image
# => example.com/my-app:1593084080 or something like that
Consider using sentinel files if you are generating a lot of files.
out/.webpack.sentinel: $(shell find src/ -type f)
mkdir -p $(@D) # expands to `mkdir -p out`
node run webpack ..
touch $@ # expands to `touch out/.webpack.sentinel`