From 519c703dcc61a97cc602893972b0d905d30ec91f Mon Sep 17 00:00:00 2001 From: Mark Vasiv Date: Thu, 16 Nov 2023 18:34:18 +0100 Subject: [PATCH] Support extensions within single-target watchOS apps (#2309) ### What has changed - Adds support for app extensions within single target watchOS apps. - Extension-based watchOS apps can now define their `watchos_extension` either in `extension` or in `extensions`, the former being deprecated. - ~Removes `extension` all-together from `watchos_application`.~ ### Notes - ~This is a breaking change for projects that use extension-based `watchos_application`, they would need to migrate from `extension = ":WatchKitExtension"` to `extensions = [":WatchKitExtension"]`. Initially I tried to keep both `extension` and `extensions` params to avoid that, but it breaks other things. For instance, [rules_xcodeproj here](https://github.com/MobileNativeFoundation/rules_xcodeproj/blob/8d900a9bbfdcceabe779a9fc269d7226bb9121e7/xcodeproj/internal/top_level_targets.bzl#L241-L244).~ - This is my first PR to this repo, please let me know if I have missed anything, I'd be happy to iterate on this PR if need be! --- apple/internal/watchos_rules.bzl | 75 +++++++++++++++++-- doc/rules-watchos.md | 7 +- examples/watchos/HelloWorld/BUILD | 2 +- .../targets_under_test/watchos/BUILD | 53 +++++++++++++ .../watchos_application_tests.bzl | 11 +++ ...atchos_single_target_application_tests.bzl | 41 ++++++++++ 6 files changed, 177 insertions(+), 12 deletions(-) diff --git a/apple/internal/watchos_rules.bzl b/apple/internal/watchos_rules.bzl index 83bdfc0551..e3965c5ce4 100644 --- a/apple/internal/watchos_rules.bzl +++ b/apple/internal/watchos_rules.bzl @@ -703,6 +703,25 @@ later will be met with a rejection. Please remove the assigned watchOS 2 app `extension` and make sure a valid watchOS application delegate is referenced in the single-target `watchos_application`'s `deps`. +""") + + if len(ctx.attr.extensions) > 1: + fail(""" +Extension-based watchOS applications do not support embedded app extensions via the `extensions` attribute. + +Please remove app extensions from this target, and instead add them via `watchos_extension`'s `extensions` attribute. +""") + + watch_extension = None + if ctx.attr.extension: + watch_extension = ctx.attr.extension + elif len(ctx.attr.extensions) == 1 and ctx.attr.extensions[0][AppleBundleInfo].product_type == apple_product_type.watch2_extension: + watch_extension = ctx.attr.extensions[0] + else: + fail(""" +Extension-based watchOS applications require a valid `watchos_extension`. + +Please add a `watchos_extension` to this target `extensions` attribute. """) rule_descriptor = rule_support.rule_descriptor( @@ -799,7 +818,7 @@ reproducible error case.".format( bundle_verification_targets = [ struct( - target = ctx.attr.extension, + target = watch_extension, parent_bundle_id_reference = [ "NSExtension", "NSExtensionAttributes", @@ -857,7 +876,7 @@ reproducible error case.".format( bundle_location = processor.location.watch, bundle_name = bundle_name, embed_target_dossiers = True, - embedded_targets = [ctx.attr.extension], + embedded_targets = [watch_extension], entitlements = entitlements.codesigning, label_name = label.name, platform_prerequisites = platform_prerequisites, @@ -869,7 +888,7 @@ reproducible error case.".format( actions = actions, bundle_extension = bundle_extension, bundle_name = bundle_name, - debug_dependencies = [ctx.attr.extension], + debug_dependencies = [watch_extension], dsym_info_plist_template = apple_mac_toolchain_info.dsym_info_plist_template, executable_name = executable_name, label_name = label.name, @@ -880,7 +899,7 @@ reproducible error case.".format( ), partials.embedded_bundles_partial( bundle_embedded_bundles = True, - embeddable_targets = [ctx.attr.extension], + embeddable_targets = [watch_extension], platform_prerequisites = platform_prerequisites, watch_bundles = [archive], ), @@ -907,7 +926,7 @@ reproducible error case.".format( apple_mac_toolchain_info = apple_mac_toolchain_info, binary_artifact = binary_artifact, bundle_dylibs = True, - dependency_targets = [ctx.attr.extension], + dependency_targets = [watch_extension], label_name = label.name, platform_prerequisites = platform_prerequisites, ), @@ -919,7 +938,7 @@ reproducible error case.".format( partials.apple_symbols_file_partial( actions = actions, binary_artifact = binary_artifact, - dependency_targets = [ctx.attr.extension], + dependency_targets = [watch_extension], dsym_binaries = {}, label_name = label.name, include_symbols_in_bundle = False, @@ -1436,6 +1455,15 @@ def _watchos_single_target_application_impl(ctx): fail(""" Single-target watchOS applications do not support watchOS 2 extensions or their delegates. +Please remove the assigned watchOS 2 app `extension` and make sure a valid watchOS application +delegate is referenced in the single-target `watchos_application`'s `deps`. +""") + + for extenstion in ctx.attr.extensions: + if extenstion[AppleBundleInfo].product_type == apple_product_type.watch2_extension: + fail(""" +Single-target watchOS applications do not support watchOS 2 extensions or their delegates. + Please remove the assigned watchOS 2 app `extension` and make sure a valid watchOS application delegate is referenced in the single-target `watchos_application`'s `deps`. """) @@ -1461,7 +1489,9 @@ delegate is referenced in the single-target `watchos_application`'s `deps`. shared_capabilities = ctx.attr.shared_capabilities, ) cc_toolchain_forwarder = ctx.split_attr._cc_toolchain_forwarder - embeddable_targets = ctx.attr.deps + embeddable_targets = ( + ctx.attr.deps + ctx.attr.frameworks + ctx.attr.extensions + ) executable_name = ctx.attr.executable_name features = features_support.compute_enabled_features( requested_features = ctx.features, @@ -1534,6 +1564,8 @@ delegate is referenced in the single-target `watchos_application`'s `deps`. rule_descriptor = rule_descriptor, ) + bundle_verification_targets = [struct(target = ext) for ext in ctx.attr.extensions] + processor_partials = [ partials.apple_bundle_info_partial( actions = actions, @@ -1612,6 +1644,11 @@ delegate is referenced in the single-target `watchos_application`'s `deps`. platform_prerequisites = platform_prerequisites, watch_bundles = [archive], ), + partials.extension_safe_validation_partial( + is_extension_safe = True, + rule_label = label, + targets_to_validate = ctx.attr.frameworks, + ), partials.framework_import_partial( actions = actions, apple_mac_toolchain_info = apple_mac_toolchain_info, @@ -1620,12 +1657,13 @@ delegate is referenced in the single-target `watchos_application`'s `deps`. platform_prerequisites = platform_prerequisites, provisioning_profile = provisioning_profile, rule_descriptor = rule_descriptor, - targets = ctx.attr.deps, + targets = embeddable_targets, ), partials.resources_partial( actions = actions, apple_mac_toolchain_info = apple_mac_toolchain_info, bundle_extension = bundle_extension, + bundle_verification_targets = bundle_verification_targets, bundle_id = bundle_id, bundle_name = bundle_name, executable_name = executable_name, @@ -1635,6 +1673,7 @@ delegate is referenced in the single-target `watchos_application`'s `deps`. resource_deps = resource_deps, rule_descriptor = rule_descriptor, rule_label = label, + targets_to_avoid = ctx.attr.frameworks, top_level_infoplists = top_level_infoplists, top_level_resources = top_level_resources, version = ctx.attr.version, @@ -1648,6 +1687,15 @@ delegate is referenced in the single-target `watchos_application`'s `deps`. label_name = label.name, platform_prerequisites = platform_prerequisites, ), + partials.apple_symbols_file_partial( + actions = actions, + binary_artifact = binary_artifact, + dependency_targets = embeddable_targets, + dsym_binaries = debug_outputs.dsym_binaries, + label_name = label.name, + include_symbols_in_bundle = False, + platform_prerequisites = platform_prerequisites, + ), ] if platform_prerequisites.platform.is_device: @@ -1741,6 +1789,17 @@ It is considered an error if the watchOS 2 application extension is assigned to watchOS application, which is constructed if the `watchos_application` target is assigned `deps`. This attribute will not support additional types of `watchos_extension`s in the future. + +This attribute is deprecated, please use `extensions` instead. +""", + ), + "extensions": attr.label_list( + providers = [[AppleBundleInfo, WatchosExtensionBundleInfo]], + doc = """ +In case of single-target watchOS app, a list of watchOS application extensions to include in the final watch app bundle. + +In case of an extension-based watchOS app, a list with a single element, +the watchOS 2 `watchos_extension` that is required to be bundled within a watchOS 2 app. """, ), "storyboards": attr.label_list( diff --git a/doc/rules-watchos.md b/doc/rules-watchos.md index 651ac7d5e5..0ccda39411 100644 --- a/doc/rules-watchos.md +++ b/doc/rules-watchos.md @@ -10,8 +10,8 @@ watchos_application(name, deps, resources, additional_linker_inputs, app_icons, app_intents, bundle_id, bundle_id_suffix, bundle_name, codesign_inputs, codesignopts, entitlements, entitlements_validation, executable_name, exported_symbols_lists, - extension, families, frameworks, infoplists, ipa_post_processor, linkopts, - minimum_deployment_os_version, minimum_os_version, platform_type, + extension, extensions, families, frameworks, infoplists, ipa_post_processor, + linkopts, minimum_deployment_os_version, minimum_os_version, platform_type, provisioning_profile, shared_capabilities, stamp, storyboards, strings, version) @@ -37,7 +37,8 @@ Builds and bundles a watchOS Application. | entitlements_validation | An `entitlements_validation_mode` to control the validation of the requested entitlements against the provisioning profile to ensure they are supported. | String | optional | `"loose"` | | executable_name | The desired name of the executable, if the bundle has an executable. If this attribute is not set, then the name of the `bundle_name` attribute will be used if it is set; if not, then the name of the target will be used instead. | String | optional | `""` | | exported_symbols_lists | A list of targets containing exported symbols lists files for the linker to control symbol resolution.

Each file is expected to have a list of global symbol names that will remain as global symbols in the compiled binary owned by this framework. All other global symbols will be treated as if they were marked as `__private_extern__` (aka `visibility=hidden`) and will not be global in the output file.

See the man page documentation for `ld(1)` on macOS for more details. | List of labels | optional | `[]` | -| extension | The watchOS 2 `watchos_extension` that is required to be bundled within a watchOS 2 application.

It is considered an error if the watchOS 2 application extension is assigned to a single target watchOS application, which is constructed if the `watchos_application` target is assigned `deps`.

This attribute will not support additional types of `watchos_extension`s in the future. | Label | optional | `None` | +| extension | The watchOS 2 `watchos_extension` that is required to be bundled within a watchOS 2 application.

It is considered an error if the watchOS 2 application extension is assigned to a single target watchOS application, which is constructed if the `watchos_application` target is assigned `deps`.

This attribute will not support additional types of `watchos_extension`s in the future.

This attribute is deprecated, please use `extensions` instead. | Label | optional | `None` | +| extensions | In case of single-target watchOS app, a list of watchOS application extensions to include in the final watch app bundle.

In case of an extension-based watchOS app, a list with a single element, the watchOS 2 `watchos_extension` that is required to be bundled within a watchOS 2 app. | List of labels | optional | `[]` | | families | A list of device families supported by this rule. At least one must be specified. | List of strings | optional | `["watch"]` | | frameworks | A list of framework targets (see [`watchos_framework`](https://github.com/bazelbuild/rules_apple/blob/master/doc/rules-watchos.md#watchos_framework)) that this target depends on. | List of labels | optional | `[]` | | infoplists | A list of .plist files that will be merged to form the Info.plist for this target. At least one file must be specified. Please see [Info.plist Handling](https://github.com/bazelbuild/rules_apple/blob/master/doc/common_info.md#infoplist-handling) for what is supported. | List of labels | required | | diff --git a/examples/watchos/HelloWorld/BUILD b/examples/watchos/HelloWorld/BUILD index 2d745d9c51..5952264c8b 100644 --- a/examples/watchos/HelloWorld/BUILD +++ b/examples/watchos/HelloWorld/BUILD @@ -90,7 +90,7 @@ watchos_application( name = "HelloWorld-WatchApplication", app_icons = ["//examples/resources:WatchAppIcon.xcassets"], bundle_id = "com.example.hello-world.watch", - extension = ":HelloWorld-WatchExtension", + extensions = [":HelloWorld-WatchExtension"], infoplists = [":WatchApp-Info.plist"], minimum_os_version = "4.0", storyboards = [ diff --git a/test/starlark_tests/targets_under_test/watchos/BUILD b/test/starlark_tests/targets_under_test/watchos/BUILD index d36319ee9a..f07749e44b 100644 --- a/test/starlark_tests/targets_under_test/watchos/BUILD +++ b/test/starlark_tests/targets_under_test/watchos/BUILD @@ -91,6 +91,28 @@ watchos_application( ], ) +watchos_application( + name = "single_target_app_with_extension", + app_icons = ["//test/starlark_tests/resources:WatchAppIcon.xcassets"], + bundle_id = "com.google.example", + entitlements = "//test/starlark_tests/resources:entitlements.plist", + extensions = [":watchos_app_extension"], + infoplists = [ + "//test/starlark_tests/resources:WatchosAppInfo.plist", + ], + minimum_os_version = common.min_os_watchos.single_target_app, + provisioning_profile = "//test/testdata/provisioning:integration_testing_ios.mobileprovision", + resources = [ + "//test/starlark_tests/resources:example_filegroup", + "//test/starlark_tests/resources:localization", + "//test/starlark_tests/resources:resource_bundle", + ], + tags = common.fixture_tags, + deps = [ + "//test/starlark_tests/resources:watchkit_single_target_app_main_lib", + ], +) + watchos_application( name = "app_with_ext_with_imported_fmwk", app_icons = ["//test/starlark_tests/resources:WatchAppIcon.xcassets"], @@ -507,6 +529,23 @@ ios_application( ], ) +ios_application( + name = "ios_watchos_with_watchos_extension_within_extensions", + bundle_id = "com.google", + bundle_name = "companion", + families = ["iphone"], + infoplists = [ + "//test/starlark_tests/resources:Info.plist", + ], + minimum_os_version = common.min_os_ios.arm_sim_support, + provisioning_profile = "//test/testdata/provisioning:integration_testing_ios.mobileprovision", + tags = common.fixture_tags, + watch_application = ":watchos_app_with_extension_within_extensions", + deps = [ + ":swift_lib", + ], +) + ios_application( name = "ios_watchos_with_watchos_extension_and_symbols_in_bundle", bundle_id = "com.google", @@ -572,6 +611,20 @@ watchos_application( tags = common.fixture_tags, ) +watchos_application( + name = "watchos_app_with_extension_within_extensions", + app_icons = ["//test/starlark_tests/resources:WatchAppIcon.xcassets"], + bundle_id = "com.google.example", + bundle_name = "app", + extensions = [":watchos_ext_with_extension"], + infoplists = [ + "//test/starlark_tests/resources:WatchosAppInfo.plist", + ], + minimum_os_version = common.min_os_watchos.baseline, + provisioning_profile = "//test/testdata/provisioning:integration_testing_ios.mobileprovision", + tags = common.fixture_tags, +) + watchos_application( name = "single_target_app_too_low_minos", app_icons = ["//test/starlark_tests/resources:WatchAppIcon.xcassets"], diff --git a/test/starlark_tests/watchos_application_tests.bzl b/test/starlark_tests/watchos_application_tests.bzl index 5af592b60d..988b8c50b8 100644 --- a/test/starlark_tests/watchos_application_tests.bzl +++ b/test/starlark_tests/watchos_application_tests.bzl @@ -177,6 +177,17 @@ def watchos_application_test_suite(name): tags = [name], ) + # Tests inclusion of extensions within Watch extensions if defined with `extensions` as opposed to `extension` + archive_contents_test( + name = "{}_contains_watchos_extension_extensions".format(name), + build_type = "device", + target_under_test = "//test/starlark_tests/targets_under_test/watchos:ios_watchos_with_watchos_extension_within_extensions", + contains = [ + "$BUNDLE_ROOT/Watch/app.app/PlugIns/ext.appex/PlugIns/watchos_app_extension.appex/watchos_app_extension", + ], + tags = [name], + ) + # Tests that the tsan support libraries are found in the app extension bundle of a watchOS app. archive_contents_test( name = "{}_contains_tsan_dylib_device_test".format(name), diff --git a/test/starlark_tests/watchos_single_target_application_tests.bzl b/test/starlark_tests/watchos_single_target_application_tests.bzl index 71c9c59ef9..1dc9b663c3 100644 --- a/test/starlark_tests/watchos_single_target_application_tests.bzl +++ b/test/starlark_tests/watchos_single_target_application_tests.bzl @@ -38,6 +38,10 @@ load( "//test/starlark_tests/rules:analysis_target_actions_test.bzl", "analysis_target_actions_test", ) +load( + "//test/starlark_tests/rules:apple_dsym_bundle_info_test.bzl", + "apple_dsym_bundle_info_test", +) def watchos_single_target_application_test_suite(name): """Test suite for watchos_single_target_application. @@ -90,6 +94,30 @@ delegate is referenced in the single-target `watchos_application`'s `deps`. ], ) + apple_verification_test( + name = "{}_entitlements_simulator_test".format(name), + build_type = "simulator", + target_under_test = "//test/starlark_tests/targets_under_test/watchos:single_target_app", + verifier_script = "verifier_scripts/entitlements_verifier.sh", + tags = [name], + ) + + apple_verification_test( + name = "{}_entitlements_device_test".format(name), + build_type = "device", + target_under_test = "//test/starlark_tests/targets_under_test/watchos:single_target_app", + verifier_script = "verifier_scripts/entitlements_verifier.sh", + tags = [name], + ) + + apple_dsym_bundle_info_test( + name = "{}_dsym_bundle_info_files_test".format(name), + target_under_test = "//test/starlark_tests/targets_under_test/watchos:single_target_app", + expected_direct_dsyms = ["dSYMs/single_target_app.app.dSYM"], + expected_transitive_dsyms = ["dSYMs/single_target_app.app.dSYM"], + tags = [name], + ) + infoplist_contents_test( name = "{}_plist_test".format(name), target_under_test = "//test/starlark_tests/targets_under_test/watchos:single_target_app", @@ -151,6 +179,19 @@ delegate is referenced in the single-target `watchos_application`'s `deps`. ], ) + archive_contents_test( + name = "{}_contains_extension_bundle_test".format(name), + build_type = "device", + contains = [ + "$BUNDLE_ROOT/single_target_app_with_extension", + "$BUNDLE_ROOT/PlugIns/watchos_app_extension.appex/watchos_app_extension", + ], + target_under_test = "//test/starlark_tests/targets_under_test/watchos:single_target_app_with_extension", + tags = [ + name, + ], + ) + infoplist_contents_test( name = "{}_capability_set_derived_bundle_id_plist_test".format(name), target_under_test = "//test/starlark_tests/targets_under_test/watchos:single_target_app_with_capability_set_derived_bundle_id",