docs: desktop dev contract, app-builder.json schema, quickstart

DESKTOP_DEV.md freezes the app ABI (app_main + app_event + CZ_EV_*), the
LVGL-shared-via-dlopen mechanism, and the 8-issue Windows plan so future
work can't silently drift.

APP_BUILDER_JSON.md documents the backward-compatible schema extension
(runtime / entry / lvgl_version / caps / assets). QUICKSTART.md is the
3-minute onboarding for mac/Linux.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
LiHaohua
2026-05-06 14:30:34 +08:00
committed by eggfly
co-authored by Claude Opus 4.7
parent 463a523d47
commit 372de634e3
3 changed files with 407 additions and 0 deletions
+87
View File
@@ -0,0 +1,87 @@
# `app-builder.json` schema
Every directory in an application repository that should be discovered, built
and packaged contains one `app-builder.json` at its root. The file plays two
roles:
1. **Packaging** — feeds the CI `.deb` pipeline (existing behaviour).
2. **Desktop dev loop** — tells `czdev` how to build and run the app inside the
emulator (new fields, optional).
Fields added for the desktop loop are all optional; a file that only contains
the packaging fields keeps working exactly as before.
## Full schema
```jsonc
{
// ── Packaging (existing) ─────────────────────────────────────────
"package_name": "hello_cz", // Debian package name (lowercase, dash)
"version": "0.1", // SemVer-ish; goes into control file
"app_name": "Hello CZ", // Display name in APPLaunch
"bin_name": "hello_cz", // Shared-object basename (no lib prefix)
"description": "Desktop dev hello app",
// ── Desktop dev (new, all optional) ──────────────────────────────
"runtime": "lvgl-dlopen", // "lvgl-dlopen" (default) | "legacy-deb-only"
"entry": "app_main", // C symbol name, default "app_main"
"event_entry": "app_event", // optional, default "app_event"
"lvgl_version": "9.5", // host must match on major.minor
"caps": [ // capabilities the app declares it uses
"keyboard",
"audio",
"network"
],
"assets": [ // paths relative to app dir; copied next
"assets/fonts/", // to the built library under its app dir
"assets/sprites/"
]
}
```
## Field reference
| Field | Required | Type | Default | Used by |
|---|---|---|---|---|
| `package_name` | yes | string | — | deb, czdev |
| `version` | no | string | `"0.1"` | deb |
| `app_name` | no | string | same as `package_name` | deb, czdev |
| `bin_name` | yes | string | — | deb, czdev |
| `description` | no | string | `""` | deb |
| `runtime` | no | `"lvgl-dlopen"` \| `"legacy-deb-only"` | `"lvgl-dlopen"` | czdev |
| `entry` | no | string | `"app_main"` | czdev, emulator |
| `event_entry` | no | string | `"app_event"` | czdev, emulator |
| `lvgl_version` | no | string | `"9.5"` | czdev (refuses mismatch) |
| `caps` | no | string[] | `[]` | czdev (future: sandboxing) |
| `assets` | no | string[] | `[]` | czdev (stage next to lib) |
## Runtime modes
- **`lvgl-dlopen`** — the app is a shared library that exports the `entry` and
optional `event_entry` symbols described in `cz_app.h`. `czdev run` loads it
into the emulator via `dlopen` / `LoadLibrary`. This is the default for new
apps.
- **`legacy-deb-only`** — the project is a standalone executable (Framebuffer,
SDL, Qt, Python, …). `czdev` skips it during `run`; the CI `.deb` pipeline
still produces a package. Use this for examples in
`CardputerZero-Examples/` that are not LVGL.
Apps that omit `runtime` are treated as `lvgl-dlopen` iff they export
`app_main`; otherwise `czdev` prints a clear error and suggests setting
`"runtime": "legacy-deb-only"`.
## Capabilities (`caps`)
Reserved vocabulary (enforcement comes later):
| Cap | Means |
|---|---|
| `keyboard` | Uses the 44-key physical keyboard / emulator skin |
| `audio` | Plays audio via ALSA on device, SDL_mixer in emulator |
| `network` | Reads hostname / IP / Wi-Fi state |
| `filesystem` | Writes into `/usr/share/APPLaunch` or the emu sandbox |
| `pty` | Spawns a PTY (terminal-style apps) |
| `process` | fork/exec of sub-processes |
Today `caps` is metadata only; a future milestone uses it to decide which
HAL shims the emulator pre-loads.
+178
View File
@@ -0,0 +1,178 @@
# Desktop Development for CardputerZero Apps
This document describes how the **desktop emulator** lets you develop 320×170
LVGL apps for M5 CardputerZero without having the physical device. It is the
contract between:
- App authors writing `.dylib` / `.so` / `.dll` modules
- The emulator (`cardputer-zero-emu`) that loads them
- The real device (APPLaunch on aarch64 Linux) that loads the same sources
compiled natively
The goal: **one source tree → same `.dylib/.so/.dll` on desktop, same `.so`
inside the `.deb` on the device — zero per-platform `#ifdef` in app code.**
---
## 1. How LVGL is shared between emulator and app (all platforms)
The emulator hosts a single LVGL instance. Apps are **loaded into the same
process** via `dlopen` / `LoadLibrary` and call LVGL directly — they do **not**
link their own LVGL.
On macOS and Linux this works because:
- `emulator.exe` links LVGL with `-Wl,-force_load` (mac) / `--whole-archive`
(Linux), so every LVGL symbol lives in the emulator's global symbol table.
- The app `.dylib/.so` is linked with `-undefined dynamic_lookup` (mac) /
`-Wl,--unresolved-symbols=ignore-all` (Linux) — LVGL references are left
unresolved at link time.
- `dlopen(RTLD_GLOBAL)` at runtime binds the app's LVGL references to the
emulator's copy.
Windows needs a different shape (see §4) but the **result is the same**: one
LVGL instance, one display, one input group, one timer handler.
On the real device, APPLaunch does the same thing with `dlopen` against its own
LVGL — the app `.so` behaves identically.
### Rules for app authors
1. **Do not call `lv_init()` or `lv_display_create()`.** The host already did.
2. **Your `lv_conf.h` must byte-match the emulator's.** v9.5, color depth 16,
same font set, same feature flags. Mismatch = segfault. Use the provided
`sdk/include/lv_conf.h` verbatim.
3. **Entry point is `app_main(lv_obj_t* parent)`.** Hang your UI off `parent`.
4. **Clean up in `app_event(CZ_EV_EXIT_REQUEST)`.** Delete widgets you own.
5. **Do not export C++ symbols across the boundary.** Only `extern "C"`
functions. C++ statics inside the library are fine.
---
## 2. App ABI contract (`cz_app.h`)
Every app exports exactly two C symbols:
```c
// sdk/include/cz_app.h
#pragma once
#include <lvgl.h>
#if defined(_WIN32)
#define CZ_APP_EXPORT __declspec(dllexport)
#else
#define CZ_APP_EXPORT __attribute__((visibility("default")))
#endif
#ifdef __cplusplus
extern "C" {
#endif
CZ_APP_EXPORT void app_main(lv_obj_t *parent);
CZ_APP_EXPORT void app_event(int type, void *data);
#ifdef __cplusplus
}
#endif
enum {
CZ_EV_PAUSE = 1, // app is being backgrounded; persist state
CZ_EV_RESUME = 2, // app is foregrounded again
CZ_EV_EXIT_REQUEST = 3, // about to unload; free everything
CZ_EV_SIDE_KEY = 4, // data: (int*) side-button id (ESC/HOME/...)
// Future values are additive. Unknown types must be ignored by the app.
};
```
**Why this shape:**
- `app_main(parent)` — one UI entry point. `parent` is a full-screen container
already sized 320×170. The app never touches display / screen APIs.
- `app_event(int, void*)` — system notifications. Integer type + opaque data
keeps the ABI cheap to extend. Old apps that don't know a new type just
ignore it.
- No string-keyed events, no C++ types, no callbacks with ownership semantics.
- `lvgl_version` is pinned in `app-builder.json` — CI refuses to build if the
app's declared version and the host's version disagree on major/minor.
Apps that only need UI provide `app_main` and leave `app_event` as a one-line
no-op. It is mandatory so the host can always call it.
---
## 3. Why we use LVGL's API directly (no wrapper SDK)
APPLaunch and UserDemo on the real device are written against LVGL v9 directly.
If the desktop emulator introduced its own wrapper, the two surfaces would
drift and every app would need double maintenance. LVGL v9's API is stable
within the 9.x line (we pin 9.5); we version-lock so that 9.6 adoption is an
explicit coordinated bump, not a silent breakage.
We keep the host→app surface (§2) minimal precisely because LVGL already is
the SDK.
---
## 4. Windows LVGL + emulator — known issues and plan
Windows currently builds the emulator with `EMU_STATIC_APP=1` (the app is
static-linked into the exe). To reach the same "host + dlopen'd app" model as
mac/Linux we need to resolve these:
| # | Issue | Root cause | Plan |
|---|---|---|---|
| 1 | PE requires all symbols resolved at link time | No equivalent of ELF `--unresolved-symbols=ignore-all` or Mach-O `-undefined dynamic_lookup` | Produce `lvgl.dll` + `liblvgl.dll.a` import lib; app links the import lib |
| 2 | LVGL global data not marked dllexport | v9 headers use `LV_ATTRIBUTE_EXTERN_DATA` inconsistently for fonts/styles/builtin tables | Apply a one-shot Windows export header that blankets `__declspec(dllexport/dllimport)` over the public API. Revisit on each LVGL upgrade. |
| 3 | MinGW `__attribute__((weak))` doesn't link | Known MinGW limitation | Already handled via macro in `emu_compat_win.h:9` |
| 4 | C++ runtime duplicated across DLLs | MinGW defaults to `-static-libstdc++`; EXE and app.dll each bring a copy | App ABI is `extern "C"` only (§2). LVGL is C. No C++ objects cross the boundary. |
| 5 | App can't reach emulator symbols via `RTLD_DEFAULT` | Windows hides symbols unless exported | Emulator uses `__declspec(dllexport)` whitelist for the few host-provided helpers (or `--export-all-symbols` during bring-up) |
| 6 | Risk of two `lvgl.dll` copies loaded | If app.dll and emulator.exe resolve different copies, LVGL state splits | CI packs both into one directory; `czdev run` uses `SetDllDirectory` to pin lookup |
| 7 | SDL2 / freetype DLL bundling | MinGW runtime deps | Existing `ldd | awk` logic in `emulator-build.yml:116` stays |
| 8 | Freetype disabled on Windows | Present workaround; CJK fallback differs from mac/Linux | Accept for now; re-enable in a dedicated follow-up task |
The app source does **not** change per platform — all Windows-specific work
lives in the emulator + LVGL build.
---
## 5. Explicit non-goals (this development cycle)
These are good ideas but deliberately **not** in scope right now, to keep the
LVGL desktop-dev loop small and shippable:
- **Python / Qt / Tkinter / raw-framebuffer examples.** They require syscall
interception (fb0 mmap, evdev, ALSA). Use the existing
`CardputerZero-Examples/scripts/dev-on-mac/` Docker path for those.
- **Rust `framebuffer` + `evdev` examples.** Same reason.
- **Subprocess + shared-memory runtime channel.** The dlopen model covers
100% of LVGL apps. Subprocess/SHM is reserved for a later pass when we
want to cover fb/Qt/Python.
- **Hot `dlclose` / live reload.** `dlclose` is effectively a no-op on macOS
and dangerous across C++ statics. `czdev watch` will rebuild and restart
the emulator process instead.
- **Emscripten / WASM target for `czdev`.** The web playground continues to
build via the emulator's own CI; `czdev` stays desktop-only.
- **GUI IDE.** CLI first. Tauri shell is parked until the CLI loop is solid.
---
## 6. Directory layout (target state)
```
CardputerZero-AppBuilder/
├── emulator/ # git submodule → eggfly/M5CardputerZero-Emulator
├── crates/
│ └── czdev/ # Rust CLI: list / build / run / watch / deploy / doctor
├── src-tauri/ # existing Tauri shell (unchanged)
├── sdk/
│ ├── include/
│ │ ├── cz_app.h # app ABI (§2)
│ │ └── lv_conf.h # pinned LVGL config (must match emulator's)
│ └── cmake/
│ └── CZApp.cmake # cz_add_lvgl_app() helper
└── docs/
└── DESKTOP_DEV.md # this file
```
App repos consume the SDK by pointing CMake at `sdk/cmake/CZApp.cmake`; they
do not vendor LVGL.
+142
View File
@@ -0,0 +1,142 @@
# Quickstart — desktop dev for CardputerZero apps
Get a 320×170 LVGL app running on your Mac or Linux machine in ~3 minutes —
no CardputerZero device required.
## 1. Prerequisites
Install the native toolchain.
**macOS:**
```bash
brew install cmake pkg-config sdl2 sdl2_image sdl2_mixer freetype
```
**Linux (Debian/Ubuntu):**
```bash
sudo apt install -y build-essential cmake pkg-config \
libsdl2-dev libsdl2-image-dev libsdl2-mixer-dev libfreetype-dev
```
**Windows:** MSYS2 MINGW64 shell. See
[DESKTOP_DEV.md §4](DESKTOP_DEV.md#4-windows-lvgl--emulator--known-issues-and-plan)
for the Windows-specific work still in progress — the mac/Linux loop below is
what's supported end-to-end today.
You also need a recent Rust toolchain (for `czdev`). If you don't have one:
```bash
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
```
## 2. Clone with submodules
```bash
git clone --recursive git@github.com:m5stack/CardputerZero-AppBuilder.git
cd CardputerZero-AppBuilder
```
If you already cloned without `--recursive`:
```bash
git submodule update --init --recursive
```
## 3. Verify the environment
```bash
cargo run -p czdev --release -- doctor
```
You should see all required rows green. If anything is MISSING, the output
shows the exact install command for your OS.
## 4. Run the hello app
```bash
cargo run -p czdev --release -- run examples/hello_cz
```
On first run this will:
1. Build the emulator (once, cached in `emulator/build/`).
2. Build `examples/hello_cz` into `examples/hello_cz/.czdev/build/`.
3. Stage the resulting `libhello_cz.dylib` (or `.so`) into the emulator's
`apps/` directory.
4. Launch the emulator with the app loaded via `dlopen`.
You should see a 320×170 LCD inside a keyboard skin, showing
`Hello, CardputerZero!`. Close the emulator window to exit.
## 5. Edit-run loop
```bash
cargo run -p czdev --release -- watch examples/hello_cz
```
The watcher polls `src/`, `include/`, `assets/`, `CMakeLists.txt` and
`app-builder.json`. Any change triggers a rebuild and relaunches the emulator.
## 6. Writing your own app
Copy `examples/hello_cz/` and edit `src/hello_cz.c`. The ABI is documented in
`sdk/include/cz_app.h`:
```c
#include <cz_app.h>
void app_main(lv_obj_t *parent) {
lv_obj_t *label = lv_label_create(parent);
lv_label_set_text(label, "your UI here");
lv_obj_center(label);
}
void app_event(int type, void *data) {
(void)type; (void)data;
}
```
The `CMakeLists.txt` is three lines:
```cmake
cmake_minimum_required(VERSION 3.16)
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/../../sdk/cmake")
include(CZApp)
cz_add_lvgl_app(my_app SOURCES src/my_app.c)
```
And the manifest (see `docs/APP_BUILDER_JSON.md`):
```json
{
"package_name": "my_app",
"bin_name": "my_app",
"app_name": "My App",
"runtime": "lvgl-dlopen",
"lvgl_version": "9.5"
}
```
## 7. Shipping to a real device
Building the aarch64 `.deb` stays on CI — trigger the existing
`build-deb.yml` workflow (see the repo README). Then push to the device:
```bash
cargo run -p czdev --release -- deploy \
--host pi@192.168.50.150 \
--deb path/to/my_app_arm64.deb
```
## Troubleshooting
- **`emulator submodule not checked out`** — you forgot `--recursive`. Fix:
`git submodule update --init --recursive`.
- **LVGL link errors about unresolved symbols** — expected in the app
library; they're resolved at `dlopen` time by the emulator. If the linker
*fails* instead of warns, see `DESKTOP_DEV.md` for the per-platform link
flags (`CZApp.cmake` handles these automatically).
- **`indev_read_cb is not registered` warnings in the log** — benign; the
emulator falls back to a default keypad indev when the app doesn't install
its own `lv_sdl_keyboard_create`.
- **macOS: `Library not loaded: @rpath/SDL2.framework/...`** — `brew install
sdl2` puts the library in a non-framework path; re-run `czdev doctor` and
install what it reports.