Skip to content

Commit

Permalink
Add support for alternate app icons in UIKit via actool in Xcode 14+ …
Browse files Browse the repository at this point in the history
…(first introduced in 13).

PiperOrigin-RevId: 578548155
  • Loading branch information
nglevin authored and swiple-rules-gardener committed Nov 1, 2023
1 parent e8e7a0c commit 6fc1952
Show file tree
Hide file tree
Showing 15 changed files with 209 additions and 11 deletions.
2 changes: 2 additions & 0 deletions apple/internal/ios_rules.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,7 @@ def _ios_application_impl(ctx):
bundle_verification_targets = bundle_verification_targets,
environment_plist = ctx.file._environment_plist,
platform_prerequisites = platform_prerequisites,
primary_icon_name = ctx.attr.primary_app_icon,
resource_deps = resource_deps,
rule_descriptor = rule_descriptor,
rule_label = label,
Expand Down Expand Up @@ -1954,6 +1955,7 @@ ios_application = rule_factory.create_apple_rule(
rule_attrs.app_icon_attrs(
icon_extension = ".appiconset",
icon_parent_extension = ".xcassets",
supports_alternate_icons = True,
),
rule_attrs.app_intents_attrs(
deps_cfg = transition_support.apple_platform_split_transition,
Expand Down
6 changes: 6 additions & 0 deletions apple/internal/partials/resources.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,7 @@ def _resources_partial_impl(
mac_exec_group,
output_discriminator,
platform_prerequisites,
primary_icon_name,
resource_deps,
rule_descriptor,
rule_label,
Expand Down Expand Up @@ -432,6 +433,7 @@ def _resources_partial_impl(
"output_discriminator": output_discriminator,
"parent_dir": parent_dir,
"platform_prerequisites": platform_prerequisites,
"primary_icon_name": primary_icon_name,
"product_type": rule_descriptor.product_type,
"rule_label": rule_label,
}
Expand Down Expand Up @@ -525,6 +527,7 @@ def resources_partial(
mac_exec_group,
output_discriminator = None,
platform_prerequisites,
primary_icon_name = None,
resource_deps,
rule_descriptor,
rule_label,
Expand Down Expand Up @@ -564,6 +567,8 @@ def resources_partial(
output_discriminator: A string to differentiate between different target intermediate files
or `None`.
platform_prerequisites: Struct containing information on the platform being targeted.
primary_icon_name: An optional String to identify the name of the primary app icon when
alternate app icons have been provided for the app.
resource_deps: A list of dependencies that the resource aspect has been applied to.
rule_descriptor: A rule descriptor for platform and product types from the rule context.
rule_label: The label of the target being analyzed.
Expand Down Expand Up @@ -595,6 +600,7 @@ def resources_partial(
mac_exec_group = mac_exec_group,
output_discriminator = output_discriminator,
platform_prerequisites = platform_prerequisites,
primary_icon_name = primary_icon_name,
resource_deps = resource_deps,
rule_descriptor = rule_descriptor,
rule_label = rule_label,
Expand Down
3 changes: 2 additions & 1 deletion apple/internal/partials/support/resources_support.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ def _asset_catalogs(
output_discriminator,
parent_dir,
platform_prerequisites,
primary_icon_name,
product_type,
rule_label,
**_kwargs):
Expand All @@ -178,7 +179,6 @@ def _asset_catalogs(
assets_plist = None
infoplists = []
if not parent_dir:
# TODO(kaipi): Merge this into the top level Info.plist.
assets_plist_path = paths.join(parent_dir or "", "xcassets-info.plist")
assets_plist = intermediates.file(
actions = actions,
Expand All @@ -203,6 +203,7 @@ def _asset_catalogs(
output_dir = assets_dir,
output_plist = assets_plist,
platform_prerequisites = platform_prerequisites,
primary_icon_name = primary_icon_name,
product_type = product_type,
resolved_xctoolrunner = apple_mac_toolchain_info.resolved_xctoolrunner,
)
Expand Down
1 change: 1 addition & 0 deletions apple/internal/resource_actions/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ bzl_library(
"//apple/internal/utils:xctoolrunner",
"@bazel_skylib//lib:collections",
"@bazel_skylib//lib:paths",
"@bazel_skylib//lib:sets",
"@build_bazel_apple_support//lib:apple_support",
],
)
Expand Down
64 changes: 58 additions & 6 deletions apple/internal/resource_actions/actool.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ load(
"@bazel_skylib//lib:paths.bzl",
"paths",
)
load(
"@bazel_skylib//lib:sets.bzl",
"sets",
)
load(
"@build_bazel_apple_support//lib:apple_support.bzl",
"apple_support",
Expand All @@ -46,6 +50,7 @@ def _actool_args_for_special_file_types(
asset_files,
bundle_id,
platform_prerequisites,
primary_icon_name,
product_type):
"""Returns command line arguments needed to compile special assets.
Expand All @@ -59,6 +64,8 @@ def _actool_args_for_special_file_types(
asset_files: The asset catalog files.
bundle_id: The bundle ID to configure for this target.
platform_prerequisites: Struct containing information on the platform being targeted.
primary_icon_name: An optional String to identify the name of the primary app icon when
alternate app icons have been provided for the app.
product_type: The product type identifier used to describe the current bundle type.
Returns:
Expand Down Expand Up @@ -116,14 +123,55 @@ def _actool_args_for_special_file_types(
[appicon_extension],
attr = "app_icons",
).keys()
if len(icon_dirs) != 1:
if len(icon_dirs) != 1 and not primary_icon_name:
formatted_dirs = "[\n %s\n]" % ",\n ".join(icon_dirs)
fail("The asset catalogs should contain exactly one directory named " +
"*.%s among its asset catalogs, " % appicon_extension +
"but found the following: " + formatted_dirs, "app_icons")

app_icon_name = paths.split_extension(paths.basename(icon_dirs[0]))[0]
args += ["--app-icon", app_icon_name]
# Alternate icons are only supported for UIKit applications on iOS, tvOS, visionOS and
# iOS-on-macOS (Catalyst)
if (platform_prerequisites.platform_type == apple_common.platform_type.watchos or
platform_prerequisites.platform_type == apple_common.platform_type.macos or
product_type != apple_product_type.application):
fail("The asset catalogs should contain exactly one directory named " +
"*.%s among its asset catalogs, " % appicon_extension +
"but found the following: " + formatted_dirs, "app_icons")
else:
fail("""
Found multiple app icons among the asset catalogs with no primary_app_icon assigned.
If you intend to assign multiple app icons to this target, please declare which of these is intended
to be the primary app icon with the primary_app_icon attribute on the rule itself.
app_icons was assigned the following: {formatted_dirs}
""".format(formatted_dirs = formatted_dirs))
elif primary_icon_name:
# Check that primary_icon_name matches one of the icon sets, then add actool arguments
# for `--alternate-app-icon` and `--app_icon` as appropriate. These do NOT overlap.
app_icon_names = sets.make()
for icon_dir in icon_dirs:
app_icon_names = sets.insert(
app_icon_names,
paths.split_extension(paths.basename(icon_dir))[0],
)
app_icon_name_list = sets.to_list(app_icon_names)
found_primary = False
for app_icon_name in app_icon_name_list:
if app_icon_name == primary_icon_name:
found_primary = True
args += ["--app-icon", primary_icon_name]
else:
args += ["--alternate-app-icon", app_icon_name]
if not found_primary:
fail("""
Could not find the primary icon named "{primary_icon_name}" in the list of app_icons provided.
Found the following icon names from those provided: {app_icon_names}.
""".format(
primary_icon_name = primary_icon_name,
app_icon_names = ", ".join(app_icon_name_list),
))
else:
app_icon_name = paths.split_extension(paths.basename(icon_dirs[0]))[0]
args += ["--app-icon", app_icon_name]

# Add arguments for launch images, if there are any.
launch_image_files = [f for f in asset_files if ".launchimage/" in f.path]
Expand Down Expand Up @@ -155,6 +203,7 @@ def compile_asset_catalog(
output_dir,
output_plist,
platform_prerequisites,
primary_icon_name,
product_type,
resolved_xctoolrunner):
"""Creates an action that compiles asset catalogs.
Expand All @@ -177,6 +226,8 @@ def compile_asset_catalog(
output_plist: The file reference for the output plist that should be merged
into Info.plist. May be None if the output plist is not desired.
platform_prerequisites: Struct containing information on the platform being targeted.
primary_icon_name: An optional String to identify the name of the primary app icon when
alternate app icons have been provided for the app.
product_type: The product type identifier used to describe the current bundle type.
resolved_xctoolrunner: A struct referencing the resolved wrapper for "xcrun" tools.
"""
Expand All @@ -198,6 +249,7 @@ def compile_asset_catalog(
asset_files = asset_files,
bundle_id = bundle_id,
platform_prerequisites = platform_prerequisites,
primary_icon_name = primary_icon_name,
product_type = product_type,
))
args.extend(collections.before_each(
Expand Down
20 changes: 18 additions & 2 deletions apple/internal/rule_attrs.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -611,16 +611,22 @@ hermetic given these inputs to ensure that the result can be safely cached.
),
}

def _app_icon_attrs(*, icon_extension = ".appiconset", icon_parent_extension = ".xcassets"):
def _app_icon_attrs(
*,
icon_extension = ".appiconset",
icon_parent_extension = ".xcassets",
supports_alternate_icons = False):
"""Returns the attribute required to define app icons for the given target.
Args:
icon_extension: A String representing the extension required of the directory containing the
app icon assets. Optional. Defaults to `.appiconset`.
icon_parent_extension: A String representing the extension required of the parent directory
of the directory containing the app icon assets. Optional. Defaults to `.xcassets`.
supports_alternate_icons: Bool representing if the rule supports alternate icons. False by
default.
"""
return {
app_icon_attrs = {
"app_icons": attr.label_list(
allow_files = True,
doc = """
Expand All @@ -632,6 +638,16 @@ named `*.{app_icon_parent_extension}/*.{app_icon_extension}` and there may be on
),
),
}
if supports_alternate_icons:
app_icon_attrs = dicts.add(app_icon_attrs, {
"primary_app_icon": attr.string(
doc = """
An optional String to identify the name of the primary app icon when alternate app icons have been
provided for the app.
""",
),
})
return app_icon_attrs

def _launch_images_attrs():
"""Returns the attribute required to support launch images for a given target."""
Expand Down
5 changes: 4 additions & 1 deletion apple/internal/tvos_rules.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,7 @@ def _tvos_application_impl(ctx):
environment_plist = ctx.file._environment_plist,
mac_exec_group = mac_exec_group,
platform_prerequisites = platform_prerequisites,
primary_icon_name = ctx.attr.primary_app_icon,
resource_deps = resource_deps,
rule_descriptor = rule_descriptor,
rule_label = label,
Expand Down Expand Up @@ -1089,7 +1090,9 @@ tvos_application = rule_factory.create_apple_rule(
predeclared_outputs = {"archive": "%{name}.ipa"},
attrs = [
apple_support.platform_constraint_attrs(),
rule_attrs.app_icon_attrs(),
rule_attrs.app_icon_attrs(
supports_alternate_icons = True,
),
rule_attrs.app_intents_attrs(
deps_cfg = transition_support.apple_platform_split_transition,
),
Expand Down
5 changes: 4 additions & 1 deletion apple/internal/visionos_rules.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,7 @@ Resolved Xcode is version {xcode_version}.
environment_plist = ctx.file._environment_plist,
mac_exec_group = mac_exec_group,
platform_prerequisites = platform_prerequisites,
primary_icon_name = ctx.attr.primary_app_icon,
resource_deps = resource_deps,
rule_descriptor = rule_descriptor,
rule_label = label,
Expand Down Expand Up @@ -379,7 +380,9 @@ visionos_application = rule_factory.create_apple_rule(
predeclared_outputs = {"archive": "%{name}.zip"},
attrs = [
apple_support.platform_constraint_attrs(),
rule_attrs.app_icon_attrs(),
rule_attrs.app_icon_attrs(
supports_alternate_icons = True,
),
rule_attrs.binary_linking_attrs(
deps_cfg = transition_support.apple_platform_split_transition,
extra_deps_aspects = [
Expand Down
41 changes: 41 additions & 0 deletions test/starlark_tests/ios_application_resources_test.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,47 @@ def ios_application_resources_test_suite(name):
tags = [name],
)

# Test that when alternate app icons are declared alongside the primary app icon, that they are
# bundled in expected locations with the app, that they are embedded within the plist
# referencing their file names, and that they are also bundled within the asset catalog for the
# application.
archive_contents_test(
name = "{}_alt_app_icons_test".format(name),
build_type = "device",
target_under_test = "//test/starlark_tests/targets_under_test/ios:app_with_alternate_app_icons",
contains = [
"$BUNDLE_ROOT/[email protected]",
"$BUNDLE_ROOT/app_icon76x76@2x~ipad.png",
"$BUNDLE_ROOT/[email protected]",
"$BUNDLE_ROOT/app_icon-bazel76x76@2x~ipad.png",
],
plist_test_file = "$CONTENT_ROOT/Info.plist",
plist_test_values = {
"CFBundleIcons:CFBundlePrimaryIcon:CFBundleIconFiles:0": "app_icon",
"CFBundleIcons:CFBundleAlternateIcons:CFBundleIconFiles:0": "app_icon-bazel",
},
text_test_file = "$BUNDLE_ROOT/Assets.car",
text_test_values = ["app_icon", "app_icon-bazel"],
tags = [name],
)

analysis_failure_message_test(
name = "{}_alt_app_icons_missing_primary_icon_name_test".format(name),
target_under_test = "//test/starlark_tests/targets_under_test/ios:app_with_alternate_app_icons_without_primary",
expected_error = """
Found multiple app icons among the asset catalogs with no primary_app_icon assigned.
If you intend to assign multiple app icons to this target, please declare which of these is intended
to be the primary app icon with the primary_app_icon attribute on the rule itself.
app_icons was assigned the following: [
third_party/bazel_rules/rules_apple/test/starlark_tests/resources/app_icons_with_alts_ios.xcassets/app_icon-bazel.appiconset,
third_party/bazel_rules/rules_apple/test/starlark_tests/resources/app_icons_with_alts_ios.xcassets/app_icon.appiconset
]
""",
tags = [name],
)

# Tests that apple_bundle_import files are bundled correctly with the application.
archive_contents_test(
name = "{}_apple_bundle_test".format(name),
Expand Down
5 changes: 5 additions & 0 deletions test/starlark_tests/resources/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,11 @@ filegroup(
srcs = glob(["app_icons_ios.xcassets/**"]),
)

filegroup(
name = "app_icons_with_alts_ios",
srcs = glob(["app_icons_with_alts_ios.xcassets/**"]),
)

filegroup(
name = "launch_images_ios",
srcs = glob(["launch_images_ios.xcassets/**"]),
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"images" : [
{
"filename" : "Bazel_logo.png",
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"images" : [
{
"filename" : "app_icon.png",
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

1 comment on commit 6fc1952

@brentleyjones
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.