Skip to content

Commit d1711fb

Browse files
authored
support icon composer app icons for xcode 26+ (#2733)
1 parent 433db70 commit d1711fb

47 files changed

Lines changed: 542 additions & 60 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apple/internal/bundling_support.bzl

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -248,10 +248,19 @@ See https://github.com/bazelbuild/rules_apple/blob/master/doc/shared_capabilitie
248248
suffix_default = suffix_default,
249249
)
250250

251-
def _ensure_single_xcassets_type(*, attr, extension, files, message = None):
251+
def _ensure_single_xcassets_type(
252+
*,
253+
additional_path_fragments = [],
254+
attr,
255+
extension,
256+
files,
257+
message = None):
252258
"""Helper for when an xcassets catalog should have a single sub type.
253259
254260
Args:
261+
additional_path_fragments: Additional path fragments to check for in the path (in order), used
262+
to handle actool inputs that don't use the `xcassets` directory, such as Xcode 26 `icon`
263+
bundles generated by the Icon Composer tool.
255264
attr: The attribute to associate with the build failure if the list of
256265
files has an element that is not in a directory with the given
257266
extension.
@@ -267,7 +276,7 @@ def _ensure_single_xcassets_type(*, attr, extension, files, message = None):
267276
_ensure_path_format(
268277
attr = attr,
269278
files = files,
270-
path_fragments_list = [["xcassets", extension]],
279+
path_fragments_list = [["xcassets", extension], additional_path_fragments],
271280
message = message,
272281
)
273282

apple/internal/partials/app_assets_validation.bzl

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,15 @@ def _app_assets_validation_partial_impl(
8585
message = message,
8686
)
8787
else:
88+
# For Xcode 26, appiconset-s can live alongside iOS/macOS/watchOS .icon bundle contents
89+
# to ensure rendering is correct for pre-26 Apple OSes, so we need to make that
90+
# exception here.
91+
additional_path_fragments = []
92+
xcode_version = platform_prerequisites.xcode_version_config.xcode_version()
93+
if xcode_version >= apple_common.dotted_version("26.0"):
94+
additional_path_fragments.append("icon")
8895
bundling_support.ensure_single_xcassets_type(
96+
additional_path_fragments = additional_path_fragments,
8997
attr = "app_icons",
9098
extension = "appiconset",
9199
files = app_icons,

apple/internal/providers.bzl

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -427,14 +427,15 @@ AppleResourceInfo, new_appleresourceinfo = provider(
427427
"asset_catalogs": "Resources that need to be embedded into Assets.car.",
428428
"datamodels": "Datamodel files.",
429429
"framework": "Apple framework bundle from `ios_framework` and `tvos_framework` targets.",
430-
"infoplists": """Plist files to be merged and processed. Plist files that should not be
431-
merged into the root Info.plist should be propagated in `plists`. Because of this, infoplists should
432-
only be bucketed with the `bucketize_typed` method.""",
430+
"infoplists": """Plist files to be merged and processed. Plist files that should not be \
431+
merged into the root Info.plist should be propagated in `plists`. Because of this, infoplists \
432+
should only be bucketed with the `bucketize_typed` method.""",
433433
"metals": """Metal Shading Language source files to be compiled into a single .metallib file
434434
and bundled at the top level.""",
435435
"mlmodels": "Core ML model files that should be processed and bundled at the top level.",
436436
"plists": "Resource Plist files that should not be merged into Info.plist",
437-
"pngs": "PNG images which are not bundled in an .xcassets folder.",
437+
"pngs": """PNG images which are not bundled in an .xcassets folder or an .icon folder in \
438+
Xcode 26+.""",
438439
"processed": "Typed resources that have already been processed.",
439440
"storyboards": "Storyboard files.",
440441
"strings": "Localization strings files.",

apple/internal/resource_actions/actool.bzl

Lines changed: 76 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ load(
4949

5050
_supports_visionos = hasattr(apple_common.platform_type, "visionos")
5151

52+
# TODO: b/425967223 - Rework the validation to better account for Xcode 26, and make it a bit more
53+
# readable via helper functions as we add more conditions.
54+
5255
def _actool_args_for_special_file_types(
5356
*,
5457
asset_files,
@@ -77,6 +80,13 @@ def _actool_args_for_special_file_types(
7780
"""
7881
args = []
7982

