fix: harden AppIcon.icon patch for newer XcodeGen layouts

Brace-match the AppIcon.icon PBX object instead of relying on fixed
regex shapes. Fixes 'AppIcon.icon entry has unexpected shape' when
XcodeGen/Xcode 26 emits a different PBXFileReference format.

Co-authored-by: Rocky <hkgood@users.noreply.github.com>
This commit is contained in:
Cursor Agent
2026-07-03 08:50:43 +00:00
parent e57e88a2fe
commit 7c83b351aa
+67 -74
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# XcodeGen 2.43 expands AppIcon.icon into a PBXGroup and adds icon.json / svg # XcodeGen expands AppIcon.icon into a PBXGroup and adds icon.json / svg
# files to Copy Bundle Resources. Icon Composer bundles must be a single # files to Copy Bundle Resources. Icon Composer bundles must be a single
# PBXFileReference (folder.iconcomposer.icon) linked to the target so actool # PBXFileReference (folder.iconcomposer.icon) linked to the target so actool
# compiles them together with Assets.xcassets. # compiles them together with Assets.xcassets.
@@ -32,62 +32,49 @@ if "/* AppIcon.icon in Resources */" in text and "folder.iconcomposer.icon" in t
print("AppIcon.icon already patched") print("AppIcon.icon already patched")
sys.exit(0) sys.exit(0)
icon_uuid_match = re.search( icon_ref_match = re.search(
r"([A-F0-9]{24}) /\* AppIcon\.icon \*/ = \{", r"(?P<indent>[ \t]*)(?P<uuid>[A-F0-9]{24}) /\* AppIcon\.icon \*/ = \{",
text, text,
) )
if not icon_uuid_match: if not icon_ref_match:
print("error: AppIcon.icon not found in project.pbxproj", file=sys.stderr) print("error: AppIcon.icon not found in project.pbxproj", file=sys.stderr)
sys.exit(1) sys.exit(1)
icon_uuid = icon_uuid_match.group(1)
group_pattern = re.compile( icon_uuid = icon_ref_match.group("uuid")
rf"(?P<indent>\t\t){icon_uuid} /\* AppIcon\.icon \*/ = \{{\n" indent = icon_ref_match.group("indent")
r"\t\t\tisa = PBXGroup;\n" block_start = icon_ref_match.start()
r"\t\t\tchildren = \(\n"
r"(?:\t\t\t\t[A-F0-9]{24} /\* .* \*/,\n)*" # Brace-match the PBX object so we tolerate XcodeGen format drift.
r"\t\t\t\);\n" brace_open = text.find("{", icon_ref_match.end() - 1)
r"\t\t\tpath = AppIcon\.icon;\n" depth = 0
r"\t\t\tsourceTree = \"<group>\";\n" block_end = None
r"\t\t\};", for index in range(brace_open, len(text)):
re.MULTILINE, char = text[index]
if char == "{":
depth += 1
elif char == "}":
depth -= 1
if depth == 0:
# Include trailing semicolon when present.
block_end = index + 1
if block_end < len(text) and text[block_end] == ";":
block_end += 1
break
if block_end is None:
print("error: could not parse AppIcon.icon PBX block", file=sys.stderr)
sys.exit(1)
inner_indent = indent + "\t"
replacement = (
f"{indent}{icon_uuid} /* AppIcon.icon */ = {{\n"
f"{inner_indent}isa = PBXFileReference;\n"
f"{inner_indent}lastKnownFileType = folder.iconcomposer.icon;\n"
f"{inner_indent}path = AppIcon.icon;\n"
f"{inner_indent}sourceTree = \"<group>\";\n"
f"{indent}}};"
) )
text = text[:block_start] + replacement + text[block_end:]
group_match = group_pattern.search(text)
if group_match:
indent = group_match.group("indent")
replacement = (
f"{indent}{icon_uuid} /* AppIcon.icon */ = {{\n"
f"\t\t\tisa = PBXFileReference;\n"
f"\t\t\tlastKnownFileType = folder.iconcomposer.icon;\n"
f"\t\t\tpath = AppIcon.icon;\n"
f"\t\t\tsourceTree = \"<group>\";\n"
f"\t\t}};"
)
text = text[: group_match.start()] + replacement + text[group_match.end() :]
else:
file_ref_pattern = re.compile(
rf"\t\t{icon_uuid} /\* AppIcon\.icon \*/ = \{{\n"
r"\t\t\tisa = PBXFileReference;\n"
r"\t\t\tlastKnownFileType = [^;]+;\n"
r"\t\t\tpath = AppIcon\.icon;\n"
r"\t\t\tsourceTree = \"<group>\";\n"
r"\t\t\};",
re.MULTILINE,
)
file_ref_match = file_ref_pattern.search(text)
if not file_ref_match:
print("error: AppIcon.icon entry has unexpected shape", file=sys.stderr)
sys.exit(1)
replacement = (
f"\t\t{icon_uuid} /* AppIcon.icon */ = {{\n"
f"\t\t\tisa = PBXFileReference;\n"
f"\t\t\tlastKnownFileType = folder.iconcomposer.icon;\n"
f"\t\t\tpath = AppIcon.icon;\n"
f"\t\t\tsourceTree = \"<group>\";\n"
f"\t\t}};"
)
text = text[: file_ref_match.start()] + replacement + text[file_ref_match.end() :]
nested_resource_names = ("icon.json", "App Icon Template.svg") nested_resource_names = ("icon.json", "App Icon Template.svg")
lines = text.splitlines(keepends=True) lines = text.splitlines(keepends=True)
@@ -107,31 +94,37 @@ build_entry = (
f"\t\t{build_uuid} /* AppIcon.icon in Resources */ = " f"\t\t{build_uuid} /* AppIcon.icon in Resources */ = "
f"{{isa = PBXBuildFile; fileRef = {icon_uuid} /* AppIcon.icon */; }};\n" f"{{isa = PBXBuildFile; fileRef = {icon_uuid} /* AppIcon.icon */; }};\n"
) )
text = text.replace("/* Begin PBXBuildFile section */\n", "/* Begin PBXBuildFile section */\n" + build_entry, 1) if f"{build_uuid} /* AppIcon.icon in Resources */" not in text:
text = text.replace(
"/* Begin PBXBuildFile section */\n",
"/* Begin PBXBuildFile section */\n" + build_entry,
1,
)
resources_phase = re.search( if f"{build_uuid} /* AppIcon.icon in Resources */," not in text:
r"\t\t(?P<phase_uuid>[A-F0-9]{24}) /\* Resources \*/ = \{\n" resources_phase = re.search(
r"\t\t\tisa = PBXResourcesBuildPhase;\n" r"\t\t(?P<phase_uuid>[A-F0-9]{24}) /\* Resources \*/ = \{\n"
r"\t\t\tbuildActionMask = 2147483647;\n" r"\t\t\tisa = PBXResourcesBuildPhase;\n"
r"\t\t\tfiles = \(\n" r"\t\t\tbuildActionMask = 2147483647;\n"
r"(?P<body>.*?Assets\.xcassets in Resources.*?\n)" r"\t\t\tfiles = \(\n"
r"(?P<rest>.*?)" r"(?P<body>.*?Assets\.xcassets in Resources.*?\n)"
r"\t\t\t\);\n" r"(?P<rest>.*?)"
r"\t\t\trunOnlyForDeploymentPostprocessing = 0;\n" r"\t\t\t\);\n"
r"\t\t\};", r"\t\t\trunOnlyForDeploymentPostprocessing = 0;\n"
text, r"\t\t\};",
re.DOTALL, text,
) re.DOTALL,
if not resources_phase: )
print("error: OSGKeyboard Resources phase not found", file=sys.stderr) if not resources_phase:
sys.exit(1) print("error: OSGKeyboard Resources phase not found", file=sys.stderr)
sys.exit(1)
insert_at = resources_phase.end("body") insert_at = resources_phase.end("body")
text = ( text = (
text[:insert_at] text[:insert_at]
+ f"\t\t\t\t{build_uuid} /* AppIcon.icon in Resources */,\n" + f"\t\t\t\t{build_uuid} /* AppIcon.icon in Resources */,\n"
+ text[insert_at:] + text[insert_at:]
) )
pbxproj.write_text(text) pbxproj.write_text(text)
print("Patched AppIcon.icon -> folder.iconcomposer.icon (target Resources)") print("Patched AppIcon.icon -> folder.iconcomposer.icon (target Resources)")