diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index f9c5aa7..67cede3 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -6,12 +6,10 @@ on: branches: [main] paths-ignore: - '**.md' - - 'docs/**' - 'LICENSE' pull_request: paths-ignore: - '**.md' - - 'docs/**' - 'LICENSE' permissions: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..e165940 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,206 @@ +# Contributing to lvgl-python + +lvgl-python owns the CPython runtime layer, the packaging, the tests and the +release policy. lvgl-bindings owns the generator and the API decisions. The +generated bindings and the `fs_driver.py` helper are vendored here at one exact +bindings commit: they are inputs, not hand-edited source. + +| If the change concerns... | Start in... | +|---|---| +| API availability, generated wrappers, or the synced Python helpers | [lvgl-bindings](https://github.com/PyDevices/lvgl-bindings) | +| CPython object ownership, GIL or lock behavior, wheel packaging, CPython tests | this repository | +| A program's screens, board configuration or user interaction | the application's own repository | + +Good first changes are a focused regression test for a lifecycle or conversion +bug, a fix to the docs, or a fix to the handwritten runtime when the generated +API already offers the surface you want. The docs for this package live with +[lvgl-bindings](https://github.com/PyDevices/lvgl-bindings/tree/main/docs), and +the [docs site](https://github.com/PyDevices/lvgl-bindings/blob/main/docs/index.md) is built from there. + +## Where things are + +```text +lvgl-python/ + LVGL_BINDINGS_COMMIT exact source of the vendored bindings + JPEGIO_COMMIT exact source of src/jpegio/ + generated/lvgl_python.c vendored binding, synced from lvgl-bindings + generated/lvgl.pyi type stubs, copied into the install + lv_conf.h LVGL config for the build + fs_driver.py optional helper (import fs_driver) + lvgl/ LVGL C sources (git submodule) + src/lvpy_runtime.c, .h CPython glue: wrappers, converters, locking, the GIL + src/lvpy_jpegio.c registers the JPEG decoder at lv.init() + src/jpegio/ jpegio's LVGL decoder and TJpgDec (synced) + tests/ unit tests + scripts/ sync, version and wheel scripts + setup.py builds the one extension module +``` + +| Component | Role | +|-----------|------| +| `generated/lvgl_python.c` | Types, methods, module functions and callbacks | +| `src/lvpy_runtime.c`, `src/lvpy_runtime.h` | Object wrappers (`py_lv_obj_t` maps an `lv_obj_t *` to Python and keeps its callback dicts), struct wrappers (`py_lv_struct_t`), conversion helpers, locking, GIL handling and callback ownership | +| `setup.py` | Defines the `lvgl` extension and compiles the runtime, the generated binding and all the LVGL C sources | +| `lvgl-bindings` | The generator, not needed to compile | + +Do not edit `generated/lvgl_python.c`, `generated/lvgl.pyi`, `fs_driver.py` or +`lv_conf.h` here. Change the generator or the canonical helper in +lvgl-bindings, regenerate there, and sync. Read `src/lvpy_runtime.h` first, then +follow one call from `import lvgl` into `generated/lvgl_python.c` and back +through `src/lvpy_runtime.c`; `tests/test_lvgl_init.py` is short and shows the +lifecycle contract the runtime has to keep. + +## Build and test + +You need only this clone and its `lvgl` submodule: [Build lvgl-python from source](https://github.com/PyDevices/lvgl-bindings/blob/main/docs/guides/build-lvgl-python-from-source.md) +has the toolchain and the editable install. Then: + +```bash +.venv/bin/python -m unittest discover -s tests -v +``` + +The tests cover the recorded bindings source, the installed `lvgl.pyi` stub, +initialization and deinitialization, widget creation, callback lifetime +(`tests/test_callback_lifetime.py`, `tests/test_struct_callback_lifetime.py`) +and teardown at interpreter exit. After changing native code +(`src/lvpy_runtime.c` or `generated/lvgl_python.c`), reinstall the editable +build before you test: an editable install doesn't recompile on import. + +## Sync from lvgl-bindings + +The vendored files track the latest synced lvgl-bindings commit. A normal +`pip install -e .` compiles them, so you don't need a lvgl-bindings clone to +build or test. + +Clone [lvgl-bindings](https://github.com/PyDevices/lvgl-bindings) as a sibling +only to change the generator or regenerate bindings: to edit the model or the +CPython emitter in `binding/`, to run `./regenerate_all.sh --target cpython`, +or to run the shared smoke script `tools/test_lvgl_smoke.py`. To refresh the +vendored files, without a sibling clone, from an exact commit or a release tag: + +```bash +./scripts/sync_from_lvgl_bindings.sh --ref <40-character-commit-sha> +./scripts/sync_from_lvgl_bindings.sh --ref v9.5.N +``` + +The script clones lvgl-bindings into a temporary directory, copies the +generated files, and updates the `lvgl` submodule pin. The JPEG decoder comes +the same way from +[micropython-pydevices](https://github.com/PyDevices/micropython-pydevices)'s +`modules/jpegio`, so a JPEG draws the same on every interpreter: + +```bash +./scripts/sync_from_jpegio.sh --ref <40-character-commit-sha> +``` + +To change API coverage, regenerate in a lvgl-bindings clone, commit that +source, sync its exact SHA, and reinstall: + +```bash +cd ../lvgl-bindings && ./regenerate_all.sh --target cpython +cd ../lvgl-python +./scripts/sync_from_lvgl_bindings.sh --ref <40-character-commit-sha> +.venv/bin/pip install -e . +``` + +## Releases + +`lvgl-python` is the packaged CPython endpoint for the API lvgl-bindings +generates. Release inputs are immutable: every sync takes a full 40-character +bindings commit or a `vX.Y.Z` bindings tag and records the resolved commit in +`LVGL_BINDINGS_COMMIT`. + +Generation, validation, synchronization and publication are separate actions: + +- `lvgl-bindings/regenerate_all.sh` writes artifacts only. +- `lvgl-bindings/scripts/release_dry_run.sh` generates into temporary storage, + verifies every artifact, runs the tests and computes the expected version, + without committing, tagging, pushing, dispatching or publishing. +- This repository's **Sync and release** workflow defaults to `dry-run`. + `sync` mode commits and pushes the synchronized sources. `release` mode + commits, creates a GitHub Release and starts package publication. +- A normal push to either repository never starts a release. + +### Version + +Versions are `LVGL_MAJOR.LVGL_MINOR.N`, so the current line is `9.5.N`. `N` is +one greater than the highest existing `v9.5.*` tag here. Preview it without +changing anything: + +```bash +./scripts/next_release_version.sh --verbose +``` + +### Dry run locally + +Sync an exact bindings source, rebuild the extension, run its tests and the +matching canonical smoke suite, then look at the next version: + +```bash +./scripts/sync_from_lvgl_bindings.sh --ref <40-character-bindings-sha> +python -m pip install -e . +python -m unittest discover -s tests -v +python ../lvgl-bindings/tools/test_lvgl_smoke.py +./scripts/next_release_version.sh --verbose +``` + +The sync changes only the local checkout: the vendored `generated/lvgl_python.c`, +`generated/lvgl.pyi`, `lv_conf.h`, the Python helpers, the LVGL submodule pin +and `LVGL_BINDINGS_COMMIT`. + +### Run the workflow + +Run **Sync and release** with two inputs: `lvgl_bindings_ref` (an exact full SHA +or a `vX.Y.Z` tag) and `mode` (`dry-run`, `sync` or `release`). + +```bash +# Validate only; nothing external changes. +gh workflow run sync-and-release.yml --repo PyDevices/lvgl-python \ + -f lvgl_bindings_ref=<40-character-bindings-sha> -f mode=dry-run + +# Commit and push the exact sync, without a release. +gh workflow run sync-and-release.yml --repo PyDevices/lvgl-python \ + -f lvgl_bindings_ref=<40-character-bindings-sha> -f mode=sync + +# Commit, create a GitHub Release and publish the packages. +gh workflow run sync-and-release.yml --repo PyDevices/lvgl-python \ + -f lvgl_bindings_ref=<40-character-bindings-sha> -f mode=release +``` + +The **Release bindings** workflow in lvgl-bindings is the coordinated entry +point. It validates the exact source first and dispatches `mode=release` only +when its `publish` input is turned on. + +### Package matrix + +The release workflow uses the PyDevices reusable native and WebAssembly package +builder, and `pyproject.toml` keeps the platform policy: + +- CPython 3.11 to 3.15 and free-threaded 3.15t, Linux x86_64 manylinux wheels. +- CPython 3.11 to 3.15 and free-threaded 3.15t, Windows AMD64 wheels. +- CPython 3.13 and 3.14, Android arm64-v8a and x86_64 wheels. +- Pyodide and WebAssembly wheels from the reusable release workflow. + +Native wheel smoke tests import `lvgl`, call `init()` and call `deinit()`. +Android wheels are cross-built and checked on a device by the PyDevices Android +application workflow. + +### Check a wheel locally + +```bash +python -m pip install build +python -m build --wheel +python -m pip install --force-reinstall dist/pydevices_lvgl-*.whl +python -m unittest discover -s tests -v +``` + +The wheel must contain `lvgl.pyi` beside the native extension; the tests verify +the install. To reproduce the release platforms, use cibuildwheel: Linux +manylinux builds need Docker, Windows builds need native MSVC, and Android +builds need the Android SDK and NDK that cibuildwheel uses. + +### Where publication stops + +Only a published GitHub Release, or an explicit exact-tag retry, runs +`publish-release-packages.yml`. Implementation and dry-run validation create no +GitHub Release, so they cannot upload to TestPyPI. diff --git a/README.md b/README.md index 3f3b051..375e539 100644 --- a/README.md +++ b/README.md @@ -1,161 +1,43 @@ -# lvgl-python - -Native CPython extension for [LVGL](https://lvgl.io/), generated from [`lvgl-bindings`](https://github.com/PyDevices/lvgl-bindings) with **no MicroPython interpreter**. - -This is the publishing endpoint in the LVGL family: it turns synced bindings into versioned `pydevices-lvgl` wheels on TestPyPI. See [lvgl-bindings — The LVGL family](https://github.com/PyDevices/lvgl-bindings#the-lvgl-family) for how the family fits together. - -**Synced from lvgl-bindings:** the generated bindings and the `fs_driver.py` helper here are synced from [lvgl-bindings](https://github.com/PyDevices/lvgl-bindings) at the commit pinned in `LVGL_BINDINGS_COMMIT`. Do not edit them here — change them in lvgl-bindings and re-sync. - -> **Pip name:** `pydevices-lvgl` · **Import:** `import lvgl as lv` - -```python -import lvgl as lv -# import display_driver # optional; comes with pydevices, needs a board_config -``` - -## Install - -Prebuilt wheels are published as **`pydevices-lvgl`** on [TestPyPI](https://test.pypi.org/project/pydevices-lvgl/) (import as `lvgl`). CI builds a separate wheel for each CPython minor (3.11–3.15, plus the free-threaded 3.15t) on Linux x86_64 and Windows x64, **Android** wheels for **3.13–3.14** (`android_21_arm64_v8a`, `android_21_x86_64` per [PEP 738](https://peps.python.org/pep-0738/); cibuildwheel has no `armeabi_v7a` yet), plus a **Pyodide** `pyemscripten_2026_0_wasm32` wheel (`cp314`) — pip/micropip select the tag that matches your interpreter. - -**Android (python-for-android / PyDevices):** install the matching wheel from TestPyPI when building an APK, or let the `pydeviceslvgl` p4a recipe fetch it (see [android-template](https://github.com/PyDevices/android-template)): - -```bash -pip install -i https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ \ - --only-binary=:all: --platform android_21_arm64_v8a pydevices-lvgl -``` +![A blue button reading Hello from LVGL on a light gray background](docs/images/hello.png) -```bash -pip install -i https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ pydevices-lvgl -``` - -**Upgrading from a wheel that still had `display_driver.py`:** upgrade -`pydevices` and `pydevices-lvgl` in one `pip install -U pydevices pydevices-lvgl`. -Upgraded on its own after a `pydevices` that already carries the file, pip's -uninstall of the old `pydevices-lvgl` deletes the `display_driver.py` both -wheels wrote; `pip install --force-reinstall --no-deps pydevices` puts it back. - -**Pyodide / micropip** (browser WASM; same project): - -```python -import micropip -await micropip.install("pydevices-lvgl", index_urls="https://test.pypi.org/simple/") -``` - -Pin a release: - -```bash -pip install -i https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ pydevices-lvgl==9.5.25 -``` - -`--extra-index-url https://pypi.org/simple/` lets pip fetch dependencies (e.g. setuptools) from PyPI. Use the same `pip` / `python` you will run (e.g. `pip.exe` with Windows 3.14). - -Quick check: - -```bash -python -c "import lvgl as lv; lv.init(); lv.deinit(); print('ok')" -``` +# lvgl-python -**WSL + Windows Python** — `pip.exe` / `python.exe` on PATH: +lvgl-python is the part of OmniPython that puts LVGL in CPython: the native `lvgl` module, published as `pydevices-lvgl`, with the same API as the MicroPython-compatible and CircuitPython-compatible builds. ```bash -pip.exe install -i https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ pydevices-lvgl==9.5.25 -python.exe -c "import lvgl as lv; lv.init(); lv.deinit(); print('ok')" +python -m pip install -i https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ pydevices-lvgl ``` -To build from source instead, see **[building.md](docs/building.md)**. - -## Usage - -### 1. PyDevices Standard Quickstart (Recommended) - -When using `pydevices` board configs, pydevices' `display_driver` sets up the display, input devices, and background timer automatically. It ships in the `pydevices` wheel, not this one (it was here until 2026-10): - -```python -import display_driver # noqa: F401 - initializes display, input, and timer -import lvgl as lv -from display_driver import app - -scr = lv.screen_active() -label = lv.label(scr) -label.set_text("Hello from PyDevices LVGL!") -label.center() - -# Standalone scripts: keep the process alive -# Interactive REPL (python -i): app.run() returns immediately so the prompt is usable! -app.run() -``` - -### 2. Standalone Raw LVGL Setup (Custom backends) - -For custom setups without a `pydevices` board config: - ```python import lvgl as lv lv.init() - -disp = lv.display_create(240, 240) +disp = lv.display_create(240, 120) disp.set_color_format(lv.COLOR_FORMAT.RGB565) -buf = lv.draw_buf_create(240, 240, lv.COLOR_FORMAT.RGB565, 0) -disp.set_draw_buffers(buf, None) -disp.set_render_mode(lv.DISPLAY_RENDER_MODE.PARTIAL) +buf = bytearray(240 * 120 * 2) +disp.set_buffers(buf, None, len(buf), lv.DISPLAY_RENDER_MODE.FULL) +flushed = [] +disp.set_flush_cb(lambda d, area, px: (flushed.append(area.get_width()), d.flush_ready())) -scr = lv.screen_active() -label = lv.label(scr) -label.set_text("Hello") -print(label.get_text()) +button = lv.button(lv.screen_active()) +button.center() +label = lv.label(button) +label.set_text("Hello from LVGL") +button.add_event_cb(lambda e: print("clicked"), lv.EVENT.CLICKED, None) +button.send_event(lv.EVENT.CLICKED, None) -def on_clicked(event): - print("clicked", event.get_code()) - -scr.add_event_cb(on_clicked, lv.EVENT.CLICKED, None) -scr.send_event(lv.EVENT.CLICKED, None) - -lv.deinit() +lv.tick_inc(50) +lv.refr_now(disp) +print(label.get_text(), "- drew", flushed[-1], "px wide") ``` +```text +clicked +Hello from LVGL - drew 240 px wide +``` -## Type checking - -Wheels install `lvgl.pyi` next to the `lvgl` extension module. For editable -builds, `pip install -e .` copies `generated/lvgl.pyi` at build time. - -If your checker does not pick it up, set `python.analysis.stubPath` (Pylance) or -`stubPath` (pyright) to a directory containing `lvgl.pyi`, or symlink -`typings/lvgl/__init__.pyi` to this repo’s vendored `generated/lvgl.pyi`. - -### API style - -The binding follows the same naming as the MicroPython port where possible: - -| Pattern | Example | -|---------|---------| -| Symbol namespace | `lv.SYMBOL.OK`, `lv.SYMBOL.CANCEL` | -| Enum namespaces | `lv.COLOR_FORMAT.RGB565`, `lv.DISPLAY_RENDER_MODE.PARTIAL` | -| Widget types (callable) | `lv.obj()`, `lv.label(parent)`, `lv.button(parent)` | -| Object methods | `label.set_text("…")`, `scr.add_event_cb(cb, lv.EVENT.CLICKED, None)` | -| Module functions | `lv.display_create(240, 240)`, `lv.screen_active()` | -| Struct methods | `event.get_code()` on `lv_event_t` wrappers | - -Struct-backed APIs, including displays, use methods consistently on all three runtimes (`disp.set_flush_cb(...)`, `disp.set_render_mode(...)`). - -### Limitations - -- **Unsafe C signatures are intentionally absent.** Variadic and unbounded pointer-to-pointer APIs are omitted unless the generator has a reviewed safe conversion. -- **Target exceptions are explicit.** The shared API manifest records the small set of LVGL features unavailable on a particular runtime. -- **A callback lives as long as its registration.** `add_event_cb(handler, ...)` holds your handler (and a `user_data` dict, if you pass one) until the widget is deleted or the event is removed; then the binding lets them go. Dropping your Python reference to the widget does not stop the callback while the widget exists, and a handler may delete its own widget. Callbacks set on an `lv.anim_t` live while the Python struct or a running copy of it exists; a timer's callback lives while the timer does. -- **One thread drives LVGL.** Make every `lvgl` call on one thread, and let go of every `lvgl` object there. Other threads can run any Python that doesn't touch `lvgl`; on a free-threaded build they run in parallel with drawing. See [docs/threads.md](docs/threads.md). - -## Links - -- [Source](https://github.com/PyDevices/lvgl-python) -- [Newcomer's guide](docs/newcomers.md) — repository architecture and contribution boundaries -- [Issues](https://github.com/PyDevices/lvgl-python/issues) -- [building.md](docs/building.md) — build from source -- Related: [lvgl-bindings](https://github.com/PyDevices/lvgl-bindings), [pydevices-examples](https://github.com/PyDevices/pydevices-examples) - -What's planned next is in [ROADMAP.md](ROADMAP.md). +The picture at the top is the frame that program drew. -## License +[Docs](https://github.com/PyDevices/lvgl-bindings/blob/main/docs/get-started/install.md#cpython-with-lvgl-python) · [Examples](https://pydevices.github.io/pydevices-examples/pyscript/) · [Roadmap](ROADMAP.md) -MIT — see [LICENSE](LICENSE). +OmniPython is made by [PyDevices](https://pydevices.com). diff --git a/docs/README.md b/docs/README.md deleted file mode 100644 index a5d298c..0000000 --- a/docs/README.md +++ /dev/null @@ -1,14 +0,0 @@ -# lvgl-python documentation - -- [newcomers.md](newcomers.md) — architecture, code ownership, and safe first changes. - -- [building.md](building.md) — local build/dev workflow, syncing generated - bindings from lvgl-bindings. -- [publishing.md](publishing.md) — how a bindings change becomes a versioned - `pydevices-lvgl` wheel on TestPyPI, and how to install it. Secrets/credential - ownership lives in the org-wide runbook, - [dotgithub/docs/publishing-automation.md](https://github.com/PyDevices/dotgithub/blob/main/docs/publishing-automation.md) — - linked from here rather than duplicated. - -See the [root README](../README.md) for how this repo fits into the -[LVGL family](https://github.com/PyDevices/lvgl-bindings#the-lvgl-family). diff --git a/docs/building.md b/docs/building.md deleted file mode 100644 index 85e90e6..0000000 --- a/docs/building.md +++ /dev/null @@ -1,281 +0,0 @@ -# Building from source - -Build and develop the native CPython extension locally. Day-to-day use should -prefer the TestPyPI wheel — see **[README.md](../README.md#install)**. - -This repo is self-contained for a normal build: you only need **this clone** -(plus the `lvgl` submodule). No MicroPython, CircuitPython, or workspace -orchestrator is required. - -## What is already vendored - -These files are **committed in this repo** (they track the latest published -binding sync from [lvgl-bindings](https://github.com/PyDevices/lvgl-bindings)): - -| Path | Role | -|------|------| -| `generated/lvgl_python.c` | Generated CPython binding | -| `generated/lvgl.pyi` | Type stubs (copied into the install) | -| `LVGL_BINDINGS_COMMIT` | Exact lvgl-bindings source commit for all vendored artifacts | -| `lv_conf.h` | LVGL config used for the build | -| `fs_driver.py` | Optional helper (`import fs_driver`) | -| `lvgl/` | LVGL C sources (git submodule) | -| `src/jpegio/` | The JPEG decoder (jpegio's, the one MicroPython and CircuitPython use) | -| `JPEGIO_COMMIT` | Exact micropython-pydevices source commit for `src/jpegio/` | - -A normal `pip install -e .` compiles those vendored sources. You do **not** need -a local `lvgl-bindings` tree to build or test this package. - -## When to clone `lvgl-bindings` (optional) - -Clone [lvgl-bindings](https://github.com/PyDevices/lvgl-bindings) as a **sibling** -only when you are changing the **generator** or regenerating bindings: - -```text -workspace/ - lvgl-bindings/ # optional — generator + canonical smoke suite - lvgl-python/ # this repo -``` - -Do that when you need to: - -- Edit the target-neutral model or CPython emitter in `lvgl-bindings/binding/` -- Run `./regenerate_all.sh --target cpython`, then sync the output into this repo -- Run the shared cross-interpreter smoke script at `lvgl-bindings/tools/test_lvgl_smoke.py` - -To refresh vendored files from GitHub **without** a sibling clone, use: - -```bash -./scripts/sync_from_lvgl_bindings.sh --ref <40-character-commit-sha> -./scripts/sync_from_lvgl_bindings.sh --ref v9.5.N -``` - -That script clones lvgl-bindings into a temp directory, copies the generated -files, and updates the `lvgl` submodule pin. The JPEG decoder comes the same -way from [micropython-pydevices](https://github.com/PyDevices/micropython-pydevices)' -`modules/jpegio`, so a JPEG draws the same on every interpreter, scaled and -contained included: - -```bash -./scripts/sync_from_jpegio.sh --ref <40-character-commit-sha> -``` Release flow: **[publishing.md](publishing.md)**. - -## Requirements - -### All platforms - -- Python 3.11+ with `pip` and `setuptools` -- Vendored files above (already in the clone) -- `git submodule update --init lvgl` - -### WSL / Linux / macOS - -- GCC or Clang -- `python3-dev` (or equivalent) matching your Python version - -On Debian/Ubuntu: - -```bash -sudo apt install python3-dev build-essential -``` - -### Windows (native or via WSL + `pip.exe`) - -- [python.org](https://www.python.org/) CPython (or another MSVC-built Python 3.11+) -- **Microsoft C++ Build Tools** with the **Desktop development with C++** workload - ([Visual Studio Build Tools](https://visualstudio.microsoft.com/visual-cpp-build-tools/)) -- MinGW is **not** supported for python.org Windows Python; use MSVC. - -`setup.py` selects MSVC warning flags on Windows and uses a linker response file -(LVGL compiles many `.c` files; the raw `link.exe` command line exceeds Windows -limits). - -## Repository layout - -```text -lvgl-python/ -├── LVGL_BINDINGS_COMMIT # exact immutable source -├── JPEGIO_COMMIT # exact source of src/jpegio/ -├── generated/lvgl_python.c # vendored binding (synced from lvgl-bindings) -├── generated/lvgl.pyi -├── lv_conf.h -├── fs_driver.py -├── lvgl/ # LVGL git submodule -├── src/lvpy_runtime.c -├── src/lvpy_runtime.h -├── src/lvpy_jpegio.c # registers the JPEG decoder at lv.init() -├── src/jpegio/ # jpegio's LVGL decoder + TJpgDec (synced) -├── tests/ # unit tests -├── scripts/ # sync / publish / pyodide wheel -└── setup.py -``` - -Clone with submodules: - -```bash -git clone --recurse-submodules https://github.com/PyDevices/lvgl-python.git -# or after a plain clone: -git submodule update --init lvgl -``` - -## Build and install - -`pip install` compiles `src/lvpy_runtime.c`, `generated/lvgl_python.c`, and LVGL -sources under `lvgl/src`. - -Use **editable** install (`-e`) while developing so the `.so` / `.pyd` beside -this directory stays in sync with rebuilds. - -### WSL (Linux Python) - -```bash -git clone --recurse-submodules https://github.com/PyDevices/lvgl-python.git -cd lvgl-python -python3 -m venv .venv -.venv/bin/pip install -r requirements-dev.txt -.venv/bin/pip install -e . -``` - -Quick import check: - -```bash -.venv/bin/python -c "import lvgl as lv; lv.init(); lv.deinit(); print('ok')" -``` - -Unit tests: - -```bash -.venv/bin/python -m unittest discover -s tests -``` - -### Windows Python from WSL (no copy to `C:\`) - -Keep the repo on the WSL filesystem and install into **Windows** Python with -`pip.exe`: - -```bash -cd /path/to/lvgl-python -pip.exe install -e "$(wslpath -w "$PWD")" -python.exe -c "import lvgl as lv; lv.init(); lv.deinit(); print('ok')" -``` - -The first build compiles every LVGL source and may take several minutes over -`\\wsl.localhost\...`. - -### Windows (native shell) - -```powershell -cd C:\path\to\lvgl-python -git submodule update --init lvgl -py -m pip install -e . -py -c "import lvgl as lv; lv.init(); lv.deinit(); print('ok')" -``` - -Open a **new** terminal after installing Build Tools so `cl.exe` is on `PATH`. - -### Changing API coverage (needs lvgl-bindings) - -Change the canonical API policy or generator in a sibling lvgl-bindings clone, -regenerate all affected targets, commit that source, sync its exact SHA, then reinstall: - -```bash -cd ../lvgl-bindings && ./regenerate_all.sh --target cpython -cd ../lvgl-python -./scripts/sync_from_lvgl_bindings.sh --ref <40-character-commit-sha> -.venv/bin/pip install -e . # or pip.exe on Windows -``` - -## Development - -After the first full build, incremental rebuilds are much faster. With -`setuptools` and `wheel` in the target venv: - -```bash -.venv/bin/python setup.py build_ext --inplace -``` - -Editable install does **not** recompile on import; rerun `pip install -e .` (or -`build_ext --inplace`) after C source changes (`src/lvpy_runtime.c` or -`generated/lvgl_python.c`). - -- Generator work: [`lvgl-bindings`](https://github.com/PyDevices/lvgl-bindings) -- CPython runtime / packaging: this repo (`src/lvpy_runtime.c`, `setup.py`) - -## Android (python-for-android) - -Prefer a **prebuilt** `pydevices-lvgl` wheel tagged `android_21_*` for your ABI -(`arm64_v8a` or `x86_64`) from TestPyPI. The -[android-template `pydeviceslvgl` p4a recipe](https://github.com/PyDevices/android-template/tree/main/p4a_recipes/pydeviceslvgl) -installs a matching wheel when `--extra-index-url` points at TestPyPI, otherwise -cross-compiles from this tree (`git submodule update --init lvgl`). - -```bash -export P4A_pydeviceslvgl_DIR=/path/to/lvgl-python # optional in-tree fallback -``` - -## Pyodide / WebAssembly - -Native Linux/macOS/Windows wheels from TestPyPI are **not** loadable in Pyodide. -Each **Publish release packages** run also builds a `pyemscripten_2026_0` wasm32 -wheel. Preferred install in the browser: - -```python -import micropip -await micropip.install("pydevices-lvgl", index_urls="https://test.pypi.org/simple/") -``` - -### Local rebuild (optional) - -- Network on first run (downloads the Pyodide xbuildenv + emsdk) - -cibuildwheel drives pyodide-build and the emscripten toolchain, so there is no -separate script and no host-Python requirement to match: - -```bash -pipx run cibuildwheel --platform pyodide -``` - -Wheels land in `wheelhouse/`. This is the same command CI runs, and it builds -every Pyodide target the `build` selector in `pyproject.toml` allows. - -## Architecture - -| Component | Role | -|-----------|------| -| `generated/lvgl_python.c` (vendored) | Types, methods, module functions, callbacks | -| `src/lvpy_runtime.c` / `src/lvpy_runtime.h` | CPython glue: wrappers, convertors, GIL | -| `setup.py` | Builds the `lvgl` extension module | -| `lvgl-bindings` (optional sibling) | Generator only — not required to compile | - -**Object wrappers** (`py_lv_obj_t`) map `lv_obj_t *` to Python and keep -per-object callback dicts. **Struct wrappers** (`py_lv_struct_t`) expose LVGL -structs. - -**Event callbacks** (phase 7): pass a Python callable to `add_event_cb`. With -`user_data=None`, the wrapper stores callbacks on the target object. - -## Emission phases - -| Phase | Coverage | -|-------|----------| -| 1 | `lv.init()` / `lv.deinit()`, integer constants, `LV_SYMBOL_*` strings | -| 2 | Enum namespaces (`lv.EVENT.CLICKED`, `lv.COLOR_FORMAT.*`, …) | -| 3 | Struct types, field get/set | -| 4 | Struct methods (`event.get_code()`, …) | -| 5 | Widget / `lv_obj` types, constructors, methods | -| 6 | Module-level functions (`display_create`, `screen_active`, …) | -| 7 | Python callbacks (`add_event_cb`, flush/timer hooks, …) | - -Phases 1–7 are enabled in the generator today. - -## Known limitations - -- **Vendored build inputs**: you can compile from this repo alone; regenerate in - lvgl-bindings only when changing the generator, then sync. -- **Windows toolchain**: python.org CPython on Windows requires MSVC Build Tools; - MinGW cannot build this extension for that interpreter. - -## Related projects - -- [PyDevices/lvgl-bindings](https://github.com/PyDevices/lvgl-bindings) — binding generator -- [PyDevices/pydevices-examples](https://github.com/PyDevices/pydevices-examples) — consumer of `import lvgl` diff --git a/docs/images/hello.png b/docs/images/hello.png new file mode 100644 index 0000000..c9fbd4d Binary files /dev/null and b/docs/images/hello.png differ diff --git a/docs/newcomers.md b/docs/newcomers.md deleted file mode 100644 index ef527da..0000000 --- a/docs/newcomers.md +++ /dev/null @@ -1,181 +0,0 @@ -# Newcomer's guide to the lvgl-python codebase - -`lvgl-python` packages LVGL for CPython as the native `lvgl` extension -distributed under the `pydevices-lvgl` project name. It is one endpoint of a -shared binding family: it exposes the same intentional Python-shaped API as -the MicroPython and CircuitPython targets, but it does **not** embed or run a -MicroPython interpreter. - -The most important boundary is ownership. This repository owns the CPython -runtime layer, packaging, tests, and release policy. The sibling -`lvgl-bindings` repository owns the generator and canonical API decisions. -Generated binding files and the `fs_driver.py` helper are -vendored here at one exact upstream commit; they are inputs to this project, -not ordinary hand-edited source files. - -## A mental model - -```text -lvgl-bindings (generator and API policy) - | - | explicit sync at a recorded 40-character commit - v -lvgl-python repository - |-- generated/lvgl_python.c and lvgl.pyi generated binding surface - |-- src/lvpy_runtime.c CPython ownership, callbacks, locking - |-- lvgl/ pinned upstream C submodule - `-- setup.py compiles one CPython extension - | - v -import lvgl as lv - |-- generated wrappers convert Python arguments and results - |-- runtime wrappers manage objects, structs, GIL, and callback lifetime - `-- LVGL C core owns widgets, displays, rendering, and event dispatch -``` - -Most users install a prebuilt `pydevices-lvgl` wheel from TestPyPI, then use -`import lvgl as lv`; they do not clone this repository, initialize its -submodule, or compile LVGL. The build tree exists for binding/runtime -contributors and release automation, not as the normal application setup. - -```bash -pip install -i https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ pydevices-lvgl -``` - -An application may provide its own display backend. In the PyDevices -convenience path it imports `display_driver`, pydevices' adapter around -`appdev.App` and `board_config`. It ships in the `pydevices` wheel, outside this -one, and is not required for a standalone backend. - -## Repository map - -| Path | Purpose | -|---|---| -| `generated/lvgl_python.c` | Vendored generated Python-to-LVGL wrappers, types, constants, and callback entry points. | -| `generated/lvgl.pyi` | Type stubs installed beside the extension wheel. | -| `src/lvpy_runtime.c`, `src/lvpy_runtime.h` | Handwritten CPython-specific implementation: object/struct wrappers, conversion helpers, locking, GIL handling, and callback ownership. | -| `lvgl/` | Pinned upstream LVGL C git submodule compiled into the extension. | -| `lv_conf.h` | LVGL build configuration for the CPython target. | -| `setup.py` | Defines the `lvgl` extension and compiles runtime, generated bindings, and all LVGL C sources. | -| `fs_driver.py` | Synced LVGL filesystem helper; change its canonical copy in `lvgl-bindings`. | -| `LVGL_BINDINGS_COMMIT` | Exact 40-character source commit used for the vendored binding inputs. | -| `tests/` | Native-extension lifecycle and wheel-installation contracts. | -| `docs/building.md` | Authoritative local build, sync, Android, and Pyodide instructions. | -| `docs/publishing.md` | Release safety model and platform-wheel policy. | -| `scripts/sync_from_lvgl_bindings.sh` | Explicit upstream sync tool; it records the resolved source and updates the submodule pin. | - -## Follow a normal PyDevices application - -The usual PyDevices entry point imports `display_driver` before `lvgl`. That -adapter selects the active board configuration and wires display flush, input, -and LVGL event-loop coordination; the application then creates widgets. - -```python -import display_driver # wires LVGL display, input, and event loop -import lvgl as lv - -scr = lv.screen_active() -label = lv.label(scr) -label.set_text("Hello from PyDevices LVGL!") -label.center() -``` - -1. Installing the supported wheel provides the binary module named `lvgl`; - installing `pydevices` provides the `display_driver` helper. -2. Importing `display_driver` first establishes the PyDevices display/input - integration. It owns LVGL ticking and refresh coordination for this path. -3. Importing `lvgl` exposes the generated module surface and runtime support - types: LVGL-style enum namespaces, functions, object constructors, and - struct methods rather than a separate Python UI model. -4. Widget constructors and methods enter generated wrappers. They validate and - convert Python values, take the runtime's LVGL lock where needed, and call - the corresponding LVGL C API. - -For a custom backend without PyDevices board configuration, use the raw LVGL -setup in the root README instead of adapting this convenience-path example. - -The public API is generated, but the lifecycle, ownership, and concurrency -behavior in that path are CPython runtime work. Those concerns belong in -`src/lvpy_runtime.*`, not in an ad hoc edit to the generated file. - -## Boundaries and invariants - -- Do not manually edit `generated/lvgl_python.c`, `generated/lvgl.pyi`, - `fs_driver.py`, or `lv_conf.h`. Change the generator or - canonical helper in `lvgl-bindings`, regenerate there, then use the explicit - sync workflow here. -- `LVGL_BINDINGS_COMMIT` is a reproducible-source record, not a loose version - hint. A sync accepts an immutable full commit SHA or release tag and records - the resolved 40-character commit. -- A callback registered with `add_event_cb` is owned by its registration, not - by the widget's Python wrapper: `mp_lv_callback` stores it in a dict that - LVGL's event descriptor points at, and the runtime drops that dict's one - reference when the event is removed or the object deleted. The drop is - deferred until no LVGL call or Python callback is in flight, because LVGL - sends `LV_EVENT_DELETE` to the runtime's hook before the user's. Dropping the - wrapper neither stops nor leaks the callback; `tests/test_callback_lifetime.py` - pins each case. -- A struct's callbacks (`lv_anim_t`, `lv_timer_t`) share one dict reference, - owned by the struct: the generated setters pass the struct as - `containing_struct`. A Python-owned struct releases it in its dealloc; a - timer and each running copy of an animation release theirs through LVGL's - `ext_data` free_cb (`lvpy_timer_bind`, `lvpy_anim_bind`), which needs - `LV_USE_EXT_DATA`, on for the CPython build only. - `tests/test_struct_callback_lifetime.py` pins each case. -- Pair `lv.init()` and `lv.deinit()` in standalone programs. Tests cover this - lifecycle, including callback cleanup during interpreter shutdown. -- The binding intentionally omits unsafe C signatures such as unbounded - pointer-to-pointer or variadic APIs unless conversion is reviewed. Do not - restore one merely to make the C surface look complete. -- The project shares naming and selected API policy with other runtimes, but - it is a CPython extension. A feature's availability may be explicitly - different on another target. -- The extension does not choose a display, event loop, or board. Treat the - PyDevices `display_driver` path as one integration, not as a requirement of - `import lvgl`. - -## When you need the source tree - -Only work on a source checkout when you are changing the generated binding, -the CPython runtime, packaging, or the release process. In that case, -[building.md](building.md) is the authoritative platform-specific build and -test guide; it covers the LVGL submodule and editable native build. It is not -needed to use a published wheel. - -The focused test suite verifies the recorded bindings source, the installed -`lvgl.pyi` stub, initialization/deinitialization, widget creation, and -teardown callbacks at interpreter exit. Read [publishing.md](publishing.md) -before changing sync, version, wheel-matrix, or TestPyPI release behavior: -normal pushes do not publish a wheel. - -## Safe first contributions - -Good first changes are a focused regression test for a CPython lifecycle or -conversion bug, a clarification in the build documentation, or a fix to the -handwritten runtime layer when the generated API already expresses the desired -surface. Confirm which repository owns the change before editing: - -| If the change concerns… | Start in… | -|---|---| -| API availability, generated wrappers, or synced Python helpers | `lvgl-bindings` | -| CPython object ownership, GIL/lock behavior, wheel packaging, or CPython tests | `lvgl-python` | -| A PyDevices application's screens, board configuration, or user interaction | that consuming application/repository | - -After changing any native code, rebuild the editable extension and run the -focused tests. If a proposed change moves generated files or the `lvgl` -submodule, use the documented sync path rather than staging a hand-picked -mixture of outputs. - -## Where to learn next - -Read `src/lvpy_runtime.h` first, then follow one call from `import lvgl` into -`generated/lvgl_python.c` and back through `src/lvpy_runtime.c`. -`tests/test_lvgl_init.py` is short and shows the lifecycle contract the runtime -has to keep. - -For the user-facing surface, both setup paths are in the -[README](../README.md#usage). For how the binding is generated and which API -decisions it makes, go to -[lvgl-bindings](https://github.com/PyDevices/lvgl-bindings). Building from -source is in [building.md](building.md), and releases are in -[publishing.md](publishing.md). diff --git a/docs/publishing.md b/docs/publishing.md deleted file mode 100644 index 6de41fe..0000000 --- a/docs/publishing.md +++ /dev/null @@ -1,113 +0,0 @@ -# Publishing and releases - -`lvgl-python` is the packaged CPython endpoint for the shared API generated by -[`lvgl-bindings`](https://github.com/PyDevices/lvgl-bindings). Release inputs -are immutable: every sync accepts only a full 40-character bindings commit SHA -or a `vX.Y.Z` bindings tag and records the resolved commit in -`LVGL_BINDINGS_COMMIT`. - -## Safety model - -Generation, validation, synchronization, and publication are separate actions. - -- `lvgl-bindings/regenerate_all.sh` writes artifacts only. -- `lvgl-bindings/scripts/release_dry_run.sh` generates in temporary storage, - verifies all artifacts, runs tests, and computes the expected version without - committing, tagging, pushing, dispatching, or publishing. -- This repo's **Sync and release** workflow defaults to `dry-run`. -- `sync` mode is the explicit request to commit and push synchronized sources. -- `release` mode is the explicit request to commit, create a GitHub Release, - and start package publication. -- A normal push to either repository never starts a release. - -## Version scheme - -Versions are `LVGL_MAJOR.LVGL_MINOR.N`. For the current API line this means -`9.5.N`; `N` is one greater than the highest existing `v9.5.*` tag in this -repository. Preview it without mutation: - -```bash -./scripts/next_release_version.sh --verbose -``` - -## Local dry run - -Sync an exact bindings source, rebuild the extension, run its focused tests and -the matching canonical smoke suite, then inspect the next version: - -```bash -./scripts/sync_from_lvgl_bindings.sh --ref <40-character-bindings-sha> -python -m pip install -e . -python -m unittest discover -s tests -v -python ../lvgl-bindings/tools/test_lvgl_smoke.py -./scripts/next_release_version.sh --verbose -``` - -The sync command changes only the local checkout. It vendors -`generated/lvgl_python.c`, `generated/lvgl.pyi`, `lv_conf.h`, the Python -helpers, the LVGL submodule pin, and `LVGL_BINDINGS_COMMIT`. - -## GitHub workflow - -Run **Sync and release** with two required inputs: - -- `lvgl_bindings_ref`: exact full SHA or `vX.Y.Z` tag. -- `mode`: `dry-run`, `sync`, or `release`. - -Examples: - -```bash -# Validate only; no external mutation. -gh workflow run sync-and-release.yml --repo PyDevices/lvgl-python \ - -f lvgl_bindings_ref=<40-character-bindings-sha> -f mode=dry-run - -# Commit and push the exact sync, but do not create a release. -gh workflow run sync-and-release.yml --repo PyDevices/lvgl-python \ - -f lvgl_bindings_ref=<40-character-bindings-sha> -f mode=sync - -# Explicitly commit, create a GitHub Release, and publish packages. -gh workflow run sync-and-release.yml --repo PyDevices/lvgl-python \ - -f lvgl_bindings_ref=<40-character-bindings-sha> -f mode=release -``` - -The explicit **Release bindings** workflow in `lvgl-bindings` provides the -coordinated upstream entry point. It validates the exact source first and only -dispatches `mode=release` when its `publish` input is deliberately enabled. - -## Package matrix - -The published-release workflow uses the PyDevices reusable native/WASM package -builder. The repository's `pyproject.toml` keeps the platform policy: - -- CPython 3.11–3.15 and free-threaded 3.15t Linux x86_64 manylinux wheels. -- CPython 3.11–3.15 and free-threaded 3.15t Windows AMD64 wheels. -- CPython 3.13–3.14 Android arm64-v8a and x86_64 wheels. -- Pyodide/WebAssembly wheels supplied by the reusable release workflow. - -Native wheel smoke tests import `lvgl`, call `init()`, and call `deinit()`. -Android wheels are cross-built and validated on device by the PyDevices Android -application workflow. - -## Local wheel validation - -For a development wheel on the current host: - -```bash -python -m pip install build -python -m build --wheel -python -m pip install --force-reinstall dist/pydevices_lvgl-*.whl -python -m unittest discover -s tests -v -``` - -The wheel must contain `lvgl.pyi` beside the native extension. The focused test -suite verifies that installation contract. - -To reproduce release-platform wheels, use cibuildwheel. Linux manylinux builds -require Docker; Windows builds require native MSVC; Android builds require the -Android SDK/NDK environment used by cibuildwheel. - -## Publication boundary - -Only a published GitHub Release or an explicit exact-tag retry invokes -`publish-release-packages.yml`. Implementation and dry-run validation must not -create a GitHub Release and therefore cannot upload to TestPyPI. diff --git a/docs/threads.md b/docs/threads.md deleted file mode 100644 index bd35fb6..0000000 --- a/docs/threads.md +++ /dev/null @@ -1,39 +0,0 @@ -# Threads - -Drive LVGL from one thread. Every `lvgl` call happens on that thread: -creating widgets, setting their properties, `lv.timer_handler()`, -`lv.refr_now()`. Your callbacks run there too, because LVGL calls them from -inside those calls. - -Every `lvgl` object is also let go of on that thread: a widget, a struct -such as an `lv.anim_t`, a callback you handed to LVGL. In practice that -means keeping them in variables the LVGL thread owns, and never passing -them to another thread to drop. - -Other threads can run any Python that doesn't touch `lvgl`: audio, -networking, file I/O, number crunching. Send their results to the LVGL -thread through a `queue.Queue` and apply them there. - -## On a free-threaded Python - -On a free-threaded build (3.15t wheels are published), `lvgl` declares that -it doesn't need the GIL, so importing it leaves the GIL off and your other -threads run in parallel with drawing. - -The one-thread rule is what makes that safe. LVGL isn't thread-safe, and the -binding keeps shared state of its own with no lock around it: scratch -buffers for converting structs, the table of callbacks it holds for you, and -the references it lets go of once a call returns. Two threads calling -`lvgl` at once can corrupt that state on any build; without the GIL it -happens sooner. - -One thing to watch: if `lvgl` objects sit in a reference cycle, the cycle -collector frees them on whichever thread it happens to run. In a threaded -program, delete widgets yourself on the LVGL thread (`widget.delete()`) -rather than leaving them to the collector. - -## Checking it - -`tests/test_free_threading.py` runs on free-threaded builds. It checks that -importing `lvgl` leaves the GIL off, and that frames drawn on one thread -while other threads run Python are byte for byte the frames drawn alone. diff --git a/pyproject.toml b/pyproject.toml index ccd8e63..6edce95 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,8 +18,8 @@ packages = [] py-modules = ["fs_driver"] # Wheel CI: one wheel per CPython minor (cp311…cp315, plus free-threaded cp315t) × Linux + Windows, -# and cp313–cp314 for Android and Pyodide — see README. -# Local reproduction: docs/publishing.md § "Local wheel builds (cibuildwheel)" (Linux needs Docker). +# and cp313–cp314 for Android and Pyodide; see the install page of the LVGL docs. +# Local reproduction: CONTRIBUTING.md, "Check a wheel locally" (Linux needs Docker). [tool.cibuildwheel] build = "cp311-* cp312-* cp313-* cp314-* cp315-* cp315t-*" # 3.15 and 3.15t on Linux and Windows only, for now.