From 4201ab10230df1a7088dc2a62bef9489bb29ad87 Mon Sep 17 00:00:00 2001 From: liu weikai Date: Sat, 23 May 2026 18:17:37 +0800 Subject: [PATCH] Add Wayland-compatible AppStore runtime --- .gitattributes | 6 ++ README.md | 40 ++++++++++++ SConstruct | 43 ++++++++++++- applications/appstore.desktop | 5 ++ appstore.py | 56 ++++++++++++----- bin/M5CardputerZero-AppStore | 56 +++++++++++++++++ config_defaults.mk | 2 + main/SConstruct | 12 +++- main/src/main.cpp | 111 +++++++++++++++++++++++++++++----- 9 files changed, 298 insertions(+), 33 deletions(-) create mode 100644 .gitattributes create mode 100644 bin/M5CardputerZero-AppStore diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..c2e69ca --- /dev/null +++ b/.gitattributes @@ -0,0 +1,6 @@ +*.sh text eol=lf +*.py text eol=lf +SConstruct text eol=lf +*/SConstruct text eol=lf +*.desktop text eol=lf +*.mk text eol=lf diff --git a/README.md b/README.md index f483533..b4d1ad7 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,46 @@ M5APPSTORE_STATE_DIR=/root/.local/share/cardputerzero-appstore M5APPSTORE_APP_ROOT=/usr/share/APPLaunch ``` +## Display Backends + +AppStore has two Linux display runtimes behind one APPLaunch entry: + +```text +/usr/share/APPLaunch/bin/M5CardputerZero-AppStore + -> wrapper selected by .desktop Exec +/usr/share/APPLaunch/bin/M5CardputerZero-AppStore-wayland + -> SDL/LVGL window for labwc/Wayland or X11 +/usr/share/APPLaunch/bin/M5CardputerZero-AppStore-fbdev + -> direct framebuffer LVGL runtime for the legacy launcher +``` + +The wrapper selects the windowed runtime when `WAYLAND_DISPLAY` or `DISPLAY` +is present, and otherwise selects the framebuffer runtime. It can be forced +with: + +```bash +M5APPSTORE_DISPLAY_BACKEND=wayland M5CardputerZero-AppStore +M5APPSTORE_DISPLAY_BACKEND=fbdev M5CardputerZero-AppStore +``` + +This keeps the old launcher compatible while allowing `cardputer-zero-shell` +inside labwc to launch AppStore as a compositor-managed window. + +Build the windowed runtime with: + +```bash +APPSTORE_DISPLAY_BACKEND=wayland scons +``` + +Build the framebuffer runtime with: + +```bash +APPSTORE_DISPLAY_BACKEND=fbdev scons +``` + +On the CardputerZero cross-build, the framebuffer runtime remains the supported +target. The SDL/Wayland runtime is a native Linux build. + Default registry: ```text diff --git a/SConstruct b/SConstruct index 0c97506..7e1a2a1 100644 --- a/SConstruct +++ b/SConstruct @@ -18,6 +18,33 @@ freetype_config_lines = [ ] +def requested_display_backend(): + value = ( + os.environ.get("APPSTORE_DISPLAY_BACKEND") + or os.environ.get("M5APPSTORE_DISPLAY_BACKEND") + or "auto" + ).strip().lower() + aliases = { + "": "auto", + "auto": "auto", + "fb": "fbdev", + "fbdev": "fbdev", + "framebuffer": "fbdev", + "direct-fb": "fbdev", + "device": "fbdev", + "sdl": "sdl", + "wayland": "sdl", + "labwc": "sdl", + "x11": "sdl", + "window": "sdl", + } + if value not in aliases: + raise RuntimeError( + "Unsupported APPSTORE_DISPLAY_BACKEND={!r}. Use auto, fbdev, sdl, wayland, labwc, or x11.".format(value) + ) + return aliases[value] + + def resolve_sdk_path(): candidates = [] sdk_override = os.environ.get("SDK_PATH") @@ -50,12 +77,15 @@ def resolve_sdk_path(): def write_config_tmp(lines): - path = local_path / "build" / "config" / "config_tmp.mk" + path = local_path / "build" / "config" / "config_defaults.generated.mk" path.parent.mkdir(parents=True, exist_ok=True) - content = "\n".join(lines) + "\n" + base_path = local_path / "config_defaults.mk" + base = base_path.read_text() if base_path.exists() else "" + content = base.rstrip() + "\n\n" + "\n".join(lines) + "\n" changed = not path.exists() or path.read_text() != content if changed: path.write_text(content) + os.environ["CONFIG_DEFAULT_FILE"] = str(path) if changed or not generated_config_matches(lines): invalidate_generated_config() @@ -166,7 +196,14 @@ def ensure_static_lib_ready(): ensure_sysroot_lib_layout(Path(static_lib)) +display_backend = requested_display_backend() + if "CardputerZero" in os.environ: + if display_backend == "sdl": + raise RuntimeError( + "APPSTORE_DISPLAY_BACKEND=sdl/wayland is a native Linux build. " + "The CardputerZero cross build remains fbdev." + ) sysroot_dir = local_path / static_lib os.environ["CARDPUTERZERO_STATIC_LIB_SYSROOT"] = str(sysroot_dir) generic_include = sysroot_dir / "usr" / "include" @@ -199,7 +236,7 @@ if "CardputerZero" in os.environ: ] ) write_config_tmp(config_lines) -elif arch != "aarch64": +elif arch != "aarch64" or display_backend == "sdl": write_config_tmp( [ "CONFIG_V9_5_LV_USE_SDL=y", diff --git a/applications/appstore.desktop b/applications/appstore.desktop index b12414d..2ad4cb9 100644 --- a/applications/appstore.desktop +++ b/applications/appstore.desktop @@ -1,7 +1,12 @@ [Desktop Entry] Name=AppStore +TryExec=/usr/share/APPLaunch/bin/M5CardputerZero-AppStore Exec=/usr/share/APPLaunch/bin/M5CardputerZero-AppStore Terminal=false Icon=share/images/appstore.png Type=Application +StartupWMClass=M5CardputerZero-AppStore-wayland +X-Zero-AppId=cardputerzero-appstore +X-Zero-Display=wayland +X-Zero-ShortName=STORE diff --git a/appstore.py b/appstore.py index 81a9bb1..83879ec 100755 --- a/appstore.py +++ b/appstore.py @@ -590,7 +590,8 @@ def repair_applaunch_desktop(app: dict[str, Any], files: list[str]) -> str: for exec_value in candidate_execs(app, files): binary = exec_binary_path(exec_value) if binary and Path(binary).exists() and os.access(binary, os.X_OK): - rewrite_desktop_exec(desktop, binary) + if os.access(desktop, os.W_OK): + rewrite_desktop_exec(desktop, binary) return binary return "" @@ -848,6 +849,31 @@ def run_package_command(args: list[str]) -> None: raise RuntimeError(command_error(result)) +def zero_helper_available() -> bool: + helper = Path("/usr/local/sbin/zero-helper") + return helper.exists() and os.access(helper, os.X_OK) + + +def running_as_root() -> bool: + return hasattr(os, "geteuid") and os.geteuid() == 0 + + +def run_zero_helper(args: list[str]) -> None: + if not zero_helper_available(): + raise RuntimeError("zero-helper is required for privileged package operations") + run_package_command(["/usr/local/sbin/zero-helper", *args]) + + +def run_privileged_package_command(helper_args: list[str], root_args: list[str]) -> None: + if zero_helper_available(): + run_zero_helper(helper_args) + return + if running_as_root(): + run_package_command(root_args) + return + raise RuntimeError("zero-helper is required for privileged package operations") + + def repair_dpkg_state() -> None: if not shutil.which("dpkg"): return @@ -859,7 +885,10 @@ def repair_dpkg_state() -> None: ) if (audit.stdout or audit.stderr).strip(): emit("PROGRESS", "apt", 0, 0, -1, "Repairing package database") - run_package_command(["dpkg", "--configure", "-a"]) + run_privileged_package_command( + ["appstore", "repair-dpkg"], + ["dpkg", "--configure", "-a"], + ) def package_files(package: str) -> list[str]: @@ -891,14 +920,14 @@ def uninstall(app_id: str) -> int: return 1 try: repair_dpkg_state() + emit("PROGRESS", "uninstall", 0, 0, -1, "Removing package") if shutil.which("apt-get"): - emit("PROGRESS", "uninstall", 0, 0, -1, "Removing package") - run_package_command(["apt-get", "-y", "remove", package]) + root_args = ["apt-get", "-y", "remove", package] elif shutil.which("dpkg"): - emit("PROGRESS", "uninstall", 0, 0, -1, "Removing package") - run_package_command(["dpkg", "-r", package]) + root_args = ["dpkg", "-r", package] else: raise RuntimeError("apt-get or dpkg is required to uninstall deb packages") + run_privileged_package_command(["appstore", "remove", package], root_args) write_json(installed_path(), records) emit("PROGRESS", "uninstall", 1, 1, 100, "Remove complete") emit("UNINSTALLED", app_id) @@ -920,22 +949,21 @@ def install(app_id: str, reinstall: bool = False, upgrade: bool = False) -> int: package = deb_package_name(app) if not package: raise RuntimeError("deb package name missing") - repair_dpkg_state() stage = "upgrade" if upgrade else "install" operation = "Upgrading" if upgrade else "Installing" complete = "Upgrade complete" if upgrade else "Install complete" + repair_dpkg_state() + emit("PROGRESS", stage, 0, 0, -1, f"{operation} package") if shutil.which("apt-get"): - args = ["apt-get", "-y"] + root_args = ["apt-get", "-y"] if reinstall: - args.append("--reinstall") - args += ["install", str(deb_path)] - emit("PROGRESS", stage, 0, 0, -1, f"{operation} package") - run_package_command(args) + root_args.append("--reinstall") + root_args += ["install", str(deb_path)] elif shutil.which("dpkg"): - emit("PROGRESS", stage, 0, 0, -1, f"{operation} package") - run_package_command(["dpkg", "-i", str(deb_path)]) + root_args = ["dpkg", "-i", str(deb_path)] else: raise RuntimeError("apt-get or dpkg is required to install deb packages") + run_privileged_package_command(["appstore", "install-deb", str(deb_path), package], root_args) records = installed_records() files = package_files(package) repaired_exec = repair_applaunch_desktop(app, files) diff --git a/bin/M5CardputerZero-AppStore b/bin/M5CardputerZero-AppStore new file mode 100644 index 0000000..df9bd60 --- /dev/null +++ b/bin/M5CardputerZero-AppStore @@ -0,0 +1,56 @@ +#!/bin/sh +# SPDX-License-Identifier: MIT + +set -eu + +case "$0" in + */*) app_dir=${0%/*} ;; + *) app_dir=/usr/share/APPLaunch/bin ;; +esac + +backend=${M5APPSTORE_DISPLAY_BACKEND:-${APPSTORE_DISPLAY_BACKEND:-auto}} + +case "$backend" in + auto|"") + if [ -n "${WAYLAND_DISPLAY:-}" ] || [ -n "${DISPLAY:-}" ]; then + backend=wayland + else + backend=fbdev + fi + ;; + wayland|labwc|sdl|x11|window) + backend=wayland + ;; + fb|fbdev|framebuffer|direct-fb|device) + backend=fbdev + ;; + *) + echo "M5CardputerZero-AppStore: unsupported M5APPSTORE_DISPLAY_BACKEND=$backend" >&2 + exit 2 + ;; +esac + +if [ "$backend" = "wayland" ]; then + binary=$app_dir/M5CardputerZero-AppStore-wayland + if [ ! -x "$binary" ]; then + echo "M5CardputerZero-AppStore: Wayland/SDL runtime is not installed: $binary" >&2 + exit 127 + fi + if [ -n "${WAYLAND_DISPLAY:-}" ] && [ -z "${SDL_VIDEODRIVER:-}" ]; then + export SDL_VIDEODRIVER=wayland + fi + exec "$binary" "$@" +fi + +binary=$app_dir/M5CardputerZero-AppStore-fbdev +if [ ! -x "$binary" ]; then + legacy=$app_dir/M5CardputerZero-AppStore.bin + if [ -x "$legacy" ]; then + binary=$legacy + else + echo "M5CardputerZero-AppStore: framebuffer runtime is not installed: $binary" >&2 + exit 127 + fi +fi + +exec "$binary" "$@" diff --git a/config_defaults.mk b/config_defaults.mk index 13ad954..beb7175 100644 --- a/config_defaults.mk +++ b/config_defaults.mk @@ -18,3 +18,5 @@ CONFIG_V9_5_LV_FS_POSIX_LETTER=65 CONFIG_V9_5_LV_FS_POSIX_PATH="/" CONFIG_V9_5_LV_FS_POSIX_CACHE_SIZE=0 CONFIG_V9_5_LV_USE_LODEPNG=y +CONFIG_V9_5_LV_BUILD_EXAMPLES=n +CONFIG_V9_5_LV_BUILD_DEMOS=n diff --git a/main/SConstruct b/main/SConstruct index c0e11da..ce30989 100644 --- a/main/SConstruct +++ b/main/SConstruct @@ -70,6 +70,16 @@ if FREETYPE_INCLUDE: component["INCLUDE"] += [FREETYPE_INCLUDE] if "CONFIG_V9_5_LV_USE_FREETYPE" in os.environ: REQUIREMENTS += ["freetype"] + if not FREETYPE_INCLUDE: + append_pkg_config("freetype2", lvgl_component["DEFINITIONS"], REQUIREMENTS, + LINK_SEARCH_PATH, LDFLAGS) + +lvgl_component["SRCS"] = [ + src for src in lvgl_component["SRCS"] + if "/lvgl/examples/" not in str(src).replace("\\", "/") + and "/lvgl/demos/" not in str(src).replace("\\", "/") +] + if "CONFIG_V9_5_LV_USE_SDL" in os.environ: lvgl_component["REQUIREMENTS"] += ["SDL2"] lvgl_component["INCLUDE"] += ["/usr/include/SDL2"] @@ -85,8 +95,6 @@ if "CONFIG_V9_5_LV_USE_SDL" in os.environ: if os.path.exists(sdl_lib): lvgl_component["LINK_SEARCH_PATH"] += [sdl_lib] lvgl_component["DEFINITIONS"] += ["-D_REENTRANT"] - append_pkg_config("freetype2", lvgl_component["DEFINITIONS"], REQUIREMENTS, - LINK_SEARCH_PATH, LDFLAGS) for src in list(lvgl_component["SRCS"]): if "lv_sdl_keyboard.c" in str(src): lvgl_component["SRCS"].remove(src) diff --git a/main/src/main.cpp b/main/src/main.cpp index 2fdf9ea..452898a 100644 --- a/main/src/main.cpp +++ b/main/src/main.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -22,6 +23,7 @@ #include #if LV_USE_SDL +#include #include "lvgl/src/drivers/sdl/lv_sdl_keyboard.h" #include "lvgl/src/drivers/sdl/lv_sdl_mouse.h" #include "lvgl/src/drivers/sdl/lv_sdl_window.h" @@ -40,6 +42,7 @@ constexpr int kLineHeight = 15; constexpr uint32_t kEscLongPressMs = 1200; constexpr uint32_t kJobStartDelayMs = 80; constexpr uint32_t kJobPollIntervalMs = 250; +constexpr uint32_t kPackageJobTimeoutMs = 120000; struct StoreApp { std::string id; @@ -359,6 +362,22 @@ std::string backend_error_message(const std::string &out) return out.empty() ? "Operation failed" : out; } +void stop_job_process_group() +{ + if (g_job_pid <= 0) return; + kill(-g_job_pid, SIGTERM); + usleep(200000); + int status = 0; + pid_t waited = waitpid(g_job_pid, &status, WNOHANG); + if (waited == 0) { + kill(-g_job_pid, SIGKILL); + waitpid(g_job_pid, &status, 0); + } else if (waited < 0) { + waitpid(g_job_pid, &status, WNOHANG); + } + g_job_pid = -1; +} + std::string sync_status_message(const std::string &out) { std::string error; @@ -1346,7 +1365,8 @@ void finish_backend_job(const std::string &out, const std::string &rc_text) void poll_backend_job() { if (!g_job_running || g_job_pending_start) return; - uint32_t elapsed = lv_tick_elaps(g_job_start_tick) / 1000; + uint32_t elapsed_ms = lv_tick_elaps(g_job_start_tick); + uint32_t elapsed = elapsed_ms / 1000; std::string out = read_text_file(g_job_output_path); parse_job_progress(out); std::string detail = g_job_detail.empty() ? job_action_label(g_job_action) : g_job_detail; @@ -1354,6 +1374,16 @@ void poll_backend_job() detail += " " + std::to_string(g_job_progress) + "%"; } g_status_message = detail + " " + one_line(g_job_title, 16) + " " + std::to_string(elapsed) + "s"; + if (elapsed_ms > kPackageJobTimeoutMs && !file_exists(g_job_rc_path)) { + stop_job_process_group(); + std::ofstream rc_file(g_job_rc_path); + rc_file << "124\n"; + rc_file.close(); + std::ofstream out_file(g_job_output_path, std::ios::app); + out_file << "ERROR\tPackage operation timed out waiting for authorization or apt/dpkg\n"; + out_file.close(); + out = read_text_file(g_job_output_path); + } if (!file_exists(g_job_rc_path)) return; finish_backend_job(out, read_text_file(g_job_rc_path)); } @@ -1974,20 +2004,58 @@ void handle_lv_key_event(lv_event_t *event) } #endif -int get_st7789v_fbdev(char *dev_path, size_t buf_size) +bool is_internal_fb_name(const char *name) +{ + return name && + (std::strstr(name, "fb_st7789v") || + std::strstr(name, "st7789") || + std::strstr(name, "panel-mipi-dbid") || + std::strstr(name, "mipi-dbid")); +} + +int get_internal_fbdev(char *dev_path, size_t buf_size) { if (!dev_path || buf_size == 0) return -1; + int only_fb = -1; + int fb_count = 0; + FILE *fp = std::fopen("/proc/fb", "r"); - if (!fp) return -1; - char line[256]; - int fb_num = -1; - while (std::fgets(line, sizeof(line), fp)) { - if (std::strstr(line, "fb_st7789v") && std::sscanf(line, "%d", &fb_num) == 1) break; - } - std::fclose(fp); - if (fb_num < 0) return -1; - std::snprintf(dev_path, buf_size, "/dev/fb%d", fb_num); - return 0; + if (fp) { + char line[256]; + while (std::fgets(line, sizeof(line), fp)) { + int fb_num = -1; + if (std::sscanf(line, "%d", &fb_num) != 1) continue; + only_fb = fb_num; + fb_count++; + if (is_internal_fb_name(line)) { + std::fclose(fp); + std::snprintf(dev_path, buf_size, "/dev/fb%d", fb_num); + return 0; + } + } + std::fclose(fp); + } + + for (int fb_num = 0; fb_num < 8; ++fb_num) { + char name_path[96]; + std::snprintf(name_path, sizeof(name_path), "/sys/class/graphics/fb%d/name", fb_num); + fp = std::fopen(name_path, "r"); + if (!fp) continue; + char name[128] = {}; + bool matched = std::fgets(name, sizeof(name), fp) && is_internal_fb_name(name); + std::fclose(fp); + if (matched) { + std::snprintf(dev_path, buf_size, "/dev/fb%d", fb_num); + return 0; + } + } + + if (fb_count == 1 && only_fb >= 0) { + std::snprintf(dev_path, buf_size, "/dev/fb%d", only_fb); + return 0; + } + + return -1; } #ifndef APPSTORE_EMBEDDED @@ -2048,8 +2116,12 @@ void lv_linux_disp_init() { char fbdev[64] = {}; const char *device = getenv_default("LV_LINUX_FBDEV_DEVICE", nullptr); - if (!device && get_st7789v_fbdev(fbdev, sizeof(fbdev)) == 0) device = fbdev; + if (!device) device = getenv_default("CARDPUTER_ZERO_FB", nullptr); + if (!device) device = getenv_default("ZEROSHELL_FBDEV", nullptr); + if (!device && get_internal_fbdev(fbdev, sizeof(fbdev)) == 0) device = fbdev; if (!device) device = "/dev/fb0"; + setenv("LV_LINUX_FBDEV_DEVICE", device, 0); + setenv("APPLAUNCH_LINUX_FBDEV_DEVICE", device, 0); lv_display_t *disp = lv_linux_fbdev_create(); if (disp) lv_linux_fbdev_set_file(disp, device); } @@ -2061,8 +2133,19 @@ void lv_linux_indev_init() {} #elif LV_USE_SDL void lv_linux_disp_init() { + SDL_SetHint(SDL_HINT_APP_NAME, "AppStore"); + SDL_SetHint(SDL_HINT_VIDEO_WAYLAND_ALLOW_LIBDECOR, "0"); + SDL_SetHint("SDL_VIDEO_WAYLAND_PREFER_LIBDECOR", "0"); + setenv("SDL_VIDEO_WAYLAND_WMCLASS", "cardputerzero-appstore", 0); + setenv("SDL_VIDEO_X11_WMCLASS", "M5CardputerZero-AppStore-wayland", 0); lv_display_t *disp = lv_sdl_window_create(kScreenWidth, kScreenHeight); - lv_sdl_window_set_title(disp, "STORE"); + lv_sdl_window_set_title(disp, "AppStore"); + if (SDL_Window *window = lv_sdl_window_get_window(disp)) { + SDL_SetWindowBordered(window, SDL_FALSE); + SDL_SetWindowResizable(window, SDL_FALSE); + SDL_SetWindowMinimumSize(window, kScreenWidth, kScreenHeight); + SDL_SetWindowSize(window, kScreenWidth, kScreenHeight); + } } void lv_linux_indev_init()