diff --git a/apple/internal/partials/resources.bzl b/apple/internal/partials/resources.bzl index a24a5f622a..da4490a2de 100644 --- a/apple/internal/partials/resources.bzl +++ b/apple/internal/partials/resources.bzl @@ -14,7 +14,7 @@ """Partial implementations for resource processing. -Resources are procesed according to type, by a series of methods that deal with the specifics for +Resources are processed according to type, by a series of methods that deal with the specifics for each resource type. Each of this methods returns a struct, which always have a `files` field containing resource tuples as described in processor.bzl. Optionally, the structs can also have an `infoplists` field containing a list of plists that should be merged into the root Info.plist. @@ -371,7 +371,7 @@ def resources_partial( occur. bundle_name: The name of the output bundle. executable_name: The name of the output executable. - bundle_verification_targets: List of structs that reference embedable targets that need to + bundle_verification_targets: List of structs that reference embeddable targets that need to be validated. The structs must have a `target` field with the target containing an Info.plist file that will be validated. The structs may also have a `parent_bundle_id_reference` field that contains the plist path, in list form, to the diff --git a/tools/plisttool/plisttool.py b/tools/plisttool/plisttool.py index c1bd5c8ebb..b6fc8c01b4 100644 --- a/tools/plisttool/plisttool.py +++ b/tools/plisttool/plisttool.py @@ -70,7 +70,7 @@ compared against the final compiled plist for consistency. The keys of the dictionary are the labels of the targets to which the associated plists belong. See below for the details of how these are validated. - child_plist_required_values: If present, a dictionary constaining the + child_plist_required_values: If present, a dictionary containing the entries for key/value pairs a child is required to have. This dictionary is keyed by the label of the child targets (just like the `child_plists`), and the valures are a list of key/value pairs. The @@ -185,10 +185,14 @@ 'has the wrong value for "%s"; expected %r, but found %r.' ) -MISSING_VERSION_KEY_MSG = ( +MISSING_PLIST_KEY_MSG = ( 'Target "%s" is missing %s.' ) +MISSING_PLIST_KEY_IN_PARENT_MSG = ( + 'Target "%s" is missing %s in dictionary %s.' +) + INVALID_VERSION_KEY_VALUE_MSG = ( 'Target "%s" has a %s that doesn\'t meet Apple\'s guidelines: "%s". See ' 'https://developer.apple.com/library/content/technotes/tn2420/_index.html' @@ -196,6 +200,89 @@ 'https://developer.apple.com/library/content/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html' ) +UNEXPECTED_EXAPPEXTENSIONATTRIBUTES = ( + 'Target %s has an unexpected key, EXAppExtensionAttributes, for product type com.apple.product-type.app-extension. ' + 'Plugin extensions expect key NSExtension. ' + 'To build an app extension with ExtensionKit, set `extensionkit_extension = True`. ' +) + +UNEXPECTED_NSEXTENSION = ( + 'Target %s has an unexpected key, NSExtension, for product type com.apple.product-type.extensionkit-extension. ' + 'ExtensionKit extensions expect key EXAppExtensionAttributes. ' + 'To build an app extension without ExtensionKit, set `extensionkit_extension = False`. ' +) + +KNOWN_EXTENSIONKIT_EXTENSION_POINT_IDENTIFIERS = ( + 'com.apple.appintents-extension', + 'com.apple.background-asset-downloader-extension', + 'com.apple.deviceactivityui.report-extension', + 'com.apple.discovery-extension' +) + +KNOWN_NSEXTENSION_EXTENSION_POINT_IDENTIFIERS = ( + 'com.apple.AppSSO.idp-extension', + 'com.apple.AudioUnit', + 'com.apple.AudioUnit-UI', + 'com.apple.FinderSync', + 'com.apple.ManagedSettings.shield-action-service', + 'com.apple.ManagedSettingsUI.shield-configuration-service', + 'com.apple.Safari.content-blocker', + 'com.apple.Safari.extension', + 'com.apple.Safari.web-extension', + 'com.apple.authentication-services-account-authentication-modification-ui', + 'com.apple.authentication-services-credential-provider-ui', + 'com.apple.broadcast-services-setupui', + 'com.apple.broadcast-services-upload', + 'com.apple.calendar.virtualconference', + 'com.apple.callkit.call-directory', + 'com.apple.classkit.context-provider', + 'com.apple.ctk-tokens', + 'com.apple.deviceactivity.monitor-extension', + 'com.apple.dt.Xcode.extension.source-editor', + 'com.apple.email.extension', + 'com.apple.fileprovider-actionsui', + 'com.apple.fileprovider-nonui', + 'com.apple.identitylookup.classification-ui', + 'com.apple.identitylookup.message-filter', + 'com.apple.intents-service', + 'com.apple.intents-ui-service', + 'com.apple.keyboard-service', + 'com.apple.location.push.service', + 'com.apple.matter.support.extension.device-setup', + 'com.apple.message-payload-provider', + 'com.apple.message-payload-provider', + 'com.apple.networkextension.app-proxy', + 'com.apple.networkextension.dns-proxy', + 'com.apple.networkextension.filter-control', + 'com.apple.networkextension.filter-data', + 'com.apple.networkextension.packet-tunnel', + 'com.apple.photo-editing', + 'com.apple.photo-project', + 'com.apple.printing.discovery', + 'com.apple.quicklook.preview', + 'com.apple.quicklook.thumbnail', + 'com.apple.services', + 'com.apple.share-services', + 'com.apple.spotlight.import', + 'com.apple.tv-top-shelf', + 'com.apple.ui-services', + 'com.apple.usernotifications.content-extension', + 'com.apple.usernotifications.service', + 'com.apple.widgetkit-extension', +) + +KNOWN_EXTENSIONKIT_WARNING = ( + 'Target %s with extension point %s is known to be an ExtensionKit App Extension. ' + 'The target rule may be misconfigured. ' + 'ExtensionKit App Extension targets expect the attribute `extensionkit_extension = True`.' +) + +KNOWN_NSEXTENSION_WARNING = ( + 'Target %s with extension point %s is known to be an NSExtension. ' + 'The target rule may be misconfigured. ' + 'NSExtension targets expect the attribute `extensionkit_extension = False`.' +) + PLUTIL_CONVERSION_TO_XML_FAILED_MSG = ( 'While processing target "%s", plutil failed (%d) to convert "%s" to xml.' ) @@ -343,8 +430,8 @@ # All valid keys in the info_plist_options control structure. _INFO_PLIST_OPTIONS_KEYS = frozenset([ - 'child_plists', 'child_plist_required_values', 'pkginfo', 'version_file', - 'version_keys_required', + 'child_plists', 'child_plist_required_values', 'extensionkit_keys_required', + 'nsextension_keys_required', 'pkginfo', 'version_file', 'version_keys_required', ]) # All valid keys in the entitlements_options control structure. @@ -897,11 +984,25 @@ def update_plist(self, out_plist, subs_engine): out_plist['CFBundleShortVersionString'] = short_version_string def validate_plist(self, plist): + self._validate_plist_version(plist) + self._validate_child_plist_required_values(plist) + self._validate_plist_extensionkit(plist) + self._validate_plist_nsextension(plist) + + def _validate_plist_version(self, plist): + """When `version_keys_required`, checks that the given plist has version keys. Validates version keys. + + Args: + plist: The dictionary representing final plist. + Raises: + PlistToolError: For any issues found. + """ + if self.options.get('version_keys_required'): for k in ('CFBundleVersion', 'CFBundleShortVersionString'): # This also errors if they are there but the empty string or zero. if not plist.get(k, None): - raise PlistToolError(MISSING_VERSION_KEY_MSG % (self.target, k)) + raise PlistToolError(MISSING_PLIST_KEY_MSG % (self.target, k)) # If the version keys are set, they must be valid (even if they were # not required). @@ -913,6 +1014,15 @@ def validate_plist(self, plist): raise PlistToolError(INVALID_VERSION_KEY_VALUE_MSG % ( self.target, k, v)) + def _validate_child_plist_required_values(self, plist): + """Validates required values. + + Args: + plist: The dictionary representing final plist. + Raises: + PlistToolError: For any issues found. + """ + child_plists = self.options.get('child_plists') child_plist_required_values = self.options.get( 'child_plist_required_values') @@ -928,6 +1038,133 @@ def validate_plist(self, plist): else: self._write_pkginfo(pkginfo_file, plist) + def _validate_plist_extensionkit(self, plist): + """When `extensionkit_keys_required`, checks that the given plist is valid for an ExtensionKit App Extension. + + ExtensionKit App Extensions expect a `EXAppExtensionAttributes` dictionary containing an `EXExtensionPointIdentifier` entry. + If the given extension point is not recognized as an ExtensionKit extension, but known to be an NSExtension, provide a + warning with a hint to resolve; this is not a blocking error, since supported extension point strings may change over time. + If an NSExtension key is found, raise an error. If the ExtensionKit keys are not found, raise an error. + + Args: + plist: The dictionary representing final plist. + Raises: + PlistToolError: For any issues found. + """ + + if not self.options.get('extensionkit_keys_required'): + return + + # Check for extension point identifiers within any keys, so we can surface useful warnings before errors. + unchecked_extension_point_identifier = self._any_extension_point_identifier(plist) + known_extension_point = False + for known_extension_point_identifier in KNOWN_EXTENSIONKIT_EXTENSION_POINT_IDENTIFIERS: + if unchecked_extension_point_identifier == known_extension_point_identifier: + # This is a known ExtensionKit extension point, skip checking against known NSExtension extension points, + # since the lists may overlap + known_extension_point = True + + if not known_extension_point: + for known_extension_point_identifier in KNOWN_NSEXTENSION_EXTENSION_POINT_IDENTIFIERS: + if unchecked_extension_point_identifier == known_extension_point_identifier: + # This is a known NSExtension extension point, and we can provide a useful warning. + # Don't raise a blocking error, since Apple may change available extension points in the future. + print(KNOWN_NSEXTENSION_WARNING % (self.target, unchecked_extension_point_identifier)) + + # Explicitly check against None, since an empty dictionary should raise an error + if plist.get('NSExtension', None) is not None: + raise PlistToolError(UNEXPECTED_NSEXTENSION % self.target) + + self._validate_extension_point_identifier(self.target, plist, 'EXAppExtensionAttributes', 'EXExtensionPointIdentifier') + + def _validate_plist_nsextension(self, plist): + """When `nsextension_keys_required`, checks that the given plist is valid for an NSExtension App Extension. + + NSExtension App Extensions expect a `NSExtension` dictionary containing an `NSExtensionPointIdentifier` entry. + If the given extension point is not recognized as an NSExtension, but known to be an ExtensionKit Extension, provide a + warning with a hint to resolve; this is not a blocking error, since supported extension point strings may change over time. + If an EXAppExtensionAttributes key is found, raise an error. If the NSExtension keys are not found, raise an error. + + Args: + plist: The dictionary representing final plist. + Raises: + PlistToolError: For any issues found. + """ + + if not self.options.get('nsextension_keys_required'): + return + + # Check for extension point identifiers within any keys, so we can surface useful warnings before errors. + unchecked_extension_point_identifier = self._any_extension_point_identifier(plist) + known_extension_point = False + for known_extension_point_identifier in KNOWN_NSEXTENSION_EXTENSION_POINT_IDENTIFIERS: + if unchecked_extension_point_identifier == known_extension_point_identifier: + # This is a known NSExtension extension point, skip checking against known ExtensionKit extension points, + # since the lists may overlap + known_extension_point = True + + if not known_extension_point: + for known_extension_point_identifier in KNOWN_EXTENSIONKIT_EXTENSION_POINT_IDENTIFIERS: + if unchecked_extension_point_identifier == known_extension_point_identifier: + # This is a known ExtensionKit extension point, and we can provide a useful warning. + # Don't raise a blocking error, since Apple may change available extension points in the future. + print(KNOWN_EXTENSIONKIT_WARNING % (self.target, unchecked_extension_point_identifier)) + + # Explicitly check against None, since an empty dictionary should raise an error + if plist.get('EXAppExtensionAttributes', None) is not None: + raise PlistToolError(UNEXPECTED_EXAPPEXTENSIONATTRIBUTES % self.target) + + self._validate_extension_point_identifier(self.target, plist, 'NSExtension', 'NSExtensionPointIdentifier') + + @staticmethod + def _any_extension_point_identifier(plist): + """Finds any extension point identifier within an Info.plist. + + The extension point identifier may not exist in the correct key, but this can be used to provide actionable warnings. + + Args: + plist: The dictionary representing final plist. + Returns: + An extension point identifier string + """ + nsextension_parent = plist.get('NSExtension', None) + if nsextension_parent: + nsextension_point_identifier = nsextension_parent.get('NSExtensionPointIdentifier') + if nsextension_point_identifier: + return nsextension_point_identifier + + exappextension_parent = plist.get('EXAppExtensionAttributes', None) + if exappextension_parent: + exappextension_point_identifier = exappextension_parent.get('EXExtensionPointIdentifier') + if exappextension_point_identifier: + return exappextension_point_identifier + + return None + + @staticmethod + def _validate_extension_point_identifier(target, plist, parent_key, extension_point_identifier_key): + """Finds an extension point identifier within the given keys. + + The extension point identifier may not exist in the correct key, but this can be used to provide actionable warnings. + + Args: + target: The name of the target being processed. + plist: The dictionary representing final plist. + parent_key: The top-level plist key containing the extension point identifier (NSExtension or EXAppExtensionAttributes) + extension_point_identifier_key: The key within `parent` (NSExtensionPointIdentifier or EXExtensionPointIdentifier) + Raises: + PlistToolError: For missing keys. + Returns: + An extension point identifier string + """ + parent = plist.get(parent_key, None) + if not parent: + raise PlistToolError(MISSING_PLIST_KEY_MSG % (target, parent_key)) + + extension_point_identifier = parent.get(extension_point_identifier_key, None) + if not extension_point_identifier: + raise PlistToolError(MISSING_PLIST_KEY_IN_PARENT_MSG % (target, extension_point_identifier_key, parent_key)) + @staticmethod def _validate_children(plist, child_plists, child_required_values, target): """Validates a target's plist is consistent with its children. diff --git a/tools/plisttool/plisttool_unittest.py b/tools/plisttool/plisttool_unittest.py index d594a69087..7910804786 100644 --- a/tools/plisttool/plisttool_unittest.py +++ b/tools/plisttool/plisttool_unittest.py @@ -1091,7 +1091,7 @@ def test_unknown_info_plist_options_keys_raise(self): def test_missing_version(self): with self.assertRaisesRegex( plisttool.PlistToolError, - re.escape(plisttool.MISSING_VERSION_KEY_MSG % ( + re.escape(plisttool.MISSING_PLIST_KEY_MSG % ( _testing_target, 'CFBundleVersion'))): plist = {'CFBundleShortVersionString': '1.0'} _plisttool_result({ @@ -1104,7 +1104,7 @@ def test_missing_version(self): def test_missing_short_version(self): with self.assertRaisesRegex( plisttool.PlistToolError, - re.escape(plisttool.MISSING_VERSION_KEY_MSG % ( + re.escape(plisttool.MISSING_PLIST_KEY_MSG % ( _testing_target, 'CFBundleShortVersionString'))): plist = {'CFBundleVersion': '1.0'} _plisttool_result({ @@ -1117,7 +1117,7 @@ def test_missing_short_version(self): def test_empty_version(self): with self.assertRaisesRegex( plisttool.PlistToolError, - re.escape(plisttool.MISSING_VERSION_KEY_MSG % ( + re.escape(plisttool.MISSING_PLIST_KEY_MSG % ( _testing_target, 'CFBundleVersion'))): plist = { 'CFBundleShortVersionString': '1.0', @@ -1133,7 +1133,7 @@ def test_empty_version(self): def test_empty_short_version(self): with self.assertRaisesRegex( plisttool.PlistToolError, - re.escape(plisttool.MISSING_VERSION_KEY_MSG % ( + re.escape(plisttool.MISSING_PLIST_KEY_MSG % ( _testing_target, 'CFBundleShortVersionString'))): plist = { 'CFBundleShortVersionString': '', @@ -1197,6 +1197,186 @@ def test_entitlements_options_var_subs(self): }, }, {'Foo': 'abc123.'}) + def test_invalid_nsextension_attribute_in_extensionkit(self): + with self.assertRaisesRegex( + plisttool.PlistToolError, + re.escape(plisttool.UNEXPECTED_NSEXTENSION % _testing_target)): + plist = { + 'NSExtension': { + 'NSExtensionPointIdentifier': 'com.apple.email.extension', + }, + } + _plisttool_result({ + 'plists': [plist], + 'info_plist_options': { + 'extensionkit_keys_required': True, + }, + }) + + def test_empty_nsextension_attribute_in_extensionkit(self): + with self.assertRaisesRegex( + plisttool.PlistToolError, + re.escape(plisttool.UNEXPECTED_NSEXTENSION % _testing_target)): + plist = { + 'NSExtension': { }, + } + _plisttool_result({ + 'plists': [plist], + 'info_plist_options': { + 'extensionkit_keys_required': True, + }, + }) + + def test_extra_nsextension_attribute_in_extensionkit(self): + with self.assertRaisesRegex( + plisttool.PlistToolError, + re.escape(plisttool.UNEXPECTED_NSEXTENSION % _testing_target)): + plist = { + 'EXAppExtensionAttributes': { + 'EXExtensionPointIdentifier': 'com.apple.appintents-extension', + }, + 'NSExtension': { + 'NSExtensionPointIdentifier': 'com.apple.appintents-extension', + }, + } + _plisttool_result({ + 'plists': [plist], + 'info_plist_options': { + 'extensionkit_keys_required': True, + }, + }) + + def test_valid_extensionkit(self): + plist = { + 'EXAppExtensionAttributes': { + 'EXExtensionPointIdentifier': 'com.apple.appintents-extension', + }, + } + _plisttool_result({ + 'plists': [plist], + 'info_plist_options': { + 'extensionkit_keys_required': True, + }, + }) + + def test_valid_extensionkit_with_known_nsextension(self): + # Although `com.apple.email.extension` is a known NSExtension, don't raise an error, + # since this may change in the future. + plist = { + 'EXAppExtensionAttributes': { + 'EXExtensionPointIdentifier': 'com.apple.email.extension', + }, + } + _plisttool_result({ + 'plists': [plist], + 'info_plist_options': { + 'extensionkit_keys_required': True, + }, + }) + + def test_valid_extensionkit_with_unknown_extension_point(self): + plist = { + 'EXAppExtensionAttributes': { + 'EXExtensionPointIdentifier': 'com.example.extension', + }, + } + _plisttool_result({ + 'plists': [plist], + 'info_plist_options': { + 'extensionkit_keys_required': True, + }, + }) + + def test_invalid_extensionkit_attribute_in_nsextension(self): + with self.assertRaisesRegex( + plisttool.PlistToolError, + re.escape(plisttool.UNEXPECTED_EXAPPEXTENSIONATTRIBUTES % _testing_target)): + plist = { + 'EXAppExtensionAttributes': { + 'EXExtensionPointIdentifier': 'com.apple.appintents-extension', + }, + } + _plisttool_result({ + 'plists': [plist], + 'info_plist_options': { + 'nsextension_keys_required': True, + }, + }) + + def test_empty_extensionkit_attribute_in_nsextension(self): + with self.assertRaisesRegex( + plisttool.PlistToolError, + re.escape(plisttool.UNEXPECTED_EXAPPEXTENSIONATTRIBUTES % _testing_target)): + plist = { + 'EXAppExtensionAttributes': { }, + } + _plisttool_result({ + 'plists': [plist], + 'info_plist_options': { + 'nsextension_keys_required': True, + }, + }) + + def test_extra_extensionkit_attribute_in_nsextension(self): + with self.assertRaisesRegex( + plisttool.PlistToolError, + re.escape(plisttool.UNEXPECTED_EXAPPEXTENSIONATTRIBUTES % _testing_target)): + plist = { + 'EXAppExtensionAttributes': { + 'EXExtensionPointIdentifier': 'com.apple.email.extension', + }, + 'NSExtension': { + 'NSExtensionPointIdentifier': 'com.apple.email.extension', + }, + } + _plisttool_result({ + 'plists': [plist], + 'info_plist_options': { + 'nsextension_keys_required': True, + }, + }) + + def test_valid_nsextension(self): + plist = { + 'NSExtension': { + 'NSExtensionPointIdentifier': 'com.apple.email.extension', + }, + } + _plisttool_result({ + 'plists': [plist], + 'info_plist_options': { + 'nsextension_keys_required': True, + }, + }) + + def test_valid_nsextension_with_known_extensionkit(self): + # Although `com.apple.appintents-extension` is a known ExtensionKit extension, + # don't raise an error, since this may change in the future. + plist = { + 'NSExtension': { + 'NSExtensionPointIdentifier': 'com.apple.appintents-extension', + }, + } + _plisttool_result({ + 'plists': [plist], + 'info_plist_options': { + 'nsextension_keys_required': True, + }, + }) + + def test_valid_nsextension_with_unknown_extension_point(self): + plist = { + 'NSExtension': { + 'NSExtensionPointIdentifier': 'com.apple.some-new-extension', + }, + } + _plisttool_result({ + 'plists': [plist], + 'info_plist_options': { + 'nsextension_keys_required': True, + }, + }) + def test_entitlements_options_raw_subs(self): plist1 = {'Bar': 'abc123.*'} self._assert_plisttool_result({