In GNU Make 4.4 (confirmed on 4.4.1), "export VAR ?= $(shell COMMAND)"
re-evaluates $(shell) in every recursive sub-make, even though the variable was
already evaluated and exported by the top make and the sub-make's $(origin VAR)
correctly reports "environment". Per the documented semantics of ?= ("set only
if the variable is not already set") and the fact that "environment" qualifies
as "set", the RHS should not be evaluated again. Make 4.3 honors this; 4.4.1
does not.
The redundant shell-outs scale linearly with sub-make recursion count and
assignment count, which can be catastrophic for projects with many such
patterns and deep $(MAKE) -C chains. We have one real-world Makefile that runs
in ~33 ms with 400 execve under 4.3 and runs for 30+ seconds with 25,000+
execve under 4.4.1, same source tree.
ENVIRONMENT
Reproduces on:
GNU Make 4.4.1, Ubuntu 26.04, bash 5.3.9, Linux 7.0.0-15
Does not reproduce on:
GNU Make 4.3, Ubuntu 24.04, bash 5.2.21, Linux 6.8.0-107
REPRODUCER
Save as Mfile (the lines below "$(PHASES):" and "inner-%:" must begin with a
TAB):
export TOP ?= $(shell pwd)
export SUB ?= $(shell readlink -f $(TOP)/sub)
export OTHER ?= $(shell readlink -f $(TOP)/other)
SUBDIRS := a b c d e f g h i j
PHASES := p1 p2 p3 p4
.PHONY: all $(PHASES) inner-%
all: $(PHASES)
$(PHASES):
@for d in $(SUBDIRS); do $(MAKE) -f $(firstword $(MAKEFILE_LIST))
inner-$$d-$@; done
inner-%:
@echo "origin TOP=$(origin TOP) SUB=$(origin SUB) OTHER=$(origin OTHER)"
Run:
strace -f -e trace=execve -o trace.log make -f Mfile >/dev/null
grep -oP 'execve\("/usr/bin/(readlink|pwd)"' trace.log | sort | uniq -c
make -f Mfile 2>&1 | grep 'origin TOP' | head -1
EXPECTED (observed under make 4.3)
12 execve("/usr/bin/pwd"
8 execve("/usr/bin/readlink"
origin TOP=environment SUB=environment OTHER=environment
Sub-make sees the variables as environment-origin and ?= correctly no-ops;
$(shell) does not re-run.
ACTUAL (observed under make 4.4.1)
36 execve("/usr/bin/pwd"
48 execve("/usr/bin/readlink"
origin TOP=environment SUB=environment OTHER=environment
Sub-make also sees environment-origin, but $(shell) still re-runs. 3x-6x the
shell-outs of 4.3 even for this small Makefile.
DIAGNOSIS
The ?= operator is documented as approximately equivalent to:
ifeq ($(origin VAR),undefined)
VAR = ...
endif
Since $(origin VAR) returns "environment" (not "undefined") in the sub-make,
the ?= line should be a no-op and the $(shell ...) on the RHS should never be
evaluated. Under 4.4.1 the $(shell ...) is evaluated anyway, suggesting the ?=
"is variable set?" check or its RHS-evaluation guard was broken between 4.3 and
4.4.
WORKAROUND
Replacing ?= with := (immediate assignment, computed once at top-make parse
time) avoids the problem at the cost of breaking command-line variable
override. An override-safe workaround is to guard explicitly:
ifeq ($(origin TOP),undefined)
export TOP := $(shell pwd)
endif
Either form makes the Makefile compatible with both 4.3 and 4.4.
This email may contain confidential and privileged information and is intended
solely for the use of the addressee(s). Unless you are the addressee or are
authorized to receive messages for the addressee, you may not use, copy,
disseminate, or disclose the information or any attachments to any third party.
If you have received this correspondence in error, please notify the sender
immediately and delete this email. Your cooperation and understanding are
greatly appreciated. Attention Federal Customers: Please note this email
platform is NOT approved to communicate (send or receive) CUI. For questions on
the approved system to communicate CUI, please contact your designated Vcinity
Representative.