diff --git a/README.md b/README.md index 08c02fc..377a76d 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Mosaic -A lightweight Linux system shell built with Qt 6 and Qt Quick for industrial +A lightweight Linux system shell built with Qt 6 Widgets for industrial embedded devices. ## Documentation diff --git a/apps/dummy-application/mosaic-app.json b/apps/dummy-application/mosaic-app.json index 41dbca6..8bcfe79 100644 --- a/apps/dummy-application/mosaic-app.json +++ b/apps/dummy-application/mosaic-app.json @@ -8,6 +8,7 @@ "license": "GPL-3.0-only", "license_file": "LICENSE", "app": { + "ui_toolkit": "quick", "source_dir": "app", "executable": "dummy-application", "arguments": [] diff --git a/docs/app-generation.rst b/docs/app-generation.rst index d79c918..e56e6e1 100644 --- a/docs/app-generation.rst +++ b/docs/app-generation.rst @@ -37,10 +37,12 @@ rendering invoke neither Cargo nor the network. Yocto fetches the crates, and Cargo retains its standard frozen build and executable installation tasks. Manifest build and runtime dependencies extend ``DEPENDS`` and ``RDEPENDS:${PN}``, -with duplicate names removed and lists sorted. Application build dependencies -include ``qtbase``, ``qtdeclarative``, and ``qtdeclarative-native``. Runtime -dependencies include ``qtbase-plugins``, ``qtdeclarative-qmlplugins``, and -``qtwayland-plugins``. Services receive no automatic Qt dependencies. +with duplicate names removed and lists sorted. ``app.ui_toolkit`` selects the +automatic Qt dependencies listed in :doc:`applications`. Widgets applications +do not automatically depend on Qt Quick. Services receive no automatic Qt +dependencies. Display-platform packages are supplied by the product session. +Linked libraries and upstream package recommendations also contribute to the +final image. Each application package depends on its required service packages, named ``mosaic-service--``. Service recipes append installation of @@ -59,9 +61,9 @@ Each application recipe generates Qt Application Manager (QAM) metadata during installation at ``${datadir}/mosaic/apps//info.yaml`` (normally ``/usr/share/mosaic/apps//info.yaml``). The application package includes this directory. See :doc:`applications` for the application authoring contract. -Shell integration must configure QAM discovery of this directory and display -installed application names. Native QAM applications require Wayland support -in the distro and compositor integration in the shell. +Generating this metadata does not install or start QAM. Multi-process display +integration requires a compositor, its client platform plugins, and a launcher +that discovers the installed application metadata. Tool files ---------- diff --git a/docs/applications.rst b/docs/applications.rst index 9e33af6..d2c1103 100644 --- a/docs/applications.rst +++ b/docs/applications.rst @@ -6,8 +6,8 @@ To add an application: 1. Create ``apps//`` with a ``mosaic-app.json`` manifest, Qt/CMake application code, and a license file. Use ``apps/dummy-application/`` as an example. -2. Set the manifest's application ID, display name, source directory, and - executable name. Add any dependencies and optional services. +2. Set the manifest's application ID, display name, source directory, executable + name and UI toolkit. Add any dependencies and optional services. 3. Add the CMake installation rules shown below, then build your chosen image using the commands in :doc:`building`. @@ -16,8 +16,27 @@ Build selection uses the manifest: - ``enabled``: enable or exclude the bundle in both profiles. - ``include_in_prod``: also include the enabled bundle in PROD. -Selection includes the bundle's required services. Applications start when -selected from the launcher after unlock. Services run independently. +Selection includes the bundle's required services. Services run independently. + +UI toolkit and display compatibility +------------------------------------ + +Mosaic's shell uses Qt Widgets. Application bundles may use Qt Widgets or +Qt Quick, selected by the required ``app.ui_toolkit`` field. This controls build +and runtime dependencies, not application launch or display ownership. + +The QEMU session uses a direct CPU-rendered display owned by the shell. Running +independent graphical applications alongside it requires a compositor and +launcher integration. The current session does not provide application +composition or guaranteed notification stacking. A future Wayland session must +supply its platform plugins and define the application window protocol. + +Applications must work without a GPU. For Qt Quick, use +``QT_QUICK_BACKEND=software`` and avoid shader and particle effects. Widgets +applications must also avoid GPU-dependent components such as ``QOpenGLWidget``. +Test scrolling, animation, scaling and responsiveness on the intended device. +See `Qt's software renderer limitations +`_. Manifest fields --------------- @@ -40,9 +59,11 @@ The following top-level fields are required: - ``license_file`` points to the bundle's license file. - ``app`` describes the application build and executable. -``app`` requires ``source_dir`` and ``executable``. The source directory contains -``CMakeLists.txt``. The executable is an installed program name, starting with -a letter or digit and containing only letters, digits, dots, underscores, +``app`` requires ``source_dir``, ``executable``, and ``ui_toolkit``. +``ui_toolkit`` accepts exactly ``"widgets"`` or ``"quick"``, with no default. +Missing or invalid values are errors, including in disabled bundles. The source +directory contains ``CMakeLists.txt``. The executable is an installed program +name, starting with a letter or digit and containing only letters, digits, dots, underscores, hyphens, or ``+``. Arguments are supplied separately in the optional ``arguments`` string list, which defaults to ``[]``. Empty argument strings are allowed, while NUL characters are rejected. @@ -52,9 +73,27 @@ Both ``app`` and each service accept optional ``build_dependencies`` and names and runtime package names respectively. Names start with a lowercase letter or digit and contain lowercase letters, digits, dots, hyphens, or ``+``. Entries are plain names without version constraints or BitBake expressions and must be -available in your configured Yocto layers. The application's standard Qt Quick -dependencies are already included. Declare any additional libraries, and declare -service dependencies separately. +available in your configured Yocto layers. The toolkit's standard dependencies +are included: + +.. list-table:: Automatic application dependencies + :header-rows: 1 + + * - ``ui_toolkit`` + - Build recipes + - Explicit runtime packages + * - ``widgets`` + - ``qtbase`` + - ``qtbase-plugins`` + * - ``quick`` + - ``qtbase``, ``qtdeclarative``, ``qtdeclarative-native`` + - ``qtbase-plugins``, ``qtdeclarative-qmlplugins`` + +Yocto also resolves linked-library dependencies and upstream package +recommendations. These are aggregate Qt packages, not individual plugin +selections. Additional dependencies extend this baseline and are deduplicated. +Declare service dependencies separately. Display-platform dependencies belong +to the product session. ``services`` is an optional list, defaulting to ``[]``. Each service requires: @@ -90,17 +129,36 @@ sets it from ``app.executable``. Install the target into cmake_minimum_required(VERSION 3.16) project(example LANGUAGES CXX) include(GNUInstallDirs) - find_package(Qt6 REQUIRED COMPONENTS Quick) + find_package(Qt6 6.5 REQUIRED COMPONENTS Widgets) set(MOSAIC_APP_EXECUTABLE "example" CACHE STRING "Installed application name") qt_add_executable(example_target main.cpp) - target_link_libraries(example_target PRIVATE Qt6::Quick) + target_link_libraries(example_target PRIVATE Qt6::Widgets) set_target_properties(example_target PROPERTIES OUTPUT_NAME "${MOSAIC_APP_EXECUTABLE}") install(TARGETS example_target RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}") install(DIRECTORY resources/ DESTINATION "${CMAKE_INSTALL_DATADIR}/mosaic/apps/example") +This example uses ``ui_toolkit: "widgets"``. Its ``main.cpp`` creates a +``QApplication`` and shows a widget. For ``ui_toolkit: "quick"``, replace the Qt +discovery and executable setup above with: + +.. code-block:: cmake + + find_package(Qt6 6.5 REQUIRED COMPONENTS Quick) + qt_standard_project_setup(REQUIRES 6.5) + qt_add_executable(example_target main.cpp) + qt_add_qml_module(example_target + URI Example + QML_FILES Main.qml) + target_link_libraries(example_target PRIVATE Qt6::Quick) + +Keep the same output-name and installation rules. The Quick entry point creates +a ``QGuiApplication`` and ``QQmlApplicationEngine``, then loads ``Example.Main`` +with ``engine.loadFromModule("Example", "Main")``. Declare additional Qt modules +in CMake when used. + The installed executable must match ``app.executable`` or the build fails. Example manifest diff --git a/docs/building.rst b/docs/building.rst index d013f21..fc066fa 100644 --- a/docs/building.rst +++ b/docs/building.rst @@ -158,7 +158,7 @@ QEMU startup QEMU images boot to ``graphical.target``, which starts ``mosaic-shell.service``. The service runs ``/usr/bin/mosaic-shell`` on VT1 as the locked, -non-login ``mosaic`` account with ``video``, ``input``, and ``render`` group membership. It waits +non-login ``mosaic`` account with ``video`` and ``input`` group membership. It waits for DRM card0, VT1, and systemd user-session initialization, and conflicts with ``getty@tty1.service``. If VT1 already has a controlling process, startup fails instead of waiting for that process to release it. Use the serial console for @@ -175,33 +175,67 @@ The product layer installs ``/etc/mosaic/session.env`` with: .. code-block:: text - QT_QPA_PLATFORM=eglfs - QT_QPA_EGLFS_INTEGRATION=eglfs_kms - QT_QPA_EGLFS_KMS_CONFIG=/etc/mosaic/eglfs_kms.json - QT_QPA_EGLFS_HIDECURSOR=1 - QSG_RHI_BACKEND=opengl - QT_SHADER_CACHE_PATH=/var/cache/mosaic - -These settings select EGLFS/KMS and Qt Quick's OpenGL renderer. The installed -``/etc/mosaic/eglfs_kms.json`` selects ``/dev/dri/card0`` and disables the hardware -cursor. The session hides the pointer and stores shader cache files in its -persistent cache directory. Udev rules grant the ``video`` group access to card0 -and the ``render`` group access to DRM render nodes. - -The QEMU distro override enables ``opengl``. The pinned Qt recipe then enables -``gbm``, ``gles2``, and ``eglfs`` alongside ``kms``, ``libinput``, and ``udev``; -Mesa provides EGL, GLES, GBM, and runtime drivers. The session includes Mesa's -runtime drivers explicitly because they are loaded dynamically, along with Qt -platform/QML plugins and DejaVu Sans. The standard virtio GPU uses Mesa's -software rendering fallback, so host GPU acceleration is not required. -This uses the OpenGL rendering pipeline rather than Qt Quick's software backend. -STM32 graphics configuration is unchanged. + QT_QPA_PLATFORM=linuxfb + QT_QPA_FB_DRM=1 + QT_QUICK_BACKEND=software + +The shell uses Qt Widgets raster painting. LinuxFB uses DRM dumb buffers and +page flipping instead of EGL/OpenGL. This requires DRM dumb-buffer support from +the display driver. Qt Quick applications use their dedicated software renderer. +The session owns the display directly and is not a multi-process compositor. + +The QEMU distro no longer enables OpenGL. The pinned Qt recipe supplies +``no-opengl``, ``linuxfb``, ``kms``, ``libinput`` and ``udev`` support. The session +selects Qt platform plugins and DejaVu Sans, without explicitly selecting Mesa +drivers or QML packages. Upstream Qt plugin packages remain aggregates with +their normal dependencies and recommendations. STM32 startup configuration is +unchanged. Product graphics policy belongs in ``meta-mosaic-product``, including the service, -environment file, KMS configuration, and udev rules under -``recipes-shell/mosaic-session/``. To change the display setup on a running image, -edit ``/etc/mosaic/session.env`` or ``/etc/mosaic/eglfs_kms.json`` and restart the service. -A missing environment file or failed display/QML startup fails the service. +environment file and udev rules under ``recipes-shell/mosaic-session/``. To change +the display setup, edit ``/etc/mosaic/session.env`` and restart the service. +A missing environment file or failed display startup fails the service. + +Native shell development +------------------------ + +With Qt 6.5 or newer, CMake, a C++17 compiler and PAM development files installed, +build the shell independently of Yocto from the repository root: + +.. code-block:: bash + + qt-cmake -S shell -B shell/build -DBUILD_TESTING=ON + cmake --build shell/build + ctest --test-dir shell/build --output-on-failure + ./shell/build/app + +Use the native desktop platform on macOS, without the Linux session environment. +The tests use a fake authentication backend. The application uses the real PAM +backend and does not provide a development password or bypass. Native UI checks +do not require changing the host's PAM configuration and do not establish target +authentication or graphics behavior. + +Target resource verification +---------------------------- + +On a Linux builder, inspect the production image package manifest with all +application bundles disabled. Confirm the shell-only dependency path does not +install Qt Quick/QML or Mesa. Repeat with a Widgets application and a Quick +application selected, checking that each includes its required services and +runtime. Investigate transitive dependencies and recommendations rather than +assuming that removing an explicit package removes it from the image. + +On the reference device, verify boot, display, keyboard/touch input and PAM. +Measure platform memory against the below-100 MiB normal-use and below-150 MiB +scenario limits. Include the kernel, required platform services, Qt and any +compositor, excluding product-specific applications and services. Report total +device memory separately, with consistent shared-memory and cache accounting. +Record image revision, resolution, workload, startup time and idle CPU usage. +When compositor and notification integration is available, verify critical-event +receipt-to-visible latency below one second while an application is active. + +Native builds cannot verify the final package closure, Linux display startup, +embedded memory savings or notification latency. Lock-screen authentication -------------------------- @@ -236,8 +270,7 @@ Distribution policy dev and prod policy. Mosaic uses Poky with glibc, systemd for init and device management, and journald for logging. -``MOSAIC_APP_LIFECYCLE`` selects the application lifecycle policy: -``terminate`` (default) for running only one application at a time, or -``background`` for keeping applications running in the background after closing -or switching away from them. Override it in ``build/conf/local.conf``. This changes -reponsiveness and resource use of the system based on needs. +``MOSAIC_APP_LIFECYCLE`` reserves the lifecycle policy values ``terminate`` +(default) and ``background``. The shell recipe validates this setting, but it +does not implement launching, switching or retaining applications. The intended +normal policy is to terminate the active application before starting another. diff --git a/docs/index.rst b/docs/index.rst index 7f363f0..804784a 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,10 +1,11 @@ Mosaic documentation ==================== -Mosaic is a lightweight Linux system shell built with Yocto Project, Qt 6 and -Qt Quick for industrial embedded devices. It provides a lock screen, application -launcher, status bar and shared notifications. Developers add their own Qt -applications and background services without changing the shell. +Mosaic is a lightweight Linux system shell built with Yocto Project and Qt 6 +Widgets for industrial embedded devices. Its architecture separates the shell, +user applications and background services. Developers package Qt Widgets or +software-rendered Qt Quick applications without changing shared shell source. +See :doc:`applications` for the toolkit and display integration contract. .. toctree:: :maxdepth: 2 diff --git a/docs/mosiac-structure.rst b/docs/mosiac-structure.rst index c1d7ce2..4b0eae1 100644 --- a/docs/mosiac-structure.rst +++ b/docs/mosiac-structure.rst @@ -1,15 +1,14 @@ Mosaic structure ================ -Mosaic is a lightweight Linux system shell built with Qt 6 and Qt Quick for +Mosaic is a lightweight Linux system shell built with Qt 6 Widgets for industrial embedded devices. Project layout -------------- ``shell/`` - The system shell, including the lock screen, home screen, application - launcher, and notification interface. + The Widgets system shell and its authentication/session logic. ``apps/`` User applications, each in its own folder. Applications must use Qt and @@ -24,6 +23,6 @@ Project layout ``docs/`` Project documentation. -Applications appear in the launcher after unlock and start when selected. -Systemd manages background service startup and operation independently of the -active application. +Application packaging supports Widgets and Quick. The direct display session +owns the screen, while independent application surfaces require compositor and +launcher integration. Systemd manages background services independently. diff --git a/shell/CMakeLists.txt b/shell/CMakeLists.txt index 2d643fb..622ede5 100644 --- a/shell/CMakeLists.txt +++ b/shell/CMakeLists.txt @@ -1,43 +1,37 @@ cmake_minimum_required(VERSION 3.16) - project(Mosaic LANGUAGES CXX) set(CMAKE_EXPORT_COMPILE_COMMANDS ON) - set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) -find_path(PAM_INCLUDE_DIR NAMES security/pam_appl.h) +find_path(PAM_INCLUDE_DIR NAMES security/pam_appl.h pam/pam_appl.h) find_library(PAM_LIBRARY NAMES pam) if(NOT PAM_INCLUDE_DIR OR NOT PAM_LIBRARY) - message(FATAL_ERROR "PAM development files not found. Install libpam0g-dev on Ubuntu, or provide PAM in the target sysroot.") + message(FATAL_ERROR "PAM development files not found") endif() -find_package(Qt6 6.5 REQUIRED COMPONENTS Quick QuickControls2 Concurrent) +find_package(Qt6 6.5 REQUIRED COMPONENTS Widgets Concurrent) qt_standard_project_setup(REQUIRES 6.5) -qt_add_executable(app src/main.cpp) - -set_source_files_properties(qml/Main.qml PROPERTIES QT_RESOURCE_ALIAS Main.qml) - -qt_add_qml_module(app - URI Mosaic - QML_FILES - qml/Main.qml - qml/Shell.qml - qml/LockScreen.qml - qml/HomeScreen.qml - SOURCES - src/session/sessionController.h - src/session/sessionController.cpp - - src/auth/AuthenticationBackend.h - src/auth/PamAuthenticationBackend.h - src/auth/PamAuthenticationBackend.cpp -) - -target_include_directories(app PRIVATE src/session src/auth ${PAM_INCLUDE_DIR}) - -target_link_libraries(app PRIVATE Qt6::Quick Qt6::QuickControls2 Qt6::Concurrent ${PAM_LIBRARY}) +qt_add_library(shell-ui STATIC + src/ShellWindow.cpp src/ShellWindow.h + src/session/sessionController.cpp src/session/sessionController.h + src/auth/AuthenticationBackend.h) +target_include_directories(shell-ui PUBLIC src src/session src/auth) +target_link_libraries(shell-ui PUBLIC Qt6::Widgets) +target_compile_options(shell-ui PRIVATE -Wall -Wextra) + +qt_add_executable(app src/main.cpp src/auth/PamAuthenticationBackend.cpp src/auth/PamAuthenticationBackend.h) +target_include_directories(app PRIVATE ${PAM_INCLUDE_DIR}) +target_link_libraries(app PRIVATE shell-ui Qt6::Concurrent ${PAM_LIBRARY}) target_compile_options(app PRIVATE -Wall -Wextra) + +include(CTest) +if(BUILD_TESTING) + find_package(Qt6 6.5 REQUIRED COMPONENTS Test) + qt_add_executable(session-test tests/sessionTest.cpp) + target_link_libraries(session-test PRIVATE shell-ui Qt6::Test) + add_test(NAME session COMMAND session-test -platform offscreen) +endif() diff --git a/shell/qml/HomeScreen.qml b/shell/qml/HomeScreen.qml deleted file mode 100644 index 9d7ec86..0000000 --- a/shell/qml/HomeScreen.qml +++ /dev/null @@ -1,12 +0,0 @@ -import QtQuick - -Item { - anchors.fill: parent - - Text { - text: "HOME" - color: "black" - font.pixelSize: 44 - anchors.centerIn: parent - } -} diff --git a/shell/qml/LockScreen.qml b/shell/qml/LockScreen.qml deleted file mode 100644 index 8707bdf..0000000 --- a/shell/qml/LockScreen.qml +++ /dev/null @@ -1,88 +0,0 @@ -import QtQuick -import QtQuick.Controls - -Rectangle { - id: root - color: "white" - - property string errorMessage: "" - signal unlockRequested(string password) - - function resetPasswordInput() { - passwordField.clear() - - if (root.visible) - passwordField.forceActiveFocus() - } - - function submitPassword() { - const password = passwordField.text - - if (password.length === 0) - return - - passwordField.clear() - root.unlockRequested(password) - - Qt.callLater(root.resetPasswordInput) - } - - Component.onCompleted: Qt.callLater(root.resetPasswordInput) - onVisibleChanged: Qt.callLater(root.resetPasswordInput) - - Column { - anchors.centerIn: parent - width: Math.max(0, Math.min(320, root.width - 40)) - spacing: 16 - - Label { - id: clockLabel - property date currentTime: new Date() - - text: Qt.formatTime(currentTime, "hh:mm") - color: "Black" - font.pixelSize: 64 - anchors.horizontalCenter: parent.horizontalCenter - - Timer { - interval: 1000 - repeat: true - running: root.visible - triggeredOnStart: true - onTriggered: clockLabel.currentTime = new Date() - } - } - - TextField { - id: passwordField - width: parent.width - height: width / 3 - - - horizontalAlignment: TextInput.AlignHCenter - placeholderText: qsTr("****") - - font.pixelSize: 34 - color: "black" - background: Rectangle { - color: "white" - border.width: 2 - border.color: "gainsboro" - } - - echoMode: TextInput.Password - inputMethodHints: Qt.ImhHiddenText | Qt.ImhSensitiveData | Qt.ImhNoPredictiveText - - onAccepted: root.submitPassword() - } - - Label { - width: parent.width - text: root.errorMessage - visible: text.length > 0 - color: "red" - wrapMode: Text.WordWrap - horizontalAlignment: Text.AlignHCenter - } - } -} diff --git a/shell/qml/Main.qml b/shell/qml/Main.qml deleted file mode 100644 index 85d9b21..0000000 --- a/shell/qml/Main.qml +++ /dev/null @@ -1,14 +0,0 @@ -import QtQuick -import QtQuick.Window - -Window { - width: 640 - height: 480 - visible: true - title: qsTr("Mosaic") - color: "white" - - Shell { - anchors.fill: parent - } -} diff --git a/shell/qml/Shell.qml b/shell/qml/Shell.qml deleted file mode 100644 index 6b55270..0000000 --- a/shell/qml/Shell.qml +++ /dev/null @@ -1,23 +0,0 @@ -import QtQuick -import Mosaic - -Item { - HomeScreen { - anchors.fill: parent - visible: !SessionController.locked - enabled: !SessionController.locked - } - - LockScreen { - anchors.fill: parent - z: 1 - - visible: SessionController.locked - enabled: SessionController.locked - errorMessage: SessionController.error_msg - - onUnlockRequested: (password) => { - SessionController.requestUnlock(password) - } - } -} diff --git a/shell/src/ShellWindow.cpp b/shell/src/ShellWindow.cpp new file mode 100644 index 0000000..17724d1 --- /dev/null +++ b/shell/src/ShellWindow.cpp @@ -0,0 +1,97 @@ +#include "ShellWindow.h" +#include "sessionController.h" +#include +#include +#include +#include +#include + +ShellWindow::ShellWindow(SessionController &session, QWidget *parent) + : QWidget(parent), session(session), pages(new QStackedWidget(this)), + password(new QLineEdit), clock(new QLabel), error(new QLabel) +{ + setWindowTitle(tr("Mosaic")); + resize(640, 480); + setStyleSheet("QWidget { background: white; color: black; }" + "QLineEdit { border: 2px solid gainsboro; padding: 12px; }"); + auto *layout = new QVBoxLayout(this); + layout->setContentsMargins(0, 0, 0, 0); + layout->addWidget(pages); + auto *lock = new QWidget; + auto *lockLayout = new QVBoxLayout(lock); + lockLayout->setContentsMargins(20, 20, 20, 20); + lockLayout->setSpacing(16); + lockLayout->addStretch(); + clock->setAlignment(Qt::AlignCenter); + clock->setStyleSheet("font-size: 64px;"); + lockLayout->addWidget(clock); + password->setObjectName("password"); + password->setMaximumWidth(320); + password->setAlignment(Qt::AlignCenter); + password->setPlaceholderText("****"); + password->setStyleSheet("font-size: 34px;"); + password->setEchoMode(QLineEdit::Password); + password->setInputMethodHints(Qt::ImhHiddenText | Qt::ImhSensitiveData | Qt::ImhNoPredictiveText); + lockLayout->addWidget(password, 0, Qt::AlignHCenter); + error->setObjectName("authenticationError"); + error->setWordWrap(true); + error->setAlignment(Qt::AlignCenter); + lockLayout->addWidget(error); + lockLayout->addStretch(); + pages->addWidget(lock); + auto *home = new QLabel(tr("HOME")); + home->setAlignment(Qt::AlignCenter); + home->setStyleSheet("font-size: 44px;"); + pages->addWidget(home); + clockTimer.setSingleShot(true); + clockTimer.setTimerType(Qt::PreciseTimer); + connect(&clockTimer, &QTimer::timeout, this, &ShellWindow::refreshClock); + connect(&session, &SessionController::stateChanged, this, &ShellWindow::refreshSession); + connect(&session, &SessionController::errorMsgChanged, this, [this] { + error->setText(this->session.getErrorMsg()); + }); + connect(password, &QLineEdit::returnPressed, this, [this] { + const QString input = password->text(); + password->clear(); + if (input.isEmpty()) + return; + this->session.requestUnlock(input); + password->setFocus(); + }); + refreshSession(); +} + +void ShellWindow::refreshSession() +{ + const bool locked = session.isLocked(); + pages->setCurrentIndex(locked ? 0 : 1); + password->clear(); + password->setEnabled(session.state() == SessionController::State::LOCKED); + if (locked) { + password->setFocus(); + refreshClock(); + } else { + clockTimer.stop(); + } +} + +void ShellWindow::refreshClock() +{ + if (!isVisible() || !session.isLocked()) + return; + const QDateTime now = QDateTime::currentDateTime(); + clock->setText(now.toString("hh:mm")); + clockTimer.start(60000 - (now.time().second() * 1000 + now.time().msec())); +} + +void ShellWindow::showEvent(QShowEvent *event) +{ + QWidget::showEvent(event); + refreshSession(); +} + +void ShellWindow::hideEvent(QHideEvent *event) +{ + clockTimer.stop(); + QWidget::hideEvent(event); +} diff --git a/shell/src/ShellWindow.h b/shell/src/ShellWindow.h new file mode 100644 index 0000000..23712b1 --- /dev/null +++ b/shell/src/ShellWindow.h @@ -0,0 +1,29 @@ +#pragma once + +#include +#include + +class SessionController; +class QLabel; +class QLineEdit; +class QStackedWidget; + +class ShellWindow : public QWidget +{ +public: + explicit ShellWindow(SessionController &session, QWidget *parent = nullptr); + +protected: + void showEvent(QShowEvent *event) override; + void hideEvent(QHideEvent *event) override; + +private: + void refreshSession(); + void refreshClock(); + SessionController &session; + QStackedWidget *pages; + QLineEdit *password; + QLabel *clock; + QLabel *error; + QTimer clockTimer; +}; diff --git a/shell/src/main.cpp b/shell/src/main.cpp index 8a56b47..10941b3 100644 --- a/shell/src/main.cpp +++ b/shell/src/main.cpp @@ -1,15 +1,15 @@ -#include -#include +#include "PamAuthenticationBackend.h" +#include "sessionController.h" +#include "ShellWindow.h" +#include int main(int argc, char *argv[]) { - QGuiApplication app(argc, argv); - QQmlApplicationEngine engine; - - QObject::connect(&engine, &QQmlApplicationEngine::quit, &app, &QGuiApplication::quit); - QObject::connect(&engine, &QQmlApplicationEngine::quit, &app, &QGuiApplication::quit); - - engine.loadFromModule("Mosaic", "Main"); - + QApplication app(argc, argv); + QApplication::setStyle("Fusion"); + PamAuthenticationBackend backend; + SessionController session(backend); + ShellWindow window(session); + window.show(); return app.exec(); } diff --git a/shell/src/session/sessionController.cpp b/shell/src/session/sessionController.cpp index 69e832d..0b84b7a 100644 --- a/shell/src/session/sessionController.cpp +++ b/shell/src/session/sessionController.cpp @@ -1,7 +1,7 @@ #include "sessionController.h" -SessionController::SessionController(QObject *parent) - : QObject(parent) +SessionController::SessionController(AuthenticationBackend &backend, QObject *parent) + : QObject(parent), authenticationBackend(backend) { connect(&authenticationBackend, &AuthenticationBackend::authenticationFinished, @@ -41,6 +41,7 @@ bool SessionController::isLocked() const void SessionController::lock() { ++m_requestId; + setErrorMsg(""); setState(State::LOCKED); } diff --git a/shell/src/session/sessionController.h b/shell/src/session/sessionController.h index 37771e0..e33a607 100644 --- a/shell/src/session/sessionController.h +++ b/shell/src/session/sessionController.h @@ -1,15 +1,12 @@ #pragma once -#include "PamAuthenticationBackend.h" +#include "AuthenticationBackend.h" #include #include -#include class SessionController : public QObject { Q_OBJECT - QML_ELEMENT - QML_SINGLETON Q_PROPERTY(bool locked READ isLocked NOTIFY stateChanged) Q_PROPERTY(QString error_msg READ getErrorMsg NOTIFY errorMsgChanged) @@ -18,20 +15,21 @@ class SessionController : public QObject enum class State { LOCKED, AUTHENTICATING, UNLOCKED }; Q_ENUM(State) - explicit SessionController(QObject *parent = nullptr); + explicit SessionController(AuthenticationBackend &backend, QObject *parent = nullptr); QString getErrorMsg() const; bool isLocked() const; + State state() const { return m_state; } - Q_INVOKABLE void lock(); - Q_INVOKABLE void requestUnlock(const QString &password); + void lock(); + void requestUnlock(const QString &password); signals: void stateChanged(); void errorMsgChanged(); private: - PamAuthenticationBackend authenticationBackend; + AuthenticationBackend &authenticationBackend; quint64 m_requestId = 0; State m_state = State::LOCKED; diff --git a/shell/tests/sessionTest.cpp b/shell/tests/sessionTest.cpp new file mode 100644 index 0000000..1646b10 --- /dev/null +++ b/shell/tests/sessionTest.cpp @@ -0,0 +1,88 @@ +#include "ShellWindow.h" +#include "sessionController.h" +#include +#include +#include +#include + +class FakeBackend : public AuthenticationBackend +{ +public: + quint64 id = 0; + int calls = 0; + QString input; + void authenticate(quint64 request, QString password) override + { + id = request; + input = password; + ++calls; + } + void finish(quint64 request, Result result) { emit authenticationFinished(request, result); } +}; + +class SessionTest : public QObject +{ + Q_OBJECT +private slots: + void transitions() + { + FakeBackend backend; + SessionController session(backend); + QVERIFY(session.isLocked()); + session.requestUnlock(""); + QCOMPARE(backend.calls, 0); + QVERIFY(!session.getErrorMsg().isEmpty()); + session.requestUnlock("wrong"); + QCOMPARE(session.state(), SessionController::State::AUTHENTICATING); + session.requestUnlock("duplicate"); + QCOMPARE(backend.calls, 1); + backend.finish(backend.id, AuthenticationBackend::Result::REJECTED); + QCOMPARE(session.state(), SessionController::State::LOCKED); + QCOMPARE(session.getErrorMsg(), QString("Authentication rejected")); + session.requestUnlock("password"); + const auto stale = backend.id; + session.lock(); + session.requestUnlock("new password"); + backend.finish(stale, AuthenticationBackend::Result::SUCCESS); + QCOMPARE(session.state(), SessionController::State::AUTHENTICATING); + backend.finish(backend.id, AuthenticationBackend::Result::BACKENDFAILURE); + QCOMPARE(session.getErrorMsg(), QString("Authentication unavailable")); + session.requestUnlock("password"); + backend.finish(backend.id, AuthenticationBackend::Result::SUCCESS); + QVERIFY(!session.isLocked()); + QVERIFY(session.getErrorMsg().isEmpty()); + } + void submission() + { + FakeBackend backend; + SessionController session(backend); + ShellWindow window(session); + window.show(); + auto *field = window.findChild("password"); + QCOMPARE(field->echoMode(), QLineEdit::Password); + QTest::keyClick(field, Qt::Key_Return); + QCOMPARE(backend.calls, 0); + window.resize(480, 640); + field->setText("secret"); + QTest::keyClick(field, Qt::Key_Return); + QCOMPARE(backend.input, QString("secret")); + QVERIFY(field->text().isEmpty()); + QVERIFY(!field->isEnabled()); + backend.finish(backend.id, AuthenticationBackend::Result::REJECTED); + QVERIFY(field->isEnabled()); + QTRY_VERIFY(field->hasFocus()); + QCOMPARE(window.findChild("authenticationError")->text(), QString("Authentication rejected")); + backend.finish(backend.id, AuthenticationBackend::Result::SUCCESS); + QVERIFY(session.isLocked()); + field->setText("correct"); + QTest::keyClick(field, Qt::Key_Return); + backend.finish(backend.id, AuthenticationBackend::Result::SUCCESS); + QCOMPARE(window.findChild()->currentIndex(), 1); + session.lock(); + QCOMPARE(window.findChild()->currentIndex(), 0); + QVERIFY(field->text().isEmpty()); + QTRY_VERIFY(field->hasFocus()); + } +}; +QTEST_MAIN(SessionTest) +#include "sessionTest.moc" diff --git a/tools/app_layer.py b/tools/app_layer.py index 42c4d70..b81ce47 100644 --- a/tools/app_layer.py +++ b/tools/app_layer.py @@ -44,12 +44,15 @@ def render_application_files(manifest: Manifest) -> list[tuple[Path, str]]: package = f"mosaic-app-{manifest.id}" unpacked = "${UNPACKDIR}/" + bundle.name + quick = manifest.app.ui_toolkit == "quick" build_dependencies = sorted({ - "qtbase", "qtdeclarative", "qtdeclarative-native", + "qtbase", + *(["qtdeclarative", "qtdeclarative-native"] if quick else []), *manifest.app.build_dependencies, }) runtime_dependencies = sorted({ - "qtbase-plugins", "qtdeclarative-qmlplugins", "qtwayland-plugins", + "qtbase-plugins", + *(["qtdeclarative-qmlplugins"] if quick else []), *manifest.app.runtime_dependencies, *(f"mosaic-service-{manifest.id}-{service.id}" for service in manifest.services), }) diff --git a/tools/app_manifest.py b/tools/app_manifest.py index 47c9382..5183004 100644 --- a/tools/app_manifest.py +++ b/tools/app_manifest.py @@ -84,6 +84,7 @@ class Component(ManifestModel): class Application(Component): + ui_toolkit: Literal["widgets", "quick"] source_dir: Annotated[SourceDirectory, AfterValidator(_cmake_directory)] executable: Annotated[Text, Field(pattern=r"^[A-Za-z0-9][A-Za-z0-9._+-]*$")] arguments: list[Argument] = Field(default_factory=list) diff --git a/tools/tests/test_app_layer.py b/tools/tests/test_app_layer.py index 28d638e..80ae723 100644 --- a/tools/tests/test_app_layer.py +++ b/tools/tests/test_app_layer.py @@ -19,7 +19,9 @@ def manifest() -> Manifest: return manifest.model_copy(update={"enabled": True}) -def test_layer_profile_selection(manifest: Manifest) -> None: +@pytest.mark.parametrize("toolkit", ["widgets", "quick"]) +def test_layer_profile_selection(manifest: Manifest, toolkit: str) -> None: + manifest.app.ui_toolkit = toolkit manifests = [ manifest.model_copy(update={"id": name, "enabled": enabled, "include_in_prod": prod}) for name, enabled, prod in [ diff --git a/tools/tests/test_app_manifest.py b/tools/tests/test_app_manifest.py index f4127ea..d7f7fb1 100644 --- a/tools/tests/test_app_manifest.py +++ b/tools/tests/test_app_manifest.py @@ -53,6 +53,7 @@ def test_load_example(manifest_path: Path, data: dict[str, object]) -> None: assert manifest.id == "dummy-application" assert manifest.app.source_dir == manifest_path.parent / "app" assert manifest.services[0].unit[0].is_absolute() + assert manifest.app.ui_toolkit == "quick" assert manifest.app.build_dependencies == [] assert manifest.manifest_path == manifest_path assert {name: {"settings": profile.settings} for name, profile in manifest.profiles.items()} == data["profiles"] @@ -92,6 +93,10 @@ def test_dependencies_arguments_and_units(manifest_path: Path, data: dict[str, o (("app",), [], "app"), (("app", "source_dir"), "missing", "app.source_dir"), (("app", "source_dir"), "tests", "app.source_dir"), + (("app", "ui_toolkit"), "qml", "app.ui_toolkit"), + (("app", "ui_toolkit"), "", "app.ui_toolkit"), + (("app", "ui_toolkit"), None, "app.ui_toolkit"), + (("app", "ui_toolkit"), True, "app.ui_toolkit"), (("app", "executable"), "app --flag", "app.executable"), (("app", "arguments"), "--flag", "app.arguments"), (("app", "arguments"), [False], "app.arguments[0]"), @@ -224,3 +229,17 @@ def test_empty_apps_directory(tmp_path: Path) -> None: def test_missing_apps_directory(tmp_path: Path) -> None: with pytest.raises(ManifestError, match="apps_dir"): _ = load_manifests(tmp_path / "missing") + + +@pytest.mark.parametrize("toolkit", ["widgets", "quick"]) +def test_ui_toolkit(manifest_path: Path, data: dict[str, object], toolkit: str) -> None: + set_value(data, ("app", "ui_toolkit"), toolkit) + write_manifest(manifest_path, data) + assert load_manifest(manifest_path).app.ui_toolkit == toolkit + + +def test_missing_ui_toolkit(manifest_path: Path, data: dict[str, object]) -> None: + del cast(dict[str, object], data["app"])["ui_toolkit"] + write_manifest(manifest_path, data) + with pytest.raises(ManifestError, match=r"manifest\.app\.ui_toolkit.*required"): + _ = load_manifest(manifest_path) diff --git a/tools/tests/test_application_recipe.py b/tools/tests/test_application_recipe.py index 4f10b0b..a5f0f76 100644 --- a/tools/tests/test_application_recipe.py +++ b/tools/tests/test_application_recipe.py @@ -12,7 +12,7 @@ import pytest -from tools.app_layer import render_application_files +from tools.app_layer import render_application_files, render_service_files from tools.app_manifest import Manifest, load_manifest @@ -38,7 +38,7 @@ def manifest(tmp_path: Path) -> Manifest: "schema_version": 1, "id": "example", "name": "Example æøå", "version": "1.2.3", "enabled": True, "include_in_prod": False, "license": "MIT", "license_file": "LICENSE", - "app": {"source_dir": "src/gui", "executable": "visible-app"}, + "app": {"ui_toolkit": "quick", "source_dir": "src/gui", "executable": "visible-app"}, }), encoding="utf-8") return load_manifest(path) @@ -69,7 +69,9 @@ def read_metadata(path: Path) -> dict: return json.loads(content.removeprefix(header)) -def test_recipe_and_no_writes(manifest: Manifest) -> None: +@pytest.mark.parametrize("toolkit", ["widgets", "quick"]) +def test_recipe_and_no_writes(manifest: Manifest, toolkit: str) -> None: + manifest.app.ui_toolkit = toolkit bundle = manifest.manifest_path.parent before = {path: path.read_bytes() for path in bundle.rglob("*") if path.is_file()} files = render_application_files(manifest) @@ -84,8 +86,11 @@ def test_recipe_and_no_writes(manifest: Manifest) -> None: assert 'SRC_URI = "file://bundle;unpack=0"' in recipe assert 'S = "${UNPACKDIR}/bundle/src/gui"' in recipe assert 'inherit qt6-cmake' in recipe - assert 'DEPENDS += "qtbase qtdeclarative qtdeclarative-native"' in recipe - assert 'RDEPENDS:${PN} += "qtbase-plugins qtdeclarative-qmlplugins qtwayland-plugins"' in recipe + build = "qtbase qtdeclarative qtdeclarative-native" if toolkit == "quick" else "qtbase" + runtime = "qtbase-plugins qtdeclarative-qmlplugins" if toolkit == "quick" else "qtbase-plugins" + assert f'DEPENDS += "{build}"' in recipe + assert f'RDEPENDS:${{PN}} += "{runtime}"' in recipe + assert "qtwayland-plugins" not in recipe assert 'EXTRA_OECMAKE += "-DMOSAIC_APP_EXECUTABLE=visible-app"' in recipe assert 'do_install[postfuncs] += "mosaic_install_app_metadata"' in recipe assert 'FILES:${PN} += "${datadir}/mosaic/apps/example"' in recipe @@ -94,11 +99,13 @@ def test_recipe_and_no_writes(manifest: Manifest) -> None: assert list(bundle.parent.iterdir()) == [bundle] -def test_dependencies_and_selection_boundary(manifest: Manifest) -> None: +@pytest.mark.parametrize("toolkit", ["widgets", "quick"]) +def test_dependencies_and_selection_boundary(manifest: Manifest, toolkit: str) -> None: path = manifest.manifest_path data = json.loads(path.read_text()) - data["app"]["build_dependencies"] = ["zlib", "qtbase", "zlib"] - data["app"]["runtime_dependencies"] = ["zlib", "qtwayland-plugins", "zlib"] + data["app"]["ui_toolkit"] = toolkit + data["app"]["build_dependencies"] = ["zlib", "qtbase", "qtdeclarative", "qtdeclarative-native", "zlib"] + data["app"]["runtime_dependencies"] = ["zlib", "qtbase-plugins", "qtdeclarative-qmlplugins", "qtwayland-plugins", "zlib"] data["services"] = [] for service_id in ("telemetry", "alarm"): unit = path.parent / f"{service_id}.service" @@ -110,7 +117,15 @@ def test_dependencies_and_selection_boundary(manifest: Manifest) -> None: }) data["app"]["runtime_dependencies"].append("mosaic-service-example-alarm") path.write_text(json.dumps(data), encoding="utf-8") - files = render_application_files(load_manifest(path)) + validated = load_manifest(path) + for service in validated.services: + service_recipe = render_service_files(validated, service)[0][1] + assert 'DEPENDS += "service-build-only"' in service_recipe + assert 'RDEPENDS:${PN} += "service-runtime-only"' in service_recipe + assert "qtbase" not in service_recipe + assert "qtdeclarative" not in service_recipe + assert "qtwayland" not in service_recipe + files = render_application_files(validated) recipe = files[0][1] assert 'DEPENDS += "qtbase qtdeclarative qtdeclarative-native zlib"' in recipe assert 'RDEPENDS:${PN} += "mosaic-service-example-alarm mosaic-service-example-telemetry qtbase-plugins qtdeclarative-qmlplugins qtwayland-plugins zlib"' in recipe diff --git a/tools/tests/test_service_recipe.py b/tools/tests/test_service_recipe.py index 84a40a1..9c3e616 100644 --- a/tools/tests/test_service_recipe.py +++ b/tools/tests/test_service_recipe.py @@ -28,7 +28,7 @@ def bundle(tmp_path: Path) -> Path: (bundle / "mosaic-app.json").write_text(json.dumps({ "schema_version": 1, "id": "example", "name": "Example", "version": "1.2.3", "enabled": True, "include_in_prod": False, "license": "MIT", "license_file": "LICENSE", - "app": {"source_dir": "app", "executable": "example"}, + "app": {"ui_toolkit": "widgets", "source_dir": "app", "executable": "example"}, "services": [{"id": "worker", "source_dir": "service", "unit": "worker.service", "start": "boot"}], })) return bundle diff --git a/yocto/layers/meta-mosaic-product/README b/yocto/layers/meta-mosaic-product/README index a34d196..b4922da 100644 --- a/yocto/layers/meta-mosaic-product/README +++ b/yocto/layers/meta-mosaic-product/README @@ -16,9 +16,8 @@ without this graphical startup package. mosaic-session installs and enables mosaic-shell.service on graphical.target. The QEMU image boots to graphical.target in both dev and prod profiles. The service runs /usr/bin/mosaic-shell as the locked, non-login mosaic account, -with video, input, and render group membership. A udev rule grants that account +with video and input group membership. A udev rule grants that account exclusive access to tty1 and grants the video group read/write access to DRM card0. -DRM render nodes are accessible to the render group. Input access uses the standard udev input group permissions. The service waits for card0, tty1, and systemd-user-sessions.service. It owns VT1 @@ -28,16 +27,15 @@ systemd creates /run/mosaic with mode 0700 and sets XDG_RUNTIME_DIR to that path It also creates /var/lib/mosaic with mode 0750 as HOME and /var/cache/mosaic with mode 0700 as XDG_CACHE_HOME. Both persist across service restarts and boots. -/etc/mosaic/session.env selects Qt EGLFS with the eglfs_kms integration and -the OpenGL Qt Quick renderer. /etc/mosaic/eglfs_kms.json selects DRM card0 and -disables the hardware cursor; the session hides the pointer entirely. Shader -cache files use /var/cache/mosaic. +/etc/mosaic/session.env selects LinuxFB with DRM dumb buffers for CPU raster +painting and QT_QUICK_BACKEND=software for Quick applications. The display +driver must support dumb buffers. The session owns the display directly and +does not compose independent application processes. -The QEMU distro override enables opengl, which selects EGL, GLES, GBM, and EGLFS -in the pinned Qt/Mesa recipes. The session includes Mesa's runtime drivers, -Qt platform/QML plugins, and DejaVu Sans. The standard virtio GPU uses Mesa's -software rendering fallback; host GPU acceleration is not required. STM32 -graphics policy is unchanged. Mosaic owns the display directly through EGLFS. +QEMU no longer enables the OpenGL distro feature. The session selects Qt +platform plugins and DejaVu Sans without explicit Mesa drivers or QML packages. +Qt packages remain upstream aggregates with their normal dependencies and +recommendations. STM32 startup policy is unchanged. Output goes to journald. Failed processes restart after two seconds, with at most five starts in a 60-second interval. After correcting a persistent failure, use diff --git a/yocto/layers/meta-mosaic-product/conf/distro/mosaic.conf b/yocto/layers/meta-mosaic-product/conf/distro/mosaic.conf index d0dfa9f..da98e6b 100644 --- a/yocto/layers/meta-mosaic-product/conf/distro/mosaic.conf +++ b/yocto/layers/meta-mosaic-product/conf/distro/mosaic.conf @@ -22,9 +22,6 @@ FULL_OPTIMIZATION = "-Os ${DEBUG_LEVELFLAG}" DISTRO_FEATURES_OPTED_OUT = "*" DISTRO_FEATURES = "pci ldconfig pam ptest multiarch ipv4 ipv6 wayland" -# The QEMU session uses Qt EGLFS/KMS with Mesa EGL, GLES, and GBM. -DISTRO_FEATURES:append:qemuarm64 = " opengl" - PACKAGECONFIG:remove:pn-systemd = "hibernate localed machined nss-mymachines" USE_NLS = "no" diff --git a/yocto/layers/meta-mosaic-product/recipes-shell/mosaic-session/files/70-mosaic-session.rules b/yocto/layers/meta-mosaic-product/recipes-shell/mosaic-session/files/70-mosaic-session.rules index 9fc48be..8619c07 100644 --- a/yocto/layers/meta-mosaic-product/recipes-shell/mosaic-session/files/70-mosaic-session.rules +++ b/yocto/layers/meta-mosaic-product/recipes-shell/mosaic-session/files/70-mosaic-session.rules @@ -1,3 +1,2 @@ SUBSYSTEM=="tty", KERNEL=="tty1", OWNER="mosaic", GROUP="mosaic", MODE="0600", TAG+="systemd" SUBSYSTEM=="drm", KERNEL=="card0", GROUP="video", MODE="0660", TAG+="systemd" -SUBSYSTEM=="drm", KERNEL=="renderD*", GROUP="render", MODE="0660" diff --git a/yocto/layers/meta-mosaic-product/recipes-shell/mosaic-session/files/eglfs_kms.json b/yocto/layers/meta-mosaic-product/recipes-shell/mosaic-session/files/eglfs_kms.json deleted file mode 100644 index db7a503..0000000 --- a/yocto/layers/meta-mosaic-product/recipes-shell/mosaic-session/files/eglfs_kms.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "device": "/dev/dri/card0", - "hwcursor": false -} diff --git a/yocto/layers/meta-mosaic-product/recipes-shell/mosaic-session/files/mosaic-shell.service b/yocto/layers/meta-mosaic-product/recipes-shell/mosaic-session/files/mosaic-shell.service index 77e860d..1381b91 100644 --- a/yocto/layers/meta-mosaic-product/recipes-shell/mosaic-session/files/mosaic-shell.service +++ b/yocto/layers/meta-mosaic-product/recipes-shell/mosaic-session/files/mosaic-shell.service @@ -11,7 +11,7 @@ StartLimitBurst=5 Type=simple User=mosaic Group=mosaic -SupplementaryGroups=video input render +SupplementaryGroups=video input StateDirectory=mosaic StateDirectoryMode=0750 CacheDirectory=mosaic diff --git a/yocto/layers/meta-mosaic-product/recipes-shell/mosaic-session/files/session.env b/yocto/layers/meta-mosaic-product/recipes-shell/mosaic-session/files/session.env index de5a5cc..06ae42e 100644 --- a/yocto/layers/meta-mosaic-product/recipes-shell/mosaic-session/files/session.env +++ b/yocto/layers/meta-mosaic-product/recipes-shell/mosaic-session/files/session.env @@ -1,6 +1,3 @@ -QT_QPA_PLATFORM=eglfs -QT_QPA_EGLFS_INTEGRATION=eglfs_kms -QT_QPA_EGLFS_KMS_CONFIG=/etc/mosaic/eglfs_kms.json -QT_QPA_EGLFS_HIDECURSOR=1 -QSG_RHI_BACKEND=opengl -QT_SHADER_CACHE_PATH=/var/cache/mosaic +QT_QPA_PLATFORM=linuxfb +QT_QPA_FB_DRM=1 +QT_QUICK_BACKEND=software diff --git a/yocto/layers/meta-mosaic-product/recipes-shell/mosaic-session/mosaic-session_0.1.bb b/yocto/layers/meta-mosaic-product/recipes-shell/mosaic-session/mosaic-session_0.1.bb index 4b28ad8..9e6489f 100644 --- a/yocto/layers/meta-mosaic-product/recipes-shell/mosaic-session/mosaic-session_0.1.bb +++ b/yocto/layers/meta-mosaic-product/recipes-shell/mosaic-session/mosaic-session_0.1.bb @@ -5,7 +5,6 @@ LIC_FILES_CHKSUM = "file://${COMMON_LICENSE_DIR}/GPL-3.0-only;md5=c79ff39f19dfec SRC_URI = " \ file://mosaic-shell.service \ file://session.env \ - file://eglfs_kms.json \ file://70-mosaic-session.rules \ " S = "${UNPACKDIR}" @@ -14,11 +13,11 @@ COMPATIBLE_MACHINE = "^qemuarm64$" inherit systemd useradd -RDEPENDS:${PN} = "shell qtbase-plugins qtdeclarative-qmlplugins mesa-megadriver ttf-dejavu-sans" +RDEPENDS:${PN} = "shell qtbase-plugins ttf-dejavu-sans" USERADD_PACKAGES = "${PN}" -GROUPADD_PARAM:${PN} = "--system mosaic; --system render" -USERADD_PARAM:${PN} = "--system --gid mosaic --groups video,input,render --home-dir /var/lib/mosaic \ +GROUPADD_PARAM:${PN} = "--system mosaic" +USERADD_PARAM:${PN} = "--system --gid mosaic --groups video,input --home-dir /var/lib/mosaic \ --no-create-home --shell ${base_sbindir}/nologin --password '!' mosaic" SYSTEMD_SERVICE:${PN} = "mosaic-shell.service" @@ -28,9 +27,8 @@ do_install() { install -d ${D}${systemd_system_unitdir} ${D}${sysconfdir}/mosaic ${D}${nonarch_libdir}/udev/rules.d install -m 0644 ${UNPACKDIR}/mosaic-shell.service ${D}${systemd_system_unitdir}/mosaic-shell.service install -m 0644 ${UNPACKDIR}/session.env ${D}${sysconfdir}/mosaic/session.env - install -m 0644 ${UNPACKDIR}/eglfs_kms.json ${D}${sysconfdir}/mosaic/eglfs_kms.json install -m 0644 ${UNPACKDIR}/70-mosaic-session.rules ${D}${nonarch_libdir}/udev/rules.d/70-mosaic-session.rules } -CONFFILES:${PN} = "${sysconfdir}/mosaic/session.env ${sysconfdir}/mosaic/eglfs_kms.json" +CONFFILES:${PN} = "${sysconfdir}/mosaic/session.env" FILES:${PN} += "${systemd_system_unitdir}/mosaic-shell.service ${nonarch_libdir}/udev/rules.d/70-mosaic-session.rules" diff --git a/yocto/layers/meta-mosaic/README b/yocto/layers/meta-mosaic/README index 55bfa89..f53de5a 100644 --- a/yocto/layers/meta-mosaic/README +++ b/yocto/layers/meta-mosaic/README @@ -40,7 +40,7 @@ Layer contents shell: Builds the local shell/ sources with Qt 6 and installs the executable as /usr/bin/mosaic-shell. The recipe depends on libpam for the lock screen's authentication backend. Runtime dependencies - include Qt platform plugins, Qt Quick QML imports, and libpam-runtime + include Qt platform plugins and libpam-runtime for PAM's common configuration, authentication modules, and helper. It installs /etc/pam.d/mosaic-shell and marks it as package configuration. The backend uses this policy's common-auth and common-account stacks diff --git a/yocto/layers/meta-mosaic/recipes-shell/shell/shell_0.1.bb b/yocto/layers/meta-mosaic/recipes-shell/shell/shell_0.1.bb index abc96da..d59270e 100644 --- a/yocto/layers/meta-mosaic/recipes-shell/shell/shell_0.1.bb +++ b/yocto/layers/meta-mosaic/recipes-shell/shell/shell_0.1.bb @@ -1,5 +1,5 @@ SUMMARY = "Mosaic embedded system shell" -DESCRIPTION = "Qt Quick system shell for embedded Linux devices." +DESCRIPTION = "Qt Widgets system shell for embedded Linux devices." HOMEPAGE = "https://git.ntnu.no/sindrelm/Mosaic" LICENSE = "GPL-3.0-only" LIC_FILES_CHKSUM = "file://${COMMON_LICENSE_DIR}/GPL-3.0-only;md5=c79ff39f19dfec6d293b95dea7b07891" @@ -8,7 +8,6 @@ FILESEXTRAPATHS:prepend := "${THISDIR}/../../../../../:" SRC_URI = " \ file://shell/CMakeLists.txt \ file://shell/src \ - file://shell/qml \ file://LICENSE \ file://mosaic-shell.pam \ " @@ -16,9 +15,11 @@ S = "${UNPACKDIR}/shell" inherit qt6-cmake -DEPENDS += "qtbase qtdeclarative qtdeclarative-native libpam" +EXTRA_OECMAKE += "-DBUILD_TESTING=OFF" -RDEPENDS:${PN} += "qtbase-plugins qtdeclarative-qmlplugins libpam-runtime" +DEPENDS += "qtbase libpam" + +RDEPENDS:${PN} += "qtbase-plugins libpam-runtime" CONFFILES:${PN} += "${sysconfdir}/pam.d/mosaic-shell"