From e443741a6aac92358b84a9a5c98361ea2199a91b Mon Sep 17 00:00:00 2001 From: Russell Martin Date: Thu, 9 Nov 2023 15:11:36 -0500 Subject: [PATCH] Add support for third-party GUI framework plugins --- .github/workflows/ci.yml | 5 +- changes/1524.feature.rst | 1 + setup.cfg | 6 + src/briefcase/bootstraps/__init__.py | 6 + src/briefcase/bootstraps/base.py | 142 ++++ src/briefcase/bootstraps/pursuedpybear.py | 79 ++ src/briefcase/bootstraps/pygame.py | 82 ++ src/briefcase/bootstraps/pyside2.py | 134 +++ src/briefcase/bootstraps/pyside6.py | 139 ++++ src/briefcase/bootstraps/toga.py | 208 +++++ src/briefcase/commands/new.py | 68 +- tests/commands/new/test_build_app_context.py | 816 +++++++++++++++++-- 12 files changed, 1619 insertions(+), 67 deletions(-) create mode 100644 changes/1524.feature.rst create mode 100644 src/briefcase/bootstraps/__init__.py create mode 100644 src/briefcase/bootstraps/base.py create mode 100644 src/briefcase/bootstraps/pursuedpybear.py create mode 100644 src/briefcase/bootstraps/pygame.py create mode 100644 src/briefcase/bootstraps/pyside2.py create mode 100644 src/briefcase/bootstraps/pyside6.py create mode 100644 src/briefcase/bootstraps/toga.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 529fae190..c83c2bcd2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -156,7 +156,8 @@ jobs: verify-apps: name: Build app needs: unit-tests - uses: beeware/.github/.github/workflows/app-build-verify.yml@main +# uses: beeware/.github/.github/workflows/app-build-verify.yml@main + uses: rmartin16/.github-beeware/.github/workflows/app-build-verify.yml@gui-plugin-support with: # This *must* be the version of Python that is the system Python on the # Ubuntu version used to run Linux tests. We use a fixed ubuntu-22.04 @@ -166,6 +167,8 @@ jobs: python-version: "3.10" runner-os: ${{ matrix.runner-os }} framework: ${{ matrix.framework }} + briefcase-template-source: https://github.com/rmartin16/briefcase-template.git + briefcase-template-branch: gui-plugin-support strategy: fail-fast: false matrix: diff --git a/changes/1524.feature.rst b/changes/1524.feature.rst new file mode 100644 index 000000000..cd52cfa51 --- /dev/null +++ b/changes/1524.feature.rst @@ -0,0 +1 @@ +Creating new projects with arbitrary third-party GUI frameworks is now supported via plugins. diff --git a/setup.cfg b/setup.cfg index 810541366..3f5f5bf34 100644 --- a/setup.cfg +++ b/setup.cfg @@ -112,6 +112,12 @@ where = src [options.entry_points] console_scripts = briefcase = briefcase.__main__:main +briefcase.bootstraps = + Toga = briefcase.bootstraps.toga + PySide2 = briefcase.bootstraps.pyside2 + PySide6 = briefcase.bootstraps.pyside6 + PursuedPyBear = briefcase.bootstraps.pursuedpybear + Pygame = briefcase.bootstraps.pygame briefcase.platforms = android = briefcase.platforms.android iOS = briefcase.platforms.iOS diff --git a/src/briefcase/bootstraps/__init__.py b/src/briefcase/bootstraps/__init__.py new file mode 100644 index 000000000..1b4e70d65 --- /dev/null +++ b/src/briefcase/bootstraps/__init__.py @@ -0,0 +1,6 @@ +from briefcase.bootstraps.base import BaseGuiPlugin # noqa: F401 +from briefcase.bootstraps.pursuedpybear import PursuedPyBearGuiPlugin # noqa: F401 +from briefcase.bootstraps.pygame import PygameGuiPlugin # noqa: F401 +from briefcase.bootstraps.pyside2 import PySide2GuiPlugin # noqa: F401 +from briefcase.bootstraps.pyside6 import PySide6GuiPlugin # noqa: F401 +from briefcase.bootstraps.toga import TogaGuiPlugin # noqa: F401 diff --git a/src/briefcase/bootstraps/base.py b/src/briefcase/bootstraps/base.py new file mode 100644 index 000000000..282bd5fe5 --- /dev/null +++ b/src/briefcase/bootstraps/base.py @@ -0,0 +1,142 @@ +from __future__ import annotations + +from abc import ABC +from typing import Literal, TypedDict + + +class ContextDict(TypedDict): + formal_name: str + app_name: str + class_name: str + module_name: str + project_name: str + description: str + author: str + author_email: str + bundle: str + url: str + license: str + + +class BaseGuiPlugin(ABC): + """Definition for a plugin that defines a new Briefcase app.""" + + name: str + fields: list[str] = [ + "app_source", + "start_app_source", + "requires", + "macos_requires", + "macos_universal_build", + "linux_requires", + "linux_system_debian_system_requires", + "linux_system_debian_system_runtime_requires", + "linux_system_rhel_system_requires", + "linux_system_rhel_system_runtime_requires", + "linux_system_suse_system_requires", + "linux_system_suse_system_runtime_requires", + "linux_system_arch_system_requires", + "linux_system_arch_system_runtime_requires", + "linux_appimage_manylinux", + "linux_appimage_system_requires", + "linux_appimage_linuxdeploy_plugins", + "linux_flatpak_runtime", + "linux_flatpak_runtime_version", + "linux_flatpak_sdk", + "windows_requires", + "ios_requires", + "ios_supported", + "android_requires", + "android_supported", + "web_requires", + "web_supported", + "web_style_framework", + ] + + def __init__(self, context: ContextDict): + # context contains metadata about the app being created + self.context = context + + def app_source(self) -> str | None: + """The Python source code for the project.""" + + def start_app_source(self) -> str | None: + """The Python source code to start the app from __main__.py.""" + + def requires(self) -> str | None: + """List of package requirements for all platforms.""" + + def macos_requires(self) -> str | None: + """List of package requirements for macOS.""" + + def macos_universal_build(self) -> Literal["true", "false"] | None: + """Whether to create a universal build for macOS.""" + + def linux_requires(self) -> str | None: + """List of package requirements for Linux.""" + + def linux_system_debian_system_requires(self) -> str | None: + """List of system package requirements to build the app.""" + + def linux_system_debian_system_runtime_requires(self) -> str | None: + """List of system package requirements to run the app on Debian.""" + + def linux_system_rhel_system_requires(self) -> str | None: + """List of system package requirements to build the app on RHEL.""" + + def linux_system_rhel_system_runtime_requires(self) -> str | None: + """List of system package requirements to run the app on RHEL.""" + + def linux_system_suse_system_requires(self) -> str | None: + """List of system package requirements to build the app on SUSE.""" + + def linux_system_suse_system_runtime_requires(self) -> str | None: + """List of system package requirements to run the app on SUSE.""" + + def linux_system_arch_system_requires(self) -> str | None: + """List of system package requirements to build the app on Arch.""" + + def linux_system_arch_system_runtime_requires(self) -> str | None: + """List of system package requirements to run the app on Arch.""" + + def linux_appimage_manylinux(self) -> str | None: + """The manylinux base, e.g. manylinux2014, to use to build the app.""" + + def linux_appimage_system_requires(self) -> str | None: + """List of system package requirements to build the app in to an AppImage.""" + + def linux_appimage_linuxdeploy_plugins(self) -> str | None: + """List of linuxdeploy plugins to use to build the app in to an AppImage.""" + + def linux_flatpak_runtime(self) -> str | None: + """The Flatpak runtime, e.g. org.gnome.Platform, for the app.""" + + def linux_flatpak_runtime_version(self) -> str | None: + """The Flatpak runtime version, e.g. 44, for the app.""" + + def linux_flatpak_sdk(self) -> str | None: + """The Flatpak SDK, e.g. org.gnome.Sdk, for the app.""" + + def windows_requires(self) -> str | None: + """List of package requirements for Windows.""" + + def ios_requires(self) -> str | None: + """List of package requirements for iOS.""" + + def ios_supported(self) -> Literal["true", "false"] | None: + """Whether the GUI framework supports iOS.""" + + def android_requires(self) -> str | None: + """List of package requirements for Android.""" + + def android_supported(self) -> Literal["true", "false"] | None: + """Whether the GUI framework supports Android.""" + + def web_requires(self) -> str | None: + """List of package requirements for Web.""" + + def web_supported(self) -> Literal["true", "false"] | None: + """Whether the GUI framework supports Web.""" + + def web_style_framework(self) -> str | None: + """The style framework, e.g. Bootstrap or Shoelace, for web.""" diff --git a/src/briefcase/bootstraps/pursuedpybear.py b/src/briefcase/bootstraps/pursuedpybear.py new file mode 100644 index 000000000..b122dd5cb --- /dev/null +++ b/src/briefcase/bootstraps/pursuedpybear.py @@ -0,0 +1,79 @@ +from briefcase.bootstraps.base import BaseGuiPlugin + + +class PursuedPyBearGuiPlugin(BaseGuiPlugin): + name = "PursuedPyBear" + + def app_source(self): + return """\ +import os +import sys + +try: + from importlib import metadata as importlib_metadata +except ImportError: + # Backwards compatibility - importlib.metadata was added in Python 3.8 + import importlib_metadata + +import ppb + + +class {{ cookiecutter.class_name }}(ppb.Scene): + def __init__(self, **props): + super().__init__(**props) + + self.add( + ppb.Sprite( + image=ppb.Image("{{ cookiecutter.module_name }}/resources/{{ cookiecutter.app_name }}.png"), + ) + ) + + +def main(): + # Linux desktop environments use app's .desktop file to integrate the app + # to their application menus. The .desktop file of this app will include + # StartupWMClass key, set to app's formal name, which helps associate + # app's windows to its menu item. + # + # For association to work any windows of the app must have WMCLASS + # property set to match the value set in app's desktop file. For PPB this + # is set using environment variable. + + # Find the name of the module that was used to start the app + app_module = sys.modules["__main__"].__package__ + # Retrieve the app's metadata + metadata = importlib_metadata.metadata(app_module) + + os.environ["SDL_VIDEO_X11_WMCLASS"] = metadata["Formal-Name"] + + ppb.run( + starting_scene={{ cookiecutter.class_name }}, + title=metadata["Formal-Name"], + ) +""" + + def requires(self): + return """ + "ppb~=1.1", +""" + + def macos_requires(self): + return "" + + def linux_requires(self): + return "" + + def linux_appimage_manylinux(self): + return "manylinux2014" + + def linux_flatpak_runtime(self): + return "org.freedesktop.Platform" + + def linux_flatpak_runtime_version(self): + return "22.08" + + def linux_flatpak_sdk(self): + return "org.freedesktop.Sdk" + + +plugin = PursuedPyBearGuiPlugin diff --git a/src/briefcase/bootstraps/pygame.py b/src/briefcase/bootstraps/pygame.py new file mode 100644 index 000000000..fb0797170 --- /dev/null +++ b/src/briefcase/bootstraps/pygame.py @@ -0,0 +1,82 @@ +from briefcase.bootstraps.base import BaseGuiPlugin + + +class PygameGuiPlugin(BaseGuiPlugin): + name = "Pygame" + + def app_source(self): + return """\ +import os +import sys + +import pygame + +try: + from importlib import metadata as importlib_metadata +except ImportError: + # Backwards compatibility - importlib.metadata was added in Python 3.8 + import importlib_metadata + +SCREEN_WIDTH, SCREEN_HEIGHT = 800, 600 +WHITE = (255, 255, 255) + + +def main(): + # Linux desktop environments use app's .desktop file to integrate the app + # to their application menus. The .desktop file of this app will include + # StartupWMClass key, set to app's formal name, which helps associate + # app's windows to its menu item. + # + # For association to work any windows of the app must have WMCLASS + # property set to match the value set in app's desktop file. For PPB this + # is set using environment variable. + + # Find the name of the module that was used to start the app + app_module = sys.modules["__main__"].__package__ + # Retrieve the app's metadata + metadata = importlib_metadata.metadata(app_module) + + os.environ["SDL_VIDEO_X11_WMCLASS"] = metadata["Formal-Name"] + + pygame.init() + pygame.display.set_caption(metadata["Formal-Name"]) + screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT)) + + running = True + while running: + for event in pygame.event.get(): + if event.type == pygame.QUIT: + running = False + break + + screen.fill(WHITE) + pygame.display.flip() + + pygame.quit() +""" + + def requires(self): + return """ + "pygame~=2.2", +""" + + def macos_requires(self): + return "" + + def linux_requires(self): + return "" + + def linux_appimage_manylinux(self): + return "manylinux2014" + + def linux_flatpak_runtime(self): + return "org.freedesktop.Platform" + + def linux_flatpak_runtime_version(self): + return "22.08" + + def linux_flatpak_sdk(self): + return "org.freedesktop.Sdk" + + +plugin = PygameGuiPlugin diff --git a/src/briefcase/bootstraps/pyside2.py b/src/briefcase/bootstraps/pyside2.py new file mode 100644 index 000000000..c6cfc8963 --- /dev/null +++ b/src/briefcase/bootstraps/pyside2.py @@ -0,0 +1,134 @@ +from briefcase.bootstraps.base import BaseGuiPlugin + + +class PySide2GuiPlugin(BaseGuiPlugin): + name = "PySide2" + + def app_source(self): + return """\ +import sys + +try: + from importlib import metadata as importlib_metadata +except ImportError: + # Backwards compatibility - importlib.metadata was added in Python 3.8 + import importlib_metadata + +from PySide2 import QtWidgets + + +class {{ cookiecutter.class_name }}(QtWidgets.QMainWindow): + def __init__(self): + super().__init__() + self.init_ui() + + def init_ui(self): + self.setWindowTitle("{{ cookiecutter.app_name }}") + self.show() + + +def main(): + # Linux desktop environments use app's .desktop file to integrate the app + # to their application menus. The .desktop file of this app will include + # StartupWMClass key, set to app's formal name, which helps associate + # app's windows to its menu item. + # + # For association to work any windows of the app must have WMCLASS + # property set to match the value set in app's desktop file. For PySide2 + # this is set with setApplicationName(). + + # Find the name of the module that was used to start the app + app_module = sys.modules["__main__"].__package__ + # Retrieve the app's metadata + metadata = importlib_metadata.metadata(app_module) + + QtWidgets.QApplication.setApplicationName(metadata["Formal-Name"]) + + app = QtWidgets.QApplication(sys.argv) + main_window = {{ cookiecutter.class_name }}() + sys.exit(app.exec_()) +""" + + def requires(self): + return """ + "pyside2~=5.15", +""" + + def macos_requires(self): + return "" + + def linux_requires(self): + return "" + + def linux_system_debian_system_requires(self): + return "" + + def linux_system_debian_system_runtime_requires(self): + return """ + # Derived from https://doc.qt.io/qt-6/linux-requirements.html + "libxrender1", + "libxcb-render0", + "libxcb-render-util0", + "libxcb-shape0", + "libxcb-randr0", + "libxcb-xfixes0", + "libxcb-xkb1", + "libxcb-sync1", + "libxcb-shm0", + "libxcb-icccm4", + "libxcb-keysyms1", + "libxcb-image0", + "libxcb-util1", + "libxkbcommon0", + "libxkbcommon-x11-0", + "libfontconfig1", + "libfreetype6", + "libxext6", + "libx11-6", + "libxcb1", + "libx11-xcb1", + "libsm6", + "libice6", + "libglib2.0-0", + "libgl1", + "libegl1-mesa", + "libdbus-1-3", + "libgssapi-krb5-2", +""" + + def linux_system_rhel_system_requires(self): + return "" + + def linux_system_rhel_system_runtime_requires(self): + return """ + "qt5-qtbase-gui", +""" + + def linux_system_suse_system_requires(self): + return "" + + def linux_system_suse_system_runtime_requires(self): + return """ + "libQt5Gui5", +""" + + def linux_system_arch_system_requires(self): + return "" + + def linux_system_arch_system_runtime_requires(self): + return "" + + def linux_appimage_manylinux(self): + return "manylinux2014" + + def linux_flatpak_runtime(self): + return "org.kde.Platform" + + def linux_flatpak_runtime_version(self): + return "6.4" + + def linux_flatpak_sdk(self): + return "org.kde.Sdk" + + +plugin = PySide2GuiPlugin diff --git a/src/briefcase/bootstraps/pyside6.py b/src/briefcase/bootstraps/pyside6.py new file mode 100644 index 000000000..c1e96b780 --- /dev/null +++ b/src/briefcase/bootstraps/pyside6.py @@ -0,0 +1,139 @@ +from briefcase.bootstraps.base import BaseGuiPlugin + + +class PySide6GuiPlugin(BaseGuiPlugin): + name = "PySide6" + + def app_source(self): + return """\ +import sys + +try: + from importlib import metadata as importlib_metadata +except ImportError: + # Backwards compatibility - importlib.metadata was added in Python 3.8 + import importlib_metadata + +from PySide6 import QtWidgets + + +class {{ cookiecutter.class_name }}(QtWidgets.QMainWindow): + def __init__(self): + super().__init__() + self.init_ui() + + def init_ui(self): + self.setWindowTitle("{{ cookiecutter.app_name }}") + self.show() + + +def main(): + # Linux desktop environments use app's .desktop file to integrate the app + # to their application menus. The .desktop file of this app will include + # StartupWMClass key, set to app's formal name, which helps associate + # app's windows to its menu item. + # + # For association to work any windows of the app must have WMCLASS + # property set to match the value set in app's desktop file. For PySide2 + # this is set with setApplicationName(). + + # Find the name of the module that was used to start the app + app_module = sys.modules["__main__"].__package__ + # Retrieve the app's metadata + metadata = importlib_metadata.metadata(app_module) + + QtWidgets.QApplication.setApplicationName(metadata["Formal-Name"]) + + app = QtWidgets.QApplication(sys.argv) + main_window = {{ cookiecutter.class_name }}() + sys.exit(app.exec()) +""" + + def requires(self): + return """ + "PySide6-Essentials~=6.5", + # "PySide6-Addons~=6.5", +""" + + def macos_requires(self): + return "" + + def linux_requires(self): + return "" + + def linux_system_debian_system_requires(self): + return "" + + def linux_system_debian_system_runtime_requires(self): + return """ + # Derived from https://doc.qt.io/qt-6/linux-requirements.html + "libxrender1", + "libxcb-render0", + "libxcb-render-util0", + "libxcb-shape0", + "libxcb-randr0", + "libxcb-xfixes0", + "libxcb-xkb1", + "libxcb-sync1", + "libxcb-shm0", + "libxcb-icccm4", + "libxcb-keysyms1", + "libxcb-image0", + "libxcb-util1", + "libxkbcommon0", + "libxkbcommon-x11-0", + "libfontconfig1", + "libfreetype6", + "libxext6", + "libx11-6", + "libxcb1", + "libx11-xcb1", + "libsm6", + "libice6", + "libglib2.0-0", + "libgl1", + "libegl1-mesa", + "libdbus-1-3", + "libgssapi-krb5-2", +""" + + def linux_system_rhel_system_requires(self): + return "" + + def linux_system_rhel_system_runtime_requires(self): + return """ + "qt6-qtbase-gui", +""" + + def linux_system_suse_system_requires(self): + return "" + + def linux_system_suse_system_runtime_requires(self): + return """ + "libQt6Gui6", +""" + + def linux_system_arch_system_requires(self): + return """ + "qt6-base", +""" + + def linux_system_arch_system_runtime_requires(self): + return """ + "qt6-base", +""" + + def linux_appimage_manylinux(self): + return "manylinux_2_28" + + def linux_flatpak_runtime(self): + return "org.kde.Platform" + + def linux_flatpak_runtime_version(self): + return "6.4" + + def linux_flatpak_sdk(self): + return "org.kde.Sdk" + + +plugin = PySide6GuiPlugin diff --git a/src/briefcase/bootstraps/toga.py b/src/briefcase/bootstraps/toga.py new file mode 100644 index 000000000..e70adfc9e --- /dev/null +++ b/src/briefcase/bootstraps/toga.py @@ -0,0 +1,208 @@ +from briefcase.bootstraps.base import BaseGuiPlugin + + +class TogaGuiPlugin(BaseGuiPlugin): + name = "Toga" + + def app_source(self): + return '''\ +import toga +from toga.style import Pack +from toga.style.pack import COLUMN, ROW + + +class {{ cookiecutter.class_name }}(toga.App): + + def startup(self): + """Construct and show the Toga application. + + Usually, you would add your application to a main content box. We then create a + main window (with a name matching the app), and show the main window. + """ + main_box = toga.Box() + + self.main_window = toga.MainWindow(title=self.formal_name) + self.main_window.content = main_box + self.main_window.show() + + +def main(): + return {{ cookiecutter.class_name }}() +''' + + def start_app_source(self): + return """\ + main().main_loop() +""" + + def requires(self): + return None + + def macos_requires(self): + return """ + "toga-cocoa~=0.4.0", +""" + + def macos_universal_build(self): + return "true" + + def linux_requires(self): + return """ + "toga-gtk~=0.4.0", +""" + + def linux_system_debian_system_requires(self): + return """ + # Needed to compile pycairo wheel + "libcairo2-dev", + # Needed to compile PyGObject wheel + "libgirepository1.0-dev", +""" + + def linux_system_debian_system_runtime_requires(self): + return """ + # Needed to provide GTK and its GI bindings + "gir1.2-gtk-3.0", + "libgirepository-1.0-1", + # Dependencies that GTK looks for at runtime + "libcanberra-gtk3-module", + # Needed to provide WebKit2 at runtime + # "gir1.2-webkit2-4.0", +""" + + def linux_system_rhel_system_requires(self): + return """ + # Needed to compile pycairo wheel + "cairo-gobject-devel", + # Needed to compile PyGObject wheel + "gobject-introspection-devel", +""" + + def linux_system_rhel_system_runtime_requires(self): + return """ + # Needed to support Python bindings to GTK + "gobject-introspection", + # Needed to provide GTK + "gtk3", + # Dependencies that GTK looks for at runtime + "libcanberra-gtk3", + # Needed to provide WebKit2 at runtime + # "webkit2gtk3", +""" + + def linux_system_suse_system_requires(self): + return """ + # Needed to compile pycairo wheel + "cairo-devel", + # Needed to compile PyGObject wheel + "gobject-introspection-devel", +""" + + def linux_system_suse_system_runtime_requires(self): + return """ + # Needed to provide GTK + "gtk3", + # Needed to support Python bindings to GTK + "gobject-introspection", "typelib(Gtk)=3.0", + # Dependencies that GTK looks for at runtime + "libcanberra-gtk3-0", + # Needed to provide WebKit2 at runtime + # "libwebkit2gtk3", + # "typelib(WebKit2)", +""" + + def linux_system_arch_system_requires(self): + return """ + # Needed to compile pycairo wheel + "cairo", + # Needed to compile PyGObject wheel + "gobject-introspection", + # Runtime dependencies that need to exist so that the + # Arch package passes final validation. + # Needed to provide GTK + "gtk3", + # Dependencies that GTK looks for at runtime + "libcanberra", + # Needed to provide WebKit2 + # "webkit2gtk", +""" + + def linux_system_arch_system_runtime_requires(self): + return """ + # Needed to provide GTK + "gtk3", + # Needed to provide PyGObject bindings + "gobject-introspection-runtime", + # Dependencies that GTK looks for at runtime + "libcanberra", + # Needed to provide WebKit2 at runtime + # "webkit2gtk", +""" + + def linux_appimage_manylinux(self): + return "manylinux2014" + + def linux_appimage_system_requires(self): + return """ + # Needed to compile pycairo wheel + "cairo-gobject-devel", + # Needed to compile PyGObject wheel + "gobject-introspection-devel", + # Needed to provide GTK + "gtk3-devel", + # Dependencies that GTK looks for at runtime, that need to be + # in the build environment to be picked up by linuxdeploy + "libcanberra-gtk3", + "PackageKit-gtk3-module", + "gvfs-client", +""" + + def linux_appimage_linuxdeploy_plugins(self): + return """ + "DEPLOY_GTK_VERSION=3 gtk", +""" + + def linux_flatpak_runtime(self): + return "org.gnome.Platform" + + def linux_flatpak_runtime_version(self): + return "44" + + def linux_flatpak_sdk(self): + return "org.gnome.Sdk" + + def windows_requires(self): + return """ + "toga-winforms~=0.4.0", +""" + + def ios_requires(self): + return """ + "toga-iOS~=0.4.0", + "std-nslog~=1.0.0", +""" + + def ios_supported(self): + return "true" + + def android_requires(self): + return """ + "toga-android~=0.4.0", +""" + + def android_supported(self): + return "true" + + def web_requires(self): + return """ + "toga-web~=0.4.0", +""" + + def web_supported(self): + return "true" + + def web_style_framework(self): + return "Shoelace v2.3" + + +plugin = TogaGuiPlugin diff --git a/src/briefcase/commands/new.py b/src/briefcase/commands/new.py index aa24ce22b..4681d957d 100644 --- a/src/briefcase/commands/new.py +++ b/src/briefcase/commands/new.py @@ -1,7 +1,10 @@ +from __future__ import annotations + +import contextlib import re import unicodedata from email.utils import parseaddr -from typing import Optional +from types import ModuleType from urllib.parse import urlparse from packaging.version import Version @@ -17,6 +20,11 @@ from .base import BaseCommand +try: + from importlib_metadata import entry_points +except ImportError: # pragma: no-cover-if-lt-py310 + from importlib.metadata import entry_points + def titlecase(s): """Convert a string to titlecase. @@ -60,6 +68,14 @@ def titlecase(s): ) +def get_gui_bootstraps() -> dict[str, ModuleType]: + """Loads built-in and third party GUI bootstraps.""" + return { + entry_point.name: entry_point.load() + for entry_point in entry_points(group="briefcase.bootstraps") + } + + class NewCommand(BaseCommand): cmd_line = "briefcase new" command = "new" @@ -401,21 +417,26 @@ def build_app_context(self): ], ) - gui_framework = self.input_select( + bootstraps = get_gui_bootstraps() + bootstrap_choices = [ + "Toga", + "PySide2 (does not support iOS/Android deployment)", + "PySide6 (does not support iOS/Android deployment)", + "PursuedPyBear (does not support iOS/Android deployment)", + "Pygame (does not support iOS/Android deployment)", + ] + builtin_bootstraps = [n.split(" ")[0] for n in bootstrap_choices] + bootstrap_choices += [f for f in bootstraps if f not in builtin_bootstraps] + bootstrap_choices += ["None"] + + bootstrap = self.input_select( intro=""" What GUI toolkit do you want to use for this project?""", variable="GUI framework", - options=[ - "Toga", - "PySide2 (does not support iOS/Android deployment)", - "PySide6 (does not support iOS/Android deployment)", - "PursuedPyBear (does not support iOS/Android deployment)", - "Pygame (does not support iOS/Android deployment)", - "None", - ], + options=bootstrap_choices, ) - return { + context = { "formal_name": formal_name, "app_name": app_name, "class_name": class_name, @@ -427,13 +448,28 @@ def build_app_context(self): "bundle": bundle, "url": url, "license": project_license, - "gui_framework": (gui_framework.split())[0], + } + + plugin_context = {} + if bootstrap != "None": + try: + plugin = bootstraps[bootstrap].plugin(context=context) + except KeyError: + plugin = bootstraps[bootstrap.split(" ")[0]].plugin(context=context) + for context_field in plugin.fields: + with contextlib.suppress(AttributeError): + if (context_value := getattr(plugin, context_field)()) is not None: + plugin_context[context_field] = context_value + + return { + **context, + **plugin_context, } def new_app( self, - template: Optional[str] = None, - template_branch: Optional[str] = None, + template: str | None = None, + template_branch: str | None = None, **options, ): """Ask questions to generate a new application, and generate a stub project from @@ -520,8 +556,8 @@ def verify_tools(self): def __call__( self, - template: Optional[str] = None, - template_branch: Optional[str] = None, + template: str | None = None, + template_branch: str | None = None, **options, ): # Confirm host compatibility, and that all required tools are available. diff --git a/tests/commands/new/test_build_app_context.py b/tests/commands/new/test_build_app_context.py index 238983738..38c5c88d8 100644 --- a/tests/commands/new/test_build_app_context.py +++ b/tests/commands/new/test_build_app_context.py @@ -1,4 +1,4 @@ -def test_question_sequence(new_command): +def test_question_sequence_toga(new_command): """Questions are asked, a context is constructed.""" # Prime answers for all the questions. @@ -12,28 +12,513 @@ def test_question_sequence(new_command): "grace@navy.mil", # author email "https://navy.mil/myapplication", # URL "4", # license - "1", # GUI toolkit + "1", # Toga GUI toolkit ] - assert new_command.build_app_context() == { - "formal_name": "My Application", - "class_name": "MyApplication", - "app_name": "myapplication", - "module_name": "myapplication", - "bundle": "org.beeware", - "project_name": "My Project", - "description": "Cool stuff", - "author": "Grace Hopper", - "author_email": "grace@navy.mil", - "url": "https://navy.mil/myapplication", - "license": "GNU General Public License v2 (GPLv2)", - "gui_framework": "Toga", - } - - -def test_question_sequence_with_nondefault_gui(new_command): - """Questions are asked, a context is constructed, but the GUI option is formatted to - extract the GUI name.""" + assert new_command.build_app_context() == dict( + android_requires=""" + "toga-android~=0.4.0", +""", + android_supported="true", + app_name="myapplication", + app_source='''\ +import toga +from toga.style import Pack +from toga.style.pack import COLUMN, ROW + + +class {{ cookiecutter.class_name }}(toga.App): + + def startup(self): + """Construct and show the Toga application. + + Usually, you would add your application to a main content box. We then create a + main window (with a name matching the app), and show the main window. + """ + main_box = toga.Box() + + self.main_window = toga.MainWindow(title=self.formal_name) + self.main_window.content = main_box + self.main_window.show() + + +def main(): + return {{ cookiecutter.class_name }}() +''', + author="Grace Hopper", + author_email="grace@navy.mil", + bundle="org.beeware", + class_name="MyApplication", + description="Cool stuff", + formal_name="My Application", + ios_requires=""" + "toga-iOS~=0.4.0", + "std-nslog~=1.0.0", +""", + ios_supported="true", + license="GNU General Public License v2 (GPLv2)", + linux_appimage_linuxdeploy_plugins=""" + "DEPLOY_GTK_VERSION=3 gtk", +""", + linux_appimage_manylinux="manylinux2014", + linux_appimage_system_requires=""" + # Needed to compile pycairo wheel + "cairo-gobject-devel", + # Needed to compile PyGObject wheel + "gobject-introspection-devel", + # Needed to provide GTK + "gtk3-devel", + # Dependencies that GTK looks for at runtime, that need to be + # in the build environment to be picked up by linuxdeploy + "libcanberra-gtk3", + "PackageKit-gtk3-module", + "gvfs-client", +""", + linux_flatpak_runtime="org.gnome.Platform", + linux_flatpak_runtime_version="44", + linux_flatpak_sdk="org.gnome.Sdk", + linux_requires=""" + "toga-gtk~=0.4.0", +""", + linux_system_arch_system_requires=""" + # Needed to compile pycairo wheel + "cairo", + # Needed to compile PyGObject wheel + "gobject-introspection", + # Runtime dependencies that need to exist so that the + # Arch package passes final validation. + # Needed to provide GTK + "gtk3", + # Dependencies that GTK looks for at runtime + "libcanberra", + # Needed to provide WebKit2 + # "webkit2gtk", +""", + linux_system_arch_system_runtime_requires=""" + # Needed to provide GTK + "gtk3", + # Needed to provide PyGObject bindings + "gobject-introspection-runtime", + # Dependencies that GTK looks for at runtime + "libcanberra", + # Needed to provide WebKit2 at runtime + # "webkit2gtk", +""", + linux_system_debian_system_requires=""" + # Needed to compile pycairo wheel + "libcairo2-dev", + # Needed to compile PyGObject wheel + "libgirepository1.0-dev", +""", + linux_system_debian_system_runtime_requires=""" + # Needed to provide GTK and its GI bindings + "gir1.2-gtk-3.0", + "libgirepository-1.0-1", + # Dependencies that GTK looks for at runtime + "libcanberra-gtk3-module", + # Needed to provide WebKit2 at runtime + # "gir1.2-webkit2-4.0", +""", + linux_system_rhel_system_requires=""" + # Needed to compile pycairo wheel + "cairo-gobject-devel", + # Needed to compile PyGObject wheel + "gobject-introspection-devel", +""", + linux_system_rhel_system_runtime_requires=""" + # Needed to support Python bindings to GTK + "gobject-introspection", + # Needed to provide GTK + "gtk3", + # Dependencies that GTK looks for at runtime + "libcanberra-gtk3", + # Needed to provide WebKit2 at runtime + # "webkit2gtk3", +""", + linux_system_suse_system_requires=""" + # Needed to compile pycairo wheel + "cairo-devel", + # Needed to compile PyGObject wheel + "gobject-introspection-devel", +""", + linux_system_suse_system_runtime_requires=""" + # Needed to provide GTK + "gtk3", + # Needed to support Python bindings to GTK + "gobject-introspection", "typelib(Gtk)=3.0", + # Dependencies that GTK looks for at runtime + "libcanberra-gtk3-0", + # Needed to provide WebKit2 at runtime + # "libwebkit2gtk3", + # "typelib(WebKit2)", +""", + macos_requires=""" + "toga-cocoa~=0.4.0", +""", + macos_universal_build="true", + module_name="myapplication", + project_name="My Project", + start_app_source="""\ + main().main_loop() +""", + url="https://navy.mil/myapplication", + web_requires=""" + "toga-web~=0.4.0", +""", + web_style_framework="Shoelace v2.3", + web_supported="true", + windows_requires=""" + "toga-winforms~=0.4.0", +""", + ) + + +def test_question_sequence_pyside2(new_command): + """Questions are asked, a context is constructed.""" + + # Prime answers for all the questions. + new_command.input.values = [ + "My Application", # formal name + "", # app name - accept the default + "org.beeware", # bundle ID + "My Project", # project name + "Cool stuff", # description + "Grace Hopper", # author + "grace@navy.mil", # author email + "https://navy.mil/myapplication", # URL + "4", # license + "2", # PySide2 GUI toolkit + ] + + assert new_command.build_app_context() == dict( + app_name="myapplication", + app_source="""\ +import sys + +try: + from importlib import metadata as importlib_metadata +except ImportError: + # Backwards compatibility - importlib.metadata was added in Python 3.8 + import importlib_metadata + +from PySide2 import QtWidgets + + +class {{ cookiecutter.class_name }}(QtWidgets.QMainWindow): + def __init__(self): + super().__init__() + self.init_ui() + + def init_ui(self): + self.setWindowTitle("{{ cookiecutter.app_name }}") + self.show() + + +def main(): + # Linux desktop environments use app's .desktop file to integrate the app + # to their application menus. The .desktop file of this app will include + # StartupWMClass key, set to app's formal name, which helps associate + # app's windows to its menu item. + # + # For association to work any windows of the app must have WMCLASS + # property set to match the value set in app's desktop file. For PySide2 + # this is set with setApplicationName(). + + # Find the name of the module that was used to start the app + app_module = sys.modules["__main__"].__package__ + # Retrieve the app's metadata + metadata = importlib_metadata.metadata(app_module) + + QtWidgets.QApplication.setApplicationName(metadata["Formal-Name"]) + + app = QtWidgets.QApplication(sys.argv) + main_window = {{ cookiecutter.class_name }}() + sys.exit(app.exec_()) +""", + author="Grace Hopper", + author_email="grace@navy.mil", + bundle="org.beeware", + class_name="MyApplication", + description="Cool stuff", + formal_name="My Application", + license="GNU General Public License v2 (GPLv2)", + macos_requires="", + linux_requires="", + linux_appimage_manylinux="manylinux2014", + linux_flatpak_runtime="org.kde.Platform", + linux_flatpak_runtime_version="6.4", + linux_flatpak_sdk="org.kde.Sdk", + linux_system_arch_system_requires="", + linux_system_arch_system_runtime_requires="", + linux_system_debian_system_requires="", + linux_system_debian_system_runtime_requires=""" + # Derived from https://doc.qt.io/qt-6/linux-requirements.html + "libxrender1", + "libxcb-render0", + "libxcb-render-util0", + "libxcb-shape0", + "libxcb-randr0", + "libxcb-xfixes0", + "libxcb-xkb1", + "libxcb-sync1", + "libxcb-shm0", + "libxcb-icccm4", + "libxcb-keysyms1", + "libxcb-image0", + "libxcb-util1", + "libxkbcommon0", + "libxkbcommon-x11-0", + "libfontconfig1", + "libfreetype6", + "libxext6", + "libx11-6", + "libxcb1", + "libx11-xcb1", + "libsm6", + "libice6", + "libglib2.0-0", + "libgl1", + "libegl1-mesa", + "libdbus-1-3", + "libgssapi-krb5-2", +""", + linux_system_rhel_system_requires="", + linux_system_rhel_system_runtime_requires=""" + "qt5-qtbase-gui", +""", + linux_system_suse_system_requires="", + linux_system_suse_system_runtime_requires=""" + "libQt5Gui5", +""", + module_name="myapplication", + project_name="My Project", + requires=""" + "pyside2~=5.15", +""", + url="https://navy.mil/myapplication", + ) + + +def test_question_sequence_pyside6(new_command): + """Questions are asked, a context is constructed.""" + + # Prime answers for all the questions. + new_command.input.values = [ + "My Application", # formal name + "", # app name - accept the default + "org.beeware", # bundle ID + "My Project", # project name + "Cool stuff", # description + "Grace Hopper", # author + "grace@navy.mil", # author email + "https://navy.mil/myapplication", # URL + "4", # license + "3", # PySide6 GUI toolkit + ] + + assert new_command.build_app_context() == dict( + app_name="myapplication", + app_source="""\ +import sys + +try: + from importlib import metadata as importlib_metadata +except ImportError: + # Backwards compatibility - importlib.metadata was added in Python 3.8 + import importlib_metadata + +from PySide6 import QtWidgets + + +class {{ cookiecutter.class_name }}(QtWidgets.QMainWindow): + def __init__(self): + super().__init__() + self.init_ui() + + def init_ui(self): + self.setWindowTitle("{{ cookiecutter.app_name }}") + self.show() + + +def main(): + # Linux desktop environments use app's .desktop file to integrate the app + # to their application menus. The .desktop file of this app will include + # StartupWMClass key, set to app's formal name, which helps associate + # app's windows to its menu item. + # + # For association to work any windows of the app must have WMCLASS + # property set to match the value set in app's desktop file. For PySide2 + # this is set with setApplicationName(). + + # Find the name of the module that was used to start the app + app_module = sys.modules["__main__"].__package__ + # Retrieve the app's metadata + metadata = importlib_metadata.metadata(app_module) + + QtWidgets.QApplication.setApplicationName(metadata["Formal-Name"]) + + app = QtWidgets.QApplication(sys.argv) + main_window = {{ cookiecutter.class_name }}() + sys.exit(app.exec()) +""", + author="Grace Hopper", + author_email="grace@navy.mil", + bundle="org.beeware", + class_name="MyApplication", + description="Cool stuff", + formal_name="My Application", + license="GNU General Public License v2 (GPLv2)", + macos_requires="", + linux_requires="", + linux_appimage_manylinux="manylinux_2_28", + linux_flatpak_runtime="org.kde.Platform", + linux_flatpak_runtime_version="6.4", + linux_flatpak_sdk="org.kde.Sdk", + linux_system_arch_system_requires=""" + "qt6-base", +""", + linux_system_arch_system_runtime_requires=""" + "qt6-base", +""", + linux_system_debian_system_requires="", + linux_system_debian_system_runtime_requires=""" + # Derived from https://doc.qt.io/qt-6/linux-requirements.html + "libxrender1", + "libxcb-render0", + "libxcb-render-util0", + "libxcb-shape0", + "libxcb-randr0", + "libxcb-xfixes0", + "libxcb-xkb1", + "libxcb-sync1", + "libxcb-shm0", + "libxcb-icccm4", + "libxcb-keysyms1", + "libxcb-image0", + "libxcb-util1", + "libxkbcommon0", + "libxkbcommon-x11-0", + "libfontconfig1", + "libfreetype6", + "libxext6", + "libx11-6", + "libxcb1", + "libx11-xcb1", + "libsm6", + "libice6", + "libglib2.0-0", + "libgl1", + "libegl1-mesa", + "libdbus-1-3", + "libgssapi-krb5-2", +""", + linux_system_rhel_system_requires="", + linux_system_rhel_system_runtime_requires=""" + "qt6-qtbase-gui", +""", + linux_system_suse_system_requires="", + linux_system_suse_system_runtime_requires=""" + "libQt6Gui6", +""", + module_name="myapplication", + project_name="My Project", + requires=""" + "PySide6-Essentials~=6.5", + # "PySide6-Addons~=6.5", +""", + url="https://navy.mil/myapplication", + ) + + +def test_question_sequence_pursuedpybear(new_command): + """Questions are asked, a context is constructed.""" + + # Prime answers for all the questions. + new_command.input.values = [ + "My Application", # formal name + "", # app name - accept the default + "org.beeware", # bundle ID + "My Project", # project name + "Cool stuff", # description + "Grace Hopper", # author + "grace@navy.mil", # author email + "https://navy.mil/myapplication", # URL + "4", # license + "4", # PursuedPyBear GUI toolkit + ] + + assert new_command.build_app_context() == dict( + app_name="myapplication", + app_source="""\ +import os +import sys + +try: + from importlib import metadata as importlib_metadata +except ImportError: + # Backwards compatibility - importlib.metadata was added in Python 3.8 + import importlib_metadata + +import ppb + + +class {{ cookiecutter.class_name }}(ppb.Scene): + def __init__(self, **props): + super().__init__(**props) + + self.add( + ppb.Sprite( + image=ppb.Image("{{ cookiecutter.module_name }}/resources/{{ cookiecutter.app_name }}.png"), + ) + ) + + +def main(): + # Linux desktop environments use app's .desktop file to integrate the app + # to their application menus. The .desktop file of this app will include + # StartupWMClass key, set to app's formal name, which helps associate + # app's windows to its menu item. + # + # For association to work any windows of the app must have WMCLASS + # property set to match the value set in app's desktop file. For PPB this + # is set using environment variable. + + # Find the name of the module that was used to start the app + app_module = sys.modules["__main__"].__package__ + # Retrieve the app's metadata + metadata = importlib_metadata.metadata(app_module) + + os.environ["SDL_VIDEO_X11_WMCLASS"] = metadata["Formal-Name"] + + ppb.run( + starting_scene={{ cookiecutter.class_name }}, + title=metadata["Formal-Name"], + ) +""", + author="Grace Hopper", + author_email="grace@navy.mil", + bundle="org.beeware", + class_name="MyApplication", + description="Cool stuff", + formal_name="My Application", + license="GNU General Public License v2 (GPLv2)", + macos_requires="", + linux_requires="", + linux_appimage_manylinux="manylinux2014", + linux_flatpak_runtime="org.freedesktop.Platform", + linux_flatpak_runtime_version="22.08", + linux_flatpak_sdk="org.freedesktop.Sdk", + module_name="myapplication", + project_name="My Project", + requires=""" + "ppb~=1.1", +""", + url="https://navy.mil/myapplication", + ) + + +def test_question_sequence_pygame(new_command): + """Questions are asked, a context is constructed.""" # Prime answers for all the questions. new_command.input.values = [ @@ -46,23 +531,112 @@ def test_question_sequence_with_nondefault_gui(new_command): "grace@navy.mil", # author email "https://navy.mil/myapplication", # URL "4", # license - "2", # GUI toolkit + "5", # Pygame GUI toolkit ] - assert new_command.build_app_context() == { - "formal_name": "My Application", - "class_name": "MyApplication", - "app_name": "myapplication", - "module_name": "myapplication", - "bundle": "org.beeware", - "project_name": "My Project", - "description": "Cool stuff", - "author": "Grace Hopper", - "author_email": "grace@navy.mil", - "url": "https://navy.mil/myapplication", - "license": "GNU General Public License v2 (GPLv2)", - "gui_framework": "PySide2", - } + assert new_command.build_app_context() == dict( + app_name="myapplication", + app_source="""\ +import os +import sys + +import pygame + +try: + from importlib import metadata as importlib_metadata +except ImportError: + # Backwards compatibility - importlib.metadata was added in Python 3.8 + import importlib_metadata + +SCREEN_WIDTH, SCREEN_HEIGHT = 800, 600 +WHITE = (255, 255, 255) + + +def main(): + # Linux desktop environments use app's .desktop file to integrate the app + # to their application menus. The .desktop file of this app will include + # StartupWMClass key, set to app's formal name, which helps associate + # app's windows to its menu item. + # + # For association to work any windows of the app must have WMCLASS + # property set to match the value set in app's desktop file. For PPB this + # is set using environment variable. + + # Find the name of the module that was used to start the app + app_module = sys.modules["__main__"].__package__ + # Retrieve the app's metadata + metadata = importlib_metadata.metadata(app_module) + + os.environ["SDL_VIDEO_X11_WMCLASS"] = metadata["Formal-Name"] + + pygame.init() + pygame.display.set_caption(metadata["Formal-Name"]) + screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT)) + + running = True + while running: + for event in pygame.event.get(): + if event.type == pygame.QUIT: + running = False + break + + screen.fill(WHITE) + pygame.display.flip() + + pygame.quit() +""", + author="Grace Hopper", + author_email="grace@navy.mil", + bundle="org.beeware", + class_name="MyApplication", + description="Cool stuff", + formal_name="My Application", + license="GNU General Public License v2 (GPLv2)", + macos_requires="", + linux_requires="", + linux_appimage_manylinux="manylinux2014", + linux_flatpak_runtime="org.freedesktop.Platform", + linux_flatpak_runtime_version="22.08", + linux_flatpak_sdk="org.freedesktop.Sdk", + module_name="myapplication", + project_name="My Project", + requires=""" + "pygame~=2.2", +""", + url="https://navy.mil/myapplication", + ) + + +def test_question_sequence_none(new_command): + """Questions are asked, a context is constructed.""" + + # Prime answers for all the questions. + new_command.input.values = [ + "My Application", # formal name + "", # app name - accept the default + "org.beeware", # bundle ID + "My Project", # project name + "Cool stuff", # description + "Grace Hopper", # author + "grace@navy.mil", # author email + "https://navy.mil/myapplication", # URL + "4", # license + "6", # None + ] + + assert new_command.build_app_context() == dict( + app_name="myapplication", + author="Grace Hopper", + author_email="grace@navy.mil", + bundle="org.beeware", + class_name="MyApplication", + description="Cool stuff", + formal_name="My Application", + license="GNU General Public License v2 (GPLv2)", + module_name="myapplication", + project_name="My Project", + url="https://navy.mil/myapplication", + ) def test_question_sequence_with_no_user_input(new_command): @@ -70,17 +644,159 @@ def test_question_sequence_with_no_user_input(new_command): new_command.input.enabled = False - assert new_command.build_app_context() == { - "app_name": "helloworld", - "author": "Jane Developer", - "author_email": "jane@example.com", - "bundle": "com.example", - "class_name": "HelloWorld", - "description": "My first application", - "formal_name": "Hello World", - "gui_framework": "Toga", - "license": "BSD license", - "module_name": "helloworld", - "project_name": "Hello World", - "url": "https://example.com/helloworld", - } + assert new_command.build_app_context() == dict( + android_requires=""" + "toga-android~=0.4.0", +""", + android_supported="true", + app_name="helloworld", + app_source='''\ +import toga +from toga.style import Pack +from toga.style.pack import COLUMN, ROW + + +class {{ cookiecutter.class_name }}(toga.App): + + def startup(self): + """Construct and show the Toga application. + + Usually, you would add your application to a main content box. We then create a + main window (with a name matching the app), and show the main window. + """ + main_box = toga.Box() + + self.main_window = toga.MainWindow(title=self.formal_name) + self.main_window.content = main_box + self.main_window.show() + + +def main(): + return {{ cookiecutter.class_name }}() +''', + author="Jane Developer", + author_email="jane@example.com", + bundle="com.example", + class_name="HelloWorld", + description="My first application", + formal_name="Hello World", + ios_requires=""" + "toga-iOS~=0.4.0", + "std-nslog~=1.0.0", +""", + ios_supported="true", + license="BSD license", + linux_appimage_linuxdeploy_plugins=""" + "DEPLOY_GTK_VERSION=3 gtk", +""", + linux_appimage_manylinux="manylinux2014", + linux_appimage_system_requires=""" + # Needed to compile pycairo wheel + "cairo-gobject-devel", + # Needed to compile PyGObject wheel + "gobject-introspection-devel", + # Needed to provide GTK + "gtk3-devel", + # Dependencies that GTK looks for at runtime, that need to be + # in the build environment to be picked up by linuxdeploy + "libcanberra-gtk3", + "PackageKit-gtk3-module", + "gvfs-client", +""", + linux_flatpak_runtime="org.gnome.Platform", + linux_flatpak_runtime_version="44", + linux_flatpak_sdk="org.gnome.Sdk", + linux_requires=""" + "toga-gtk~=0.4.0", +""", + linux_system_arch_system_requires=""" + # Needed to compile pycairo wheel + "cairo", + # Needed to compile PyGObject wheel + "gobject-introspection", + # Runtime dependencies that need to exist so that the + # Arch package passes final validation. + # Needed to provide GTK + "gtk3", + # Dependencies that GTK looks for at runtime + "libcanberra", + # Needed to provide WebKit2 + # "webkit2gtk", +""", + linux_system_arch_system_runtime_requires=""" + # Needed to provide GTK + "gtk3", + # Needed to provide PyGObject bindings + "gobject-introspection-runtime", + # Dependencies that GTK looks for at runtime + "libcanberra", + # Needed to provide WebKit2 at runtime + # "webkit2gtk", +""", + linux_system_debian_system_requires=""" + # Needed to compile pycairo wheel + "libcairo2-dev", + # Needed to compile PyGObject wheel + "libgirepository1.0-dev", +""", + linux_system_debian_system_runtime_requires=""" + # Needed to provide GTK and its GI bindings + "gir1.2-gtk-3.0", + "libgirepository-1.0-1", + # Dependencies that GTK looks for at runtime + "libcanberra-gtk3-module", + # Needed to provide WebKit2 at runtime + # "gir1.2-webkit2-4.0", +""", + linux_system_rhel_system_requires=""" + # Needed to compile pycairo wheel + "cairo-gobject-devel", + # Needed to compile PyGObject wheel + "gobject-introspection-devel", +""", + linux_system_rhel_system_runtime_requires=""" + # Needed to support Python bindings to GTK + "gobject-introspection", + # Needed to provide GTK + "gtk3", + # Dependencies that GTK looks for at runtime + "libcanberra-gtk3", + # Needed to provide WebKit2 at runtime + # "webkit2gtk3", +""", + linux_system_suse_system_requires=""" + # Needed to compile pycairo wheel + "cairo-devel", + # Needed to compile PyGObject wheel + "gobject-introspection-devel", +""", + linux_system_suse_system_runtime_requires=""" + # Needed to provide GTK + "gtk3", + # Needed to support Python bindings to GTK + "gobject-introspection", "typelib(Gtk)=3.0", + # Dependencies that GTK looks for at runtime + "libcanberra-gtk3-0", + # Needed to provide WebKit2 at runtime + # "libwebkit2gtk3", + # "typelib(WebKit2)", +""", + macos_requires=""" + "toga-cocoa~=0.4.0", +""", + macos_universal_build="true", + module_name="helloworld", + project_name="Hello World", + start_app_source="""\ + main().main_loop() +""", + url="https://example.com/helloworld", + web_requires=""" + "toga-web~=0.4.0", +""", + web_style_framework="Shoelace v2.3", + web_supported="true", + windows_requires=""" + "toga-winforms~=0.4.0", +""", + )