diff --git a/Scripts/generate-xcodeproj.sh b/Scripts/generate-xcodeproj.sh index 289d9c1..d22b132 100755 --- a/Scripts/generate-xcodeproj.sh +++ b/Scripts/generate-xcodeproj.sh @@ -5,6 +5,7 @@ set -euo pipefail ROOT="$(cd "$(dirname "$0")/.." && pwd)" cd "$ROOT" +PBXPROJ="$ROOT/OSGKeyboard.xcodeproj/project.pbxproj" # XcodeGen requires configFiles listed in project.yml to exist on disk. # Signing.local.xcconfig is gitignored so each machine keeps its own team ID. @@ -21,4 +22,21 @@ if [[ ! -f "$SIGNING_LOCAL" ]]; then fi 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 diff --git a/Scripts/patch-icon-composer.sh b/Scripts/patch-icon-composer.sh index 657c64a..d5544f8 100755 --- a/Scripts/patch-icon-composer.sh +++ b/Scripts/patch-icon-composer.sh @@ -1,8 +1,6 @@ #!/usr/bin/env bash -# XcodeGen expands AppIcon.icon into a PBXGroup and adds icon.json / svg -# files to Copy Bundle Resources. Icon Composer bundles must be a single -# PBXFileReference (folder.iconcomposer.icon) linked to the target so actool -# compiles them together with Assets.xcassets. +# Fallback patch when XcodeGen does not emit folder.iconcomposer.icon for +# AppIcon.icon (older XcodeGen) or uses an unexpected PBX layout. set -euo pipefail ROOT="$(cd "$(dirname "$0")/.." && pwd)" @@ -25,45 +23,78 @@ import sys import uuid 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]) text = pbxproj.read_text() -if "/* AppIcon.icon in Resources */" in text and "folder.iconcomposer.icon" in text: - print("AppIcon.icon already patched") +if is_valid_icon_setup(text): + print(f"AppIcon.icon already configured (patch v{PATCH_VERSION})") sys.exit(0) -icon_ref_match = re.search( +icon_ref_match = None +for pattern in ( r"(?P[ \t]*)(?P[A-F0-9]{24}) /\* AppIcon\.icon \*/ = \{", - text, -) + r"(?P[ \t]*)(?P[A-F0-9]{24}) /\* OSGKeyboard/AppIcon\.icon \*/ = \{", +): + icon_ref_match = re.search(pattern, text) + if icon_ref_match: + break + if not icon_ref_match: - print("error: AppIcon.icon not found in project.pbxproj", file=sys.stderr) + for match in re.finditer( + r"(?P[ \t]*)(?P[A-F0-9]{24}) /\* (?P[^*]+) \*/ = \{", + 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) 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] - 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) +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" replacement = ( @@ -89,25 +120,24 @@ for line in lines: filtered.append(line) text = "".join(filtered) -build_uuid = uuid.uuid4().hex[:24].upper() -build_entry = ( - 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: +if not re.search(r"AppIcon\.icon in Resources", text): + build_uuid = uuid.uuid4().hex[:24].upper() + build_entry = ( + f"\t\t{build_uuid} /* AppIcon.icon in Resources */ = " + 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: resources_phase = re.search( r"\t\t(?P[A-F0-9]{24}) /\* Resources \*/ = \{\n" r"\t\t\tisa = PBXResourcesBuildPhase;\n" r"\t\t\tbuildActionMask = 2147483647;\n" r"\t\t\tfiles = \(\n" - r"(?P.*?Assets\.xcassets in Resources.*?\n)" + r"(?P.*? in Resources.*?\n)" r"(?P.*?)" r"\t\t\t\);\n" r"\t\t\trunOnlyForDeploymentPostprocessing = 0;\n" @@ -116,7 +146,10 @@ if f"{build_uuid} /* AppIcon.icon in Resources */," not in text: re.DOTALL, ) 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) 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) -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 diff --git a/project.yml b/project.yml index 5bc8b09..f687650 100644 --- a/project.yml +++ b/project.yml @@ -6,11 +6,8 @@ name: OSGKeyboard options: bundleIdPrefix: com.osgkeyboard - # Xcode 26 Icon Composer bundles must be treated as a single file, - # not expanded into icon.json + SVG children (see XcodeGen #1556). - fileTypes: - icon: - file: true + # XcodeGen 2.37+ emits AppIcon.icon as folder.iconcomposer.icon natively. + # Older XcodeGen releases may need Scripts/patch-icon-composer.sh (fallback). # Minimum OS: iOS 26. We dropped older iOS support so the # legacy speech + AVAudioSession branching could be removed in # favour of iOS 26's `SpeechAnalyzer` (always on-device) and @@ -58,7 +55,6 @@ targets: platform: iOS sources: - path: OSGKeyboard/AppIcon.icon - type: file buildPhase: resources - path: OSGKeyboard excludes: