From f33cc8f9b103e946ba6de24d2ad2f9ca9ae222ee Mon Sep 17 00:00:00 2001 From: Brad Barnett <127794626+bdbarnett@users.noreply.github.com> Date: Fri, 30 Oct 2026 05:44:31 -0500 Subject: [PATCH 1/5] Build LVGL from omnipython-lvgl under one name, lvgl --modules lvgl now builds LVGL for either interpreter: build_mp.py takes ports/micropython or ports/circuitpython from omnipython-lvgl, pinned in modules.lock, using a sibling checkout when there is one and a sparse fetch otherwise. all includes it. The old names lvgl-micropython, lvgl-circuitpython and lvgl-bindings are refused with a pointer to lvgl. jpegio finds the LVGL module by its generated binding, in both make and CMake. The release-firmware lane builds CircuitPython cells with lvgl, and its omnipython-lvgl-ref input repins modules.lock through scripts/pin_lvgl_ref.sh. --- .github/workflows/jpegio.yml | 8 +- .github/workflows/release-firmware.yml | 42 ++-- CONTRIBUTING.md | 4 +- .../mpconfigboard.mk | 2 +- .../mpconfigboard.mk | 2 +- .../mpconfigboard.mk | 2 +- .../mpconfigboard.mk | 2 +- .../mpconfigboard.mk | 2 +- build_mp.py | 193 +++++++++++------- docs/reference/files.md | 2 +- docs/reference/modules.md | 5 +- modules.lock | 18 +- modules/all/manifest.py | 7 + modules/displayif/README.md | 2 +- .../displayif/docs/soft-reset-and-bring-up.md | 2 +- modules/jpegio/README.md | 24 +-- modules/jpegio/manifest.py | 2 +- modules/jpegio/micropython.cmake | 29 +-- modules/jpegio/micropython.mk | 21 +- modules/jpegio/src/jpegio.c | 6 +- modules/jpegio/src/lvgl_decoder.c | 2 +- modules/jpegio/src/lvgl_decoder.h | 2 +- .../jpegio/tests/hardware/board_lvgl_glass.py | 2 +- modules/jpegio/tests/test_jpegio_lvgl.py | 14 +- scripts/exercise_build.sh | 2 +- scripts/pin_lvgl_ref.sh | 21 ++ scripts/release_firmware.py | 44 ++-- test_build_mp.py | 123 +++++++---- test_release_firmware.py | 16 +- test_stage_firmware_page.py | 2 +- 30 files changed, 343 insertions(+), 260 deletions(-) create mode 100755 scripts/pin_lvgl_ref.sh diff --git a/.github/workflows/jpegio.yml b/.github/workflows/jpegio.yml index f9eaa3fa..eba2f1f8 100644 --- a/.github/workflows/jpegio.yml +++ b/.github/workflows/jpegio.yml @@ -1,6 +1,6 @@ # jpegio from a bare clone, the way a user builds it: build_mp.py fetches # MicroPython and the pinned modules itself. Its golden corpus without LVGL, -# then beside lvgl-micropython as LVGL's JPEG decoder. (These checks moved +# then beside LVGL (omnipython-lvgl) as its JPEG decoder. (These checks moved # here with jpegio from displayif's clean-build workflow, 2026-10-06.) name: jpegio @@ -72,11 +72,11 @@ jobs: sudo apt-get update sudo apt-get install -y libffi-dev pkg-config - - name: Build the unix port with jpegio and lvgl-micropython - run: python3 build_mp.py --port unix --modules jpegio,lvgl-micropython + - name: Build the unix port with jpegio and lvgl + run: python3 build_mp.py --port unix --modules jpegio,lvgl - name: One TJpgDec in the firmware, and it is jpegio's - # LVGL's own tjpgd.o compiles empty (LV_USE_TJPGD 0 in lvgl-bindings), + # LVGL's own tjpgd.o compiles empty (LV_USE_TJPGD 0 in omnipython-lvgl), # so exactly one object defines jd_prepare, and jpegio's two objects # call into it. working-directory: builds/unix/standard diff --git a/.github/workflows/release-firmware.yml b/.github/workflows/release-firmware.yml index 6334138c..aca3d671 100644 --- a/.github/workflows/release-firmware.yml +++ b/.github/workflows/release-firmware.yml @@ -36,8 +36,8 @@ on: description: Build the Raspberry Pi Pico 2 and the Pimoroni Pico Plus 2 W too (off until a board is on the bench) default: false type: boolean - lvgl-circuitpython-ref: - description: lvgl-circuitpython branch, tag or commit for the CircuitPython builds (left empty, it takes the commit modules.lock pins) + omnipython-lvgl-ref: + description: omnipython-lvgl branch, tag or full 40-character commit for every LVGL build (left empty, it takes the commit modules.lock pins) default: '' type: string schedule: @@ -46,6 +46,7 @@ on: paths: - '.github/workflows/release-firmware.yml' - 'scripts/release_firmware.py' + - 'scripts/pin_lvgl_ref.sh' - 'scripts/stage_firmware_page.py' - 'test_release_firmware.py' - 'test_stage_firmware_page.py' @@ -101,18 +102,11 @@ jobs: steps: - uses: actions/checkout@v7 - - name: lvgl-circuitpython, which the CircuitPython builds compile in - if: matrix.interpreter == 'circuitpython' + - name: omnipython-lvgl at the ref asked for, in modules.lock + if: inputs.omnipython-lvgl-ref != '' env: - LVGL_CP_REF: ${{ inputs.lvgl-circuitpython-ref }} - run: | - # empty input: the commit modules.lock pins - LVGL_CP_REF="${LVGL_CP_REF:-$(awk '$1=="lvgl-circuitpython"{print $3}' "$GITHUB_WORKSPACE/modules.lock")}" - mkdir -p "$RUNNER_TEMP/ws" && cd "$RUNNER_TEMP/ws" - git clone --quiet --depth 1 --branch "$LVGL_CP_REF" https://github.com/PyDevices/lvgl-circuitpython.git \ - || { git clone --quiet https://github.com/PyDevices/lvgl-circuitpython.git \ - && git -C lvgl-circuitpython checkout --quiet "$LVGL_CP_REF"; } - git -C lvgl-circuitpython rev-parse HEAD | tee "$RUNNER_TEMP/lvgl-circuitpython.commit" + REF: ${{ inputs.omnipython-lvgl-ref }} + run: scripts/pin_lvgl_ref.sh "$REF" - name: Packages the builds need run: | @@ -162,9 +156,7 @@ jobs: - name: Build ${{ matrix.id }} run: | start=$(date +%s) - ws=() - [[ "${{ matrix.interpreter }}" == circuitpython ]] && ws=(--ws "$RUNNER_TEMP/ws") - python3 scripts/release_firmware.py build "${{ matrix.id }}" out "${ws[@]}" + python3 scripts/release_firmware.py build "${{ matrix.id }}" out { echo "### ${{ matrix.id }}" echo "- toolchain cache: ${{ steps.deps.outputs.cache-hit == 'true' && 'hit' || (matrix.deps == 'none' && 'none needed' || 'miss') }}" @@ -197,17 +189,12 @@ jobs: merge-multiple: true path: in - - name: lvgl-circuitpython's commit - id: lvgl + # The same pin the builds took, so firmware.json's modules.lock says it. + - name: omnipython-lvgl at the ref asked for, in modules.lock + if: inputs.omnipython-lvgl-ref != '' env: - LVGL_CP_REF: ${{ inputs.lvgl-circuitpython-ref }} - run: | - # empty input: the commit modules.lock pins - LVGL_CP_REF="${LVGL_CP_REF:-$(awk '$1=="lvgl-circuitpython"{print $3}' "$GITHUB_WORKSPACE/modules.lock")}" - git clone --quiet --depth 1 --branch "$LVGL_CP_REF" https://github.com/PyDevices/lvgl-circuitpython.git lvgl-cp \ - || { git clone --quiet https://github.com/PyDevices/lvgl-circuitpython.git lvgl-cp \ - && git -C lvgl-cp checkout --quiet "$LVGL_CP_REF"; } - echo "commit=$(git -C lvgl-cp rev-parse HEAD)" >> "$GITHUB_OUTPUT" + REF: ${{ inputs.omnipython-lvgl-ref }} + run: scripts/pin_lvgl_ref.sh "$REF" - name: Check the files and write firmware.json env: @@ -216,8 +203,7 @@ jobs: run: | python3 scripts/release_firmware.py assemble in firmware $PICO2 \ --tag "$TAG" \ - --run-url "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" \ - --lvgl-circuitpython-commit "${{ steps.lvgl.outputs.commit }}" + --run-url "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" ls -l firmware cat firmware/firmware.json diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cdfa2d3b..e20e1e1c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -11,9 +11,9 @@ fix is a good first contribution. |---|---| | `build_mp.py` | The build command. | | `UPSTREAM` | The MicroPython commit the patches apply to. | -| `modules.lock`, `deps.lock` | What gets fetched when it's missing, lvgl-bindings included. | +| `modules.lock`, `deps.lock` | What gets fetched when it's missing, omnipython-lvgl included. | | `modules/` | The module manifest, `all/`, `tflite/` and `vision/` (esp-vision's), the modules linked or cloned, and the C modules that live only here: `audioif`, `cameraif`, `castif`, `displayif`, `h264enc`, `jpegio`, `mdns`, `pngio`, `ppa`, `tsmux`, `usbhost`. Each has its own README and tests. | -| `modules/.lvgl-bindings` | The lvgl-bindings lvgl-micropython compiles: the sibling checkout, linked, or `modules.lock`'s commit, fetched. | +| `modules/.omnipython-lvgl` | LVGL, its bindings and its ports, which `--modules lvgl` compiles: the sibling checkout, linked, or `modules.lock`'s commit, fetched. | | `patches/` | The patch series and `apply_patches.py`. | | `variants/`, `boards/` | Our variants and CircuitPython boards. | | `scripts/` | `exercise_build.sh`, `release_firmware.py`, `stage_firmware_page.py`, `serve_workbench.sh`, `freeze_list.py`, `update_provenance.py`. | diff --git a/boards/circuitpython/espressif/lilygo_twatch_s3_single_app/mpconfigboard.mk b/boards/circuitpython/espressif/lilygo_twatch_s3_single_app/mpconfigboard.mk index ad36fdd5..77a9d61a 100644 --- a/boards/circuitpython/espressif/lilygo_twatch_s3_single_app/mpconfigboard.mk +++ b/boards/circuitpython/espressif/lilygo_twatch_s3_single_app/mpconfigboard.mk @@ -29,7 +29,7 @@ FLASH_SIZE_SDKCONFIG = boards/$(BOARD)/sdkconfig-flash.defaults CIRCUITPY_STORAGE_EXTEND = 0 CIRCUITPY_DUALBANK = 0 -# LVGL links its own AnimatedGIF decoder (lvgl-circuitpython's micropython.mk). +# LVGL links its own AnimatedGIF decoder (omnipython-lvgl's ports/circuitpython/micropython.mk). CIRCUITPY_GIFIO = 0 # Specialized board. diff --git a/boards/circuitpython/espressif/waveshare_esp32p4_wifi6_dev_kit/mpconfigboard.mk b/boards/circuitpython/espressif/waveshare_esp32p4_wifi6_dev_kit/mpconfigboard.mk index f70bb5a3..c514b6dc 100644 --- a/boards/circuitpython/espressif/waveshare_esp32p4_wifi6_dev_kit/mpconfigboard.mk +++ b/boards/circuitpython/espressif/waveshare_esp32p4_wifi6_dev_kit/mpconfigboard.mk @@ -34,5 +34,5 @@ FLASH_SIZE_SDKCONFIG = boards/$(BOARD)/sdkconfig-flash.defaults CIRCUITPY_STORAGE_EXTEND = 0 CIRCUITPY_DUALBANK = 0 -# LVGL links its own AnimatedGIF decoder (lvgl-circuitpython's micropython.mk). +# LVGL links its own AnimatedGIF decoder (omnipython-lvgl's ports/circuitpython/micropython.mk). CIRCUITPY_GIFIO = 0 diff --git a/boards/circuitpython/espressif/waveshare_esp32p4_wifi6_touch_lcd_4b/mpconfigboard.mk b/boards/circuitpython/espressif/waveshare_esp32p4_wifi6_touch_lcd_4b/mpconfigboard.mk index 31f4977c..da7e077c 100644 --- a/boards/circuitpython/espressif/waveshare_esp32p4_wifi6_touch_lcd_4b/mpconfigboard.mk +++ b/boards/circuitpython/espressif/waveshare_esp32p4_wifi6_touch_lcd_4b/mpconfigboard.mk @@ -32,5 +32,5 @@ FLASH_SIZE_SDKCONFIG = boards/$(BOARD)/sdkconfig-flash.defaults CIRCUITPY_STORAGE_EXTEND = 0 CIRCUITPY_DUALBANK = 0 -# LVGL links its own AnimatedGIF decoder (lvgl-circuitpython's micropython.mk). +# LVGL links its own AnimatedGIF decoder (omnipython-lvgl's ports/circuitpython/micropython.mk). CIRCUITPY_GIFIO = 0 diff --git a/boards/circuitpython/espressif/waveshare_esp32s3_touch_lcd_43/mpconfigboard.mk b/boards/circuitpython/espressif/waveshare_esp32s3_touch_lcd_43/mpconfigboard.mk index b16856dd..8ce547b0 100644 --- a/boards/circuitpython/espressif/waveshare_esp32s3_touch_lcd_43/mpconfigboard.mk +++ b/boards/circuitpython/espressif/waveshare_esp32s3_touch_lcd_43/mpconfigboard.mk @@ -34,7 +34,7 @@ FLASH_SIZE_SDKCONFIG = boards/$(BOARD)/sdkconfig-flash.defaults CIRCUITPY_STORAGE_EXTEND = 0 CIRCUITPY_DUALBANK = 0 -# LVGL links its own AnimatedGIF decoder (lvgl-circuitpython's micropython.mk). +# LVGL links its own AnimatedGIF decoder (omnipython-lvgl's ports/circuitpython/micropython.mk). CIRCUITPY_GIFIO = 0 CIRCUITPY_DOTCLOCKFRAMEBUFFER = 1 diff --git a/boards/circuitpython/espressif/waveshare_esp32s3_touch_lcd_7/mpconfigboard.mk b/boards/circuitpython/espressif/waveshare_esp32s3_touch_lcd_7/mpconfigboard.mk index dded4b95..0d655732 100644 --- a/boards/circuitpython/espressif/waveshare_esp32s3_touch_lcd_7/mpconfigboard.mk +++ b/boards/circuitpython/espressif/waveshare_esp32s3_touch_lcd_7/mpconfigboard.mk @@ -35,5 +35,5 @@ FLASH_SIZE_SDKCONFIG = boards/$(BOARD)/sdkconfig-flash.defaults CIRCUITPY_STORAGE_EXTEND = 0 CIRCUITPY_DUALBANK = 0 -# LVGL links its own AnimatedGIF decoder (lvgl-circuitpython's micropython.mk). +# LVGL links its own AnimatedGIF decoder (omnipython-lvgl's ports/circuitpython/micropython.mk). CIRCUITPY_GIFIO = 0 diff --git a/build_mp.py b/build_mp.py index 925cfb66..ad447dcc 100755 --- a/build_mp.py +++ b/build_mp.py @@ -225,21 +225,23 @@ def micropython_version(mp, upstream): return tag, git_out(mp, "rev-parse", "--short=10", upstream) -# lvgl-bindings is pinned in modules.lock beside the modules, but it is not a -# module: it is the generated binding and LVGL that lvgl-micropython compiles. -# It lands in modules/.lvgl-bindings (the dot keeps it out of "all" and of the -# module list) and reaches lvgl-micropython as BINDINGS_DIR, so that module's -# own pin and fetch never come into a build made here. -LVGL_BINDINGS = "lvgl-bindings" -LVGL_MODULE = "lvgl-micropython" -# lvgl-circuitpython is pinned in modules.lock too, but only the CircuitPython -# release lane reads that row: it clones the commit and gives it to --modules by path. -LVGL_CIRCUITPYTHON = "lvgl-circuitpython" +# omnipython-lvgl is pinned in modules.lock beside the modules, but it is not +# one module directory: it is LVGL, its generated bindings and one port per +# interpreter (ports/micropython, ports/circuitpython), which read generated/, +# lv_conf.h and lvgl/ from the repository's root. The checkout lands in +# modules/.omnipython-lvgl (the dot keeps it out of the module list): in the +# workspace a link to the sibling checkout, which wins like every other module, +# anywhere else a sparse fetch at the pinned commit. --modules names it `lvgl`, +# and "all" takes it too; a build gets the port for its interpreter. +LVGL_REPO = "omnipython-lvgl" +LVGL_MODULE = "lvgl" +# The names it went by before the four LVGL repositories became one. +LVGL_OLD_NAMES = ("lvgl-micropython", "lvgl-circuitpython", "lvgl-bindings") def ensure_modules(ws): for name, url, commit in read_lock("modules.lock"): - if name in (LVGL_BINDINGS, LIBRARY, LVGL_CIRCUITPYTHON): + if name in (LVGL_REPO, LIBRARY): continue dest = MODULES_DIR / name if dest.exists() or dest.is_symlink(): @@ -250,40 +252,60 @@ def ensure_modules(ws): clone_at(url, commit, dest) -def lvgl_bindings(ws, module_dirs): - """The lvgl-bindings a build with lvgl-micropython compiles, or None - when the build has no lvgl-micropython. +def refuse_old_lvgl_names(names): + """lvgl-micropython, lvgl-circuitpython and lvgl-bindings are one repository + now, omnipython-lvgl, named `lvgl` here whatever the interpreter.""" + old = [n for n in names if n in LVGL_OLD_NAMES] + if old: + die(f"{', '.join(old)}: LVGL is one module now, {LVGL_MODULE} (the {LVGL_REPO} repository), " + f"for MicroPython and CircuitPython alike: --modules ...,{LVGL_MODULE}") - In a workspace, modules/.lvgl-bindings links to the sibling checkout and - builds it as it is, like every other module. Anywhere else, it is fetched - at its modules.lock commit: lvgl-bindings' generated/ and lv_conf.h, and - LVGL's src/ at the commit lvgl-bindings records. Shallow, sparse and - blobless, so it downloads tens of MB rather than LVGL's whole history.""" - if not any(module_name(d) == LVGL_MODULE for d in module_dirs): - return None + +def lvgl_root_of(d): + """The omnipython-lvgl checkout `d` is, or is a port directory of, else None.""" + real = Path(d).resolve() + if (real / "lv_conf.h").is_file() and (real / "ports" / "micropython").is_dir(): + return real + root = real.parent.parent + if real.parent.name == "ports" and (root / "lv_conf.h").is_file(): + return root + return None + + +def lvgl_port(root, interpreter): + return (Path(root) / "ports" / interpreter).resolve() + + +def lvgl_checkout(ws): + """modules/.omnipython-lvgl: LVGL, its bindings and its ports, at their + modules.lock commit (or the workspace's own checkout). + + Fetched shallow, sparse and blobless: the ports, generated/, python/ and + the root's files (lv_conf.h), and LVGL's src/ at the commit the repository + records, so it downloads tens of MB rather than LVGL's whole history.""" rows = {row[0]: row[1:] for row in read_lock("modules.lock")} - if LVGL_BINDINGS not in rows: - die(f"modules.lock has no {LVGL_BINDINGS} row, which {LVGL_MODULE} needs") - url, commit = rows[LVGL_BINDINGS] - dest = MODULES_DIR / f".{LVGL_BINDINGS}" + if LVGL_REPO not in rows: + die(f"modules.lock has no {LVGL_REPO} row, which {LVGL_MODULE} comes from") + url, commit = rows[LVGL_REPO] + dest = MODULES_DIR / f".{LVGL_REPO}" if dest.is_symlink(): if (dest / "lv_conf.h").is_file(): - return dest + return dest.resolve() dest.unlink() elif dest.is_dir(): if git_out(dest, "rev-parse", "HEAD") == commit and (dest / "lvgl" / "lvgl.h").is_file(): - return dest + return dest.resolve() say(f"{dest} is not at modules.lock's {commit[:12]}; fetching it again") shutil.rmtree(dest) - sibling = ws / LVGL_BINDINGS if ws else None + sibling = ws / LVGL_REPO if ws else None if sibling and (sibling / "lv_conf.h").is_file(): if not (sibling / "lvgl" / "lvgl.h").is_file(): die(f"{sibling} has no LVGL: git -C {sibling} submodule update --init --depth 1 lvgl") link(dest, sibling) - return dest + return dest.resolve() if not re.fullmatch(r"[0-9a-f]{40}", commit): - die(f"modules.lock pins {LVGL_BINDINGS} at {commit!r}: it takes a full 40-character commit") - say(f"fetching {url} at {commit[:12]} into {dest} (generated/, lv_conf.h and LVGL's src/)") + die(f"modules.lock pins {LVGL_REPO} at {commit!r}: it takes a full 40-character commit") + say(f"fetching {url} at {commit[:12]} into {dest} (the ports, generated/, lv_conf.h and LVGL's src/)") part = dest.with_name(dest.name + ".partial") shutil.rmtree(part, ignore_errors=True) @@ -297,18 +319,18 @@ def sparse(repo, origin, sha, paths): if r.returncode: shutil.rmtree(part, ignore_errors=True) die(f"could not fetch {origin} at {sha} (git {cmd[0]}: {r.stderr.strip() or r.returncode});" - f" is modules.lock's {LVGL_BINDINGS} commit on GitHub?") + f" is modules.lock's {LVGL_REPO} commit on GitHub?") - sparse(part, url, commit, ["generated", "lvgl"]) + sparse(part, url, commit, ["generated", "lvgl", "ports", "python"]) lvgl_sha = git_out(part, "ls-tree", "HEAD", "lvgl").split()[2:3] lvgl_url = git_out(part, "config", "-f", ".gitmodules", "submodule.lvgl.url") if not lvgl_sha or not lvgl_url: shutil.rmtree(part, ignore_errors=True) - die(f"{LVGL_BINDINGS} {commit[:12]} records no lvgl submodule") + die(f"{LVGL_REPO} {commit[:12]} records no lvgl submodule") shutil.rmtree(part / "lvgl") sparse(part / "lvgl", lvgl_url, lvgl_sha[0], ["src"]) part.rename(dest) - return dest + return dest.resolve() # omnipython-lib is the repository of our Python-only packages: pydevices, @@ -567,13 +589,27 @@ def module_choices(): return sorted(p.name for p in MODULES_DIR.iterdir() if p.is_dir() and not p.name.startswith(".")) -def resolve_modules(spec, library=None): +def resolve_modules(spec, library=None, interpreter="micropython", lvgl=None): """The comma list as modules/manifest.py will read it (paths made absolute), the module directories it amounts to ("all" expanded), which the esp32 conventions and the build record need, and the library packages it names. A name that is no module of ours but a package of the library is - required from it; `library` is the Library to ask, built only then.""" + required from it; `library` is the Library to ask, built only then. + + `lvgl`, and "all", take omnipython-lvgl's port for `interpreter`, as does a + path to an omnipython-lvgl checkout; `lvgl` is called for the checkout + only then, so a build without LVGL fetches none. Without `lvgl`, "all" + leaves LVGL out and the name `lvgl` is refused.""" names, dirs, packages = [], [], [] + refuse_old_lvgl_names([Path(s.strip()).name for s in spec.split(",")]) + + def lvgl_dir(root=None): + if root is None: + if lvgl is None: + die(f"{LVGL_MODULE}: no {LVGL_REPO} checkout to build it from") + root = lvgl() + return lvgl_port(root, interpreter) + for item in (s.strip() for s in spec.split(",")): if not item: continue @@ -581,12 +617,25 @@ def resolve_modules(spec, library=None): path = Path(item).expanduser().resolve() if not path.is_dir(): die(f"module path does not exist: {item}") + root = lvgl_root_of(path) + if root and path != lvgl_dir(root): + if path != root: + die(f"{item} is {LVGL_REPO}'s port for another interpreter; this build takes " + f"{lvgl_dir(root)} (or name it {LVGL_MODULE})") + path = lvgl_dir(root) names.append(str(path)) dirs.append(path) elif item == "all": names.append("all") # modules/all/manifest.py's rule: every module but the opt-in ones. dirs += [MODULES_DIR / n for n in module_choices() if n != "all" and not (MODULES_DIR / n / "OPT_IN").is_file()] + # and LVGL, which modules/all/manifest.py finds through $PYDEVICES_LVGL. + if lvgl is not None: + dirs.append(lvgl_dir()) + elif item == LVGL_MODULE and not (MODULES_DIR / item).is_dir(): + path = lvgl_dir() + names.append(str(path)) + dirs.append(path) elif (MODULES_DIR / item).is_dir(): names.append(item) dirs.append(MODULES_DIR / item) @@ -594,8 +643,8 @@ def resolve_modules(spec, library=None): names.append(item) packages.append(item) else: - have = module_choices() + (sorted(library.names()) if library else []) - die(f"no module '{item}' in {MODULES_DIR} (have: {', '.join(have)})") + have = module_choices() + [LVGL_MODULE] + (sorted(library.names()) if library else []) + die(f"no module '{item}' in {MODULES_DIR} (have: {', '.join(sorted(have))})") unique = [] for d in dirs: if d.resolve() not in [u.resolve() for u in unique]: @@ -603,14 +652,17 @@ def resolve_modules(spec, library=None): return ",".join(names), unique, sorted(set(packages)) -def record(dirs, bindings=None, library=None): - """A row per module, one for the lvgl-bindings lvgl-micropython compiles - when the build has it, and one for the omnipython-lib the build's library - packages came from: which commit went in, and whether it had uncommitted - changes.""" +def lvgl_in(module_dirs): + """The omnipython-lvgl checkout a build compiles, or None when it has no LVGL.""" + return next((r for r in (lvgl_root_of(d) for d in module_dirs) if r), None) + + +def record(dirs, library=None): + """A row per module (omnipython-lvgl's under its repository's name) and one + for the omnipython-lib the build's library packages came from: which commit + went in, and whether it had uncommitted changes.""" rows = {} - named = ([(module_name(d), d) for d in dirs] + ([(LVGL_BINDINGS, bindings)] if bindings else []) - + ([(LIBRARY, library)] if library else [])) + named = [(module_name(d), d) for d in dirs] + ([(LIBRARY, library)] if library else []) for name, d in named: real = d.resolve() rev = git_out(real, "describe", "--always", "--dirty", "--abbrev=12") or "(not a git checkout)" @@ -674,7 +726,7 @@ def foreign_sources(build, roots): names the source first), so it catches the next way it goes wrong too. `roots` are the build's module directories (and anything else the build - is meant to compile from, such as lvgl-bindings): a source counts as its + is meant to compile from, such as omnipython-lvgl's root): a source counts as its module's when the directory it was found under is inside one of them. CMake ports name sources by absolute path and write no .P files.""" roots = [Path(os.path.realpath(r)) for r in roots] @@ -1115,6 +1167,8 @@ def module_name(d): real = d.resolve() if d.parent == MODULES_DIR: return d.name + if lvgl_root_of(real): + return LVGL_REPO top = git_out(real, "rev-parse", "--show-toplevel") common = git_out(real, "rev-parse", "--path-format=absolute", "--git-common-dir") if top and Path(top).resolve() == real and Path(common).name == ".git": @@ -1196,7 +1250,7 @@ def cp_frozen_manifest(cp, port, board, variant, module_dirs, library_packages, """The FROZEN_MANIFEST of a CircuitPython build that freezes our Python, or None when it freezes none: the port's own base (the unix coverage variant's manifest), the board's FROZEN_MPY_DIRS content, the Python of the modules - named (palettes, pdwidgets, lvgl-circuitpython's fs_driver, ...), and the + named (palettes, pdwidgets, omnipython-lvgl's fs_driver, ...), and the library packages named. Returns (manifest path, make arguments).""" manifests = [d for d in module_dirs if cp_frozen_manifest_dir(d)] if not manifests and not library_packages: @@ -1267,20 +1321,6 @@ def cp_user_c_modules(module_dirs, link_dir): return dirs -def refuse_lvgl_micropython(names): - """lvgl-micropython is MicroPython's LVGL binding. CircuitPython compiles - with -Wshadow -Werror, which its generated binding trips over, so a build - that reached that error would only say 'shadows a global declaration'. - CircuitPython's binding is lvgl-circuitpython, given to --modules by path.""" - if LVGL_MODULE in names: - die( - f"{LVGL_MODULE} is MicroPython's LVGL binding and does not build on CircuitPython " - "(its generated code stops on -Werror=shadow). For --interpreter circuitpython use " - "lvgl-circuitpython, passed by path: --modules /path/to/lvgl-circuitpython " - "(see its docs/build-and-flash.md)" - ) - - def cp_espressif_env(port_dir, board): """What CircuitPython's espressif port needs before make: its ESP-IDF's submodules, compilers and Python packages. Returns the shell prefix that @@ -1401,8 +1441,8 @@ def build_circuitpython(args, make_extra, ws): die("--interpreter circuitpython builds from Linux or WSL") if args.flash: die("--flash is for MicroPython's esp32 port") - # Before anything is fetched: a plain name or a path ending in the name. - refuse_lvgl_micropython([Path(i.strip()).name for i in (args.modules or "").split(",")]) + # Before anything is fetched. + refuse_old_lvgl_names([Path(i.strip()).name for i in (args.modules or "").split(",")]) ensure_modules(ws) cp = ensure_circuitpython() lock = lock_beside(cp) # noqa: F841 -- held until exit @@ -1435,8 +1475,7 @@ def build_circuitpython(args, make_extra, ws): say("Modules: " + ", ".join(module_choices())) spec = input('Which (comma list; full paths for others; empty for none)? ').strip() library = Library(ws) - spec, module_dirs, library_packages = resolve_modules(spec, library) - refuse_lvgl_micropython([module_name(d) for d in module_dirs]) + spec, module_dirs, library_packages = resolve_modules(spec, library, "circuitpython", lambda: lvgl_checkout(ws)) if library_packages or any(LIBRARY in (d.resolve() / "manifest.py").read_text() for d in module_dirs if cp_frozen_manifest_dir(d)): library.build() @@ -1457,8 +1496,7 @@ def build_circuitpython(args, make_extra, ws): "modules": spec, "module_set": module_set, "make_args": make_extra} if components: want["esp_idf_components"] = [f"{n} {dep_version(n, d)}" for n, d in zip(components, component_dirs)] - bindings = lvgl_bindings(ws, module_dirs) - revisions = record(module_dirs, bindings, library.source) + revisions = record(module_dirs, library.source) rec_path = build_dir_for(build, want, args.clean, revisions) user_c = cp_user_c_modules(module_dirs, build / "usermods") frozen, frozen_args = (None, []) if any(a.startswith("FROZEN_MANIFEST=") for a in make_extra) else \ @@ -1466,9 +1504,7 @@ def build_circuitpython(args, make_extra, ws): env = {k: v for k, v in os.environ.items() if k not in ("USER_C_MODULES", "FROZEN_MANIFEST", "BUILD", "BOARD", "VARIANT", "VARIANT_DIR", "PYTHONPATH", - "BINDINGS_DIR")} - if bindings: - env["BINDINGS_DIR"] = str(bindings.resolve()) + "BINDINGS_DIR", "PYDEVICES_LVGL")} if library.tree: env["PYDEVICES_LIBRARY"] = library.tree.as_posix() env["PYDEVICES_INTERPRETER"] = "circuitpython" @@ -1656,7 +1692,8 @@ def main(): say("Modules: " + ", ".join(module_choices())) spec = input('Which (comma list; full paths for others; "all"; empty for none)? ').strip() library = Library(ws) - spec, module_dirs, library_packages = resolve_modules(spec, library) + spec, module_dirs, library_packages = resolve_modules(spec, library, "micropython", lambda: lvgl_checkout(ws)) + lvgl_root = lvgl_in(module_dirs) if library_packages or uses_library(module_dirs, frozen_manifest(port, board, variant)): library.build() @@ -1674,13 +1711,12 @@ def main(): "module_set": module_set} # Each module's commit too: a module that moved keeps its name, and make # would reuse frozen content and qstr headers from before it moved. - bindings = lvgl_bindings(ws, module_dirs) - revisions = record(module_dirs, bindings, library.source) + revisions = record(module_dirs, library.source) build_dir_for(build, want, args.clean, revisions) env = dict(os.environ) for leak in ("USER_C_MODULES", "FROZEN_MANIFEST", "BUILD", "BOARD", "VARIANT", "BOARD_DIR", "VARIANT_DIR", - "BINDINGS_DIR"): + "BINDINGS_DIR", "PYDEVICES_LVGL"): env.pop(leak, None) env["PYDEVICES_MODULES"] = spec # The frozen pydevices leaves its host-only files off a board's image. @@ -1690,8 +1726,9 @@ def main(): env["MICROPY_GIT_TAG"], env["MICROPY_GIT_HASH"] = version if library.tree: env["PYDEVICES_LIBRARY"] = library.tree.as_posix() - if bindings: - env["BINDINGS_DIR"] = bindings.resolve().as_posix() + if lvgl_root: + # modules/all/manifest.py takes LVGL's port from here. + env["PYDEVICES_LVGL"] = lvgl_port(lvgl_root, "micropython").as_posix() if os.name == "nt": # Upstream's tools open sources without encoding=; Windows' default is cp1252. env.setdefault("PYTHONUTF8", "1") @@ -1748,7 +1785,7 @@ def shell(cmd, log=None): # those, and this build compiles them again from their modules. # The variant tree is ours too: a variant's manifest adds its own C module # (webassembly's wasmbridge) from beside the variant. - roots = module_dirs + ([bindings] if bindings else []) + [VARIANTS_DIR / port] + roots = module_dirs + ([lvgl_root] if lvgl_root else []) + [VARIANTS_DIR / port] for dep, src in foreign_sources(build, roots): say(f"compiling {dep.relative_to(build).with_suffix('.o').as_posix()} again: it was compiled from {src}") dep.with_suffix(".o").unlink(missing_ok=True) diff --git a/docs/reference/files.md b/docs/reference/files.md index 96a0b674..77cc62fb 100644 --- a/docs/reference/files.md +++ b/docs/reference/files.md @@ -4,7 +4,7 @@ |---|---| | [`build_mp.py`](https://github.com/PyDevices/micropython-pydevices/blob/main/build_mp.py) | The build command: [every option](build-command.md). It fills in `micropython/`, `modules/` and `deps/` if they're missing, prepares the MicroPython tree and runs the port's own `make`. | | [`UPSTREAM`](https://github.com/PyDevices/micropython-pydevices/blob/main/UPSTREAM) | The MicroPython commit everything applies to. | -| [`modules.lock`](https://github.com/PyDevices/micropython-pydevices/blob/main/modules.lock) | The commits `build_mp.py` fetches when a module isn't there. It also pins lvgl-bindings, the generated binding and LVGL that lvgl-micropython compiles: a build with lvgl-micropython fetches it into `modules/.lvgl-bindings`. Beside sibling checkouts, these are links to those checkouts instead. | +| [`modules.lock`](https://github.com/PyDevices/micropython-pydevices/blob/main/modules.lock) | The commits `build_mp.py` fetches when a module isn't there. It also pins [omnipython-lvgl](https://github.com/PyDevices/omnipython-lvgl), LVGL with its bindings and a port per interpreter: a build with `lvgl` (or `all`) fetches it into `modules/.omnipython-lvgl` and compiles the port for its interpreter. Beside sibling checkouts, these are links to those checkouts instead. | | [`deps.lock`](https://github.com/PyDevices/micropython-pydevices/blob/main/deps.lock) | The toolchains `build_mp.py` fetches: ESP-IDF, emsdk, SDL2 for MinGW, the TinyUSB the esp32 port builds on, and the ESP-IDF components CircuitPython builds ask for. It also pins CircuitPython, for `--interpreter circuitpython`. | | [`provenance.json`](https://github.com/PyDevices/micropython-pydevices/blob/main/provenance.json) | The MicroPython pin and the checksum of every patch in [`patches/micropython/`](https://github.com/PyDevices/micropython-pydevices/tree/main/patches/micropython) and [`patches/tinyusb/`](https://github.com/PyDevices/micropython-pydevices/tree/main/patches/tinyusb). | | [`modules/`](https://github.com/PyDevices/micropython-pydevices/tree/main/modules) | [`manifest.py`](https://github.com/PyDevices/micropython-pydevices/blob/main/modules/manifest.py), the manifest every build freezes; `all/`; the modules, [linked or cloned](modules.md); and `.esp-vision/`, the pin for [esp-vision's modules](../how-it-works/esp-vision.md). | diff --git a/docs/reference/modules.md b/docs/reference/modules.md index 2df19dd8..34088417 100644 --- a/docs/reference/modules.md +++ b/docs/reference/modules.md @@ -25,7 +25,8 @@ tracked here or linked from a sibling checkout or cloned at the commit in | Module | Where it comes from | |---|---| -| `audiodsp`, `pygraphics`, `ulab`, `lvgl-micropython` | Cloned at the commit in `modules.lock` when `modules/` is missing, or linked from a sibling checkout. | +| `audiodsp`, `pygraphics`, `ulab` | Cloned at the commit in `modules.lock` when `modules/` is missing, or linked from a sibling checkout. | +| `lvgl` | [omnipython-lvgl](https://github.com/PyDevices/omnipython-lvgl), fetched at the commit in `modules.lock` into `modules/.omnipython-lvgl`, or linked from a sibling checkout. A build compiles its `ports/micropython` or `ports/circuitpython`, whichever the interpreter is, and `all` includes it. | | `pydevices`, `palettes`, `pdwidgets`, `audiocomponents` | Manifests here that require the Python packages from omnipython-lib. | | `micropython-lib` | MicroPython's own upstream packages, frozen from the checkout the MicroPython tree carries. | | `tflite`, `vision` | [esp-vision's modules](../guides/use-esp-vision-modules.md), opt-in: `all` leaves them out. | @@ -59,7 +60,7 @@ it is. "Unix" is CircuitPython's unix port, built and imported in | h264enc | no: it is the ESP32-P4's encoder | ESP32-P4 | Espressif's `esp_h264`, which CircuitPython's port, built with the component manager off, can't fetch itself, is pinned in `deps.lock` and fetched by `build_mp.py`. Name ppa with it. | | picotts | no | no, on purpose | CircuitPython has Adafruit's `adafruit_picotts`, the same engine and API as a native `.mpy`. | | tflite, vision | no | no, on purpose | esp-vision's modules, for MicroPython only. | -| lvgl-micropython | no | no | CircuitPython's binding is `lvgl-circuitpython`, passed by path. | +| lvgl | yes | all five | One name for both interpreters: a CircuitPython build compiles omnipython-lvgl's `ports/circuitpython`. A board build needs `CIRCUITPY_GIFIO=0`, since LVGL links its own AnimatedGIF decoder; our boards set it. | | micropython-lib | no | no | MicroPython's own upstream packages. | | pydevices, palettes, pdwidgets | frozen | frozen | Python only. On unix, pydevices also freezes its desktop files (the boards drop them), and pdwidgets and palettes with it: together their names pass CircuitPython's 64 KB frozen-string pool, so `build_mp.py` builds the unix port with four-byte qstr offsets (patches 0001 and 0010). Boards keep two-byte offsets. | diff --git a/modules.lock b/modules.lock index fd8fcd2f..ac0b2664 100644 --- a/modules.lock +++ b/modules.lock @@ -13,18 +13,14 @@ # from other repositories (audiocomponents' two) are pinned by tag in its # lib-lock.json, not here. # -# lvgl-bindings is not a module but the source lvgl-micropython compiles: a -# build that has lvgl-micropython fetches it at this commit into -# modules/.lvgl-bindings (a sibling checkout is linked there instead) and -# hands it to lvgl-micropython, whose own LVGL_BINDINGS_COMMIT then goes unused. -# -# lvgl-circuitpython is not a module build_mp.py clones either: the CircuitPython -# cells of release-firmware.yml check it out at this commit and pass it to -# --modules by path. Move the commit here to move the CircuitPython builds. +# omnipython-lvgl is not a module directory either: it is LVGL, its generated +# bindings and a port per interpreter. --modules names it lvgl ("all" takes it +# too), and a build fetches it at this commit into modules/.omnipython-lvgl (a +# sibling checkout is linked there instead) and compiles the port for its +# interpreter. Move the commit here to move every LVGL build, MicroPython's and +# CircuitPython's. audiodsp https://github.com/PyDevices/audiodsp.git f763ec30d008a668fe8cbadf132c56f2fc099749 -lvgl-bindings https://github.com/PyDevices/lvgl-bindings.git 22f8383eedc785a6c87c57c7ed46c95c454cebe3 -lvgl-micropython https://github.com/PyDevices/lvgl-micropython.git 48937fcd87e574488178997e2bafe73d9bf02240 +omnipython-lvgl https://github.com/PyDevices/omnipython-lvgl.git 84d1de375b014c7d49b4ccbd3be551813091f9ef pygraphics https://github.com/PyDevices/pygraphics.git 1c193cfef534a3bdb85d4b112a66e0d4e76692ca ulab https://github.com/v923z/micropython-ulab.git 1d3ddd8f5228e2c7335dac71f4846e6abb0ec9f9 omnipython-lib https://github.com/PyDevices/pydevices.git 80a1fcbcf9ed970746ae9c178cadf1ab92d5d75b -lvgl-circuitpython https://github.com/PyDevices/lvgl-circuitpython.git 5478910d2505de02a72bf30384c33ca6cecc9825 diff --git a/modules/all/manifest.py b/modules/all/manifest.py index e706c099..737fc0fd 100644 --- a/modules/all/manifest.py +++ b/modules/all/manifest.py @@ -6,6 +6,9 @@ # runs that file and the manifest tool includes a file only once, so it # applies the same rule itself: a module's manifest.py if it has one, else # its C half (ulab keeps its glue in code/). +# +# LVGL is not in modules/: it is omnipython-lvgl's ports/micropython, which +# build_mp.py names in $PYDEVICES_LVGL, and "all" takes it from there. import os _root = os.path.dirname(os.getcwd()) # include() runs a manifest from its own directory @@ -31,3 +34,7 @@ _name, _dir ) ) + +_lvgl = os.environ.get("PYDEVICES_LVGL") +if _lvgl: + include(_lvgl) diff --git a/modules/displayif/README.md b/modules/displayif/README.md index 069be1c5..a9d3d400 100644 --- a/modules/displayif/README.md +++ b/modules/displayif/README.md @@ -86,7 +86,7 @@ unix, `include("$(PORT_DIR)/boards/manifest.py")` on esp32) or you lose ```bash make BOARD=ESP32_GENERIC_S3 USER_C_MODULES=/abs/path/to/displayif make BOARD=ESP32_GENERIC_S3 \ - USER_C_MODULES="/abs/path/to/displayif;/abs/path/to/lvgl-micropython" + USER_C_MODULES="/abs/path/to/displayif;/abs/path/to/omnipython-lvgl/ports/micropython" ``` On Make ports (unix, windows, mimxrt, samd) point it at the directory that diff --git a/modules/displayif/docs/soft-reset-and-bring-up.md b/modules/displayif/docs/soft-reset-and-bring-up.md index 4cab5a9a..0323b25b 100644 --- a/modules/displayif/docs/soft-reset-and-bring-up.md +++ b/modules/displayif/docs/soft-reset-and-bring-up.md @@ -157,7 +157,7 @@ approach. Do not stack silent fallbacks. | Horizontal “sliding” / tearing under load (Qualia 720×720) | PSRAM cannot sustain 16bpp DPI alone | `bounce_buffer_size_px = 20 * h_res` + dirty-row `esp_cache_msync` in native blit/fill/`refresh` | | `AttributeError: 'DotClockFramebuffer' object has no attribute 'refresh'` (or `blit`) | Custom `attr` replaces `locals_dict` lookup | Expose methods in `attr` (same pattern as `mipidsi.Display`) | | UI ~1–2 FPS / multi‑second full redraws | Python per-pixel / `memoryview` path into SPIRAM FB (MP has no `memoryview.cast`) | Buffer typecode `'B'` + native `blit` / `fill_rect`; `fbdisplay` calls them when present | -| Illegal instruction / crash inside LVGL draw or early init (not only soft-reset) | `LV_GLOBAL_CUSTOM` not rooted in `MP_STATE_VM(mp_lv_roots)` | `lvgl-bindings` conf / emit / generated | +| Illegal instruction / crash inside LVGL draw or early init (not only soft-reset) | `LV_GLOBAL_CUSTOM` not rooted in `MP_STATE_VM(mp_lv_roots)` | `omnipython-lvgl` conf / emit / generated | | LVGL seconds advance ~½ wall clock | Fixed `tick_inc(period)` vs real elapsed | `lv.tick_inc(elapsed_ms)` from wall time in the timer callback | | Crash / corruption if the app timer runs during LVGL DisplayDriver setup | Tick into half-initialized LVGL | Stop the app timer around setup; arm loop only when ready (`display_driver`) | | Touch down never seen / stuck pressed | indev `read_cb` polls but does not always write PRESSED/RELEASED | Always call the touch callback after poll so LVGL sees edges | diff --git a/modules/jpegio/README.md b/modules/jpegio/README.md index eebf5e9a..474f00a2 100644 --- a/modules/jpegio/README.md +++ b/modules/jpegio/README.md @@ -16,7 +16,7 @@ media modules roadmap's Phase 1), so a firmware that wants JPEG now names directory's [`micropython.mk`](micropython.mk) / [`micropython.cmake`](micropython.cmake). Output is native-order RGB565, block by block, straight from TJpgDec — no full-frame buffer inside the module. -Beside the `lvgl-micropython` usermod it is also LVGL's JPEG decoder: the +Beside the `lvgl` usermod it is also LVGL's JPEG decoder: the same TJpgDec, registered through LVGL's public `lv_image_decoder_create` API, so `lv.image` shows JPEGs (webcam MJPEG frames included) with one decoder in the firmware — see [LVGL image decoder](#lvgl-image-decoder). @@ -261,7 +261,7 @@ to byte-swap for `RGB565_SWAPPED`); see above. And errors are typed - [`third_party/tjpgd/`](third_party/tjpgd/) — vendored TJpgDec: `tjpgd.c`, `tjpgd.h`, `tjpgdcnf.h`. The firmware's only TJpgDec, always compiled. - [`lvgl_decoder.c`](src/lvgl_decoder.c) / [`lvgl_decoder.h`](src/lvgl_decoder.h) — the LVGL image decoder on that - TJpgDec; compiled only beside `lvgl-micropython` (below). + TJpgDec; compiled only beside `lvgl` (below). - [`micropython.mk`](micropython.mk) / [`micropython.cmake`](micropython.cmake) — the module's glue ([`manifest.py`](manifest.py) names it with `c_module`). @@ -271,14 +271,14 @@ refuses to build with any other `JD_FORMAT`. **With LVGL (jpegio self-detects).** LVGL's own TJPGD is off by config on MicroPython -(`LV_USE_TJPGD 0` in lvgl-bindings' `lv_conf.h`), so nothing clashes with +(`LV_USE_TJPGD 0` in omnipython-lvgl's `lv_conf.h`), so nothing clashes with the vendored copy and LVGL has no JPEG decoder of its own; this module -supplies one. When lvgl-micropython is in the same build (`USER_C_MODULES` -names it, which a [`build_mp.py`](../../build_mp.py) build with both modules does), both glue files +supplies one. When LVGL is in the same build (`USER_C_MODULES` +names omnipython-lvgl's `ports/micropython`, which a [`build_mp.py`](../../build_mp.py) build with both modules does), both glue files compile `lvgl_decoder.c` with `-DJPEGIO_LVGL_DECODER=1`; its `lvgl/lvgl.h` -resolves through the bindings directory lvgl-micropython puts on the include +resolves through the repository root that port puts on the include path. `JPEGIO_LVGL=0` / `1` (make) or `-DJPEGIO_LVGL=OFF` / `ON` (CMake) -forces the decision. Without lvgl-micropython nothing links against LVGL +forces the decision. Without LVGL nothing links against LVGL (this module's CI builds it both ways), but the module still exports `register_lvgl_decoder()` and `lvgl_decoders()`; see *Why these two names are unconditional* below. @@ -295,7 +295,7 @@ of CircuitPython's jpegio by Jeff Epler for Adafruit Industries (MIT). ## LVGL image decoder The decoder itself ([`lvgl_decoder.c`](src/lvgl_decoder.c)) is compiled only beside -`lvgl-micropython` (see Build); the two Python names below exist on every +`lvgl` (see Build); the two Python names below exist on every build. It is an ordinary LVGL image decoder named `"jpegio"` (`lv_image_decoder_t.name`), created with `lv_image_decoder_create`, so `lv.image(...).set_src(...)` and every other LVGL image path decode @@ -331,8 +331,8 @@ a second decoder: came before `lv.init()`, and after every `lv.deinit()` / `lv.init()` cycle (LVGL clears its decoder list on deinit). On a build with no decoder in it the call always raises `RuntimeError`, and the message names both the cause - and the way out: *no lvgl-micropython sibling at build time (rebuild with - `JPEGIO_LVGL=1` to force it)*. + and the way out: *lvgl was not in the build (name both modules, or rebuild + with `JPEGIO_LVGL=1`)*. `jpegio.lvgl_decoders() -> tuple` lists LVGL's registered image decoders by name in the order LVGL consults them — `("jpegio", "LODEPNG", "BIN")` once @@ -370,7 +370,7 @@ brings the break straight back. Do not re-conditionalise either function. **Sources.** A variable source (`lv.image_dsc_t`) whose `data` starts with the SOI marker `FF D8`, or a file (through a registered `lv.fs_drv_t`, e.g. -lvgl-bindings' `fs_driver.py`) whose first two bytes are `FF D8` — the +omnipython-lvgl's `fs_driver.py`) whose first two bytes are `FF D8` — the extension is not consulted. Nothing more is sniffed: LVGL's own `is_jpg()` wants a 10-byte JFIF signature and so rejects every UVC MJPEG frame; here `jd_prepare` judges, for both source kinds, in the decoder's *info* step. @@ -389,7 +389,7 @@ blends RGB565 into an RGB565 display by row copy, its fast path. **Memory.** The decoder decodes the whole image in its *open* step into a draw buffer of `w * h * 2` bytes (from LVGL's image-cache allocator, i.e. the MicroPython heap), which LVGL frees at *close* — every refresh, unless -LVGL's image cache is on (`LV_CACHE_DEF_SIZE`, 0 in lvgl-bindings). That is +LVGL's image cache is on (`LV_CACHE_DEF_SIZE`, 0 in omnipython-lvgl). That is what LVGL's own full-decode decoders (`lv_lodepng.c`) do. It is not the block-wise, no-frame-buffer path `JpegDecoder.decode(callable)` offers: LVGL's `lv_tjpgd.c` decodes MCU by MCU only through exports it added to its diff --git a/modules/jpegio/manifest.py b/modules/jpegio/manifest.py index 47e3c42c..4036f4ac 100644 --- a/modules/jpegio/manifest.py +++ b/modules/jpegio/manifest.py @@ -1,5 +1,5 @@ # jpegio: JPEG decoding into RGB565 (CircuitPython's jpegio API, on ChaN's -# TJpgDec), and LVGL's JPEG decoder when lvgl-micropython is in the build. +# TJpgDec), and LVGL's JPEG decoder when LVGL (omnipython-lvgl) is in the build. # C only; every port. Moved here from displayif (media modules roadmap, # Phase 1). c_module(".") diff --git a/modules/jpegio/micropython.cmake b/modules/jpegio/micropython.cmake index 5b05517b..e47a721c 100644 --- a/modules/jpegio/micropython.cmake +++ b/modules/jpegio/micropython.cmake @@ -28,19 +28,24 @@ if(ESP_PLATFORM AND IDF_TARGET STREQUAL "esp32p4") target_link_libraries(usermod_jpegio INTERFACE idf::esp_driver_jpeg) endif() -# The LVGL image decoder (lvgl_decoder.c) is built only when lvgl-micropython is -# in this build, decided from USER_C_MODULES the way micropython.mk decides it: -# what is on disk beside this module says nothing about what this build -# selected, and jpegio has to build without LVGL. Override with -DJPEGIO_LVGL=ON/OFF. -# The decoder includes "lvgl/lvgl.h" and "lvgl/src/...", which resolve through -# the bindings directory lvgl-micropython itself puts on the include path, so -# nothing here needs to know where the bindings are. +# The LVGL image decoder (lvgl_decoder.c) is built only when LVGL is in this +# build: when USER_C_MODULES names omnipython-lvgl's ports/micropython (its +# repository root, two levels up, holds the generated binding), decided the +# way micropython.mk decides it: what is on disk beside this module says +# nothing about what this build selected, and jpegio has to build without +# LVGL. Override with -DJPEGIO_LVGL=ON/OFF. The decoder includes "lvgl/lvgl.h" +# and "lvgl/src/...", which resolve through the root that port itself puts on +# the include path, so nothing here needs to know where the checkout is. if(NOT DEFINED JPEGIO_LVGL) - if("${USER_C_MODULES}" MATCHES "lvgl-micropython") - set(JPEGIO_LVGL ON) - else() - set(JPEGIO_LVGL OFF) - endif() + set(JPEGIO_LVGL OFF) + foreach(_jpegio_mod ${USER_C_MODULES}) + if(NOT IS_DIRECTORY "${_jpegio_mod}") + get_filename_component(_jpegio_mod "${_jpegio_mod}" DIRECTORY) + endif() + if(EXISTS "${_jpegio_mod}/../../generated/lvgl_micropython.c") + set(JPEGIO_LVGL ON) + endif() + endforeach() endif() if(JPEGIO_LVGL) diff --git a/modules/jpegio/micropython.mk b/modules/jpegio/micropython.mk index fe166c17..578a680f 100644 --- a/modules/jpegio/micropython.mk +++ b/modules/jpegio/micropython.mk @@ -2,8 +2,8 @@ # Platform-neutral C, built on every port. # # The vendored third_party/tjpgd/tjpgd.c is the firmware's only TJpgDec. LVGL's own copy -# is off by config (LV_USE_TJPGD 0 in lvgl-bindings' lv_conf.h on MicroPython), -# so when lvgl-micropython is in the same build jpegio also compiles +# is off by config (LV_USE_TJPGD 0 in omnipython-lvgl's lv_conf.h on MicroPython), +# so when LVGL is in the same build jpegio also compiles # lvgl_decoder.c: an LVGL image decoder on this TJpgDec, registered through # LVGL's public lv_image_decoder_create API. Without it nothing here changes # and the build stays LVGL-less. @@ -43,24 +43,23 @@ SRC_USERMOD_LIB_C += $(JPEGIO_THIRD_PARTY)/tjpgd/tjpgd.c SRC_USERMOD_LIB_C += $(JPEGIO_DIR)/jpegio_hw.c SRC_USERMOD_LIB_C += $(JPEGIO_THIRD_PARTY)/jpegenc/jpegenc.c -# lvgl-micropython is in this build if USER_C_MODULES names it -- either as a -# module directory itself (MicroPython 1.29 c_module()) or as a parent -# directory one level above it (py.mk's glob). Override with JPEGIO_LVGL=0/1 -# on the make command line. -JPEGIO_LVMP_DIR := $(firstword $(filter %/lvgl-micropython,$(USER_C_MODULES:/=)) $(foreach d,$(USER_C_MODULES),$(wildcard $(d)/lvgl-micropython))) +# LVGL is in this build if USER_C_MODULES names omnipython-lvgl's +# ports/micropython: a module directory whose repository root, two levels up, +# holds the generated binding. Override with JPEGIO_LVGL=0/1 on the make +# command line. +JPEGIO_LVMP_DIR := $(firstword $(foreach d,$(USER_C_MODULES:/=),$(if $(wildcard $(d)/../../generated/lvgl_micropython.c),$(d)))) JPEGIO_LVGL ?= $(if $(JPEGIO_LVMP_DIR),1,0) ifeq ($(JPEGIO_LVGL),1) # lvgl_decoder.c includes "lvgl/lvgl.h" and "lvgl/src/...", which resolve -# through the -I$(BINDINGS_DIR) lvgl-micropython adds, wherever its bindings -# are (a sibling checkout or its own fetched .deps/), so nothing here derives -# that path. +# through the -I of omnipython-lvgl's root that its port adds, wherever that +# checkout is, so nothing here derives that path. CFLAGS_USERMOD += -DJPEGIO_LVGL_DECODER=1 # No qstrs of its own (the Python-visible names live in jpegio.c). SRC_USERMOD_LIB_C += $(JPEGIO_DIR)/lvgl_decoder.c # lvgl.h with LV_USE_FLOAT trips -Werror=double-promotion / float-conversion # on ports that append those after CFLAGS_USERMOD (unix, webassembly): the -# same per-object suppression lvgl-micropython puts on LVGL's own objects. +# same per-object suppression omnipython-lvgl puts on LVGL's own objects. $(eval $(BUILD)/$(patsubst $(JPEGIO_MOD_DIR)/%,$(notdir $(JPEGIO_MOD_DIR))/%,$(JPEGIO_DIR)/lvgl_decoder.o): CFLAGS += -Wno-double-promotion -Wno-float-conversion) endif endif diff --git a/modules/jpegio/src/jpegio.c b/modules/jpegio/src/jpegio.c index ea39fdb1..c1365fb4 100644 --- a/modules/jpegio/src/jpegio.c +++ b/modules/jpegio/src/jpegio.c @@ -25,8 +25,8 @@ #error "jpegio needs TJpgDec built with JD_FORMAT 1 (RGB565) -- see tjpgd/tjpgdcnf.h" #endif -// Defined by micropython.mk / micropython.cmake only when the lvgl-micropython -// sibling usermod is in the same build: jpegio then also registers an LVGL +// Defined by micropython.mk / micropython.cmake only when the omnipython-lvgl +// usermod (lvgl) is in the same build: jpegio then also registers an LVGL // image decoder on its TJpgDec (lvgl_decoder.c). #ifndef JPEGIO_LVGL_DECODER #define JPEGIO_LVGL_DECODER 0 @@ -546,7 +546,7 @@ static mp_obj_t jpegio_register_lvgl_decoder(void) { } return mp_const_true; #else - mp_raise_msg(&mp_type_RuntimeError, MP_ERROR_TEXT("jpegio was built without its LVGL decoder: lvgl-micropython was not in the build (name both modules, or rebuild with JPEGIO_LVGL=1)")); + mp_raise_msg(&mp_type_RuntimeError, MP_ERROR_TEXT("jpegio was built without its LVGL decoder: lvgl was not in the build (name both modules, or rebuild with JPEGIO_LVGL=1)")); #endif } static MP_DEFINE_CONST_FUN_OBJ_0(jpegio_register_lvgl_decoder_obj, jpegio_register_lvgl_decoder); diff --git a/modules/jpegio/src/lvgl_decoder.c b/modules/jpegio/src/lvgl_decoder.c index d72406ce..fe9fba8c 100644 --- a/modules/jpegio/src/lvgl_decoder.c +++ b/modules/jpegio/src/lvgl_decoder.c @@ -2,7 +2,7 @@ // jpegio's LVGL image decoder: baseline JPEG through jpegio's own TJpgDec, // registered with LVGL's public lv_image_decoder_create API. Phase 2 of the // org's docs/jpegio-vision.md: LVGL's built-in TJPGD is off (LV_USE_TJPGD 0 -// in lvgl-bindings' lv_conf.h on MicroPython), so a firmware with both +// in omnipython-lvgl's lv_conf.h on MicroPython), so a firmware with both // usermods carries one TJpgDec -- jpegio's, vendored in tjpgd/ -- and this // file is that object code's second user. // diff --git a/modules/jpegio/src/lvgl_decoder.h b/modules/jpegio/src/lvgl_decoder.h index 96318c2b..57af151b 100644 --- a/modules/jpegio/src/lvgl_decoder.h +++ b/modules/jpegio/src/lvgl_decoder.h @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT // jpegio's LVGL image decoder (lvgl_decoder.c). Compiled only when the -// lvgl-micropython sibling usermod is on the same build (JPEGIO_LVGL_DECODER). +// omnipython-lvgl usermod (lvgl) is on the same build (JPEGIO_LVGL_DECODER). #ifndef JPEGIO_LVGL_DECODER_H #define JPEGIO_LVGL_DECODER_H diff --git a/modules/jpegio/tests/hardware/board_lvgl_glass.py b/modules/jpegio/tests/hardware/board_lvgl_glass.py index 567a5f29..45842993 100644 --- a/modules/jpegio/tests/hardware/board_lvgl_glass.py +++ b/modules/jpegio/tests/hardware/board_lvgl_glass.py @@ -25,7 +25,7 @@ scr.set_style_bg_color(lv.color_black(), 0) img = lv.image(scr) try: - import fs_driver # frozen in by lvgl-micropython's manifest: LVGL reads files through it + import fs_driver # frozen in by omnipython-lvgl's manifest: LVGL reads files through it fs_driver.register("S") img.set_src("S:" + PATH) diff --git a/modules/jpegio/tests/test_jpegio_lvgl.py b/modules/jpegio/tests/test_jpegio_lvgl.py index c2a1bbd1..7b525955 100644 --- a/modules/jpegio/tests/test_jpegio_lvgl.py +++ b/modules/jpegio/tests/test_jpegio_lvgl.py @@ -1,4 +1,4 @@ -"""jpegio -- its LVGL image decoder, on unix MicroPython built beside lvgl-micropython. +"""jpegio -- its LVGL image decoder, on unix MicroPython built beside lvgl (omnipython-lvgl). The LVGL half of the Phase 2 gate in the org's docs/jpegio-vision.md, checked mechanically: @@ -39,11 +39,11 @@ -> one refresh -> gc.collect() dies with SIGBUS in MicroPython's gc_sweep_run_finalisers -> mp_load_method_maybe (a finaliser-flagged GC block whose type pointer is garbage) with no jpegio involved at all: - reproduced with a label on a build without displayif (lvgl-micropython + reproduced with a label on a build without displayif (omnipython-lvgl glue, not this decoder). Skips (exit 0, prints SKIP) when `import lvgl` fails: the interpreter was -built without the lvgl-micropython usermod, and this module's LVGL-less +built without the lvgl usermod, and this module's LVGL-less CI job runs this file too. Before skipping it asserts the other half of the contract, the one that only a decoder-less build can check: jpegio.register_lvgl_decoder() and jpegio.lvgl_decoders() are module @@ -57,7 +57,7 @@ try: import lvgl as lv except ImportError: - # No lvgl-micropython in this build, so lvgl_decoder.c was not compiled -- + # No lvgl in this build, so lvgl_decoder.c was not compiled -- # but the two module attributes are unconditional (jpegio.c keeps them out # of the #if, so the CMake ports' QSTR pass sees their names and the API # never silently disappears). This is the only build that can check it. @@ -73,7 +73,7 @@ assert jpegio.lvgl_decoders() == (), jpegio.lvgl_decoders() print("no LVGL decoder compiled in: lvgl_decoders() == ()") - print("SKIP: no `lvgl` module in this interpreter (build with the lvgl-micropython usermod)") + print("SKIP: no `lvgl` module in this interpreter (build with the lvgl usermod)") sys.exit(0) import binascii @@ -189,7 +189,7 @@ def flush_cb(self, disp, area, color_p): disp.flush_ready() def setup_display(frame): - """320x240 RGB565 display like lvgl-bindings/tools/test_lvgl_smoke.py's _setup_display.""" + """320x240 RGB565 display like omnipython-lvgl/tools/test_lvgl_smoke.py's _setup_display.""" disp = lv.display_create(W, H) disp.set_color_format(lv.COLOR_FORMAT.RGB565) buf = lv.draw_buf_create(W, H, lv.COLOR_FORMAT.RGB565, 0) @@ -351,7 +351,7 @@ def main(): assert sha256_hex(frame.buf) == bg, "half a scan drew something: %s" % sha256_hex(frame.buf) print("half a scan (%d of %d bytes): info OK, open refused, nothing drawn, refr_now survived" % (len(half), len(rst))) - # 5. a file source through fs_driver.py (frozen in by lvgl-micropython's manifest) + # 5. a file source through fs_driver.py (frozen in by omnipython-lvgl's manifest) print("== 5. file source") try: import fs_driver diff --git a/scripts/exercise_build.sh b/scripts/exercise_build.sh index b7dda9ca..36dff4ab 100755 --- a/scripts/exercise_build.sh +++ b/scripts/exercise_build.sh @@ -225,7 +225,7 @@ print('RUN', sys.implementation._machine, np.sum(np.array([1, 2, 3])), board_con elif [ $m_t = pico ]; then build pico --port rp2 --board RPI_PICO --modules displayif,jpegio,pygraphics,palettes,pdwidgets else - build pico2 --port rp2 --board RPI_PICO2 --modules displayif,jpegio,lvgl-micropython + build pico2 --port rp2 --board RPI_PICO2 --modules displayif,jpegio,lvgl fi done diff --git a/scripts/pin_lvgl_ref.sh b/scripts/pin_lvgl_ref.sh new file mode 100755 index 00000000..1804743c --- /dev/null +++ b/scripts/pin_lvgl_ref.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +# Point modules.lock's omnipython-lvgl row at a branch, tag or commit, for a +# build of a ref that isn't pinned yet: release-firmware.yml runs it when its +# omnipython-lvgl-ref input is given. A branch or tag becomes the commit it +# names now, since build_mp.py fetches by full commit. +# +# scripts/pin_lvgl_ref.sh +set -euo pipefail +ref="${1:?usage: $0 }" +lock="$(dirname "$0")/../modules.lock" +url=$(awk '$1=="omnipython-lvgl"{print $2}' "$lock") +[ -n "$url" ] || { echo "modules.lock has no omnipython-lvgl row" >&2; exit 1; } +if [[ "$ref" =~ ^[0-9a-f]{40}$ ]]; then + sha=$ref +else + # An annotated tag lists twice; its peeled line (^{}), the commit, sorts last. + sha=$(git ls-remote "$url" "$ref" "$ref^{}" | awk 'END{print $1}') +fi +[[ "$sha" =~ ^[0-9a-f]{40}$ ]] || { echo "no branch, tag or commit '$ref' in $url" >&2; exit 1; } +sed -i -E "s|^(omnipython-lvgl[[:space:]]+[^[:space:]]+[[:space:]]+)[0-9a-f]{40}|\1$sha|" "$lock" +grep '^omnipython-lvgl' "$lock" diff --git a/scripts/release_firmware.py b/scripts/release_firmware.py index 9b40cec2..55e1332c 100755 --- a/scripts/release_firmware.py +++ b/scripts/release_firmware.py @@ -8,14 +8,13 @@ Print the build matrix as JSON for the workflow: one row per cell. The Pico 2's and the Pico Plus 2 W's rows are left out unless --pico2 is given. - release_firmware.py build [--ws DIR] + release_firmware.py build Build one cell with build_mp.py and put its files, a sha256 of each and - a cell.json in //. --ws is a directory holding a - checkout of lvgl-circuitpython, which the CircuitPython cells compile - in. + a cell.json in //. LVGL (omnipython-lvgl) comes in at + the commit modules.lock pins, for MicroPython and CircuitPython alike. release_firmware.py assemble [--pico2] [--tag TAG] - [--run-url URL] [--lvgl-circuitpython-commit SHA] + [--run-url URL] Check every cell in (one directory per cell, as `build` wrote them), name the files for the set and write firmware.json beside them in . Exits 1 if any check fails. It never @@ -75,23 +74,23 @@ chip="esp32s3", offset=0, kind="esp32", deps="idf"), dict(id="cp-p4-devkit", label="ESP32-P4-WIFI6-DEV-KIT", interpreter="circuitpython", port="espressif", board="waveshare_esp32p4_wifi6_dev_kit", variant=None, flash="16MB", - modules="audiodsp,pygraphics,pngio,tsmux,ppa,jpegio,cameraif,castif,h264enc,usbhost,pydevices,pdwidgets,palettes,@lvgl-circuitpython", + modules="audiodsp,pygraphics,pngio,tsmux,ppa,jpegio,cameraif,castif,h264enc,usbhost,pydevices,pdwidgets,palettes,lvgl", chip="esp32p4", offset=0, kind="espressif", deps="cp-esp"), dict(id="cp-s3-lcd7", label="ESP32-S3-Touch-LCD-7", interpreter="circuitpython", port="espressif", board="waveshare_esp32s3_touch_lcd_7", variant=None, flash="8MB", - modules="audiodsp,pygraphics,pngio,tsmux,pydevices,pdwidgets,palettes,@lvgl-circuitpython", + modules="audiodsp,pygraphics,pngio,tsmux,pydevices,pdwidgets,palettes,lvgl", chip="esp32s3", offset=0, kind="espressif", deps="cp-esp"), dict(id="cp-s3-lcd43", label="ESP32-S3-Touch-LCD-4.3", interpreter="circuitpython", port="espressif", board="waveshare_esp32s3_touch_lcd_43", variant=None, flash="8MB", - modules="audiodsp,pygraphics,pngio,tsmux,pydevices,pdwidgets,palettes,@lvgl-circuitpython", + modules="audiodsp,pygraphics,pngio,tsmux,pydevices,pdwidgets,palettes,lvgl", chip="esp32s3", offset=0, kind="espressif", deps="cp-esp"), dict(id="cp-twatch", label="LilyGO T-Watch S3", interpreter="circuitpython", port="espressif", board="lilygo_twatch_s3_single_app", variant=None, flash="16MB", - modules="audiodsp,pygraphics,pngio,tsmux,pydevices,pdwidgets,palettes,@lvgl-circuitpython", + modules="audiodsp,pygraphics,pngio,tsmux,pydevices,pdwidgets,palettes,lvgl", chip="esp32s3", offset=0, kind="espressif", deps="cp-esp"), dict(id="cp-feather-rp2040", label="Adafruit Feather RP2040 + display wing", interpreter="circuitpython", port="raspberrypi", board="feather_rp2040_lvgl", variant=None, flash="8MB", - modules="pygraphics,pngio,tsmux,pydevices,pdwidgets,palettes,@lvgl-circuitpython", chip="rp2040", offset=None, kind="rp2", deps="none"), + modules="pygraphics,pngio,tsmux,pydevices,pdwidgets,palettes,lvgl", chip="rp2040", offset=None, kind="rp2", deps="none"), # Off by default until a board is on the bench to run it on. dict(id="mp-pico2", label="Raspberry Pi Pico 2", interpreter="micropython", port="rp2", board="RPI_PICO2", variant=None, flash="4MB", modules="all", @@ -201,7 +200,7 @@ def build_dir(c): return REPO / "builds" / c["port"] / c["board"] / (c["variant"] or "default") -def build_args(c, ws): +def build_args(c): args = [sys.executable, str(REPO / "build_mp.py"), "--port", c["port"], "--board", c["board"]] if c["interpreter"] == "circuitpython": args[2:2] = ["--interpreter", "circuitpython"] @@ -209,24 +208,17 @@ def build_args(c, ws): args += ["--variant", c["variant"]] if c["interpreter"] == "micropython" and c["port"] == "esp32": args += ["--flash", c["flash"]] - mods = [] - for m in c["modules"].split(","): - if m.startswith("@"): - if not ws: - raise Failed(f"{c['id']} compiles in {m[1:]}: give --ws a directory holding it") - m = str(Path(ws, m[1:]).resolve()) - mods.append(m) - args += ["--modules", ",".join(mods)] + args += ["--modules", c["modules"]] return args -def build(cell_id, out, ws): +def build(cell_id, out): c = cell(cell_id) out = Path(out) / c["id"] if out.exists(): shutil.rmtree(out) out.mkdir(parents=True) - args = build_args(c, ws) + args = build_args(c) print("+", " ".join(args), flush=True) start = time.time() rc = subprocess.run(args, cwd=REPO).returncode @@ -384,7 +376,7 @@ def preview_reasons(upstream_describe, cp_version, deps): return reasons -def assemble(indir, out, pico2, tag, run_url, lvgl_cp_commit, now=None): +def assemble(indir, out, pico2, tag, run_url, now=None): indir, out = Path(indir), Path(out) now = now or datetime.datetime.now(datetime.timezone.utc) date = now.strftime("%Y%m%d") @@ -447,8 +439,6 @@ def assemble(indir, out, pico2, tag, run_url, lvgl_cp_commit, now=None): pins = {"UPSTREAM": (REPO / "UPSTREAM").read_text().strip(), "deps.lock": deps, "modules.lock": {r[0]: r[2] for r in read_lock("modules.lock")}} - if lvgl_cp_commit: - pins["lvgl-circuitpython"] = lvgl_cp_commit reasons = preview_reasons(mp_version, cp_version, deps) doc = {"schema": 1, "tag": tag, "built": now.strftime("%Y-%m-%dT%H:%M:%SZ"), "run": run_url, "micropython": mp_version, "circuitpython": cp_version, @@ -536,14 +526,12 @@ def main(argv=None): b = sub.add_parser("build") b.add_argument("cell") b.add_argument("out") - b.add_argument("--ws") a = sub.add_parser("assemble") a.add_argument("indir") a.add_argument("out") a.add_argument("--pico2", action="store_true") a.add_argument("--tag", default="") a.add_argument("--run-url", default="") - a.add_argument("--lvgl-circuitpython-commit", default="") n = sub.add_parser("notes") n.add_argument("firmware_json") n.add_argument("--out") @@ -565,9 +553,9 @@ def main(argv=None): else: print(text) elif args.cmd == "build": - build(args.cell, args.out, args.ws) + build(args.cell, args.out) elif args.cmd == "assemble": - doc = assemble(args.indir, args.out, args.pico2, args.tag, args.run_url, args.lvgl_circuitpython_commit) + doc = assemble(args.indir, args.out, args.pico2, args.tag, args.run_url) text = summary(doc) print(text) if os.environ.get("GITHUB_STEP_SUMMARY"): diff --git a/test_build_mp.py b/test_build_mp.py index ed11b3c5..31964104 100644 --- a/test_build_mp.py +++ b/test_build_mp.py @@ -127,10 +127,9 @@ def git(*args): self.assertNotEqual(build_mp.record([repo])["mod"]["changes"], first["changes"]) -class LvglBindings(unittest.TestCase): - """lvgl-bindings comes from modules.lock and lands in the build record.""" - - def git_repo(self, path, name="f.py"): +class GitRepo: + @staticmethod + def git_repo(path, name="f.py"): path.mkdir(parents=True) def git(*args): @@ -142,31 +141,97 @@ def git(*args): git("-c", "user.name=t", "-c", "user.email=t@t", "commit", "-q", "-m", "one") return path - def test_not_fetched_without_lvgl_micropython(self): + +class Lvgl(unittest.TestCase): + """omnipython-lvgl comes from modules.lock: --modules names it lvgl, a build + takes the port for its interpreter, and the record names the repository.""" + + def checkout(self, tmp): + root = GitRepo.git_repo(Path(tmp) / "omnipython-lvgl", "lv_conf.h") + for port in ("micropython", "circuitpython"): + (root / "ports" / port).mkdir(parents=True) + (root / "ports" / port / "micropython.mk").write_text("") + return root + + def modules_dir(self, tmp, *names): + modules = Path(tmp) / "modules" + for n in names: + (modules / n).mkdir(parents=True) + saved = build_mp.MODULES_DIR + build_mp.MODULES_DIR = modules + self.addCleanup(setattr, build_mp, "MODULES_DIR", saved) + return modules + + def test_lvgl_is_the_port_for_the_interpreter(self): with tempfile.TemporaryDirectory() as tmp: - self.assertIsNone(build_mp.lvgl_bindings(None, [Path(tmp) / "ulab"])) + root = self.checkout(tmp) + self.modules_dir(tmp, "jpegio") + for interp in ("micropython", "circuitpython"): + spec, dirs, _ = build_mp.resolve_modules("jpegio,lvgl", None, interp, lambda: root) + port = (root / "ports" / interp).resolve() + self.assertEqual(dirs[1], port) + self.assertEqual(spec, f"jpegio,{port}") + self.assertEqual(build_mp.lvgl_in(dirs), root.resolve()) + + def test_a_path_to_the_checkout_is_the_port_too(self): + with tempfile.TemporaryDirectory() as tmp: + root = self.checkout(tmp) + self.modules_dir(tmp) + _, dirs, _ = build_mp.resolve_modules(str(root), None, "circuitpython") + self.assertEqual(dirs, [(root / "ports" / "circuitpython").resolve()]) + with self.assertRaises(SystemExit) as cm: + build_mp.resolve_modules(str(root / "ports" / "micropython"), None, "circuitpython") + self.assertIn("another interpreter", str(cm.exception)) - def test_record_names_the_bindings(self): + def test_all_takes_lvgl_and_fetches_nothing_otherwise(self): with tempfile.TemporaryDirectory() as tmp: - mod = self.git_repo(Path(tmp) / "mod") - bindings = self.git_repo(Path(tmp) / ".lvgl-bindings", "lv_conf.h") - rows = build_mp.record([mod], bindings) - self.assertEqual(sorted(rows), ["lvgl-bindings", "mod"]) - self.assertEqual(len(rows["lvgl-bindings"]["commit"]), 40) - self.assertFalse(rows["lvgl-bindings"]["dirty"]) - (bindings / "lv_conf.h").write_text("x = 2\n") - self.assertTrue(build_mp.record([mod], bindings)["lvgl-bindings"]["dirty"]) + root = self.checkout(tmp) + self.modules_dir(tmp, "all", "a") + asked = [] + + def lvgl(): + asked.append(1) + return root + + build_mp.resolve_modules("a", None, "micropython", lvgl) + self.assertEqual(asked, []) + _, dirs, _ = build_mp.resolve_modules("all", None, "micropython", lvgl) + self.assertIn((root / "ports" / "micropython").resolve(), dirs) + + def test_the_old_names_are_refused_naming_lvgl(self): + for old in ("lvgl-micropython", "lvgl-circuitpython", "lvgl-bindings"): + with self.subTest(old), self.assertRaises(SystemExit) as cm: + build_mp.resolve_modules(f"jpegio,{old}") + self.assertIn("--modules ...,lvgl", str(cm.exception)) + + @unittest.skipIf(os.name == "nt", "the CircuitPython build is Linux or WSL") + def test_a_circuitpython_build_refuses_an_old_name_before_fetching_anything(self): + args = type("A", (), {"flash": False, "modules": "jpegio,/some/where/lvgl-circuitpython"})() + with self.assertRaises(SystemExit) as cm: + build_mp.build_circuitpython(args, [], None) + self.assertIn("lvgl", str(cm.exception)) + + def test_record_names_the_repository(self): + with tempfile.TemporaryDirectory() as tmp: + root = self.checkout(tmp) + mod = GitRepo.git_repo(Path(tmp) / "mod") + rows = build_mp.record([mod, root / "ports" / "micropython"]) + self.assertEqual(sorted(rows), ["mod", "omnipython-lvgl"]) + self.assertEqual(len(rows["omnipython-lvgl"]["commit"]), 40) + self.assertFalse(rows["omnipython-lvgl"]["dirty"]) + (root / "lv_conf.h").write_text("x = 2\n") + self.assertTrue(build_mp.record([root / "ports" / "micropython"])["omnipython-lvgl"]["dirty"]) def test_a_short_commit_is_refused(self): with tempfile.TemporaryDirectory() as tmp: repo = Path(tmp) (repo / "modules").mkdir() - (repo / "modules.lock").write_text("lvgl-bindings https://example.invalid/lvgl-bindings.git 7740c2f\n") + (repo / "modules.lock").write_text("omnipython-lvgl https://example.invalid/omnipython-lvgl.git 84d1de3\n") saved = build_mp.REPO, build_mp.MODULES_DIR build_mp.REPO, build_mp.MODULES_DIR = repo, repo / "modules" try: with self.assertRaises(SystemExit) as cm: - build_mp.lvgl_bindings(None, [repo / "modules" / "lvgl-micropython"]) + build_mp.lvgl_checkout(None) finally: build_mp.REPO, build_mp.MODULES_DIR = saved self.assertIn("full 40-character commit", str(cm.exception)) @@ -274,32 +339,12 @@ def test_the_workspace_checkout_of_the_library_wins_over_the_pin(self): def test_the_build_record_names_the_library_commit(self): with tempfile.TemporaryDirectory() as tmp: - lib = LvglBindings.git_repo(None, Path(tmp) / "lib", "libbuild.py") - rows = build_mp.record([], None, lib) + lib = GitRepo.git_repo(Path(tmp) / "lib", "libbuild.py") + rows = build_mp.record([], lib) self.assertEqual(list(rows), ["omnipython-lib"]) self.assertEqual(len(rows["omnipython-lib"]["commit"]), 40) -class RefuseLvglMicropython(unittest.TestCase): - """CircuitPython's LVGL binding is lvgl-circuitpython, not lvgl-micropython.""" - - def test_lvgl_micropython_is_refused_naming_lvgl_circuitpython(self): - with self.assertRaises(SystemExit) as cm: - build_mp.refuse_lvgl_micropython(["jpegio", "lvgl-micropython"]) - self.assertIn("lvgl-circuitpython", str(cm.exception)) - self.assertIn("--modules", str(cm.exception)) - - def test_the_circuitpython_binding_and_other_modules_pass(self): - build_mp.refuse_lvgl_micropython(["jpegio", "lvgl-circuitpython", ""]) - - @unittest.skipIf(os.name == "nt", "the CircuitPython build is Linux or WSL") - def test_a_circuitpython_build_refuses_before_fetching_anything(self): - args = type("A", (), {"flash": False, "modules": "jpegio,/some/where/lvgl-micropython"})() - with self.assertRaises(SystemExit) as cm: - build_mp.build_circuitpython(args, [], None) - self.assertIn("lvgl-circuitpython", str(cm.exception)) - - class MicropythonVersion(unittest.TestCase): """sys.version names the upstream commit's release, from tags a CI clone has to fetch.""" diff --git a/test_release_firmware.py b/test_release_firmware.py index c2cf969d..e9a4598f 100644 --- a/test_release_firmware.py +++ b/test_release_firmware.py @@ -80,19 +80,17 @@ def test_every_cell_names_its_files(self): def test_build_command(self): c = rf.cell("mp-s3-lcd7") - args = rf.build_args(c, None) + args = rf.build_args(c) self.assertIn("--flash", args) self.assertEqual(args[args.index("--variant") + 1], "LCD_7") self.assertEqual(args[args.index("--modules") + 1], "all") - def test_circuitpython_needs_the_lvgl_checkout(self): + def test_circuitpython_names_lvgl_like_micropython(self): c = rf.cell("cp-s3-lcd7") - with self.assertRaises(rf.Failed): - rf.build_args(c, None) - args = rf.build_args(c, "/tmp/ws") + args = rf.build_args(c) self.assertIn("--interpreter", args) self.assertNotIn("--flash", args) - self.assertTrue(args[args.index("--modules") + 1].endswith("/tmp/ws/lvgl-circuitpython")) + self.assertEqual(args[args.index("--modules") + 1].split(",")[-1], "lvgl") class Assemble(unittest.TestCase): @@ -104,7 +102,7 @@ def setUp(self): fake_set(self.indir) def run_assemble(self, **kw): - return rf.assemble(self.indir, self.out, kw.pop("pico2", False), kw.pop("tag", ""), "https://run", "b" * 40, **kw) + return rf.assemble(self.indir, self.out, kw.pop("pico2", False), kw.pop("tag", ""), "https://run", **kw) def fails_with(self, text): with self.assertRaises(rf.Failed) as cm: @@ -126,7 +124,7 @@ def test_firmware_json_records_the_pins_and_the_run(self): self.assertEqual(doc["pins"]["UPSTREAM"], (rf.REPO / "UPSTREAM").read_text().strip()) self.assertIn("esp-idf", doc["pins"]["deps.lock"]) self.assertIn("omnipython-lib", doc["pins"]["modules.lock"]) - self.assertEqual(doc["pins"]["lvgl-circuitpython"], "b" * 40) + self.assertIn("omnipython-lvgl", doc["pins"]["modules.lock"]) s3 = next(b for b in doc["builds"] if b["id"] == "mp-s3-lcd7") self.assertEqual(s3["flash_offset"], "0x0") self.assertEqual(s3["app_offset"], "0x10000") @@ -337,7 +335,7 @@ def setUp(self): indir = self.root / "in" indir.mkdir() fake_set(indir) - self.doc = rf.assemble(indir, self.root / "out", False, "staged-20261020", "https://run", "b" * 40) + self.doc = rf.assemble(indir, self.root / "out", False, "staged-20261020", "https://run") def test_a_preview_set_takes_only_a_staged_tag(self): rf.check_draft_tag(self.doc) diff --git a/test_stage_firmware_page.py b/test_stage_firmware_page.py index e1319a32..730d4d81 100644 --- a/test_stage_firmware_page.py +++ b/test_stage_firmware_page.py @@ -27,7 +27,7 @@ def setUp(self): indir.mkdir() fake_set(indir) self.fw = self.root / "fw" - self.doc = rf.assemble(indir, self.fw, False, "staged-20261020", "https://run", "b" * 40) + self.doc = rf.assemble(indir, self.fw, False, "staged-20261020", "https://run") self.page = self.root / "page" def test_files_manifests_and_firmware_json_land(self): From d68ccd0a3552a8275a08560dee6da45590116017 Mon Sep 17 00:00:00 2001 From: Brad Barnett <127794626+bdbarnett@users.noreply.github.com> Date: Fri, 30 Oct 2026 05:47:14 -0500 Subject: [PATCH 2/5] Coredump test: a P4 table may exist, but has no coredump slot WIFI6_DEV_KIT_H2 has carried its own partitions.csv since #219, so the test now checks for the slot that --coredump refuses on, not for the table's absence. --- test_build_mp.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/test_build_mp.py b/test_build_mp.py index 31964104..96b3f0e3 100644 --- a/test_build_mp.py +++ b/test_build_mp.py @@ -723,15 +723,16 @@ def test_only_sdkconfig_coredump_names_it(self): def test_tables_are_the_same_with_or_without_the_flag_and_only_the_s3_has_the_slot(self): # build_mp.py picks one table whatever the flag says; a standard S3 image - # and a troubleshooting one share it, so the filesystem doesn't move. The - # P4 has no table of ours, so --coredump refuses there. + # and a troubleshooting one share it, so the filesystem doesn't move. A + # P4 table, where there is one, has no coredump slot, so --coredump + # refuses there. for c in self.cells: table = build_mp.esp32_table(c["chip"], c["board"], c["variant"]) with self.subTest(c["id"]): if c["chip"] == "esp32s3": self.assertTrue(build_mp.has_coredump_partition(table)) else: - self.assertIsNone(table) + self.assertFalse(table and build_mp.has_coredump_partition(table)) def test_a_table_without_the_partition_is_seen(self): with tempfile.TemporaryDirectory() as tmp: From 99c1df4bca50aaf2c83d5698d90c394f1baba687 Mon Sep 17 00:00:00 2001 From: Brad Barnett <127794626+bdbarnett@users.noreply.github.com> Date: Fri, 30 Oct 2026 05:51:42 -0500 Subject: [PATCH 3/5] Pin omnipython-lvgl at 9b4240d, which builds on Windows --- modules.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules.lock b/modules.lock index ac0b2664..122192dd 100644 --- a/modules.lock +++ b/modules.lock @@ -20,7 +20,7 @@ # interpreter. Move the commit here to move every LVGL build, MicroPython's and # CircuitPython's. audiodsp https://github.com/PyDevices/audiodsp.git f763ec30d008a668fe8cbadf132c56f2fc099749 -omnipython-lvgl https://github.com/PyDevices/omnipython-lvgl.git 84d1de375b014c7d49b4ccbd3be551813091f9ef +omnipython-lvgl https://github.com/PyDevices/omnipython-lvgl.git 9b4240dbb476221fe74bd02c4efb388bfaa5f7e4 pygraphics https://github.com/PyDevices/pygraphics.git 1c193cfef534a3bdb85d4b112a66e0d4e76692ca ulab https://github.com/v923z/micropython-ulab.git 1d3ddd8f5228e2c7335dac71f4846e6abb0ec9f9 omnipython-lib https://github.com/PyDevices/pydevices.git 80a1fcbcf9ed970746ae9c178cadf1ab92d5d75b From b383092bb466ca6fcd5eadb7c5d05a114d4f8a0c Mon Sep 17 00:00:00 2001 From: Brad Barnett <127794626+bdbarnett@users.noreply.github.com> Date: Fri, 30 Oct 2026 05:58:37 -0500 Subject: [PATCH 4/5] Pin omnipython-lvgl at f1a0603, whose flag rules hold on Windows --- modules.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules.lock b/modules.lock index 122192dd..a8ff7bd8 100644 --- a/modules.lock +++ b/modules.lock @@ -20,7 +20,7 @@ # interpreter. Move the commit here to move every LVGL build, MicroPython's and # CircuitPython's. audiodsp https://github.com/PyDevices/audiodsp.git f763ec30d008a668fe8cbadf132c56f2fc099749 -omnipython-lvgl https://github.com/PyDevices/omnipython-lvgl.git 9b4240dbb476221fe74bd02c4efb388bfaa5f7e4 +omnipython-lvgl https://github.com/PyDevices/omnipython-lvgl.git f1a0603e00c4511327e433f2b03d028a29dd995d pygraphics https://github.com/PyDevices/pygraphics.git 1c193cfef534a3bdb85d4b112a66e0d4e76692ca ulab https://github.com/v923z/micropython-ulab.git 1d3ddd8f5228e2c7335dac71f4846e6abb0ec9f9 omnipython-lib https://github.com/PyDevices/pydevices.git 80a1fcbcf9ed970746ae9c178cadf1ab92d5d75b From 34d4bc6cebc92b6caf6c646e77e8db358f4e0b40 Mon Sep 17 00:00:00 2001 From: Brad Barnett <127794626+bdbarnett@users.noreply.github.com> Date: Fri, 30 Oct 2026 06:01:09 -0500 Subject: [PATCH 5/5] modules.lock: omnipython-lvgl at e6f88b7, main after #1 --- modules.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules.lock b/modules.lock index a8ff7bd8..70105b18 100644 --- a/modules.lock +++ b/modules.lock @@ -20,7 +20,7 @@ # interpreter. Move the commit here to move every LVGL build, MicroPython's and # CircuitPython's. audiodsp https://github.com/PyDevices/audiodsp.git f763ec30d008a668fe8cbadf132c56f2fc099749 -omnipython-lvgl https://github.com/PyDevices/omnipython-lvgl.git f1a0603e00c4511327e433f2b03d028a29dd995d +omnipython-lvgl https://github.com/PyDevices/omnipython-lvgl.git e6f88b7737e3c5252e92a6e717142875127816ca pygraphics https://github.com/PyDevices/pygraphics.git 1c193cfef534a3bdb85d4b112a66e0d4e76692ca ulab https://github.com/v923z/micropython-ulab.git 1d3ddd8f5228e2c7335dac71f4846e6abb0ec9f9 omnipython-lib https://github.com/PyDevices/pydevices.git 80a1fcbcf9ed970746ae9c178cadf1ab92d5d75b