From 1810cf6ff34021509da536496a3edd732709c34a Mon Sep 17 00:00:00 2001 From: Jarred Wilson Date: Thu, 20 Jul 2023 13:32:32 -0400 Subject: [PATCH] Add QT extension --- .../command-chain/hooks-configure-fonts | 2 +- schema/snapcraft.json | 1 + snapcraft/extensions/qt_framework.py | 217 +++++++++++++++ snapcraft/extensions/registry.py | 2 + tests/unit/extensions/test_qt_framework.py | 259 ++++++++++++++++++ tests/unit/extensions/test_registry.py | 1 + 6 files changed, 481 insertions(+), 1 deletion(-) create mode 100644 snapcraft/extensions/qt_framework.py create mode 100644 tests/unit/extensions/test_qt_framework.py diff --git a/extensions/desktop/command-chain/hooks-configure-fonts b/extensions/desktop/command-chain/hooks-configure-fonts index b6a2f1d3c62..39d1fde7999 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}/qt-framework/command-chain/hooks-configure-fonts" "$@" # 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..8b161006f37 --- /dev/null +++ b/snapcraft/extensions/qt_framework.py @@ -0,0 +1,217 @@ +# -*- 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"], + } + + @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/" + cmake_args = [ + f"-DCMAKE_FIND_ROOT_PATH=/snap/{self.qt_snaps.sdk}/current", + f"-DCMAKE_PREFIX_PATH={prefix_root}/qt6-5;{prefix_root}/qt6-4;{prefix_root}/qt6-2;{prefix_root}/qt5-15", + # f"-DQt6_DIR=/snap/{self.qt_snaps.sdk}/current/opt/qt6-2/lib/cmake/Qt6" + "-DZLIB_INCLUDE_DIR=/lib/x86_64-linux-gnu" + ] + + return ExtensionInfo(cmake_args=cmake_args) + + @overrides + def get_root_snippet(self) -> Dict[str, Any]: + platform_snap = self.qt_snaps.content + content_snap = self.qt_snaps.content + "-all" + + 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", + }, + platform_snap: { + "content": content_snap.split('/')[0], + "interface": "content", + "default-provider": platform_snap, + "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]: + # We can change this to the lightweight command-chain when + # the content snap includes the desktop-launch from + # https://github.com/snapcore/snapcraft-desktop-integration + 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/extensions/test_qt_framework.py b/tests/unit/extensions/test_qt_framework.py new file mode 100644 index 00000000000..cc33053b1bc --- /dev/null +++ b/tests/unit/extensions/test_qt_framework.py @@ -0,0 +1,259 @@ +# -*- 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"], + } + + +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/kf5"}, + "hooks": { + "configure": { + "plugs": ["desktop"], + "command-chain": ["snap/command-chain/hooks-configure-desktop"], + } + }, + "layout": {"/usr/share/X11": {"symlink": "$SNAP/kf5/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": { + "content": "qt-framework-all", + "interface": "content", + "default-provider": "qt-framework", + "target": "$SNAP/kf5", + }, + }, + } + + +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/kf5"}, + "hooks": { + "configure": { + "plugs": ["desktop"], + "command-chain": ["snap/command-chain/hooks-configure-desktop"], + } + }, + "layout": {"/usr/share/X11": {"symlink": "$SNAP/kf5/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": { + "content": "qt-framework-all", + "interface": "content", + "default-provider": "qt-framework", + "target": "$SNAP/kf5", + }, + }, + } + + +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() == { + "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" + "${SNAPCRAFT_CMAKE_ARGS:+:$SNAPCRAFT_CMAKE_ARGS}" + ) + }, + ] + } + + +def test_get_part_snippet_with_external_sdk(qt_framework_extension_with_build_snap): + assert qt_framework_extension_with_build_snap.get_part_snippet() == { + "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" + "${SNAPCRAFT_CMAKE_ARGS:+:$SNAPCRAFT_CMAKE_ARGS}" + ) + }, + ] + } + + +def test_get_parts_snippet(qt_framework_extension): + source = get_extensions_data_dir() / "desktop" / "qt-framework" + + 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" / "qt-framework" + + 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" / "qt-framework" + 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",