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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 60 additions & 26 deletions docs/app-generation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -25,32 +25,43 @@ Recipe and image integration
----------------------------

The generator creates one recipe per application and one per bundled service.
Recipes use standard Yocto classes for Qt/CMake builds and systemd integration.
They reference the bundle's sources and license file. CMake and BitBake own compilation,
installation, and packaging.

Applications and services explicitly declare their additional build and runtime
dependencies in the manifest. Build dependency recipe names map to ``DEPENDS``.
Runtime package names map to ``RDEPENDS``. The generator supplies the standard Qt
application dependencies rather than inferring dependencies from source code.

Each application package depends on its required service packages. Service
recipes install the supplied systemd unit file or files and enable services
declared with ``start: boot``. Systemd runs services independently of application
selection and termination.
Application recipes inherit ``qt6-cmake``. Service recipes inherit ``cmake systemd``
or ``cargo systemd`` according to the files in ``source_dir``. Each recipe fetches
the complete local bundle, sets ``S`` to its component's source directory, and
uses the bundle's version, license, and license-file checksum. The inherited
classes own compilation and installation, and BitBake owns packaging.

Cargo lockfiles are parsed locally with ``tomllib``. The renderer adds sorted,
deduplicated crates.io URLs and named SHA-256 checksums to the recipe. Parsing and
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.

Each application package depends on its required service packages, named
``mosaic-service-<app-id>-<service-id>``. Service recipes append installation of
the declared units into ``${systemd_system_unitdir}``, preserving contents and
filenames with mode ``0644``. They include the units in the package and set
``SYSTEMD_SERVICE:${PN}`` and ``SYSTEMD_AUTO_ENABLE:${PN}`` to enable them.
Systemd runs services independently of application selection and termination.

Layer configuration supplies ``MOSAIC_APPS_COMMON`` and
``MOSAIC_APPS_DEV_ONLY`` to the existing application packagegroup. Disabled
bundles are excluded. Enabled bundles with ``include_in_prod: true`` are included
in both profiles, while the remaining enabled bundles are included only in dev.
Selecting an application also includes its required service packages.