83+
is_xcode_26_or_later = (
84+
platform_prerequisites.xcode_version_config.xcode_version() >=
85+
apple_common.dotted_version("26.0")
86+
)
87+
icon_files = []
88+
icon_bundle_files = []
89+
8090
if product_type in (
8191
apple_product_type.messages_extension,
8292
apple_product_type.messages_sticker_pack_extension,
@@ -121,27 +131,72 @@ def _actool_args_for_special_file_types(
121131
appicon_extension = "solidimagestack"
122132
icon_files = [f for f in asset_files if ".solidimagestack/" in f.path]
123133
else:
134+
icon_bundle_files = [f for f in asset_files if ".icon/" in f.path]
124135
appicon_extension = "appiconset"
125136
icon_files = [f for f in asset_files if ".appiconset/" in f.path]
126137

138+
if not is_xcode_26_or_later and len(icon_bundle_files):
139+
fail("""\
140+
Found Icon Composer .icon bundles among the assigned app_icons. These are only supported \
141+
on Xcode 26 or later.""")
142+
127143
# Add arguments for app icons, if there are any.
128-
if icon_files:
144+
if icon_files or icon_bundle_files:
129145
icon_dirs = group_files_by_directory(
130146
icon_files,
131147
[appicon_extension],
132148
attr = "app_icons",
133149
).keys()
134-
if len(icon_dirs) != 1 and not primary_icon_name:
150+
has_exactly_one_icon_dir = False
151+
152+
if is_xcode_26_or_later:
153+
icon_bundle_dirs = group_files_by_directory(
154+
icon_bundle_files,
155+
["icon"],
156+
attr = "app_icons",
157+
).keys()
158+
159+
if len(icon_dirs + icon_bundle_dirs) == 1:
160+
has_exactly_one_icon_dir = True
161+
elif len(icon_dirs) == 1 and len(icon_bundle_dirs) == 1:
162+
# Carve out; the AppIcon and Icon bundles can be used together to support Apple OSes
163+
# prior to 26 and the new Apple OS 26 icon features for iOS/macOS/watchOS as long as
164+
# their names match perfectly.
165+
icon_paths_to_compare = [icon_dirs[0], icon_bundle_dirs[0]]
166+
unique_icon_names = dict()
167+
for icon_path in icon_paths_to_compare:
168+
unique_icon_names[paths.split_extension(paths.basename(icon_path))[0]] = True
169+
if len(unique_icon_names) == 1:
170+
has_exactly_one_icon_dir = True
171+
icon_dirs.extend(icon_bundle_dirs)
172+
173+
elif len(icon_dirs) == 1:
174+
has_exactly_one_icon_dir = True
175+
176+
if not has_exactly_one_icon_dir and not primary_icon_name:
135177
formatted_dirs = "[\n %s\n]" % ",\n ".join(icon_dirs)
136178

137179
# Alternate icons are only supported for UIKit applications on iOS, tvOS, visionOS and
138180
# iOS-on-macOS (Catalyst)
139181
if (platform_prerequisites.platform_type == apple_common.platform_type.watchos or
140182
platform_prerequisites.platform_type == apple_common.platform_type.macos or
141183
product_type != apple_product_type.application):
142-
fail("The asset catalogs should contain exactly one directory named " +
143-
"*.%s among its asset catalogs, " % appicon_extension +
144-
"but found the following: " + formatted_dirs, "app_icons")
184+
xcode_26_workaround_message = ""
185+
if is_xcode_26_or_later:
186+
xcode_26_workaround_message = (
187+
"which can be accompanied by exactly one Icon Composer .icon bundle of " +
188+
"the same name, "
189+
)
190+
fail("""
191+
The asset catalogs should contain exactly one directory named *.{appicon_extension} among its \
192+
asset catalogs, \
193+
{xcode_26_workaround_message}\
194+
but found the following: \
195+
{formatted_dirs}""".format(
196+
appicon_extension = appicon_extension,
197+
formatted_dirs = formatted_dirs,
198+
xcode_26_workaround_message = xcode_26_workaround_message,
199+
), "app_icons")
145200
else:
146201
fail("""
147202
Found multiple app icons among the asset catalogs with no primary_app_icon assigned.
@@ -279,13 +334,27 @@ def compile_asset_catalog(
279334
"actool",
280335
"--compile",
281336
xctoolrunner_support.prefixed_path(output_dir.path),
337+
"--errors",
338+
"--warnings",
339+
"--notices",
340+
"--output-format",
341+
"human-readable-text",
282342
"--platform",
283343
actool_platform,
284344
"--minimum-deployment-target",
285345
platform_prerequisites.minimum_os,
286346
"--compress-pngs",
287347
]
288348

349+
xcode_config = platform_prerequisites.xcode_version_config
350+
351+
if platform_prerequisites.platform_type == "macos" and (
352+
xcode_config.xcode_version() >= apple_common.dotted_version("26.0")
353+
):
354+
# Required for the Icon Composer .icon bundles to work as inputs, even though it's not
355+
# documented. Xcode 26 currently relies on this flag to be set.
356+
args.extend(["--lightweight-asset-runtime-mode", "enabled"])
357+
289358
args.extend(_actool_args_for_special_file_types(
290359
asset_files = asset_files,
291360
bundle_id = bundle_id,
@@ -321,7 +390,7 @@ def compile_asset_catalog(
321390

322391
xcassets = group_files_by_directory(
323392
asset_files,
324-
["xcassets", "xcstickers"],
393+
["icon", "xcassets", "xcstickers"],
325394
attr = "asset_catalogs",
326395
).keys()
327396

@@ -336,7 +405,7 @@ def compile_asset_catalog(
336405
inputs = asset_files,
337406
mnemonic = "AssetCatalogCompile",
338407
outputs = actool_outputs,
339-
xcode_config = platform_prerequisites.xcode_version_config,
408+
xcode_config = xcode_config,
340409
)
341410

342411
if alternate_icons:

apple/internal/resource_actions/ibtool.bzl

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,12 @@ def compile_storyboard(
7777
"ibtool",
7878
"--compilation-directory",
7979
xctoolrunner_support.prefixed_path(output_dir.dirname),
80+
"--errors",
81+
"--warnings",
82+
"--notices",
83+
"--auto-activate-custom-fonts",
84+
"--output-format",
85+
"human-readable-text",
8086
]
8187

8288
min_os = platform_prerequisites.minimum_os

apple/internal/resources.bzl

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -258,7 +258,9 @@ def _bucketize_data(
258258
resource_swift_module = swift_module
259259
elif ".alticon/" in resource_short_path:
260260
bucket_name = "asset_catalogs"
261-
elif ".xcassets/" in resource_short_path or ".xcstickers/" in resource_short_path:
261+
elif (".icon/" in resource_short_path or
262+
".xcassets/" in resource_short_path or
263+
".xcstickers/" in resource_short_path):
262264
bucket_name = "asset_catalogs"
263265
elif ".xcdatamodel" in resource_short_path or ".xcmappingmodel/" in resource_short_path:
264266
bucket_name = "datamodels"

apple/internal/rule_attrs.bzl

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -623,7 +623,9 @@ def _app_icon_attrs(
623623
doc = """
624624
Files that comprise the app icons for the application. Each file must have a containing directory
625625
named `*.{app_icon_parent_extension}/*.{app_icon_extension}` and there may be only one such
626-
`.{app_icon_extension}` directory in the list.""".format(
626+
`.{app_icon_extension}` directory in the list. In Xcode 26+ for iOS/macOS/watchOS, an `*.icon`
627+
bundle can be provided along with the `*.{app_icon_parent_extension}` bundle to support 26 and
628+
pre-26 Apple OS rendering.""".format(
627629
app_icon_extension = icon_extension,
628630
app_icon_parent_extension = icon_parent_extension,
629631
),
@@ -634,8 +636,12 @@ named `*.{app_icon_parent_extension}/*.{app_icon_extension}` and there may be on
634636
"primary_app_icon": attr.string(
635637
doc = """
636638
An optional String to identify the name of the primary app icon when alternate app icons have been
637-
provided for the app.
638-
""",
639+
provided for the app. This should match both the `*.icon` bundle in iOS/macOS/watchOS 26+ and the
640+
`*.{app_icon_parent_extension}/.appiconset` bundle's AppIcon resource in previous versions of
641+
iOS/macOS/watchOS.
642+
""".format(
643+
app_icon_parent_extension = icon_parent_extension,
644+
),
639645
),
640646
})
641647
return app_icon_attrs

doc/providers.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -494,7 +494,7 @@ Provider that propagates buckets of resources that are differentiated by type.
494494
| <a id="AppleResourceInfo-metals"></a>metals | Metal Shading Language source files to be compiled into a single .metallib file and bundled at the top level. |
495495
| <a id="AppleResourceInfo-mlmodels"></a>mlmodels | Core ML model files that should be processed and bundled at the top level. |
496496
| <a id="AppleResourceInfo-plists"></a>plists | Resource Plist files that should not be merged into Info.plist |
497-
| <a id="AppleResourceInfo-pngs"></a>pngs | PNG images which are not bundled in an .xcassets folder. |
497+
| <a id="AppleResourceInfo-pngs"></a>pngs | PNG images which are not bundled in an .xcassets folder or an .icon folder in Xcode 26+. |
498498
| <a id="AppleResourceInfo-processed"></a>processed | Typed resources that have already been processed. |
499499
| <a id="AppleResourceInfo-storyboards"></a>storyboards | Storyboard files. |
500500
| <a id="AppleResourceInfo-strings"></a>strings | Localization strings files. |

0 commit comments

Comments
 (0)