ruk·si

make

Updated at 2021-08-16 07:00

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 handling
  • SHELL := bash = always use bash, regardless where /bin/sh points to
  • .SHELLFLAGS ... = use bash 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`

Sources