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",