diff --git a/apple/internal/aspects/BUILD b/apple/internal/aspects/BUILD index dcda2b61e7..ca4555cc99 100644 --- a/apple/internal/aspects/BUILD +++ b/apple/internal/aspects/BUILD @@ -16,6 +16,10 @@ bzl_library( deps = [ "//apple/internal:cc_info_support", "//apple/internal/providers:app_intents_info", + "@bazel_skylib//lib:collections", + "@build_bazel_apple_support//lib:apple_support", + "@build_bazel_rules_swift//swift", + "@build_bazel_rules_swift//swift:module_name", ], ) diff --git a/apple/internal/aspects/app_intents_aspect.bzl b/apple/internal/aspects/app_intents_aspect.bzl index 8b7f13fc9d..0f5e66477f 100644 --- a/apple/internal/aspects/app_intents_aspect.bzl +++ b/apple/internal/aspects/app_intents_aspect.bzl @@ -14,11 +14,27 @@ """Implementation of the aspect that propagates AppIntentsInfo providers.""" +load( + "@bazel_skylib//lib:collections.bzl", + "collections", +) +load( + "@build_bazel_apple_support//lib:apple_support.bzl", + "apple_support", +) +load("@build_bazel_rules_apple//apple/internal:cc_info_support.bzl", "cc_info_support") load( "@build_bazel_rules_apple//apple/internal/providers:app_intents_info.bzl", "AppIntentsInfo", ) -load("@build_bazel_rules_apple//apple/internal:cc_info_support.bzl", "cc_info_support") +load( + "@build_bazel_rules_swift//swift:module_name.bzl", + "derive_swift_module_name", +) +load( + "@build_bazel_rules_swift//swift:swift.bzl", + "SwiftInfo", +) def _app_intents_aspect_impl(target, ctx): """Implementation of the swift source files propation aspect.""" @@ -32,13 +48,25 @@ def _app_intents_aspect_impl(target, ctx): "Found the following SDK frameworks: %s" % sdk_frameworks.to_list(), ) + swiftconstvalues_files = [] + module_names = collections.uniq([x.name for x in target[SwiftInfo].direct_modules if x.swift]) + if not module_names: + module_names = [derive_swift_module_name(ctx.label)] + xcode_version_config = ctx.attr._xcode_config[apple_common.XcodeVersionConfig] + if xcode_version_config.xcode_version() >= apple_common.dotted_version("15.0"): + swiftconstvalues_files = target[OutputGroupInfo]["const_values"].to_list() + return [ AppIntentsInfo( + intent_module_names = module_names, swift_source_files = ctx.rule.files.srcs, + swiftconstvalues_files = swiftconstvalues_files, ), ] app_intents_aspect = aspect( implementation = _app_intents_aspect_impl, + # The only attrs required for this aspect are for the `xcode_version` >= 15.0 check above. + attrs = apple_support.action_required_attrs(), doc = "Collects Swift source files from swift_library targets required by AppIntents tooling.", ) diff --git a/apple/internal/partials/app_intents_metadata_bundle.bzl b/apple/internal/partials/app_intents_metadata_bundle.bzl index 008709210f..34f34deab0 100644 --- a/apple/internal/partials/app_intents_metadata_bundle.bzl +++ b/apple/internal/partials/app_intents_metadata_bundle.bzl @@ -88,15 +88,30 @@ def _app_intents_metadata_bundle_partial_impl( label.name + "_app_intents_stub_binary", ) + # Mirroring Xcode 15+ behavior, the metadata tool only looks at the first split for a given arch + # rather than every possible set of source files and inputs. Oddly, this only applies to the + # swift source files and the swiftconstvalues files; the triples and other files do cover all + # available archs. + first_cc_toolchain_key = cc_toolchains.keys()[0] + metadata_bundle = generate_app_intents_metadata_bundle( actions = actions, apple_fragment = platform_prerequisites.apple_fragment, bundle_binary = fat_stub_binary, + constvalues_files = [ + swiftconstvalues_file + for dep in deps[first_cc_toolchain_key] + for swiftconstvalues_file in dep[AppIntentsInfo].swiftconstvalues_files + ], + intents_module_names = [ + intent_module_name + for dep in deps[first_cc_toolchain_key] + for intent_module_name in dep[AppIntentsInfo].intent_module_names + ], label = label, source_files = [ swift_source_file - for split_deps in deps.values() - for dep in split_deps + for dep in deps[first_cc_toolchain_key] for swift_source_file in dep[AppIntentsInfo].swift_source_files ], target_triples = [ @@ -134,9 +149,10 @@ def app_intents_metadata_bundle_partial( Args: actions: The actions provider from ctx.actions. - cc_toolchains: Dictionary of CcToolchainInfo providers for current target splits. + cc_toolchains: Dictionary of CcToolchainInfo and ApplePlatformInfo providers under a split + transition to relay target platform information. ctx: The Starlark context for a rule target being built. - deps: List of dependencies implementing the AppIntents protocol. + deps: Dictionary of targets under a split transition implementing the AppIntents protocol. disabled_features: List of features to be disabled for C++ link actions. features: List of features to be enabled for C++ link actions. label: Label of the target being built. diff --git a/apple/internal/providers/app_intents_info.bzl b/apple/internal/providers/app_intents_info.bzl index a6758ba8c8..b72f0a8a2b 100644 --- a/apple/internal/providers/app_intents_info.bzl +++ b/apple/internal/providers/app_intents_info.bzl @@ -15,6 +15,13 @@ """AppIntentsInfo provider implementation for AppIntents support for Apple rules.""" AppIntentsInfo = provider( - doc = "Private provider to propagate `.swift` source files required by AppIntents processing.", - fields = ["swift_source_files"], + doc = "Private provider to propagate source files required by AppIntents processing.", + fields = { + "intent_module_names": """ +A List with the module names where App Intents are expected to be found.""", + "swift_source_files": """ +A List with the swift source Files to handle via app intents processing.""", + "swiftconstvalues_files": """ +A List with the swiftconstvalues Files to handle via app intents processing for Xcode 15+.""", + }, ) diff --git a/apple/internal/resource_actions/app_intents.bzl b/apple/internal/resource_actions/app_intents.bzl index cfe1dca2c5..eba3acdd8e 100644 --- a/apple/internal/resource_actions/app_intents.bzl +++ b/apple/internal/resource_actions/app_intents.bzl @@ -22,8 +22,10 @@ def generate_app_intents_metadata_bundle( actions, apple_fragment, bundle_binary, - source_files, + constvalues_files, + intents_module_names, label, + source_files, target_triples, xcode_version_config): """Process and generate AppIntents metadata bundle (Metadata.appintents). @@ -32,8 +34,12 @@ def generate_app_intents_metadata_bundle( actions: The actions provider from `ctx.actions`. apple_fragment: An Apple fragment (ctx.fragments.apple). bundle_binary: File referencing an application/extension/framework binary. - source_files: List of Swift source files implementing the AppIntents protocol. + constvalues_files: List of swiftconstvalues files generated from Swift source files + implementing the AppIntents protocol. + intents_module_names: List of Strings with the module names corresponding to the modules + found which have intents compiled. label: Label for the current target (`ctx.label`). + source_files: List of Swift source files implementing the AppIntents protocol. target_triples: List of Apple target triples from `CcToolchainInfo` providers. xcode_version_config: The `apple_common.XcodeVersionConfig` provider from the current ctx. Returns: @@ -52,14 +58,49 @@ def generate_app_intents_metadata_bundle( args.add("appintentsmetadataprocessor") args.add("--binary-file", bundle_binary) - args.add("--module-name", label.name) + + if len(intents_module_names) > 1: + fail(""" +Found the following module names in the top level target {label} for app_intents: {intents_module_names} + +App Intents must have only one module name for metadata generation to work correctly. +""".format( + intents_module_names = ", ".join(intents_module_names), + label = str(label), + )) + elif len(intents_module_names) == 0: + fail(""" +Could not find a module name for app_intents. One is required for App Intents metadata generation. +""") + + args.add("--module-name", intents_module_names[0]) args.add("--output", output.dirname) - args.add_all("--source-files", source_files) + args.add_all( + source_files, + before_each = "--source-files", + ) + transitive_inputs = [depset(source_files)] + args.add("--sdk-root", apple_support.path_placeholders.sdkroot()) args.add_all(target_triples, before_each = "--target-triple") if xcode_version_config.xcode_version() >= apple_common.dotted_version("15.0"): - # TODO(b/295227222): Generate app intents metadata with --compile-time-extraction using - # .swiftconstvals instead of --legacy-extraction at the earliest convenience. - args.add("--legacy-extraction") + args.add_all( + constvalues_files, + before_each = "--swift-const-vals", + ) + transitive_inputs.append(depset(constvalues_files)) + args.add("--compile-time-extraction") + if xcode_version_config.xcode_version() >= apple_common.dotted_version("15.3"): + # Read the build version from the fourth component of the Xcode version. + xcode_version_split = str(xcode_version_config.xcode_version()).split(".") + if len(xcode_version_split) < 4: + fail("""\ +Internal Error: Expected xcode_config to report the Xcode version with the build version as the \ +fourth component of the full version string, but instead found {xcode_version_string}. Please file \ +an issue with the Apple BUILD rules with repro steps. +""".format( + xcode_version_string = str(xcode_version_config.xcode_version()), + )) + args.add("--xcode-version", xcode_version_split[3]) apple_support.run_shell( actions = actions, @@ -82,7 +123,7 @@ elif [[ "$output" == *"skipping writing output"* ]]; then exit 1 fi ''', - inputs = depset([bundle_binary], transitive = [depset(source_files)]), + inputs = depset([bundle_binary], transitive = transitive_inputs), outputs = [output], mnemonic = "AppIntentsMetadataProcessor", xcode_config = xcode_version_config, diff --git a/test/starlark_tests/common.bzl b/test/starlark_tests/common.bzl index 7746b53e87..061eef7eb4 100644 --- a/test/starlark_tests/common.bzl +++ b/test/starlark_tests/common.bzl @@ -37,6 +37,7 @@ _min_os_ios = struct( oldest_supported = "11.0", nplus1 = "13.0", stable_swift_abi = "12.2", + widget_configuration_intents_support = "16.0", ) _min_os_macos = struct( diff --git a/test/starlark_tests/ios_application_tests.bzl b/test/starlark_tests/ios_application_tests.bzl index e53c7df0a0..0ef7ee3d8f 100644 --- a/test/starlark_tests/ios_application_tests.bzl +++ b/test/starlark_tests/ios_application_tests.bzl @@ -779,6 +779,49 @@ def ios_application_test_suite(name): tags = [name], ) + # Test app with a Widget Configuration Intent with a computed property generates and bundles Metadata.appintents bundle. + archive_contents_test( + name = "{}_with_widget_configuration_intent_contains_app_intents_metadata_bundle_test".format(name), + build_type = "simulator", + target_under_test = "//test/starlark_tests/targets_under_test/ios:app_with_widget_configuration_intent", + contains = [ + "$BUNDLE_ROOT/Metadata.appintents/extract.actionsdata", + "$BUNDLE_ROOT/Metadata.appintents/version.json", + ], + tags = [ + name, + ], + ) + + # Test app that has two Intents defined as top level modules generates an error message. + analysis_failure_message_test( + name = "{}_with_two_app_intents_and_two_modules_fails".format(name), + target_under_test = "//test/starlark_tests/targets_under_test/ios:app_with_app_intent_and_widget_configuration_intent", + expected_error = ( + "App Intents must have only one module name for metadata generation to work correctly." + ).format( + package = "//test/starlark_tests/targets_under_test/ios", + ), + tags = [ + name, + ], + ) + + # Test app with App Intents generates and bundles Metadata.appintents bundle for fat binaries. + archive_contents_test( + name = "{}_fat_build_contains_app_intents_metadata_bundle_test".format(name), + build_type = "simulator", + cpus = { + "ios_multi_cpus": ["x86_64", "sim_arm64"], + }, + target_under_test = "//test/starlark_tests/targets_under_test/ios:app_with_app_intents", + contains = [ + "$BUNDLE_ROOT/Metadata.appintents/extract.actionsdata", + "$BUNDLE_ROOT/Metadata.appintents/version.json", + ], + tags = [name], + ) + # Test Metadata.appintents bundle contents for simulator and device. archive_contents_test( name = "{}_metadata_appintents_bundle_contents_for_simulator_test".format(name), diff --git a/test/starlark_tests/resources/BUILD b/test/starlark_tests/resources/BUILD index 23a6263819..16e5f9fae7 100644 --- a/test/starlark_tests/resources/BUILD +++ b/test/starlark_tests/resources/BUILD @@ -1118,6 +1118,14 @@ swift_library( tags = common.fixture_tags, ) +swift_library( + name = "widget_configuration_intent", + srcs = ["widget_configuration_intent.swift"], + linkopts = ["-Wl,-framework,AppIntents"], + module_name = "WidgetConfigurationIntent", + tags = common.fixture_tags, +) + # -------------------------------------------------------------------------------- # C/C++ rules with data diff --git a/test/starlark_tests/resources/widget_configuration_intent.swift b/test/starlark_tests/resources/widget_configuration_intent.swift new file mode 100644 index 0000000000..e8ed01be95 --- /dev/null +++ b/test/starlark_tests/resources/widget_configuration_intent.swift @@ -0,0 +1,59 @@ +// Copyright 2023 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import AppIntents + +enum RefreshInterval: String, AppEnum { + case hourly, daily, weekly + + static var typeDisplayRepresentation: TypeDisplayRepresentation = "Refresh Interval" + static var caseDisplayRepresentations: [RefreshInterval: DisplayRepresentation] = [ + .hourly: "Every Hour", + .daily: "Every Day", + .weekly: "Every Week", + ] +} + +struct FavoriteSoup: WidgetConfigurationIntent { + static var title: LocalizedStringResource = "Favorite Soup" + static var description = IntentDescription("Shows a picture of your favorite soup!") + + @Parameter(title: "Soup") + var name: String? + + @Parameter(title: "Shuffle", default: true) + var shuffle: Bool + + @Parameter(title: "Refresh", default: .daily) + var interval: RefreshInterval + + static var parameterSummary: some ParameterSummary { + When(\.$shuffle, .equalTo, true) { + Summary { + \.$name + \.$shuffle + \.$interval + } + } otherwise: { + Summary { + \.$name + \.$shuffle + } + } + } + + func perform() async throws -> some IntentResult & ProvidesDialog { + return .result(dialog: "This is an intent with a computed property!") + } +} diff --git a/test/starlark_tests/targets_under_test/ios/BUILD b/test/starlark_tests/targets_under_test/ios/BUILD index f397b2615b..1bc2399e35 100644 --- a/test/starlark_tests/targets_under_test/ios/BUILD +++ b/test/starlark_tests/targets_under_test/ios/BUILD @@ -4478,6 +4478,46 @@ ios_application( ], ) +ios_application( + name = "app_with_widget_configuration_intent", + app_intents = [ + "//test/starlark_tests/resources:widget_configuration_intent", + ], + bundle_id = "com.google.example", + families = ["iphone"], + infoplists = [ + "//test/starlark_tests/resources:Info.plist", + ], + minimum_os_version = common.min_os_ios.widget_configuration_intents_support, + provisioning_profile = "//test/testdata/provisioning:integration_testing_ios.mobileprovision", + tags = common.fixture_tags, + deps = [ + "//test/starlark_tests/resources:swift_uikit_appdelegate", + "//test/starlark_tests/resources:widget_configuration_intent", + ], +) + +ios_application( + name = "app_with_app_intent_and_widget_configuration_intent", + app_intents = [ + "//test/starlark_tests/resources:app_intent", + "//test/starlark_tests/resources:widget_configuration_intent", + ], + bundle_id = "com.google.example", + families = ["iphone"], + infoplists = [ + "//test/starlark_tests/resources:Info.plist", + ], + minimum_os_version = common.min_os_ios.widget_configuration_intents_support, + provisioning_profile = "//test/testdata/provisioning:integration_testing_ios.mobileprovision", + tags = common.fixture_tags, + deps = [ + "//test/starlark_tests/resources:app_intent", + "//test/starlark_tests/resources:swift_uikit_appdelegate", + "//test/starlark_tests/resources:widget_configuration_intent", + ], +) + # --------------------------------------------------------------------------------------- # Targets for base_bundle_id and bundle_id flows with and without shared capabilities.