Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -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
40 changes: 40 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
43 changes: 40 additions & 3 deletions SConstruct
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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",
Expand Down
5 changes: 5 additions & 0 deletions applications/appstore.desktop
Original file line number Diff line number Diff line change
@@ -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

56 changes: 42 additions & 14 deletions appstore.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 ""

Expand Down Expand Up @@ -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
Expand All @@ -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]:
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand Down
56 changes: 56 additions & 0 deletions bin/M5CardputerZero-AppStore
Original file line number Diff line number Diff line change
@@ -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" "$@"
2 changes: 2 additions & 0 deletions config_defaults.mk
Original file line number Diff line number Diff line change
Expand Up @@ -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
12 changes: 10 additions & 2 deletions main/SConstruct
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand All @@ -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)
Expand Down
Loading