WARNINGS
March 20, 2026 · View on GitHub
unmake offers various checks to optimize your makefiles.
Note that unmake does not evaluate makefiles, and therefore ignores quirks arising from macro expansions.
Most warnings feature line numbers with an approximate location of the issue in the makefile.
General
MISSING_FINAL_EOL
When a text file suddenly reaches End Of File (EOF) without a Line Feed (LF), then the file is said to not feature a final End Of Line (EOL).
UNIX text files expect each line to terminate in a final End Of Line, including the last line. Omitting a final EOF can cause subtle text processing errors.
Fail
PKG = curl<EOF>
Pass
PKG = curl<LF>
<EOF>
Mitigation
- Configure EditorConfig and text editors to apply a final EOL.
PHONY_TARGET
Prerequisites of this special target are targets themselves; these targets (known as phony targets) shall be considered always out-of-date when the make utility begins executing. If a phony target’s commands are executed, that phony target shall then be considered up-to-date until the execution of make completes. Subsequent occurrences of .PHONY shall also apply these rules to the additional targets. A .PHONY special target with no prerequisites shall be ignored. If the -t option is specified, phony targets shall not be touched. Phony targets shall not be removed if make receives one of the asynchronous events explicitly described in the ASYNCHRONOUS EVENTS section.
--POSIX 202x Issue 8/D3
Briefly, make assumes that most rule targets are actual filenames. However, conventional targets named all, lint, install, uninstall, publish, test*, or clean*, are usually not actual filenames. These are logical targets.
When make is requested to perform these logical, top-level targets, then make needs to know not to apply the usual file-based caching. The way to do this is by declaring .PHONY: special rules, whose prerequisites are your logical targets.
You may write logical target declarations as whitespace delimited prerequisites in a single .PHONY: rule, or distribute logical target declarations among multiple .PHONY: rules.
As well, aggregate targets like port: cross-compile archive, that do not have any commands, are usually not actual filenames themselves. Aggregate, commandless targets are also logical targets. Which means that they should also have an entry as a prerequisite in a .PHONY: special rule. However, make sometimes infers target trees based on common C filename patterns. So we cannot reliably warn on potentially .PHONY worthy empty-command targets until a complete inferred rule system is implemented.
Due to the variance in artifact names, unmake cannot automate checking for all possible targets deserving .PHONY declarations. Neither make nor unmake knows this application-specific information. The makefile maintainer should supply this information, and configure any needed .PHONY declarations accordingly.
makefile authors may use variables in .PHONY prerequisites:
ALLTARGETS!=ls -a
.PHONY: $(ALLTARGETS)
The above example effectively treats every target as .PHONY, without resorting to boilerplate or breaking POSIX compatibility.
Fail
all:
echo "Hello World!"
test: test-1 test-2
test-1:
echo "Hello World!"
test-2:
echo "Hi World!"
clean:
-rm -rf bin
Pass
.PHONY: all
all:
echo "Hello World!"
.PHONY: test test-1 test-2
test: test-1 test-2
test-1:
echo "Hello World!"
test-2:
echo "Hi World!"
.PHONY: clean
clean:
-rm -rf bin
.PHONY: empty
empty:;
.PHONY: port
port: cross-compile archive
empty:;
port: cross-compile archive
If the hypothetical cross-compile and archive targets are themselves merely logical targets rather than filenames, then they should be declared .PHONY as well.
ALLTARGETS!=ls -a *
.PHONY: $(ALLTARGETS)
all: welcome
welcome:
echo "Hello World!"
Mitigation
- Avoid using make build artifacts named
all,test*, orclean*. - Declare any targets named
all,lint,install,uninstall,publish,test*, orclean*as.PHONY - Declare command-less rule targets as
.PHONY - Note that POSIX usually requires a semicolon (
;) when declaring rules without commands. - Note that special targets like
.NOTPARALLEL,.PHONY,.POSIX,.WAIT, etc., should not themselves be declared as.PHONY
MAKEFILE_PRECEDENCE
By default, the following files shall be tried in sequence: ./makefile and ./Makefile.
--POSIX 202x Issue 8/D3
Using the lowercase filename makefile loads slightly faster than the capitalized filename Makefile. The lowercase naming reduces strain on filesystem requests.
Fail
Makefile:
PKG = curl
Pass
makefile:
PKG = curl
Mitigation
- Rename
Makefiletomakefile
CURDIR_ASSIGNMENT_NOP
The CURDIR environment variable shall not affect the value of the CURDIR macro unless the -e option is specified. If the -e option is not specified, there is a CURDIR environment variable set, and its value is different from the CURDIR macro value, the environment variable value shall be set to the macro value. If CURDIR is defined in the makefile, present in the MAKEFLAGS environment variable, or specified on the command line, it shall replace the original value of the CURDIR macro in accordance with the logical order described above, but shall not cause make to change its current working directory.
Assignment to CURDIR does not actually change the current working directory of the make execution.
Fail
CURDIR = build
Mitigation
- Avoid assigning the
CURDIRmacro - Note that some commands offer a built-in way to adjust the current directory, e.g.
tar -C <dir> - Promote complex logic to a dedicated script
WD_NOP
make often resets the working directory across successive commands, and across successive rules. Common commands for changing directories, such as cd, pushd, and popd, may not have the desired effect.
Furthermore, push and popd are GNU bash extensions to the POSIX sh interpreter standard, and are likely to fail on other machines.
Fail
all:
cd foo
all:
pushd foo
all:
popd
Mitigation
- Avoid running makefile commands beginning with
cd,pushd, orpopd - Reduce use of shell implementation-specific commands in makefiles
- Note that some commands offer a built-in way to adjust the current directory, e.g.
tar -C <dir> - Promote complex logic to a dedicated script
WAIT_NOP
When .WAIT appears as a target, it shall have no effect.
.WAIT is intended for use as a pseudo-prerequisite marker, in order to customize synchronization logic. .WAIT behaves as a useless no operation (NOP) when written as a target.
Fail
.WAIT:
test: test-1 test-2
test-1:
echo "Hello World!"
test-2:
echo "Hi World!"
Pass
test: test-1 .WAIT test-2
test-1:
echo "Hello World!"
test-2:
echo "Hi World!"
test: test-1 test-2
test-1:
echo "Hello World!"
test-2:
echo "Hi World!"
Mitigation
- Use
.WAITas an optional pseudo-prerequisite syncronization marker - Avoid declaring
.WAITas a target.
PHONY_NOP
A .PHONY special target with no prerequisites shall be ignored.
.PHONY with no prerequisites behaves as a useless no operation (NOP). When using the special target .PHONY rule, specify at least one prerequisite.
Fail
.PHONY:
foo: foo.c
gcc -o foo foo.c
Pass
foo: foo.c
gcc -o foo foo.c
clean:
rm -rf bin
.PHONY: clean
foo: foo.c
gcc -o foo foo.c
clean:
rm -rf bin
Mitigation
- Use
.WAITas an optional pseudo-prerequisite syncronization marker - Avoid declaring
.WAITas a target.
REDUNDANT_NOTPARALLEL_WAIT
The .WAIT pseudo-prerequisite disables asynchronous processing between prerequisites of a specific rule.
.NOTPARALLEL: disables asyncronous processing for all prerequisites in all rules.
Using both of these special targets simultaneously is unnecessary.
Fail
.NOTPARALLEL:
test: test-1 .WAIT test-2
test-1:
echo "Hello World!"
test-2:
echo "Hi World!"
Pass
test: test-1 .WAIT test-2
test-1:
echo "Hello World!"
test-2:
echo "Hi World!"
.NOTPARALLEL:
test: test-1 test-2
test-1:
echo "Hello World!"
test-2:
echo "Hi World!"
test: test-1 test-2
test-1:
echo "Hello World!"
test-2:
echo "Hi World!"
Mitigation
- Avoid using
.NOTPARALLEL:with.WAITredundantly. - Redundancy of
.WAITwith.NOTPARALLELis best avoided.
REDUNDANT_SILENT_AT
At (@) elides an individual command from make output. This is useful for reducing log noise.
The .SILENT special target also elides commands from make output. If the special rule .SILENT: is declared with no prerequisites, then all make commands globally are silenced. If the special rule .SILENT: is declared with prerequisite targets, then all commands for those specific targets are silenced.
Using both of these simultaneously is unnecessary.
Fail
.SILENT:
lint:
@unmake .
.SILENT: lint
lint:
@unmake .
Pass
.SILENT:
lint:
unmake .
lint:
@unmake .
lint:
unmake .
Mitigation
- Avoid using at (
@) with.SILENTredundantly. - Redundancy of
.SILENTwith at (@) is best avoided.
REDUNDANT_IGNORE_MINUS
Hyphen-minus (-) continues makefile execution past soft failure exit codes of an individual command. This is useful for implementing cleanup tasks and other idempotent tasks.
The .IGNORE special target also continues makefile execution past soft failures. If the special rule .SILENT: is declared with prerequisite targets, then exit codes for commands for those specific targets are ignored. However, declaring .IGNORE: with no prerequisites is likely to cause subtle build problems.
Using both - and .IGNORE simultaneously is unnecessary.
If the special rule .IGNORE: is declared with no prerequisites, then exit codes of all make commands globally are ignored. Due to more severe issues with .IGNORE: declared with no prerequisites, detailed in the GLOBAL_IGNORE check, the REDUNDANT_IGNORE_MINUS check does not provide an automatic check for redundant - with a global .IGNORE: declaration.
Fail
.IGNORE:
clean:
-rm -rf bin
.IGNORE: clean
clean:
-rm -rf bin
Pass
IGNORE: clean
clean:
rm -rf bin
clean:
-rm -rf bin
clean:
rm -rf bin
Mitigation
- Note that
.IGNORE:declared with no prerequisites is likely to cause subtle build problems. - Avoid using hyphen-minus (
-) with.IGNOREredundantly. - Redundancy of
.IGNOREwith hyphen-minus (-) is best avoided.
GLOBAL_IGNORE
When the special target rule .IGNORE: is declared with no prerequisites, then make ignores exit codes for all make commands, for all rules. This is hazardous, and tends to invite file corruption.
Caution: Avoid using .IGNORE: this way. When using the special target .IGNORE rule, declare at least one prerequisite.
Fail
.IGNORE:
foo: foo.c
gcc -o foo foo.c
clean:
rm -f foo
Pass
.IGNORE: clean
foo: foo.c
gcc -o foo foo.c
clean:
rm -f foo
foo: foo.c
gcc -o foo foo.c
clean:
-rm -f foo
foo: foo.c
gcc -o foo foo.c
clean:
rm -f foo
Mitigation
- Avoid using
.IGNORE:without at least one prerequisite. - Optionally, apply hyphen-minus (
-) to individual commands.
SIMPLIFY_AT / SIMPLIFY_MINUS
Using at (@) or hyphen-minus (-) command prefixes for several individual commands in a rule can be simplified to a .SILENT or .IGNORE declaration respectively.
Due to flexibility needs, this warning emits automatically for rules with at least two or more commands, where all of the commands feature the same at (@) or hyphen-minus (-) prefix. Rules with zero or one command, and rules with mixed command prefixes, may not trigger this warning.
We generally recommend using .SILENT / .IGNORE over individual at (@) / hyphen-minus (-).
Fail
welcome:
-echo foo
-echo bar
-echo baz
welcome:
@echo foo
@echo bar
@echo baz
Pass
.IGNORE: welcome
welcome:
echo foo
echo bar
echo baz
.SILENT: welcome
welcome:
echo foo
echo bar
echo baz
.SILENT:
welcome:
echo foo
echo bar
echo baz
Mitigation
- Use
.SILENT/.IGNOREtargets rather than individual at (@) / hyphen-minus (-) targets. - Note that
.IGNOREmay have poor behavior without at least one prerequisite.
IMPLEMENTATION_DEFINED_TARGET
The interpretation of targets containing the characters '%' and '"' is implementation-defined.
POSIX make has no portable semantic for percent signs (%) or double-quotes (") in targets or prerequisites. Using these can vendor lock a makefile onto a specific make implementation, and/or trigger build failures.
Fail
all: foo%
foo%: foo.c
gcc -o foo% foo.c
all: "foo"
"foo": foo.c
gcc -o "foo" foo.c
Mitigation
- Avoid percents (
%) and double-quotes ("), in targets and prerequisites.
COMMAND_COMMENT
When a rule command contains a sharp (#), then make forwards the comment to the shell interpreter. This can cause the command to fail in multiline commands. This can cause the command to fail in certain shell interpreters. This increases log noise.
Fail
foo: foo.c
#build foo
gcc -o foo foo.c
foo: foo.c
@#gcc -o foo foo.c
foo: foo.c
-#gcc -o foo foo.c
foo: foo.c
+#gcc -o foo foo.c
foo: foo.c
gcc \
#output file \
-o foo \
foo.c
Pass
foo: foo.c
#build foo
gcc -o foo foo.c
#build foo
foo: foo.c
gcc -o foo foo.c
#output file
foo: foo.c
gcc \
-o foo \
foo.c
foo: foo.c
gcc -o foo foo.c
#foo: foo.c
# gcc -o foo foo.c
<remove rule>
Mitigation
- Move comments up above multiline commands.
- Move comments to the leftmost column, fully dedented.
- Consider removing extraneous lines.
REPEATED_COMMAND_PREFIX
Supplying the same command prefix multiple times is wasteful.
Fail
test:
@@+-+--echo "Hello World!"
Pass
test:
@+-echo "Hello World!"
test:
echo "Hello World!"
(Any combination of @, +, and - is fine as long as none of the prefix types are duplicated.)
Mitigation
- Remove redundant code.
- Code that is redundant should be removed.
BLANK_COMMAND
Rule commands consisting of nothing more than at (@), plus (+), minus (-) prefixes, and/or whitespace, can produce spurious results when the essentially empty command is executed. Without any prefixes, blank commands are likely to trigger parse errors.
Blank commands are distinct from blank lines, which normally act as comments.
Blank commands are distinct from rules that are reset to have no commands.
Fail
test:
@+-
Pass
test:
@+-echo "Hello World!"
test:
@+-echo "Hello World!"
test:;
#test:
<rule removed>
Mitigation
- Give the command something useful to do.
- Remove extraneous code.
WHITESPACE_LEADING_COMMAND
After any optional @/+/- prefix modifiers, whitespace leading a command is bad form. In commands, leading whitespace may be a sign of a typo in an earlier multiline instructions.
The earliest whitespace for a command, should consist mainly of the standard one-tab indentation.
Successive lines in a multiline make command commonly may use tabs for visual clarity.
Fail
foo:
<tab><space>gcc -o foo foo.c
foo:
<tab>@+-<space>gcc -o foo foo.c
Pass
foo:
<tab><no space>gcc -o foo foo.c
foo:
<tab>@+-<no space>gcc -o foo foo.c
foo:
gcc \
-o \
foo \
foo.c
Mitigation
- Verify multiline instruction syntax in earlier commands.
- Avoid inserting whitespace between
@/+/-prefix modifiers and the rest of the command. - Generally, avoid starting commands with whitespace.
- Consider indenting successive lines in a multiline make command with 1 tab (prerequisites) or 2 tabs (commands), for visual clarity.
NO_RULES
make generally expects a makefile to define at least one (non-special) rule to provide some action on when running make. Excepting include files like sys.mk or *.include.mk.
Fail
makefile:
.POSIX:
PKG = curl
Pass
makefile:
.POSIX:
PKG = curl
all:
apt install -y $(PKG)
provision.include.mk:
PKG = curl
Mitigation
- Declare at least one non-special rule in most makefiles.
- Rename include files to
*.include.mk.
RULE_ALL
make interprets the first non-special rule as the default rule. Apart from non-special targets, the top-most rule is conventionally named all. This helps to avoid confusion and accidents.
Common include files like sys.mk and *.include.mk may export rules, named all or something else. However, the top-most, non-special default rule semantic still applies, so order include lines and rule declarations carefully.
Fail
makefile:
.POSIX:
build:
echo "Hello World!"
Pass
makefile:
.POSIX:
all:
echo "Hello World!"
Optionally, list subsequent rules as prerequisites for the all target.
makefile:
.POSIX:
all: build
build:
echo "Hello World!"
foo.include.mk:
build:
echo "Hello World!"
Mitigation
- Name the top-most, non-special, default rule
all. - Note that the order of
includelines and rule declarations interacts with the top-most, non-special, default rule.
STRICT_POSIX
To receive exactly the behavior described in this section, the user shall ensure that a portable makefile shall:
• Include the special target .POSIX
• Omit any special target reserved for implementations (a leading period followed by uppercase letters) that has not been specified by this section
The behavior of make is unspecified if either or both of these conditions are not met.
It is good form to begin most makefiles with a .POSIX: special target rule marker. This marker instructs make implementations to preserve processing semantics as defined in the POSIX standard without alteration. Omitting the marker may result in unknown behavior. So most makefiles benefit from more predictable behavior by leading with .POSIX:. You can declare this marker at the very first line of a makefile, or after some blank/comment lines.
However, makefiles named *.include.mk, designed for simple text inclusion into other makefiles, should omit the .POSIX: marker.
Also, make distributions commonly install a sys.mk include file that provides defines a foundational set of macros, include lines, and rules for make implementations. A .POSIX: marker may not be necessary for make distribution files. As well, files named like GNUmakefile, that are known to be implementation-specific, should not use this marker. But most any POSIX makefile not named to indicate its intention as an include file, should feature the .POSIX: marker.
Fail
makefile:
PKG = curl
Pass
makefile:
.POSIX:
PKG = curl
provision.include.mk:
PKG = curl
GNUmakefile:
PKG = curl
Special targets like .POSIX and .PHONY are important, but they may be elided from other passing examples in this document, for brevity.
Mitigation
- Declare
.POSIX:in most makefiles. - Rename makefiles intended for inclusion to
*.include.mk. - Avoid declaring
.POSIX:in makefiles for specific implementations likeGNUmakefile.
RESERVED_TARGET
Targets and prerequisites consisting of a leading
<period>followed by the uppercase letters "POSIX" and then any other characters are reserved for future standardization. Targets and prerequisites consisting of a leading<period>followed by one or more uppercase letters, that are not described above, are reserved for implementation extensions.
Other than certain special targets, POSIX reserves targets and prerequisites of the form .(A-Z)... for either future POSIX use, or for implementation-specific extensions.
Generally, such targets are non-portable. However, the user may have simply mistyped a well-known POSIX special target name. Note that typos may trigger parse errors.
Fail
.POSIXX:
.TEST:
echo "Hello World!"
test: .TEST-UNIT .TEST-INTEGRATION
Pass
.POSIX:
foo: foo.c
gcc -o foo foo.c
test: foo
./foo
test: test-unit test-integration
Mitigation
- Avoid using reserved names in targets or prerequisites.
- Consider pair programming to spot typos.
Undefined Behavior (UB)
Linter warnings concerning UB level portability issues tend to carry higher risk compared to other warnings. This is a consequence of the POSIX standard not specifying any particular error handling (or error detection) semantic for make implementations to follow.
In the case of UB, a makefile may trigger an error message during certain project builds, silently skip processing, corrupt files, segfault, fire missiles, and/or any number of undefined behaviors.
UB_LATE_POSIX_MARKER
If it appears as the first non-comment line in the makefile, make shall process the makefile as specified by this section; otherwise, the behavior of make is unspecified.
When the .POSIX: rule is used in a makefile, it must be the first thing in the makefile, apart from any blank or commented lines.
Note that common include files like sys.mk or *.include.mk, should not present .POSIX:, as their text necessarily copies as late as some earlier include line in an outer makefile.
Fail
makefile:
PKG = curl
.POSIX:
makefile:
.POSIX:
.POSIX:
provision.include.mk:
.POSIX:
PKG = curl
Pass
makefile:
.POSIX:
PKG = curl
provision.include.mk:
PKG = curl
Mitigation
- Move
.POSIXto the first non-blank, non-commented line in the makefile. - Avoid mixing the
.POSIXtarget with other targets in a single rule declaration. - Avoid declaring
.POSIX:insys.mkor*.include.mk. - Avoid declaring
.POSIX:multiple times.
UB_AMBIGUOUS_INCLUDE
This standard does not specify precedence between macro definition and include directives. Thus, the behavior of:
include =foo.mkis unspecified.
Ambiguous include/macro instructions do not have a clear meaning. The instruction may behave as include the path =foo.mk, or behave as defining a macro with the name include and the value foo.mk. Parsing destabilizes.
Fail
include =foo.mk
Pass
include=foo.mk
include foo.mk
INCLUDE = include
$(INCLUDE) =foo.mk
PTH = =foo.mk
include $(PTH)
Mitigation
- Avoid using equals (
=) in path names. - Avoid using lowercase
includeas a macro name. - Consider removing whitespace between macro names and assignment operators.
UB_MAKEFLAGS_ASSIGNMENT
The result of setting MAKEFLAGS in the Makefile is unspecified.
The MAKEFLAGS macro is designed as read-only, set aside for make implementations to store command line flags.
POSIX compliant make implementations automatically preserve command line flags with MAKEFLAGS.
make implementations implicitly forward MAKEFLAGS to any child $(MAKE) invocations on behalf of the makefile user.
Fail
MAKEFLAGS = -j
all:
$(MAKE) $(MAKEFLAGS) foo.mk
Pass
all:
$(MAKE) foo.mk
Mitigation
- Avoid assigning to the
MAKEFLAGSmacro. - Move complex logic to a dedicated script.
UB_SHELL_MACRO
The value of the SHELL environment variable shall not be used as a macro and shall not be modified by defining the SHELL macro in a makefile or on the command line.
SHELL provides low level functionality to make implementation internals. Expanding or assigning this macro is discouraged.
make implementations that use SHELL, tend to set useful defaults. Overriding the defaults may produce non-portable, fragile makefiles.
Some implementations do not define SHELL. Assigning a value SHELL can create an misleading, non-portable impression of makefile behavior.
Due to unmake not evaluating macro expansions, expansion of the SHELL macro is not implemented as an automatic check.
Some ancient platforms may present SHELL with a cmd[.exe] interpreter. But even Windows Command Prompt, the Chocolatey GNU make interpreter tends to default to a POSIX compliant sh interpreter suitable for use with makefile commands.
Fail
SHELL = sh
all:
$(SHELL) script.sh
all:
${SHELL} script.sh
Pass
all:
./script.sh
sh -c "echo $$SHELL"
Mitigation
- Avoid assignments to the
SHELLmakefile macro. - Treat the
SHELLmakefile macro as a private, internal make macro - Note that a distinct
SHELLenvironment variable may be available to commands, apart from theSHELLmake macro. - Move complex shell logic to a dedicated shell script.
SILENT_INCLUDE
If, after proceeding as described above, the file still cannot be opened:
If the word include was prefixed with a
character, the file shall be ignored. Otherwise, an error shall occur.
Silent includes suppress error handling in the event that an include line is unable to process. Error suppressions like this risk system corruption and overcomplicate troubleshooting.
Fail
-include foo.mk
Pass
include foo.mk
Mitigation
- Avoid silent include lines.
- Delegate fancy logic into a shell script or a separate build system.
NONPORTABLE_ASSIGNMENT
The single colon equals (:=) macro assignment operator is not portable POSIX. It is an extension that risks different behavior across make implementations.
Instead, select a modern, portable macro assignment operator. POSIX make supports a variety of portable assignment operators with specialized evaluation semantics for assorted use cases.
Fail
CLIENT := curl --version
Pass
CLIENT ::= curl --version
CLIENT :::= curl --version
CLIENT ?= curl --version
CLIENT != curl --version
CLIENT += curl --version
CLIENT = curl --version
Mitigation
- Replace nonportable
:=assignments with modern POSIX equivalents.