4 Commits de47bacb28 ... 5689b570b6

Autore SHA1 Messaggio Data
  Stefano Babic 5689b570b6 swupdategui-rs: add recipe for the recovery GUI written in Rust 2 giorni fa
  Stefano Babic 93e9b3a766 swupdate-gui: create the device names opened by the GUI 2 giorni fa
  Stefano Babic 2223eb11b2 swupdate-gui: build with LVGL 9.4 2 giorni fa
  Stefano Babic bcb4be70d6 lvgl: use the C library allocator 2 giorni fa

+ 4 - 0
recipes-graphics/lvgl/files/clib-malloc.cfg

@@ -0,0 +1,4 @@
+# Use the C library allocator instead of LVGL's builtin pool. The builtin
+# pool has a fixed size (64 KB by default) and running out of it ends in
+# LV_ASSERT_HANDLER (while(1)): the recovery GUI hangs at 100% CPU.
+CONFIG_LV_USE_CLIB_MALLOC=y

+ 4 - 0
recipes-graphics/lvgl/lvgl_%.bbappend

@@ -1 +1,5 @@
+FILESEXTRAPATHS:prepend := "${THISDIR}/files:"
+
 PACKAGECONFIG += "fbdev gridnav"
+
+SRC_URI += "file://clib-malloc.cfg"

+ 220 - 0
recipes-support/rescuegui/files/0001-Ported-to-lvgl-9.4.patch

@@ -0,0 +1,220 @@
+From a5b2f9fcead3ddbea046e9e1b3e807d251a406f3 Mon Sep 17 00:00:00 2001
+From: Stefano Babic <stefano.babic@swupdate.org>
+Date: Tue, 28 Apr 2026 09:51:36 +0200
+Subject: [PATCH] Ported to lvgl 9.4
+
+There are some incompatibilities in the API, so port to 9.4.0.
+
+Upstream-Status: Pending
+
+Signed-off-by: Stefano Babic <stefano.babic@swupdate.org>
+---
+ CMakeLists.txt             |  2 +-
+ rec-screen-file-browser.c  | 12 ++++++------
+ rec-screen-main.c          |  2 +-
+ rec-screen-network-setup.c | 18 +++++++++---------
+ rec-screen-progress.c      |  8 ++++----
+ 5 files changed, 21 insertions(+), 21 deletions(-)
+
+diff --git a/CMakeLists.txt b/CMakeLists.txt
+index 156dfb0..ff003e2 100644
+--- a/CMakeLists.txt
++++ b/CMakeLists.txt
+@@ -1,5 +1,5 @@
+ 
+-cmake_minimum_required( VERSION 3.1 )
++cmake_minimum_required(VERSION 4.1)
+ project( recovery_gui )
+ 
+ 
+diff --git a/rec-screen-file-browser.c b/rec-screen-file-browser.c
+index a7673a1..920e0ad 100644
+--- a/rec-screen-file-browser.c
++++ b/rec-screen-file-browser.c
+@@ -166,7 +166,7 @@ rec_screen_file_clearDryRunChk(struct FileBrowserScreen *FileBrowser)
+ static void rec_screen_file_onClickDryRunChk(lv_event_t *E)
+ {
+ 	struct FileBrowserScreen *FileBrowser =
+-		(struct FileBrowserScreen *)E->user_data;
++		(struct FileBrowserScreen *)lv_event_get_user_data(E);
+ 
+ 	lv_event_code_t Code = lv_event_get_code(E);
+ 	if (LV_EVENT_VALUE_CHANGED == Code) {
+@@ -286,7 +286,7 @@ rec_screen_file_updateCurrentDir(struct FileBrowserScreen *FileBrowser,
+ 
+ static void rec_screen_file_onClickDirEntry(lv_event_t *E)
+ {
+-	const char *SelectedDir = (const char *)E->user_data;
++	const char *SelectedDir = (const char *)lv_event_get_user_data(E);
+ 	struct FileBrowserScreen *FileBrowser = AccessFileBrowserScreen();
+ 
+ 	const bool Status =
+@@ -313,7 +313,7 @@ static void rec_screen_file_onClickDirEntry(lv_event_t *E)
+ static void rec_screen_file_onClickFileEntry(lv_event_t *E)
+ {
+ 	struct FileBrowserScreen *FileBrowser =
+-		(struct FileBrowserScreen *)E->user_data;
++		(struct FileBrowserScreen *)lv_event_get_user_data(E);
+ 
+ 	lv_obj_t *CheckBox = lv_event_get_target(E);
+ 	const char *FileInfo = lv_checkbox_get_text(CheckBox);
+@@ -476,7 +476,7 @@ rec_screen_file_createFileList(struct FileBrowserScreen *FileBrowser)
+ static void rec_screen_file_onClickButtonStart(lv_event_t *E)
+ {
+ 	struct FileBrowserScreen *FileBrowser =
+-		(struct FileBrowserScreen *)E->user_data;
++		(struct FileBrowserScreen *)lv_event_get_user_data(E);
+ 
+ 	rec_screen_progress_showScreen();
+ 
+@@ -540,7 +540,7 @@ rec_screen_file_createStartButton(struct FileBrowserScreen *FileBrowser)
+ static void rec_screen_file_onClickRescanButton(lv_event_t *E)
+ {
+ 	struct FileBrowserScreen *FileBrowser =
+-		(struct FileBrowserScreen *)E->user_data;
++		(struct FileBrowserScreen *)lv_event_get_user_data(E);
+ 
+ 	rec_screen_file_deleteFileEntries(FileBrowser);
+ 
+@@ -574,7 +574,7 @@ rec_screen_file_createRescanButton(struct FileBrowserScreen *FileBrowser)
+ static void rec_screen_file_onClickCancelButton(lv_event_t *E)
+ {
+ 	struct FileBrowserScreen *FileBrowser =
+-		(struct FileBrowserScreen *)E->user_data;
++		(struct FileBrowserScreen *)lv_event_get_user_data(E);
+ 	rec_screen_file_deleteFileEntries(FileBrowser);
+ 
+ 	rec_screen_main_showScreen();
+diff --git a/rec-screen-main.c b/rec-screen-main.c
+index 19f1e60..90883ef 100644
+--- a/rec-screen-main.c
++++ b/rec-screen-main.c
+@@ -80,7 +80,7 @@ static void rec_screen_main_OnClickNetworkSettingButton(lv_event_t *Event)
+ 
+ static void rec_screen_main_onClickRestartButton(lv_event_t *Event)
+ {
+-	struct MainScreen *MainScreen = (struct MainScreen *)Event->user_data;
++	struct MainScreen *MainScreen = (struct MainScreen *)lv_event_get_user_data(Event);
+ 
+ 	const int rebootStatus = system("reboot");
+ 	if (0 != rebootStatus) {
+diff --git a/rec-screen-network-setup.c b/rec-screen-network-setup.c
+index 230e66f..dbd9ab3 100644
+--- a/rec-screen-network-setup.c
++++ b/rec-screen-network-setup.c
+@@ -548,9 +548,9 @@ static void rec_screen_network_InterfaceList_handleTableRowSelection(
+ 
+ static void rec_screen_network_InterfaceList_onClickTableRow(lv_event_t *E)
+ {
+-	const struct Ipv4Info *IpInfo = (struct Ipv4Info *)E->user_data;
++	const struct Ipv4Info *IpInfo = (struct Ipv4Info *)lv_event_get_user_data(E);
+ 
+-	lv_obj_t *Button = E->current_target;
++	lv_obj_t *Button = lv_event_get_current_target_obj(E);
+ 
+ 	lv_obj_t *Checkbox = lv_obj_get_child(Button, 1);
+ 
+@@ -600,7 +600,7 @@ static void rec_screen_network_InterfaceList_createTableRowEntry_IpAddress(
+ 	lv_obj_t *IpAddrLabel = lv_label_create(Button);
+ 
+ 	lv_obj_set_width(IpAddrLabel, LV_PCT(25));
+-	lv_obj_set_style_text_align(IpAddrLabel, LV_ALIGN_LEFT_MID,
++	lv_obj_set_style_text_align(IpAddrLabel, LV_TEXT_ALIGN_LEFT,
+ 				    LV_PART_MAIN);
+ 	lv_label_set_text(IpAddrLabel, IpInfo->Address);
+ 
+@@ -614,7 +614,7 @@ static void rec_screen_network_InterfaceList_createTableRowEntry_Netmask(
+ 	lv_obj_t *NetmaskLabel = lv_label_create(Button);
+ 
+ 	lv_obj_set_width(NetmaskLabel, LV_PCT(25));
+-	lv_obj_set_style_text_align(NetmaskLabel, LV_ALIGN_LEFT_MID,
++	lv_obj_set_style_text_align(NetmaskLabel, LV_TEXT_ALIGN_LEFT,
+ 				    LV_PART_MAIN);
+ 	lv_label_set_text(NetmaskLabel, IpInfo->Netmask);
+ 
+@@ -628,7 +628,7 @@ static void rec_screen_network_InterfaceList_createTableRowEntry_Dhcp(
+ 	lv_obj_t *DhcpLabel = lv_label_create(Button);
+ 
+ 	lv_obj_set_width(DhcpLabel, LV_PCT(25));
+-	lv_obj_set_style_text_align(DhcpLabel, LV_ALIGN_LEFT_MID, LV_PART_MAIN);
++	lv_obj_set_style_text_align(DhcpLabel, LV_TEXT_ALIGN_LEFT, LV_PART_MAIN);
+ 
+ 	const char *dhcpFieldText = util_networking_getDhcpText(IpInfo);
+ 	lv_label_set_text(DhcpLabel, dhcpFieldText);
+@@ -701,7 +701,7 @@ rec_screen_network_InterfaceList_createTableInterfaceNameColumnHeader(
+ 
+ 	lv_obj_set_width(NetIfHeader, LV_PCT(25));
+ 	lv_label_set_text(NetIfHeader, "NETIF");
+-	lv_obj_set_style_text_align(NetIfHeader, LV_ALIGN_LEFT_MID,
++	lv_obj_set_style_text_align(NetIfHeader, LV_TEXT_ALIGN_LEFT,
+ 				    LV_PART_MAIN);
+ }
+ 
+@@ -712,7 +712,7 @@ rec_screen_network_InterfaceList_createTableIpAddrColumnHeader(lv_obj_t *Header)
+ 
+ 	lv_obj_set_width(IpAddressHeader, LV_PCT(25));
+ 	lv_label_set_text(IpAddressHeader, "IP ADDRESS");
+-	lv_obj_set_style_text_align(IpAddressHeader, LV_ALIGN_LEFT_MID,
++	lv_obj_set_style_text_align(IpAddressHeader, LV_TEXT_ALIGN_LEFT,
+ 				    LV_PART_MAIN);
+ }
+ 
+@@ -723,7 +723,7 @@ static void rec_screen_network_InterfaceList_createTableNetmaskColumnHeader(
+ 
+ 	lv_obj_set_width(NetmaskHeader, LV_PCT(25));
+ 	lv_label_set_text(NetmaskHeader, "NETMASK");
+-	lv_obj_set_style_text_align(NetmaskHeader, LV_ALIGN_LEFT_MID,
++	lv_obj_set_style_text_align(NetmaskHeader, LV_TEXT_ALIGN_LEFT,
+ 				    LV_PART_MAIN);
+ }
+ 
+@@ -734,7 +734,7 @@ rec_screen_network_InterfaceList_createTableDhcpColumnHeader(lv_obj_t *Header)
+ 
+ 	lv_obj_set_width(DhcpHeader, LV_PCT(25));
+ 	lv_label_set_text(DhcpHeader, "DHCP");
+-	lv_obj_set_style_text_align(DhcpHeader, LV_ALIGN_LEFT_MID,
++	lv_obj_set_style_text_align(DhcpHeader, LV_TEXT_ALIGN_LEFT,
+ 				    LV_PART_MAIN);
+ }
+ 
+diff --git a/rec-screen-progress.c b/rec-screen-progress.c
+index 82513fd..a326646 100644
+--- a/rec-screen-progress.c
++++ b/rec-screen-progress.c
+@@ -585,21 +585,21 @@ rec_screen_progressScreen_showLogMessages(struct ProgressScreen *ProgressScreen)
+ 
+ static void rec_screen_progress_onClickShowLogsButton(lv_event_t *E)
+ {
+-	struct ProgressScreen *ProgressScreen = E->user_data;
++	struct ProgressScreen *ProgressScreen = lv_event_get_user_data(E);
+ 
+ 	rec_screen_progressScreen_showLogMessages(ProgressScreen);
+ }
+ 
+ static void rec_screen_progress_onClickBackButton(lv_event_t *E)
+ {
+-	struct ProgressScreen *ProgressScreen = E->user_data;
++	struct ProgressScreen *ProgressScreen = lv_event_get_user_data(E);
+ 
+ 	rec_screen_progressScreen_showProgressBars(ProgressScreen);
+ }
+ 
+ static void rec_screen_progress_onClickExportLogsButton(lv_event_t *E)
+ {
+-	struct ProgressScreen *screen = E->user_data;
++	struct ProgressScreen *screen = lv_event_get_user_data(E);
+ 
+ 	char *filename = NULL;
+ 	const int res = util_files_export_swupdate_logs(&filename);
+@@ -636,7 +636,7 @@ static void rec_screen_progressScreen_createLogsMessagesTextArea(
+ 	lv_obj_center(ProgressScreen->logMessages.TextArea);
+ 
+ 	lv_obj_set_style_text_align(ProgressScreen->logMessages.TextArea,
+-				    LV_ALIGN_LEFT_MID, LV_PART_MAIN);
++				    LV_TEXT_ALIGN_LEFT, LV_PART_MAIN);
+ }
+ 
+ /**
+-- 
+2.43.0
+

+ 5 - 0
recipes-support/rescuegui/files/90-recovery-gui.rules

@@ -0,0 +1,5 @@
+# swupdate-gui (recovery_gui) opens fixed device names:
+# /dev/fb, /dev/input/keyboard0 and /dev/input/touchscreen0
+SUBSYSTEM=="graphics", KERNEL=="fb0", SYMLINK+="fb"
+SUBSYSTEM=="input", KERNEL=="event*", ENV{ID_INPUT_KEYBOARD}=="1", SYMLINK+="input/keyboard0"
+SUBSYSTEM=="input", KERNEL=="event*", ENV{ID_INPUT_TOUCHSCREEN}=="1", SYMLINK+="input/touchscreen0"

+ 8 - 0
recipes-support/rescuegui/swupdate-gui_git.bb

@@ -12,6 +12,8 @@ DEPENDS:append = " \
 SRC_URI = "git://github.com/sbabic/SWUpdateGUI.git;protocol=https;branch=main \
 	file://swupdate-gui \
 	file://swupdate-gui.service \
+	file://0001-Ported-to-lvgl-9.4.patch \
+	file://90-recovery-gui.rules \
 	"
 SRCREV = "d6bd129b4cea1e5c53bcad077a7d25af8260ef84"
 
@@ -37,8 +39,14 @@ do_install:append () {
         install -m 0755 ${S}/scripts/recovery-get-dhcp-status.sh ${D}${bindir}
         install -m 0755 ${S}/scripts/recovery-set-dhcp.sh ${D}${bindir}
         install -m 0755 ${S}/scripts/recovery-set-static.sh ${D}${bindir}
+
+        # The GUI opens /dev/fb and /dev/input/{keyboard0,touchscreen0}
+        install -d ${D}${sysconfdir}/udev/rules.d
+        install -m 0644 ${UNPACKDIR}/90-recovery-gui.rules ${D}${sysconfdir}/udev/rules.d/
 }
 
+FILES:${PN} += "${sysconfdir}/udev/rules.d/90-recovery-gui.rules"
+
 RDEPENDS:${PN}:append = " \
 	swupdate \
 	"

+ 60 - 0
recipes-support/rescuegui/swupdategui-rs-crates.inc

@@ -0,0 +1,60 @@
+# Autogenerated with 'bitbake -c update_crates ${PN}'
+
+# from Cargo.lock
+SRC_URI += " \
+    crate://crates.io/aho-corasick/1.1.5 \
+    crate://crates.io/bindgen/0.72.1 \
+    crate://crates.io/bitflags/2.13.2 \
+    crate://crates.io/cexpr/0.6.0 \
+    crate://crates.io/cfg-if/1.0.5 \
+    crate://crates.io/clang-sys/1.9.1 \
+    crate://crates.io/either/1.19.0 \
+    crate://crates.io/glob/0.3.4 \
+    crate://crates.io/itertools/0.13.0 \
+    crate://crates.io/libc/0.2.190 \
+    crate://crates.io/libloading/0.8.9 \
+    crate://crates.io/log/0.4.34 \
+    crate://crates.io/memchr/2.8.3 \
+    crate://crates.io/minimal-lexical/0.2.1 \
+    crate://crates.io/nom/7.1.3 \
+    crate://crates.io/pkg-config/0.3.34 \
+    crate://crates.io/prettyplease/0.2.37 \
+    crate://crates.io/proc-macro2/1.0.107 \
+    crate://crates.io/quote/1.0.47 \
+    crate://crates.io/regex/1.13.1 \
+    crate://crates.io/regex-automata/0.4.18 \
+    crate://crates.io/regex-syntax/0.8.11 \
+    crate://crates.io/rustc-hash/2.1.3 \
+    crate://crates.io/shlex/1.3.0 \
+    crate://crates.io/syn/2.0.119 \
+    crate://crates.io/unicode-ident/1.0.27 \
+    crate://crates.io/windows-link/0.2.1 \
+"
+
+SRC_URI[aho-corasick-1.1.5.sha256sum] = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba"
+SRC_URI[bindgen-0.72.1.sha256sum] = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895"
+SRC_URI[bitflags-2.13.2.sha256sum] = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06"
+SRC_URI[cexpr-0.6.0.sha256sum] = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
+SRC_URI[cfg-if-1.0.5.sha256sum] = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600"
+SRC_URI[clang-sys-1.9.1.sha256sum] = "157a8ba7b480713b56f4c09fd13fc3e0a22a5dfab8097ba61cbc5feef950788a"
+SRC_URI[either-1.19.0.sha256sum] = "0e9c71c2167ca323c882b99918929403426e2373ea17242ff5653e0d5e1058be"
+SRC_URI[glob-0.3.4.sha256sum] = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b"
+SRC_URI[itertools-0.13.0.sha256sum] = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
+SRC_URI[libc-0.2.190.sha256sum] = "ce5d3ddc6d3fa000eb1536d85e147bfe31aacaba692ed6a876f95cb7c855be78"
+SRC_URI[libloading-0.8.9.sha256sum] = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55"
+SRC_URI[log-0.4.34.sha256sum] = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
+SRC_URI[memchr-2.8.3.sha256sum] = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
+SRC_URI[minimal-lexical-0.2.1.sha256sum] = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
+SRC_URI[nom-7.1.3.sha256sum] = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
+SRC_URI[pkg-config-0.3.34.sha256sum] = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548"
+SRC_URI[prettyplease-0.2.37.sha256sum] = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
+SRC_URI[proc-macro2-1.0.107.sha256sum] = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
+SRC_URI[quote-1.0.47.sha256sum] = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
+SRC_URI[regex-1.13.1.sha256sum] = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d"
+SRC_URI[regex-automata-0.4.18.sha256sum] = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2"
+SRC_URI[regex-syntax-0.8.11.sha256sum] = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
+SRC_URI[rustc-hash-2.1.3.sha256sum] = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d"
+SRC_URI[shlex-1.3.0.sha256sum] = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
+SRC_URI[syn-2.0.119.sha256sum] = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
+SRC_URI[unicode-ident-1.0.27.sha256sum] = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563"
+SRC_URI[windows-link-0.2.1.sha256sum] = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"

+ 16 - 0
recipes-support/rescuegui/swupdategui-rs/swupdate-gui.service

@@ -0,0 +1,16 @@
+[Unit]
+Description=SWUpdate Light GUI
+After=swupdate.service
+
+[Service]
+Type=simple
+ExecStartPre=-/bin/sh -c "echo 0 > /sys/class/graphics/fbcon/cursor_blink"
+# The GUI owns the framebuffer: detach the framebuffer console, otherwise
+# kernel messages (e.g. partition rescans during an update) are drawn over it
+ExecStartPre=-/bin/sh -c "for v in /sys/class/vtconsole/vtcon*; do grep -q 'frame buffer' $$v/name && echo 0 > $$v/bind; done"
+ExecStart=/usr/bin/recovery_gui
+ExecStopPost=-/bin/sh -c "for v in /sys/class/vtconsole/vtcon*; do grep -q 'frame buffer' $$v/name && echo 1 > $$v/bind; done"
+Restart=on-failure
+
+[Install]
+WantedBy=multi-user.target

+ 56 - 0
recipes-support/rescuegui/swupdategui-rs_git.bb

@@ -0,0 +1,56 @@
+SUMMARY = "Recovery GUI application (Rust)"
+DESCRIPTION = "GUI for the recovery system, written in Rust. It works with LVGL \
+and the framebuffer and talks to SWUpdate via libswupdate. It is a drop-in \
+replacement for swupdate-gui."
+HOMEPAGE = "https://github.com/sbabic/SWUpdateGUI"
+
+LICENSE = "GPL-2.0-or-later"
+LIC_FILES_CHKSUM = "file://LICENSE;md5=de4b1111cc7f3c8bc81546de6f9b24e4"
+
+SRC_URI = "git://github.com/sbabic/SWUpdateGUI.git;protocol=https;branch=rust-port \
+	file://swupdate-gui.service \
+	file://90-recovery-gui.rules \
+	"
+SRCREV = "e904a5768fba63dc484d2fdc948ae10772406af9"
+
+PV = "1.0.0+git"
+
+include swupdategui-rs-crates.inc
+
+inherit cargo cargo-update-recipe-crates pkgconfig systemd
+
+# Bindings for LVGL and libswupdate are generated by bindgen, which needs libclang.
+# LVGL is found with pkg-config, libswupdate in the target sysroot.
+DEPENDS += "clang-native lvgl swupdate"
+
+export BINDGEN_EXTRA_CLANG_ARGS = "--sysroot=${STAGING_DIR_HOST} -I${STAGING_INCDIR}"
+export SWUPDATE_INCLUDE_DIR = "${STAGING_INCDIR}"
+export SWUPDATE_LIB_DIR = "${STAGING_LIBDIR}"
+
+SYSTEMD_SERVICE:${PN} = "swupdate-gui.service"
+
+do_install:append() {
+	install -d ${D}${systemd_system_unitdir}
+	install -m 0644 ${UNPACKDIR}/swupdate-gui.service ${D}${systemd_system_unitdir}
+
+	install -d ${D}${sysconfdir}/recovery_gui
+	install -m 0644 ${S}/config/config.txt ${D}${sysconfdir}/recovery_gui/
+
+	install -d ${D}${bindir}
+	for script in ${S}/scripts/recovery-*.sh; do
+		install -m 0755 ${script} ${D}${bindir}
+	done
+
+	# The GUI opens /dev/fb and /dev/input/{keyboard0,touchscreen0}
+	install -d ${D}${sysconfdir}/udev/rules.d
+	install -m 0644 ${UNPACKDIR}/90-recovery-gui.rules ${D}${sysconfdir}/udev/rules.d/
+}
+
+FILES:${PN} += "${sysconfdir}/udev/rules.d/90-recovery-gui.rules"
+
+# Same files as the C implementation
+RPROVIDES:${PN} = "swupdate-gui"
+RREPLACES:${PN} = "swupdate-gui"
+RCONFLICTS:${PN} = "swupdate-gui"
+
+RDEPENDS:${PN} = "swupdate"