fix: rely on XcodeGen native AppIcon.icon; harden patch fallback

Remove fileTypes icon override and explicit type:file so XcodeGen 2.37+
emits folder.iconcomposer.icon directly. Skip patch when native output is
valid; otherwise run patch v3 with broader PBX discovery and validation.

Fixes persistent 'AppIcon.icon entry has unexpected shape' on Xcode 26.

Co-authored-by: Rocky <hkgood@users.noreply.github.com>
This commit is contained in:
Cursor Agent
2026-07-03 08:53:28 +00:00
parent 7410627476
commit b3f68151d1
3 changed files with 101 additions and 46 deletions
+19 -1
View File
@@ -5,6 +5,7 @@ set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)" ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$ROOT" cd "$ROOT"
PBXPROJ="$ROOT/OSGKeyboard.xcodeproj/project.pbxproj"
# XcodeGen requires configFiles listed in project.yml to exist on disk. # XcodeGen requires configFiles listed in project.yml to exist on disk.
# Signing.local.xcconfig is gitignored so each machine keeps its own team ID. # Signing.local.xcconfig is gitignored so each machine keeps its own team ID.
@@ -21,4 +22,21 @@ if [[ ! -f "$SIGNING_LOCAL" ]]; then
fi fi
xcodegen generate xcodegen generate
"$ROOT/Scripts/patch-icon-composer.sh"
if python3 - "$PBXPROJ" <<'PY'
import re
import sys
from pathlib import Path
text = Path(sys.argv[1]).read_text()
has_icon_type = "folder.iconcomposer.icon" in text
has_icon_path = bool(re.search(r"path = (?:OSGKeyboard/)?AppIcon\.icon;", text))
has_resources = bool(re.search(r"AppIcon\.icon in Resources", text))
sys.exit(0 if has_icon_type and has_icon_path and has_resources else 1)
PY
then
echo "AppIcon.icon configured (XcodeGen native)"
else
echo "Applying AppIcon.icon compatibility patch..."
"$ROOT/Scripts/patch-icon-composer.sh"
fi
+80 -39
View File
@@ -1,8 +1,6 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# XcodeGen expands AppIcon.icon into a PBXGroup and adds icon.json / svg # Fallback patch when XcodeGen does not emit folder.iconcomposer.icon for
# files to Copy Bundle Resources. Icon Composer bundles must be a single # AppIcon.icon (older XcodeGen) or uses an unexpected PBX layout.
# PBXFileReference (folder.iconcomposer.icon) linked to the target so actool
# compiles them together with Assets.xcassets.
set -euo pipefail set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)" ROOT="$(cd "$(dirname "$0")/.." && pwd)"
@@ -25,45 +23,78 @@ import sys
import uuid import uuid
from pathlib import Path from pathlib import Path
PATCH_VERSION = 3
def extract_block(text: str, brace_open: int) -> tuple[str, int] | None:
depth = 0
for index in range(brace_open, len(text)):
char = text[index]
if char == "{":
depth += 1
elif char == "}":
depth -= 1
if depth == 0:
block_end = index + 1
if block_end < len(text) and text[block_end] == ";":
block_end += 1
return text[brace_open:block_end], block_end
return None
def is_valid_icon_setup(text: str) -> bool:
return (
"folder.iconcomposer.icon" in text
and bool(re.search(r"path = (?:OSGKeyboard/)?AppIcon\.icon;", text))
and bool(re.search(r"AppIcon\.icon in Resources", text))
)
pbxproj = Path(sys.argv[1]) pbxproj = Path(sys.argv[1])
text = pbxproj.read_text() text = pbxproj.read_text()
if "/* AppIcon.icon in Resources */" in text and "folder.iconcomposer.icon" in text: if is_valid_icon_setup(text):
print("AppIcon.icon already patched") print(f"AppIcon.icon already configured (patch v{PATCH_VERSION})")
sys.exit(0) sys.exit(0)
icon_ref_match = re.search( icon_ref_match = None
for pattern in (
r"(?P<indent>[ \t]*)(?P<uuid>[A-F0-9]{24}) /\* AppIcon\.icon \*/ = \{", r"(?P<indent>[ \t]*)(?P<uuid>[A-F0-9]{24}) /\* AppIcon\.icon \*/ = \{",
text, r"(?P<indent>[ \t]*)(?P<uuid>[A-F0-9]{24}) /\* OSGKeyboard/AppIcon\.icon \*/ = \{",
) ):
icon_ref_match = re.search(pattern, text)
if icon_ref_match:
break
if not icon_ref_match: if not icon_ref_match:
print("error: AppIcon.icon not found in project.pbxproj", file=sys.stderr) for match in re.finditer(
r"(?P<indent>[ \t]*)(?P<uuid>[A-F0-9]{24}) /\* (?P<comment>[^*]+) \*/ = \{",
text,
):
brace_open = text.find("{", match.end() - 1)
block = extract_block(text, brace_open)
if block and re.search(r"path = (?:OSGKeyboard/)?AppIcon\.icon;", block[0]):
icon_ref_match = match
break
if not icon_ref_match:
print(
f"error: AppIcon.icon not found in project.pbxproj (patch v{PATCH_VERSION}).\n"
"Run: git pull origin main && brew upgrade xcodegen\n"
"Then re-run ./Scripts/generate-xcodeproj.sh",
file=sys.stderr,
)
sys.exit(1) sys.exit(1)
icon_uuid = icon_ref_match.group("uuid") icon_uuid = icon_ref_match.group("uuid")
indent = icon_ref_match.group("indent") indent = icon_ref_match.group("indent")
block_start = icon_ref_match.start() block_start = icon_ref_match.start()
# Brace-match the PBX object so we tolerate XcodeGen format drift.
brace_open = text.find("{", icon_ref_match.end() - 1) brace_open = text.find("{", icon_ref_match.end() - 1)
depth = 0 parsed = extract_block(text, brace_open)
block_end = None if not parsed:
for index in range(brace_open, len(text)): print(
char = text[index] f"error: could not parse AppIcon.icon PBX block (patch v{PATCH_VERSION})",
if char == "{": file=sys.stderr,
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) sys.exit(1)
_, block_end = parsed
inner_indent = indent + "\t" inner_indent = indent + "\t"
replacement = ( replacement = (
@@ -89,25 +120,24 @@ for line in lines:
filtered.append(line) filtered.append(line)
text = "".join(filtered) text = "".join(filtered)
build_uuid = uuid.uuid4().hex[:24].upper() if not re.search(r"AppIcon\.icon in Resources", text):
build_entry = ( build_uuid = uuid.uuid4().hex[:24].upper()
f"\t\t{build_uuid} /* AppIcon.icon in Resources */ = " build_entry = (
f"{{isa = PBXBuildFile; fileRef = {icon_uuid} /* AppIcon.icon */; }};\n" f"\t\t{build_uuid} /* AppIcon.icon in Resources */ = "
) f"{{isa = PBXBuildFile; fileRef = {icon_uuid} /* AppIcon.icon */; }};\n"
if f"{build_uuid} /* AppIcon.icon in Resources */" not in text: )
text = text.replace( text = text.replace(
"/* Begin PBXBuildFile section */\n", "/* Begin PBXBuildFile section */\n",
"/* Begin PBXBuildFile section */\n" + build_entry, "/* Begin PBXBuildFile section */\n" + build_entry,
1, 1,
) )
if f"{build_uuid} /* AppIcon.icon in Resources */," not in text:
resources_phase = re.search( resources_phase = re.search(
r"\t\t(?P<phase_uuid>[A-F0-9]{24}) /\* Resources \*/ = \{\n" r"\t\t(?P<phase_uuid>[A-F0-9]{24}) /\* Resources \*/ = \{\n"
r"\t\t\tisa = PBXResourcesBuildPhase;\n" r"\t\t\tisa = PBXResourcesBuildPhase;\n"
r"\t\t\tbuildActionMask = 2147483647;\n" r"\t\t\tbuildActionMask = 2147483647;\n"
r"\t\t\tfiles = \(\n" r"\t\t\tfiles = \(\n"
r"(?P<body>.*?Assets\.xcassets in Resources.*?\n)" r"(?P<body>.*? in Resources.*?\n)"
r"(?P<rest>.*?)" r"(?P<rest>.*?)"
r"\t\t\t\);\n" r"\t\t\t\);\n"
r"\t\t\trunOnlyForDeploymentPostprocessing = 0;\n" r"\t\t\trunOnlyForDeploymentPostprocessing = 0;\n"
@@ -116,7 +146,10 @@ if f"{build_uuid} /* AppIcon.icon in Resources */," not in text:
re.DOTALL, re.DOTALL,
) )
if not resources_phase: if not resources_phase:
print("error: OSGKeyboard Resources phase not found", file=sys.stderr) print(
f"error: OSGKeyboard Resources phase not found (patch v{PATCH_VERSION})",
file=sys.stderr,
)
sys.exit(1) sys.exit(1)
insert_at = resources_phase.end("body") insert_at = resources_phase.end("body")
@@ -127,5 +160,13 @@ if f"{build_uuid} /* AppIcon.icon in Resources */," not in text:
) )
pbxproj.write_text(text) pbxproj.write_text(text)
print("Patched AppIcon.icon -> folder.iconcomposer.icon (target Resources)")
if not is_valid_icon_setup(pbxproj.read_text()):
print(
f"error: AppIcon.icon patch finished but validation failed (patch v{PATCH_VERSION})",
file=sys.stderr,
)
sys.exit(1)
print(f"Patched AppIcon.icon -> folder.iconcomposer.icon (patch v{PATCH_VERSION})")
PY PY
+2 -6
View File
@@ -6,11 +6,8 @@
name: OSGKeyboard name: OSGKeyboard
options: options:
bundleIdPrefix: com.osgkeyboard bundleIdPrefix: com.osgkeyboard
# Xcode 26 Icon Composer bundles must be treated as a single file, # XcodeGen 2.37+ emits AppIcon.icon as folder.iconcomposer.icon natively.
# not expanded into icon.json + SVG children (see XcodeGen #1556). # Older XcodeGen releases may need Scripts/patch-icon-composer.sh (fallback).
fileTypes:
icon:
file: true
# Minimum OS: iOS 26. We dropped older iOS support so the # Minimum OS: iOS 26. We dropped older iOS support so the
# legacy speech + AVAudioSession branching could be removed in # legacy speech + AVAudioSession branching could be removed in
# favour of iOS 26's `SpeechAnalyzer` (always on-device) and # favour of iOS 26's `SpeechAnalyzer` (always on-device) and
@@ -58,7 +55,6 @@ targets:
platform: iOS platform: iOS
sources: sources:
- path: OSGKeyboard/AppIcon.icon - path: OSGKeyboard/AppIcon.icon
type: file
buildPhase: resources buildPhase: resources
- path: OSGKeyboard - path: OSGKeyboard
excludes: excludes: