diff --git a/extensions/desktop/command-chain/desktop-launch b/extensions/desktop/command-chain/desktop-launch
index f8b3a9cc83a..3843fa6a644 100644
--- a/extensions/desktop/command-chain/desktop-launch
+++ b/extensions/desktop/command-chain/desktop-launch
@@ -1,5 +1,5 @@
#!/bin/bash
-set -- "${SNAP}/gnome-platform/command-chain/desktop-launch" "$@"
+set -- "${SNAP_DESKTOP_RUNTIME}/command-chain/desktop-launch" "$@"
# shellcheck source=/dev/null
source "${SNAP}/snap/command-chain/run"
diff --git a/extensions/desktop/command-chain/hooks-configure-fonts b/extensions/desktop/command-chain/hooks-configure-fonts
index b6a2f1d3c62..19c5fb67c4f 100644
--- a/extensions/desktop/command-chain/hooks-configure-fonts
+++ b/extensions/desktop/command-chain/hooks-configure-fonts
@@ -1,5 +1,5 @@
#!/bin/bash
-set -- "${SNAP}/gnome-platform/command-chain/hooks-configure-fonts" "$@"
+set -- "${SNAP_DESKTOP_RUNTIME}/command-chain/hooks-configure-desktop" "$@"
# shellcheck source=/dev/null
source "${SNAP}/snap/command-chain/run"
diff --git a/schema/snapcraft.json b/schema/snapcraft.json
index c9c61369e47..03e47939541 100644
--- a/schema/snapcraft.json
+++ b/schema/snapcraft.json
@@ -914,6 +914,7 @@
"gnome-3-34",
"gnome-3-38",
"kde-neon",
+ "qt-framework",
"ros1-noetic",
"ros2-foxy"
]
diff --git a/snapcraft/extensions/qt_framework.py b/snapcraft/extensions/qt_framework.py
new file mode 100644
index 00000000000..99ffa4e9b80
--- /dev/null
+++ b/snapcraft/extensions/qt_framework.py
@@ -0,0 +1,220 @@
+# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
+#
+# Copyright 2022 Canonical Ltd.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 3 as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+"""Generic QT Framework extension to support core22 and onwards."""
+import dataclasses
+import functools
+from typing import Any, Dict, List, Optional, Tuple
+
+from overrides import overrides
+
+from .extension import Extension, get_extensions_data_dir, prepend_to_env
+
+_SDK_SNAP = {"core22": "qt-framework-sdk"}
+
+
+@dataclasses.dataclass
+class ExtensionInfo:
+ """Content/SDK build information."""
+
+ cmake_args: list
+
+
+@dataclasses.dataclass
+class QTSnaps:
+ """A structure of QT related snaps."""
+
+ sdk: str
+ content: str
+ builtin: bool = True
+
+
+class QTFramework(Extension):
+ r"""The QT Framework extension.
+
+ This extension makes it easy to assemble QT based applications.
+
+ It configures each application with the following plugs:
+
+ \b
+ - Common Icon Themes.
+ - Common Sound Themes.
+ - The QT Frameworks runtime libraries and utilities.
+
+ For easier desktop integration, it also configures each application
+ entry with these additional plugs:
+
+ \b
+ - desktop (https://snapcraft.io/docs/desktop-interface)
+ - desktop-legacy (https://snapcraft.io/docs/desktop-legacy-interface)
+ - opengl (https://snapcraft.io/docs/opengl-interface)
+ - wayland (https://snapcraft.io/docs/wayland-interface)
+ - x11 (https://snapcraft.io/docs/x11-interface)
+ """
+
+ @staticmethod
+ @overrides
+ def get_supported_bases() -> Tuple[str, ...]:
+ return ("core22",)
+
+ @staticmethod
+ @overrides
+ def get_supported_confinement() -> Tuple[str, ...]:
+ return "strict", "devmode"
+
+ @staticmethod
+ @overrides
+ def is_experimental(base: Optional[str]) -> bool:
+ return False
+
+ @overrides
+ def get_app_snippet(self) -> Dict[str, Any]:
+ return {
+ "command-chain": ["snap/command-chain/desktop-launch"],
+ "plugs": ["desktop", "desktop-legacy", "opengl", "wayland", "x11"],
+ "environment": {
+ "QT_PLUGIN_PATH": "$SNAP/qt-framework/opt/qt5-15/plugins:$SNAP/usr/lib/$SNAPCRAFT_ARCH_TRIPLET/qt5/plugins",
+ },
+ }
+
+ @functools.cached_property
+ def qt_snaps(self) -> QTSnaps:
+ """Return the QT related snaps to use to construct the environment."""
+ base = self.yaml_data["base"]
+ sdk_snap = _SDK_SNAP[base]
+
+ build_snaps: List[str] = []
+ for part in self.yaml_data["parts"].values():
+ build_snaps.extend(part.get("build-snaps", []))
+
+ builtin = True
+
+ for snap in build_snaps:
+ if sdk_snap == snap.split("/")[0]:
+ builtin = False
+ break
+
+ # The same except the trailing -sd
+ content = sdk_snap[:-4]
+
+ return QTSnaps(sdk=sdk_snap, content=content, builtin=builtin)
+
+ @functools.cached_property
+ def ext_info(self) -> ExtensionInfo:
+ """Return the extension info cmake_args, provider, content, build_snaps."""
+ prefix_root = f"/snap/{self.qt_snaps.sdk}/current/opt"
+ versions = ["qt6-5", "qt6-4", "qt6-2", "qt5-15"]
+ prefix_path = "-DCMAKE_PREFIX_PATH="
+
+ for version in versions:
+ prefix_path += f"{prefix_root}/{version}"
+
+ cmake_args = [
+ f"-DCMAKE_FIND_ROOT_PATH=/snap/{self.qt_snaps.sdk}/current",
+ prefix_path,
+ "-DZLIB_INCLUDE_DIR=/lib/x86_64-linux-gnu",
+ ]
+
+ return ExtensionInfo(cmake_args=cmake_args)
+
+ @overrides
+ def get_root_snippet(self) -> Dict[str, Any]:
+ return {
+ "assumes": ["snapd2.43"], # for 'snapctl is-connected'
+ "compression": "lzo",
+ "plugs": {
+ "desktop": {"mount-host-font-cache": False},
+ "icon-themes": {
+ "interface": "content",
+ "target": "$SNAP/data-dir/icons",
+ "default-provider": "gtk-common-themes",
+ },
+ "sound-themes": {
+ "interface": "content",
+ "target": "$SNAP/data-dir/sounds",
+ "default-provider": "gtk-common-themes",
+ },
+ self.qt_snaps.content: {
+ "interface": "content",
+ "default-provider": self.qt_snaps.content,
+ "target": "$SNAP/qt-framework",
+ },
+ },
+ "environment": {"SNAP_DESKTOP_RUNTIME": "$SNAP/qt-framework"},
+ "hooks": {
+ "configure": {
+ "plugs": ["desktop"],
+ "command-chain": ["snap/command-chain/hooks-configure-fonts"],
+ }
+ },
+ "layout": {
+ "/usr/share/X11": {"symlink": "$SNAP/qt-framework/usr/share/X11"}
+ },
+ }
+
+ @overrides
+ def get_part_snippet(self, *, plugin_name: str) -> Dict[str, Any]:
+ sdk_snap = self.qt_snaps.sdk
+ cmake_args = self.ext_info.cmake_args
+
+ return {
+ "build-environment": [
+ {
+ "PATH": prepend_to_env(
+ "PATH", [f"/snap/{sdk_snap}/current/usr/bin"]
+ ),
+ },
+ {
+ "XDG_DATA_DIRS": prepend_to_env(
+ "XDG_DATA_DIRS",
+ [
+ f"$CRAFT_STAGE/usr/share:/snap/{sdk_snap}/current/usr/share",
+ "/usr/share",
+ ],
+ ),
+ },
+ {
+ "SNAPCRAFT_CMAKE_ARGS": prepend_to_env(
+ "SNAPCRAFT_CMAKE_ARGS", cmake_args
+ ),
+ },
+ ],
+ "build-packages": [
+ "libgl1-mesa-dev",
+ ],
+ }
+
+ @overrides
+ def get_parts_snippet(self) -> Dict[str, Any]:
+ source = get_extensions_data_dir() / "desktop" / "command-chain"
+
+ if self.qt_snaps.builtin:
+ return {
+ "qt-framework/sdk": {
+ "source": str(source),
+ "plugin": "make",
+ "make-parameters": [f"PLATFORM_PLUG={self.qt_snaps.content}"],
+ "build-snaps": [self.qt_snaps.sdk],
+ },
+ }
+
+ return {
+ "qt-framework/sdk": {
+ "source": str(source),
+ "plugin": "make",
+ "make-parameters": [f"PLATFORM_PLUG={self.qt_snaps.content}"],
+ },
+ }
diff --git a/snapcraft/extensions/registry.py b/snapcraft/extensions/registry.py
index 77ddb6ebe4d..854eeac68b0 100644
--- a/snapcraft/extensions/registry.py
+++ b/snapcraft/extensions/registry.py
@@ -22,6 +22,7 @@
from .gnome import GNOME
from .kde_neon import KDENeon
+from .qt_framework import QTFramework
from .ros2_humble import ROS2HumbleExtension
if TYPE_CHECKING:
@@ -33,6 +34,7 @@
"gnome": GNOME,
"ros2-humble": ROS2HumbleExtension,
"kde-neon": KDENeon,
+ "qt-framework": QTFramework,
}
diff --git a/tests/unit/commands/test_list_extensions.py b/tests/unit/commands/test_list_extensions.py
index 2fef48836dd..a2b0c5f4997 100644
--- a/tests/unit/commands/test_list_extensions.py
+++ b/tests/unit/commands/test_list_extensions.py
@@ -42,6 +42,7 @@ def test_command(emitter, command):
gnome-3-34 core18
gnome-3-38 core20
kde-neon core18, core20, core22
+ qt-framework core22
ros1-noetic core20
ros2-foxy core20
ros2-humble core22"""
@@ -68,6 +69,7 @@ def test_command_extension_dups(emitter, command):
gnome-3-34 core18
gnome-3-38 core20
kde-neon core18, core20, core22
+ qt-framework core22
ros1-noetic core20
ros2-foxy core20
ros2-humble core22"""
diff --git a/tests/unit/extensions/test_qt_framework.py b/tests/unit/extensions/test_qt_framework.py
new file mode 100644
index 00000000000..f5f5818926e
--- /dev/null
+++ b/tests/unit/extensions/test_qt_framework.py
@@ -0,0 +1,232 @@
+# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
+#
+# Copyright 2023 Canonical Ltd.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 3 as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+import pytest
+
+from snapcraft.extensions import qt_framework
+from snapcraft.extensions.extension import get_extensions_data_dir
+
+############
+# Fixtures #
+############
+
+
+@pytest.fixture
+def qt_framework_extension():
+ return qt_framework.QTFramework(
+ yaml_data={"base": "core22", "parts": {}}, arch="amd64", target_arch="amd64"
+ )
+
+
+@pytest.fixture
+def qt_framework_extension_with_build_snap():
+ return qt_framework.QTFramework(
+ yaml_data={
+ "base": "core22",
+ "parts": {"part1": {"build-snaps": ["qt-framework-sdk/latest/stable"]}},
+ },
+ arch="amd64",
+ target_arch="amd64",
+ )
+
+
+@pytest.fixture
+def qt_framework_extension_with_default_build_snap_from_latest_edge():
+ return qt_framework.QTFramework(
+ yaml_data={
+ "base": "core22",
+ "parts": {"part1": {"build-snaps": ["qt-framework-sdk/latest/edge"]}},
+ },
+ arch="amd64",
+ target_arch="amd64",
+ )
+
+
+###################
+# QTFramework Extension #
+###################
+
+
+def test_get_supported_bases(qt_framework_extension):
+ assert qt_framework_extension.get_supported_bases() == ("core22",)
+
+
+def test_get_supported_confinement(qt_framework_extension):
+ assert qt_framework_extension.get_supported_confinement() == ("strict", "devmode")
+
+
+def test_is_experimental():
+ assert qt_framework.QTFramework.is_experimental(base="core22") is False
+
+
+def test_get_app_snippet(qt_framework_extension):
+ assert qt_framework_extension.get_app_snippet() == {
+ "command-chain": ["snap/command-chain/desktop-launch"],
+ "plugs": ["desktop", "desktop-legacy", "opengl", "wayland", "x11"],
+ "environment": {
+ "QT_PLUGIN_PATH": "$SNAP/qt-framework/opt/qt5-15/plugins:$SNAP/usr/lib/$SNAPCRAFT_ARCH_TRIPLET/qt5/plugins",
+ },
+ }
+
+
+def test_get_root_snippet(qt_framework_extension):
+ assert qt_framework_extension.get_root_snippet() == {
+ "assumes": ["snapd2.43"],
+ "compression": "lzo",
+ "environment": {"SNAP_DESKTOP_RUNTIME": "$SNAP/qt-framework"},
+ "hooks": {
+ "configure": {
+ "plugs": ["desktop"],
+ "command-chain": ["snap/command-chain/hooks-configure-fonts"],
+ }
+ },
+ "layout": {"/usr/share/X11": {"symlink": "$SNAP/qt-framework/usr/share/X11"}},
+ "plugs": {
+ "desktop": {"mount-host-font-cache": False},
+ "icon-themes": {
+ "interface": "content",
+ "target": "$SNAP/data-dir/icons",
+ "default-provider": "gtk-common-themes",
+ },
+ "sound-themes": {
+ "interface": "content",
+ "target": "$SNAP/data-dir/sounds",
+ "default-provider": "gtk-common-themes",
+ },
+ "qt-framework": {
+ "interface": "content",
+ "default-provider": "qt-framework",
+ "target": "$SNAP/qt-framework",
+ },
+ },
+ }
+
+
+def test_get_root_snippet_with_external_sdk(qt_framework_extension_with_build_snap):
+ assert qt_framework_extension_with_build_snap.get_root_snippet() == {
+ "assumes": ["snapd2.43"],
+ "compression": "lzo",
+ "environment": {"SNAP_DESKTOP_RUNTIME": "$SNAP/qt-framework"},
+ "hooks": {
+ "configure": {
+ "plugs": ["desktop"],
+ "command-chain": ["snap/command-chain/hooks-configure-fonts"],
+ }
+ },
+ "layout": {"/usr/share/X11": {"symlink": "$SNAP/qt-framework/usr/share/X11"}},
+ "plugs": {
+ "desktop": {"mount-host-font-cache": False},
+ "icon-themes": {
+ "interface": "content",
+ "target": "$SNAP/data-dir/icons",
+ "default-provider": "gtk-common-themes",
+ },
+ "sound-themes": {
+ "interface": "content",
+ "target": "$SNAP/data-dir/sounds",
+ "default-provider": "gtk-common-themes",
+ },
+ "qt-framework": {
+ "interface": "content",
+ "default-provider": "qt-framework",
+ "target": "$SNAP/qt-framework",
+ },
+ },
+ }
+
+
+class TestGetPartSnippet:
+ """Tests for QTFramework.get_part_snippet when using the default sdk snap name."""
+
+ def test_get_part_snippet(self, qt_framework_extension):
+ self.assert_get_part_snippet(qt_framework_extension)
+
+ def test_get_part_snippet_latest_edge(
+ self, qt_framework_extension_with_default_build_snap_from_latest_edge
+ ):
+ self.assert_get_part_snippet(
+ qt_framework_extension_with_default_build_snap_from_latest_edge
+ )
+
+ @staticmethod
+ def assert_get_part_snippet(qt_framework_instance):
+ assert qt_framework_instance.get_part_snippet(plugin_name="cmake") == {
+ "build-environment": [
+ {"PATH": "/snap/qt-framework-sdk/current/usr/bin${PATH:+:$PATH}"},
+ {
+ "XDG_DATA_DIRS": "$CRAFT_STAGE/usr/share:/snap/qt-framework-sdk/current/usr/share:/usr/share${XDG_DATA_DIRS:+:$XDG_DATA_DIRS}"
+ },
+ {
+ "SNAPCRAFT_CMAKE_ARGS": "-DCMAKE_FIND_ROOT_PATH=/snap/qt-framework-sdk/current:-DCMAKE_PREFIX_PATH=/snap/qt-framework-sdk/current/opt/qt6-5/snap/qt-framework-sdk/current/opt/qt6-4/snap/qt-framework-sdk/current/opt/qt6-2/snap/qt-framework-sdk/current/opt/qt5-15:-DZLIB_INCLUDE_DIR=/lib/x86_64-linux-gnu${SNAPCRAFT_CMAKE_ARGS:+:$SNAPCRAFT_CMAKE_ARGS}"
+ },
+ ],
+ "build-packages": ["libgl1-mesa-dev"],
+ }
+
+
+def test_get_part_snippet_with_external_sdk(qt_framework_extension_with_build_snap):
+ assert qt_framework_extension_with_build_snap.get_part_snippet(
+ plugin_name="cmake"
+ ) == {
+ "build-environment": [
+ {"PATH": "/snap/qt-framework-sdk/current/usr/bin${PATH:+:$PATH}"},
+ {
+ "XDG_DATA_DIRS": "$CRAFT_STAGE/usr/share:/snap/qt-framework-sdk/current/usr/share:/usr/share${XDG_DATA_DIRS:+:$XDG_DATA_DIRS}"
+ },
+ {
+ "SNAPCRAFT_CMAKE_ARGS": "-DCMAKE_FIND_ROOT_PATH=/snap/qt-framework-sdk/current:-DCMAKE_PREFIX_PATH=/snap/qt-framework-sdk/current/opt/qt6-5/snap/qt-framework-sdk/current/opt/qt6-4/snap/qt-framework-sdk/current/opt/qt6-2/snap/qt-framework-sdk/current/opt/qt5-15:-DZLIB_INCLUDE_DIR=/lib/x86_64-linux-gnu${SNAPCRAFT_CMAKE_ARGS:+:$SNAPCRAFT_CMAKE_ARGS}"
+ },
+ ],
+ "build-packages": ["libgl1-mesa-dev"],
+ }
+
+
+def test_get_parts_snippet(qt_framework_extension):
+ source = get_extensions_data_dir() / "desktop" / "command-chain"
+
+ assert qt_framework_extension.get_parts_snippet() == {
+ "qt-framework/sdk": {
+ "source": str(source),
+ "plugin": "make",
+ "make-parameters": ["PLATFORM_PLUG=qt-framework"],
+ "build-snaps": ["qt-framework-sdk"],
+ }
+ }
+
+
+def test_get_parts_snippet_with_external_sdk(qt_framework_extension_with_build_snap):
+ source = get_extensions_data_dir() / "desktop" / "command-chain"
+
+ assert qt_framework_extension_with_build_snap.get_parts_snippet() == {
+ "qt-framework/sdk": {
+ "source": str(source),
+ "plugin": "make",
+ "make-parameters": ["PLATFORM_PLUG=qt-framework"],
+ }
+ }
+
+
+def test_get_parts_snippet_with_external_sdk_different_channel(
+ qt_framework_extension_with_default_build_snap_from_latest_edge,
+):
+ source = get_extensions_data_dir() / "desktop" / "command-chain"
+ assert qt_framework_extension_with_default_build_snap_from_latest_edge.get_parts_snippet() == {
+ "qt-framework/sdk": {
+ "source": str(source),
+ "plugin": "make",
+ "make-parameters": ["PLATFORM_PLUG=qt-framework"],
+ }
+ }
diff --git a/tests/unit/extensions/test_registry.py b/tests/unit/extensions/test_registry.py
index 118e8151715..666d62a9ef6 100644
--- a/tests/unit/extensions/test_registry.py
+++ b/tests/unit/extensions/test_registry.py
@@ -27,6 +27,7 @@ def test_get_extension_names():
"gnome",
"ros2-humble",
"kde-neon",
+ "qt-framework",
"fake-extension-experimental",
"fake-extension-extra",
"fake-extension",