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.

Reply via email to