Each application recipe installs a JSON launcher entry at
``/usr/share/mosaic/apps/<id>.json`` containing its existing ``id``, ``name``,
``executable``, and ``arguments``.
The shell reads installed entries, displays each application's name, and starts
its executable with the listed arguments when selected after unlock. The launcher
owns discovery and startup of installed applications.
Each application recipe generates Qt Application Manager (QAM) metadata during
installation at ``${datadir}/mosaic/apps/<id>/info.yaml`` (normally
``/usr/share/mosaic/apps/<id>/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.

Tool files
----------
Expand All @@ -67,15 +78,38 @@ file handling. It consists of three files under ``tools/``:
data without writing generated files.

``app_layer.py``
Generates recipes, launcher entries, and layer configuration from the
Generates recipes and layer configuration from the
validated data, and removes obsolete output.

Parser and renderer interfaces
------------------------------

``load_manifest(path)`` and ``load_manifests(apps_dir)`` return Pydantic models
with defaults filled in, paths resolved to absolute ``Path`` objects, and units
normalized to lists. Invalid input raises ``ManifestError`` with the manifest
path and affected field. All bundles are validated before selection.

Service models expose derived ``build_system`` and ``crates`` properties. Crate
records contain ``name``, ``version``, and ``checksum``. These properties are not
manifest JSON fields.

``render_application_files(manifest)`` and
``render_service_files(manifest, service)`` return lists of layer-relative
``(Path, str)`` pairs without writing files. Unsupported recipe literals raise
``ValueError`` identifying the field.

Verification
------------

Verification covers an application with a service, multiple unit files,
dependency mapping, dev/prod selection, invalid manifests, repeatable generation,
and stale output removal. BitBake parsing and an image build validate the
generated recipes.
Launcher integration checks cover installed applications appearing by name and
starting on selection while their services run independently.
Install the development dependencies in the environment described in
:doc:`building`, then run the tooling tests from the repository root:

.. code-block:: bash
.venv-tools/bin/python -m pip install -r tools/requirements-dev.txt
.venv-tools/bin/python -m pytest
Tests live under ``tools/tests/``. They cover manifest validation, recipe
rendering, native installation, and generated layer replacement. Yocto builds
and target checks are needed to verify cross-compilation, packaging, crate
fetching, and service startup.
205 changes: 171 additions & 34 deletions docs/applications.rst
Original file line number Diff line number Diff line change
@@ -1,24 +1,29 @@
User applications
=================

Each application lives under ``apps/`` with a ``mosaic-app.json`` manifest,
Qt/CMake application code, and a license. You can optionally write services,
systemd units, and tests alongside the application.
To add an application:

1. Create ``apps/<your-app>/`` 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.
3. Add the CMake installation rules shown below, then build your chosen image
using the commands in :doc:`building`.

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.
selected from the launcher after unlock. Services run independently.

Manifest fields
---------------

The parser reads ``apps/*/mosaic-app.json`` in directory order and validates all
bundles, including disabled ones. Unknown fields and duplicate JSON keys are
errors. Each error identifies the manifest and affected field.
All manifests must be valid, including disabled bundles. Unknown fields and
duplicate JSON keys are errors. Errors identify the manifest and affected field.

The following top-level fields are required:

Expand All @@ -30,7 +35,8 @@ The following top-level fields are required:
``~``.
- ``enabled`` and ``include_in_prod`` are booleans with the selection behavior
described above.
- ``license`` is a nonempty Yocto license identifier or expression.
- ``license`` is a nonempty Yocto license identifier or expression, without
quotes, backslashes, or dollar signs.
- ``license_file`` points to the bundle's license file.
- ``app`` describes the application build and executable.

Expand All @@ -45,54 +51,185 @@ Both ``app`` and each service accept optional ``build_dependencies`` and
``runtime_dependencies`` lists, defaulting to ``[]``. These contain Yocto recipe
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. Provider
availability is checked by BitBake.
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.

``services`` is an optional list, defaulting to ``[]``. Each service requires:

- ``id``, following the application ID format and unique within the bundle.
- ``source_dir``, containing the service's ``CMakeLists.txt``.
- ``source_dir``, containing exactly one of ``CMakeLists.txt`` or ``Cargo.toml``.
Cargo services also require ``Cargo.lock`` in the same directory.
- ``unit``, a path or nonempty list of paths to systemd unit files. Unit filenames
must be unique across bundles and contain only letters, digits, dots,
underscores, ``@``, ``:``, ``+``, or hyphens.
- ``start``, currently accepting only ``"boot"``.

All source, license, and unit paths are relative to the manifest's directory.
They must exist and stay within that directory after resolving symlinks. Text
fields other than arguments reject control characters. The parser checks file
presence, not CMake code, license validity, or systemd unit contents.
They must exist and stay within that directory after resolving symlinks, as must
service build files and lockfiles. Use ASCII letters, digits, underscores, dots,
slashes, ``+``, or hyphens in bundle, source, and license paths. Unit paths also
allow ``@`` and ``:``. Text fields other than arguments reject control characters.

``profiles`` is an optional object, defaulting to ``{}``, with optional ``dev``
and ``prod`` entries. Each entry contains a required ``settings`` object holding
application-defined JSON values. The parser preserves these settings without
interpreting them.
application-defined JSON values.

CMake build and installation
----------------------------

Use ``MOSAIC_APP_EXECUTABLE`` as the executable target's ``OUTPUT_NAME``. Mosaic
sets it from ``app.executable``. Install the target into
``${CMAKE_INSTALL_BINDIR}`` and required resources into
``${CMAKE_INSTALL_DATADIR}/mosaic/apps/<id>``. For example, an application with ID
``example``, ``main.cpp``, and ``resources/`` can use:

.. code-block:: cmake
cmake_minimum_required(VERSION 3.16)
project(example LANGUAGES CXX)
include(GNUInstallDirs)
find_package(Qt6 REQUIRED COMPONENTS Quick)
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)
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")
The installed executable must match ``app.executable`` or the build fails.

Example manifest
----------------

.. literalinclude:: ../apps/dummy-application/mosaic-app.json
:language: json

Parser interface
----------------
Adding services
---------------

Add each service to the manifest's ``services`` list. Provide a CMake or Cargo
project and systemd units with ``ExecStart`` paths matching the installed
executables. Include an appropriate ``[Install]`` section so systemd can start
the service at boot. Mosaic installs and enables the declared units for you.

CMake service example
~~~~~~~~~~~~~~~~~~~~~

Inside an existing application bundle, add:

.. code-block:: text
services/collector/
CMakeLists.txt
main.c
collector.service
Add this entry to the manifest's ``services`` array:

.. code-block:: json
{
"id": "collector",
"source_dir": "services/collector",
"unit": "services/collector/collector.service",
"start": "boot",
"build_dependencies": ["zlib"],
"runtime_dependencies": []
}
``CMakeLists.txt`` owns executable naming and installation:

.. code-block:: cmake
cmake_minimum_required(VERSION 3.16)
project(collector LANGUAGES C)
include(GNUInstallDirs)
find_package(ZLIB REQUIRED)
add_executable(collector main.c)
target_link_libraries(collector PRIVATE ZLIB::ZLIB)
install(TARGETS collector RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}")
For a minimal ``main.c`` that prints the linked library version:

.. code-block:: c
#include <stdio.h>
#include <zlib.h>
int main(void) { puts(zlibVersion()); return 0; }
Use this ``collector.service`` for the one-shot example:

.. code-block:: ini
[Unit]
Description=Collector example
[Service]
Type=oneshot
ExecStart=/usr/bin/collector
[Install]
WantedBy=multi-user.target
Rust service example
~~~~~~~~~~~~~~~~~~~~

A standalone Cargo package can use this layout:

.. code-block:: text
services/reporter/
Cargo.toml
Cargo.lock
src/main.rs
reporter.service
Add this manifest entry:

.. code-block:: json
{
"id": "reporter",
"source_dir": "services/reporter",
"unit": "services/reporter/reporter.service",
"start": "boot",
"build_dependencies": [],
"runtime_dependencies": []
}
``Cargo.toml`` defines the executable through Cargo's normal naming rules:

.. code-block:: toml
[package]
name = "reporter"
version = "0.1.0"
edition = "2021"
``tools/app_manifest.py`` exposes ``load_manifest(path)`` for one bundle and
``load_manifests(apps_dir)`` for all bundles. They return typed Pydantic models with
optional defaults filled in, source and file paths resolved to absolute
``Path`` objects, and ``unit`` normalized to a list. Fields are accessed as
attributes, such as ``manifest.app.executable``. Each model exposes
``manifest_path`` for diagnostics. An empty application directory returns an
empty list. Invalid input raises ``ManifestError`` without writing files.
Use ``fn main() { println!("Reporter ready"); }`` in ``src/main.rs``. Generate
and commit ``Cargo.lock`` by running ``cargo generate-lockfile --offline`` in
``services/reporter``. Copy the unit example above to ``reporter.service``,
changing its description and ``ExecStart`` to ``/usr/bin/reporter``. Rust
executables are installed into ``/usr/bin`` in the standard image configuration.

Install the development dependencies once in the build tools environment
described in :doc:`building`, then run all Python tooling tests from the
repository root:
``source_dir`` may also identify a Cargo workspace root containing its lockfile.
Builds use Cargo's default package, target, and feature selection. Keep workspace
members and path dependencies inside the application bundle, including dependencies
outside ``source_dir``.

.. code-block:: bash
Declare Rust crate dependencies in ``Cargo.toml`` and update ``Cargo.lock`` with
Cargo before building. Native library recipe dependencies belong in the service's
``build_dependencies`` and additional runtime packages in ``runtime_dependencies``.
For example, a crate using system OpenSSL needs ``openssl`` in
``build_dependencies``.

.venv-tools/bin/python -m pip install -r tools/requirements-dev.txt
.venv-tools/bin/python -m pytest
Use crates.io dependencies or bundled path dependencies. Git dependencies and
alternative registries are not supported. Keep ``Cargo.lock`` committed and up
to date, including the checksums Cargo records for registry dependencies.

Pytest discovers tests under ``tools/tests/``. It does not scan upstream Yocto
sources. To run only the manifest tests, append
``tools/tests/test_app_manifest.py`` to the pytest command.
See :doc:`app-generation` for how Mosaic generates recipes and packages bundles.
Loading
Loading