Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix app intents with Xcode 15.3+ #2418

Merged
merged 7 commits into from
Jul 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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