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 Makefiles 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/shpoints to.SHELLFLAGS ...= usebashin 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\tindentation 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`