From d36f7b3b7421440d9d2b42d3377904fda8a5ec26 Mon Sep 17 00:00:00 2001 From: Oliver Gassner Date: Tue, 13 Oct 2026 20:12:28 +0200 Subject: [PATCH 01/11] Explore Kobalt PocketBook port with pinned fork and compatibility probe --- .gitmodules | 3 + README.md | 4 + docs/research/kobalt-pocketbook-port.md | 130 ++++++++++++++++++++++++ external/kobalt | 1 + licenses/README.md | 8 ++ tools/kobalt-probe/CMakeLists.txt | 31 ++++++ tools/kobalt-probe/README.md | 56 ++++++++++ tools/kobalt-probe/main.cpp | 70 +++++++++++++ 8 files changed, 303 insertions(+) create mode 100644 .gitmodules create mode 100644 docs/research/kobalt-pocketbook-port.md create mode 160000 external/kobalt create mode 100644 tools/kobalt-probe/CMakeLists.txt create mode 100644 tools/kobalt-probe/README.md create mode 100644 tools/kobalt-probe/main.cpp diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..211fb77 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "external/kobalt"] + path = external/kobalt + url = https://github.com/42lizard/kobalt.git diff --git a/README.md b/README.md index 30d46e7..40ba4c7 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,10 @@ Other devices, including Verse Pro, need separate validation. ## Build +An isolated [Kobalt port experiment](tools/kobalt-probe/README.md) pins +`42lizard/kobalt` under `external/kobalt` and checks PocketBook build prerequisites. +It is not yet a working reader and is excluded from the normal app builds below. + With Docker and Compose running, from the repository root: ```sh diff --git a/docs/research/kobalt-pocketbook-port.md b/docs/research/kobalt-pocketbook-port.md new file mode 100644 index 0000000..8be3a7c --- /dev/null +++ b/docs/research/kobalt-pocketbook-port.md @@ -0,0 +1,130 @@ +# Kobalt on PocketBook: source feasibility + +Date: 2026-10-13. Reviewed source: +[`cbc0f4e9118c0e791056e36bc0b66d90e2af027c`](https://github.com/42lizard/kobalt/tree/cbc0f4e9118c0e791056e36bc0b66d90e2af027c). +This is source research, not evidence of a working PocketBook port. Build and +device results must be recorded separately. The initial target is the current +Qt 6 InkPad 4 environment; older PocketBook compatibility is not implied. + +## Assessment + +A standalone PocketBook port looks feasible without replacing the reader UI or +CREngine. The strongest evidence is the existing desktop host: it already uses +the shared Qt Widgets reader and Qt-only Android controller without Nickel or +the Android runtime. Shared controls support both Kobo Qt generations. This is +an engineering inference from the existing +[source boundaries](https://github.com/42lizard/kobalt/blob/cbc0f4e9118c0e791056e36bc0b66d90e2af027c/docs/development.md#shared-reader-controls) +and [desktop target](https://github.com/42lizard/kobalt/blob/cbc0f4e9118c0e791056e36bc0b66d90e2af027c/desktop/CMakeLists.txt). +Linux alone does not establish binary, firmware or display compatibility. + +The Kobo installation package is the wrong starting point. Its services depend +on Nickel keyboard, light, battery, lifecycle and presentation adapters. Its +refresh implementation uses Kobo Qt attributes, native symbol lookup and +backing-store flush hooks. Those mechanisms need a PocketBook counterpart. +[Kobo host](https://github.com/42lizard/kobalt/blob/cbc0f4e9118c0e791056e36bc0b66d90e2af027c/src/platform/nickel/common/reader_host_services.cpp), +[Kobo display](https://github.com/42lizard/kobalt/blob/cbc0f4e9118c0e791056e36bc0b66d90e2af027c/src/platform/nickel/qt5/refresh.cpp). + +## Reuse and adaptation boundaries + +- **Reader and engine:** preserve publication loading, rendering, shared Widgets + and reading state. The standalone controller already opens plain EPUB archives + and constructs the shared reader window. + [Controller](https://github.com/42lizard/kobalt/blob/cbc0f4e9118c0e791056e36bc0b66d90e2af027c/src/platform/android/reader.cpp). +- **Host services:** implement the existing callbacks for clock, battery, page + buttons, fonts, suspend state, frontlight, dictionary, keyboard, presentation + and browser. Unsupported services still need valid callbacks/factories; + `complete()` checks their presence. Desktop provides examples. + [Interface](https://github.com/42lizard/kobalt/blob/cbc0f4e9118c0e791056e36bc0b66d90e2af027c/src/platform/api/reader_host_services.h), + [desktop services](https://github.com/42lizard/kobalt/blob/cbc0f4e9118c0e791056e36bc0b66d90e2af027c/src/platform/desktop/services.cpp). +- **Display:** supply `textRefresh`, `pageRefresh`, and + `setReaderRefreshWindow`. Desktop's implementations are no-ops; host rendering + therefore cannot establish actual e-ink refresh quality. Device testing must + check page turns, menu cleanup and repaint completion. + [Refresh API](https://github.com/42lizard/kobalt/blob/cbc0f4e9118c0e791056e36bc0b66d90e2af027c/src/platform/api/kobo_refresh.h), + [desktop adapter](https://github.com/42lizard/kobalt/blob/cbc0f4e9118c0e791056e36bc0b66d90e2af027c/src/platform/desktop/display.cpp). +- **Input:** the existing canvas converts touch to mouse events for embedded + Widgets; host services map page keys. Verify PocketBook's delivered events + before choosing a direct full-screen QWidget or reusing the canvas, avoiding + duplicate translation. The canvas uses Qt 6 touch APIs and is not a drop-in + Qt 5 host. + [Canvas](https://github.com/42lizard/kobalt/blob/cbc0f4e9118c0e791056e36bc0b66d90e2af027c/src/platform/android/canvas.h). +- **Rotation:** the standalone controller passes a null sensor and host rotation + callback. Automatic rotation is optional at runtime, but the shared + `ReaderOrientation` still includes/calls Qt Sensors and inherits + `QOrientationFilter`. Simply omitting the Sensors link is insufficient. If + the SDK lacks it, make sensor support optional in this narrow module. + [Header](https://github.com/42lizard/kobalt/blob/cbc0f4e9118c0e791056e36bc0b66d90e2af027c/src/platform/api/reader_orientation.h), + [implementation](https://github.com/42lizard/kobalt/blob/cbc0f4e9118c0e791056e36bc0b66d90e2af027c/src/platform/api/reader_orientation.cpp). +- **Local progress:** keep independent Kobalt state initially. `ReadingBook` + supports a null native bridge, saves local checkpoints, and skips identical + checkpoint writes. Hosts choose directories. Use a dedicated document cache: + CREngine deletes unrelated files found in that directory. + [Book state](https://github.com/42lizard/kobalt/blob/cbc0f4e9118c0e791056e36bc0b66d90e2af027c/src/reading/reading_book.cpp), + [storage paths](https://github.com/42lizard/kobalt/blob/cbc0f4e9118c0e791056e36bc0b66d90e2af027c/src/storage/reader_storage.h). +- **Native progress and Readest:** `ReadingBridge` models a platform database + and activity recorder, while `PositionCodec` maps engine/platform positions. + These are extension points, not proof of compatible position formats. A + local-only port will not automatically participate in Readest Sync. + [Bridge](https://github.com/42lizard/kobalt/blob/cbc0f4e9118c0e791056e36bc0b66d90e2af027c/src/reading/reading_bridge.h), + [codec](https://github.com/42lizard/kobalt/blob/cbc0f4e9118c0e791056e36bc0b66d90e2af027c/src/platform/api/position_codec.h). +- **Launch and assets:** adapt desktop bundle paths to PocketBook directories. + Desktop initializes bundled fonts/assets and accepts a book path as its first + argument. Reuse these responsibilities; a new library UI is unnecessary for + the first experiment. + [Desktop entry point](https://github.com/42lizard/kobalt/blob/cbc0f4e9118c0e791056e36bc0b66d90e2af027c/src/platform/desktop/main.cpp). + +## Proposed minimal stages + +1. Pin the user's fork as a submodule. Keep Kobalt platform changes in that fork + and PocketBook build/package integration in this repository. Preserve shared + renderer/UI sources, dependency pins, patches and notices. +2. Cross-build a standalone target around the existing controller and a small + PocketBook host. Start with one explicit DRM-free EPUB path, independent + state/cache directories, no native database bridge and no page animations. +3. Validate shared reader behavior on the host, then on InkPad 4: visible page, + touch/page keys, menus, checkpoint/reopen, Home/exit, suspend/wake and USB + lifecycle. Hardware must establish refresh quality, memory use and power + behavior. +4. Add device services incrementally. Library/default-reader registration, + native positions and Readest sync are later capabilities with separate tests. + +These are proposed stages, not completed implementation. Upstream also +explicitly distinguishes portable tests from firmware integration checks in its +[test documentation](https://github.com/42lizard/kobalt/blob/cbc0f4e9118c0e791056e36bc0b66d90e2af027c/docs/development.md#tests). + +## Initial local evidence + +The fork is pinned, unmodified, under `external/kobalt` on the separate +`experiment/kobalt-pocketbook` branch. The reproducible +[compatibility probe](../../tools/kobalt-probe/README.md) links Kobalt's publication +component, Qt Widgets and InkView with the existing PocketBook toolchain. + +On 2026-10-13 the ARM build and existing ABI check passed: ARM softfp, +`/lib/ld-linux.so.3`, no runtime search path, and the existing firmware +symbol-version ceilings. This establishes compilation/linkage of that component, +not runtime compatibility of the full reader. No binary was installed on a device. + +The host CTest also passed: an EPUB from Kobalt's fixture generator was inspected, +compressed chapter text read, invalid ZIP input rejected and a Qt Widgets window +painted. CREngine, Kobalt's reader controls and hardware behavior are outside +this probe's coverage. + +The current SDK contains Qt Widgets and Network 6.8.2, but no Qt Sensors module. +The retained `build/readest-sync/firmware-6.11.1683/` file inventories list +`libQt6Widgets.so.6.10.3` and `libQt6Network.so.6.10.3`. These local observations +support using the native Qt runtime for this target, not all PocketBook models. +The existing SDK deliberately declares imported Qt targets instead of using +Debian's normal Qt package configuration, which requires OpenGL. Kobalt's +standalone CMake wiring therefore also needs an adaptation; changing the target +name or the compiler alone is insufficient. + +## Distribution inventory + +Upstream labels its own source MIT, CREngine patches GPL v2 or later, and Qt +patches under Qt's licenses. It describes the combined package as AGPL v3 and +supplies corresponding source/dependency sources with releases. Preserve this +inventory instead of treating the whole executable as MIT-only. This records +upstream's stated distribution model, not a separate legal assessment. +[License statement](https://github.com/42lizard/kobalt/blob/cbc0f4e9118c0e791056e36bc0b66d90e2af027c/README.md#license), +[credits](https://github.com/42lizard/kobalt/blob/cbc0f4e9118c0e791056e36bc0b66d90e2af027c/ACKNOWLEDGEMENTS.md), +[source-bundle procedure](https://github.com/42lizard/kobalt/blob/cbc0f4e9118c0e791056e36bc0b66d90e2af027c/docs/development.md#ci-and-tagged-releases). diff --git a/external/kobalt b/external/kobalt new file mode 160000 index 0000000..cbc0f4e --- /dev/null +++ b/external/kobalt @@ -0,0 +1 @@ +Subproject commit cbc0f4e9118c0e791056e36bc0b66d90e2af027c diff --git a/licenses/README.md b/licenses/README.md index 171e267..46343f5 100644 --- a/licenses/README.md +++ b/licenses/README.md @@ -35,6 +35,14 @@ not copies of the firmware controls. ## Notice provenance and scope +The optional `external/kobalt` submodule retains Nico Verbruggen's +[Kobalt license](../external/kobalt/LICENSE), +[acknowledgements](../external/kobalt/ACKNOWLEDGEMENTS.md) and dependency notices. +The compatibility probe compiles its publication component and links Qt Widgets; +it does not bundle CREngine. A complete reader port must preserve the separate +license/source inventory described in Kobalt's README; the root MIT license does +not relicense Kobalt's combined reader package. + The `*-copyright.txt` files are unmodified Debian package notices retrieved from the project's existing Qt build image on 2026-09-14. Standard license texts are copied from that image's `/usr/share/common-licenses`; references to that directory diff --git a/tools/kobalt-probe/CMakeLists.txt b/tools/kobalt-probe/CMakeLists.txt new file mode 100644 index 0000000..26cc76c --- /dev/null +++ b/tools/kobalt-probe/CMakeLists.txt @@ -0,0 +1,31 @@ +cmake_minimum_required(VERSION 3.21) +project(kobalt-probe LANGUAGES CXX) +option(KOBALT_PROBE_HOST "Run the compatibility probe on the build host" OFF) +set(KOBALT_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/../../external/kobalt") +if(NOT EXISTS "${KOBALT_ROOT}/cmake/ReaderComponents.cmake") + message(FATAL_ERROR "Run git submodule update --init external/kobalt first") +endif() +set(KOBALT_QT_MAJOR 6) +if(KOBALT_PROBE_HOST) + find_package(Qt6 REQUIRED COMPONENTS Core Gui Widgets) + add_executable(kobalt-probe main.cpp) + target_compile_definitions(kobalt-probe PRIVATE KOBALT_PROBE_HOST) +else() + include(../../mk/pocketbook-qt6.cmake) + pocketbook_add_app(kobalt-probe SOURCES main.cpp) +endif() +include("${KOBALT_ROOT}/cmake/Strings.cmake") +include("${KOBALT_ROOT}/cmake/ReaderComponents.cmake") +kobalt_publication_library(kobalt-publication) +target_compile_options(kobalt-publication PRIVATE "-iquote${KOBALT_ROOT}/src") +target_include_directories(kobalt-probe PRIVATE "${KOBALT_ROOT}/src") +target_link_libraries(kobalt-probe PRIVATE kobalt-publication Qt6::Widgets Qt6::Gui Qt6::Core) +if(KOBALT_PROBE_HOST) + enable_testing() + execute_process(COMMAND "${Python3_EXECUTABLE}" -c + "import runpy,sys; runpy.run_path(sys.argv[1])['book'](sys.argv[2], chapter_count=1, paragraphs=1)" + "${KOBALT_ROOT}/tests/fixtures/reader_fixture.py" "${CMAKE_CURRENT_BINARY_DIR}/probe.epub" + COMMAND_ERROR_IS_FATAL ANY) + add_test(NAME kobalt-publication-widgets COMMAND kobalt-probe --self-test "${CMAKE_CURRENT_BINARY_DIR}/probe.epub") + set_tests_properties(kobalt-publication-widgets PROPERTIES ENVIRONMENT "QT_QPA_PLATFORM=offscreen") +endif() diff --git a/tools/kobalt-probe/README.md b/tools/kobalt-probe/README.md new file mode 100644 index 0000000..87b0fcf --- /dev/null +++ b/tools/kobalt-probe/README.md @@ -0,0 +1,56 @@ +# Kobalt PocketBook compatibility probe + +This is an isolated port experiment, **not a working Kobalt reader**. It compiles +the fork's unchanged publication/archive component with Qt Widgets and the +existing PocketBook startup adapter. It does not include CREngine, Kobalt's +reader UI, native reading-state integration or a device installer. + +The source is pinned by the `external/kobalt` submodule to the user's +[fork](https://github.com/42lizard/kobalt). Original project: +[Nico Verbruggen's Kobalt](https://github.com/nicoverbruggen/kobalt). +Do not run Kobalt's Kobo installer on PocketBook. + +## Reproduce the checks + +From the repository root: + +```sh +git submodule update --init external/kobalt + +# Cross-compile for the current InkPad 4 target and check the ARM ABI. +docker compose run --rm qt6 sh -ec ' + cmake -S tools/kobalt-probe -B build/kobalt/probe-arm -DCMAKE_BUILD_TYPE=Release + cmake --build build/kobalt/probe-arm -j2 + python3 tools/check_qt6_app.py build/kobalt/probe-arm/kobalt-probe.app +' + +# Run the same publication component and a Widgets paint check on the host. +docker compose run --rm qt6 sh -ec ' + unset CMAKE_TOOLCHAIN_FILE + cmake -S tools/kobalt-probe -B build/kobalt/probe-host -DKOBALT_PROBE_HOST=ON -DCMAKE_BUILD_TYPE=Release + cmake --build build/kobalt/probe-host -j2 + ctest --test-dir build/kobalt/probe-host --output-on-failure +' +``` + +The host test generates an EPUB with Kobalt's own fixture generator, inspects +its archive, reads compressed chapter text, rejects invalid ZIP input and grabs +a painted Widgets window. It does not test EPUB layout or actual e-ink output. + +The ARM binary is `build/kobalt/probe-arm/kobalt-probe.app`. If launched manually, +it shows a clearly labelled probe screen and Close button. An optional first +argument supplies a read-only EPUB archive to inspect. No native databases or +book files are written. The probe is excluded from normal app builds/releases. + +## Next port milestone + +Reuse the shared reader/CREngine and standalone controller through a small +PocketBook host. Build the pinned engine dependencies with the PocketBook +toolchain, make Qt Sensors optional, adapt startup/assets/state paths and connect +the existing display/host-service interfaces. Start with one explicit EPUB path +and local Kobalt progress, then validate page turns, touch, exit and suspend on +the device. Native library registration and Readest integration are separate work. + +See the [source research and evidence](../../docs/research/kobalt-pocketbook-port.md). +All upstream notices remain in the submodule; a complete reader package has +additional dependency/source-distribution requirements described there. diff --git a/tools/kobalt-probe/main.cpp b/tools/kobalt-probe/main.cpp new file mode 100644 index 0000000..fcf20ba --- /dev/null +++ b/tools/kobalt-probe/main.cpp @@ -0,0 +1,70 @@ +#include "publication/publication.h" +#include "publication/zip_resource.h" +#include +#include +#include +#include +#include +#include +#ifndef KOBALT_PROBE_HOST +#include +#endif + +// A build/input prerequisite check, not the Kobalt reader or a device installer. +int main(int argc, char **argv) { +#ifndef KOBALT_PROBE_HOST + qputenv("QT_PLUGIN_PATH", "/ebrmain/plugins"); + qputenv("QT_QPA_PLATFORM", "pocketbook2"); + QCoreApplication::setSetuidAllowed(true); + InitInkview(TASK_MAKEACTIVE); +#endif + QApplication app(argc, argv); + app.setStyle("Fusion"); + QWidget window; + QVBoxLayout layout(&window); + QLabel message("Kobalt compatibility probe\nQt Widgets + unchanged publication code\nThis is not the EPUB reader."); + message.setWordWrap(true); + layout.addWidget(&message); + QPushButton close("Close"); + layout.addWidget(&close); + QObject::connect(&close, &QPushButton::clicked, &app, &QApplication::quit); + + if (app.arguments().contains("--self-test")) { + QFile book(app.arguments().value(2)); + QString error; + if (!book.open(QIODevice::ReadOnly)) return 1; + kobalt::Publication reader; + if (!reader.inspect(book, &error) || !reader.names().contains("c1.xhtml") || reader.protectedBook()) { + qCritical("Publication inspection failed: %s", qPrintable(error)); + return 2; + } + kobalt::ZipResource chapter; + if (!chapter.open(book, "c1.xhtml") || !chapter.next().contains("A reader keeps")) return 5; + kobalt::Publication invalid; + if (invalid.inspect(QByteArray("not a ZIP"), &error)) return 3; + window.resize(600, 800); + window.show(); + app.processEvents(); + return window.grab().isNull() ? 4 : 0; + } + if (app.arguments().size() > 1) { + QFile book(app.arguments().at(1)); + kobalt::Publication publication; + QString error; + if (!book.open(QIODevice::ReadOnly)) { + message.setText(book.errorString()); + } else if (!publication.inspect(book, &error)) { + message.setText(error); + } else { + message.setText(QString("Archive inspected: %1 entries\nProtected: %2\nNo rendering or reading-state changes.") + .arg(publication.names().size()).arg(publication.protectedBook() ? "yes" : "no")); + } + } +#ifdef KOBALT_PROBE_HOST + window.resize(600, 800); +#else + window.setGeometry(0, 0, ScreenWidth(), ScreenHeight() - PanelHeight()); +#endif + window.show(); + return app.exec(); +} From 1f7a9b30a3dd40d5341c10bcfd1a5e51a5be4369 Mon Sep 17 00:00:00 2001 From: Oliver Gassner Date: Tue, 13 Oct 2026 20:41:49 +0200 Subject: [PATCH 02/11] Package Kobalt for InkPad Color 3 with firmware compatibility checks --- Dockerfile.qt6 | 8 +- README.md | 8 +- apps/kobalt/README.md | 114 ++++++++++++++++++++ apps/kobalt/tests/test_package.py | 37 +++++++ apps/kobalt/tools/check_firmware.py | 60 +++++++++++ apps/kobalt/tools/package.py | 56 ++++++++++ apps/kobalt/tools/prepare_firmware.py | 61 +++++++++++ docs/research/kobalt-color3-runtime.md | 139 +++++++++++++++++++++++++ external/kobalt | 2 +- 9 files changed, 477 insertions(+), 8 deletions(-) create mode 100644 apps/kobalt/README.md create mode 100644 apps/kobalt/tests/test_package.py create mode 100644 apps/kobalt/tools/check_firmware.py create mode 100644 apps/kobalt/tools/package.py create mode 100644 apps/kobalt/tools/prepare_firmware.py create mode 100644 docs/research/kobalt-color3-runtime.md diff --git a/Dockerfile.qt6 b/Dockerfile.qt6 index 5daa86a..b61f0f7 100644 --- a/Dockerfile.qt6 +++ b/Dockerfile.qt6 @@ -4,11 +4,11 @@ FROM ghcr.io/fstanis/pocketbook-sdk-qt6-builder@sha256:1878e90fd4c9b8e736cd49b61 # Native build tools/QML runtime support offscreen host tests; armel libraries # build the device app. OpenSSL 3 replaces the legacy SDK crypto archive. RUN apt-get update && apt-get install -y --no-install-recommends \ - curl python3 pkg-config ninja-build \ + curl python3 pkg-config ninja-build patch \ libssl-dev:armel libcurl4-openssl-dev:armel libxml2-dev:armel \ - libsqlite3-dev:armel libjson-c-dev:armel \ - qt6-base-dev qt6-declarative-dev \ - libssl-dev libcurl4-openssl-dev libxml2-dev libsqlite3-dev libjson-c-dev \ + libsqlite3-dev:armel libjson-c-dev:armel libpng-dev:armel \ + qt6-base-dev qt6-declarative-dev qt6-sensors-dev \ + libssl-dev libcurl4-openssl-dev libxml2-dev libsqlite3-dev libjson-c-dev libpng-dev \ qml6-module-qtquick qml6-module-qtquick-window qml6-module-qtqml-workerscript \ && rm -rf /var/lib/apt/lists/* diff --git a/README.md b/README.md index 40ba4c7..051763c 100644 --- a/README.md +++ b/README.md @@ -16,9 +16,11 @@ Other devices, including Verse Pro, need separate validation. ## Build -An isolated [Kobalt port experiment](tools/kobalt-probe/README.md) pins -`42lizard/kobalt` under `external/kobalt` and checks PocketBook build prerequisites. -It is not yet a working reader and is excluded from the normal app builds below. +The experimental [Kobalt reader port](apps/kobalt/README.md) targets **InkPad +Color 3 (PB743K3), U743k3.6.10.2854**. It reuses Kobalt's EPUB renderer and +reading interface through the `external/kobalt` fork. It has separate build +and packaging commands; the normal app builds below remain unchanged. +Host reading tests and firmware import checks pass; device acceptance is pending. With Docker and Compose running, from the repository root: diff --git a/apps/kobalt/README.md b/apps/kobalt/README.md new file mode 100644 index 0000000..1e99041 --- /dev/null +++ b/apps/kobalt/README.md @@ -0,0 +1,114 @@ +# Kobalt on PocketBook + +Experimental standalone EPUB reader for **InkPad Color 3 (PB743K3), firmware +U743k3.6.10.2854**. The device supplies Qt 6.8.2 and its `pocketbook2` platform +plugin. Other models and firmware versions are not validated. + +The port reuses [Nico Verbruggen's Kobalt](https://github.com/nicoverbruggen/kobalt) +through [42lizard's fork](https://github.com/42lizard/kobalt/tree/pocketbook-port) +in `external/kobalt`. CREngine, the reading interface, settings, bookmarks and +local state remain upstream components. A small PocketBook host replaces Kobo +integration. This is a development build; physical-device acceptance is pending. + +## Build and verify + +From the parent repository root, initialize the submodule and build the SDK image: + +```sh +git submodule update --init external/kobalt +docker compose build qt6 +``` + +Download the [official target firmware](https://download.pocketbook-int.com/fw/Yitoa/743k3/user/20260105/sw_20260105_U743k3_6.10.2854_user.zip) +and extract its `SWUPDATE.BIN` to `build/kobalt/firmware-color3/`. This is a local +build input; do not copy the firmware onto the device. Install Python 3.11+ +and 7-Zip (`7zz`) on the host, then extract the libraries: + +```sh +python3 apps/kobalt/tools/prepare_firmware.py \ + build/kobalt/firmware-color3/SWUPDATE.BIN build/kobalt/firmware-prepared +``` + +The helper rejects a different firmware SHA-256. Extraction requires roughly +1 GiB of disk space. Provenance and runtime evidence are recorded in +[the firmware report](../../docs/research/kobalt-color3-runtime.md). + +```sh +docker compose run --rm qt6 python3 external/kobalt/pocketbook/build.py \ + --firmware-libs /workspace/build/kobalt/firmware-prepared/runtime/lib --jobs 4 + +docker compose run --rm qt6 python3 apps/kobalt/tools/check_firmware.py \ + external/kobalt/build/pocketbook/arm/app/kobalt \ + --library-dir build/kobalt/firmware-prepared/runtime/lib \ + --library-dir build/kobalt/firmware-prepared/rootfs/lib + +docker compose run --rm qt6 python3 external/kobalt/pocketbook/build.py --host --jobs 4 +python3 apps/kobalt/tests/test_package.py +``` + +The host test opens a real EPUB, turns a page, closes the reader and compares the +rendered page after reopening with the same profile. The firmware check verifies +ARM softfp, loader, absence of build RPATHs, direct library availability and exact +strong imported symbol names/versions. It does not simulate the device or verify +all transitive firmware dependencies. + +Run the complete upstream regression suite with `--host --regression`. Use +`--skip-dependencies` after dependencies and assets have been built. Dependency +versions, checksums and CREngine patches use Kobalt's existing manifests. + +## Package + +```sh +python3 apps/kobalt/tools/package.py \ + --binary external/kobalt/build/pocketbook/arm/app/kobalt \ + --assets external/kobalt/build/pocketbook/arm/assets/install/.adds/kobalt/assets \ + --notice external/kobalt/LICENSE \ + --notice external/kobalt/ACKNOWLEDGEMENTS.md \ + --notices-dir external/kobalt/resources/notices \ + --output build/kobalt/Kobalt-PB743K3.zip +``` + +The packager refuses to overwrite an existing ZIP. Font and third-party licenses +are included under `notices/`; acknowledgments and full notices are also +compiled into the reader's About screen. The ZIP contains only: + +```text +applications/kobalt.app launcher +system/kobalt/bin/kobalt ARM executable +system/kobalt/assets/ fonts, hyphenation and manifest +system/kobalt/notices/ license, acknowledgments and third-party notices +``` + +No Qt libraries, firmware, Kobo installation files, books or user state are +packaged. The combined reader includes GPL/AGPL dependencies: use Kobalt's +[corresponding-source procedure](../../external/kobalt/docs/development.md#ci-and-tagged-releases) +when distributing binaries. A checkout alone is not a complete source bundle. + +## Install and try + +Connect the PB743K3 by USB. Extract the ZIP on the computer, then copy only +`applications/kobalt.app` and `system/kobalt/` to the matching device paths. +Merge directories; keep existing `config/`, fonts, diagnostics and other apps. +For an update, keep a copy of the old launcher, binary and assets first. +Safely eject and disconnect USB, then open **Kobalt** in Applications. + +Choose **Open EPUB**, select an unprotected local EPUB, turn pages and exit. +The next launch offers the last book and restores its saved position. +Hardware page keys and touchscreen page turns use the existing reader actions. +Home exits after pending reading work drains. The reader's close action returns +to its start screen. An EPUB path may also be passed to the launcher. + +Kobalt stores its own progress, bookmarks, preferences and layout cache under +`/mnt/ext1/system/kobalt/config`. Logs are in `app.log` with one bounded +startup rotation; diagnostic files are under `diagnostics/`. It does not copy or +modify PocketBook's reading database and does not register as the default reader. + +Native-library progress/annotation integration and Readest sync are not part of +this port. Automatic G-sensor rotation, in-app frontlight control, dictionary +installation and external browser launch are unavailable. Manual rotation uses +InkView. Native keyboard, color refresh, suspend/wake, Home, USB handling and +rotation still need validation on the actual PB743K3. + +To remove the application, delete `applications/kobalt.app` and +`system/kobalt/bin`, `assets`, and `notices`. Retain `config` if reading progress +should survive reinstallation; deleting all of `system/kobalt` removes its data. diff --git a/apps/kobalt/tests/test_package.py b/apps/kobalt/tests/test_package.py new file mode 100644 index 0000000..f1d4f57 --- /dev/null +++ b/apps/kobalt/tests/test_package.py @@ -0,0 +1,37 @@ +import importlib.util +from pathlib import Path +import tempfile +import unittest +import zipfile + +SOURCE = Path(__file__).resolve().parents[1] / 'tools/package.py' + + +class PackageTest(unittest.TestCase): + def test_package_contains_only_launcher_binary_assets_and_notices(self): + spec = importlib.util.spec_from_file_location('kobalt_package', SOURCE) + package = importlib.util.module_from_spec(spec) + spec.loader.exec_module(package) + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + binary = root / 'kobalt' + binary.write_bytes(b'\x7fELFfixture') + assets = root / 'assets' + assets.mkdir() + (assets / 'manifest.json').write_text('{}') + notice = root / 'LICENSE' + notice.write_text('fixture notice') + output = root / 'package.zip' + package.create(binary, assets, [notice], output) + with zipfile.ZipFile(output) as archive: + self.assertEqual(set(archive.namelist()), { + 'applications/kobalt.app', 'system/kobalt/bin/kobalt', + 'system/kobalt/assets/manifest.json', 'system/kobalt/notices/LICENSE'}) + self.assertEqual(archive.read('system/kobalt/bin/kobalt'), binary.read_bytes()) + self.assertIn(b'"$@"', archive.read('applications/kobalt.app')) + with self.assertRaises(FileExistsError): + package.create(binary, assets, [notice], output) + + +if __name__ == '__main__': + unittest.main() diff --git a/apps/kobalt/tools/check_firmware.py b/apps/kobalt/tools/check_firmware.py new file mode 100644 index 0000000..76f306a --- /dev/null +++ b/apps/kobalt/tools/check_firmware.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python3 +"""Check the reader's direct imports against real, extracted target firmware.""" +import argparse +from pathlib import Path +import re +import subprocess + + +def elf(path, *options): + return subprocess.check_output(['arm-linux-gnueabi-readelf', *options, str(path)], text=True) + + +def symbols(path): + for line in elf(path, '--wide', '--dyn-syms').splitlines(): + fields = line.split() + if len(fields) >= 8 and fields[0].rstrip(':').isdigit(): + yield fields[4], fields[6], fields[7] + + +def check(binary, directories): + header = elf(binary, '-h') + if 'Machine: ARM' not in header or 'soft-float ABI' not in header: + raise ValueError('Expected ARM soft-float ABI') + if '/lib/ld-linux.so.3' not in elf(binary, '-l'): + raise ValueError('Wrong runtime loader') + dynamic = elf(binary, '-d') + if 'RPATH' in dynamic or 'RUNPATH' in dynamic: + raise ValueError('Build paths must not be embedded') + libraries = {} + exports = set() + for directory in directories: + for path in sorted(directory.iterdir()): + if path.is_symlink() or not path.is_file(): + continue + with path.open('rb') as stream: + if stream.read(4) != b'\x7fELF': + continue + soname = re.search(r'\(SONAME\).*\[(.*?)\]', elf(path, '-d')) + if soname: + libraries[soname[1]] = path + for binding, index, name in symbols(path): + if index != 'UND' and binding in ('GLOBAL', 'WEAK'): + exports.add(name.replace('@@', '@')) + if '@@' in name: + exports.add(name.split('@@')[0]) + needed = set(re.findall(r'\(NEEDED\).*\[(.*?)\]', dynamic)) + missing_libraries = needed - libraries.keys() + imports = {name for binding, index, name in symbols(binary) if binding == 'GLOBAL' and index == 'UND'} + missing_symbols = imports - exports + if missing_libraries or missing_symbols: + raise ValueError(f'Missing libraries: {sorted(missing_libraries)}\nMissing symbols: {sorted(missing_symbols)}') + print(f'{binary}: {len(needed)} direct libraries and {len(imports)} strong imports match firmware; ARM softfp, loader and paths passed') + + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('binary', type=Path) + parser.add_argument('--library-dir', type=Path, action='append', required=True) + args = parser.parse_args() + check(args.binary, args.library_dir) diff --git a/apps/kobalt/tools/package.py b/apps/kobalt/tools/package.py new file mode 100644 index 0000000..074afb2 --- /dev/null +++ b/apps/kobalt/tools/package.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python3 +"""Create an opt-in PocketBook package without reader data or firmware files.""" +import argparse +from pathlib import Path +import zipfile + +LAUNCHER = b'''#!/bin/sh +root=/mnt/ext1/system/kobalt +mkdir -p "$root" +if [ -f "$root/app.log" ] && [ "$(wc -c < "$root/app.log")" -gt 65536 ]; then + mv "$root/app.log" "$root/app.previous.log" +fi +exec "$root/bin/kobalt" "$@" >> "$root/app.log" 2>&1 +''' + + +def create(binary, assets, notices, output): + if Path(assets).is_symlink() or not Path(assets).is_dir(): + raise ValueError('Assets must be a regular directory') + files = [(Path(binary), 'system/kobalt/bin/kobalt')] + for path in sorted(Path(assets).rglob('*')): + if path.is_symlink(): + raise ValueError(f'Linked asset is not allowed: {path}') + if path.is_file(): + files.append((path, 'system/kobalt/assets/' + path.relative_to(assets).as_posix())) + files.extend((Path(path), 'system/kobalt/notices/' + Path(path).name) for path in notices) + names = [name for _, name in files] + if len(names) != len(set(names)) or not Path(assets, 'manifest.json').is_file(): + raise ValueError('Duplicate package path or missing asset manifest') + for path, _ in files: + if path.is_symlink() or not path.is_file(): + raise ValueError(f'Not a regular package input: {path}') + with zipfile.ZipFile(output, 'x', compression=zipfile.ZIP_DEFLATED) as archive: + entry = zipfile.ZipInfo('applications/kobalt.app') + entry.external_attr = 0o100755 << 16 + archive.writestr(entry, LAUNCHER) + for path, name in files: + entry = zipfile.ZipInfo(name) + entry.external_attr = (0o100755 if name.endswith('/bin/kobalt') else 0o100644) << 16 + entry.compress_type = zipfile.ZIP_DEFLATED + archive.writestr(entry, path.read_bytes()) + + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--binary', required=True, type=Path) + parser.add_argument('--assets', required=True, type=Path) + parser.add_argument('--notice', action='append', required=True, type=Path) + parser.add_argument('--notices-dir', type=Path) + parser.add_argument('--output', required=True, type=Path) + args = parser.parse_args() + if args.notices_dir: + if not args.notices_dir.is_dir() or args.notices_dir.is_symlink(): + parser.error('--notices-dir must be a regular directory') + args.notice.extend(sorted(args.notices_dir.iterdir())) + create(args.binary, args.assets, args.notice, args.output) diff --git a/apps/kobalt/tools/prepare_firmware.py b/apps/kobalt/tools/prepare_firmware.py new file mode 100644 index 0000000..7aa739c --- /dev/null +++ b/apps/kobalt/tools/prepare_firmware.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python3 +"""Extract link/check inputs from the official PB743K3 U743k3.6.10.2854 firmware. + +Requires 7zz. Inputs stay in the build directory and are never packaged. +""" +import argparse +import hashlib +from pathlib import Path +import shutil +import subprocess +import zlib + +SWUPDATE_SHA256 = '0ca8343a41e2497e0d038ed8f105be2f4d7f60ce9747e8ada24b164c5d5fd444' + + +def image(source, offset, output): + decoder = zlib.decompressobj(16 + zlib.MAX_WBITS) + with source.open('rb') as stream, output.open('wb') as target: + stream.seek(offset) + while not decoder.eof: + block = stream.read(1024 * 1024) + if not block: + raise ValueError('Truncated firmware image') + target.write(decoder.decompress(block)) + + +def extract(archive, output, *members): + output.mkdir(parents=True, exist_ok=True) + subprocess.run(['7zz', 'e', '-y', str(archive), '-o' + str(output), *members], check=True) + for member in members: + path = output / Path(member).name + if path.is_symlink() or not path.is_file(): + raise ValueError(f'Firmware member missing or linked: {member}') + + +def prepare(source, output): + if not shutil.which('7zz'): + raise ValueError('Install 7-Zip with the 7zz executable') + with source.open('rb') as stream: + digest = hashlib.file_digest(stream, 'sha256').hexdigest() + if digest != SWUPDATE_SHA256: + raise ValueError('Expected the exact PB743K3 U743k3.6.10.2854 SWUPDATE.BIN') + output.mkdir(parents=True, exist_ok=True) + image(source, 15336448, output / 'rootfs.raw') + image(source, 20579328, output / 'ebrmain.raw') + extract(output / 'ebrmain.raw', output / 'ebrmain', 'cramfs.img') + extract(output / 'rootfs.raw', output / 'rootfs/lib', + 'lib/libc.so.6', 'lib/libm.so.6', 'lib/libgcc_s.so.1', 'lib/ld-linux.so.3') + extract(output / 'ebrmain/cramfs.img', output / 'runtime/lib', + *[f'lib/libQt6{module}.so.6.8.2' for module in ('Core', 'Gui', 'Widgets', 'Network')], + 'lib/libstdc++.so.6.0.32', 'lib/libinkview.so', 'lib/libfreetype.so.6.20.1', + 'lib/libpng16.so.16.43.0', 'lib/libz.so.1.3.1') + print(output / 'runtime/lib') + + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('swupdate', type=Path) + parser.add_argument('output', type=Path) + args = parser.parse_args() + prepare(args.swupdate, args.output) diff --git a/docs/research/kobalt-color3-runtime.md b/docs/research/kobalt-color3-runtime.md new file mode 100644 index 0000000..df25644 --- /dev/null +++ b/docs/research/kobalt-color3-runtime.md @@ -0,0 +1,139 @@ +# Kobalt target runtime: InkPad Color 3 + +Inspected on 2026-10-13. Target: PocketBook InkPad Color 3, **PB743K3**, +user-reported firmware **U743k3.6.10.2854**. These findings supersede the +InkPad 4 runtime assumption in the initial feasibility study. No application +was run on the device during this investigation, and no device files were written. + +## Result + +The exact firmware ships **Qt 6.8.2**, including Core, Gui, Widgets and Network, +and the native **pocketbook2** Qt platform plugin. The existing Qt 6.8.2 +PocketBook SDK is therefore a suitable starting point. There is no observed +Qt-major-version blocker requiring a Qt 5 rewrite or bundling a foreign Qt +runtime. This is static compatibility evidence; successful application launch, +touch, refresh, suspend and shutdown still require device testing. + +## Primary source and artifact identity + +The [official support page](https://pocketbook.ch/en-ch/support) loads the +[product API for model 2256](https://pocketbook.ch/front-api/api/en-ch/product?product_id=2256). +Its `6.10.2854` entry links to the +[official firmware archive](https://download.pocketbook-int.com/fw/Yitoa/743k3/user/20260105/sw_20260105_U743k3_6.10.2854_user.zip). +The archive contains `fwinfo.xml` with version `U743k3.6.10.2854` and +`SWUPDATE.BIN`; the binary header also identifies `PocketBook743k3`. + +SHA-256 digests of inspected artifacts: + +| Artifact | SHA-256 | +| --- | --- | +| Downloaded ZIP | `769fb0ed98446fb134f6baf177504e1ab2e9552b2944f2a4ec55fab059d305cf` | +| `SWUPDATE.BIN` | `0ca8343a41e2497e0d038ed8f105be2f4d7f60ce9747e8ada24b164c5d5fd444` | +| `libQt6Core.so.6.8.2` | `1f8d2a814dbf64857ad7fbd4232b254267fd33e7c0434d95e71a8bb730b87f69` | +| Main-root `libc.so.6` | `a2e901366e98b866ec3521e5ebff30cc89387212be2d1deeb68fc093048adde3` | +| `libstdc++.so.6.0.32` | `a7b28dc0bb197ae4ee79ec6c0ec6af596dc88e60ad97553ef5351b0fcff819ef` | +| `libinkview.so` | `dbb87acdfeb18e246adb2abc265bb04786e2bb3eacd30c43f617a39987f69d7c` | +| `libqpocketbook2.so` | `e4e69c99932f7a62e7cfc11c502dc2b5177181eb71978cacfe6747306bf0a795` | + +## Shipped runtime observations + +Evidence comes from the downloaded firmware, inspected with 7-Zip 26.04, +GNU ARM `readelf`/`objdump` in the existing build container, and `strings`. +These are shell-tool observations, not REA Evidence IDs. REA's firmware tool +reported `capability_unavailable`: its Binwalk integration requires Linux and +an explicitly configured Binwalk executable, unavailable in the connected +macOS REA service. No REA setup changes were necessary for this bounded inspection. + +| Question | Observation | +| --- | --- | +| Qt generation/version | Real Core binary contains `Qt 6.8.2 (arm-little_endian-ilp32-eabi shared (dynamic) release build; by Clang 15.0.3)`. | +| Qt modules needed for standalone Widgets host | Real Core, Gui, Widgets and Network libraries are present, all version 6.8.2. | +| Qt Sensors | No Qt Sensors library appears in the main root, boot cramfs, ebrmain or nested runtime inventories. Make sensor support optional; do not assume the SDK omission alone establishes runtime absence. | +| ARM ABI | Qt Core ELF header: ELF32, little endian, ARM, EABI5, **soft-float ABI** (`0x5000200`). Attributes: ARMv7 application profile, Thumb-2, VFPv3, NEONv1. This is incompatible with an ARM hard-float calling convention. | +| Loader | Main root supplies `/lib/ld-linux.so.3`; Qt Core lists `ld-linux.so.3` as a dependency. | +| C runtime | Main-root `libc.so.6` identifies GNU C Library / Buildroot 2.39; highest numeric exported version definition is `GLIBC_2.39`. | +| C++ runtime | Firmware `libstdc++.so.6.0.32` exports through `GLIBCXX_3.4.32` and `CXXABI_1.3.14`; also includes ARM-specific ABI tags. | +| Native Qt platform | `plugins/platforms/libqpocketbook2.so`, plugin key `pocketbook2`; links Qt6 Gui/Core, InkView, `libhwconfig.so` and `libcommon_utilities.so`. | +| Refresh connection | The platform plugin imports InkView `FullUpdate`, `FullUpdateHQ`, `PartialUpdate`, `PartialUpdateHQ`, `PartialUpdateBW`, `PartialUpdateBlack` and `PartialUpdateDU4`. This establishes an existing firmware refresh integration, not its behavior for every application repaint. | + +Version ceilings are an initial filter, not a substitute for comparing the final +executable's undefined symbols, exact symbol versions and direct dependencies +against the extracted firmware exports. Qt private API compatibility is not +established merely by the public Qt version. + +The native platform plugin should own the hardware presentation path initially. +For an eventual direct framebuffer implementation, the +[KOReader PB743K3 adapter](https://github.com/koreader/koreader/blob/master/frontend/device/pocketbook/device.lua) +is useful independent implementation evidence: it sets RGB24 despite a reported +8-bit framebuffer, forced landscape-counterclockwise orientation, and viewport +`x=3, y=2, w=1398, h=1864`. These values were not independently measured on this +device and should not be copied into a native Qt host without a demonstrated need. + +## InkView exports + +All requested names exist as global defined function symbols in this firmware's +`libinkview.so`: + +| Symbol | ELF virtual address | Static observation | +| --- | --- | --- | +| `GetBatteryPower` | `0x75afc` | Tail branch to `hw_power@plt`. | +| `IsCharging` | `0x75ad0` | Tail branch to `hw_ischarging@plt`. | +| `GetFrontlightState` | `0x79abc` | Defined, 96 bytes. | +| `SetFrontlightState` | `0x79b78` | Defined, 88 bytes. | +| `OpenKeyboard` | `0x80b50` | Defined, 4536 bytes; `OpenKeyboardEx` also exists. | +| `FullUpdate` | `0x553d4` | Defined, 60 bytes; `FullUpdateHQ` also exists. | +| `BanSleep` | `0x75d18` | Tail branch to `hw_ban_suspend@plt`, not an empty stub. | +| `iv_sleepmode` | `0x783b8` | Defined, 20 bytes. | + +These are linked virtual addresses, not process runtime addresses. Symbol presence +does not verify SDK header declarations, function signatures, argument semantics +or power-management behavior. Use the corresponding API declaration and test the +chosen lifecycle on the actual device. + +## Retained local evidence and extraction + +Artifacts are ignored build outputs under `build/kobalt/firmware-color3/`: + +| Path relative to that directory | Contents | +| --- | --- | +| `fwinfo.xml`, `SWUPDATE.BIN` | Original ZIP members. | +| `rootfs.raw` | Gzip member at byte **15336448** in SWUPDATE, decompressed to a 32 MiB ext2 image. | +| `ebrmain.raw` | Gzip member at byte **20579328**, decompressed to a 517820416-byte ext2 image. | +| `cramfs.img` | Boot cramfs at byte **8193024**, length 6156288; separate from the main root. | +| `ebrmain/cramfs.img` | Member extracted from ebrmain; despite its filename this is **SquashFS 4, LZO**, not cramfs. Contains the real Qt/InkView libraries. | +| `rootfs/lib/libc.so.6`, `rootfs/lib/ld-linux.so.3` | Main-root C runtime and loader for final-binary checks. | +| `runtime/lib/libQt6Core.so.6.8.2` | Real Qt Core library. | +| `runtime/lib/libQt6Gui.so.6.8.2` | Real Qt Gui library. | +| `runtime/lib/libQt6Widgets.so.6.8.2` | Real Qt Widgets library. | +| `runtime/lib/libQt6Network.so.6.8.2` | Real Qt Network library. | +| `runtime/lib/libstdc++.so.6.0.32`, `runtime/lib/libinkview.so` | Real C++ runtime and InkView library. | +| `ebrmain/plugins/platforms/libqpocketbook2.so` | Native QPA plugin. | +| `files.txt`, `rootfs-files.txt`, `cramfs-files.txt`, `runtime-files.txt` | `7zz l` inventories of each respective filesystem. | + +The ebrmain `lib/libQt6*.so.6.8.2` entries are symlinks to +`../cramfs/lib/`. Merely extracting those links does not extract the actual +libraries. The runtime paths above refer to real files extracted from the nested +SquashFS image. 7-Zip declined unsafe/out-of-tree links during the preliminary +link inventory extraction; the required real files were subsequently extracted +directly without following those links. + +Recheck a selected library using the existing container, for example: + +```sh +docker compose run --rm qt6 arm-linux-gnueabi-readelf -h -A -d \ + /workspace/build/kobalt/firmware-color3/runtime/lib/libQt6Core.so.6.8.2 +docker compose run --rm qt6 arm-linux-gnueabi-readelf --version-info \ + /workspace/build/kobalt/firmware-color3/rootfs/lib/libc.so.6 +docker compose run --rm qt6 arm-linux-gnueabi-readelf --wide --dyn-syms \ + /workspace/build/kobalt/firmware-color3/runtime/lib/libinkview.so +``` + +## Remaining validation + +- Compare the completed port's imports and packaged dependency set to these + exact libraries; a successful initial publication probe is not a full-reader check. +- Device launch, EPUB opening, touch and page keys, local position persistence, + menus, color refresh, Home/exit, USB handling and suspend/wake remain runtime + acceptance work. +- Native library registration and Readest synchronization are separate from + the initial local-only reader port. diff --git a/external/kobalt b/external/kobalt index cbc0f4e..410d661 160000 --- a/external/kobalt +++ b/external/kobalt @@ -1 +1 @@ -Subproject commit cbc0f4e9118c0e791056e36bc0b66d90e2af027c +Subproject commit 410d661d5f0e657041733f3815349de75bc9ffa2 From 2668ec65244616890dac2cd0a6e44d87c551ee8a Mon Sep 17 00:00:00 2001 From: Oliver Gassner Date: Tue, 13 Oct 2026 20:50:27 +0200 Subject: [PATCH 03/11] Pin reviewed reader input fixes and complete host test dependencies --- Dockerfile.qt6 | 2 +- external/kobalt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile.qt6 b/Dockerfile.qt6 index b61f0f7..c0fc723 100644 --- a/Dockerfile.qt6 +++ b/Dockerfile.qt6 @@ -7,7 +7,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ curl python3 pkg-config ninja-build patch \ libssl-dev:armel libcurl4-openssl-dev:armel libxml2-dev:armel \ libsqlite3-dev:armel libjson-c-dev:armel libpng-dev:armel \ - qt6-base-dev qt6-declarative-dev qt6-sensors-dev \ + qt6-base-dev qt6-declarative-dev qt6-sensors-dev libqt6sql6-sqlite \ libssl-dev libcurl4-openssl-dev libxml2-dev libsqlite3-dev libjson-c-dev libpng-dev \ qml6-module-qtquick qml6-module-qtquick-window qml6-module-qtqml-workerscript \ && rm -rf /var/lib/apt/lists/* diff --git a/external/kobalt b/external/kobalt index 410d661..99ee8e1 160000 --- a/external/kobalt +++ b/external/kobalt @@ -1 +1 @@ -Subproject commit 410d661d5f0e657041733f3815349de75bc9ffa2 +Subproject commit 99ee8e14bf08f6f6382b23630a51c808b4ca4e38 From d27ba6713b5a2a21d624b790e665bb6cc1d8d9f2 Mon Sep 17 00:00:00 2001 From: Oliver Gassner Date: Tue, 13 Oct 2026 20:54:24 +0200 Subject: [PATCH 04/11] Pin upstream frontlight fixture correction found during regression --- external/kobalt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/external/kobalt b/external/kobalt index 99ee8e1..e1ba089 160000 --- a/external/kobalt +++ b/external/kobalt @@ -1 +1 @@ -Subproject commit 99ee8e14bf08f6f6382b23630a51c808b4ca4e38 +Subproject commit e1ba0890795255e7d6bdcc09b8262be9dcd87d1b From a32c4b3f343ea5b34f3e880c2aa9ac1c2adc83cb Mon Sep 17 00:00:00 2001 From: Oliver Gassner Date: Tue, 13 Oct 2026 21:46:05 +0200 Subject: [PATCH 05/11] Update Kobalt port with platform event compatibility fix --- docs/research/kobalt-color3-runtime.md | 23 +++++++++++++++++++++++ external/kobalt | 2 +- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/docs/research/kobalt-color3-runtime.md b/docs/research/kobalt-color3-runtime.md index df25644..38196c6 100644 --- a/docs/research/kobalt-color3-runtime.md +++ b/docs/research/kobalt-color3-runtime.md @@ -128,6 +128,29 @@ docker compose run --rm qt6 arm-linux-gnueabi-readelf --wide --dyn-syms \ /workspace/build/kobalt/firmware-color3/runtime/lib/libinkview.so ``` +## Qt Widgets startup compatibility + +The first device launches reached the start screen and then crashed in +`QRegion::operator+=`, called by `QApplication::compressEvent`. The native GDB +capture is retained locally in `build/kobalt/startup-debug/device-attempt-4.log`. +The QPA caller's return address maps to ELF virtual address `0x22548` in +`libqpocketbook2.so`. ARM disassembly immediately before that address shows a +12-byte allocation, `QEvent::QEvent` with type 78 (`UpdateLater`), and +`QCoreApplication::postEvent`. This is a plain event without a region payload. + +[Qt 6.8.2's QWidget event compression](https://github.com/qt/qtbase/blob/v6.8.2/src/widgets/kernel/qapplication.cpp#L785-L835) +assumes that duplicate `UpdateLater` events carry a private `QUpdateLaterEvent` +region. Posting two plain events to one QObject reproduces a segmentation fault +on host Qt 6.8.2 as well. The PocketBook application subclass delegates these +non-widget updates to the QGuiApplication path; QWidget receivers retain their +normal compression. The public event-queue regression test fails before the +change and verifies delivery of both updates afterward. The EPUB reopen test +also passes. Device confirmation remains pending. + +The same device capture reports a 1404×1735 window on a 1404×1872 screen, with +device pixel ratio 1. The reported small top-left interface therefore still +needs a separate scaling check after startup is stable. + ## Remaining validation - Compare the completed port's imports and packaged dependency set to these diff --git a/external/kobalt b/external/kobalt index e1ba089..7ab9c98 160000 --- a/external/kobalt +++ b/external/kobalt @@ -1 +1 @@ -Subproject commit e1ba0890795255e7d6bdcc09b8262be9dcd87d1b +Subproject commit 7ab9c980e3a9db9accc04bd3aaa649ed46fa10dd From 51eca2da3f01ed6a3b435741e24ce2eea3389fa9 Mon Sep 17 00:00:00 2001 From: Oliver Gassner Date: Thu, 15 Oct 2026 20:33:56 +0200 Subject: [PATCH 06/11] Add native Kobalt library entry and firmware ARM workflow checks --- Dockerfile.qt6 | 2 +- apps/kobalt/README.md | 46 +++++++++++++--- apps/kobalt/tests/test_registration.py | 43 +++++++++++++++ apps/kobalt/tools/check_arm_runtime.py | 53 +++++++++++++++++++ apps/kobalt/tools/package.py | 3 ++ apps/kobalt/tools/prepare_firmware.py | 11 +++- apps/kobalt/tools/register_reader.py | 72 ++++++++++++++++++++++++++ docs/research/kobalt-color3-runtime.md | 42 ++++++++++++--- external/kobalt | 2 +- 9 files changed, 258 insertions(+), 16 deletions(-) create mode 100644 apps/kobalt/tests/test_registration.py create mode 100644 apps/kobalt/tools/check_arm_runtime.py create mode 100644 apps/kobalt/tools/register_reader.py diff --git a/Dockerfile.qt6 b/Dockerfile.qt6 index c0fc723..409e36c 100644 --- a/Dockerfile.qt6 +++ b/Dockerfile.qt6 @@ -4,7 +4,7 @@ FROM ghcr.io/fstanis/pocketbook-sdk-qt6-builder@sha256:1878e90fd4c9b8e736cd49b61 # Native build tools/QML runtime support offscreen host tests; armel libraries # build the device app. OpenSSL 3 replaces the legacy SDK crypto archive. RUN apt-get update && apt-get install -y --no-install-recommends \ - curl python3 pkg-config ninja-build patch \ + curl python3 pkg-config ninja-build patch qemu-user \ libssl-dev:armel libcurl4-openssl-dev:armel libxml2-dev:armel \ libsqlite3-dev:armel libjson-c-dev:armel libpng-dev:armel \ qt6-base-dev qt6-declarative-dev qt6-sensors-dev libqt6sql6-sqlite \ diff --git a/apps/kobalt/README.md b/apps/kobalt/README.md index 1e99041..bf10c99 100644 --- a/apps/kobalt/README.md +++ b/apps/kobalt/README.md @@ -56,6 +56,26 @@ Run the complete upstream regression suite with `--host --regression`. Use `--skip-dependencies` after dependencies and assets have been built. Dependency versions, checksums and CREngine patches use Kobalt's existing manifests. +Also run the ARM workflow against the firmware runtime before device testing: + +```sh +python3 apps/kobalt/tools/prepare_firmware.py \ + build/kobalt/firmware-color3/SWUPDATE.BIN build/kobalt/firmware-prepared --runtime-test +docker compose run --rm qt6 python3 external/kobalt/pocketbook/build.py \ + --arm-test --firmware-libs /workspace/build/kobalt/firmware-prepared/runtime/lib --jobs 4 +docker compose run --rm qt6 python3 apps/kobalt/tools/check_arm_runtime.py \ + build/kobalt/firmware-prepared +python3 apps/kobalt/tests/test_registration.py +``` + +The SDK image includes `qemu-arm`; rebuild it after updating its Dockerfile. +The test uses real ARM reader code, firmware Qt/libc and an offscreen platform +plugin. InkView display, power and task calls are stubbed. It renders, turns a +page, closes through the reader action and restores the position, including a +filename with spaces and non-ASCII characters. The captured first page is in +`build/kobalt/arm-runtime-test/first-page.png`. This does not validate the device's +native QPA, touch, refresh or library task switching. + ## Package ```sh @@ -90,18 +110,30 @@ Connect the PB743K3 by USB. Extract the ZIP on the computer, then copy only `applications/kobalt.app` and `system/kobalt/` to the matching device paths. Merge directories; keep existing `config/`, fonts, diagnostics and other apps. For an update, keep a copy of the old launcher, binary and assets first. -Safely eject and disconnect USB, then open **Kobalt** in Applications. +Register the additional reader in the native library: + +```sh +python3 apps/kobalt/tools/register_reader.py /Volumes/PB743K3 +``` + +This adds Kobalt to EPUB's **Open with** choices, preserving the current default +and other associations. The first existing configuration is backed up as +`system/config/extensions.cfg.before-kobalt`; repeated registration is harmless. +Safely eject and disconnect USB. In the PocketBook library, long-press an +unprotected EPUB and choose **Open with → Kobalt**. Starting Kobalt from +Applications opens the native library. There is no separate Kobalt library or +desktop file picker. -Choose **Open EPUB**, select an unprotected local EPUB, turn pages and exit. -The next launch offers the last book and restores its saved position. +Turn pages and exit; opening the same book restores its saved position. Hardware page keys and touchscreen page turns use the existing reader actions. -Home exits after pending reading work drains. The reader's close action returns -to its start screen. An EPUB path may also be passed to the launcher. +Home and the reader's close action exit after pending reading work drains, +allowing PocketBook to resume the previous task. An EPUB path may also be passed +to the launcher. Successful opening updates InkView's task title and recent books. Kobalt stores its own progress, bookmarks, preferences and layout cache under `/mnt/ext1/system/kobalt/config`. Logs are in `app.log` with one bounded startup rotation; diagnostic files are under `diagnostics/`. It does not copy or -modify PocketBook's reading database and does not register as the default reader. +directly write PocketBook's reading database and does not become the default reader. Native-library progress/annotation integration and Readest sync are not part of this port. Automatic G-sensor rotation, in-app frontlight control, dictionary @@ -112,3 +144,5 @@ rotation still need validation on the actual PB743K3. To remove the application, delete `applications/kobalt.app` and `system/kobalt/bin`, `assets`, and `notices`. Retain `config` if reading progress should survive reinstallation; deleting all of `system/kobalt` removes its data. +Remove `kobalt.app` from the EPUB handler list in `system/config/extensions.cfg`. +Do not restore the backup blindly if other associations have changed since installation. diff --git a/apps/kobalt/tests/test_registration.py b/apps/kobalt/tests/test_registration.py new file mode 100644 index 0000000..64ee3b2 --- /dev/null +++ b/apps/kobalt/tests/test_registration.py @@ -0,0 +1,43 @@ +import importlib.util +from pathlib import Path +import tempfile +import unittest + +spec = importlib.util.spec_from_file_location('register_reader', Path(__file__).resolve().parents[1] / 'tools/register_reader.py') +registration = importlib.util.module_from_spec(spec) +spec.loader.exec_module(registration) + + +class RegistrationTest(unittest.TestCase): + def test_registration_preserves_default_other_readers_and_comments(self): + before = '# personal\r\nepub:@EPUB_file:1:custom.app,eink-reader.app:ICON_EPUB\r\nfb2:Book:1:other.app:ICON_FB2\r\n' + after = registration.register(before) + self.assertEqual(after, before.replace('custom.app,eink-reader.app', 'custom.app,eink-reader.app,kobalt.app')) + self.assertEqual(registration.register(after), after) + + def test_empty_config_uses_exact_target_firmware_defaults(self): + self.assertEqual(registration.register(''), registration.DEFAULT_EPUB.replace(':ICON_EPUB', ',kobalt.app:ICON_EPUB') + '\n') + + def test_ambiguous_config_is_rejected_without_guessing(self): + with self.assertRaises(ValueError): + registration.register(registration.DEFAULT_EPUB + '\n' + registration.DEFAULT_EPUB) + with self.assertRaises(ValueError): + registration.register('epub:malformed\n') + + def test_install_keeps_original_backup_and_later_associations(self): + with tempfile.TemporaryDirectory() as directory: + config = Path(directory) / 'extensions.cfg' + original = (registration.DEFAULT_EPUB + '\n').encode() + config.write_bytes(original) + registration.install(config) + backup = config.with_name('extensions.cfg.before-kobalt') + self.assertEqual(backup.read_bytes(), original) + edited = config.read_bytes() + b'pdf:Book:1:custom.app:ICON_PDF\n' + config.write_bytes(edited) + registration.install(config) + self.assertEqual(config.read_bytes(), edited) + self.assertEqual(backup.read_bytes(), original) + + +if __name__ == '__main__': + unittest.main() diff --git a/apps/kobalt/tools/check_arm_runtime.py b/apps/kobalt/tools/check_arm_runtime.py new file mode 100644 index 0000000..2c57f90 --- /dev/null +++ b/apps/kobalt/tools/check_arm_runtime.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python3 +"""Run the real ARM reader test with firmware Qt/libc and an offscreen display. + +Requires prepare_firmware.py --runtime-test output, the SDK compiler/readelf, +and qemu-arm. Hardware calls are replaced; this does not validate the native QPA, +physical keys, refresh or the device task manager. +""" +import argparse +from pathlib import Path +import re +import subprocess + +ROOT = Path(__file__).resolve().parents[3] + + +def run(firmware, binary, output, qemu): + firmware, binary, output = firmware.resolve(), binary.resolve(), output.resolve() + libraries = [firmware / 'runtime/lib', firmware / 'rootfs/lib', firmware / 'rootfs/usr/lib'] + for directory in libraries: + for library in list(directory.glob('*.so*')): + if not library.is_file() or library.is_symlink(): + continue + result = subprocess.run(['arm-linux-gnueabi-readelf', '-d', str(library)], capture_output=True, text=True) + soname = re.search(r'\(SONAME\).*\[(.*?)\]', result.stdout) + if soname: + name = soname[1] + if Path(name).name != name: + raise ValueError('Invalid library SONAME') + alias = directory / name + if not alias.exists(): + alias.symlink_to(library.name) + output.mkdir(parents=True, exist_ok=True) + shim = output / 'inkview-stub.so' + subprocess.run(['arm-linux-gnueabi-gcc', '-shared', '-fPIC', + str(ROOT / 'external/kobalt/tests/pocketbook/inkview_stub.c'), '-o', str(shim)], check=True) + command = [qemu, '-L', str(firmware / 'rootfs')] + for item in [f'LD_LIBRARY_PATH={":".join(map(str, libraries))}', f'LD_PRELOAD={shim}', + 'QT_QPA_PLATFORM=offscreen', 'QT_LOGGING_RULES=*.debug=false', + f'QT_PLUGIN_PATH={firmware / "ebrmain/plugins"}', + f'KOBALT_TEST_CAPTURE={output / "first-page.png"}']: + command.extend(['-E', item]) + command.append(str(binary)) + subprocess.run(command, check=True, timeout=120) + + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('firmware', type=Path) + parser.add_argument('--binary', type=Path, default=ROOT / 'external/kobalt/build/pocketbook/arm/app/pocketbook-test') + parser.add_argument('--output', type=Path, default=ROOT / 'build/kobalt/arm-runtime-test') + parser.add_argument('--qemu', default='qemu-arm') + args = parser.parse_args() + run(args.firmware, args.binary, args.output, args.qemu) diff --git a/apps/kobalt/tools/package.py b/apps/kobalt/tools/package.py index 074afb2..7e369c9 100644 --- a/apps/kobalt/tools/package.py +++ b/apps/kobalt/tools/package.py @@ -5,6 +5,9 @@ import zipfile LAUNCHER = b'''#!/bin/sh +if [ "$#" -eq 0 ]; then + exec /ebrmain/bin/bookshelf.app +fi root=/mnt/ext1/system/kobalt mkdir -p "$root" if [ -f "$root/app.log" ] && [ "$(wc -c < "$root/app.log")" -gt 65536 ]; then diff --git a/apps/kobalt/tools/prepare_firmware.py b/apps/kobalt/tools/prepare_firmware.py index 7aa739c..799f6e5 100644 --- a/apps/kobalt/tools/prepare_firmware.py +++ b/apps/kobalt/tools/prepare_firmware.py @@ -33,7 +33,7 @@ def extract(archive, output, *members): raise ValueError(f'Firmware member missing or linked: {member}') -def prepare(source, output): +def prepare(source, output, runtime_test=False): if not shutil.which('7zz'): raise ValueError('Install 7-Zip with the 7zz executable') with source.open('rb') as stream: @@ -50,6 +50,12 @@ def prepare(source, output): *[f'lib/libQt6{module}.so.6.8.2' for module in ('Core', 'Gui', 'Widgets', 'Network')], 'lib/libstdc++.so.6.0.32', 'lib/libinkview.so', 'lib/libfreetype.so.6.20.1', 'lib/libpng16.so.16.43.0', 'lib/libz.so.1.3.1') + if runtime_test: + for archive, destination, members in ( + (output / 'rootfs.raw', output / 'rootfs', ['lib/*', 'usr/lib/*']), + (output / 'ebrmain/cramfs.img', output / 'runtime', ['lib/*']), + (output / 'ebrmain.raw', output / 'ebrmain', ['plugins/platforms/libqoffscreen.so'])): + subprocess.run(['7zz', 'x', '-y', str(archive), '-o' + str(destination), *members], check=True) print(output / 'runtime/lib') @@ -57,5 +63,6 @@ def prepare(source, output): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument('swupdate', type=Path) parser.add_argument('output', type=Path) + parser.add_argument('--runtime-test', action='store_true', help='Also extract libraries and offscreen QPA for ARM tests') args = parser.parse_args() - prepare(args.swupdate, args.output) + prepare(args.swupdate, args.output, args.runtime_test) diff --git a/apps/kobalt/tools/register_reader.py b/apps/kobalt/tools/register_reader.py new file mode 100644 index 0000000..8522245 --- /dev/null +++ b/apps/kobalt/tools/register_reader.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python3 +"""Add Kobalt to PocketBook's EPUB 'Open with' choices, preserving the default.""" +import argparse +from pathlib import Path +import os +import shutil +import tempfile + +# PB743K3 U743k3.6.10.2854 /ebrmain/config/extensions.cfg. +DEFAULT_EPUB = 'epub:@EPUB_file:1:eink-reader_with_blink.app,eink-reader_with_epub2.app:ICON_EPUB' + + +def register(text): + lines = text.splitlines(keepends=True) + entries = [i for i, line in enumerate(lines) if line.startswith('epub:')] + if len(entries) > 1: + raise ValueError('Multiple EPUB associations; resolve them before registering Kobalt') + if not entries: + if lines and not lines[-1].endswith(('\n', '\r')): + lines[-1] += '\n' + entries = [len(lines)] + lines.append(DEFAULT_EPUB + '\n') + index = entries[0] + line = lines[index] + content = line.rstrip('\r\n') + fields = content.split(':') + if len(fields) != 5 or not fields[3] or any(not app for app in fields[3].split(',')): + raise ValueError('Malformed EPUB association') + if 'kobalt.app' not in fields[3].split(','): + fields[3] += ',kobalt.app' + lines[index] = ':'.join(fields) + line[len(content):] + return ''.join(lines) + + +def install(config): + if config.is_symlink(): + raise ValueError('Refusing a linked extensions.cfg') + before = config.read_bytes() if config.exists() else b'' + after = register(before.decode('utf-8')).encode('utf-8') + if before == after: + return + backup = config.with_name(config.name + '.before-kobalt') + if config.exists() and not backup.exists(): + with backup.open('xb') as stream: + stream.write(before) + stream.flush() + os.fsync(stream.fileno()) + shutil.copystat(config, backup) + with tempfile.NamedTemporaryFile(dir=config.parent, prefix='.extensions-kobalt-', delete=False) as stream: + temporary = Path(stream.name) + stream.write(after) + stream.flush() + os.fsync(stream.fileno()) + try: + if (config.read_bytes() if config.exists() else b'') != before: + raise RuntimeError('extensions.cfg changed during registration') + temporary.chmod(config.stat().st_mode if config.exists() else 0o644) + temporary.replace(config) + if config.read_bytes() != after: + raise OSError('Registration verification failed') + finally: + temporary.unlink(missing_ok=True) + + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('device', type=Path, help='Mounted PocketBook volume') + args = parser.parse_args() + if not (args.device / 'applications/kobalt.app').is_file(): + parser.error('Install Kobalt before registering it') + install(args.device / 'system/config/extensions.cfg') + print('Kobalt is available under Open with; the current default reader is preserved.') diff --git a/docs/research/kobalt-color3-runtime.md b/docs/research/kobalt-color3-runtime.md index 38196c6..4206a5d 100644 --- a/docs/research/kobalt-color3-runtime.md +++ b/docs/research/kobalt-color3-runtime.md @@ -145,18 +145,48 @@ on host Qt 6.8.2 as well. The PocketBook application subclass delegates these non-widget updates to the QGuiApplication path; QWidget receivers retain their normal compression. The public event-queue regression test fails before the change and verifies delivery of both updates afterward. The EPUB reopen test -also passes. Device confirmation remains pending. +also passes. The user subsequently confirmed successful startup on the device. The same device capture reports a 1404×1735 window on a 1404×1872 screen, with -device pixel ratio 1. The reported small top-left interface therefore still -needs a separate scaling check after startup is stable. +device pixel ratio 1. The subsequent screenshot showed Android's logical-pixel +start page at native pixel scale. The port now uses the native library as its +entry point and the shared reader's display-sized opening view instead. + +## ARM EPUB rendering and native library entry + +The ARM workflow test reproduced a second failure with the extracted firmware +Qt and C runtime: SIGBUS in CREngine's `newItem`, while capturing the first +rendered page. GDB identified a `vst1.64 ... [r0 :128]` store to an address ending +in `0x568`. `LVFontGlyphCacheItem` declares `alignas(16)` but its two allocation +paths used `malloc`, which returned an address aligned to only eight bytes. +`crengine-glyph-alignment.patch` makes both paths use `posix_memalign` with the +structure's alignment. The same ARM test then passed: open, rendered page, +page turn, close, reopen at the same position, and failed-open recovery. + +`apps/kobalt/tools/check_arm_runtime.py` runs this check under QEMU using the +actual firmware loader, libc, Qt and offscreen QPA. Only hardware display, task +and power calls are stubbed. A test image is written to +`build/kobalt/arm-runtime-test/first-page.png`. It is not evidence that the +PocketBook QPA or native task switching works on the physical device. +Local before/after traces remain under `build/kobalt/arm-runtime/`. + +The firmware's EPUB association is +`epub:@EPUB_file:1:eink-reader_with_blink.app,eink-reader_with_epub2.app:ICON_EPUB`. +The registration helper appends `kobalt.app` while preserving existing user +handlers and their order. This follows the native `extensions.cfg` format also +used by [KOReader's PocketBook integration](https://github.com/koreader/koreader/blob/master/frontend/device/pocketbook/device.lua). +The no-argument launcher uses the firmware's `/ebrmain/bin/bookshelf.app`. +Successful reading calls InkView's `SetSubtaskInfo`, `BookReady` and `AddLastOpen`; +the reader close action drains work and exits. Progress and bookmarks still +use Kobalt storage; this is not complete parity with the Kobo integration. ## Remaining validation - Compare the completed port's imports and packaged dependency set to these exact libraries; a successful initial publication probe is not a full-reader check. -- Device launch, EPUB opening, touch and page keys, local position persistence, +- Device EPUB opening, touch and page keys, local position persistence, menus, color refresh, Home/exit, USB handling and suspend/wake remain runtime acceptance work. -- Native library registration and Readest synchronization are separate from - the initial local-only reader port. +- Native library registration and task return need physical acceptance. + PocketBook-native progress/annotation interoperability, frontlight, dictionary + integration and Readest synchronization remain separate work. diff --git a/external/kobalt b/external/kobalt index 7ab9c98..6274b8f 160000 --- a/external/kobalt +++ b/external/kobalt @@ -1 +1 @@ -Subproject commit 7ab9c980e3a9db9accc04bd3aaa649ed46fa10dd +Subproject commit 6274b8f792cd7b2f3c46bf17795bf6a7eb472416 From aea6e13a7732ff578dba9f35bfa45c7b86b82893 Mon Sep 17 00:00:00 2001 From: Oliver Gassner Date: Thu, 15 Oct 2026 21:00:38 +0200 Subject: [PATCH 07/11] Fix Kobalt native library handler installation --- apps/kobalt/README.md | 8 ++++++-- apps/kobalt/tests/test_package.py | 16 +++++++++++++++- apps/kobalt/tests/test_registration.py | 22 ++++++++++++++++++++++ apps/kobalt/tools/package.py | 10 +++++++--- apps/kobalt/tools/register_reader.py | 4 ++-- docs/research/kobalt-color3-runtime.md | 24 ++++++++++++++++++++++++ 6 files changed, 76 insertions(+), 8 deletions(-) diff --git a/apps/kobalt/README.md b/apps/kobalt/README.md index bf10c99..6bbe129 100644 --- a/apps/kobalt/README.md +++ b/apps/kobalt/README.md @@ -94,6 +94,7 @@ compiled into the reader's About screen. The ZIP contains only: ```text applications/kobalt.app launcher +system/bin/kobalt.app native library reader handler system/kobalt/bin/kobalt ARM executable system/kobalt/assets/ fonts, hyphenation and manifest system/kobalt/notices/ license, acknowledgments and third-party notices @@ -107,7 +108,7 @@ when distributing binaries. A checkout alone is not a complete source bundle. ## Install and try Connect the PB743K3 by USB. Extract the ZIP on the computer, then copy only -`applications/kobalt.app` and `system/kobalt/` to the matching device paths. +`applications/kobalt.app`, `system/bin/kobalt.app` and `system/kobalt/` to the matching device paths. Merge directories; keep existing `config/`, fonts, diagnostics and other apps. For an update, keep a copy of the old launcher, binary and assets first. Register the additional reader in the native library: @@ -124,6 +125,9 @@ unprotected EPUB and choose **Open with → Kobalt**. Starting Kobalt from Applications opens the native library. There is no separate Kobalt library or desktop file picker. +Both launcher files are required: the native library resolves reader handlers +under `system/bin`, while the Applications screen uses `applications`. + Turn pages and exit; opening the same book restores its saved position. Hardware page keys and touchscreen page turns use the existing reader actions. Home and the reader's close action exit after pending reading work drains, @@ -141,7 +145,7 @@ installation and external browser launch are unavailable. Manual rotation uses InkView. Native keyboard, color refresh, suspend/wake, Home, USB handling and rotation still need validation on the actual PB743K3. -To remove the application, delete `applications/kobalt.app` and +To remove the application, delete `applications/kobalt.app`, `system/bin/kobalt.app` and `system/kobalt/bin`, `assets`, and `notices`. Retain `config` if reading progress should survive reinstallation; deleting all of `system/kobalt` removes its data. Remove `kobalt.app` from the EPUB handler list in `system/config/extensions.cfg`. diff --git a/apps/kobalt/tests/test_package.py b/apps/kobalt/tests/test_package.py index f1d4f57..9ae59d9 100644 --- a/apps/kobalt/tests/test_package.py +++ b/apps/kobalt/tests/test_package.py @@ -1,5 +1,6 @@ import importlib.util from pathlib import Path +import subprocess import tempfile import unittest import zipfile @@ -25,10 +26,23 @@ def test_package_contains_only_launcher_binary_assets_and_notices(self): package.create(binary, assets, [notice], output) with zipfile.ZipFile(output) as archive: self.assertEqual(set(archive.namelist()), { - 'applications/kobalt.app', 'system/kobalt/bin/kobalt', + 'applications/kobalt.app', 'system/bin/kobalt.app', 'system/kobalt/bin/kobalt', 'system/kobalt/assets/manifest.json', 'system/kobalt/notices/LICENSE'}) self.assertEqual(archive.read('system/kobalt/bin/kobalt'), binary.read_bytes()) self.assertIn(b'"$@"', archive.read('applications/kobalt.app')) + # InkView resolves EPUB handlers in system/bin, not applications. + entry = archive.getinfo('system/bin/kobalt.app') + self.assertEqual((entry.external_attr >> 16) & 0o777, 0o755) + handler = root / 'handler.app' + application = root / 'application-stub' + handler.write_bytes(archive.read(entry).replace( + b'/mnt/ext1/applications/kobalt.app', str(application).encode())) + application.write_text('#!/bin/sh\nprintf "%s\\n" "$#" "$@"\nexit 23\n') + application.chmod(0o755) + book = '/mnt/ext1/Books/Bären and Lúthien.epub' + result = subprocess.run(['sh', str(handler), book], capture_output=True, text=True) + self.assertEqual(result.returncode, 23) + self.assertEqual(result.stdout, '1\n' + book + '\n') with self.assertRaises(FileExistsError): package.create(binary, assets, [notice], output) diff --git a/apps/kobalt/tests/test_registration.py b/apps/kobalt/tests/test_registration.py index 64ee3b2..1bdda46 100644 --- a/apps/kobalt/tests/test_registration.py +++ b/apps/kobalt/tests/test_registration.py @@ -1,5 +1,7 @@ import importlib.util from pathlib import Path +import subprocess +import sys import tempfile import unittest @@ -9,6 +11,26 @@ class RegistrationTest(unittest.TestCase): + def test_cli_requires_the_native_library_handler(self): + with tempfile.TemporaryDirectory() as directory: + device = Path(directory) + application = device / 'applications/kobalt.app' + application.parent.mkdir() + application.write_text('#!/bin/sh\n') + config = device / 'system/config/extensions.cfg' + config.parent.mkdir(parents=True) + command = [sys.executable, str(Path(registration.__file__)), str(device)] + result = subprocess.run(command, capture_output=True, text=True) + self.assertNotEqual(result.returncode, 0) + self.assertIn('system/bin', result.stderr) + self.assertFalse(config.exists()) + handler = device / 'system/bin/kobalt.app' + handler.parent.mkdir() + handler.write_text('#!/bin/sh\n') + result = subprocess.run(command, capture_output=True, text=True) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertIn('kobalt.app', config.read_text()) + def test_registration_preserves_default_other_readers_and_comments(self): before = '# personal\r\nepub:@EPUB_file:1:custom.app,eink-reader.app:ICON_EPUB\r\nfb2:Book:1:other.app:ICON_FB2\r\n' after = registration.register(before) diff --git a/apps/kobalt/tools/package.py b/apps/kobalt/tools/package.py index 7e369c9..330be73 100644 --- a/apps/kobalt/tools/package.py +++ b/apps/kobalt/tools/package.py @@ -15,6 +15,9 @@ fi exec "$root/bin/kobalt" "$@" >> "$root/app.log" 2>&1 ''' +READER_HANDLER = b'''#!/bin/sh +exec /mnt/ext1/applications/kobalt.app "$@" +''' def create(binary, assets, notices, output): @@ -34,9 +37,10 @@ def create(binary, assets, notices, output): if path.is_symlink() or not path.is_file(): raise ValueError(f'Not a regular package input: {path}') with zipfile.ZipFile(output, 'x', compression=zipfile.ZIP_DEFLATED) as archive: - entry = zipfile.ZipInfo('applications/kobalt.app') - entry.external_attr = 0o100755 << 16 - archive.writestr(entry, LAUNCHER) + for name, content in [('applications/kobalt.app', LAUNCHER), ('system/bin/kobalt.app', READER_HANDLER)]: + entry = zipfile.ZipInfo(name) + entry.external_attr = 0o100755 << 16 + archive.writestr(entry, content) for path, name in files: entry = zipfile.ZipInfo(name) entry.external_attr = (0o100755 if name.endswith('/bin/kobalt') else 0o100644) << 16 diff --git a/apps/kobalt/tools/register_reader.py b/apps/kobalt/tools/register_reader.py index 8522245..6748b0f 100644 --- a/apps/kobalt/tools/register_reader.py +++ b/apps/kobalt/tools/register_reader.py @@ -66,7 +66,7 @@ def install(config): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument('device', type=Path, help='Mounted PocketBook volume') args = parser.parse_args() - if not (args.device / 'applications/kobalt.app').is_file(): - parser.error('Install Kobalt before registering it') + if not all((args.device / path).is_file() for path in ('applications/kobalt.app', 'system/bin/kobalt.app')): + parser.error('Install both Kobalt launchers (applications and system/bin) before registering it') install(args.device / 'system/config/extensions.cfg') print('Kobalt is available under Open with; the current default reader is preserved.') diff --git a/docs/research/kobalt-color3-runtime.md b/docs/research/kobalt-color3-runtime.md index 4206a5d..3a07642 100644 --- a/docs/research/kobalt-color3-runtime.md +++ b/docs/research/kobalt-color3-runtime.md @@ -180,6 +180,30 @@ Successful reading calls InkView's `SetSubtaskInfo`, `BookReady` and `AddLastOpe the reader close action drains work and exits. Progress and bookmarks still use Kobalt storage; this is not complete parity with the Kobo integration. +## Native library handler lookup + +The first library-entry package omitted `system/bin/kobalt.app`. Selecting +Kobalt in **Open with** consequently never reached the launcher in +`applications`, including after a reboot. An instrumented launcher produced no +new entry, while the device's `handlers.cfg` recorded the Kobalt selection. + +Static inspection of the exact firmware's `libinkview.so` (SHA-256 +`dbb87acdfeb18e246adb2abc265bb04786e2bb3eacd30c43f617a39987f69d7c`) +traced `OpenBook2` through `iv_OpenBook` at `0x42d70`. The multitasking launch +branch at `0x438a0` constructs `/mnt/ext1/system/bin/`, checks execute +access and falls back to `/ebrmain/bin/`. It does not use the +Applications directory for this reader lookup. REA evidence: +`ev_ecb92b7d2f48b19551b58a1713c1fddc6da7c217b81398c373ecc9fa38b195e1`. +This is static control-flow evidence, not an observed successful device launch. +The installed KOReader uses the same two-entry arrangement. + +The package now supplies an executable `system/bin/kobalt.app` that forwards +all arguments to `applications/kobalt.app`. Registration refuses an installation +missing either entry. The package regression failed on the missing handler +before the fix and now checks its presence, executable mode, argument forwarding +and exit status. The temporary diagnostic launcher is removed during the repair. +Physical EPUB opening remains pending after this package correction. + ## Remaining validation - Compare the completed port's imports and packaged dependency set to these From 7aef777cb2661f9d5a93fc59879328163de03648 Mon Sep 17 00:00:00 2001 From: Oliver Gassner Date: Thu, 15 Oct 2026 21:18:21 +0200 Subject: [PATCH 08/11] Verify PocketBook EPUB opening without network interfaces --- apps/kobalt/README.md | 10 +++++-- apps/kobalt/tools/check_arm_runtime.py | 12 ++++++-- docs/research/kobalt-color3-runtime.md | 39 +++++++++++++++++++++++++- external/kobalt | 2 +- 4 files changed, 57 insertions(+), 6 deletions(-) diff --git a/apps/kobalt/README.md b/apps/kobalt/README.md index 6bbe129..aa7a903 100644 --- a/apps/kobalt/README.md +++ b/apps/kobalt/README.md @@ -63,8 +63,8 @@ python3 apps/kobalt/tools/prepare_firmware.py \ build/kobalt/firmware-color3/SWUPDATE.BIN build/kobalt/firmware-prepared --runtime-test docker compose run --rm qt6 python3 external/kobalt/pocketbook/build.py \ --arm-test --firmware-libs /workspace/build/kobalt/firmware-prepared/runtime/lib --jobs 4 -docker compose run --rm qt6 python3 apps/kobalt/tools/check_arm_runtime.py \ - build/kobalt/firmware-prepared +docker compose run --rm --cap-add SYS_ADMIN qt6 python3 apps/kobalt/tools/check_arm_runtime.py \ + build/kobalt/firmware-prepared --without-network python3 apps/kobalt/tests/test_registration.py ``` @@ -76,6 +76,12 @@ filename with spaces and non-ASCII characters. The captured first page is in `build/kobalt/arm-runtime-test/first-page.png`. This does not validate the device's native QPA, touch, refresh or library task switching. +The isolated network check leaves loopback unconfigured, matching the device +failure that previously stopped opening at `loopback-listen`. PocketBook now +uses local Unix sockets for EPUB resource transfer. `SYS_ADMIN` is needed only +to create the test's network namespace; the reader needs no elevated rights. +Pass `--book PATH` to run the same check with a local EPUB instead of the fixture. + ## Package ```sh diff --git a/apps/kobalt/tools/check_arm_runtime.py b/apps/kobalt/tools/check_arm_runtime.py index 2c57f90..cf7e623 100644 --- a/apps/kobalt/tools/check_arm_runtime.py +++ b/apps/kobalt/tools/check_arm_runtime.py @@ -13,7 +13,7 @@ ROOT = Path(__file__).resolve().parents[3] -def run(firmware, binary, output, qemu): +def run(firmware, binary, output, qemu, without_network=False, book=None): firmware, binary, output = firmware.resolve(), binary.resolve(), output.resolve() libraries = [firmware / 'runtime/lib', firmware / 'rootfs/lib', firmware / 'rootfs/usr/lib'] for directory in libraries: @@ -39,7 +39,12 @@ def run(firmware, binary, output, qemu): f'QT_PLUGIN_PATH={firmware / "ebrmain/plugins"}', f'KOBALT_TEST_CAPTURE={output / "first-page.png"}']: command.extend(['-E', item]) + if book: + command.extend(['-E', f'KOBALT_TEST_BOOK={book.resolve(strict=True)}']) command.append(str(binary)) + if without_network: + # A fresh Linux network namespace has no configured loopback address. + command = ['unshare', '--net', '--', *command] subprocess.run(command, check=True, timeout=120) @@ -49,5 +54,8 @@ def run(firmware, binary, output, qemu): parser.add_argument('--binary', type=Path, default=ROOT / 'external/kobalt/build/pocketbook/arm/app/pocketbook-test') parser.add_argument('--output', type=Path, default=ROOT / 'build/kobalt/arm-runtime-test') parser.add_argument('--qemu', default='qemu-arm') + parser.add_argument('--without-network', action='store_true', + help='Run with no configured loopback or external network (requires CAP_SYS_ADMIN)') + parser.add_argument('--book', type=Path, help='Use a local EPUB instead of the generated fixture') args = parser.parse_args() - run(args.firmware, args.binary, args.output, args.qemu) + run(args.firmware, args.binary, args.output, args.qemu, args.without_network, args.book) diff --git a/docs/research/kobalt-color3-runtime.md b/docs/research/kobalt-color3-runtime.md index 3a07642..38b6535 100644 --- a/docs/research/kobalt-color3-runtime.md +++ b/docs/research/kobalt-color3-runtime.md @@ -202,7 +202,44 @@ all arguments to `applications/kobalt.app`. Registration refuses an installation missing either entry. The package regression failed on the missing handler before the fix and now checks its presence, executable mode, argument forwarding and exit status. The temporary diagnostic launcher is removed during the repair. -Physical EPUB opening remains pending after this package correction. +The subsequent device run reached Kobalt's opening view and failure dialog, +confirming that the corrected library handler launched the reader. + +## Reading without an IP loopback interface + +The next device log recorded `pocketbook-book-requested`, followed by +`native-resource-failed` with stage `loopback-listen` and Qt socket error 9. +The publication had passed inspection; the localhost resource listener failed +before rendering. The PocketBook adapter's loopback setup stub returns +`ENOTSUP`, so the shared TCP transport had assumed a configured loopback address. + +The real ARM workflow reproduced the failed open in a fresh Linux network +namespace (`unshare --net`), where loopback has no address. The test now reports +an opening failure immediately, reducing this negative check to 2.27 seconds. +`check_arm_runtime.py --without-network` retains this environment as a regression +check; `--book PATH` exercises a supplied local EPUB with the same workflow. + +PocketBook now selects `QLocalServer`/`QLocalSocket` for the shared resource +protocol. Its Linux abstract socket name contains the process ID and an opening +counter; it does not expose the authentication secret. Existing authentication, +limits, cancellation and drain ordering are retained. The socket disappears +with the process and needs neither network configuration nor elevated rights. +Other targets retain their loopback TCP transport. See Qt's +[local server options](https://doc.qt.io/qt-6.8/qlocalserver.html#SocketOption-enum) +and [local socket options](https://doc.qt.io/qt-6.8/qlocalsocket.html#SocketOption-enum). + +The same isolated-network ARM workflow passed after the change (8.88 seconds). +Two existing device EPUBs also passed opening, page turn, close and restored +position under the exact firmware runtime (13.89 and 23.55 seconds). Books and +captures remain private local test inputs under `build/kobalt/epub-open-debug/`. +The new executable's 13 direct libraries and 1,678 strong imports match the +firmware. These checks do not establish physical display/input acceptance. + +The full host regression passed 72 of 73 targets. `reader-window` failed its +existing `closeWithPendingBookmark` timing assertion, including an isolated +rerun (97 cases passed, one failed, two skipped). The same assertion also fails +in the saved baseline build without this transport change. This remains a +known test limitation; the suite is not reported as fully passing. ## Remaining validation diff --git a/external/kobalt b/external/kobalt index 6274b8f..6de0711 160000 --- a/external/kobalt +++ b/external/kobalt @@ -1 +1 @@ -Subproject commit 6274b8f792cd7b2f3c46bf17795bf6a7eb472416 +Subproject commit 6de0711c7efecb15f06b6505af8d839eced67693 From b01e548e14035c21a5deebd160348b2de9febe2a Mon Sep 17 00:00:00 2001 From: Oliver Gassner Date: Thu, 15 Oct 2026 21:35:53 +0200 Subject: [PATCH 09/11] Sync PocketBook reader with Kobalt upstream f2268b5 --- apps/kobalt/README.md | 7 +++- docs/research/kobalt-color3-runtime.md | 57 ++++++++++++++++++++++++-- external/kobalt | 2 +- 3 files changed, 59 insertions(+), 7 deletions(-) diff --git a/apps/kobalt/README.md b/apps/kobalt/README.md index aa7a903..2526e68 100644 --- a/apps/kobalt/README.md +++ b/apps/kobalt/README.md @@ -8,7 +8,9 @@ The port reuses [Nico Verbruggen's Kobalt](https://github.com/nicoverbruggen/kob through [42lizard's fork](https://github.com/42lizard/kobalt/tree/pocketbook-port) in `external/kobalt`. CREngine, the reading interface, settings, bookmarks and local state remain upstream components. A small PocketBook host replaces Kobo -integration. This is a development build; physical-device acceptance is pending. +integration. Startup and EPUB opening have been confirmed on the device with +fork commit `6de0711`. The subsequent upstream synchronization and the remaining +display, input and lifecycle behavior still need physical-device acceptance. ## Build and verify @@ -78,7 +80,8 @@ native QPA, touch, refresh or library task switching. The isolated network check leaves loopback unconfigured, matching the device failure that previously stopped opening at `loopback-listen`. PocketBook now -uses local Unix sockets for EPUB resource transfer. `SYS_ADMIN` is needed only +reads EPUB resources directly from the ZIP through the shared upstream reader. +`SYS_ADMIN` is needed only to create the test's network namespace; the reader needs no elevated rights. Pass `--book PATH` to run the same check with a local EPUB instead of the fixture. diff --git a/docs/research/kobalt-color3-runtime.md b/docs/research/kobalt-color3-runtime.md index 38b6535..fbfea61 100644 --- a/docs/research/kobalt-color3-runtime.md +++ b/docs/research/kobalt-color3-runtime.md @@ -219,7 +219,7 @@ an opening failure immediately, reducing this negative check to 2.27 seconds. `check_arm_runtime.py --without-network` retains this environment as a regression check; `--book PATH` exercises a supplied local EPUB with the same workflow. -PocketBook now selects `QLocalServer`/`QLocalSocket` for the shared resource +Fork commit `6de0711` selected `QLocalServer`/`QLocalSocket` for the shared resource protocol. Its Linux abstract socket name contains the process ID and an opening counter; it does not expose the authentication secret. Existing authentication, limits, cancellation and drain ordering are retained. The socket disappears @@ -241,11 +241,60 @@ rerun (97 cases passed, one failed, two skipped). The same assertion also fails in the saved baseline build without this transport change. This remains a known test limitation; the suite is not reported as fully passing. +The user subsequently confirmed that this installed version opened an EPUB on +the InkPad. Its package, `Kobalt-PB743K3-local-socket.zip`, has SHA-256 +`22b7e813c0288501eb30bdbc44ebc1265a57d084acd9747678dcbd549875735c`. + +## Upstream synchronization + +Upstream `f2268b52c125cf65646f7b93f250d00901378324` adds 66 commits since the +fork point, including direct local EPUB resource access. The shared controller +now uses `openLocalPublication`, which inspects the archive and reads bounded +ZIP streams on the engine worker. The PocketBook-only local socket adapter is +therefore removed. The source lease, resource limits, cancellation and drain +ordering are supplied by the upstream implementation. + +The merge retains PocketBook launch, InkView integration, glyph alignment and +Qt 6.8 compatibility changes. Integration adjustments cover the standalone +font library, sensor-free orientation and dependency caching with system PNG +and explicit cross-compiler configuration. The installed, device-confirmed +`6de0711` package remains separate from this synchronized build. + +Host and isolated-network ARM workflows pass with the fixture and both device +books. The workflow now waits for `ReaderWindow::readyForPresentation()` rather +than a fixed 300 ms delay: with the larger book, the old test captured the +temporary "Updating page layout" overlay as its expected page. Saved before/ +after-turn/reopened images established that race; the corrected comparison +still requires a changed page after PageDown and identical content on reopen. + +For reference, complete ARM workflow times were 8.88/8.71 seconds for the +fixture, 13.89/15.04 seconds for the first device book and 23.55/24.11 seconds +for the second, before/after synchronization. These use the same EPUBs, fresh +profiles, Release ARM softfp configuration, firmware Qt/libc, QEMU offscreen +display and isolated network namespace. They include two openings, a page turn, +shutdown, image comparisons and failure recovery. The readiness change and +varying host build load prevent attributing the differences to ZIP access or +claiming a speedup. Peak memory and native paint/refresh counts were not measured; +these runs do not establish physical-device performance. + +The synchronized executable passes firmware checks with 13 direct libraries +and 1,653 strong imports. The 117-file package includes upstream's ten additional +hyphenation patterns and is retained locally as +`Kobalt-PB743K3-upstream-f2268b5.zip` (SHA-256 +`8fbdc5496e72d731a38f0baf04dec2f3d0be6e82487c8b1a9cd6286aff9474d4`). +It has not replaced the confirmed build on the reader. + +The synchronized full host suite passes all 107 test groups in 258.56 seconds, +including the reader-window groups. All six package/registration tests, the UI +architecture check and the 379-file C++ formatting check pass. Follow-up +Standards and Spec reviews report no source-level findings. Physical acceptance +of this synchronized build remains outstanding. + ## Remaining validation -- Compare the completed port's imports and packaged dependency set to these - exact libraries; a successful initial publication probe is not a full-reader check. -- Device EPUB opening, touch and page keys, local position persistence, +- Validate the synchronized build on the device; its imports and emulated + workflows pass, while the preceding `6de0711` build has confirmed EPUB opening. +- Touch and page keys, local position persistence, menus, color refresh, Home/exit, USB handling and suspend/wake remain runtime acceptance work. - Native library registration and task return need physical acceptance. diff --git a/external/kobalt b/external/kobalt index 6de0711..6ecab6a 160000 --- a/external/kobalt +++ b/external/kobalt @@ -1 +1 @@ -Subproject commit 6de0711c7efecb15f06b6505af8d839eced67693 +Subproject commit 6ecab6a53d329e0f4d3e9df3ed5ff3a73aec2f0b From 7a894fe776c33a816448ddba85ab4f3b085de310 Mon Sep 17 00:00:00 2001 From: Oliver Gassner Date: Thu, 15 Oct 2026 21:43:36 +0200 Subject: [PATCH 10/11] Track rebased Kobalt port and verified InkPad installation --- apps/kobalt/README.md | 9 +++++++++ docs/research/kobalt-color3-runtime.md | 21 ++++++++++++++++++++- external/kobalt | 2 +- 3 files changed, 30 insertions(+), 2 deletions(-) diff --git a/apps/kobalt/README.md b/apps/kobalt/README.md index 2526e68..6879906 100644 --- a/apps/kobalt/README.md +++ b/apps/kobalt/README.md @@ -12,6 +12,15 @@ integration. Startup and EPUB opening have been confirmed on the device with fork commit `6de0711`. The subsequent upstream synchronization and the remaining display, input and lifecycle behavior still need physical-device acceptance. +## Fork maintenance + +Keep the fork's `main` aligned with upstream. Rebase `pocketbook-port` onto new +upstream revisions instead of merging them, preserving a linear patch series +for a future upstream contribution. Before rewriting published history, archive +device-confirmed and tested source revisions, use an explicit force-with-lease, +and update this repository's submodule pin. The current rebased port is `1a17e2f`; +its source tree matches the fully tested pre-rebase revision `6ecab6a` exactly. + ## Build and verify From the parent repository root, initialize the submodule and build the SDK image: diff --git a/docs/research/kobalt-color3-runtime.md b/docs/research/kobalt-color3-runtime.md index fbfea61..60ebb62 100644 --- a/docs/research/kobalt-color3-runtime.md +++ b/docs/research/kobalt-color3-runtime.md @@ -282,7 +282,10 @@ and 1,653 strong imports. The 117-file package includes upstream's ten additiona hyphenation patterns and is retained locally as `Kobalt-PB743K3-upstream-f2268b5.zip` (SHA-256 `8fbdc5496e72d731a38f0baf04dec2f3d0be6e82487c8b1a9cd6286aff9474d4`). -It has not replaced the confirmed build on the reader. +It was subsequently installed on the connected reader after the history rebase +described below. All 117 package files match the archive; hashes of Kobalt user +state and native reader associations remain unchanged. Replaced files and logs +are backed up locally under `build/kobalt/epub-open-debug/before-upstream/`. The synchronized full host suite passes all 107 test groups in 258.56 seconds, including the reader-window groups. All six package/registration tests, the UI @@ -290,6 +293,22 @@ architecture check and the 379-file C++ formatting check pass. Follow-up Standards and Spec reviews report no source-level findings. Physical acceptance of this synchronized build remains outstanding. +## Linear port history + +At the user's request, the port was rebased onto upstream `f2268b5` as six +linear commits, ending at `1a17e2f`. There are no merge commits in the port delta. +The obsolete socket workaround is omitted; the upstream integration adjustments +remain in the final commit. Both `1a17e2f` and the previously tested `6ecab6a` +have tree `3aa5ea8079d53b4146f0f7e3d047406e046cdb29`, so source and packaged binary +are unchanged by the history rewrite. The 107-test result applies to this exact +tree; no additional full test run was needed for the rebase. + +Non-release archive tags preserve both histories on the fork: +`archive/pocketbook-pre-rebase-20261015` and +`archive/pocketbook-device-confirmed-20261015`. The published port branch was +updated with a lease pinned to its previous exact commit. Future upstream +updates should use rebases to keep the port suitable for upstream review. + ## Remaining validation - Validate the synchronized build on the device; its imports and emulated diff --git a/external/kobalt b/external/kobalt index 6ecab6a..1a17e2f 160000 --- a/external/kobalt +++ b/external/kobalt @@ -1 +1 @@ -Subproject commit 6ecab6a53d329e0f4d3e9df3ed5ff3a73aec2f0b +Subproject commit 1a17e2f08eccdffca00e100e28982188c5f4cb2e From 7344bf5fcfee7e29535d1d2622488a45fb05b7b8 Mon Sep 17 00:00:00 2001 From: Oliver Gassner Date: Fri, 16 Oct 2026 22:37:22 +0200 Subject: [PATCH 11/11] Delegate the PocketBook buildchain to the Kobalt fork --- apps/kobalt/README.md | 188 ++++--------------------- apps/kobalt/tests/test_package.py | 51 ------- apps/kobalt/tests/test_registration.py | 65 --------- apps/kobalt/tools/check_arm_runtime.py | 61 -------- apps/kobalt/tools/check_firmware.py | 60 -------- apps/kobalt/tools/package.py | 63 --------- apps/kobalt/tools/prepare_firmware.py | 68 --------- apps/kobalt/tools/register_reader.py | 72 ---------- docs/research/kobalt-color3-runtime.md | 34 +++++ external/kobalt | 2 +- 10 files changed, 66 insertions(+), 598 deletions(-) delete mode 100644 apps/kobalt/tests/test_package.py delete mode 100644 apps/kobalt/tests/test_registration.py delete mode 100644 apps/kobalt/tools/check_arm_runtime.py delete mode 100644 apps/kobalt/tools/check_firmware.py delete mode 100644 apps/kobalt/tools/package.py delete mode 100644 apps/kobalt/tools/prepare_firmware.py delete mode 100644 apps/kobalt/tools/register_reader.py diff --git a/apps/kobalt/README.md b/apps/kobalt/README.md index 6879906..8963583 100644 --- a/apps/kobalt/README.md +++ b/apps/kobalt/README.md @@ -1,170 +1,44 @@ # Kobalt on PocketBook -Experimental standalone EPUB reader for **InkPad Color 3 (PB743K3), firmware -U743k3.6.10.2854**. The device supplies Qt 6.8.2 and its `pocketbook2` platform -plugin. Other models and firmware versions are not validated. +The experimental PocketBook InkPad Color 3 port is maintained in +[42lizard/kobalt](https://github.com/42lizard/kobalt/tree/pocketbook-port), included +here as the `external/kobalt` submodule. Its buildchain is now self-contained: +the fork includes the SDK recipe, firmware preparation, ABI/runtime checks, +packaging, registration helper and tests. A standalone clone does not require +this monorepo. -The port reuses [Nico Verbruggen's Kobalt](https://github.com/nicoverbruggen/kobalt) -through [42lizard's fork](https://github.com/42lizard/kobalt/tree/pocketbook-port) -in `external/kobalt`. CREngine, the reading interface, settings, bookmarks and -local state remain upstream components. A small PocketBook host replaces Kobo -integration. Startup and EPUB opening have been confirmed on the device with -fork commit `6de0711`. The subsequent upstream synchronization and the remaining -display, input and lifecycle behavior still need physical-device acceptance. +## Build and install -## Fork maintenance - -Keep the fork's `main` aligned with upstream. Rebase `pocketbook-port` onto new -upstream revisions instead of merging them, preserving a linear patch series -for a future upstream contribution. Before rewriting published history, archive -device-confirmed and tested source revisions, use an explicit force-with-lease, -and update this repository's submodule pin. The current rebased port is `1a17e2f`; -its source tree matches the fully tested pre-rebase revision `6ecab6a` exactly. +Follow [PocketBook build and installation](../../external/kobalt/pocketbook/README.md) +for the complete workflow. From this repository: -## Build and verify - -From the parent repository root, initialize the submodule and build the SDK image: +If this checkout has old monorepo build output, move +`external/kobalt/build/pocketbook` aside once; the new container uses a different +source mount, which invalidates the old CMake caches. ```sh git submodule update --init external/kobalt -docker compose build qt6 -``` - -Download the [official target firmware](https://download.pocketbook-int.com/fw/Yitoa/743k3/user/20260105/sw_20260105_U743k3_6.10.2854_user.zip) -and extract its `SWUPDATE.BIN` to `build/kobalt/firmware-color3/`. This is a local -build input; do not copy the firmware onto the device. Install Python 3.11+ -and 7-Zip (`7zz`) on the host, then extract the libraries: - -```sh -python3 apps/kobalt/tools/prepare_firmware.py \ - build/kobalt/firmware-color3/SWUPDATE.BIN build/kobalt/firmware-prepared -``` - -The helper rejects a different firmware SHA-256. Extraction requires roughly -1 GiB of disk space. Provenance and runtime evidence are recorded in -[the firmware report](../../docs/research/kobalt-color3-runtime.md). - -```sh -docker compose run --rm qt6 python3 external/kobalt/pocketbook/build.py \ - --firmware-libs /workspace/build/kobalt/firmware-prepared/runtime/lib --jobs 4 - -docker compose run --rm qt6 python3 apps/kobalt/tools/check_firmware.py \ - external/kobalt/build/pocketbook/arm/app/kobalt \ - --library-dir build/kobalt/firmware-prepared/runtime/lib \ - --library-dir build/kobalt/firmware-prepared/rootfs/lib - -docker compose run --rm qt6 python3 external/kobalt/pocketbook/build.py --host --jobs 4 -python3 apps/kobalt/tests/test_package.py -``` - -The host test opens a real EPUB, turns a page, closes the reader and compares the -rendered page after reopening with the same profile. The firmware check verifies -ARM softfp, loader, absence of build RPATHs, direct library availability and exact -strong imported symbol names/versions. It does not simulate the device or verify -all transitive firmware dependencies. - -Run the complete upstream regression suite with `--host --regression`. Use -`--skip-dependencies` after dependencies and assets have been built. Dependency -versions, checksums and CREngine patches use Kobalt's existing manifests. - -Also run the ARM workflow against the firmware runtime before device testing: - -```sh -python3 apps/kobalt/tools/prepare_firmware.py \ - build/kobalt/firmware-color3/SWUPDATE.BIN build/kobalt/firmware-prepared --runtime-test -docker compose run --rm qt6 python3 external/kobalt/pocketbook/build.py \ - --arm-test --firmware-libs /workspace/build/kobalt/firmware-prepared/runtime/lib --jobs 4 -docker compose run --rm --cap-add SYS_ADMIN qt6 python3 apps/kobalt/tools/check_arm_runtime.py \ - build/kobalt/firmware-prepared --without-network -python3 apps/kobalt/tests/test_registration.py +cd external/kobalt +./build-pocketbook.sh --firmware /absolute/path/SWUPDATE.BIN --jobs 4 --test ``` -The SDK image includes `qemu-arm`; rebuild it after updating its Dockerfile. -The test uses real ARM reader code, firmware Qt/libc and an offscreen platform -plugin. InkView display, power and task calls are stubbed. It renders, turns a -page, closes through the reader action and restores the position, including a -filename with spaces and non-ASCII characters. The captured first page is in -`build/kobalt/arm-runtime-test/first-page.png`. This does not validate the device's -native QPA, touch, refresh or library task switching. +The firmware input must be the official PB743K3 U743k3.6.10.2854 +`SWUPDATE.BIN`; its checksum is verified. The outputs are +`external/kobalt/build/pocketbook/Kobalt-PB743K3.zip` and its `pocketbook.json` +provenance record. Firmware libraries and user data are never packaged. +Installation and registration are documented in the fork; the build does not +write to a connected device. -The isolated network check leaves loopback unconfigured, matching the device -failure that previously stopped opening at `loopback-listen`. PocketBook now -reads EPUB resources directly from the ZIP through the shared upstream reader. -`SYS_ADMIN` is needed only -to create the test's network namespace; the reader needs no elevated rights. -Pass `--book PATH` to run the same check with a local EPUB instead of the fixture. - -## Package - -```sh -python3 apps/kobalt/tools/package.py \ - --binary external/kobalt/build/pocketbook/arm/app/kobalt \ - --assets external/kobalt/build/pocketbook/arm/assets/install/.adds/kobalt/assets \ - --notice external/kobalt/LICENSE \ - --notice external/kobalt/ACKNOWLEDGEMENTS.md \ - --notices-dir external/kobalt/resources/notices \ - --output build/kobalt/Kobalt-PB743K3.zip -``` - -The packager refuses to overwrite an existing ZIP. Font and third-party licenses -are included under `notices/`; acknowledgments and full notices are also -compiled into the reader's About screen. The ZIP contains only: - -```text -applications/kobalt.app launcher -system/bin/kobalt.app native library reader handler -system/kobalt/bin/kobalt ARM executable -system/kobalt/assets/ fonts, hyphenation and manifest -system/kobalt/notices/ license, acknowledgments and third-party notices -``` - -No Qt libraries, firmware, Kobo installation files, books or user state are -packaged. The combined reader includes GPL/AGPL dependencies: use Kobalt's -[corresponding-source procedure](../../external/kobalt/docs/development.md#ci-and-tagged-releases) -when distributing binaries. A checkout alone is not a complete source bundle. - -## Install and try - -Connect the PB743K3 by USB. Extract the ZIP on the computer, then copy only -`applications/kobalt.app`, `system/bin/kobalt.app` and `system/kobalt/` to the matching device paths. -Merge directories; keep existing `config/`, fonts, diagnostics and other apps. -For an update, keep a copy of the old launcher, binary and assets first. -Register the additional reader in the native library: - -```sh -python3 apps/kobalt/tools/register_reader.py /Volumes/PB743K3 -``` - -This adds Kobalt to EPUB's **Open with** choices, preserving the current default -and other associations. The first existing configuration is backed up as -`system/config/extensions.cfg.before-kobalt`; repeated registration is harmless. -Safely eject and disconnect USB. In the PocketBook library, long-press an -unprotected EPUB and choose **Open with → Kobalt**. Starting Kobalt from -Applications opens the native library. There is no separate Kobalt library or -desktop file picker. - -Both launcher files are required: the native library resolves reader handlers -under `system/bin`, while the Applications screen uses `applications`. - -Turn pages and exit; opening the same book restores its saved position. -Hardware page keys and touchscreen page turns use the existing reader actions. -Home and the reader's close action exit after pending reading work drains, -allowing PocketBook to resume the previous task. An EPUB path may also be passed -to the launcher. Successful opening updates InkView's task title and recent books. - -Kobalt stores its own progress, bookmarks, preferences and layout cache under -`/mnt/ext1/system/kobalt/config`. Logs are in `app.log` with one bounded -startup rotation; diagnostic files are under `diagnostics/`. It does not copy or -directly write PocketBook's reading database and does not become the default reader. +## Fork maintenance -Native-library progress/annotation integration and Readest sync are not part of -this port. Automatic G-sensor rotation, in-app frontlight control, dictionary -installation and external browser launch are unavailable. Manual rotation uses -InkView. Native keyboard, color refresh, suspend/wake, Home, USB handling and -rotation still need validation on the actual PB743K3. +Keep the fork's `main` aligned with upstream. Rebase `pocketbook-port` onto new +upstream revisions instead of merging them, preserving a linear patch series +for a future upstream contribution. Before rewriting published history, archive +device-confirmed and tested source revisions, use an explicit force-with-lease, +and update this repository's submodule pin. -To remove the application, delete `applications/kobalt.app`, `system/bin/kobalt.app` and -`system/kobalt/bin`, `assets`, and `notices`. Retain `config` if reading progress -should survive reinstallation; deleting all of `system/kobalt` removes its data. -Remove `kobalt.app` from the EPUB handler list in `system/config/extensions.cfg`. -Do not restore the backup blindly if other associations have changed since installation. +The historical runtime evidence and device acceptance status are recorded in +[the firmware report](../../docs/research/kobalt-color3-runtime.md). Startup and +EPUB opening were confirmed with `6de0711`; the rebased upstream synchronization +`1a17e2f` has been installed but still awaits physical acceptance. Remaining +native integration limitations are listed in the fork's documentation. diff --git a/apps/kobalt/tests/test_package.py b/apps/kobalt/tests/test_package.py deleted file mode 100644 index 9ae59d9..0000000 --- a/apps/kobalt/tests/test_package.py +++ /dev/null @@ -1,51 +0,0 @@ -import importlib.util -from pathlib import Path -import subprocess -import tempfile -import unittest -import zipfile - -SOURCE = Path(__file__).resolve().parents[1] / 'tools/package.py' - - -class PackageTest(unittest.TestCase): - def test_package_contains_only_launcher_binary_assets_and_notices(self): - spec = importlib.util.spec_from_file_location('kobalt_package', SOURCE) - package = importlib.util.module_from_spec(spec) - spec.loader.exec_module(package) - with tempfile.TemporaryDirectory() as directory: - root = Path(directory) - binary = root / 'kobalt' - binary.write_bytes(b'\x7fELFfixture') - assets = root / 'assets' - assets.mkdir() - (assets / 'manifest.json').write_text('{}') - notice = root / 'LICENSE' - notice.write_text('fixture notice') - output = root / 'package.zip' - package.create(binary, assets, [notice], output) - with zipfile.ZipFile(output) as archive: - self.assertEqual(set(archive.namelist()), { - 'applications/kobalt.app', 'system/bin/kobalt.app', 'system/kobalt/bin/kobalt', - 'system/kobalt/assets/manifest.json', 'system/kobalt/notices/LICENSE'}) - self.assertEqual(archive.read('system/kobalt/bin/kobalt'), binary.read_bytes()) - self.assertIn(b'"$@"', archive.read('applications/kobalt.app')) - # InkView resolves EPUB handlers in system/bin, not applications. - entry = archive.getinfo('system/bin/kobalt.app') - self.assertEqual((entry.external_attr >> 16) & 0o777, 0o755) - handler = root / 'handler.app' - application = root / 'application-stub' - handler.write_bytes(archive.read(entry).replace( - b'/mnt/ext1/applications/kobalt.app', str(application).encode())) - application.write_text('#!/bin/sh\nprintf "%s\\n" "$#" "$@"\nexit 23\n') - application.chmod(0o755) - book = '/mnt/ext1/Books/Bären and Lúthien.epub' - result = subprocess.run(['sh', str(handler), book], capture_output=True, text=True) - self.assertEqual(result.returncode, 23) - self.assertEqual(result.stdout, '1\n' + book + '\n') - with self.assertRaises(FileExistsError): - package.create(binary, assets, [notice], output) - - -if __name__ == '__main__': - unittest.main() diff --git a/apps/kobalt/tests/test_registration.py b/apps/kobalt/tests/test_registration.py deleted file mode 100644 index 1bdda46..0000000 --- a/apps/kobalt/tests/test_registration.py +++ /dev/null @@ -1,65 +0,0 @@ -import importlib.util -from pathlib import Path -import subprocess -import sys -import tempfile -import unittest - -spec = importlib.util.spec_from_file_location('register_reader', Path(__file__).resolve().parents[1] / 'tools/register_reader.py') -registration = importlib.util.module_from_spec(spec) -spec.loader.exec_module(registration) - - -class RegistrationTest(unittest.TestCase): - def test_cli_requires_the_native_library_handler(self): - with tempfile.TemporaryDirectory() as directory: - device = Path(directory) - application = device / 'applications/kobalt.app' - application.parent.mkdir() - application.write_text('#!/bin/sh\n') - config = device / 'system/config/extensions.cfg' - config.parent.mkdir(parents=True) - command = [sys.executable, str(Path(registration.__file__)), str(device)] - result = subprocess.run(command, capture_output=True, text=True) - self.assertNotEqual(result.returncode, 0) - self.assertIn('system/bin', result.stderr) - self.assertFalse(config.exists()) - handler = device / 'system/bin/kobalt.app' - handler.parent.mkdir() - handler.write_text('#!/bin/sh\n') - result = subprocess.run(command, capture_output=True, text=True) - self.assertEqual(result.returncode, 0, result.stderr) - self.assertIn('kobalt.app', config.read_text()) - - def test_registration_preserves_default_other_readers_and_comments(self): - before = '# personal\r\nepub:@EPUB_file:1:custom.app,eink-reader.app:ICON_EPUB\r\nfb2:Book:1:other.app:ICON_FB2\r\n' - after = registration.register(before) - self.assertEqual(after, before.replace('custom.app,eink-reader.app', 'custom.app,eink-reader.app,kobalt.app')) - self.assertEqual(registration.register(after), after) - - def test_empty_config_uses_exact_target_firmware_defaults(self): - self.assertEqual(registration.register(''), registration.DEFAULT_EPUB.replace(':ICON_EPUB', ',kobalt.app:ICON_EPUB') + '\n') - - def test_ambiguous_config_is_rejected_without_guessing(self): - with self.assertRaises(ValueError): - registration.register(registration.DEFAULT_EPUB + '\n' + registration.DEFAULT_EPUB) - with self.assertRaises(ValueError): - registration.register('epub:malformed\n') - - def test_install_keeps_original_backup_and_later_associations(self): - with tempfile.TemporaryDirectory() as directory: - config = Path(directory) / 'extensions.cfg' - original = (registration.DEFAULT_EPUB + '\n').encode() - config.write_bytes(original) - registration.install(config) - backup = config.with_name('extensions.cfg.before-kobalt') - self.assertEqual(backup.read_bytes(), original) - edited = config.read_bytes() + b'pdf:Book:1:custom.app:ICON_PDF\n' - config.write_bytes(edited) - registration.install(config) - self.assertEqual(config.read_bytes(), edited) - self.assertEqual(backup.read_bytes(), original) - - -if __name__ == '__main__': - unittest.main() diff --git a/apps/kobalt/tools/check_arm_runtime.py b/apps/kobalt/tools/check_arm_runtime.py deleted file mode 100644 index cf7e623..0000000 --- a/apps/kobalt/tools/check_arm_runtime.py +++ /dev/null @@ -1,61 +0,0 @@ -#!/usr/bin/env python3 -"""Run the real ARM reader test with firmware Qt/libc and an offscreen display. - -Requires prepare_firmware.py --runtime-test output, the SDK compiler/readelf, -and qemu-arm. Hardware calls are replaced; this does not validate the native QPA, -physical keys, refresh or the device task manager. -""" -import argparse -from pathlib import Path -import re -import subprocess - -ROOT = Path(__file__).resolve().parents[3] - - -def run(firmware, binary, output, qemu, without_network=False, book=None): - firmware, binary, output = firmware.resolve(), binary.resolve(), output.resolve() - libraries = [firmware / 'runtime/lib', firmware / 'rootfs/lib', firmware / 'rootfs/usr/lib'] - for directory in libraries: - for library in list(directory.glob('*.so*')): - if not library.is_file() or library.is_symlink(): - continue - result = subprocess.run(['arm-linux-gnueabi-readelf', '-d', str(library)], capture_output=True, text=True) - soname = re.search(r'\(SONAME\).*\[(.*?)\]', result.stdout) - if soname: - name = soname[1] - if Path(name).name != name: - raise ValueError('Invalid library SONAME') - alias = directory / name - if not alias.exists(): - alias.symlink_to(library.name) - output.mkdir(parents=True, exist_ok=True) - shim = output / 'inkview-stub.so' - subprocess.run(['arm-linux-gnueabi-gcc', '-shared', '-fPIC', - str(ROOT / 'external/kobalt/tests/pocketbook/inkview_stub.c'), '-o', str(shim)], check=True) - command = [qemu, '-L', str(firmware / 'rootfs')] - for item in [f'LD_LIBRARY_PATH={":".join(map(str, libraries))}', f'LD_PRELOAD={shim}', - 'QT_QPA_PLATFORM=offscreen', 'QT_LOGGING_RULES=*.debug=false', - f'QT_PLUGIN_PATH={firmware / "ebrmain/plugins"}', - f'KOBALT_TEST_CAPTURE={output / "first-page.png"}']: - command.extend(['-E', item]) - if book: - command.extend(['-E', f'KOBALT_TEST_BOOK={book.resolve(strict=True)}']) - command.append(str(binary)) - if without_network: - # A fresh Linux network namespace has no configured loopback address. - command = ['unshare', '--net', '--', *command] - subprocess.run(command, check=True, timeout=120) - - -if __name__ == '__main__': - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument('firmware', type=Path) - parser.add_argument('--binary', type=Path, default=ROOT / 'external/kobalt/build/pocketbook/arm/app/pocketbook-test') - parser.add_argument('--output', type=Path, default=ROOT / 'build/kobalt/arm-runtime-test') - parser.add_argument('--qemu', default='qemu-arm') - parser.add_argument('--without-network', action='store_true', - help='Run with no configured loopback or external network (requires CAP_SYS_ADMIN)') - parser.add_argument('--book', type=Path, help='Use a local EPUB instead of the generated fixture') - args = parser.parse_args() - run(args.firmware, args.binary, args.output, args.qemu, args.without_network, args.book) diff --git a/apps/kobalt/tools/check_firmware.py b/apps/kobalt/tools/check_firmware.py deleted file mode 100644 index 76f306a..0000000 --- a/apps/kobalt/tools/check_firmware.py +++ /dev/null @@ -1,60 +0,0 @@ -#!/usr/bin/env python3 -"""Check the reader's direct imports against real, extracted target firmware.""" -import argparse -from pathlib import Path -import re -import subprocess - - -def elf(path, *options): - return subprocess.check_output(['arm-linux-gnueabi-readelf', *options, str(path)], text=True) - - -def symbols(path): - for line in elf(path, '--wide', '--dyn-syms').splitlines(): - fields = line.split() - if len(fields) >= 8 and fields[0].rstrip(':').isdigit(): - yield fields[4], fields[6], fields[7] - - -def check(binary, directories): - header = elf(binary, '-h') - if 'Machine: ARM' not in header or 'soft-float ABI' not in header: - raise ValueError('Expected ARM soft-float ABI') - if '/lib/ld-linux.so.3' not in elf(binary, '-l'): - raise ValueError('Wrong runtime loader') - dynamic = elf(binary, '-d') - if 'RPATH' in dynamic or 'RUNPATH' in dynamic: - raise ValueError('Build paths must not be embedded') - libraries = {} - exports = set() - for directory in directories: - for path in sorted(directory.iterdir()): - if path.is_symlink() or not path.is_file(): - continue - with path.open('rb') as stream: - if stream.read(4) != b'\x7fELF': - continue - soname = re.search(r'\(SONAME\).*\[(.*?)\]', elf(path, '-d')) - if soname: - libraries[soname[1]] = path - for binding, index, name in symbols(path): - if index != 'UND' and binding in ('GLOBAL', 'WEAK'): - exports.add(name.replace('@@', '@')) - if '@@' in name: - exports.add(name.split('@@')[0]) - needed = set(re.findall(r'\(NEEDED\).*\[(.*?)\]', dynamic)) - missing_libraries = needed - libraries.keys() - imports = {name for binding, index, name in symbols(binary) if binding == 'GLOBAL' and index == 'UND'} - missing_symbols = imports - exports - if missing_libraries or missing_symbols: - raise ValueError(f'Missing libraries: {sorted(missing_libraries)}\nMissing symbols: {sorted(missing_symbols)}') - print(f'{binary}: {len(needed)} direct libraries and {len(imports)} strong imports match firmware; ARM softfp, loader and paths passed') - - -if __name__ == '__main__': - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument('binary', type=Path) - parser.add_argument('--library-dir', type=Path, action='append', required=True) - args = parser.parse_args() - check(args.binary, args.library_dir) diff --git a/apps/kobalt/tools/package.py b/apps/kobalt/tools/package.py deleted file mode 100644 index 330be73..0000000 --- a/apps/kobalt/tools/package.py +++ /dev/null @@ -1,63 +0,0 @@ -#!/usr/bin/env python3 -"""Create an opt-in PocketBook package without reader data or firmware files.""" -import argparse -from pathlib import Path -import zipfile - -LAUNCHER = b'''#!/bin/sh -if [ "$#" -eq 0 ]; then - exec /ebrmain/bin/bookshelf.app -fi -root=/mnt/ext1/system/kobalt -mkdir -p "$root" -if [ -f "$root/app.log" ] && [ "$(wc -c < "$root/app.log")" -gt 65536 ]; then - mv "$root/app.log" "$root/app.previous.log" -fi -exec "$root/bin/kobalt" "$@" >> "$root/app.log" 2>&1 -''' -READER_HANDLER = b'''#!/bin/sh -exec /mnt/ext1/applications/kobalt.app "$@" -''' - - -def create(binary, assets, notices, output): - if Path(assets).is_symlink() or not Path(assets).is_dir(): - raise ValueError('Assets must be a regular directory') - files = [(Path(binary), 'system/kobalt/bin/kobalt')] - for path in sorted(Path(assets).rglob('*')): - if path.is_symlink(): - raise ValueError(f'Linked asset is not allowed: {path}') - if path.is_file(): - files.append((path, 'system/kobalt/assets/' + path.relative_to(assets).as_posix())) - files.extend((Path(path), 'system/kobalt/notices/' + Path(path).name) for path in notices) - names = [name for _, name in files] - if len(names) != len(set(names)) or not Path(assets, 'manifest.json').is_file(): - raise ValueError('Duplicate package path or missing asset manifest') - for path, _ in files: - if path.is_symlink() or not path.is_file(): - raise ValueError(f'Not a regular package input: {path}') - with zipfile.ZipFile(output, 'x', compression=zipfile.ZIP_DEFLATED) as archive: - for name, content in [('applications/kobalt.app', LAUNCHER), ('system/bin/kobalt.app', READER_HANDLER)]: - entry = zipfile.ZipInfo(name) - entry.external_attr = 0o100755 << 16 - archive.writestr(entry, content) - for path, name in files: - entry = zipfile.ZipInfo(name) - entry.external_attr = (0o100755 if name.endswith('/bin/kobalt') else 0o100644) << 16 - entry.compress_type = zipfile.ZIP_DEFLATED - archive.writestr(entry, path.read_bytes()) - - -if __name__ == '__main__': - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument('--binary', required=True, type=Path) - parser.add_argument('--assets', required=True, type=Path) - parser.add_argument('--notice', action='append', required=True, type=Path) - parser.add_argument('--notices-dir', type=Path) - parser.add_argument('--output', required=True, type=Path) - args = parser.parse_args() - if args.notices_dir: - if not args.notices_dir.is_dir() or args.notices_dir.is_symlink(): - parser.error('--notices-dir must be a regular directory') - args.notice.extend(sorted(args.notices_dir.iterdir())) - create(args.binary, args.assets, args.notice, args.output) diff --git a/apps/kobalt/tools/prepare_firmware.py b/apps/kobalt/tools/prepare_firmware.py deleted file mode 100644 index 799f6e5..0000000 --- a/apps/kobalt/tools/prepare_firmware.py +++ /dev/null @@ -1,68 +0,0 @@ -#!/usr/bin/env python3 -"""Extract link/check inputs from the official PB743K3 U743k3.6.10.2854 firmware. - -Requires 7zz. Inputs stay in the build directory and are never packaged. -""" -import argparse -import hashlib -from pathlib import Path -import shutil -import subprocess -import zlib - -SWUPDATE_SHA256 = '0ca8343a41e2497e0d038ed8f105be2f4d7f60ce9747e8ada24b164c5d5fd444' - - -def image(source, offset, output): - decoder = zlib.decompressobj(16 + zlib.MAX_WBITS) - with source.open('rb') as stream, output.open('wb') as target: - stream.seek(offset) - while not decoder.eof: - block = stream.read(1024 * 1024) - if not block: - raise ValueError('Truncated firmware image') - target.write(decoder.decompress(block)) - - -def extract(archive, output, *members): - output.mkdir(parents=True, exist_ok=True) - subprocess.run(['7zz', 'e', '-y', str(archive), '-o' + str(output), *members], check=True) - for member in members: - path = output / Path(member).name - if path.is_symlink() or not path.is_file(): - raise ValueError(f'Firmware member missing or linked: {member}') - - -def prepare(source, output, runtime_test=False): - if not shutil.which('7zz'): - raise ValueError('Install 7-Zip with the 7zz executable') - with source.open('rb') as stream: - digest = hashlib.file_digest(stream, 'sha256').hexdigest() - if digest != SWUPDATE_SHA256: - raise ValueError('Expected the exact PB743K3 U743k3.6.10.2854 SWUPDATE.BIN') - output.mkdir(parents=True, exist_ok=True) - image(source, 15336448, output / 'rootfs.raw') - image(source, 20579328, output / 'ebrmain.raw') - extract(output / 'ebrmain.raw', output / 'ebrmain', 'cramfs.img') - extract(output / 'rootfs.raw', output / 'rootfs/lib', - 'lib/libc.so.6', 'lib/libm.so.6', 'lib/libgcc_s.so.1', 'lib/ld-linux.so.3') - extract(output / 'ebrmain/cramfs.img', output / 'runtime/lib', - *[f'lib/libQt6{module}.so.6.8.2' for module in ('Core', 'Gui', 'Widgets', 'Network')], - 'lib/libstdc++.so.6.0.32', 'lib/libinkview.so', 'lib/libfreetype.so.6.20.1', - 'lib/libpng16.so.16.43.0', 'lib/libz.so.1.3.1') - if runtime_test: - for archive, destination, members in ( - (output / 'rootfs.raw', output / 'rootfs', ['lib/*', 'usr/lib/*']), - (output / 'ebrmain/cramfs.img', output / 'runtime', ['lib/*']), - (output / 'ebrmain.raw', output / 'ebrmain', ['plugins/platforms/libqoffscreen.so'])): - subprocess.run(['7zz', 'x', '-y', str(archive), '-o' + str(destination), *members], check=True) - print(output / 'runtime/lib') - - -if __name__ == '__main__': - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument('swupdate', type=Path) - parser.add_argument('output', type=Path) - parser.add_argument('--runtime-test', action='store_true', help='Also extract libraries and offscreen QPA for ARM tests') - args = parser.parse_args() - prepare(args.swupdate, args.output, args.runtime_test) diff --git a/apps/kobalt/tools/register_reader.py b/apps/kobalt/tools/register_reader.py deleted file mode 100644 index 6748b0f..0000000 --- a/apps/kobalt/tools/register_reader.py +++ /dev/null @@ -1,72 +0,0 @@ -#!/usr/bin/env python3 -"""Add Kobalt to PocketBook's EPUB 'Open with' choices, preserving the default.""" -import argparse -from pathlib import Path -import os -import shutil -import tempfile - -# PB743K3 U743k3.6.10.2854 /ebrmain/config/extensions.cfg. -DEFAULT_EPUB = 'epub:@EPUB_file:1:eink-reader_with_blink.app,eink-reader_with_epub2.app:ICON_EPUB' - - -def register(text): - lines = text.splitlines(keepends=True) - entries = [i for i, line in enumerate(lines) if line.startswith('epub:')] - if len(entries) > 1: - raise ValueError('Multiple EPUB associations; resolve them before registering Kobalt') - if not entries: - if lines and not lines[-1].endswith(('\n', '\r')): - lines[-1] += '\n' - entries = [len(lines)] - lines.append(DEFAULT_EPUB + '\n') - index = entries[0] - line = lines[index] - content = line.rstrip('\r\n') - fields = content.split(':') - if len(fields) != 5 or not fields[3] or any(not app for app in fields[3].split(',')): - raise ValueError('Malformed EPUB association') - if 'kobalt.app' not in fields[3].split(','): - fields[3] += ',kobalt.app' - lines[index] = ':'.join(fields) + line[len(content):] - return ''.join(lines) - - -def install(config): - if config.is_symlink(): - raise ValueError('Refusing a linked extensions.cfg') - before = config.read_bytes() if config.exists() else b'' - after = register(before.decode('utf-8')).encode('utf-8') - if before == after: - return - backup = config.with_name(config.name + '.before-kobalt') - if config.exists() and not backup.exists(): - with backup.open('xb') as stream: - stream.write(before) - stream.flush() - os.fsync(stream.fileno()) - shutil.copystat(config, backup) - with tempfile.NamedTemporaryFile(dir=config.parent, prefix='.extensions-kobalt-', delete=False) as stream: - temporary = Path(stream.name) - stream.write(after) - stream.flush() - os.fsync(stream.fileno()) - try: - if (config.read_bytes() if config.exists() else b'') != before: - raise RuntimeError('extensions.cfg changed during registration') - temporary.chmod(config.stat().st_mode if config.exists() else 0o644) - temporary.replace(config) - if config.read_bytes() != after: - raise OSError('Registration verification failed') - finally: - temporary.unlink(missing_ok=True) - - -if __name__ == '__main__': - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument('device', type=Path, help='Mounted PocketBook volume') - args = parser.parse_args() - if not all((args.device / path).is_file() for path in ('applications/kobalt.app', 'system/bin/kobalt.app')): - parser.error('Install both Kobalt launchers (applications and system/bin) before registering it') - install(args.device / 'system/config/extensions.cfg') - print('Kobalt is available under Open with; the current default reader is preserved.') diff --git a/docs/research/kobalt-color3-runtime.md b/docs/research/kobalt-color3-runtime.md index 60ebb62..477fe28 100644 --- a/docs/research/kobalt-color3-runtime.md +++ b/docs/research/kobalt-color3-runtime.md @@ -309,6 +309,40 @@ Non-release archive tags preserve both histories on the fork: updated with a lease pinned to its previous exact commit. Future upstream updates should use rebases to keep the port suitable for upstream review. +## Standalone fork buildchain + +Fork commit `c1e884e` now owns the PocketBook SDK recipe, firmware preparation, ABI/runtime +checks, packaging, registration and Python tests. Its root `build-pocketbook.sh` +uses the existing Kobalt container-engine, dependency, font and provenance +helpers. The parent repository only pins the fork and links its instructions. +Duplicate parent helpers were removed. Existing monorepo CMake outputs must be +moved aside once because the container source mount changed to `/work`. + +Validation used an independent clone with no compiled outputs, mounting only +the fork checkout and the separately supplied, checksum-verified firmware file. +The SDK image was built from the fork's recipe. Pinned dependency archives and +font inputs were reused, then ARM and host dependencies were compiled afresh. +The full standalone command passed, including all ten Python tooling checks, +the host EPUB workflow and the exact-firmware ARM workflow with no IP network. +The final ARM workflow took 6.259 seconds; this is not device startup timing. +All 13 direct libraries and 1,653 strong imports match the firmware. The full +host regression suite passed all 108 groups in 213.24 seconds. + +The normal command also passed from clean commit `c1e884e`, without +`--allow-dirty`; its provenance record reports `dirty: false`. The 117-file ZIP +has SHA-256 `06c582119d9d5a151c29f5400b7343dedb93dc1a634a426f3317264a632a1e13`. +The generated ZIP remains 31 MiB and excludes firmware and user data. A source +change during the build fails validation before replacing a prior verified ZIP; +the regression was checked against the faulty early-publication behavior. +Firmware inputs are reused only after checksum verification; a corruption test +checks re-extraction. Standards and Spec reviews have no open findings. + +This validates Docker on an ARM64 host. The pinned SDK image also supplies +AMD64, but AMD64 and Podman builds have not been exercised. Kobo build entry +points were unchanged; no full Kobo cross-build was run. This tooling change +does not install a new device package or change the pending physical acceptance +status below. See the fork's `pocketbook/README.md` for commands and outputs. + ## Remaining validation - Validate the synchronized build on the device; its imports and emulated diff --git a/external/kobalt b/external/kobalt index 1a17e2f..c1e884e 160000 --- a/external/kobalt +++ b/external/kobalt @@ -1 +1 @@ -Subproject commit 1a17e2f08eccdffca00e100e28982188c5f4cb2e +Subproject commit c1e884e60293491986783a68fc8d62ee1f6eb012