diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..104deeb --- /dev/null +++ b/.gitattributes @@ -0,0 +1,5 @@ +*.sh text eol=lf +*.mk text eol=lf +SConstruct text eol=lf +*/SConstruct text eol=lf +bin/M5CardputerZero-AppStore text eol=lf diff --git a/README.md b/README.md index faabd89..b739251 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,74 @@ M5APPSTORE_STATE_DIR=/root/.local/share/cardputerzero-appstore M5APPSTORE_APP_ROOT=/usr/share/APPLaunch ``` +## Display runtimes + +The default CardputerZero deployment is unchanged: it builds and installs the +single `M5CardputerZero-AppStore` framebuffer/evdev binary through the existing +APPLaunch path. Wayland support is an optional dual-runtime deployment; it does +not replace the default framebuffer runtime. + +For an explicit dual-runtime deployment, the stable APPLaunch entry becomes a +wrapper that selects the display implementation at runtime: + +```text +/usr/share/APPLaunch/bin/M5CardputerZero-AppStore + wrapper at the existing launcher path +/usr/share/APPLaunch/bin/M5CardputerZero-AppStore-wayland + SDL/LVGL runtime for labwc/Wayland and X11 sessions +/usr/share/APPLaunch/bin/M5CardputerZero-AppStore-fbdev + direct framebuffer runtime for the legacy launcher +``` + +When the optional wrapper is installed, `M5APPSTORE_DISPLAY_BACKEND` selects a +runtime explicitly. If it is unset, the wrapper uses the Wayland/SDL runtime if +`WAYLAND_DISPLAY` or `DISPLAY` is present; otherwise it uses the framebuffer +runtime. This preserves the APPLaunch entry path: + +```bash +M5APPSTORE_DISPLAY_BACKEND=wayland M5CardputerZero-AppStore +M5APPSTORE_DISPLAY_BACKEND=fbdev M5CardputerZero-AppStore +``` + +The wrapper preserves a caller-provided `SDL_VIDEODRIVER`. In a Wayland session +where it is not already set, it selects SDL's Wayland driver and disables +libdecor so labwc owns the window decorations. The SDL runtime identifies its +Wayland window as `cardputerzero-appstore` and creates a fixed 320×170 AppStore +window. + +The normal framebuffer-only device build remains the existing command: + +```bash +CardputerZero=y scons -j8 +``` + +To make a dual-runtime release, build the two named runtime binaries separately +(use separate build directories or clean between configurations), then assemble +the wrapper and both binaries into one release directory: + +```bash +# CardputerZero cross build: primary framebuffer runtime +CardputerZero=y APPSTORE_DISPLAY_BACKEND=fbdev scons -j8 + +# Native aarch64 Linux build on the Wayland-capable runtime image: optional runtime +APPSTORE_DISPLAY_BACKEND=wayland scons -j8 +``` + +Install the dual-runtime release only when all three files are present: + +```bash +install -m 755 M5CardputerZero-AppStore /usr/share/APPLaunch/bin/M5CardputerZero-AppStore +install -m 755 M5CardputerZero-AppStore-fbdev /usr/share/APPLaunch/bin/M5CardputerZero-AppStore-fbdev +install -m 755 M5CardputerZero-AppStore-wayland /usr/share/APPLaunch/bin/M5CardputerZero-AppStore-wayland +``` + +The framebuffer build deliberately remains the primary cross-compilation +profile. The Wayland build is native because it links against the runtime +image's SDL2/Wayland libraries; requesting it with `CardputerZero=y` fails +early instead of producing an incompatible binary. The stock `setup.ini` +continues to deploy the framebuffer-only artifact; dual-runtime release +assembly and installation are an explicit opt-in. + Default registry: ```text diff --git a/SConstruct b/SConstruct index d8c7545..07c376f 100644 --- a/SConstruct +++ b/SConstruct @@ -21,8 +21,54 @@ version_file = local_path.parent.parent / "ext_components" / "cp0_lvgl" / "sdk_v version = version_file.read_text(encoding="utf-8") static_lib_path = sdk_path / "github_source" / f"static_lib_{version}" +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": "wayland", + "wayland": "wayland", + "labwc": "wayland", + "x11": "wayland", + "window": "wayland", + } + 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] + + +display_backend = requested_display_backend() if os.environ.get("CardputerZero", '') == 'y': + if display_backend == "wayland": + raise RuntimeError( + "APPSTORE_DISPLAY_BACKEND=wayland is a native Linux build. " + "The CardputerZero cross build remains the fbdev runtime." + ) os.environ["CONFIG_DEFAULT_FILE"] = "linux_x86_cross_cp0_config_defaults.mk" + if display_backend == "fbdev": + os.environ["APPSTORE_RUNTIME_BINARY"] = "M5CardputerZero-AppStore-fbdev" +elif display_backend == "wayland": + os.environ["CONFIG_DEFAULT_FILE"] = "linux_x86_sdl2_config_defaults.mk" + os.environ["APPSTORE_RUNTIME_BINARY"] = "M5CardputerZero-AppStore-wayland" +elif display_backend == "fbdev": + if os.environ.get("CONFIG_DEFAULT_FILE") is None: + raise RuntimeError( + "APPSTORE_DISPLAY_BACKEND=fbdev requires CardputerZero=y for the " + "cross profile, or an explicit framebuffer CONFIG_DEFAULT_FILE." + ) + os.environ["APPSTORE_RUNTIME_BINARY"] = "M5CardputerZero-AppStore-fbdev" if os.environ.get("CONFIG_DEFAULT_FILE") == None: if platform.machine() == 'x86_64': diff --git a/bin/M5CardputerZero-AppStore b/bin/M5CardputerZero-AppStore new file mode 100755 index 0000000..18aba93 --- /dev/null +++ b/bin/M5CardputerZero-AppStore @@ -0,0 +1,58 @@ +#!/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 + export SDL_VIDEO_WAYLAND_ALLOW_LIBDECOR=0 + export SDL_VIDEO_WAYLAND_PREFER_LIBDECOR=0 + 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/main/SConstruct b/main/SConstruct index 3f2416c..e50c369 100644 --- a/main/SConstruct +++ b/main/SConstruct @@ -37,6 +37,8 @@ APPSTORE_STATIC_ROOT = os.path.join(PROJECT_ROOT, "APPLaunch") APPLAUNCH_STATIC_ROOT = os.path.abspath(os.path.join(PROJECT_ROOT, "..", "APPLaunch", "APPLaunch")) STAGED_STATIC_ROOT = os.path.abspath(os.path.join("build", "static")) STAGED_APPLAUNCH = os.path.join(STAGED_STATIC_ROOT, "APPLaunch") +RUNTIME_BINARY = os.environ.get("APPSTORE_RUNTIME_BINARY", "M5CardputerZero-AppStore") +RUNTIME_WRAPPER = os.path.join(PROJECT_ROOT, "bin", "M5CardputerZero-AppStore") def stage_appstore_static_files(): @@ -65,6 +67,8 @@ def stage_appstore_static_files(): stage_appstore_static_files() STATIC_FILES += [(STAGED_APPLAUNCH, "APPLaunch")] +if RUNTIME_BINARY != "M5CardputerZero-AppStore": + STATIC_FILES += [(RUNTIME_WRAPPER, "M5CardputerZero-AppStore")] def pkg_config_flags(kind, package): try: @@ -108,6 +112,8 @@ if FREETYPE_INCLUDE: if "CONFIG_V9_5_LV_USE_FREETYPE" in os.environ: REQUIREMENTS += ["freetype"] if "CONFIG_V9_5_LV_USE_SDL" in os.environ: + REQUIREMENTS += ["SDL2"] + INCLUDE += ["/usr/include/SDL2"] lvgl_component["REQUIREMENTS"] += ["SDL2"] lvgl_component["INCLUDE"] += ["/usr/include/SDL2"] for sdl_include in [ @@ -117,6 +123,7 @@ if "CONFIG_V9_5_LV_USE_SDL" in os.environ: "/usr/local/include/SDL2", ]: if os.path.exists(sdl_include): + INCLUDE += [sdl_include] lvgl_component["INCLUDE"] += [sdl_include] for sdl_lib in ["/opt/homebrew/lib", "/usr/local/lib"]: if os.path.exists(sdl_lib): @@ -130,7 +137,7 @@ if "CONFIG_V9_5_LV_USE_SDL" in os.environ: env["COMPONENTS"].append( { - "target": "M5CardputerZero-AppStore", + "target": RUNTIME_BINARY, "SRCS": SRCS, "INCLUDE": INCLUDE, "PRIVATE_INCLUDE": PRIVATE_INCLUDE, diff --git a/main/src/main.cpp b/main/src/main.cpp index 9cabe35..cfff2ed 100644 --- a/main/src/main.cpp +++ b/main/src/main.cpp @@ -7,6 +7,7 @@ #include "hal/hal_paths.h" #endif +#include #include namespace { @@ -20,6 +21,17 @@ std::string executable_dir(const char *path) return slash == 0 ? "/" : value.substr(0, slash); } +#ifdef CONFIG_V9_5_LV_USE_SDL +void configure_sdl_runtime() +{ + // These must be configured before cp0_lvgl_run_app() initializes SDL. + setenv("SDL_VIDEO_WAYLAND_ALLOW_LIBDECOR", "0", 0); + setenv("SDL_VIDEO_WAYLAND_PREFER_LIBDECOR", "0", 0); + setenv("SDL_VIDEO_WAYLAND_WMCLASS", "cardputerzero-appstore", 0); + setenv("SDL_VIDEO_X11_WMCLASS", "cardputerzero-appstore", 0); +} +#endif + } // namespace int main(int argc, char **argv) @@ -29,6 +41,7 @@ int main(int argc, char **argv) if (appstore::run_backend_process_mode(argc, argv, &backend_exit_code)) return backend_exit_code; #ifdef CONFIG_V9_5_LV_USE_SDL + configure_sdl_runtime(); const std::string directory = executable_dir(argv && argv[0] ? argv[0] : nullptr); hal_paths_init(directory.c_str()); #endif diff --git a/main/ui/appstore.cpp b/main/ui/appstore.cpp index 4329eb0..abb2f84 100644 --- a/main/ui/appstore.cpp +++ b/main/ui/appstore.cpp @@ -12,6 +12,11 @@ #include "cp0_lvgl_app_runner.hpp" #include "hal_lvgl_bsp.h" +#ifdef CONFIG_V9_5_LV_USE_SDL +#include +#include "lvgl/src/drivers/sdl/lv_sdl_window.h" +#endif + #include #include #include @@ -111,6 +116,22 @@ void handle_signal(int) request_quit(); } +#ifdef CONFIG_V9_5_LV_USE_SDL +void configure_sdl_window() +{ + lv_display_t *display = lv_display_get_default(); + SDL_Window *window = display ? lv_sdl_window_get_window(display) : nullptr; + if (!window) return; + SDL_HideWindow(window); + SDL_SetWindowTitle(window, "AppStore"); + SDL_SetWindowBordered(window, SDL_FALSE); + SDL_SetWindowResizable(window, SDL_FALSE); + SDL_SetWindowMinimumSize(window, kScreenWidth, kScreenHeight); + SDL_SetWindowSize(window, kScreenWidth, kScreenHeight); + SDL_ShowWindow(window); +} +#endif + class AppStoreSyncTopBarComponent final : public AppTopBarComponent { public: @@ -588,6 +609,9 @@ void ui_init(int argc, char **argv) std::signal(SIGINT, handle_signal); std::signal(SIGTERM, handle_signal); cp0_zmq_log_init(); +#ifdef CONFIG_V9_5_LV_USE_SDL + configure_sdl_window(); +#endif if (auto *app = appstore_ui::AppStoreApp::current()) app->lifecycle().initialize(argc, argv); } diff --git a/tests/run_tests.sh b/tests/run_tests.sh index 7816982..e153627 100755 --- a/tests/run_tests.sh +++ b/tests/run_tests.sh @@ -3,6 +3,8 @@ set -eu build_dir="${TMPDIR:-/tmp}/appstore-tests" mkdir -p "$build_dir" "$(dirname "$0")/test_backend_boundaries.sh" +"$(dirname "$0")/test_runtime_deployment.sh" +"$(dirname "$0")/test_runtime_launcher.sh" ${CXX:-g++} -std=c++17 -Wall -Wextra -Werror \ -I"$(dirname "$0")/../main/include" -I"$(dirname "$0")/../main/interface" -I"$(dirname "$0")/../main/ui" \ "$(dirname "$0")/test_appstore_paths.cpp" \ diff --git a/tests/test_runtime_deployment.sh b/tests/test_runtime_deployment.sh new file mode 100755 index 0000000..0a8d03e --- /dev/null +++ b/tests/test_runtime_deployment.sh @@ -0,0 +1,19 @@ +#!/bin/sh +set -eu + +project_dir=$(CDPATH= cd -- "$(dirname "$0")/.." && pwd) +setup_ini="$project_dir/setup.ini" +after_cmd=$(sed -n 's/^after_cmd = //p' "$setup_ini") + +case "$after_cmd" in + *M5CardputerZero-AppStore-wayland*|*M5CardputerZero-AppStore-fbdev*) + echo "default setup.ini deployment must remain framebuffer-only" >&2 + exit 1 + ;; +esac + +printf '%s\n' "$after_cmd" | + grep -F 'install -m 755 /home/pi/dist/M5CardputerZero-AppStore /usr/share/APPLaunch/bin/M5CardputerZero-AppStore' \ + >/dev/null + +echo "default framebuffer deployment test passed" diff --git a/tests/test_runtime_launcher.sh b/tests/test_runtime_launcher.sh new file mode 100755 index 0000000..e4f2993 --- /dev/null +++ b/tests/test_runtime_launcher.sh @@ -0,0 +1,58 @@ +#!/bin/sh +set -eu + +project_dir=$(CDPATH= cd -- "$(dirname "$0")/.." && pwd) +launcher="$project_dir/bin/M5CardputerZero-AppStore" +test_dir=$(mktemp -d "${TMPDIR:-/tmp}/appstore-launcher.XXXXXX") +trap 'rm -rf "$test_dir"' EXIT HUP INT TERM + +make_runtime() { + path=$1 + printf '%s\n' '#!/bin/sh' \ + 'printf "%s|%s|%s|%s\\n" "$0" "${SDL_VIDEODRIVER:-}" "${SDL_VIDEO_WAYLAND_ALLOW_LIBDECOR:-}" "${SDL_VIDEO_WAYLAND_PREFER_LIBDECOR:-}"' \ + > "$path" + chmod +x "$path" +} + +wayland="$test_dir/M5CardputerZero-AppStore-wayland" +fbdev="$test_dir/M5CardputerZero-AppStore-fbdev" +legacy="$test_dir/M5CardputerZero-AppStore.bin" +test_launcher="$test_dir/M5CardputerZero-AppStore" +cp "$launcher" "$test_launcher" +chmod +x "$test_launcher" +make_runtime "$wayland" +make_runtime "$fbdev" + +expected="$wayland|wayland|0|0" +actual=$(env -i PATH="$PATH" WAYLAND_DISPLAY=wayland-0 "$test_launcher") +[ "$actual" = "$expected" ] + +expected="$fbdev|||" +actual=$(env -i PATH="$PATH" "$test_launcher") +[ "$actual" = "$expected" ] + +expected="$wayland|||" +actual=$(env -i PATH="$PATH" DISPLAY=:0 "$test_launcher") +[ "$actual" = "$expected" ] + +expected="$fbdev|||" +actual=$(env -i PATH="$PATH" WAYLAND_DISPLAY=wayland-0 M5APPSTORE_DISPLAY_BACKEND=fbdev "$test_launcher") +[ "$actual" = "$expected" ] + +expected="$wayland|x11|0|0" +actual=$(env -i PATH="$PATH" WAYLAND_DISPLAY=wayland-0 SDL_VIDEODRIVER=x11 "$test_launcher") +[ "$actual" = "$expected" ] + +rm "$fbdev" +make_runtime "$legacy" +expected="$legacy|||" +actual=$(env -i PATH="$PATH" "$test_launcher") +[ "$actual" = "$expected" ] + +if env -i PATH="$PATH" M5APPSTORE_DISPLAY_BACKEND=unknown "$test_launcher" >"$test_dir/error" 2>&1; then + echo "launcher accepted an invalid display backend" >&2 + exit 1 +fi +grep -F 'unsupported M5APPSTORE_DISPLAY_BACKEND=unknown' "$test_dir/error" >/dev/null + +echo "runtime launcher tests passed"