What to add
__version__ and __revision__ on the pygraphics module, so a firmware can say which pygraphics it was built from. Mirrors what audioif just landed (PyDevices/audioif#55, commit 04e9578) — that work is a working template, including the parts that turned out to matter.
Why
os.uname().version reports MicroPython's version and build date and says nothing about the user C modules compiled into the image. When a rendering changes between two firmwares, there is no way to ask the board which pygraphics produced it.
On audioif this was not hypothetical. Two boards both reported v1.29.0-dirty on 2026-09-09; the firmware had been built from a throwaway source tree during a pin move, so the commit was not recoverable from the working tree or from the .bin files left on disk — by any means. Worse, once the marker existed it immediately showed that the boards and the desktop had been running different audioif revisions for the whole life of an open issue, which had been quietly poisoning a three-way comparison. pygraphics has the same exposure: it is a user C module, it moves often, and it ships a wheel (pydevices-pygraphics) whose desktop build should answer identically.
Shape that worked
- Compute at build time, never store.
micropython.mk and micropython.cmake run git -C <repo> describe --always --dirty --abbrev=7 against the directory the build is actually reading, and pass it plus the VERSION file as -DPYGRAPHICS_VERSION / -DPYGRAPHICS_REVISION. The build directory is the only thing that knows which tree it compiled.
--dirty matters. A dirty tree is the normal state in this workspace, and a bare hash would claim more than it knows. audioif's boards currently report v0.0.3-205-g04e9578-dirty while an experiment is uncommitted, which is exactly right.
- Define the string objects once.
MP_DEFINE_STR_OBJ in one translation unit, extern from a header, and a macro that expands to the two mp_rom_map_elem_t entries so each module table gains one line. audioif's whole cost is under 250 bytes of flash, no RAM, all const. pygraphics has one module table so it is smaller still.
py/objstr.h, not py/obj.h. mp_obj_str_t is declared in the former; the first audioif build failed on exactly this.
#ifndef fallbacks to "unknown", so a build outside a checkout says so rather than failing.
- The CPython side re-exports the same pair from the extension, so
pygraphics.__revision__ answers the same on a board and in the wheel. setup.py computes it the same way.
Test it, with the control
audioif's tests/test_binding_parity.py gained four cases and two of them are the ones that matter:
- the module table carries the marker (so a rewrite of that table fails rather than shipping unmarked);
- the CPython side answers identically;
- the revision is not
"unknown" in a checkout — without this the whole mechanism can ship reporting unknown from every target while every other assertion still passes;
- and a control asserting the marker is not everywhere, if there is anything deliberately excluded.
Scope questions for this repo
src/circuitpython_spike/shared-bindings/pygraphics/__init__.c also registers a module. audioif deliberately left its spike bindings unmarked, because that binary is a local comparison artifact whose exact bytes are already pinned with written provenance — worth deciding whether the same reasoning holds here or whether the CircuitPython build is something people actually flash.
- Explicitly out of scope: lvgl. It reports its own version, and now the generator has been rebuilt it should only move when the lvgl 9.5 pin moves (Brad, 2026-09-09).
- Undecided:
usbif, cameraif, displayif. They have no releases yet, so the case is weaker; raised here rather than filed as three issues nobody has decided on.
What to add
__version__and__revision__on thepygraphicsmodule, so a firmware can say which pygraphics it was built from. Mirrors what audioif just landed (PyDevices/audioif#55, commit04e9578) — that work is a working template, including the parts that turned out to matter.Why
os.uname().versionreports MicroPython's version and build date and says nothing about the user C modules compiled into the image. When a rendering changes between two firmwares, there is no way to ask the board which pygraphics produced it.On audioif this was not hypothetical. Two boards both reported
v1.29.0-dirty on 2026-09-09; the firmware had been built from a throwaway source tree during a pin move, so the commit was not recoverable from the working tree or from the.binfiles left on disk — by any means. Worse, once the marker existed it immediately showed that the boards and the desktop had been running different audioif revisions for the whole life of an open issue, which had been quietly poisoning a three-way comparison.pygraphicshas the same exposure: it is a user C module, it moves often, and it ships a wheel (pydevices-pygraphics) whose desktop build should answer identically.Shape that worked
micropython.mkandmicropython.cmakerungit -C <repo> describe --always --dirty --abbrev=7against the directory the build is actually reading, and pass it plus theVERSIONfile as-DPYGRAPHICS_VERSION/-DPYGRAPHICS_REVISION. The build directory is the only thing that knows which tree it compiled.--dirtymatters. A dirty tree is the normal state in this workspace, and a bare hash would claim more than it knows. audioif's boards currently reportv0.0.3-205-g04e9578-dirtywhile an experiment is uncommitted, which is exactly right.MP_DEFINE_STR_OBJin one translation unit,externfrom a header, and a macro that expands to the twomp_rom_map_elem_tentries so each module table gains one line. audioif's whole cost is under 250 bytes of flash, no RAM, all const.pygraphicshas one module table so it is smaller still.py/objstr.h, notpy/obj.h.mp_obj_str_tis declared in the former; the first audioif build failed on exactly this.#ifndeffallbacks to"unknown", so a build outside a checkout says so rather than failing.pygraphics.__revision__answers the same on a board and in the wheel.setup.pycomputes it the same way.Test it, with the control
audioif's
tests/test_binding_parity.pygained four cases and two of them are the ones that matter:"unknown"in a checkout — without this the whole mechanism can ship reportingunknownfrom every target while every other assertion still passes;Scope questions for this repo
src/circuitpython_spike/shared-bindings/pygraphics/__init__.calso registers a module. audioif deliberately left its spike bindings unmarked, because that binary is a local comparison artifact whose exact bytes are already pinned with written provenance — worth deciding whether the same reasoning holds here or whether the CircuitPython build is something people actually flash.usbif,cameraif,displayif. They have no releases yet, so the case is weaker; raised here rather than filed as three issues nobody has decided on.