Skip to content
Draft
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
1 change: 1 addition & 0 deletions apple/internal/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -534,6 +534,7 @@ bzl_library(
":experimental",
":intermediates",
":outputs",
"//apple/internal/resource_actions:app_intents",
"//apple/internal/utils:bundle_paths",
"//apple/internal/utils:defines",
"@apple_support//lib:apple_support",
Expand Down
12 changes: 9 additions & 3 deletions apple/internal/codesigning_support.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -595,7 +595,8 @@ def _post_process_and_sign_archive_action(
process_and_sign_template,
provisioning_profile,
rule_descriptor,
signed_frameworks):
signed_frameworks,
ssu_training_commands = None):
"""Post-processes and signs an archived bundle.

Args:
Expand All @@ -621,6 +622,8 @@ def _post_process_and_sign_archive_action(
provisioning_profile: The provisioning profile file. May be `None`.
rule_descriptor: A rule descriptor for platform and product types from the rule context.
signed_frameworks: Depset containing each framework that has already been signed.
ssu_training_commands: Shell command lines that generate App Intents SSU (NL
training) assets on the assembled bundle before signing. May be `None`.
"""
input_files = [input_archive]
processing_tools = []
Expand Down Expand Up @@ -678,7 +681,7 @@ def _post_process_and_sign_archive_action(

# If there is no work to be done, skip the processing/signing action, just
# copy the file over.
has_work = any([signing_command_lines, ipa_post_processor_path, should_compress])
has_work = any([signing_command_lines, ipa_post_processor_path, should_compress, ssu_training_commands])
if not has_work:
actions.run_shell(
command = "cp -p '%s' '%s'" % (input_archive.path, output_archive.path),
Expand All @@ -701,6 +704,7 @@ def _post_process_and_sign_archive_action(
is_executable = True,
substitutions = {
"%ipa_post_processor%": shell.quote(ipa_post_processor_path) or "",
"%ssu_training_command_lines%": ssu_training_commands or "",
"%output_path%": shell.quote(output_archive.path),
"%should_compress%": "1" if should_compress else "",
"%should_verify%": "1", # always verify the crc
Expand All @@ -719,8 +723,10 @@ def _post_process_and_sign_archive_action(
arguments.append("should_process")
if should_compress:
arguments.append("should_compress")
if ssu_training_commands:
arguments.append("should_generate_ssu_assets")

run_on_darwin = any([signing_command_lines, ipa_post_processor_path])
run_on_darwin = any([signing_command_lines, ipa_post_processor_path, ssu_training_commands])
if run_on_darwin:
apple_support.run(
actions = actions,
Expand Down
1 change: 1 addition & 0 deletions apple/internal/ios_rules.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,7 @@ def _ios_application_impl(ctx):
),
partials.app_intents_metadata_bundle_partial(
actions = actions,
bundle_id = bundle_id,
cc_toolchains = cc_toolchain_forwarder,
deps = ctx.split_attr.app_intents,
label = label,
Expand Down
16 changes: 16 additions & 0 deletions apple/internal/partials/app_intents_metadata_bundle.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ load(
def _app_intents_metadata_bundle_partial_impl(
*,
actions,
bundle_id = None,
cc_toolchains,
deps,
label,
Expand Down Expand Up @@ -77,7 +78,16 @@ def _app_intents_metadata_bundle_partial_impl(
if str(platform_prerequisites.platform_type) == "macos":
bundle_location = processor.location.resource

# Request SSU (NL training) asset generation during bundling, mirroring Xcode's
# AppIntentsSSUTraining build phase. Requires the assembled bundle, so it is
# deferred to the bundling/signing actions rather than run here. Currently
# opt-in via the `apple.app_intents_ssu_training` feature while being validated.
app_intents_ssu_training = None
if bundle_id and "apple.app_intents_ssu_training" in platform_prerequisites.features:
app_intents_ssu_training = struct(bundle_id = bundle_id)

return struct(
app_intents_ssu_training = app_intents_ssu_training,
bundle_files = [(
bundle_location,
"Metadata.appintents",
Expand All @@ -88,6 +98,7 @@ def _app_intents_metadata_bundle_partial_impl(
def app_intents_metadata_bundle_partial(
*,
actions,
bundle_id = None,
cc_toolchains,
deps,
label,
Expand All @@ -99,6 +110,10 @@ def app_intents_metadata_bundle_partial(

Args:
actions: The actions provider from ctx.actions.
bundle_id: The bundle identifier of the bundle being processed. If set and the
`apple.app_intents_ssu_training` feature is enabled, SSU (NL training)
assets will also be generated for the bundle during the bundling/signing
actions.
cc_toolchains: Dictionary of CcToolchainInfo and ApplePlatformInfo providers under a split
transition to relay target platform information.
deps: Dictionary of targets under a split transition implementing the AppIntents protocol.
Expand All @@ -113,6 +128,7 @@ def app_intents_metadata_bundle_partial(
return partial.make(
_app_intents_metadata_bundle_partial_impl,
actions = actions,
bundle_id = bundle_id,
cc_toolchains = cc_toolchains,
deps = deps,
label = label,
Expand Down
29 changes: 28 additions & 1 deletion apple/internal/processor.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,10 @@ load(
"//apple/internal:outputs.bzl",
"outputs",
)
load(
"//apple/internal/resource_actions:app_intents.bzl",
"app_intents_ssu_training_commands",
)
load(
"//apple/internal/utils:bundle_paths.bzl",
"bundle_paths",
Expand Down Expand Up @@ -243,7 +247,8 @@ def _bundle_partial_outputs_files(
partial_outputs,
platform_prerequisites,
provisioning_profile,
rule_descriptor):
rule_descriptor,
ssu_training_commands = None):
"""Invokes bundletool to bundle the files specified by the partial outputs.

Args:
Expand All @@ -269,6 +274,9 @@ def _bundle_partial_outputs_files(
platform_prerequisites: Struct containing information on the platform being targeted.
provisioning_profile: File for the provisioning profile.
rule_descriptor: A rule descriptor for platform and product types from the rule context.
ssu_training_commands: When building tree artifact outputs, shell command lines
that generate App Intents SSU (NL training) assets on the assembled bundle
before it is signed. May be `None`.
"""

# Autotrim locales here only if the rule supports it and there weren't requested locales.
Expand Down Expand Up @@ -389,6 +397,7 @@ def _bundle_partial_outputs_files(
bundle_merge_zips = control_zips,
output = output_file.path,
code_signing_commands = codesigning_command or "",
ssu_training_commands = ssu_training_commands or "",
post_processor = post_processor_path,
)

Expand Down Expand Up @@ -530,6 +539,22 @@ def _bundle_post_process_and_sign(
signed_frameworks_depsets.append(partial_output.signed_frameworks)
transitive_signed_frameworks = depset(transitive = signed_frameworks_depsets)

# SSU (NL training) asset generation requested by the AppIntents metadata bundle
# partial. It requires the assembled bundle, so it runs within the bundling/signing
# actions, before codesigning.
app_intents_ssu_training = None
for partial_output in partial_outputs:
if getattr(partial_output, "app_intents_ssu_training", None):
app_intents_ssu_training = partial_output.app_intents_ssu_training

ssu_training_commands = None
if app_intents_ssu_training:
ssu_training_commands = app_intents_ssu_training_commands(
bundle_id = app_intents_ssu_training.bundle_id,
bundle_path = archive_paths[_LOCATION_ENUM.bundle],
resources_path = archive_paths[_LOCATION_ENUM.resource],
)

if tree_artifact_is_enabled:
extra_input_files = []

Expand Down Expand Up @@ -565,6 +590,7 @@ def _bundle_post_process_and_sign(
ipa_post_processor = ipa_post_processor,
label_name = rule_label.name,
locales_to_include = locales_to_include,
ssu_training_commands = ssu_training_commands,
output_discriminator = output_discriminator,
output_file = output_archive,
partial_outputs = partial_outputs,
Expand Down Expand Up @@ -621,6 +647,7 @@ def _bundle_post_process_and_sign(
input_archive = unprocessed_archive,
ipa_post_processor = ipa_post_processor,
label_name = rule_label.name,
ssu_training_commands = ssu_training_commands,
output_archive = output_archive,
output_archive_root_path = output_archive_root_path,
output_discriminator = output_discriminator,
Expand Down
2 changes: 2 additions & 0 deletions apple/internal/resource_actions/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,14 @@ bzl_library(
name = "app_intents",
srcs = ["app_intents.bzl"],
visibility = [
"//apple/internal:__pkg__",
"//apple/internal/partials:__pkg__",
],
deps = [
"//apple/internal:intermediates",
"//apple/internal:shared_environment",
"@apple_support//lib:apple_support",
"@bazel_skylib//lib:paths",
],
)

Expand Down
61 changes: 61 additions & 0 deletions apple/internal/resource_actions/app_intents.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"""AppIntents intents related actions."""

load("@apple_support//lib:apple_support.bzl", "apple_support")
load("@bazel_skylib//lib:paths.bzl", "paths")
load("//apple/internal:intermediates.bzl", "intermediates")
load("//apple/internal:shared_environment.bzl", "shared_environment")

Expand Down Expand Up @@ -186,3 +187,63 @@ fi
)

return output

def app_intents_ssu_training_commands(
*,
bundle_id,
contents_path,
resources_path):
"""Returns shell command lines that generate App Intents SSU (NL training) assets.

Mirrors Xcode's AppIntentsSSUTraining build phase, which runs
appintentsnltrainingprocessor on the assembled product to generate
Metadata.appintents/root.ssu.yaml and the per-locale <locale>.lproj/nlu.appintents
archives. Without these assets, Siri's assistant schema routing and App Shortcuts
utterances do not recognize the app.

The returned commands must be executed on an assembled (but not yet codesigned)
bundle, in an environment where $WORK_DIR points to the archive root and DEVELOPER_DIR
is set (i.e. within the bundling/signing actions), so that the generated assets are
sealed by the code signature.

Args:
bundle_id: The bundle identifier of the bundle being processed.
contents_path: Path to the bundle's contents directory, relative to the archive
root. Equal to the bundle root except on macOS, where it is `Contents`.
resources_path: Path to the bundle's resources directory, relative to the
archive root. Equal to the bundle root except on macOS, where it is
`Contents/Resources`.

Returns:
A string with the shell command lines to execute.
"""
contents_dir = paths.join("$WORK_DIR", contents_path) if contents_path else "$WORK_DIR"
product_dir = paths.join("$WORK_DIR", resources_path) if resources_path else "$WORK_DIR"
# The tool may report failures on its output while still exiting with 0 (e.g.
# "error: Could not archive SSU artifacts"), so inspect the output for errors
# like the AppIntentsMetadataProcessor action does.
return """\
if [[ -d "{product_dir}/Metadata.appintents" ]] && \\
xcrun --find appintentsnltrainingprocessor >/dev/null 2>&1; then
ssu_temp_dir="$(mktemp -d)"
chmod u+w "{product_dir}" "{product_dir}/Metadata.appintents"
find "{product_dir}" -maxdepth 1 -type d -name "*.lproj" -exec chmod u+w {{}} +
ssu_exit_status=0
ssu_output=$(xcrun appintentsnltrainingprocessor \\
--infoplist-path "{contents_dir}/Info.plist" \\
--temp-dir-path "$ssu_temp_dir" \\
--bundle-id "{bundle_id}" \\
--product-path "{product_dir}" \\
--extracted-metadata-path "{product_dir}/Metadata.appintents" \\
--archive-ssu-assets 2>&1) || ssu_exit_status=$?
rm -rf "$ssu_temp_dir"
if [[ "$ssu_exit_status" -ne 0 || "$ssu_output" == *error:* ]]; then
echo "$ssu_output" >&2
exit 1
fi
fi
""".format(
bundle_id = bundle_id,
contents_dir = contents_dir,
product_dir = product_dir,
)
32 changes: 32 additions & 0 deletions doc/common_info.md
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,38 @@ ios_unit_test(
)
```

### App Intents SSU Training Assets {#apple.app_intents_ssu_training}

Xcode runs an `AppIntentsSSUTraining` build phase after App Intents metadata
extraction, invoking `appintentsnltrainingprocessor` on the built product to
generate natural-language training assets: `Metadata.appintents/root.ssu.yaml`
and one compiled `<locale>.lproj/nlu.appintents` archive per localization.
Siri requires these assets to recognize App Shortcut phrases and, on iOS 26
and later, to route assistant schema requests (e.g.
`@AssistantIntent(schema: .media.playAudio)`) to the app. Without them, Siri
reports the app as not supporting the corresponding App Intents and falls back
to just launching it.

When this feature is enabled, applications that set `app_intents` generate
these assets during bundling, before codesigning:

```shell
bazel build --features=apple.app_intents_ssu_training //your/app
```

or on a per-target basis:

```bzl
ios_application(
...
app_intents = [":intents_lib"],
features = ["apple.app_intents_ssu_training"],
)
```

This step requires an Xcode toolchain that provides
`appintentsnltrainingprocessor`; it is skipped when the tool is not available.

### Codesigning performance

For larger applications, codesigning the final binary might be a
Expand Down
38 changes: 38 additions & 0 deletions tools/bundletool/bundletool_experimental.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@
contents should be placed.
code_signing_commands: An optional list of shell commands that should be
executed to sign the bundle.
ssu_training_commands: An optional string of shell command lines that generate
App Intents SSU (NL training) assets, executed after the bundle is
complete and post-processed but before it is signed.
output: The path to the directory (which will be created/cleared) that will
represent the complete bundle.
post_processor: The optional path to an executable that will be run after the
Expand Down Expand Up @@ -85,6 +88,9 @@ def _load_clonefile():

POST_PROCESSOR_ERROR_MSG_TEMPLATE = 'Post processor failed with exit code %d'

SSU_TRAINING_ERROR_MSG_TEMPLATE = (
'App Intents SSU training failed with exit code %d')

class BundleConflictError(ValueError):
"""Raised when two different files would be bundled in the same location."""

Expand Down Expand Up @@ -133,6 +139,15 @@ def __init__(self, exit_code):
POST_PROCESSOR_ERROR_MSG_TEMPLATE % exit_code)


class SSUTrainingError(EnvironmentError):
"""Raised if the App Intents SSU training commands fail."""

def __init__(self, exit_code):
self.exit_code = exit_code
EnvironmentError.__init__(self,
SSU_TRAINING_ERROR_MSG_TEMPLATE % exit_code)


class Bundler(object):
"""Implements the core functionality of the bundler."""

Expand Down Expand Up @@ -172,6 +187,10 @@ def run(self):
if post_processor:
self._post_process_bundle(output_path, post_processor)

ssu_training_commands = self._control.get('ssu_training_commands')
if ssu_training_commands:
self._generate_ssu_training_assets(output_path, ssu_training_commands)

code_signing_commands = self._control.get('code_signing_commands')
if code_signing_commands:
self._sign_bundle(output_path, code_signing_commands)
Expand Down Expand Up @@ -389,6 +408,25 @@ def _post_process_bundle(self, bundle_root, post_processor):
except subprocess.CalledProcessError as e:
raise PostProcessorError(e.returncode) from e

def _generate_ssu_training_assets(self, bundle_root, command_lines):
"""Executes the App Intents SSU training command lines on the bundle.

Args:
bundle_root: The path to the bundle.
command_lines: The shell command lines that should be executed on the
bundle to generate the SSU (NL training) assets. They reference the
bundle via a WORK_DIR environment variable and require the action's
environment (notably DEVELOPER_DIR) to locate the Xcode toolchain.
"""
env = dict(os.environ)
env['WORK_DIR'] = bundle_root
try:
subprocess.check_call(
['/bin/bash', '-e', '-u', '-o', 'pipefail', '-c', command_lines],
env=env)
except subprocess.CalledProcessError as e:
raise SSUTrainingError(e.returncode) from e

def _sign_bundle(self, bundle_root, command_lines):
"""Executes the signing command lines on the bundle.

Expand Down
Loading