Skip to content

esp32 (cmake): mpy-cross sub-make inherits a command-line BUILD=, breaking the firmware link #19667

Description

@bdbarnett

Observed on a pristine v1.28.0 checkout (git describe = v1.28.0; py/mkrules.cmake, py/makeqstrdefs.py, mpy-cross/, and py/modstring.c all unmodified), esp32 port, ESP32_GENERIC_S3.

The mpy-cross bootstrap in py/mkrules.cmake already insulates the sub-make from one inherited command-line variable:

COMMAND ${MICROPY_MAKE_EXECUTABLE} -C ${MICROPY_DIR}/mpy-cross USER_C_MODULES=

but not from BUILD=. GNU make propagates command-line variables to sub-makes, so building the esp32 port with an out-of-tree build directory:

make BOARD=ESP32_GENERIC_S3 BUILD=/path/to/out-of-tree

causes mpy-cross to be built into /path/to/out-of-tree as well. mpy-cross compiles the same py/*.c sources under its own configuration — mpy-cross/mpconfigport.h:82 sets MICROPY_PY_TSTRINGS (1) explicitly (MICROPY_PY_FSTRINGS (1) on line 81) — so its qstr module-registration fragments land in the firmware build's shared genhdr/module/. The leaked fragments are recognizable by their relative naming: makeqstrdefs maps .. → @@ and / → __, so @@__py__modstring.c.module reads ../py/modstring.c — the path as mpy-cross sees it from its own directory — unlike the firmware's absolute-path fragment names.

Six fragments leak (modbuiltins, modmath, modmicropython, modstring, modstruct, runtime); only string breaks the link, because the others register modules that exist in both configurations, while string is gated behind MICROPY_PY_TSTRINGS, which is 0 at the esp32 port's EXTRA_FEATURES ROM level. makeqstrdefs.py cat collects the directory blindly, moduledefs.h then declares mp_module_string, the firmware compiles py/modstring.c empty, and the link fails:

undefined reference to `mp_module_string'

Overriding BUILD is an ordinary thing to attempt (the port's own BUILD ?= build-$(BOARD) invites it), and the failure appears far from its cause.

Reproduction (isolated, no firmware build needed) — a parent make invoking the sub-make exactly as mkrules.cmake does:

# Makefile
unfixed:
	$(MAKE) -C $(MP)/mpy-cross USER_C_MODULES=

$ make unfixed MP=/path/to/micropython BUILD=$PWD/leaked
$ ls leaked/            # mpy-cross binary + genhdr/module/@@__py__*.module fragments

Fix, verified under the same parent-make reproduction (an explicit assignment on the sub-make command line takes precedence over the propagated one):

COMMAND ${MICROPY_MAKE_EXECUTABLE} -C ${MICROPY_DIR}/mpy-cross BUILD=${MICROPY_DIR}/mpy-cross/build USER_C_MODULES=

With that line, the same parent invocation leaves the parent's BUILD dir untouched and mpy-cross lands in mpy-cross/build/, matching MICROPY_MPYCROSS_DEPENDENCY, which is already defined as ${MICROPY_DIR}/mpy-cross/build/mpy-cross. Two notes: BUILD= (empty) would NOT work — mpy-cross's BUILD ?= build does not override a defined-but-empty variable, so the value must name the path — and the fix prevents recurrence but does not repair an already-polluted build directory, which must be deleted before rebuilding.

Activity

  1. pablogventura commented on Aug 31, 2026

    @pablogventura
    Contributor

    I'll send a PR for this: pass an explicit BUILD=.../mpy-cross/build into the mpy-cross sub-make (cmake and make bootstraps), same pattern as clearing USER_C_MODULES / FROZEN_MANIFEST. Empty BUILD= is not enough because of BUILD ?= build in mkenv.mk.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions