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:
@@ -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
|
||||||
|
|||||||
@@ -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,46 +23,79 @@ import sys
|
|||||||
import uuid
|
import uuid
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
pbxproj = Path(sys.argv[1])
|
PATCH_VERSION = 3
|
||||||
text = pbxproj.read_text()
|
|
||||||
|
|
||||||
if "/* AppIcon.icon in Resources */" in text and "folder.iconcomposer.icon" in text:
|
def extract_block(text: str, brace_open: int) -> tuple[str, int] | None:
|
||||||
print("AppIcon.icon already patched")
|
depth = 0
|
||||||
sys.exit(0)
|
for index in range(brace_open, len(text)):
|
||||||
|
|
||||||
icon_ref_match = re.search(
|
|
||||||
r"(?P<indent>[ \t]*)(?P<uuid>[A-F0-9]{24}) /\* AppIcon\.icon \*/ = \{",
|
|
||||||
text,
|
|
||||||
)
|
|
||||||
if not icon_ref_match:
|
|
||||||
print("error: AppIcon.icon not found in project.pbxproj", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
icon_uuid = icon_ref_match.group("uuid")
|
|
||||||
indent = icon_ref_match.group("indent")
|
|
||||||
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)
|
|
||||||
depth = 0
|
|
||||||
block_end = None
|
|
||||||
for index in range(brace_open, len(text)):
|
|
||||||
char = text[index]
|
char = text[index]
|
||||||
if char == "{":
|
if char == "{":
|
||||||
depth += 1
|
depth += 1
|
||||||
elif char == "}":
|
elif char == "}":
|
||||||
depth -= 1
|
depth -= 1
|
||||||
if depth == 0:
|
if depth == 0:
|
||||||
# Include trailing semicolon when present.
|
|
||||||
block_end = index + 1
|
block_end = index + 1
|
||||||
if block_end < len(text) and text[block_end] == ";":
|
if block_end < len(text) and text[block_end] == ";":
|
||||||
block_end += 1
|
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])
|
||||||
|
text = pbxproj.read_text()
|
||||||
|
|
||||||
|
if is_valid_icon_setup(text):
|
||||||
|
print(f"AppIcon.icon already configured (patch v{PATCH_VERSION})")
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
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}) /\* OSGKeyboard/AppIcon\.icon \*/ = \{",
|
||||||
|
):
|
||||||
|
icon_ref_match = re.search(pattern, text)
|
||||||
|
if icon_ref_match:
|
||||||
break
|
break
|
||||||
|
|
||||||
if block_end is None:
|
if not icon_ref_match:
|
||||||
print("error: could not parse AppIcon.icon PBX block", 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")
|
||||||
|
indent = icon_ref_match.group("indent")
|
||||||
|
block_start = icon_ref_match.start()
|
||||||
|
brace_open = text.find("{", icon_ref_match.end() - 1)
|
||||||
|
parsed = extract_block(text, brace_open)
|
||||||
|
if not parsed:
|
||||||
|
print(
|
||||||
|
f"error: could not parse AppIcon.icon PBX block (patch v{PATCH_VERSION})",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
_, block_end = parsed
|
||||||
|
|
||||||
inner_indent = indent + "\t"
|
inner_indent = indent + "\t"
|
||||||
replacement = (
|
replacement = (
|
||||||
f"{indent}{icon_uuid} /* AppIcon.icon */ = {{\n"
|
f"{indent}{icon_uuid} /* AppIcon.icon */ = {{\n"
|
||||||
@@ -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()
|
||||||
|
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"
|
||||||
)
|
)
|
||||||
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
@@ -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:
|
||||||
|
|||||||
Reference in New Issue
Block a user