Because GNU make runs each recipe line in its own separate sub-shell. The cd build succeeds, but it only changes the working directory of the short-lived shell that ran that one line; that shell then exits. The next line, make all, is handed to a brand-new shell that starts back in the directory make invoked it from. cd (like setting shell variables) only affects the process it runs in, so its effect is gone before the next line begins.
This is documented behavior: recipe lines are executed by invoking a new sub-shell for each line of the recipe, and consequently setting shell variables and invoking shell commands such as cd that set a context local to each process will not affect the following lines in the recipe.
Three fixes:
-
Put both commands on one logical line so they run in the same shell. Use && (not ;) so a failed cd aborts instead of running make in the wrong place:
target:
cd build && $(MAKE) all
-
Join across multiple source lines with a trailing backslash — make passes the joined text to a single shell:
target:
cd build &&
$(MAKE) all
-
Enable .ONESHELL, which tells make to feed all lines of every recipe to one shell invocation, so cd and variables persist down the recipe:
.ONESHELL:
target:
cd build
$(MAKE) all
Notes: Prefer $(MAKE) over a literal make so flags propagate to the sub-make and -n/-t work correctly. With .ONESHELL, the recipe-line prefixes (@, -, +) are only honored on the first line and otherwise stripped, and error handling spans the whole block rather than per line — so option 1 (cd build && $(MAKE) all) is the most portable and least surprising. A cleaner alternative is to avoid cd entirely with make's directory flag: $(MAKE) -C build all.
Sources: