Trying WPE Platform API on Raspberry Pi
WPE Platform API (also known as “new API”) is a redesigned, GObject-based platform-integration layer for WPE WebKit that replaces the older libwpe backend model. A few months ago, Kate and
Simon published two closely related blog posts about it. The first one focuses more on the API and browser
implementation details, while the second one focuses more on writing and integrating the browser within Linux distribution.
This article builds on top of the above ones, and showcases how to build and try a minimal WPE browser using WPE Platform API on Raspberry Pi. Moreover, as the WPE Platform API still evolves to some degree, this article
also explains how to use and stick to the latest WPE WebKit from main branch. This way one can play with all the latest features straight on embedded hardware.
Before going further, one should be aware that in case of a simple release build (instead of one using latest main branch) it’s better to follow official instructions instead of this article.
Setup #
This article focuses on a certain setup using Raspberry Pi 3B but it should be fairly easy to adapt the config to any other Raspberry Pi model.
As for the work environment: the Linux-based host with ability to run containers was used along with WebKit Container SDK. The SDK version was precisely 2.53-v6-d535e88 as it uses
Ubuntu 24.04.4 LTS that works well with Yocto scarthgap.
The Yocto scarthgap has been used to increase the chances that the config and commands demonstrated in this article will remain buildable for many years to follow.
Preparing the image #
The preparation of the image starts with a series of commands that create a main directory and clone important Yocto repositories along with some meta layer repositories. At this point already, it’s important to have the working directory shared between host and SDK.
# host
mkdir wpe-upstream
cd wpe-upstream
git clone https://git.yoctoproject.org/git/poky -b scarthgap
git clone git@github.com:openembedded/meta-openembedded.git -b scarthgap
git clone https://git.yoctoproject.org/git/meta-raspberrypi -b scarthgap
git clone https://github.com/Igalia/meta-webkit -b scarthgap
source poky/oe-init-build-env build
Once the build directory is created, it’s necessary to configure the meta layers in the build/conf/bblayers.conf file the following way:
# POKY_BBLAYERS_CONF_VERSION is increased each time build/conf/bblayers.conf
# changes incompatibly
POKY_BBLAYERS_CONF_VERSION = "2"
BBPATH = "${TOPDIR}"
BSPDIR := "${@os.path.abspath(os.path.dirname(d.getVar('FILE', True)) + '/../..')}"
BBFILES ?= ""
BBLAYERS ?= " \
${BSPDIR}/poky/meta \
${BSPDIR}/poky/meta-poky \
${BSPDIR}/poky/meta-yocto-bsp \
${BSPDIR}/meta-openembedded/meta-oe \
${BSPDIR}/meta-openembedded/meta-multimedia \
${BSPDIR}/meta-openembedded/meta-python \
${BSPDIR}/meta-raspberrypi \
${BSPDIR}/meta-webkit \
"
With the above, the recipes from the meta layers cloned earlier will be considered by bitbake.
Next, the most important configuration step is appending the following to build/conf/local.conf:
MACHINE = "raspberrypi3-64"
MACHINE_FEATURES:append = " vc4graphics"
GPU_MEM_256 = "128"
GPU_MEM_512 = "196"
GPU_MEM_1024 = "396"
DISTRO_FEATURES:append = " opengl egl wayland"
EXTRA_IMAGE_FEATURES = "debug-tweaks"
IMAGE_FEATURES:append = " ssh-server-dropbear hwcodecs"
IMAGE_INSTALL:append = " wpewebkit wpe-browser"
PREFERRED_VERSION_wpewebkit = "latest"
LICENSE_FLAGS_ACCEPTED = "synaptics-killswitch"
With that, wpewebkit latest will be preferred and installed in the image along with a dummy browser called wpe-browser.
To make the wpewebkit latest work, one needs to create meta-webkit/recipes-browser/wpewebkit/wpewebkit_latest.bb:
SUMMARY = "Lightweight WebKit port for embedded devices with OpenGL-ES acceleration"
DESCRIPTION = "WPE WebKit port pairs the WebKit engine with OpenGL-ES (OpenGL for Embedded Systems), \
allowing embedders to create simple and performant systems based on Web platform technologies. \
It is designed with hardware acceleration in mind, relying on EGL, and OpenGL ES."
HOMEPAGE = "https://wpewebkit.org/"
BUGTRACKER = "https://bugs.webkit.org/"
LICENSE = "BSD-2-Clause & LGPL-2.0-or-later"
LIC_FILES_CHKSUM = "file://Source/WebCore/LICENSE-LGPL-2.1;md5=a778a33ef338abbaf8b8a7c36b6eec80 "
REQUIRED_DISTRO_FEATURES = "opengl"
DEPENDS:append = " \
libsoup \
bison-native gperf-native harfbuzz-native libxml2-native ccache-native ninja-native ruby-native \
fontconfig freetype glib-2.0 harfbuzz icu jpeg pcre sqlite3 zlib libpng libtasn1 \
libwebp libxml2 libxslt virtual/egl virtual/libgles2 libepoxy libgcrypt \
unifdef-native \
"
inherit cmake features_check pkgconfig perlnative python3native
export WK_USE_CCACHE = "NO"
PACKAGECONFIG ??= "accessibility avif dfg-jit gbm gpu-process \
jit jpegxl libbacktrace \
mediasource mediastream \
remote-inspector \
sysprof \
${@' system-sysprof' \
if bb.utils.contains('BBFILE_COLLECTIONS', 'meta-gnome', True, False, d) \
else '' } \
unified-builds video webaudio woff2 wpe-platform \
${@bb.utils.contains('DISTRO_FEATURES', 'systemd', 'journald', '' ,d)} \
"
PACKAGECONFIG[reduce-size] = "-DCMAKE_BUILD_TYPE=MinSizeRel,-DCMAKE_BUILD_TYPE=Release,,"
PACKAGECONFIG[release-with-debug-info] = "-DCMAKE_BUILD_TYPE=RelWithDebInfo,-DCMAKE_BUILD_TYPE=Release,,"
# WPE features
PACKAGECONFIG[accessibility] = "-DUSE_ATK=ON,-DUSE_ATK=OFF,atk at-spi2-atk"
PACKAGECONFIG[avif] = "-DUSE_AVIF=ON,-DUSE_AVIF=OFF,libavif"
PACKAGECONFIG[bubblewrap] = "-DENABLE_BUBBLEWRAP_SANDBOX=ON -DBWRAP_EXECUTABLE=${bindir}/bwrap -DDBUS_PROXY_EXECUTABLE=${bindir}/xdg-dbus-proxy,-DENABLE_BUBBLEWRAP_SANDBOX=OFF,bubblewrap xdg-dbus-proxy libseccomp"
PACKAGECONFIG[developer-mode] = "-DDEVELOPER_MODE=ON,-DDEVELOPER_MODE=OFF,wayland-native wayland-protocols wpebackend-fdo"
PACKAGECONFIG[deviceorientation] = "-DENABLE_DEVICE_ORIENTATION=ON,-DENABLE_DEVICE_ORIENTATION=OFF,"
PACKAGECONFIG[dfg-jit] = "-DENABLE_DFG_JIT=ON,-DENABLE_DFG_JIT=OFF,"
PACKAGECONFIG[documentation] = "-DENABLE_DOCUMENTATION=ON,-DENABLE_DOCUMENTATION=OFF, gi-docgen-native gi-docgen"
PACKAGECONFIG[encryptedmedia] = "-DENABLE_ENCRYPTED_MEDIA=ON,-DENABLE_ENCRYPTED_MEDIA=OFF,libgcrypt"
PACKAGECONFIG[experimental-features] = "-DENABLE_EXPERIMENTAL_FEATURES=ON,-DENABLE_EXPERIMENTAL_FEATURES=OFF,libavif libjxl"
PACKAGECONFIG[gamepad] = "-DENABLE_GAMEPAD=ON,-DENABLE_GAMEPAD=OFF,libmanette"
PACKAGECONFIG[gbm] = "-DUSE_GBM=ON,-DUSE_GBM=OFF,libdrm"
PACKAGECONFIG[geolocation] = "-DENABLE_GEOLOCATION=ON,-DENABLE_GEOLOCATION=OFF,geoclue"
PACKAGECONFIG[gpu-process] = "-DENABLE_GPU_PROCESS=ON,-DENABLE_GPU_PROCESS=OFF,"
PACKAGECONFIG[hyphen] = "-DUSE_LIBHYPHEN=ON,-DUSE_LIBHYPHEN=OFF,hyphen"
PACKAGECONFIG[introspection] = "-DENABLE_INTROSPECTION=ON,-DENABLE_INTROSPECTION=OFF, gobject-introspection-native"
PACKAGECONFIG[jit] = "-DENABLE_JIT=ON -DENABLE_C_LOOP=OFF,-DENABLE_JIT=OFF -DENABLE_C_LOOP=ON,"
PACKAGECONFIG[jpegxl] = "-DUSE_JPEGXL=ON,-DUSE_JPEGXL=OFF,libjxl"
PACKAGECONFIG[journald] = "-DENABLE_JOURNALD_LOG=ON,-DENABLE_JOURNALD_LOG=OFF,"
PACKAGECONFIG[lcms] = "-DUSE_LCMS=ON,-DUSE_LCMS=OFF,"
PACKAGECONFIG[spellcheck] = "-DENABLE_SPELLCHECK=ON,-DENABLE_SPELLCHECK=OFF,enchant"
PACKAGECONFIG[wpe-legacy-api] = "-DENABLE_WPE_LEGACY_API=ON,-DENABLE_WPE_LEGACY_API=OFF,libwpe virtual/wpebackend,"
PACKAGECONFIG[libbacktrace] = "-DUSE_LIBBACKTRACE=ON,-DUSE_LIBBACKTRACE=OFF,libbacktrace"
PACKAGECONFIG[minibrowser] = "-DENABLE_MINIBROWSER=ON,-DENABLE_MINIBROWSER=OFF,wayland-native wayland-protocols wpebackend-fdo"
PACKAGECONFIG[mediasource] = "-DENABLE_MEDIA_SOURCE=ON,-DENABLE_MEDIA_SOURCE=OFF,gstreamer1.0 gstreamer1.0-plugins-good"
PACKAGECONFIG[mediastream] = "-DENABLE_MEDIA_STREAM=ON,-DENABLE_MEDIA_STREAM=OFF,gstreamer1.0 gstreamer1.0-plugins-bad"
PACKAGECONFIG[pdfjs] = "-DENABLE_PDFJS=ON,-DENABLE_PDFJS=OFF,"
PACKAGECONFIG[speech-synthesis] = "-DENABLE_SPEECH_SYNTHESIS=ON,-DENABLE_SPEECH_SYNTHESIS=OFF,flite"
PACKAGECONFIG[sysprof] = "-DUSE_SYSPROF_CAPTURE=ON, -DUSE_SYSPROF_CAPTURE=OFF,"
PACKAGECONFIG[system-sysprof] = "-DUSE_SYSTEM_SYSPROF_CAPTURE=ON, -DUSE_SYSTEM_SYSPROF_CAPTURE=OFF, sysprof"
PACKAGECONFIG[video] = "-DENABLE_VIDEO=ON,-DENABLE_VIDEO=OFF,gstreamer1.0 gstreamer1.0-plugins-base"
PACKAGECONFIG[webaudio] = "-DENABLE_WEB_AUDIO=ON,-DENABLE_WEB_AUDIO=OFF,gstreamer1.0 gstreamer1.0-plugins-base gstreamer1.0-plugins-good"
PACKAGECONFIG[woff2] = "-DUSE_WOFF2=ON,-DUSE_WOFF2=OFF,woff2"
PACKAGECONFIG[remote-inspector] = "-DENABLE_REMOTE_INSPECTOR=ON,-DENABLE_REMOTE_INSPECTOR=OFF,"
PACKAGECONFIG[webrtc] = "-DENABLE_WEB_RTC=ON,-DENABLE_WEB_RTC=OFF,libvpx libevent libopus openh264"
PACKAGECONFIG[qtwpe] = "-DENABLE_WPE_QT_API=ON ${CMAKE_QT_OECONF},-DENABLE_WPE_QT_API=OFF,qtbase-native qtbase qtdeclarative libepoxy wpebackend-fdo ${QT_BUILD_DEPS}"
PACKAGECONFIG[unified-builds] = "-DENABLE_UNIFIED_BUILDS=ON,-DENABLE_UNIFIED_BUILDS=OFF,"
PACKAGECONFIG[thunder] = "-DENABLE_THUNDER=ON,-DENABLE_THUNDER=OFF,virtual/open-cdm"
PACKAGECONFIG[webxr] = "-DENABLE_WEBXR=ON,-DENABLE_WEBXR=OFF,openxr"
# Build option for WPE API 1.1
PACKAGECONFIG[wpe-1-1-api] = "-DENABLE_WPE_1_1_API:BOOL=ON,-DENABLE_WPE_1_1_API:BOOL=OFF,"
# Build option for WPE platform API
PACKAGECONFIG[wpe-platform] = "-DENABLE_WPE_PLATFORM=ON,-DENABLE_WPE_PLATFORM=OFF,libinput libxkbcommon wayland-native"
EXTRA_OECMAKE = " -DPORT=WPE -G Ninja"
# TODO: documentation and introspection are disabled by default because the are
# causing cross-compiling build errors
# PACKAGECONFIG:append = " ${@bb.utils.contains('DISTRO_FEATURES', 'api-documentation', 'documentation', '' ,d)} introspection"
# If SSE code compiles, assume it runs successfully (it can't actually run
# because of cross compiling)
EXTRA_OECMAKE:append:x86 = " -DHAVE_SSE2_EXTENSIONS_EXITCODE=0"
# Javascript JIT is not supported on ppc/arm/RISCV32/mips64
PACKAGECONFIG:remove:powerpc = "jit"
PACKAGECONFIG:remove:powerpc64 = "jit"
PACKAGECONFIG:remove:powerpc64le = "jit"
PACKAGECONFIG:remove:armv4 = "jit"
PACKAGECONFIG:remove:armv5 = "jit"
PACKAGECONFIG:remove:armv6 = "jit"
PACKAGECONFIG:remove:armv7a = "jit"
PACKAGECONFIG:remove:armv7ve = "jit"
PACKAGECONFIG:remove:riscv32 = "jit"
PACKAGECONFIG:remove:riscv64 = "jit"
PACKAGECONFIG:remove:mipsarchn64 = "jit"
PACKAGECONFIG:remove:mipsarchn32 = "jit"
PACKAGECONFIG:remove:loongarch64 = "jit"
# Javascript JIT is not supported on x86
PACKAGECONFIG:remove:x86 = "jit"
LDFLAGS:append:riscv64 = " -pthread"
FULL_OPTIMIZATION:remove = "-g"
LEAD_SONAME = "libWPEWebKit.so"
PACKAGES =+ "${PN}-web-inspector-plugin ${PN}-qtwpe-qml-plugin"
FILES:${PN} += "${libdir}/wpe-webkit*/injected-bundle/libWPEInjectedBundle.so"
FILES:${PN}-web-inspector-plugin += "${datadir}/wpe-webkit-*/inspector.gresource"
# nooelint: oelint.vars.insaneskip - ignored for convenience. We need to recheck if problem persist
INSANE_SKIP:${PN}-web-inspector-plugin = "dev-so"
# nooelint: oelint.vars.insaneskip - ignored for convenience. We need to recheck if problem persist
INSANE_SKIP:${PN}-qtwpe-qml-plugin = "dev-so"
# JSC JIT on ARMv7 is better supported with Thumb2 instruction set.
ARM_INSTRUCTION_SET:armv7a = "thumb"
ARM_INSTRUCTION_SET:armv7r = "thumb"
ARM_INSTRUCTION_SET:armv7m = "thumb"
ARM_INSTRUCTION_SET:armv7ve = "thumb"
# Extra runtime depends
# nooelint: oelint.vars.dependsordered - ignored for convenience
RDEPENDS:${PN} += "\
${@bb.utils.contains('PACKAGECONFIG', 'remote-inspector', '${PN}-web-inspector-plugin', '', d)} \
${@bb.utils.contains('PACKAGECONFIG', 'gst_gl', 'gstreamer1.0-plugins-base-opengl', '', d)} \
${@bb.utils.contains('PACKAGECONFIG', 'mediasource', 'gstreamer1.0-plugins-good-isomp4', '', d)} \
${@bb.utils.contains('PACKAGECONFIG', 'webaudio', 'gstreamer1.0-plugins-good-wavparse', '', d)} \
${@bb.utils.contains('PACKAGECONFIG', 'video', 'gstreamer1.0-plugins-base-app \
gstreamer1.0-plugins-base-audioconvert \
gstreamer1.0-plugins-base-audioresample \
gstreamer1.0-plugins-base-gio \
gstreamer1.0-plugins-base-playback \
gstreamer1.0-plugins-base-typefindfunctions \
gstreamer1.0-plugins-base-videoconvertscale \
gstreamer1.0-plugins-base-volume \
gstreamer1.0-plugins-good-audiofx \
gstreamer1.0-plugins-good-audioparsers \
gstreamer1.0-plugins-good-autodetect \
gstreamer1.0-plugins-good-avi \
gstreamer1.0-plugins-good-deinterlace \
gstreamer1.0-plugins-good-interleave \
', '', d)} \
libgles2 \
"
RDEPENDS:${PN}-web-inspector-plugin += "\
shared-mime-info \
"
# Extra runtime recommends
RRECOMMENDS:${PN} += "\
ca-certificates \
ttf-dejavu-sans \
ttf-dejavu-sans-mono \
ttf-dejavu-serif \
${PN}-qtwpe-qml-plugin \
${@bb.utils.contains('PACKAGECONFIG', 'video', 'gstreamer1.0-plugins-base-meta gstreamer1.0-plugins-good-meta gstreamer1.0-plugins-bad-meta', '', d)} \
"
DEFAULT_PREFERENCE = "-1"
FILESEXTRAPATHS:prepend := "${THISDIR}/${PN}:"
# https://commits.webkit.org/319279@main
PR = "r319279"
SRCREV = "93472ec12ee947b30c7ec3c176ca0e2fb6ba6ace"
SRC_URI = "git://github.com/WebKit/WebKit.git;protocol=https;branch=main \
file://0001-libpas-Only-include-stdatomic.h-when-compiling-with.patch \
file://0002-WebDriver-Guard-LOG_CHANNEL-check-with-LOG_DISABLED.patch \
"
S = "${WORKDIR}/git"
This file is self-contained on purpose; the idea is not to rely on any includes so that unpredictable behavior doesn’t happen in the future.
While the above file contains a lot of interesting details, the most interesting practical part is the last few lines. The SRCREV is set to point to the latest commit from the WebKit main branch (at the time
of writing). Along with that comes SRC_URI that specifies two patches required to make WPE WebKit compile.
The problem with the patches is that advancing SRCREV will likely make them unusable due to merge conflicts. Therefore, when advancing the revision, it’s recommended to remove patches from SRC_URI and face the compilation problems from scratch as
it’s very likely there will be new compilation problems anyway. Fortunately, nowadays LLMs can be used to fix any compilation problems by preparing custom patches just like the below ones (created by LLM as well). For example, most of the modern
models should manage to prepare proper patches just by pointing them to the above recipe and the temp directory with the latest logs from build commands.
Assuming one uses the wpewebkit_latest.bb above, the first patch needs to be created in: meta-webkit/recipes-browser/wpewebkit/wpewebkit/0001-libpas-Only-include-stdatomic.h-when-compiling-with.patch with the following content:
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Pawel Lampe <plampe@igalia.com>
Date: Mon, 17 Aug 2026 00:00:00 +0000
Subject: [PATCH] [libpas] Only include stdatomic.h when compiling with Clang
Some libpas .c files (e.g. jit_heap.c) are compiled as C++ via
set_source_files_properties(... PROPERTIES LANGUAGE CXX) in
Source/bmalloc/CMakeLists.txt, for TZone heap support. pas_utils.h
unconditionally does `#include <stdatomic.h>`, but GCC's own
<stdatomic.h> relies on the C11-only `_Atomic` keyword and has no
support for being included from C++ translation units (this was only
addressed in much newer GCC releases). Compiling any of the
CXX-tagged libpas .c files with GCC 13 therefore fails with:
error: '_Atomic' does not name a type
The header is only actually needed here for the Clang-specific
__c11_atomic_* intrinsics guarded by `#elif PAS_COMPILER(CLANG)`
further down in this file; the non-Clang (GCC) path uses the
__atomic_* builtins instead and does not need any of the types or
macros from <stdatomic.h>. Guard the include accordingly so GCC
builds (both plain C and the CXX-tagged libpas sources) are
unaffected by GCC's non-C++-aware <stdatomic.h>.
Upstream-Status: Pending
Signed-off-by: Pawel Lampe <plampe@igalia.com>
---
Source/bmalloc/libpas/src/libpas/pas_utils.h | 8 ++++++++
1 file changed, 8 insertions(+)
diff --git a/Source/bmalloc/libpas/src/libpas/pas_utils.h b/Source/bmalloc/libpas/src/libpas/pas_utils.h
index 962634080930..9b98fda54f43 100644
--- a/Source/bmalloc/libpas/src/libpas/pas_utils.h
+++ b/Source/bmalloc/libpas/src/libpas/pas_utils.h
@@ -42,7 +42,15 @@
#endif
#include <limits.h>
+#if PAS_COMPILER(CLANG)
+/* GCC's <stdatomic.h> relies on the C-only _Atomic keyword and is not
+ * usable when this header is included from a translation unit compiled
+ * as C++ (some libpas .c files are compiled as C++, see bmalloc's
+ * CMakeLists.txt). It is only needed here for the Clang-specific
+ * __c11_atomic_* intrinsics below; the GCC path uses __atomic_* builtins
+ * instead. */
#include <stdatomic.h>
+#endif
#include <stdbool.h>
#include <stdint.h>
#include <string.h>
--
2.43.0
The second patch should be: meta-webkit/recipes-browser/wpewebkit/wpewebkit/0002-WebDriver-Guard-LOG_CHANNEL-check-with-LOG_DISABLED.patch with:
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Pawel Lampe <plampe@igalia.com>
Date: Mon, 17 Aug 2026 00:00:00 +0000
Subject: [PATCH] [WebDriver] Guard LOG_CHANNEL check with
LOG_DISABLED/RELEASE_LOG_DISABLED
WebDriverService::handleRequest() unconditionally checks
LOG_CHANNEL(WebDriverClassic).state to decide whether it is worth
building the request/response log strings. However,
Source/WebDriver/Logging.h only declares the WebDriverClassic (and
other WebDriver) log channels inside:
#if !LOG_DISABLED || !RELEASE_LOG_DISABLED
On a release build (NDEBUG, so LOG_DISABLED is true) without journald
support and without OS_LOG/Android (so RELEASE_LOG_DISABLED is also
true) - the common configuration for an embedded Linux build without
the "journald" PACKAGECONFIG - that guard is false, so the channel is
never declared, and this direct, unguarded use of LOG_CHANNEL() fails
to compile:
error: 'LOG_CHANNEL_PREFIXWebDriverClassic' was not declared in this scope
RELEASE_LOG_INFO() itself already collapses to a no-op in that
configuration (see wtf/Assertions.h), so guard this manual
LOG_CHANNEL() state check with the same condition used to declare the
channel in Logging.h.
Upstream-Status: Pending
Signed-off-by: Pawel Lampe <plampe@igalia.com>
---
Source/WebDriver/WebDriverService.cpp | 2 ++
1 file changed, 2 insertions(+)
diff --git a/Source/WebDriver/WebDriverService.cpp b/Source/WebDriver/WebDriverService.cpp
index 07ae385f33f4..2612d239a611 100644
--- a/Source/WebDriver/WebDriverService.cpp
+++ b/Source/WebDriver/WebDriverService.cpp
@@ -381,6 +381,7 @@ bool WebDriverService::findCommand(HTTPMethod method, const String& path, Comman
void WebDriverService::handleRequest(HTTPRequestHandler::Request&& request, Function<void (HTTPRequestHandler::Response&&)>&& replyHandler)
{
Function<void (HTTPRequestHandler::Response&&)> actualReplyHandler = WTF::move(replyHandler);
+#if !LOG_DISABLED || !RELEASE_LOG_DISABLED
if (LOG_CHANNEL(WebDriverClassic).state != WTFLogChannelState::Off) {
RELEASE_LOG_INFO(WebDriverClassic, "HTTP request %s %s (body=%zu bytes)", request.method.utf8().data(), request.path.utf8().data(), request.dataLength);
actualReplyHandler = [startTime = MonotonicTime::now(), replyHandler = WTF::move(actualReplyHandler)](HTTPRequestHandler::Response&& response) mutable {
@@ -388,6 +389,7 @@ void WebDriverService::handleRequest(HTTPRequestHandler::Request&& request, Func
replyHandler(WTF::move(response));
};
}
+#endif
auto method = toCommandHTTPMethod(request.method);
if (!method) {
--
2.43.0
Once the patches are added, wpewebkit latest should build correctly. However, to make use of it one needs a browser.
To demonstrate how easy the browser for WPE WebKit with Platform API can be, a very minimalistic one will be prepared below.
The first step is to create a directory:
mkdir meta-webkit/recipes-browser/wpe-browser/
Then a file: meta-webkit/recipes-browser/wpe-browser/main.cpp that implements the whole browser:
#include <wpe/webkit.h>
int main(int argc, const char *argv[]) {
g_autoptr(GMainLoop) loop = g_main_loop_new(nullptr, false);
g_autoptr(WebKitWebView) view = WEBKIT_WEB_VIEW(g_object_new(WEBKIT_TYPE_WEB_VIEW,
nullptr));
webkit_web_view_load_uri(view,
(argc > 1) ? argv[1] : "https://wpewebkit.org");
g_main_loop_run(loop);
return EXIT_SUCCESS;
}
Then a file: meta-webkit/recipes-browser/wpe-browser/CMakeLists.txt that describes how to build it:
cmake_minimum_required(VERSION 3.16)
project(wpe-browser CXX)
set(CMAKE_CXX_STANDARD 17)
include(GNUInstallDirs)
find_package(PkgConfig REQUIRED)
# The Wayland WPE Platform already depends on wpe-platform-2.0
pkg_check_modules(WebKitDeps REQUIRED
IMPORTED_TARGET
wpe-webkit-2.0
wpe-platform-wayland-2.0
)
add_executable(wpe-browser main.cpp)
target_link_libraries(wpe-browser
PRIVATE
PkgConfig::WebKitDeps
)
install(TARGETS wpe-browser RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
and finally a recipe file: meta-webkit/recipes-browser/wpe-browser/wpe-browser_1.0.bb that allows bitbake to build the browser and install it in the image:
SUMMARY = "Minimal WPE WebKit browser launcher"
DESCRIPTION = "A minimal launcher built on the WPE Platform API, displaying \
a URL given as its only argument (defaults to https://wpewebkit.org). \
Based on https://simonpena.com/blog/2026/03/20/getting-started-with-wpe-webkit/"
HOMEPAGE = "https://simonpena.com/blog/2026/03/20/getting-started-with-wpe-webkit/"
LICENSE = "MIT"
LIC_FILES_CHKSUM = "file://${COMMON_LICENSE_DIR}/MIT;md5=0835ade698e0bcf8506ecda2f7b4f302"
FILESEXTRAPATHS:prepend := "${THISDIR}:"
SRC_URI = "file://main.cpp \
file://CMakeLists.txt \
"
S = "${WORKDIR}"
DEPENDS = "wpewebkit"
RDEPENDS:${PN} += "wpewebkit"
inherit cmake pkgconfig features_check
REQUIRED_DISTRO_FEATURES = "opengl wayland"
After all those steps, everything is ready to perform a build. This time the commands are executed in the SDK:
source poky/oe-init-build-env build
bitbake core-image-weston
cd tmp/deploy/images/raspberrypi3-64/
# flash to SD card based on preference
Trying the image #
Once the image is built and flashed to SD card, and the SD card has been used to boot the Raspberry Pi, one can SSH into it, basically by:
ssh root@<IP>
Then, the browser should work out of the box. The first command to try is the one that doesn’t need the network and therefore is the simplest:
XDG_RUNTIME_DIR=/run/user/1000 WAYLAND_DISPLAY=wayland-1 wpe-browser 'webkit://gpu/stdout'
If the network is available, it’s worth starting simple and loading some HTTP page:
XDG_RUNTIME_DIR=/run/user/1000 WAYLAND_DISPLAY=wayland-1 wpe-browser 'http://info.cern.ch'
If that works as well, one can try the HTTPS one:
XDG_RUNTIME_DIR=/run/user/1000 WAYLAND_DISPLAY=wayland-1 wpe-browser 'https://igalia.com'
If there’s an issue with certificates, it’s likely due to broken date/time, so the correct one needs to be set using a command like:
date -s '2026-09-02 22:34:56'
Conclusions #
Since the Platform API is the future of WPE, it’s worth using it already. As the above sections demonstrate, it hasn’t ever been easier to create a WPE-powered browser for embedded hardware. Moreover, nowadays as LLMs are at play,
using latest sources from main branch and quickly patching on demand is a possibility worth utilizing. With that, a broad sea of experimenting possibilities becomes wide open. However, it must not be forgotten that while main branch
is great for testing new web platform features implemented in WebKit, it’s not necessarily ideal for testing performance. In such case, it’s better to rely on releases and proper browser engine fine tuning for particular hardware one
plays with.
- Previous: WPE memory leak investigation playbook