Skip to content

Commit

Permalink
Fix app intents with Xcode 15.3+ (#2418)
Browse files Browse the repository at this point in the history
Depends on bazelbuild/rules_swift#1170

---------

Co-authored-by: Nicholas Levin <[email protected]>
Co-authored-by: Patrick Balestra <[email protected]>
Co-authored-by: Luis Padron <[email protected]>
  • Loading branch information
4 people authored Jul 10, 2024
1 parent d829d40 commit e3d630c
Show file tree
Hide file tree
Showing 10 changed files with 262 additions and 15 deletions.
4 changes: 4 additions & 0 deletions apple/internal/aspects/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -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",
],
)

Expand Down
30 changes: 29 additions & 1 deletion apple/internal/aspects/app_intents_aspect.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand All @@ -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.",
)
24 changes: 20 additions & 4 deletions apple/internal/partials/app_intents_metadata_bundle.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down Expand Up @@ -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.
Expand Down
11 changes: 9 additions & 2 deletions apple/internal/providers/app_intents_info.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -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+.""",
},
)
57 changes: 49 additions & 8 deletions apple/internal/resource_actions/app_intents.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand All @@ -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:
Expand All @@ -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,
Expand All @@ -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,
Expand Down
1 change: 1 addition & 0 deletions test/starlark_tests/common.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
43 changes: 43 additions & 0 deletions test/starlark_tests/ios_application_tests.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
8 changes: 8 additions & 0 deletions test/starlark_tests/resources/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
59 changes: 59 additions & 0 deletions test/starlark_tests/resources/widget_configuration_intent.swift
Original file line number Diff line number Diff line change
@@ -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!")
}
}
40 changes: 40 additions & 0 deletions test/starlark_tests/targets_under_test/ios/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down

0 comments on commit e3d630c

Please sign in to comment.