From f40a698f5ffd46a9a0514487e460d611b522f5dd Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Tue, 9 Jan 2024 09:34:00 +0800 Subject: [PATCH 01/16] Add changenote. --- changes/547.feature.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 changes/547.feature.rst diff --git a/changes/547.feature.rst b/changes/547.feature.rst new file mode 100644 index 000000000..93804de94 --- /dev/null +++ b/changes/547.feature.rst @@ -0,0 +1 @@ +App permissions can now be declared as part of an app's configuration. From a0f358fa5ec87dce01771c8456a3fedd3fa1fe63 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Tue, 9 Jan 2024 09:50:58 +0800 Subject: [PATCH 02/16] Add config merging for permissions and device_requires. --- src/briefcase/config.py | 18 +++++++++- tests/config/test_merge_config.py | 60 +++++++++++++++++++++++++++++-- 2 files changed, 74 insertions(+), 4 deletions(-) diff --git a/src/briefcase/config.py b/src/briefcase/config.py index 77d0e30ed..1fe867416 100644 --- a/src/briefcase/config.py +++ b/src/briefcase/config.py @@ -346,12 +346,28 @@ def merge_config(config, data): situ. :param data: The new configuration data to merge into the configuration. """ - for option in ["requires", "sources", "test_requires", "test_sources"]: + # Properties that are cumulative lists + for option in [ + "requires", + "sources", + "test_requires", + "test_sources", + ]: value = data.pop(option, []) if value: config.setdefault(option, []).extend(value) + # Properties that are cumulative tables + for option in [ + "permissions", + "device_requires", + ]: + value = data.pop(option, {}) + + if value: + config.setdefault(option, {}).update(value) + config.update(data) diff --git a/tests/config/test_merge_config.py b/tests/config/test_merge_config.py index 78f6b442b..aca5b71b6 100644 --- a/tests/config/test_merge_config.py +++ b/tests/config/test_merge_config.py @@ -14,6 +14,7 @@ def test_merge_no_data(): """If there are no new options, nothing changes.""" config = { "requires": ["first", "second"], + "permissions": {"up": True, "down": False}, "other": 1234, } @@ -21,6 +22,7 @@ def test_merge_no_data(): assert config == { "requires": ["first", "second"], + "permissions": {"up": True, "down": False}, "other": 1234, } @@ -29,22 +31,66 @@ def test_merge_no_option(): """If there are no existing options, the new option become the entire value.""" config = {"other": 1234} - merge_config(config, {"requires": ["third", "fourth"]}) + merge_config( + config, + { + "requires": ["third", "fourth"], + "permissions": {"left": True, "right": False}, + }, + ) assert config == { "requires": ["third", "fourth"], + "permissions": {"left": True, "right": False}, "other": 1234, } def test_merge(): """If there are existing options and new options, merge.""" - config = {"requires": ["first", "second"], "other": 1234} + config = { + "requires": ["first", "second"], + "permissions": {"up": True, "down": False}, + "other": 1234, + } - merge_config(config, {"requires": ["third", "fourth"], "other": 5678}) + merge_config( + config, + { + "requires": ["third", "fourth"], + "permissions": {"left": True, "right": False}, + "other": 5678, + }, + ) assert config == { "requires": ["first", "second", "third", "fourth"], + "permissions": {"up": True, "down": False, "left": True, "right": False}, + "other": 5678, + } + + +def test_merge_collision(): + """If there are repeated options, lists are appended, dictionaries are updated and + new options, merge.""" + config = { + "requires": ["first", "second"], + "permissions": {"up": True, "down": False}, + "other": 1234, + } + + merge_config( + config, + { + "requires": ["second", "fourth"], + "permissions": {"down": True, "right": False}, + "other": 5678, + }, + ) + + assert config == { + "requires": ["first", "second", "second", "fourth"], + "permissions": {"up": True, "down": True, "right": False}, "other": 5678, } @@ -53,6 +99,7 @@ def test_convert_base_definition(): """The merge operation succeeds when called on itself.""" config = { "requires": ["first", "second"], + "permissions": {"up": True, "down": False}, "other": 1234, } @@ -60,6 +107,7 @@ def test_convert_base_definition(): assert config == { "requires": ["first", "second"], + "permissions": {"up": True, "down": False}, "other": 1234, } @@ -69,6 +117,8 @@ def test_merged_keys(): config = { "requires": ["first", "second"], "sources": ["a", "b"], + "permissions": {"up": True, "down": False}, + "device_requires": {"north": True, "south": False}, "non-merge": ["1", "2"], "other": 1234, } @@ -77,6 +127,8 @@ def test_merged_keys(): config, { "requires": ["third", "fourth"], + "permissions": {"left": True, "right": False}, + "device_requires": {"west": True, "east": False}, "sources": ["c", "d"], "non-merge": ["3", "4"], }, @@ -85,6 +137,8 @@ def test_merged_keys(): assert config == { "requires": ["first", "second", "third", "fourth"], "sources": ["a", "b", "c", "d"], + "permissions": {"up": True, "down": False, "left": True, "right": False}, + "device_requires": {"north": True, "south": False, "west": True, "east": False}, "non-merge": ["3", "4"], "other": 1234, } From ac74e8f5329d008e600567297133b6edfcd8be5f Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Tue, 9 Jan 2024 12:45:57 +0800 Subject: [PATCH 03/16] Add documentation for permissions interface. --- docs/reference/configuration.rst | 38 ++++++- docs/reference/platforms/android/gradle.rst | 114 +++++++++++++++++--- docs/reference/platforms/iOS/xcode.rst | 28 +++++ docs/reference/platforms/linux/flatpak.rst | 34 ++++++ docs/reference/platforms/macOS/app.rst | 49 +++++++++ docs/reference/platforms/macOS/xcode.rst | 49 +++++++++ 6 files changed, 299 insertions(+), 13 deletions(-) diff --git a/docs/reference/configuration.rst b/docs/reference/configuration.rst index 3d36d436e..1064fb6bb 100644 --- a/docs/reference/configuration.rst +++ b/docs/reference/configuration.rst @@ -458,6 +458,42 @@ sources from all levels, starting from least to most specific. A URL where more details about the application can be found. +Permissions +=========== + +Applications may also need to declare the permissions they require. Permissions are +specified as sub-attributes of a ``permissions`` property, defined at the level of an +project, app, or platform. Permission declarations are *cumulative*; if an application +defines permissions at the global level, application level, *and* platform level, the +final set of permissions will be the *merged* set of all permissions from all levels, +starting from least to most specific, with the most specific taking priority. + +Briefcase maintains a set of cross-platform permissions: + +* ``permissions.camera`` - permission to access to the camera to take photos or video. +* ``permissions.microphone`` - permission to access the microphone. +* ``permissions.coarse_location`` - permission to determine a rough GPS location. +* ``permissions.fine_location`` - permission to determine a precise GPS location. +* ``permissions.background_location`` - permission to track GPS location while in the background. +* ``permissions.photo_library`` - permission to access to the user's photo library. + +If a cross-platform permission is used, it will be mapped to platform-specific values. +Permissions can also be specified directly as platform-specific keys. For example, +Android defines a ``android.permission.HIGH_SAMPLING_RATE_SENSORS`` permission; this +could be specified by defining +``permissions."android.permission.HIGH_SAMPLING_RATE_SENSORS"``. If a platform-specific +key is specified, it will override any value specified as part of a cross-platform +value. + +The value for each permission is a short description of why that permission is required. +If the platform requires, the value may be displayed to the user as part of an +authorization dialog. This description should describe *why* the app requires the +permission, rather than a generic description of the permission being requested. The +values for platform-specific permissions may also be Boolean or integers if required. + +The use of cross-platform may also imply other settings in your app. See the individual +platform backends for details on how cross-platform permissions are mapped. + Document types ============== @@ -498,7 +534,7 @@ will use ``resources/icon.icns`` on macOS, and ``resources/icon.ico`` on Windows. Some platforms also require different *variants* (e.g., both square and round -icons). These variants can be specified by qualifying the icon specification: +icons). These variants can be specified by qualifying the icon specification:: icon.round = "resource/round-icon" icon.square = "resource/square-icon" diff --git a/docs/reference/platforms/android/gradle.rst b/docs/reference/platforms/android/gradle.rst index d8246017e..c40139f74 100644 --- a/docs/reference/platforms/android/gradle.rst +++ b/docs/reference/platforms/android/gradle.rst @@ -117,9 +117,69 @@ Android allows for some customization of the colors used by your app: Application configuration ========================= -The following options can be added to the -``tool.briefcase.app..android`` section of your ``pyproject.toml`` -file. +The following options can be added to the ``tool.briefcase.app..android`` +section of your ``pyproject.toml`` file. + +``android_manifest_attrs_extra_content`` +---------------------------------------- + +Additional attributes that will be added verbatim to add to the ```` +declaration of the ``AndroidManifest.xml`` of your app. + +``android_manifest_extra_content`` +---------------------------------- + +Additional content that will be added verbatim to add just before the closing +```` declaration of the ``AndroidManifest.xml`` of your app. + +``android_manifest_application_attrs_extra_content`` +---------------------------------------------------- + +Additional attributes that will be added verbatim to add to the ```` +declaration of the ``AndroidManifest.xml`` of your app. + +``android_manifest_application_extra_content`` +---------------------------------------------- + +Additional content that will be added verbatim to add just before the closing +```` declaration of the ``AndroidManifest.xml`` of your app. + +``android_manifest_activity_attrs_extra_content`` +------------------------------------------------- + +Additional attributes that will be added verbatim to add to the ```` +declaration of the ``AndroidManifest.xml`` of your app. + +``android_manifest_activity_extra_content`` +------------------------------------------- + +Additional content that will be added verbatim to add just before the closing +```` declaration of the ``AndroidManifest.xml`` of your app. + +``build_gradle_extra_content`` +------------------------------ + +A string providing additional Gradle settings to use when building your app. +This will be added verbatim to the end of your ``app/build.gradle`` file. + +``features`` +------------ + +A property whose sub-properties define the features that will be marked as required by +the final app. Each entry will be converted into a ```` declaration in +your app's ``AndroidManifest.xml``, with the feature name matching the name of the +sub-attribute. + +For example, specifying:: + + features."android.hardware.bluetooth" = true + +will result in an ``AndroidManifest.xml`` declaration of:: + + + +The use of some cross-platform permissions will imply the addition of features; see +:ref:`the discussion on Android permissions ` for more details. ``version_code`` ---------------- @@ -197,18 +257,48 @@ continue to run in the background, but there will be no visual manifestation that it is running. It may also be useful as a cleanup mechanism when running in a CI configuration. -Application configuration -========================= +.. _android-permissions: -The following options can be added to the -``tool.briefcase.app..android`` section of your ``pyproject.toml`` -file: +Permissions +=========== -``build_gradle_extra_content`` ------------------------------- +Briefcase cross platform permissions map to ```` declarations in the +app's ``AppManifest.xml``: -A string providing additional Gradle settings to use when building your app. -This will be added verbatim to the end of your ``app/build.gradle`` file. +* ``camera``: ``android.permission.CAMERA`` +* ``microphone``: ``android.permission.RECORD_AUDIO`` +* ``coarse_location``: ``android.permission.ACCESS_COARSE_LOCATION`` +* ``fine_location``: ``android.permission.ACCESS_FINE_LOCATION`` +* ``background_location``: ``android.permission.ACCESS_BACKGROUND_LOCATION`` +* ``photo_library``: ``android.permission.READ_MEDIA_VISUAL_USER_SELECTED`` + +Every application will be automatically granted the ``android.permission.INTERNET`` and +``android.permission.NETWORK_STATE`` permissions. + +Specifying a ``camera`` permission will result in the following non-required features +being implicitly added to your app: + +* ``android.hardware.camera``, +* ``android.hardware.camera.any``, +* ``android.hardware.camera.front``, +* ``android.hardware.camera.external`` and +* ``android.hardware.camera.autofocus``. + +Specifying the ``coarse_location``, ``fine_location`` or ``background_location`` +permissions will result in the following non-required features being implicitly added to +your app: + +* ``android.hardware.location.network`` +* ``android.hardware.location.gps`` + +This is done to ensure that an app is not prevented from installing if the device +doesn't have the given features. You can make the feature explicitly required by +manually defining these feature requirements. For example, to make location hardware +required, you could add the following to the Android section of your +``pyproject.toml``:: + + feature."android.hardware.location.network" = True + feature."android.hardware.location.gps" = True Platform quirks =============== diff --git a/docs/reference/platforms/iOS/xcode.rst b/docs/reference/platforms/iOS/xcode.rst index ebc38d012..6a0e76c9f 100644 --- a/docs/reference/platforms/iOS/xcode.rst +++ b/docs/reference/platforms/iOS/xcode.rst @@ -64,6 +64,34 @@ run The device simulator to target. Can be either a UDID, a device name (e.g., ``"iPhone 11"``), or a device name and OS version (``"iPhone 11::iOS 13.3"``). +Application configuration +========================= + +The following options can be added to the ``tool.briefcase.app..iOS.app`` +section of your ``pyproject.toml`` file. + +``info_plist_extra_content`` +---------------------------- + +A string providing additional content that will be added verbatim to the end of your +app's ``Info.plist`` file, at the end of the main ```` declaration. + +Permissions +=========== + +Briefcase cross platform permissions map to the following keys in the app's +``Info.plist``: + +* ``camera``: ``NSCameraUsageDescription`` +* ``microphone``: ``NSMicrophoneUsageDescription`` +* ``coarse_location``: ``NSLocationDefaultAccuracyReduced=True`` if ``fine_location`` is + not defined, plus ``NSLocationWhenInUseUsageDescription`` if ``background_location`` + is not defined +* ``fine_location``: ``NSLocationDefaultAccuracyReduced=False``, plus + ``NSLocationWhenInUseUsageDescription`` if ``background_location`` is not defined +* ``background_location``: ``NSLocationAlwaysAndWhenInUseUsageDescription`` +* ``photo_library``: ``NSPhotoLibraryAddUsageDescription`` + Platform quirks =============== diff --git a/docs/reference/platforms/linux/flatpak.rst b/docs/reference/platforms/linux/flatpak.rst index e4058cf17..6ec303bf9 100644 --- a/docs/reference/platforms/linux/flatpak.rst +++ b/docs/reference/platforms/linux/flatpak.rst @@ -77,6 +77,35 @@ The following options can be added to the ``tool.briefcase.app..linux.flatpak`` section of your ``pyproject.toml`` file: +``finish_args`` +~~~~~~~~~~~~~~~ + +The arguments used to configure the Flatpak sandbox. ``finish_args`` is an attribute +that can have additional sub-attributes; each sub-attribute maps to a single property +that will be added to the app's manifest. For example, to add ``--allow=bluetooth`` as a +finish argument, you would specify:: + + device_requires."allow=bluetooth" = True + +Briefcase adds the following finish arguments by default: + +* ``share=ipc`` +* ``socket=x11`` +* ``nosocket=wayland`` +* ``share=network`` +* ``device=dri`` +* ``socket=pulseaudio`` +* ``filesystem=xdg-cache`` +* ``filesystem=xdg-config`` +* ``filesystem=xdg-data`` +* ``filesystem=xdg-documents`` +* ``socket=session-bus`` + +These can be disabled by explicitly setting their value to ``False``; for example, to +disable audio access, you would specify:: + + device_requires."socket=pulseaudio" = false + ``flatpak_runtime_repo_alias`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -115,6 +144,11 @@ build the Flatpak app. The Flatpak runtime and SDK are paired; so, both a ``flatpak_runtime`` and a corresponding ``flatpak_sdk`` must be defined. +Permissions +=========== + +Permissions are not used for Flatpak packaging. + Compilation issues with Flatpak =============================== diff --git a/docs/reference/platforms/macOS/app.rst b/docs/reference/platforms/macOS/app.rst index f68633239..e261dfa93 100644 --- a/docs/reference/platforms/macOS/app.rst +++ b/docs/reference/platforms/macOS/app.rst @@ -59,6 +59,37 @@ Application configuration The following options can be added to the ``tool.briefcase.app..macOS.app`` section of your ``pyproject.toml`` file. +``entitlement`` +~~~~~~~~~~~~~~~ + +A property whose sub-attributes define keys that will be added to the app's +``Entitlements.plist`` file. Each entry will be converted into a key in the entitlements +file. For example, specifying:: + + entitlements."com.apple.vm.networking" = true + +will result in an ``Entitlements.plist`` declaration of:: + + com.apple.vm.networking + +Any Boolean or string value can be used for an entitlement value. + +All macOS apps are automatically granted the following entitlements: + +* ``com.apple.security.cs.allow-unsigned-executable-memory`` +* ``com.apple.security.cs.disable-library-validation`` + +You can disable these default entitlements by defining them manually. For example, to +enable library validation, you could add the following to your ``pyproject.toml``:: + + entitlement."com.apple.security.cs.disable-library-validation" = false + +``info_plist_extra_content`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +A string providing additional content that will be added verbatim to the end of your +app's ``Info.plist`` file, at the end of the main ```` declaration. + ``universal_build`` ~~~~~~~~~~~~~~~~~~~ @@ -68,6 +99,24 @@ only be executable on the host platform on which it was built - i.e., if you bui an x86_64 machine, you will produce an x86_65 binary; if you build on an ARM64 machine, you will produce an ARM64 binary. +Permissions +=========== + +Briefcase cross platform permissions map to a combination of ``entitlement`` +definitions, and keys in the app's ``Info.plist``: + +* ``camera``: an entitlement of ``com.apple.security.device.camera`` +* ``microphone``: an entitlement of ``com.apple.security.device.audio-input`` +* ``coarse_location``: an ``Info.plist`` entry for ``NSLocationUsageDescription`` + (ignored if ``fine_location`` is defined); plus an entitlement of + ``com.apple.security.personal-information.location`` +* ``fine_location``: an ``Info.plist`` entry for ``NSLocationUsageDescription``; plus a + device requirement of ``com.apple.security.personal-information.location`` +* ``background_location``: an ``Info.plist`` entry for ``NSLocationUsageDescription`` + (ignored if ``fine_location`` or ``coarse_location`` is defined); plus an entitlement + of ``com.apple.security.personal-information.location`` +* ``photo_library``: an entitlement of ``com.apple.security.personal-information.photos-library`` + Platform quirks =============== diff --git a/docs/reference/platforms/macOS/xcode.rst b/docs/reference/platforms/macOS/xcode.rst index 49d756a04..da9709b85 100644 --- a/docs/reference/platforms/macOS/xcode.rst +++ b/docs/reference/platforms/macOS/xcode.rst @@ -65,6 +65,37 @@ Application configuration The following options can be added to the ``tool.briefcase.app..macOS.Xcode`` section of your ``pyproject.toml`` file. +``entitlement`` +~~~~~~~~~~~~~~~ + +A property whose sub-attributes define keys that will be added to the app's +``Entitlements.plist`` file. Each entry will be converted into a key in the entitlements +file. For example, specifying:: + + entitlements."com.apple.vm.networking" = true + +will result in an ``Entitlements.plist`` declaration of:: + + com.apple.vm.networking + +Any Boolean or string value can be used for an entitlement value. + +All macOS apps are automatically granted the following entitlements: + +* ``com.apple.security.cs.allow-unsigned-executable-memory`` +* ``com.apple.security.cs.disable-library-validation`` + +You can disable these default entitlements by defining them manually. For example, to +enable library validation, you could add the following to your ``pyproject.toml``:: + + entitlement."com.apple.security.cs.disable-library-validation" = false + +``info_plist_extra_content`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +A string providing additional content that will be added verbatim to the end of your +app's ``Info.plist`` file, at the end of the main ```` declaration. + ``universal_build`` ~~~~~~~~~~~~~~~~~~~ @@ -74,6 +105,24 @@ only be executable on the host platform on which it was built - i.e., if you bui an x86_64 machine, you will produce an x86_65 binary; if you build on an ARM64 machine, you will produce an ARM64 binary. +Permissions +=========== + +Briefcase cross platform permissions map to a combination of ``entitlement`` +definitions, and keys in the app's ``Info.plist``: + +* ``camera``: an entitlement of ``com.apple.security.device.camera`` +* ``microphone``: an entitlement of ``com.apple.security.device.audio-input`` +* ``coarse_location``: an ``Info.plist`` entry for ``NSLocationUsageDescription`` + (ignored if ``fine_location`` is defined); plus an entitlement of + ``com.apple.security.personal-information.location`` +* ``fine_location``: an ``Info.plist`` entry for ``NSLocationUsageDescription``; plus a + device requirement of ``com.apple.security.personal-information.location`` +* ``background_location``: an ``Info.plist`` entry for ``NSLocationUsageDescription`` + (ignored if ``fine_location`` or ``coarse_location`` is defined); plus an entitlement + of ``com.apple.security.personal-information.location`` +* ``photo_library``: an entitlement of ``com.apple.security.personal-information.photos-library`` + Platform quirks =============== From 528746f866c38b1bde5e5bef36f7a50e6cbf5884 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Thu, 11 Jan 2024 13:29:24 +0800 Subject: [PATCH 04/16] Add default permissions property to AppConfig. --- src/briefcase/config.py | 2 ++ tests/commands/create/test_generate_app_template.py | 1 + 2 files changed, 3 insertions(+) diff --git a/src/briefcase/config.py b/src/briefcase/config.py index 1fe867416..000253880 100644 --- a/src/briefcase/config.py +++ b/src/briefcase/config.py @@ -191,6 +191,7 @@ def __init__( icon=None, splash=None, document_type=None, + permissions=None, template=None, template_branch=None, test_sources=None, @@ -218,6 +219,7 @@ def __init__( self.icon = icon self.splash = splash self.document_types = {} if document_type is None else document_type + self.permissions = {} if permissions is None else permissions self.template = template self.template_branch = template_branch self.test_sources = test_sources diff --git a/tests/commands/create/test_generate_app_template.py b/tests/commands/create/test_generate_app_template.py index 4800600f6..2507e1766 100644 --- a/tests/commands/create/test_generate_app_template.py +++ b/tests/commands/create/test_generate_app_template.py @@ -38,6 +38,7 @@ def full_context(): "icon": None, "splash": None, "supported": True, + "permissions": {}, "document_types": {}, # Properties of the generating environment "python_version": platform.python_version(), From 138e2bcbec794c6cb967e76c575669cf647508d3 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Thu, 11 Jan 2024 13:40:51 +0800 Subject: [PATCH 05/16] Add some utilities for cookiecutter output. --- src/briefcase/integrations/cookiecutter.py | 34 +++++++++++++++++++ .../cookiecutter/test_PListExtension.py | 20 +++++++++++ .../cookiecutter/test_XMLExtension.py | 26 ++++++++++++++ 3 files changed, 80 insertions(+) create mode 100644 tests/integrations/cookiecutter/test_PListExtension.py create mode 100644 tests/integrations/cookiecutter/test_XMLExtension.py diff --git a/src/briefcase/integrations/cookiecutter.py b/src/briefcase/integrations/cookiecutter.py index 64b766309..4c292af18 100644 --- a/src/briefcase/integrations/cookiecutter.py +++ b/src/briefcase/integrations/cookiecutter.py @@ -73,3 +73,37 @@ def escape_non_ascii(obj): environment.filters["escape_toml"] = escape_toml environment.filters["escape_non_ascii"] = escape_non_ascii + + +class PListExtension(Extension): + """Jinja2 extension for generating plist values.""" + + def __init__(self, environment): + """Initialize the extension with the given environment.""" + super().__init__(environment) + + def plist_value(obj): + """Render value in plist format.""" + if isinstance(obj, bool): + if obj: + return "" + else: + return "" + else: + return f"{obj}" + + environment.filters["plist_value"] = plist_value + + +class XMLExtension(Extension): + """Jinja2 extension for generating XML values.""" + + def __init__(self, environment): + """Initialize the extension with the given environment.""" + super().__init__(environment) + + def bool_attr(obj): + """Render value in XML format appropriate for an attribute.""" + return "true" if obj else "false" + + environment.filters["bool_attr"] = bool_attr diff --git a/tests/integrations/cookiecutter/test_PListExtension.py b/tests/integrations/cookiecutter/test_PListExtension.py new file mode 100644 index 000000000..825bf4508 --- /dev/null +++ b/tests/integrations/cookiecutter/test_PListExtension.py @@ -0,0 +1,20 @@ +from unittest.mock import MagicMock + +import pytest + +from briefcase.integrations.cookiecutter import PListExtension + + +@pytest.mark.parametrize( + "value, expected", + [ + (True, ""), + (False, ""), + ("Hello world", "Hello world"), + ], +) +def test_plist_value(value, expected): + env = MagicMock() + env.filters = {} + PListExtension(env) + assert env.filters["plist_value"](value) == expected diff --git a/tests/integrations/cookiecutter/test_XMLExtension.py b/tests/integrations/cookiecutter/test_XMLExtension.py new file mode 100644 index 000000000..9fc41fe07 --- /dev/null +++ b/tests/integrations/cookiecutter/test_XMLExtension.py @@ -0,0 +1,26 @@ +from unittest.mock import MagicMock + +import pytest + +from briefcase.integrations.cookiecutter import XMLExtension + + +@pytest.mark.parametrize( + "value, expected", + [ + # Literal booleans + (True, "true"), + (False, "false"), + # True-ish values + (1, "true"), + ("Hello", "true"), + # False-ish values + (0, "false"), + ("", "false"), + ], +) +def bool_attr(value, expected): + env = MagicMock() + env.filters = {} + XMLExtension(env) + assert env.filters["bool_attr"](value) == expected From 9cab944db79568462643caa1413a3118115d7e08 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Thu, 11 Jan 2024 14:24:31 +0800 Subject: [PATCH 06/16] Added a hook for platform backends to interpret cross-platform permissions. --- src/briefcase/commands/create.py | 35 +++++++++++ tests/commands/create/conftest.py | 13 ++++ .../create/test_generate_app_template.py | 60 +++++++++++++++++++ 3 files changed, 108 insertions(+) diff --git a/src/briefcase/commands/create.py b/src/briefcase/commands/create.py index 4baa19caf..e093c64ff 100644 --- a/src/briefcase/commands/create.py +++ b/src/briefcase/commands/create.py @@ -166,6 +166,38 @@ def document_type_icon_targets(self, app: AppConfig): except KeyError: return {} + def _x_permissions(self, app: AppConfig): + """Extract the known cross-platform permission definitions from the app's + permissions definitions. + + After calling this method, the ``permissions`` declaration for the app will + only contain keys that are *not* cross-platform keys. + + :param app: The config object for the app + :returns: A dictionary of known cross-platform permission definitions. + """ + return { + key: app.permissions.pop(key, False) + for key in [ + "camera", + "microphone", + "coarse_location", + "fine_location", + "background_location", + "photo_library", + ] + } + + def permissions_context(self, app: AppConfig, x_permissions: dict[str, str]): + """Additional template context for permissions. + + :param app: The config object for the app + :param x_permissions: The dictionary of known cross-platform permission + definitions. + :returns: The template context describing permissions for the app. + """ + return {} + def output_format_template_context(self, app: AppConfig): """Additional template context required by the output format. @@ -223,6 +255,9 @@ def generate_app_template(self, app: AppConfig): } ) + # Add in any extra template context to support permissions + extra_context.update(self.permissions_context(app, self._x_permissions(app))) + # Add in any extra template context required by the output format. extra_context.update(self.output_format_template_context(app)) diff --git a/tests/commands/create/conftest.py b/tests/commands/create/conftest.py index 87f432402..fcc526e67 100644 --- a/tests/commands/create/conftest.py +++ b/tests/commands/create/conftest.py @@ -89,6 +89,19 @@ def python_version_tag(self): def output_format_template_context(self, app): return {"output_format": "dummy"} + # Handle platform-specific permissions. + # Convert all the cross-platform permissions to upper case, prefixing DUMMY_. + # All other permissions are returned under a "custom" key. + def permissions_context(self, app: AppConfig, x_permissions: dict[str, str]): + return { + "x_permissions": { + f"DUMMY_{key.upper()}": value.upper() + for key, value in x_permissions.items() + if value + }, + "permissions": app.permissions, + } + class TrackingCreateCommand(DummyCreateCommand): """A dummy creation command that doesn't actually do anything. diff --git a/tests/commands/create/test_generate_app_template.py b/tests/commands/create/test_generate_app_template.py index 2507e1766..847cbb2c7 100644 --- a/tests/commands/create/test_generate_app_template.py +++ b/tests/commands/create/test_generate_app_template.py @@ -39,6 +39,7 @@ def full_context(): "splash": None, "supported": True, "permissions": {}, + "x_permissions": {}, "document_types": {}, # Properties of the generating environment "python_version": platform.python_version(), @@ -625,3 +626,62 @@ def test_cached_missing_branch_template(monkeypatch, create_command, myapp): # Generating the template under there conditions raises an error with pytest.raises(TemplateUnsupportedVersion): create_command.generate_app_template(myapp) + + +def test_x_permissions( + monkeypatch, + create_command, + myapp, + full_context, + tmp_path, +): + # Set the Briefcase version + monkeypatch.setattr(briefcase, "__version__", "37.42.7") + full_context["briefcase_version"] = "37.42.7" + + # Define some permissions + myapp.permissions = { + # Cross-platform permissions + "camera": "I need to see you", + "microphone": "I need to hear you", + "coarse_location": "I need to know approximately where you are", + "fine_location": "I need to know exactly where you are", + "background_location": "I need to know where you are constantly", + "photo_library": "I need to see your photos", + # Custom permissions + "DUMMY_sit": "I can't sit without an invitation", + "DUMMY.leave.the.dinner.table": "It would be impolite.", + } + + # In the final context, all cross-platform permissions have been converted to upper + # case, prefixed with "DUMMY", and moved to the `x_permissions` key. + full_context["x_permissions"] = { + "DUMMY_CAMERA": "I NEED TO SEE YOU", + "DUMMY_MICROPHONE": "I NEED TO HEAR YOU", + "DUMMY_COARSE_LOCATION": "I NEED TO KNOW APPROXIMATELY WHERE YOU ARE", + "DUMMY_FINE_LOCATION": "I NEED TO KNOW EXACTLY WHERE YOU ARE", + "DUMMY_BACKGROUND_LOCATION": "I NEED TO KNOW WHERE YOU ARE CONSTANTLY", + "DUMMY_PHOTO_LIBRARY": "I NEED TO SEE YOUR PHOTOS", + } + + # All custom permissions are left as-is. + full_context["permissions"] = { + "DUMMY_sit": "I can't sit without an invitation", + "DUMMY.leave.the.dinner.table": "It would be impolite.", + } + + # There won't be a cookiecutter cache, so there won't be + # a cache path (yet). + create_command.tools.git.Repo.side_effect = git_exceptions.NoSuchPathError + + # Generate the template. + create_command.generate_app_template(myapp) + + # Cookiecutter was invoked with the expected template name and context. + create_command.tools.cookiecutter.assert_called_once_with( + "https://github.com/beeware/briefcase-Tester-Dummy-template.git", + no_input=True, + checkout="v37.42.7", + output_dir=os.fsdecode(tmp_path / "base_path/build/my-app/tester"), + extra_context=full_context, + ) From 7d447d9a4d75f82e81b89c263197e3dc1dcb8d2a Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Fri, 12 Jan 2024 10:26:48 +0800 Subject: [PATCH 07/16] Converge on using the singular for definitions, plural for template context. --- docs/reference/configuration.rst | 16 ++++----- docs/reference/platforms/android/gradle.rst | 9 +++-- docs/reference/platforms/linux/flatpak.rst | 10 +++--- docs/reference/platforms/macOS/app.rst | 14 ++++---- docs/reference/platforms/macOS/xcode.rst | 14 ++++---- src/briefcase/commands/create.py | 11 +++++-- src/briefcase/config.py | 9 ++--- tests/commands/create/conftest.py | 29 +++++++++++----- .../create/test_generate_app_template.py | 26 ++++++++++----- tests/config/test_merge_config.py | 33 +++++++++---------- 10 files changed, 97 insertions(+), 74 deletions(-) diff --git a/docs/reference/configuration.rst b/docs/reference/configuration.rst index 1064fb6bb..4302e144d 100644 --- a/docs/reference/configuration.rst +++ b/docs/reference/configuration.rst @@ -462,7 +462,7 @@ Permissions =========== Applications may also need to declare the permissions they require. Permissions are -specified as sub-attributes of a ``permissions`` property, defined at the level of an +specified as sub-attributes of a ``permission`` property, defined at the level of an project, app, or platform. Permission declarations are *cumulative*; if an application defines permissions at the global level, application level, *and* platform level, the final set of permissions will be the *merged* set of all permissions from all levels, @@ -470,18 +470,18 @@ starting from least to most specific, with the most specific taking priority. Briefcase maintains a set of cross-platform permissions: -* ``permissions.camera`` - permission to access to the camera to take photos or video. -* ``permissions.microphone`` - permission to access the microphone. -* ``permissions.coarse_location`` - permission to determine a rough GPS location. -* ``permissions.fine_location`` - permission to determine a precise GPS location. -* ``permissions.background_location`` - permission to track GPS location while in the background. -* ``permissions.photo_library`` - permission to access to the user's photo library. +* ``permission.camera`` - permission to access to the camera to take photos or video. +* ``permission.microphone`` - permission to access the microphone. +* ``permission.coarse_location`` - permission to determine a rough GPS location. +* ``permission.fine_location`` - permission to determine a precise GPS location. +* ``permission.background_location`` - permission to track GPS location while in the background. +* ``permission.photo_library`` - permission to access to the user's photo library. If a cross-platform permission is used, it will be mapped to platform-specific values. Permissions can also be specified directly as platform-specific keys. For example, Android defines a ``android.permission.HIGH_SAMPLING_RATE_SENSORS`` permission; this could be specified by defining -``permissions."android.permission.HIGH_SAMPLING_RATE_SENSORS"``. If a platform-specific +``permission."android.permission.HIGH_SAMPLING_RATE_SENSORS"``. If a platform-specific key is specified, it will override any value specified as part of a cross-platform value. diff --git a/docs/reference/platforms/android/gradle.rst b/docs/reference/platforms/android/gradle.rst index c40139f74..403b1773a 100644 --- a/docs/reference/platforms/android/gradle.rst +++ b/docs/reference/platforms/android/gradle.rst @@ -162,8 +162,8 @@ Additional content that will be added verbatim to add just before the closing A string providing additional Gradle settings to use when building your app. This will be added verbatim to the end of your ``app/build.gradle`` file. -``features`` ------------- +``feature`` +----------- A property whose sub-properties define the features that will be marked as required by the final app. Each entry will be converted into a ```` declaration in @@ -172,7 +172,7 @@ sub-attribute. For example, specifying:: - features."android.hardware.bluetooth" = true + feature."android.hardware.bluetooth" = true will result in an ``AndroidManifest.xml`` declaration of:: @@ -293,11 +293,10 @@ your app: This is done to ensure that an app is not prevented from installing if the device doesn't have the given features. You can make the feature explicitly required by -manually defining these feature requirements. For example, to make location hardware +manually defining these feature requirements. For example, to make GPS hardware required, you could add the following to the Android section of your ``pyproject.toml``:: - feature."android.hardware.location.network" = True feature."android.hardware.location.gps" = True Platform quirks diff --git a/docs/reference/platforms/linux/flatpak.rst b/docs/reference/platforms/linux/flatpak.rst index 6ec303bf9..7db89f935 100644 --- a/docs/reference/platforms/linux/flatpak.rst +++ b/docs/reference/platforms/linux/flatpak.rst @@ -77,15 +77,15 @@ The following options can be added to the ``tool.briefcase.app..linux.flatpak`` section of your ``pyproject.toml`` file: -``finish_args`` -~~~~~~~~~~~~~~~ +``finish_arg`` +~~~~~~~~~~~~~~ -The arguments used to configure the Flatpak sandbox. ``finish_args`` is an attribute +The arguments used to configure the Flatpak sandbox. ``finish_arg`` is an attribute that can have additional sub-attributes; each sub-attribute maps to a single property that will be added to the app's manifest. For example, to add ``--allow=bluetooth`` as a finish argument, you would specify:: - device_requires."allow=bluetooth" = True + finish_arg."allow=bluetooth" = True Briefcase adds the following finish arguments by default: @@ -104,7 +104,7 @@ Briefcase adds the following finish arguments by default: These can be disabled by explicitly setting their value to ``False``; for example, to disable audio access, you would specify:: - device_requires."socket=pulseaudio" = false + finish_arg."socket=pulseaudio" = false ``flatpak_runtime_repo_alias`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/reference/platforms/macOS/app.rst b/docs/reference/platforms/macOS/app.rst index e261dfa93..49b0b335f 100644 --- a/docs/reference/platforms/macOS/app.rst +++ b/docs/reference/platforms/macOS/app.rst @@ -66,7 +66,7 @@ A property whose sub-attributes define keys that will be added to the app's ``Entitlements.plist`` file. Each entry will be converted into a key in the entitlements file. For example, specifying:: - entitlements."com.apple.vm.networking" = true + entitlement."com.apple.vm.networking" = true will result in an ``Entitlements.plist`` declaration of:: @@ -108,13 +108,13 @@ definitions, and keys in the app's ``Info.plist``: * ``camera``: an entitlement of ``com.apple.security.device.camera`` * ``microphone``: an entitlement of ``com.apple.security.device.audio-input`` * ``coarse_location``: an ``Info.plist`` entry for ``NSLocationUsageDescription`` - (ignored if ``fine_location`` is defined); plus an entitlement of + (ignored if ``background_location`` or ``fine_location`` is defined); plus an + entitlement of ``com.apple.security.personal-information.location`` +* ``fine_location``: an ``Info.plist`` entry for ``NSLocationUsageDescription``(ignored + if ``background_location`` is defined); plus a device requirement of ``com.apple.security.personal-information.location`` -* ``fine_location``: an ``Info.plist`` entry for ``NSLocationUsageDescription``; plus a - device requirement of ``com.apple.security.personal-information.location`` -* ``background_location``: an ``Info.plist`` entry for ``NSLocationUsageDescription`` - (ignored if ``fine_location`` or ``coarse_location`` is defined); plus an entitlement - of ``com.apple.security.personal-information.location`` +* ``background_location``: an ``Info.plist`` entry for ``NSLocationUsageDescription``; + plus an entitlement of ``com.apple.security.personal-information.location`` * ``photo_library``: an entitlement of ``com.apple.security.personal-information.photos-library`` Platform quirks diff --git a/docs/reference/platforms/macOS/xcode.rst b/docs/reference/platforms/macOS/xcode.rst index da9709b85..931a511c4 100644 --- a/docs/reference/platforms/macOS/xcode.rst +++ b/docs/reference/platforms/macOS/xcode.rst @@ -72,7 +72,7 @@ A property whose sub-attributes define keys that will be added to the app's ``Entitlements.plist`` file. Each entry will be converted into a key in the entitlements file. For example, specifying:: - entitlements."com.apple.vm.networking" = true + entitlement."com.apple.vm.networking" = true will result in an ``Entitlements.plist`` declaration of:: @@ -114,13 +114,13 @@ definitions, and keys in the app's ``Info.plist``: * ``camera``: an entitlement of ``com.apple.security.device.camera`` * ``microphone``: an entitlement of ``com.apple.security.device.audio-input`` * ``coarse_location``: an ``Info.plist`` entry for ``NSLocationUsageDescription`` - (ignored if ``fine_location`` is defined); plus an entitlement of + (ignored if ``background_location`` or ``fine_location`` is defined); plus an + entitlement of ``com.apple.security.personal-information.location`` +* ``fine_location``: an ``Info.plist`` entry for ``NSLocationUsageDescription``(ignored + if ``background_location`` is defined); plus a device requirement of ``com.apple.security.personal-information.location`` -* ``fine_location``: an ``Info.plist`` entry for ``NSLocationUsageDescription``; plus a - device requirement of ``com.apple.security.personal-information.location`` -* ``background_location``: an ``Info.plist`` entry for ``NSLocationUsageDescription`` - (ignored if ``fine_location`` or ``coarse_location`` is defined); plus an entitlement - of ``com.apple.security.personal-information.location`` +* ``background_location``: an ``Info.plist`` entry for ``NSLocationUsageDescription``; + plus an entitlement of ``com.apple.security.personal-information.location`` * ``photo_library``: an entitlement of ``com.apple.security.personal-information.photos-library`` Platform quirks diff --git a/src/briefcase/commands/create.py b/src/briefcase/commands/create.py index e093c64ff..81e98697c 100644 --- a/src/briefcase/commands/create.py +++ b/src/briefcase/commands/create.py @@ -84,6 +84,9 @@ class CreateCommand(BaseCommand): command = "create" description = "Create a new app for a target platform." + # app properties that won't be exposed to the context + hidden_app_properties = {"permission"} + @property def app_template_url(self): """The URL for a cookiecutter repository to use when creating apps.""" @@ -177,7 +180,7 @@ def _x_permissions(self, app: AppConfig): :returns: A dictionary of known cross-platform permission definitions. """ return { - key: app.permissions.pop(key, False) + key: app.permission.pop(key, None) for key in [ "camera", "microphone", @@ -226,7 +229,11 @@ def generate_app_template(self, app: AppConfig): template_branch = app.template_branch # Construct a template context from the app configuration. - extra_context = app.__dict__.copy() + extra_context = { + key: value + for key, value in app.__dict__.items() + if key not in self.hidden_app_properties + } # Remove the context items that describe the template extra_context.pop("template") diff --git a/src/briefcase/config.py b/src/briefcase/config.py index 000253880..a86f8f2ac 100644 --- a/src/briefcase/config.py +++ b/src/briefcase/config.py @@ -191,7 +191,7 @@ def __init__( icon=None, splash=None, document_type=None, - permissions=None, + permission=None, template=None, template_branch=None, test_sources=None, @@ -219,7 +219,7 @@ def __init__( self.icon = icon self.splash = splash self.document_types = {} if document_type is None else document_type - self.permissions = {} if permissions is None else permissions + self.permission = {} if permission is None else permission self.template = template self.template_branch = template_branch self.test_sources = test_sources @@ -361,10 +361,7 @@ def merge_config(config, data): config.setdefault(option, []).extend(value) # Properties that are cumulative tables - for option in [ - "permissions", - "device_requires", - ]: + for option in ["permission"]: value = data.pop(option, {}) if value: diff --git a/tests/commands/create/conftest.py b/tests/commands/create/conftest.py index fcc526e67..d5039cb26 100644 --- a/tests/commands/create/conftest.py +++ b/tests/commands/create/conftest.py @@ -46,6 +46,7 @@ class DummyCreateCommand(CreateCommand): platform = "Tester" output_format = "Dummy" description = "Dummy create command" + hidden_app_properties = {"permission", "request"} def __init__(self, *args, support_file=None, git=None, home_path=None, **kwargs): kwargs.setdefault("logger", Log()) @@ -91,16 +92,28 @@ def output_format_template_context(self, app): # Handle platform-specific permissions. # Convert all the cross-platform permissions to upper case, prefixing DUMMY_. - # All other permissions are returned under a "custom" key. + # Add a "good lighting" request if the camera permission has been requested. def permissions_context(self, app: AppConfig, x_permissions: dict[str, str]): - return { - "x_permissions": { - f"DUMMY_{key.upper()}": value.upper() - for key, value in x_permissions.items() - if value - }, - "permissions": app.permissions, + # We don't actually need anything from the superclass; but call it to ensure + # coverage. + context = super().permissions_context(app, x_permissions) + if context: + # Make sure the base class *isn't* doing anything. + return context + + permissions = { + f"DUMMY_{key.upper()}": value.upper() + for key, value in x_permissions.items() + if value } + context["permissions"] = permissions + context["custom_permissions"] = app.permission + + requests = {"good.lighting": True} if x_permissions["camera"] else {} + requests.update(getattr(app, "request", {})) + context["requests"] = requests + + return context class TrackingCreateCommand(DummyCreateCommand): diff --git a/tests/commands/create/test_generate_app_template.py b/tests/commands/create/test_generate_app_template.py index 847cbb2c7..515ee4dbc 100644 --- a/tests/commands/create/test_generate_app_template.py +++ b/tests/commands/create/test_generate_app_template.py @@ -39,7 +39,8 @@ def full_context(): "splash": None, "supported": True, "permissions": {}, - "x_permissions": {}, + "custom_permissions": {}, + "requests": {}, "document_types": {}, # Properties of the generating environment "python_version": platform.python_version(), @@ -639,8 +640,10 @@ def test_x_permissions( monkeypatch.setattr(briefcase, "__version__", "37.42.7") full_context["briefcase_version"] = "37.42.7" - # Define some permissions - myapp.permissions = { + # Define some permissions and requests. The original "permission" and "request" + # definitions will be hidden from the final template context. + + myapp.permission = { # Cross-platform permissions "camera": "I need to see you", "microphone": "I need to hear you", @@ -652,10 +655,12 @@ def test_x_permissions( "DUMMY_sit": "I can't sit without an invitation", "DUMMY.leave.the.dinner.table": "It would be impolite.", } + myapp.request = {"tasty.beverage": True} # In the final context, all cross-platform permissions have been converted to upper - # case, prefixed with "DUMMY", and moved to the `x_permissions` key. - full_context["x_permissions"] = { + # case, prefixed with "DUMMY", and moved to the `permissions` key. Custom + # permissions have been moved to the "custom_permissions" key + full_context["permissions"] = { "DUMMY_CAMERA": "I NEED TO SEE YOU", "DUMMY_MICROPHONE": "I NEED TO HEAR YOU", "DUMMY_COARSE_LOCATION": "I NEED TO KNOW APPROXIMATELY WHERE YOU ARE", @@ -663,13 +668,18 @@ def test_x_permissions( "DUMMY_BACKGROUND_LOCATION": "I NEED TO KNOW WHERE YOU ARE CONSTANTLY", "DUMMY_PHOTO_LIBRARY": "I NEED TO SEE YOUR PHOTOS", } - - # All custom permissions are left as-is. - full_context["permissions"] = { + full_context["custom_permissions"] = { "DUMMY_sit": "I can't sit without an invitation", "DUMMY.leave.the.dinner.table": "It would be impolite.", } + # An extra request has been added because of the camera permission, and the + # custom request has been preserved. + full_context["requests"] = { + "good.lighting": True, + "tasty.beverage": True, + } + # There won't be a cookiecutter cache, so there won't be # a cache path (yet). create_command.tools.git.Repo.side_effect = git_exceptions.NoSuchPathError diff --git a/tests/config/test_merge_config.py b/tests/config/test_merge_config.py index aca5b71b6..ab3cf13e8 100644 --- a/tests/config/test_merge_config.py +++ b/tests/config/test_merge_config.py @@ -14,7 +14,7 @@ def test_merge_no_data(): """If there are no new options, nothing changes.""" config = { "requires": ["first", "second"], - "permissions": {"up": True, "down": False}, + "permission": {"up": True, "down": False}, "other": 1234, } @@ -22,7 +22,7 @@ def test_merge_no_data(): assert config == { "requires": ["first", "second"], - "permissions": {"up": True, "down": False}, + "permission": {"up": True, "down": False}, "other": 1234, } @@ -35,13 +35,13 @@ def test_merge_no_option(): config, { "requires": ["third", "fourth"], - "permissions": {"left": True, "right": False}, + "permission": {"left": True, "right": False}, }, ) assert config == { "requires": ["third", "fourth"], - "permissions": {"left": True, "right": False}, + "permission": {"left": True, "right": False}, "other": 1234, } @@ -50,7 +50,7 @@ def test_merge(): """If there are existing options and new options, merge.""" config = { "requires": ["first", "second"], - "permissions": {"up": True, "down": False}, + "permission": {"up": True, "down": False}, "other": 1234, } @@ -58,14 +58,14 @@ def test_merge(): config, { "requires": ["third", "fourth"], - "permissions": {"left": True, "right": False}, + "permission": {"left": True, "right": False}, "other": 5678, }, ) assert config == { "requires": ["first", "second", "third", "fourth"], - "permissions": {"up": True, "down": False, "left": True, "right": False}, + "permission": {"up": True, "down": False, "left": True, "right": False}, "other": 5678, } @@ -75,7 +75,7 @@ def test_merge_collision(): new options, merge.""" config = { "requires": ["first", "second"], - "permissions": {"up": True, "down": False}, + "permission": {"up": True, "down": False}, "other": 1234, } @@ -83,14 +83,14 @@ def test_merge_collision(): config, { "requires": ["second", "fourth"], - "permissions": {"down": True, "right": False}, + "permission": {"down": True, "right": False}, "other": 5678, }, ) assert config == { "requires": ["first", "second", "second", "fourth"], - "permissions": {"up": True, "down": True, "right": False}, + "permission": {"up": True, "down": True, "right": False}, "other": 5678, } @@ -99,7 +99,7 @@ def test_convert_base_definition(): """The merge operation succeeds when called on itself.""" config = { "requires": ["first", "second"], - "permissions": {"up": True, "down": False}, + "permission": {"up": True, "down": False}, "other": 1234, } @@ -107,7 +107,7 @@ def test_convert_base_definition(): assert config == { "requires": ["first", "second"], - "permissions": {"up": True, "down": False}, + "permission": {"up": True, "down": False}, "other": 1234, } @@ -117,8 +117,7 @@ def test_merged_keys(): config = { "requires": ["first", "second"], "sources": ["a", "b"], - "permissions": {"up": True, "down": False}, - "device_requires": {"north": True, "south": False}, + "permission": {"up": True, "down": False}, "non-merge": ["1", "2"], "other": 1234, } @@ -127,8 +126,7 @@ def test_merged_keys(): config, { "requires": ["third", "fourth"], - "permissions": {"left": True, "right": False}, - "device_requires": {"west": True, "east": False}, + "permission": {"left": True, "right": False}, "sources": ["c", "d"], "non-merge": ["3", "4"], }, @@ -137,8 +135,7 @@ def test_merged_keys(): assert config == { "requires": ["first", "second", "third", "fourth"], "sources": ["a", "b", "c", "d"], - "permissions": {"up": True, "down": False, "left": True, "right": False}, - "device_requires": {"north": True, "south": False, "west": True, "east": False}, + "permission": {"up": True, "down": False, "left": True, "right": False}, "non-merge": ["3", "4"], "other": 1234, } From a68ae05697e78ac91bc40c5c469d4b5bf45b46f8 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Fri, 12 Jan 2024 10:27:15 +0800 Subject: [PATCH 08/16] Correct XML extension test naming. --- tests/integrations/cookiecutter/test_XMLExtension.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integrations/cookiecutter/test_XMLExtension.py b/tests/integrations/cookiecutter/test_XMLExtension.py index 9fc41fe07..54b256a44 100644 --- a/tests/integrations/cookiecutter/test_XMLExtension.py +++ b/tests/integrations/cookiecutter/test_XMLExtension.py @@ -19,7 +19,7 @@ ("", "false"), ], ) -def bool_attr(value, expected): +def test_bool_attr(value, expected): env = MagicMock() env.filters = {} XMLExtension(env) From 237ab1b9612c35b86d5806855f916c250684610c Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Fri, 12 Jan 2024 10:27:52 +0800 Subject: [PATCH 09/16] Add iOS permission handling. --- src/briefcase/platforms/iOS/xcode.py | 45 ++++++ tests/platforms/iOS/xcode/test_create.py | 168 +++++++++++++++++++++++ 2 files changed, 213 insertions(+) diff --git a/src/briefcase/platforms/iOS/xcode.py b/src/briefcase/platforms/iOS/xcode.py index cb0a90cc1..fcb7b4e57 100644 --- a/src/briefcase/platforms/iOS/xcode.py +++ b/src/briefcase/platforms/iOS/xcode.py @@ -254,6 +254,51 @@ def select_target_device(self, udid_or_device=None): class iOSXcodeCreateCommand(iOSXcodePassiveMixin, CreateCommand): description = "Create and populate a iOS Xcode project." + def permissions_context(self, app: AppConfig, x_permissions: dict[str, str]): + """Additional template context for permissions. + + :param app: The config object for the app + :param x_permissions: The dictionary of known cross-platform permission + definitions. + :returns: The template context describing permissions for the app. + """ + permissions = {} + + if x_permissions["camera"]: + permissions["NSCameraUsageDescription"] = x_permissions["camera"] + if x_permissions["microphone"]: + permissions["NSMicrophoneUsageDescription"] = x_permissions["microphone"] + + if x_permissions["fine_location"]: + permissions["NSLocationDefaultAccuracyReduced"] = False + elif x_permissions["coarse_location"]: + permissions["NSLocationDefaultAccuracyReduced"] = True + + if x_permissions["background_location"]: + permissions["NSLocationAlwaysAndWhenInUseUsageDescription"] = x_permissions[ + "background_location" + ] + elif x_permissions["fine_location"]: + permissions["NSLocationWhenInUseUsageDescription"] = x_permissions[ + "fine_location" + ] + elif x_permissions["coarse_location"]: + permissions["NSLocationWhenInUseUsageDescription"] = x_permissions[ + "coarse_location" + ] + + if x_permissions["photo_library"]: + permissions["NSPhotoLibraryAddUsageDescription"] = x_permissions[ + "photo_library" + ] + + # Override any permission definitions with the platform specific definitions + permissions.update(app.permission) + + return { + "permissions": permissions, + } + def _extra_pip_args(self, app: AppConfig): """Any additional arguments that must be passed to pip when installing packages. diff --git a/tests/platforms/iOS/xcode/test_create.py b/tests/platforms/iOS/xcode/test_create.py index 7572530f6..f0ac1ad0b 100644 --- a/tests/platforms/iOS/xcode/test_create.py +++ b/tests/platforms/iOS/xcode/test_create.py @@ -120,3 +120,171 @@ def test_extra_pip_args(create_command, first_app_generated, tmp_path): }, ), ] + + +@pytest.mark.parametrize( + "permissions, context", + [ + # No permissions + ( + {}, + {"permissions": {}}, + ), + # Only custom permissions + ( + { + "NSCameraUsageDescription": "I need to see you", + "NSMicrophoneUsageDescription": "I need to hear you", + }, + { + "permissions": { + "NSCameraUsageDescription": "I need to see you", + "NSMicrophoneUsageDescription": "I need to hear you", + } + }, + ), + # Camera permissions + ( + { + "camera": "I need to see you", + }, + { + "permissions": { + "NSCameraUsageDescription": "I need to see you", + }, + }, + ), + # Microphone permissions + ( + { + "microphone": "I need to hear you", + }, + { + "permissions": { + "NSMicrophoneUsageDescription": "I need to hear you", + }, + }, + ), + # Coarse location permissions + ( + { + "coarse_location": "I need to know roughly where you are", + }, + { + "permissions": { + "NSLocationDefaultAccuracyReduced": True, + "NSLocationWhenInUseUsageDescription": "I need to know roughly where you are", + } + }, + ), + # Fine location permissions + ( + { + "fine_location": "I need to know exactly where you are", + }, + { + "permissions": { + "NSLocationDefaultAccuracyReduced": False, + "NSLocationWhenInUseUsageDescription": "I need to know exactly where you are", + } + }, + ), + # Background location permissions + ( + { + "background_location": "I always need to know where you are", + }, + { + "permissions": { + "NSLocationAlwaysAndWhenInUseUsageDescription": "I always need to know where you are", + } + }, + ), + # Coarse location background permissions + ( + { + "coarse_location": "I need to know roughly where you are", + "background_location": "I always need to know where you are", + }, + { + "permissions": { + "NSLocationDefaultAccuracyReduced": True, + "NSLocationAlwaysAndWhenInUseUsageDescription": "I always need to know where you are", + } + }, + ), + # Fine location background permissions + ( + { + "fine_location": "I need to know exactly where you are", + "background_location": "I always need to know where you are", + }, + { + "permissions": { + "NSLocationDefaultAccuracyReduced": False, + "NSLocationAlwaysAndWhenInUseUsageDescription": "I always need to know where you are", + } + }, + ), + # Coarse and fine location permissions + ( + { + "coarse_location": "I need to know roughly where you are", + "fine_location": "I need to know exactly where you are", + }, + { + "permissions": { + "NSLocationDefaultAccuracyReduced": False, + "NSLocationWhenInUseUsageDescription": "I need to know exactly where you are", + } + }, + ), + # Coarse and fine background location permissions + ( + { + "coarse_location": "I need to know roughly where you are", + "fine_location": "I need to know exactly where you are", + "background_location": "I always need to know where you are", + }, + { + "permissions": { + "NSLocationDefaultAccuracyReduced": False, + "NSLocationAlwaysAndWhenInUseUsageDescription": "I always need to know where you are", + } + }, + ), + # Photo library permissions + ( + { + "photo_library": "I need to see your library", + }, + { + "permissions": { + "NSPhotoLibraryAddUsageDescription": "I need to see your library" + } + }, + ), + # Override and augment by cross-platform definitions + ( + { + "camera": "I need to see you", + "NSCameraUsageDescription": "Platform specific", + "NSCustomPermission": "Custom message", + }, + { + "permissions": { + "NSCameraUsageDescription": "Platform specific", + "NSCustomPermission": "Custom message", + } + }, + ), + ], +) +def test_permissions_context(create_command, first_app, permissions, context): + """Platform-specific permissions can be added to the context.""" + # Set the permissions value + first_app.permission = permissions + # Extract the cross-platform permissions + x_permissions = create_command._x_permissions(first_app) + # Check that the final platform permissions are rendered as expected. + assert context == create_command.permissions_context(first_app, x_permissions) From c9a7b0edebe7ae6a8bbc1be5b7a4eb8f366000d3 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Fri, 12 Jan 2024 10:28:34 +0800 Subject: [PATCH 10/16] Add macOS permission handling. --- src/briefcase/platforms/macOS/__init__.py | 51 ++++- src/briefcase/platforms/macOS/app.py | 4 +- src/briefcase/platforms/macOS/xcode.py | 4 +- tests/platforms/macOS/app/test_create.py | 243 ++++++++++++++++++++++ tests/platforms/macOS/conftest.py | 4 +- 5 files changed, 299 insertions(+), 7 deletions(-) diff --git a/src/briefcase/platforms/macOS/__init__.py b/src/briefcase/platforms/macOS/__init__.py index fc468bed2..d9b4e443a 100644 --- a/src/briefcase/platforms/macOS/__init__.py +++ b/src/briefcase/platforms/macOS/__init__.py @@ -39,7 +39,9 @@ class macOSMixin: supported_host_os_reason = "macOS applications can only be built on macOS." -class macOSInstallMixin(AppPackagesMergeMixin): +class macOSCreateMixin(AppPackagesMergeMixin): + hidden_app_properties = {"permission", "entitlement"} + def _install_app_requirements( self, app: AppConfig, @@ -147,6 +149,53 @@ def _install_app_requirements( # libraries down to just the host architecture. self.thin_app_packages(app_packages_path, arch=self.tools.host_arch) + def permissions_context(self, app: AppConfig, cross_platform: dict[str, str]): + """Additional template context for permissions. + + :param app: The config object for the app + :param cross_platform: The dictionary of known cross-platform permission + definitions. + :returns: The template context describing permissions for the app. + """ + permissions = {} + + # Default entitlements for all macOS apps + entitlements = { + "com.apple.security.cs.allow-unsigned-executable-memory": True, + "com.apple.security.cs.disable-library-validation": True, + } + + if cross_platform["camera"]: + entitlements["com.apple.security.device.camera"] = True + if cross_platform["microphone"]: + entitlements["com.apple.security.device.microphone"] = True + + if cross_platform["background_location"]: + permissions["NSLocationUsageDescription"] = cross_platform[ + "background_location" + ] + entitlements["com.apple.security.personal-information.location"] = True + elif cross_platform["fine_location"]: + permissions["NSLocationUsageDescription"] = cross_platform["fine_location"] + entitlements["com.apple.security.personal-information.location"] = True + elif cross_platform["coarse_location"]: + permissions["NSLocationUsageDescription"] = cross_platform[ + "coarse_location" + ] + entitlements["com.apple.security.personal-information.location"] = True + + if cross_platform["photo_library"]: + entitlements["com.apple.security.personal-information.photo_library"] = True + + # Override any permission and entitlement definitions with the platform specific definitions + permissions.update(app.permission) + entitlements.update(getattr(app, "entitlement", {})) + + return { + "permissions": permissions, + "entitlements": entitlements, + } + class macOSRunMixin: def run_app( diff --git a/src/briefcase/platforms/macOS/app.py b/src/briefcase/platforms/macOS/app.py index 61e5932b7..34c5b504f 100644 --- a/src/briefcase/platforms/macOS/app.py +++ b/src/briefcase/platforms/macOS/app.py @@ -13,7 +13,7 @@ ) from briefcase.config import AppConfig from briefcase.platforms.macOS import ( - macOSInstallMixin, + macOSCreateMixin, macOSMixin, macOSPackageMixin, macOSRunMixin, @@ -31,7 +31,7 @@ def binary_path(self, app): return self.bundle_path(app) / f"{app.formal_name}.app" -class macOSAppCreateCommand(macOSAppMixin, macOSInstallMixin, CreateCommand): +class macOSAppCreateCommand(macOSAppMixin, macOSCreateMixin, CreateCommand): description = "Create and populate a macOS app." def support_path(self, app: AppConfig, runtime=False) -> Path: diff --git a/src/briefcase/platforms/macOS/xcode.py b/src/briefcase/platforms/macOS/xcode.py index 31f77d4ab..78ed6dee5 100644 --- a/src/briefcase/platforms/macOS/xcode.py +++ b/src/briefcase/platforms/macOS/xcode.py @@ -13,7 +13,7 @@ from briefcase.exceptions import BriefcaseCommandError from briefcase.integrations.xcode import Xcode from briefcase.platforms.macOS import ( - macOSInstallMixin, + macOSCreateMixin, macOSMixin, macOSPackageMixin, macOSRunMixin, @@ -42,7 +42,7 @@ def binary_path(self, app): return self.bundle_path(app) / "build/Release" / f"{app.formal_name}.app" -class macOSXcodeCreateCommand(macOSXcodeMixin, macOSInstallMixin, CreateCommand): +class macOSXcodeCreateCommand(macOSXcodeMixin, macOSCreateMixin, CreateCommand): description = "Create and populate a macOS Xcode project." diff --git a/tests/platforms/macOS/app/test_create.py b/tests/platforms/macOS/app/test_create.py index dc5e594d2..506bc8951 100644 --- a/tests/platforms/macOS/app/test_create.py +++ b/tests/platforms/macOS/app/test_create.py @@ -27,6 +27,249 @@ def create_command(tmp_path, first_app_templated): return command +@pytest.mark.parametrize( + "permissions, entitlements, context", + [ + # No permissions + ( + {}, + {}, + { + "permissions": {}, + "entitlements": { + "com.apple.security.cs.allow-unsigned-executable-memory": True, + "com.apple.security.cs.disable-library-validation": True, + }, + }, + ), + # Only custom permissions + ( + { + "NSCustomPermission": "Custom message", + }, + { + "com.apple.vm.networking": True, + }, + { + "permissions": { + "NSCustomPermission": "Custom message", + }, + "entitlements": { + "com.apple.security.cs.allow-unsigned-executable-memory": True, + "com.apple.security.cs.disable-library-validation": True, + "com.apple.vm.networking": True, + }, + }, + ), + # Camera permissions + ( + { + "camera": "I need to see you", + }, + {}, + { + "permissions": {}, + "entitlements": { + "com.apple.security.cs.allow-unsigned-executable-memory": True, + "com.apple.security.cs.disable-library-validation": True, + "com.apple.security.device.camera": True, + }, + }, + ), + # Microphone permissions + ( + { + "microphone": "I need to hear you", + }, + {}, + { + "permissions": {}, + "entitlements": { + "com.apple.security.cs.allow-unsigned-executable-memory": True, + "com.apple.security.cs.disable-library-validation": True, + "com.apple.security.device.microphone": True, + }, + }, + ), + # Coarse location permissions + ( + { + "coarse_location": "I need to know roughly where you are", + }, + {}, + { + "permissions": { + "NSLocationUsageDescription": "I need to know roughly where you are", + }, + "entitlements": { + "com.apple.security.cs.allow-unsigned-executable-memory": True, + "com.apple.security.cs.disable-library-validation": True, + "com.apple.security.personal-information.location": True, + }, + }, + ), + # Fine location permissions + ( + { + "fine_location": "I need to know exactly where you are", + }, + {}, + { + "permissions": { + "NSLocationUsageDescription": "I need to know exactly where you are", + }, + "entitlements": { + "com.apple.security.cs.allow-unsigned-executable-memory": True, + "com.apple.security.cs.disable-library-validation": True, + "com.apple.security.personal-information.location": True, + }, + }, + ), + # Background location permissions + ( + { + "background_location": "I always need to know where you are", + }, + {}, + { + "permissions": { + "NSLocationUsageDescription": "I always need to know where you are", + }, + "entitlements": { + "com.apple.security.cs.allow-unsigned-executable-memory": True, + "com.apple.security.cs.disable-library-validation": True, + "com.apple.security.personal-information.location": True, + }, + }, + ), + # Coarse location background permissions + ( + { + "coarse_location": "I need to know roughly where you are", + "background_location": "I always need to know where you are", + }, + {}, + { + "permissions": { + "NSLocationUsageDescription": "I always need to know where you are", + }, + "entitlements": { + "com.apple.security.cs.allow-unsigned-executable-memory": True, + "com.apple.security.cs.disable-library-validation": True, + "com.apple.security.personal-information.location": True, + }, + }, + ), + # Fine location background permissions + ( + { + "fine_location": "I need to know exactly where you are", + "background_location": "I always need to know where you are", + }, + {}, + { + "permissions": { + "NSLocationUsageDescription": "I always need to know where you are", + }, + "entitlements": { + "com.apple.security.cs.allow-unsigned-executable-memory": True, + "com.apple.security.cs.disable-library-validation": True, + "com.apple.security.personal-information.location": True, + }, + }, + ), + # Coarse and fine location permissions + ( + { + "coarse_location": "I need to know roughly where you are", + "fine_location": "I need to know exactly where you are", + }, + {}, + { + "permissions": { + "NSLocationUsageDescription": "I need to know exactly where you are", + }, + "entitlements": { + "com.apple.security.cs.allow-unsigned-executable-memory": True, + "com.apple.security.cs.disable-library-validation": True, + "com.apple.security.personal-information.location": True, + }, + }, + ), + # Coarse and fine background location permissions + ( + { + "coarse_location": "I need to know roughly where you are", + "fine_location": "I need to know exactly where you are", + "background_location": "I always need to know where you are", + }, + {}, + { + "permissions": { + "NSLocationUsageDescription": "I always need to know where you are", + }, + "entitlements": { + "com.apple.security.cs.allow-unsigned-executable-memory": True, + "com.apple.security.cs.disable-library-validation": True, + "com.apple.security.personal-information.location": True, + }, + }, + ), + # Photo library permissions + ( + { + "photo_library": "I need to see your library", + }, + {}, + { + "permissions": {}, + "entitlements": { + "com.apple.security.cs.allow-unsigned-executable-memory": True, + "com.apple.security.cs.disable-library-validation": True, + "com.apple.security.personal-information.photo_library": True, + }, + }, + ), + # Override and augment by cross-platform definitions + ( + { + "fine_location": "I need to know where you are", + "NSCustomMessage": "Custom message", + "NSLocationUsageDescription": "Platform specific", + }, + { + "com.apple.security.personal-information.location": False, + "com.apple.security.cs.disable-library-validation": False, + "com.apple.vm.networking": True, + }, + { + "permissions": { + "NSLocationUsageDescription": "Platform specific", + "NSCustomMessage": "Custom message", + }, + "entitlements": { + "com.apple.security.cs.allow-unsigned-executable-memory": True, + "com.apple.security.cs.disable-library-validation": False, + "com.apple.security.personal-information.location": False, + "com.apple.vm.networking": True, + }, + }, + ), + ], +) +def test_permissions_context( + create_command, first_app, permissions, entitlements, context +): + """Platform-specific permissions can be added to the context.""" + # Set the permission and entitlement value + first_app.permission = permissions + first_app.entitlement = entitlements + # Extract the cross-platform permissions + x_permissions = create_command._x_permissions(first_app) + # Check that the final platform permissions are rendered as expected. + assert context == create_command.permissions_context(first_app, x_permissions) + + @pytest.mark.parametrize( "host_arch, other_arch", [ diff --git a/tests/platforms/macOS/conftest.py b/tests/platforms/macOS/conftest.py index 00c7932a6..0c58839a0 100644 --- a/tests/platforms/macOS/conftest.py +++ b/tests/platforms/macOS/conftest.py @@ -5,11 +5,11 @@ from briefcase.commands.base import BaseCommand from briefcase.console import Console, Log from briefcase.integrations.subprocess import Subprocess -from briefcase.platforms.macOS.app import macOSAppMixin, macOSInstallMixin +from briefcase.platforms.macOS.app import macOSAppMixin, macOSCreateMixin from tests.utils import DummyConsole -class DummyInstallCommand(macOSAppMixin, macOSInstallMixin, BaseCommand): +class DummyInstallCommand(macOSAppMixin, macOSCreateMixin, BaseCommand): """A dummy command to expose package installation capabilities.""" command = "install" From 022f28e6ae3e3c9f843465a5e5eedc42921fbae1 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Fri, 12 Jan 2024 10:29:08 +0800 Subject: [PATCH 11/16] Add Android permission handling. --- src/briefcase/platforms/android/gradle.py | 70 ++++- tests/platforms/android/gradle/test_create.py | 262 ++++++++++++++++++ 2 files changed, 325 insertions(+), 7 deletions(-) diff --git a/src/briefcase/platforms/android/gradle.py b/src/briefcase/platforms/android/gradle.py index 3c0a38795..2cf904448 100644 --- a/src/briefcase/platforms/android/gradle.py +++ b/src/briefcase/platforms/android/gradle.py @@ -15,7 +15,7 @@ RunCommand, UpdateCommand, ) -from briefcase.config import BaseConfig, parsed_version +from briefcase.config import AppConfig, parsed_version from briefcase.exceptions import BriefcaseCommandError from briefcase.integrations.android_sdk import AndroidSDK from briefcase.integrations.subprocess import SubprocessArgT @@ -145,6 +145,7 @@ def verify_tools(self): class GradleCreateCommand(GradleMixin, CreateCommand): description = "Create and populate an Android Gradle project." + hidden_app_properties = {"permission", "feature"} def support_package_filename(self, support_revision): """The query arguments to use in a support package query request.""" @@ -152,7 +153,7 @@ def support_package_filename(self, support_revision): f"Python-{self.python_version_tag}-Android-support.b{support_revision}.zip" ) - def output_format_template_context(self, app: BaseConfig): + def output_format_template_context(self, app: AppConfig): """Additional template context required by the output format. :param app: The config object for the app @@ -181,6 +182,61 @@ def output_format_template_context(self, app: BaseConfig): ), } + def permissions_context(self, app: AppConfig, x_permissions: dict[str, str]): + """Additional template context for permissions. + + :param app: The config object for the app + :param x_permissions: The dictionary of known cross-platform permission + definitions. + :returns: The template context describing permissions for the app. + """ + # Default permissions for all Android apps + permissions = { + "android.permission.INTERNET": True, + "android.permission.ACCESS_NETWORK_STATE": True, + } + + # Default feature usage for all Android apps + features = {} + + if x_permissions["camera"]: + permissions["android.permission.CAMERA"] = True + features["android.hardware.camera"] = False + features["android.hardware.camera.any"] = False + features["android.hardware.camera.front"] = False + features["android.hardware.camera.external"] = False + features["android.hardware.camera.autofocus"] = False + + if x_permissions["microphone"]: + permissions["android.permission.RECORD_AUDIO"] = True + + if x_permissions["fine_location"]: + permissions["android.permission.ACCESS_FINE_LOCATION"] = True + features["android.hardware.location.network"] = False + features["android.hardware.location.gps"] = False + + if x_permissions["coarse_location"]: + permissions["android.permission.ACCESS_COARSE_LOCATION"] = True + features["android.hardware.location.network"] = False + features["android.hardware.location.gps"] = False + + if x_permissions["background_location"]: + permissions["android.permission.ACCESS_BACKGROUND_LOCATION"] = True + features["android.hardware.location.network"] = False + features["android.hardware.location.gps"] = False + + if x_permissions["photo_library"]: + permissions["android.permission.READ_MEDIA_VISUAL_USER_SELECTED"] = True + + # Override any permission and entitlement definitions with the platform specific definitions + permissions.update(app.permission) + features.update(getattr(app, "feature", {})) + + return { + "permissions": permissions, + "features": features, + } + class GradleUpdateCommand(GradleCreateCommand, UpdateCommand): description = "Update an existing Android Gradle project." @@ -193,10 +249,10 @@ class GradleOpenCommand(GradleMixin, OpenCommand): class GradleBuildCommand(GradleMixin, BuildCommand): description = "Build an Android debug APK." - def metadata_resource_path(self, app: BaseConfig): + def metadata_resource_path(self, app: AppConfig): return self.bundle_path(app) / self.path_index(app, "metadata_resource_path") - def update_app_metadata(self, app: BaseConfig, test_mode: bool): + def update_app_metadata(self, app: AppConfig, test_mode: bool): with self.input.wait_bar("Setting main module..."): with self.metadata_resource_path(app).open("w", encoding="utf-8") as f: # Set the name of the app's main module; this will depend @@ -209,7 +265,7 @@ def update_app_metadata(self, app: BaseConfig, test_mode: bool): """ ) - def build_app(self, app: BaseConfig, test_mode: bool, **kwargs): + def build_app(self, app: AppConfig, test_mode: bool, **kwargs): """Build an application. :param app: The application to build @@ -261,7 +317,7 @@ def add_options(self, parser): def run_app( self, - app: BaseConfig, + app: AppConfig, test_mode: bool, passthrough: list[str], device_or_avd=None, @@ -380,7 +436,7 @@ def run_app( class GradlePackageCommand(GradleMixin, PackageCommand): description = "Create an Android App Bundle and APK in release mode." - def package_app(self, app: BaseConfig, **kwargs): + def package_app(self, app: AppConfig, **kwargs): """Package the app for distribution. This involves building the release app bundle. diff --git a/tests/platforms/android/gradle/test_create.py b/tests/platforms/android/gradle/test_create.py index 2a5927e73..7441fecc9 100644 --- a/tests/platforms/android/gradle/test_create.py +++ b/tests/platforms/android/gradle/test_create.py @@ -145,3 +145,265 @@ def test_extract_packages(create_command, first_app_config, test_sources, expect first_app_config.test_sources = test_sources context = create_command.output_format_template_context(first_app_config) assert context["extract_packages"] == expected + + +@pytest.mark.parametrize( + "permissions, features, context", + [ + # No permissions + ( + {}, + {}, + { + "permissions": { + "android.permission.ACCESS_NETWORK_STATE": True, + "android.permission.INTERNET": True, + }, + "features": {}, + }, + ), + # Only custom permissions + ( + { + "android.permission.READ_CONTACTS": True, + }, + { + "android.hardware.bluetooth": True, + }, + { + "permissions": { + "android.permission.ACCESS_NETWORK_STATE": True, + "android.permission.INTERNET": True, + "android.permission.READ_CONTACTS": True, + }, + "features": { + "android.hardware.bluetooth": True, + }, + }, + ), + # Camera permissions + ( + { + "camera": "I need to see you", + }, + {}, + { + "permissions": { + "android.permission.ACCESS_NETWORK_STATE": True, + "android.permission.INTERNET": True, + "android.permission.CAMERA": True, + }, + "features": { + "android.hardware.camera": False, + "android.hardware.camera.any": False, + "android.hardware.camera.autofocus": False, + "android.hardware.camera.external": False, + "android.hardware.camera.front": False, + }, + }, + ), + # Microphone permissions + ( + { + "microphone": "I need to hear you", + }, + {}, + { + "permissions": { + "android.permission.ACCESS_NETWORK_STATE": True, + "android.permission.INTERNET": True, + "android.permission.RECORD_AUDIO": True, + }, + "features": {}, + }, + ), + # Coarse location permissions + ( + { + "coarse_location": "I need to know roughly where you are", + }, + {}, + { + "permissions": { + "android.permission.ACCESS_NETWORK_STATE": True, + "android.permission.INTERNET": True, + "android.permission.ACCESS_COARSE_LOCATION": True, + }, + "features": { + "android.hardware.location.gps": False, + "android.hardware.location.network": False, + }, + }, + ), + # Fine location permissions + ( + { + "fine_location": "I need to know exactly where you are", + }, + {}, + { + "permissions": { + "android.permission.ACCESS_NETWORK_STATE": True, + "android.permission.INTERNET": True, + "android.permission.ACCESS_FINE_LOCATION": True, + }, + "features": { + "android.hardware.location.gps": False, + "android.hardware.location.network": False, + }, + }, + ), + # Background location permissions + ( + { + "background_location": "I always need to know where you are", + }, + {}, + { + "permissions": { + "android.permission.ACCESS_NETWORK_STATE": True, + "android.permission.INTERNET": True, + "android.permission.ACCESS_BACKGROUND_LOCATION": True, + }, + "features": { + "android.hardware.location.gps": False, + "android.hardware.location.network": False, + }, + }, + ), + # Coarse location background permissions + ( + { + "coarse_location": "I need to know roughly where you are", + "background_location": "I always need to know where you are", + }, + {}, + { + "permissions": { + "android.permission.ACCESS_NETWORK_STATE": True, + "android.permission.INTERNET": True, + "android.permission.ACCESS_COARSE_LOCATION": True, + "android.permission.ACCESS_BACKGROUND_LOCATION": True, + }, + "features": { + "android.hardware.location.gps": False, + "android.hardware.location.network": False, + }, + }, + ), + # Fine location background permissions + ( + { + "fine_location": "I need to know exactly where you are", + "background_location": "I always need to know where you are", + }, + {}, + { + "permissions": { + "android.permission.ACCESS_NETWORK_STATE": True, + "android.permission.INTERNET": True, + "android.permission.ACCESS_FINE_LOCATION": True, + "android.permission.ACCESS_BACKGROUND_LOCATION": True, + }, + "features": { + "android.hardware.location.gps": False, + "android.hardware.location.network": False, + }, + }, + ), + # Coarse and fine location permissions + ( + { + "coarse_location": "I need to know roughly where you are", + "fine_location": "I need to know exactly where you are", + }, + {}, + { + "permissions": { + "android.permission.ACCESS_NETWORK_STATE": True, + "android.permission.INTERNET": True, + "android.permission.ACCESS_COARSE_LOCATION": True, + "android.permission.ACCESS_FINE_LOCATION": True, + }, + "features": { + "android.hardware.location.gps": False, + "android.hardware.location.network": False, + }, + }, + ), + # Coarse and fine background location permissions + ( + { + "coarse_location": "I need to know roughly where you are", + "fine_location": "I need to know exactly where you are", + "background_location": "I always need to know where you are", + }, + {}, + { + "permissions": { + "android.permission.ACCESS_NETWORK_STATE": True, + "android.permission.INTERNET": True, + "android.permission.ACCESS_COARSE_LOCATION": True, + "android.permission.ACCESS_FINE_LOCATION": True, + "android.permission.ACCESS_BACKGROUND_LOCATION": True, + }, + "features": { + "android.hardware.location.gps": False, + "android.hardware.location.network": False, + }, + }, + ), + # Photo library permissions + ( + { + "photo_library": "I need to see your library", + }, + {}, + { + "permissions": { + "android.permission.ACCESS_NETWORK_STATE": True, + "android.permission.INTERNET": True, + "android.permission.READ_MEDIA_VISUAL_USER_SELECTED": True, + }, + "features": {}, + }, + ), + # Override and augment by cross-platform definitions + ( + { + "camera": "I need to see you", + "android.permission.CAMERA": False, + "android.permission.READ_CONTACTS": True, + }, + { + "android.hardware.camera.external": True, + "android.hardware.bluetooth": True, + }, + { + "permissions": { + "android.permission.ACCESS_NETWORK_STATE": True, + "android.permission.INTERNET": True, + "android.permission.CAMERA": False, + "android.permission.READ_CONTACTS": True, + }, + "features": { + "android.hardware.camera": False, + "android.hardware.camera.any": False, + "android.hardware.camera.autofocus": False, + "android.hardware.camera.external": True, + "android.hardware.camera.front": False, + "android.hardware.bluetooth": True, + }, + }, + ), + ], +) +def test_permissions_context(create_command, first_app, permissions, features, context): + """Platform-specific permissions can be added to the context.""" + # Set the permission and entitlement value + first_app.permission = permissions + first_app.feature = features + # Extract the cross-platform permissions + x_permissions = create_command._x_permissions(first_app) + # Check that the final platform permissions are rendered as expected. + assert context == create_command.permissions_context(first_app, x_permissions) From 690ac4f308046b1634df2b858ffa30f8d573ec5c Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Fri, 12 Jan 2024 10:29:33 +0800 Subject: [PATCH 12/16] Add Flatpak permission handling. --- src/briefcase/platforms/linux/flatpak.py | 37 ++++ tests/platforms/linux/flatpak/test_create.py | 198 +++++++++++++++++++ 2 files changed, 235 insertions(+) diff --git a/src/briefcase/platforms/linux/flatpak.py b/src/briefcase/platforms/linux/flatpak.py index 9a124d5e6..8af298596 100644 --- a/src/briefcase/platforms/linux/flatpak.py +++ b/src/briefcase/platforms/linux/flatpak.py @@ -102,6 +102,7 @@ def flatpak_sdk(self, app): class LinuxFlatpakCreateCommand(LinuxFlatpakMixin, CreateCommand): description = "Create and populate a Linux Flatpak." + hidden_app_properties = {"permission", "finish_arg"} def output_format_template_context(self, app: AppConfig): """Add flatpak runtime/SDK details to the app template.""" @@ -111,6 +112,42 @@ def output_format_template_context(self, app: AppConfig): "flatpak_sdk": self.flatpak_sdk(app), } + def permissions_context(self, app: AppConfig, x_permissions: dict[str, str]): + """Additional template context for permissions. + + :param app: The config object for the app + :param x_permissions: The dictionary of known cross-platform permission + definitions. + :returns: The template context describing permissions for the app. + """ + # The default finish arguments that Briefcase adds on every Flatpak. + finish_args = { + # X11 + XShm access + "share=ipc": True, + "socket=x11": True, + # Disable Wayland access + "nosocket=wayland": True, + # Network access + "share=network": True, + # GPU access + "device=dri": True, + # Sound access + "socket=pulseaudio": True, + # Host filesystem access + "filesystem=xdg-cache": True, + "filesystem=xdg-config": True, + "filesystem=xdg-data": True, + "filesystem=xdg-documents": True, + # DBus access + "socket=session-bus": True, + } + + finish_args.update(getattr(app, "finish_arg", {})) + + return { + "finish_args": finish_args, + } + class LinuxFlatpakUpdateCommand(LinuxFlatpakCreateCommand, UpdateCommand): description = "Update an existing Linux Flatpak." diff --git a/tests/platforms/linux/flatpak/test_create.py b/tests/platforms/linux/flatpak/test_create.py index 0f977a8d3..b2d748df8 100644 --- a/tests/platforms/linux/flatpak/test_create.py +++ b/tests/platforms/linux/flatpak/test_create.py @@ -43,6 +43,204 @@ def test_output_format_template_context(create_command, first_app_config): } +DEFAULT_FINISH_ARGS = { + "share=ipc": True, + "socket=x11": True, + "nosocket=wayland": True, + "share=network": True, + "device=dri": True, + "socket=pulseaudio": True, + "filesystem=xdg-cache": True, + "filesystem=xdg-config": True, + "filesystem=xdg-data": True, + "filesystem=xdg-documents": True, + "socket=session-bus": True, +} + + +@pytest.mark.parametrize( + "permissions, finish_args, context", + [ + # No permissions + ( + {}, + {}, + { + "finish_args": DEFAULT_FINISH_ARGS, + }, + ), + # Only custom permissions + ( + { + "custom_permission": "Custom message", + }, + { + "allow=bluetooth": True, + }, + { + "finish_args": { + "share=ipc": True, + "socket=x11": True, + "nosocket=wayland": True, + "share=network": True, + "device=dri": True, + "socket=pulseaudio": True, + "filesystem=xdg-cache": True, + "filesystem=xdg-config": True, + "filesystem=xdg-data": True, + "filesystem=xdg-documents": True, + "socket=session-bus": True, + "allow=bluetooth": True, + }, + }, + ), + # Camera permissions + ( + { + "camera": "I need to see you", + }, + {}, + { + "finish_args": DEFAULT_FINISH_ARGS, + }, + ), + # Microphone permissions + ( + { + "microphone": "I need to hear you", + }, + {}, + { + "finish_args": DEFAULT_FINISH_ARGS, + }, + ), + # Coarse location permissions + ( + { + "coarse_location": "I need to know roughly where you are", + }, + {}, + { + "finish_args": DEFAULT_FINISH_ARGS, + }, + ), + # Fine location permissions + ( + { + "fine_location": "I need to know exactly where you are", + }, + {}, + { + "finish_args": DEFAULT_FINISH_ARGS, + }, + ), + # Background location permissions + ( + { + "background_location": "I always need to know where you are", + }, + {}, + { + "finish_args": DEFAULT_FINISH_ARGS, + }, + ), + # Coarse location background permissions + ( + { + "coarse_location": "I need to know roughly where you are", + "background_location": "I always need to know where you are", + }, + {}, + { + "finish_args": DEFAULT_FINISH_ARGS, + }, + ), + # Fine location background permissions + ( + { + "fine_location": "I need to know exactly where you are", + "background_location": "I always need to know where you are", + }, + {}, + { + "finish_args": DEFAULT_FINISH_ARGS, + }, + ), + # Coarse and fine location permissions + ( + { + "coarse_location": "I need to know roughly where you are", + "fine_location": "I need to know exactly where you are", + }, + {}, + { + "finish_args": DEFAULT_FINISH_ARGS, + }, + ), + # Coarse and fine background location permissions + ( + { + "coarse_location": "I need to know roughly where you are", + "fine_location": "I need to know exactly where you are", + "background_location": "I always need to know where you are", + }, + {}, + { + "finish_args": DEFAULT_FINISH_ARGS, + }, + ), + # Photo library permissions + ( + { + "photo_library": "I need to see your library", + }, + {}, + { + "finish_args": DEFAULT_FINISH_ARGS, + }, + ), + # Override and augment by cross-platform definitions + ( + { + "fine_location": "I need to know where you are", + "NSCustomMessage": "Custom message", + }, + { + "socket=pulseaudio": False, + "allow=bluetooth": True, + }, + { + "finish_args": { + "share=ipc": True, + "socket=x11": True, + "nosocket=wayland": True, + "share=network": True, + "device=dri": True, + "socket=pulseaudio": False, + "filesystem=xdg-cache": True, + "filesystem=xdg-config": True, + "filesystem=xdg-data": True, + "filesystem=xdg-documents": True, + "socket=session-bus": True, + "allow=bluetooth": True, + }, + }, + ), + ], +) +def test_permissions_context( + create_command, first_app, permissions, finish_args, context +): + """Platform-specific permissions can be added to the context.""" + # Set the permission and entitlement value + first_app.permission = permissions + first_app.finish_arg = finish_args + # Extract the cross-platform permissions + x_permissions = create_command._x_permissions(first_app) + # Check that the final platform permissions are rendered as expected. + assert context == create_command.permissions_context(first_app, x_permissions) + + def test_missing_runtime_config(create_command, first_app_config): """The app creation errors is a Flatpak runtime is not defined.""" create_command.tools.flatpak = MagicMock(spec_set=Flatpak) From 84a2207a62396c3fb4f96f5900817a8e7bd95053 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Fri, 12 Jan 2024 11:16:02 +0800 Subject: [PATCH 13/16] Document the flatpak module extension point. --- docs/reference/platforms/linux/flatpak.rst | 7 +++++++ src/briefcase/platforms/linux/flatpak.py | 4 ++-- src/briefcase/platforms/macOS/filters.py | 2 ++ src/briefcase/platforms/macOS/xcode.py | 2 ++ tests/commands/create/conftest.py | 2 ++ 5 files changed, 15 insertions(+), 2 deletions(-) diff --git a/docs/reference/platforms/linux/flatpak.rst b/docs/reference/platforms/linux/flatpak.rst index 7db89f935..09d30c6fb 100644 --- a/docs/reference/platforms/linux/flatpak.rst +++ b/docs/reference/platforms/linux/flatpak.rst @@ -144,6 +144,13 @@ build the Flatpak app. The Flatpak runtime and SDK are paired; so, both a ``flatpak_runtime`` and a corresponding ``flatpak_sdk`` must be defined. +``modules_extra_content`` +~~~~~~~~~~~~~~~~~~~~~~~~~ + +Additional build instructions that will be inserted into the Flatpak manifest, *after* +Python has been installed and ``pip`` is guaranteed to exist, but *before* any app code +or app packages have been installed into the Flatpak. + Permissions =========== diff --git a/src/briefcase/platforms/linux/flatpak.py b/src/briefcase/platforms/linux/flatpak.py index 8af298596..94ccc27b3 100644 --- a/src/briefcase/platforms/linux/flatpak.py +++ b/src/briefcase/platforms/linux/flatpak.py @@ -1,4 +1,4 @@ -from typing import List +from __future__ import annotations from briefcase.commands import ( BuildCommand, @@ -203,7 +203,7 @@ def run_app( self, app: AppConfig, test_mode: bool, - passthrough: List[str], + passthrough: list[str], **kwargs, ): """Start the application. diff --git a/src/briefcase/platforms/macOS/filters.py b/src/briefcase/platforms/macOS/filters.py index eefd1dc63..fe51bcec4 100644 --- a/src/briefcase/platforms/macOS/filters.py +++ b/src/briefcase/platforms/macOS/filters.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import re MACOS_LOG_PREFIX_REGEX = re.compile( diff --git a/src/briefcase/platforms/macOS/xcode.py b/src/briefcase/platforms/macOS/xcode.py index 78ed6dee5..cd8a0e648 100644 --- a/src/briefcase/platforms/macOS/xcode.py +++ b/src/briefcase/platforms/macOS/xcode.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import subprocess from briefcase.commands import ( diff --git a/tests/commands/create/conftest.py b/tests/commands/create/conftest.py index d5039cb26..50f067f18 100644 --- a/tests/commands/create/conftest.py +++ b/tests/commands/create/conftest.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from unittest import mock import pytest From b708e821fa54a5bf56b2365042e7af2626347ee2 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Fri, 12 Jan 2024 13:32:39 +0800 Subject: [PATCH 14/16] Modify the template context generate platform-appropriate keys. --- docs/reference/configuration.rst | 14 +++--- docs/reference/platforms/android/gradle.rst | 24 +++++++++-- docs/reference/platforms/iOS/xcode.rst | 20 ++++++--- docs/reference/platforms/macOS/app.rst | 37 ++++++++++------ docs/reference/platforms/macOS/xcode.rst | 37 ++++++++++------ src/briefcase/platforms/iOS/xcode.py | 29 ++++++------- src/briefcase/platforms/macOS/__init__.py | 19 ++++----- tests/platforms/iOS/xcode/test_create.py | 45 +++++++++++++------- tests/platforms/macOS/app/test_create.py | 47 ++++++++++++++------- 9 files changed, 167 insertions(+), 105 deletions(-) diff --git a/docs/reference/configuration.rst b/docs/reference/configuration.rst index 4302e144d..f4002975c 100644 --- a/docs/reference/configuration.rst +++ b/docs/reference/configuration.rst @@ -477,19 +477,15 @@ Briefcase maintains a set of cross-platform permissions: * ``permission.background_location`` - permission to track GPS location while in the background. * ``permission.photo_library`` - permission to access to the user's photo library. -If a cross-platform permission is used, it will be mapped to platform-specific values. -Permissions can also be specified directly as platform-specific keys. For example, -Android defines a ``android.permission.HIGH_SAMPLING_RATE_SENSORS`` permission; this -could be specified by defining -``permission."android.permission.HIGH_SAMPLING_RATE_SENSORS"``. If a platform-specific -key is specified, it will override any value specified as part of a cross-platform -value. +If a cross-platform permission is used, it will be mapped to platform-specific values in +whatever files are used to define permissions on that platform. + +Permissions can also be configured by adding platform-specific configuration items. See the documentation for the the platform backends to see the options that are available. The value for each permission is a short description of why that permission is required. If the platform requires, the value may be displayed to the user as part of an authorization dialog. This description should describe *why* the app requires the -permission, rather than a generic description of the permission being requested. The -values for platform-specific permissions may also be Boolean or integers if required. +permission, rather than a generic description of the permission being requested. The use of cross-platform may also imply other settings in your app. See the individual platform backends for details on how cross-platform permissions are mapped. diff --git a/docs/reference/platforms/android/gradle.rst b/docs/reference/platforms/android/gradle.rst index 403b1773a..45a05cc7a 100644 --- a/docs/reference/platforms/android/gradle.rst +++ b/docs/reference/platforms/android/gradle.rst @@ -181,6 +181,22 @@ will result in an ``AndroidManifest.xml`` declaration of:: The use of some cross-platform permissions will imply the addition of features; see :ref:`the discussion on Android permissions ` for more details. +``permission`` +-------------- + +A property whose sub-properties define the platform-specific permissions that will be +marked as required by the final app. Each entry will be converted into a +```` declaration in your app's ``AndroidManifest.xml``, with the +feature name matching the name of the sub-attribute. + +For example, specifying:: + + permission."android.permission.HIGH_SAMPLING_RATE_SENSORS" = true + +will result in an ``AndroidManifest.xml`` declaration of:: + + + ``version_code`` ---------------- @@ -275,8 +291,8 @@ app's ``AppManifest.xml``: Every application will be automatically granted the ``android.permission.INTERNET`` and ``android.permission.NETWORK_STATE`` permissions. -Specifying a ``camera`` permission will result in the following non-required features -being implicitly added to your app: +Specifying a ``camera`` permission will result in the following non-required ``feature`` +definitions being implicitly added to your app: * ``android.hardware.camera``, * ``android.hardware.camera.any``, @@ -285,8 +301,8 @@ being implicitly added to your app: * ``android.hardware.camera.autofocus``. Specifying the ``coarse_location``, ``fine_location`` or ``background_location`` -permissions will result in the following non-required features being implicitly added to -your app: +permissions will result in the following non-required ``feature`` declarations being +implicitly added to your app: * ``android.hardware.location.network`` * ``android.hardware.location.gps`` diff --git a/docs/reference/platforms/iOS/xcode.rst b/docs/reference/platforms/iOS/xcode.rst index 6a0e76c9f..cef15c266 100644 --- a/docs/reference/platforms/iOS/xcode.rst +++ b/docs/reference/platforms/iOS/xcode.rst @@ -70,17 +70,25 @@ Application configuration The following options can be added to the ``tool.briefcase.app..iOS.app`` section of your ``pyproject.toml`` file. -``info_plist_extra_content`` ----------------------------- +``info`` +-------- -A string providing additional content that will be added verbatim to the end of your -app's ``Info.plist`` file, at the end of the main ```` declaration. +A property whose sub-attributes define keys that will be added to the app's +``Info.plist`` file. Each entry will be converted into a key in the entitlements +file. For example, specifying:: + + info."UIFileSharingEnabled" = true + +will result in an ``Info.plist`` declaration of:: + + UIFileSharingEnabled + +Any Boolean or string value can be used for an ``Info.plist`` value. Permissions =========== -Briefcase cross platform permissions map to the following keys in the app's -``Info.plist``: +Briefcase cross platform permissions map to the following ``info`` keys: * ``camera``: ``NSCameraUsageDescription`` * ``microphone``: ``NSMicrophoneUsageDescription`` diff --git a/docs/reference/platforms/macOS/app.rst b/docs/reference/platforms/macOS/app.rst index 49b0b335f..5f3284cfb 100644 --- a/docs/reference/platforms/macOS/app.rst +++ b/docs/reference/platforms/macOS/app.rst @@ -84,11 +84,20 @@ enable library validation, you could add the following to your ``pyproject.toml` entitlement."com.apple.security.cs.disable-library-validation" = false -``info_plist_extra_content`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +``info`` +~~~~~~~~ -A string providing additional content that will be added verbatim to the end of your -app's ``Info.plist`` file, at the end of the main ```` declaration. +A property whose sub-attributes define keys that will be added to the app's +``Info.plist`` file. Each entry will be converted into a key in the entitlements +file. For example, specifying:: + + info."NSAppleScriptEnabled" = true + +will result in an ``Info.plist`` declaration of:: + + NSAppleScriptEnabled + +Any Boolean or string value can be used for an ``Info.plist`` value. ``universal_build`` ~~~~~~~~~~~~~~~~~~~ @@ -102,20 +111,20 @@ you will produce an ARM64 binary. Permissions =========== -Briefcase cross platform permissions map to a combination of ``entitlement`` -definitions, and keys in the app's ``Info.plist``: +Briefcase cross platform permissions map to a combination of ``info`` and ``entitlement`` +keys: -* ``camera``: an entitlement of ``com.apple.security.device.camera`` -* ``microphone``: an entitlement of ``com.apple.security.device.audio-input`` -* ``coarse_location``: an ``Info.plist`` entry for ``NSLocationUsageDescription`` +* ``microphone``: an ``entitlement`` of ``com.apple.security.device.audio-input`` +* ``camera``: an ``entitlement`` of ``com.apple.security.device.camera`` +* ``coarse_location``: an ``info`` entry for ``NSLocationUsageDescription`` (ignored if ``background_location`` or ``fine_location`` is defined); plus an entitlement of ``com.apple.security.personal-information.location`` -* ``fine_location``: an ``Info.plist`` entry for ``NSLocationUsageDescription``(ignored - if ``background_location`` is defined); plus a device requirement of +* ``fine_location``: an ``info`` entry for ``NSLocationUsageDescription``(ignored + if ``background_location`` is defined); plus an ``entitlement`` of ``com.apple.security.personal-information.location`` -* ``background_location``: an ``Info.plist`` entry for ``NSLocationUsageDescription``; - plus an entitlement of ``com.apple.security.personal-information.location`` -* ``photo_library``: an entitlement of ``com.apple.security.personal-information.photos-library`` +* ``background_location``: an ``info`` entry for ``NSLocationUsageDescription``; + plus an ``entitlement`` of ``com.apple.security.personal-information.location`` +* ``photo_library``: an ``entitlement`` of ``com.apple.security.personal-information.photos-library`` Platform quirks =============== diff --git a/docs/reference/platforms/macOS/xcode.rst b/docs/reference/platforms/macOS/xcode.rst index 931a511c4..007ea8d8a 100644 --- a/docs/reference/platforms/macOS/xcode.rst +++ b/docs/reference/platforms/macOS/xcode.rst @@ -90,11 +90,20 @@ enable library validation, you could add the following to your ``pyproject.toml` entitlement."com.apple.security.cs.disable-library-validation" = false -``info_plist_extra_content`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +``info`` +~~~~~~~~ -A string providing additional content that will be added verbatim to the end of your -app's ``Info.plist`` file, at the end of the main ```` declaration. +A property whose sub-attributes define keys that will be added to the app's +``Info.plist`` file. Each entry will be converted into a key in the entitlements +file. For example, specifying:: + + info."NSAppleScriptEnabled" = true + +will result in an ``Info.plist`` declaration of:: + + NSAppleScriptEnabled + +Any Boolean or string value can be used for an ``Info.plist`` value. ``universal_build`` ~~~~~~~~~~~~~~~~~~~ @@ -108,20 +117,20 @@ you will produce an ARM64 binary. Permissions =========== -Briefcase cross platform permissions map to a combination of ``entitlement`` -definitions, and keys in the app's ``Info.plist``: +Briefcase cross platform permissions map to a combination of ``info`` and ``entitlement`` +keys: -* ``camera``: an entitlement of ``com.apple.security.device.camera`` -* ``microphone``: an entitlement of ``com.apple.security.device.audio-input`` -* ``coarse_location``: an ``Info.plist`` entry for ``NSLocationUsageDescription`` +* ``microphone``: an ``entitlement`` of ``com.apple.security.device.audio-input`` +* ``camera``: an ``entitlement`` of ``com.apple.security.device.camera`` +* ``coarse_location``: an ``info`` entry for ``NSLocationUsageDescription`` (ignored if ``background_location`` or ``fine_location`` is defined); plus an entitlement of ``com.apple.security.personal-information.location`` -* ``fine_location``: an ``Info.plist`` entry for ``NSLocationUsageDescription``(ignored - if ``background_location`` is defined); plus a device requirement of +* ``fine_location``: an ``info`` entry for ``NSLocationUsageDescription``(ignored + if ``background_location`` is defined); plus an ``entitlement`` of ``com.apple.security.personal-information.location`` -* ``background_location``: an ``Info.plist`` entry for ``NSLocationUsageDescription``; - plus an entitlement of ``com.apple.security.personal-information.location`` -* ``photo_library``: an entitlement of ``com.apple.security.personal-information.photos-library`` +* ``background_location``: an ``info`` entry for ``NSLocationUsageDescription``; + plus an ``entitlement`` of ``com.apple.security.personal-information.location`` +* ``photo_library``: an ``entitlement`` of ``com.apple.security.personal-information.photos-library`` Platform quirks =============== diff --git a/src/briefcase/platforms/iOS/xcode.py b/src/briefcase/platforms/iOS/xcode.py index fcb7b4e57..ae88dc9f9 100644 --- a/src/briefcase/platforms/iOS/xcode.py +++ b/src/briefcase/platforms/iOS/xcode.py @@ -262,41 +262,38 @@ def permissions_context(self, app: AppConfig, x_permissions: dict[str, str]): definitions. :returns: The template context describing permissions for the app. """ - permissions = {} + # The collection of info.plist entries + info = {} if x_permissions["camera"]: - permissions["NSCameraUsageDescription"] = x_permissions["camera"] + info["NSCameraUsageDescription"] = x_permissions["camera"] if x_permissions["microphone"]: - permissions["NSMicrophoneUsageDescription"] = x_permissions["microphone"] + info["NSMicrophoneUsageDescription"] = x_permissions["microphone"] if x_permissions["fine_location"]: - permissions["NSLocationDefaultAccuracyReduced"] = False + info["NSLocationDefaultAccuracyReduced"] = False elif x_permissions["coarse_location"]: - permissions["NSLocationDefaultAccuracyReduced"] = True + info["NSLocationDefaultAccuracyReduced"] = True if x_permissions["background_location"]: - permissions["NSLocationAlwaysAndWhenInUseUsageDescription"] = x_permissions[ + info["NSLocationAlwaysAndWhenInUseUsageDescription"] = x_permissions[ "background_location" ] elif x_permissions["fine_location"]: - permissions["NSLocationWhenInUseUsageDescription"] = x_permissions[ - "fine_location" - ] + info["NSLocationWhenInUseUsageDescription"] = x_permissions["fine_location"] elif x_permissions["coarse_location"]: - permissions["NSLocationWhenInUseUsageDescription"] = x_permissions[ + info["NSLocationWhenInUseUsageDescription"] = x_permissions[ "coarse_location" ] if x_permissions["photo_library"]: - permissions["NSPhotoLibraryAddUsageDescription"] = x_permissions[ - "photo_library" - ] + info["NSPhotoLibraryAddUsageDescription"] = x_permissions["photo_library"] - # Override any permission definitions with the platform specific definitions - permissions.update(app.permission) + # Override any info.plist entries with the platform specific definitions + info.update(getattr(app, "info", {})) return { - "permissions": permissions, + "info": info, } def _extra_pip_args(self, app: AppConfig): diff --git a/src/briefcase/platforms/macOS/__init__.py b/src/briefcase/platforms/macOS/__init__.py index d9b4e443a..3bdd19c09 100644 --- a/src/briefcase/platforms/macOS/__init__.py +++ b/src/briefcase/platforms/macOS/__init__.py @@ -157,7 +157,8 @@ def permissions_context(self, app: AppConfig, cross_platform: dict[str, str]): definitions. :returns: The template context describing permissions for the app. """ - permissions = {} + # The info.plist entries for the app + info = {} # Default entitlements for all macOS apps entitlements = { @@ -171,28 +172,24 @@ def permissions_context(self, app: AppConfig, cross_platform: dict[str, str]): entitlements["com.apple.security.device.microphone"] = True if cross_platform["background_location"]: - permissions["NSLocationUsageDescription"] = cross_platform[ - "background_location" - ] + info["NSLocationUsageDescription"] = cross_platform["background_location"] entitlements["com.apple.security.personal-information.location"] = True elif cross_platform["fine_location"]: - permissions["NSLocationUsageDescription"] = cross_platform["fine_location"] + info["NSLocationUsageDescription"] = cross_platform["fine_location"] entitlements["com.apple.security.personal-information.location"] = True elif cross_platform["coarse_location"]: - permissions["NSLocationUsageDescription"] = cross_platform[ - "coarse_location" - ] + info["NSLocationUsageDescription"] = cross_platform["coarse_location"] entitlements["com.apple.security.personal-information.location"] = True if cross_platform["photo_library"]: entitlements["com.apple.security.personal-information.photo_library"] = True - # Override any permission and entitlement definitions with the platform specific definitions - permissions.update(app.permission) + # Override any info and entitlement definitions with the platform specific definitions + info.update(getattr(app, "info", {})) entitlements.update(getattr(app, "entitlement", {})) return { - "permissions": permissions, + "info": info, "entitlements": entitlements, } diff --git a/tests/platforms/iOS/xcode/test_create.py b/tests/platforms/iOS/xcode/test_create.py index f0ac1ad0b..8b7d37b4e 100644 --- a/tests/platforms/iOS/xcode/test_create.py +++ b/tests/platforms/iOS/xcode/test_create.py @@ -123,21 +123,23 @@ def test_extra_pip_args(create_command, first_app_generated, tmp_path): @pytest.mark.parametrize( - "permissions, context", + "permissions, info, context", [ # No permissions ( {}, - {"permissions": {}}, + {}, + {"info": {}}, ), # Only custom permissions ( + {}, { "NSCameraUsageDescription": "I need to see you", "NSMicrophoneUsageDescription": "I need to hear you", }, { - "permissions": { + "info": { "NSCameraUsageDescription": "I need to see you", "NSMicrophoneUsageDescription": "I need to hear you", } @@ -148,8 +150,9 @@ def test_extra_pip_args(create_command, first_app_generated, tmp_path): { "camera": "I need to see you", }, + {}, { - "permissions": { + "info": { "NSCameraUsageDescription": "I need to see you", }, }, @@ -159,8 +162,9 @@ def test_extra_pip_args(create_command, first_app_generated, tmp_path): { "microphone": "I need to hear you", }, + {}, { - "permissions": { + "info": { "NSMicrophoneUsageDescription": "I need to hear you", }, }, @@ -170,8 +174,9 @@ def test_extra_pip_args(create_command, first_app_generated, tmp_path): { "coarse_location": "I need to know roughly where you are", }, + {}, { - "permissions": { + "info": { "NSLocationDefaultAccuracyReduced": True, "NSLocationWhenInUseUsageDescription": "I need to know roughly where you are", } @@ -182,8 +187,9 @@ def test_extra_pip_args(create_command, first_app_generated, tmp_path): { "fine_location": "I need to know exactly where you are", }, + {}, { - "permissions": { + "info": { "NSLocationDefaultAccuracyReduced": False, "NSLocationWhenInUseUsageDescription": "I need to know exactly where you are", } @@ -194,8 +200,9 @@ def test_extra_pip_args(create_command, first_app_generated, tmp_path): { "background_location": "I always need to know where you are", }, + {}, { - "permissions": { + "info": { "NSLocationAlwaysAndWhenInUseUsageDescription": "I always need to know where you are", } }, @@ -206,8 +213,9 @@ def test_extra_pip_args(create_command, first_app_generated, tmp_path): "coarse_location": "I need to know roughly where you are", "background_location": "I always need to know where you are", }, + {}, { - "permissions": { + "info": { "NSLocationDefaultAccuracyReduced": True, "NSLocationAlwaysAndWhenInUseUsageDescription": "I always need to know where you are", } @@ -219,8 +227,9 @@ def test_extra_pip_args(create_command, first_app_generated, tmp_path): "fine_location": "I need to know exactly where you are", "background_location": "I always need to know where you are", }, + {}, { - "permissions": { + "info": { "NSLocationDefaultAccuracyReduced": False, "NSLocationAlwaysAndWhenInUseUsageDescription": "I always need to know where you are", } @@ -232,8 +241,9 @@ def test_extra_pip_args(create_command, first_app_generated, tmp_path): "coarse_location": "I need to know roughly where you are", "fine_location": "I need to know exactly where you are", }, + {}, { - "permissions": { + "info": { "NSLocationDefaultAccuracyReduced": False, "NSLocationWhenInUseUsageDescription": "I need to know exactly where you are", } @@ -246,8 +256,9 @@ def test_extra_pip_args(create_command, first_app_generated, tmp_path): "fine_location": "I need to know exactly where you are", "background_location": "I always need to know where you are", }, + {}, { - "permissions": { + "info": { "NSLocationDefaultAccuracyReduced": False, "NSLocationAlwaysAndWhenInUseUsageDescription": "I always need to know where you are", } @@ -258,8 +269,9 @@ def test_extra_pip_args(create_command, first_app_generated, tmp_path): { "photo_library": "I need to see your library", }, + {}, { - "permissions": { + "info": { "NSPhotoLibraryAddUsageDescription": "I need to see your library" } }, @@ -268,11 +280,13 @@ def test_extra_pip_args(create_command, first_app_generated, tmp_path): ( { "camera": "I need to see you", + }, + { "NSCameraUsageDescription": "Platform specific", "NSCustomPermission": "Custom message", }, { - "permissions": { + "info": { "NSCameraUsageDescription": "Platform specific", "NSCustomPermission": "Custom message", } @@ -280,10 +294,11 @@ def test_extra_pip_args(create_command, first_app_generated, tmp_path): ), ], ) -def test_permissions_context(create_command, first_app, permissions, context): +def test_permissions_context(create_command, first_app, permissions, info, context): """Platform-specific permissions can be added to the context.""" # Set the permissions value first_app.permission = permissions + first_app.info = info # Extract the cross-platform permissions x_permissions = create_command._x_permissions(first_app) # Check that the final platform permissions are rendered as expected. diff --git a/tests/platforms/macOS/app/test_create.py b/tests/platforms/macOS/app/test_create.py index 506bc8951..b7e286741 100644 --- a/tests/platforms/macOS/app/test_create.py +++ b/tests/platforms/macOS/app/test_create.py @@ -28,14 +28,15 @@ def create_command(tmp_path, first_app_templated): @pytest.mark.parametrize( - "permissions, entitlements, context", + "permissions, info, entitlements, context", [ # No permissions ( + {}, {}, {}, { - "permissions": {}, + "info": {}, "entitlements": { "com.apple.security.cs.allow-unsigned-executable-memory": True, "com.apple.security.cs.disable-library-validation": True, @@ -44,6 +45,7 @@ def create_command(tmp_path, first_app_templated): ), # Only custom permissions ( + {}, { "NSCustomPermission": "Custom message", }, @@ -51,7 +53,7 @@ def create_command(tmp_path, first_app_templated): "com.apple.vm.networking": True, }, { - "permissions": { + "info": { "NSCustomPermission": "Custom message", }, "entitlements": { @@ -67,8 +69,9 @@ def create_command(tmp_path, first_app_templated): "camera": "I need to see you", }, {}, + {}, { - "permissions": {}, + "info": {}, "entitlements": { "com.apple.security.cs.allow-unsigned-executable-memory": True, "com.apple.security.cs.disable-library-validation": True, @@ -82,8 +85,9 @@ def create_command(tmp_path, first_app_templated): "microphone": "I need to hear you", }, {}, + {}, { - "permissions": {}, + "info": {}, "entitlements": { "com.apple.security.cs.allow-unsigned-executable-memory": True, "com.apple.security.cs.disable-library-validation": True, @@ -97,8 +101,9 @@ def create_command(tmp_path, first_app_templated): "coarse_location": "I need to know roughly where you are", }, {}, + {}, { - "permissions": { + "info": { "NSLocationUsageDescription": "I need to know roughly where you are", }, "entitlements": { @@ -114,8 +119,9 @@ def create_command(tmp_path, first_app_templated): "fine_location": "I need to know exactly where you are", }, {}, + {}, { - "permissions": { + "info": { "NSLocationUsageDescription": "I need to know exactly where you are", }, "entitlements": { @@ -131,8 +137,9 @@ def create_command(tmp_path, first_app_templated): "background_location": "I always need to know where you are", }, {}, + {}, { - "permissions": { + "info": { "NSLocationUsageDescription": "I always need to know where you are", }, "entitlements": { @@ -149,8 +156,9 @@ def create_command(tmp_path, first_app_templated): "background_location": "I always need to know where you are", }, {}, + {}, { - "permissions": { + "info": { "NSLocationUsageDescription": "I always need to know where you are", }, "entitlements": { @@ -167,8 +175,9 @@ def create_command(tmp_path, first_app_templated): "background_location": "I always need to know where you are", }, {}, + {}, { - "permissions": { + "info": { "NSLocationUsageDescription": "I always need to know where you are", }, "entitlements": { @@ -185,8 +194,9 @@ def create_command(tmp_path, first_app_templated): "fine_location": "I need to know exactly where you are", }, {}, + {}, { - "permissions": { + "info": { "NSLocationUsageDescription": "I need to know exactly where you are", }, "entitlements": { @@ -204,8 +214,9 @@ def create_command(tmp_path, first_app_templated): "background_location": "I always need to know where you are", }, {}, + {}, { - "permissions": { + "info": { "NSLocationUsageDescription": "I always need to know where you are", }, "entitlements": { @@ -221,8 +232,9 @@ def create_command(tmp_path, first_app_templated): "photo_library": "I need to see your library", }, {}, + {}, { - "permissions": {}, + "info": {}, "entitlements": { "com.apple.security.cs.allow-unsigned-executable-memory": True, "com.apple.security.cs.disable-library-validation": True, @@ -234,6 +246,8 @@ def create_command(tmp_path, first_app_templated): ( { "fine_location": "I need to know where you are", + }, + { "NSCustomMessage": "Custom message", "NSLocationUsageDescription": "Platform specific", }, @@ -243,7 +257,7 @@ def create_command(tmp_path, first_app_templated): "com.apple.vm.networking": True, }, { - "permissions": { + "info": { "NSLocationUsageDescription": "Platform specific", "NSCustomMessage": "Custom message", }, @@ -258,11 +272,12 @@ def create_command(tmp_path, first_app_templated): ], ) def test_permissions_context( - create_command, first_app, permissions, entitlements, context + create_command, first_app, permissions, info, entitlements, context ): """Platform-specific permissions can be added to the context.""" - # Set the permission and entitlement value + # Set the permission, info and entitlement values first_app.permission = permissions + first_app.info = info first_app.entitlement = entitlements # Extract the cross-platform permissions x_permissions = create_command._x_permissions(first_app) From 49e0f1e95c251e4a5a49fe54e5aca87f50968126 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Mon, 15 Jan 2024 11:17:27 +0800 Subject: [PATCH 15/16] Update docs/reference/configuration.rst Co-authored-by: Malcolm Smith --- docs/reference/configuration.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference/configuration.rst b/docs/reference/configuration.rst index f4002975c..28cf501fc 100644 --- a/docs/reference/configuration.rst +++ b/docs/reference/configuration.rst @@ -487,7 +487,7 @@ If the platform requires, the value may be displayed to the user as part of an authorization dialog. This description should describe *why* the app requires the permission, rather than a generic description of the permission being requested. -The use of cross-platform may also imply other settings in your app. See the individual +The use of permissions may also imply other settings in your app. See the individual platform backends for details on how cross-platform permissions are mapped. Document types From 23857b1c4d5ceda6b98defe3ab4fa963b4d1e4fc Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Mon, 15 Jan 2024 11:21:11 +0800 Subject: [PATCH 16/16] Removed some stray words in Gradle docs. --- docs/reference/platforms/android/gradle.rst | 22 ++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/docs/reference/platforms/android/gradle.rst b/docs/reference/platforms/android/gradle.rst index 45a05cc7a..c9d27cbdf 100644 --- a/docs/reference/platforms/android/gradle.rst +++ b/docs/reference/platforms/android/gradle.rst @@ -123,38 +123,38 @@ section of your ``pyproject.toml`` file. ``android_manifest_attrs_extra_content`` ---------------------------------------- -Additional attributes that will be added verbatim to add to the ```` -declaration of the ``AndroidManifest.xml`` of your app. +Additional attributes that will be added verbatim to the ```` declaration of +the ``AndroidManifest.xml`` of your app. ``android_manifest_extra_content`` ---------------------------------- -Additional content that will be added verbatim to add just before the closing -```` declaration of the ``AndroidManifest.xml`` of your app. +Additional content that will be added verbatim just before the closing ```` +declaration of the ``AndroidManifest.xml`` of your app. ``android_manifest_application_attrs_extra_content`` ---------------------------------------------------- -Additional attributes that will be added verbatim to add to the ```` -declaration of the ``AndroidManifest.xml`` of your app. +Additional attributes that will be added verbatim to the ```` declaration +of the ``AndroidManifest.xml`` of your app. ``android_manifest_application_extra_content`` ---------------------------------------------- -Additional content that will be added verbatim to add just before the closing +Additional content that will be added verbatim just before the closing ```` declaration of the ``AndroidManifest.xml`` of your app. ``android_manifest_activity_attrs_extra_content`` ------------------------------------------------- -Additional attributes that will be added verbatim to add to the ```` -declaration of the ``AndroidManifest.xml`` of your app. +Additional attributes that will be added verbatim to the ```` declaration of +the ``AndroidManifest.xml`` of your app. ``android_manifest_activity_extra_content`` ------------------------------------------- -Additional content that will be added verbatim to add just before the closing -```` declaration of the ``AndroidManifest.xml`` of your app. +Additional content that will be added verbatim just before the closing ```` +declaration of the ``AndroidManifest.xml`` of your app. ``build_gradle_extra_content`` ------------------------------