Retired and archived, 2026-09-23. Nothing builds from this repository any more. Its job is done by upstream MicroPython's and CircuitPython's own
makewith the layout the PyDevices repositories assume: every repository a sibling of the interpreter checkout, and micropython-pydevices carrying the presets (manifests/, onec_module()line per module), the out-of-tree variants and boards, and the patch series withtools/prepare-micropython.shto apply it once. CircuitPython builds from each C-carrying repository'sapply_cp_patches.shagainst one checkout, thenmake. The workspace that installs the desktop interpreters keeps itsbuild_interpreters.shandprovenance.pyunder its owntools/. Everything below is history.
An optional multi-module workspace layout and build helper for MicroPython and CircuitPython.
cmods makes it easy to build custom firmware containing multiple user C modules side-by-side (such as displayif, lvgl-micropython, and pygraphics):
- MicroPython external C modules (
USER_C_MODULES,micropython.mk/micropython.cmake) - CircuitPython native extensions (
shared-bindings/shared-module, via each repo'sapply_cp_patches.sh)
Note for Experienced Developers:
cmodsis completely optional. If you already have an established build habit (such as standardmake USER_C_MODULES=...or custom CMake workflows), you can continue building MicroPython and CircuitPython exactly as you always have. This workspace is provided as a friction-free aggregator to make multi-module firmware development faster and more convenient.
Option A — clone this repo (recommended when starting fresh):
git clone https://github.com/PyDevices/cmods.git
cd cmodsOption B — copy into an existing build workspace (when you already have a directory with MicroPython / CircuitPython / usermods):
Copy the contents of this repo into that workspace root (the directory that
should contain build_mp.sh, manifest-*.py, and optionally patches/). Do
not nest a second cmods/ folder unless you intend that to be the workspace
root.
Either clone them into the workspace, or clone them as siblings and symlink:
# Into the workspace
git clone https://github.com/micropython/micropython.git micropython
cd micropython && git submodule update --init --recursive && cd ..
# Or as siblings + symlink (example layout: ../micropython next to the workspace)
ln -s ../micropython micropythonRepeat for each usermod or interpreter you want (displayif, pygraphics,
lvgl-micropython, circuitpython, …). Each MicroPython usermod must be an immediate subdirectory of the workspace (clone or symlink) and provide a micropython.mk there (optional manifest.py for frozen Python).
For audio, link two: audiodsp is the DSP
nodes and the audio pump's portable engine, and
audioif is the pump's platform drivers.
With only the first the firmware still builds and plays, but
audiopump.driver() says 'none' and audiodev goes out through machine.I2S.
patches/ is optional. When present, build_mp.sh applies every
file whose name contains micropython-<port> for the selected --port (e.g.
micropython-unix, micropython-windows). They are temporary working-tree
overlays which are reversed on every exit; the upstream checkout stays at its
pinned revision with no generated commits. Details: patches/README.md.
# Optional — only for LVGL
git clone https://github.com/PyDevices/lvgl-micropython.git lvgl-micropython
git clone https://github.com/PyDevices/lvgl-bindings.git lvgl-bindings
cd lvgl-bindings && git submodule update --init lvgl && cd ..
./lvgl-bindings/regenerate_all.sh --target micropython
./build_mp.sh --port unix --variant standardThe LVGL clone and regenerate_all.sh steps are optional — use them only
when building with LVGL. For other user C modules, add those repos (or
symlinks) instead.
- Since MicroPython 1.29 the frozen manifest names every C module (
c_module()), soUSER_C_MODULESis not passed at all; each sibling'smanifest.pynames its own C half andmanifest-micropython.pyincludes them manifest-micropython.py— frozen Python from cmod sibling repos, then includes the MicroPython upstream freeze viaFROZEN_MANIFEST_UPSTREAMmanifest-circuitpython.py— same aggregator shape for CircuitPython (build_cp.sh)build_mp.sh— setsFROZEN_MANIFEST_UPSTREAMto the freeze file MicroPython would use for the selected port/board/variant (same as a manualmakewithout override)build_cp.sh— auto-discovers*/apply_cp_patches.sh(optional extensions) and usesmanifest-circuitpython.pyfor all ports- There is no aggregator
micropython.cmakeany more: CMake ports get their modules from the manifest'sc_module()lines like the Make ports
| Script | Role |
|---|---|
build_mp.sh |
Any MicroPython port (interactive or --port / --board / --variant) |
build_cp.sh |
CircuitPython ports (interactive or --port / --board / --variant) |
build_interpreters.sh |
Desktop/wasm interpreters for local pydevices-examples work — see below |
Examples:
./build_mp.sh # interactive
./build_mp.sh --port unix --variant standard
./build_mp.sh --port rp2 --board RPI_PICO2_W
./build_mp.sh --port esp32 --board ESP32_GENERIC_P4 --variant C6_WIFI
./build_cp.sh # interactive
./build_cp.sh --port unix --variant coverage
./build_interpreters.sh --only mp-unix,cp-unixbuild_interpreters.sh builds the host interpreters used by PyDevices and installs them under workspace bin/ (micropython, micropython.exe, circuitpython, and the wasm micropython.{mjs,wasm} pair). Targets are mp-unix, mp-windows, mp-wasm, and cp-unix. The mp-wasm target builds the external pydevices variant into build-pydevices. When the pydevices core repo sits as a sibling, the script copies the binaries into pydevices/bin/; when the portal is a sibling, it installs the WebAssembly pair under PyDevices.github.io/vendor/micropython/. Use --only to build a subset, or --install-only to refresh installs from an existing build.
Desktop SDL (usdl2): when displayif is present,
MicroPython unix / windows and CircuitPython unix link native import usdl2
from that repo (not a separate usdl2 usermod). Unix needs libsdl2-dev. Windows
needs an unpacked SDL2 MinGW development ZIP
under the workspace (e.g. SDL2-2.30.10/); build_mp.sh auto-sets SDL2_DEV
or you can export it (see displayif tools/sdl2_dev_env.sh).
End-to-end bring-up for the
Waveshare ESP32-P4-WIFI6-Touch-LCD-4B
(4″ 720×720 ST7703 on MIPI DSI, GT911 on I2C) using displayif + pydevices-examples.
This is not stock MicroPython — firmware must include the displayif mipidsi
cmod.
Board configs (pydevices):
| Interpreter | Path |
|---|---|
| MicroPython | board_configs/fbdisplay/esp32-p4-wifi6-touch-lcd-4b |
| CircuitPython | board_configs/cp/fbdisplay/esp32-p4-wifi6-touch-lcd-4b |
git clone https://github.com/PyDevices/displayif.git displayif
# C6_WIFI — this board’s ESP32-C6 WiFi/BLE coprocessor (use C5_WIFI if yours is C5)
./build_mp.sh --port esp32 --board ESP32_GENERIC_P4 --variant C6_WIFIbuild_mp.sh can flash when the build finishes (offset from board.json,
0x2000 for ESP32_GENERIC_P4). Manual flash:
esptool -b 460800 --before default_reset --after hard_reset \
write_flash 0x2000 micropython/ports/esp32/build-ESP32_GENERIC_P4/firmware.binThe cmods build discovers pydevices/manifest.py and freezes the
core product packages. Install the matching board config (and optional
examples from pydevices-examples) with mpremote:
mpremote mip install --target "." \
"github:PyDevices/pydevices/board_configs/fbdisplay/esp32-p4-wifi6-touch-lcd-4b"
# optional: mpremote mip install --target "./examples" "github:PyDevices/pydevices-examples/packages/examples.json"Smoke checks:
mpremote run displayif/tools/test_mipidsi_smoke.pyimport board_config
import appdev
display_drv = board_config.display_drv
app = appdev.App(board_config)
display_drv.fill_rect(0, 0, 200, 200, 0xF800)
display_drv.show()
# Touch: poll until quit
while not app.quit_requested:
for e in app.poll():
print(e)Pinout matches the Waveshare BSP (reset 27, backlight 26, I2C 7/8,
GT911 @ 0x5D). If the panel stays black, check backlight polarity
(backlight_on_high=False in the board config) and the displayif P4 DSI LDO
path (channel 3 @ 2.5 V). Validate display/touch over USB serial before WiFi.
For the full, always-current map of PyDevices repos and how they relate, see the generated
ecosystem map — rendered
from dotgithub/data/repos_db.json,
so it cannot drift out of date the way a hand-maintained list here would.
CircuitPython does not use USER_C_MODULES. Clone lvgl-circuitpython into this workspace if you want CP and MP trees side by side.
CircuitPython (optional extensions; see lvgl-circuitpython README for CP clone setup):
Native CP modules here follow Adafruit’s
Extending CircuitPython
architecture (shared-bindings / shared-module / CIRCUITPY_*), but stay
out-of-tree: each extension keeps spikes in its own repo and
apply_cp_patches.sh copies them into a local (uncommitted) CircuitPython tree.
Adafruit’s Learn guide assumes in-tree edits; there is no official out-of-tree
C-module path. Per-repo READMEs map Learn steps → spikes/patches.
./build_cp.sh --port unix --variant standardbuild_cp.sh runs every sibling */apply_cp_patches.sh when present (pygraphics, LVGL, …). Clone only the extensions you need.
Optional: place a user_post_mpconfigport.mk at the workspace root (CircuitPython’s
user-config hook; build_cp.sh passes -I when it exists) to freeze Adafruit
asyncio/ticks for multimer.AsyncTimer. See CircuitPython building.md
and multimer internals.
MicroPython frozen asyncio (required for multimer.AsyncTimer on unix/windows):
# pydevices/manifest.py is discovered automatically.
# Add only personal extras to the optional manifest-user.py.
./build_mp.sh --port unix --variant standard
./build_mp.sh --port windows --variant devCreate any workspace directory, clone micropython and the usermods you need as
siblings (or symlink them), and build from micropython/ with USER_C_MODULES
pointing at the workspace root. See each usermod’s README (e.g.
lvgl-micropython).