From c015ad3b963db89039b949bbdda7908d07246106 Mon Sep 17 00:00:00 2001 From: smithemely <144165634+smithemely@users.noreply.github.com> Date: Tue, 22 Apr 2025 20:26:51 +0300 Subject: [PATCH 1/2] fix(ios): improve bundle identifier detection and replacement (#86) * fix: handle both quoted and unquoted bundle identifiers * feat: implement smarter base identifier detection logic * fix: properly preserve extensions with multiple segments --- lib/platforms/ios.dart | 118 ++++++++++++++++++++++++++++++++--------- 1 file changed, 92 insertions(+), 26 deletions(-) diff --git a/lib/platforms/ios.dart b/lib/platforms/ios.dart index 1914109..7f2b009 100644 --- a/lib/platforms/ios.dart +++ b/lib/platforms/ios.dart @@ -107,34 +107,100 @@ void _setIOSPackageName(dynamic packageName) { } final iosProjectString = iosProjectFile.readAsStringSync(); - final newBundleIDIOSProjectString = iosProjectString - // Replaces old bundle id from - // `PRODUCT_BUNDLE_IDENTIFIER = {{BUNDLE_ID}};` - .replaceAll( - RegExp( - r'PRODUCT_BUNDLE_IDENTIFIER = ([A-Za-z0-9.-_]+)(? m.group(1)!) + .toList(); + + if (bundleIdentifierMatches.isEmpty) { + throw Exception('No bundle identifiers found in project file'); + } + + // Find all unique base identifiers (without extensions) + // We'll consider identifiers that are substrings of others as potential base identifiers + final baseIdentifierCandidates = []; + + for (final identifier in bundleIdentifierMatches) { + bool isBaseForOthers = false; + for (final other in bundleIdentifierMatches) { + if (identifier != other && other.startsWith(identifier)) { + isBaseForOthers = true; + break; + } + } + + if (isBaseForOthers) { + baseIdentifierCandidates.add(identifier); + } + } + + // If we couldn't find any base identifiers, use the shortest one as fallback + String baseIdentifier; + if (baseIdentifierCandidates.isEmpty) { + // If there are no base candidates, it means all identifiers are unique + // Or there's only one identifier. Use the shortest as the base. + baseIdentifier = bundleIdentifierMatches + .reduce((a, b) => a.length <= b.length ? a : b); + } else { + // Use the shortest base candidate + baseIdentifier = baseIdentifierCandidates + .reduce((a, b) => a.length <= b.length ? a : b); + } + + // Create a map of all identifiers to their new values + final identifierReplacements = {}; + + for (final identifier in bundleIdentifierMatches) { + if (identifier == baseIdentifier) { + identifierReplacements[identifier] = packageName; + } else if (identifier.startsWith(baseIdentifier)) { + // This is an extension + final extension = identifier.substring(baseIdentifier.length); + identifierReplacements[identifier] = '$packageName$extension'; + } else { + // This is an unrelated identifier, skip it + _logger.w('Skipping unrelated identifier: $identifier'); + } + } + + // Apply all replacements to the project file + var newIosProjectString = iosProjectString; + + identifierReplacements.forEach((oldId, newId) { + // Replace unquoted format + newIosProjectString = newIosProjectString.replaceAll( + 'PRODUCT_BUNDLE_IDENTIFIER = $oldId;', + 'PRODUCT_BUNDLE_IDENTIFIER = $newId;', + ); + + // Replace quoted format + newIosProjectString = newIosProjectString.replaceAll( + 'PRODUCT_BUNDLE_IDENTIFIER = "$oldId";', + 'PRODUCT_BUNDLE_IDENTIFIER = "$newId";', + ); + }); + + // Special case for RunnerTests which might not be caught by the pattern above + if (identifierReplacements.containsKey(baseIdentifier)) { + newIosProjectString = newIosProjectString.replaceAll( + RegExp('PRODUCT_BUNDLE_IDENTIFIER = "$baseIdentifier\\.RunnerTests";'), + 'PRODUCT_BUNDLE_IDENTIFIER = "$packageName.RunnerTests";', + ); + + newIosProjectString = newIosProjectString.replaceAll( + RegExp('PRODUCT_BUNDLE_IDENTIFIER = $baseIdentifier\\.RunnerTests;'), + 'PRODUCT_BUNDLE_IDENTIFIER = $packageName.RunnerTests;', + ); + } + + iosProjectFile.writeAsStringSync(newIosProjectString); _logger.i('iOS bundle identifier set to: `$packageName` (project.pbxproj)'); } on _PackageRenameException catch (e) { From da2452c6d0b91144952e5eed32a9cd33799b0116 Mon Sep 17 00:00:00 2001 From: smithemely <144165634+smithemely@users.noreply.github.com> Date: Sun, 7 Sep 2025 22:23:08 +0300 Subject: [PATCH 2/2] refactor(ios): simplify and fix bundle identifier replacement logic * fix: use RegExp.escape to safely handle special characters * perf: deduplicate identifiers upfront with Set * feat: smart base detection by counting extensions * fix: correct character class with dash at end [A-Za-z0-9._-] * refactor: single replaceAllMapped for all identifier patterns --- lib/platforms/ios.dart | 102 +++++++++++++---------------------------- 1 file changed, 31 insertions(+), 71 deletions(-) diff --git a/lib/platforms/ios.dart b/lib/platforms/ios.dart index 7f2b009..739f394 100644 --- a/lib/platforms/ios.dart +++ b/lib/platforms/ios.dart @@ -108,97 +108,57 @@ void _setIOSPackageName(dynamic packageName) { final iosProjectString = iosProjectFile.readAsStringSync(); - // Extract all bundle identifiers, accounting for both quoted and unquoted formats + // Extract all bundle identifiers, using only allowed characters final bundleIdRegex = RegExp( - r'PRODUCT_BUNDLE_IDENTIFIER = "?([^";]+)"?;', - multiLine: true, + r'PRODUCT_BUNDLE_IDENTIFIER = "?([A-Za-z0-9._-]+)"?;', ); final bundleIdentifierMatches = bundleIdRegex .allMatches(iosProjectString) .map((m) => m.group(1)!) - .toList(); + .toSet(); if (bundleIdentifierMatches.isEmpty) { throw Exception('No bundle identifiers found in project file'); } - // Find all unique base identifiers (without extensions) - // We'll consider identifiers that are substrings of others as potential base identifiers - final baseIdentifierCandidates = []; + // Find the base identifier by counting extensions + String? baseIdentifier; + int maxExtensions = 0; for (final identifier in bundleIdentifierMatches) { - bool isBaseForOthers = false; + int extensionCount = 0; + for (final other in bundleIdentifierMatches) { - if (identifier != other && other.startsWith(identifier)) { - isBaseForOthers = true; - break; + // Check if 'other' extends 'identifier' with a dot separator + if (identifier != other && other.startsWith('$identifier.')) { + extensionCount++; } } - if (isBaseForOthers) { - baseIdentifierCandidates.add(identifier); + // Use the identifier that has the most extensions + if (extensionCount > maxExtensions) { + maxExtensions = extensionCount; + baseIdentifier = identifier; } } - // If we couldn't find any base identifiers, use the shortest one as fallback - String baseIdentifier; - if (baseIdentifierCandidates.isEmpty) { - // If there are no base candidates, it means all identifiers are unique - // Or there's only one identifier. Use the shortest as the base. - baseIdentifier = bundleIdentifierMatches - .reduce((a, b) => a.length <= b.length ? a : b); - } else { - // Use the shortest base candidate - baseIdentifier = baseIdentifierCandidates - .reduce((a, b) => a.length <= b.length ? a : b); - } - - // Create a map of all identifiers to their new values - final identifierReplacements = {}; - - for (final identifier in bundleIdentifierMatches) { - if (identifier == baseIdentifier) { - identifierReplacements[identifier] = packageName; - } else if (identifier.startsWith(baseIdentifier)) { - // This is an extension - final extension = identifier.substring(baseIdentifier.length); - identifierReplacements[identifier] = '$packageName$extension'; - } else { - // This is an unrelated identifier, skip it - _logger.w('Skipping unrelated identifier: $identifier'); - } - } - - // Apply all replacements to the project file - var newIosProjectString = iosProjectString; - - identifierReplacements.forEach((oldId, newId) { - // Replace unquoted format - newIosProjectString = newIosProjectString.replaceAll( - 'PRODUCT_BUNDLE_IDENTIFIER = $oldId;', - 'PRODUCT_BUNDLE_IDENTIFIER = $newId;', - ); - - // Replace quoted format - newIosProjectString = newIosProjectString.replaceAll( - 'PRODUCT_BUNDLE_IDENTIFIER = "$oldId";', - 'PRODUCT_BUNDLE_IDENTIFIER = "$newId";', - ); - }); - - // Special case for RunnerTests which might not be caught by the pattern above - if (identifierReplacements.containsKey(baseIdentifier)) { - newIosProjectString = newIosProjectString.replaceAll( - RegExp('PRODUCT_BUNDLE_IDENTIFIER = "$baseIdentifier\\.RunnerTests";'), - 'PRODUCT_BUNDLE_IDENTIFIER = "$packageName.RunnerTests";', - ); - - newIosProjectString = newIosProjectString.replaceAll( - RegExp('PRODUCT_BUNDLE_IDENTIFIER = $baseIdentifier\\.RunnerTests;'), - 'PRODUCT_BUNDLE_IDENTIFIER = $packageName.RunnerTests;', - ); - } + // If no base identifier found, use the shortest unique identifier + baseIdentifier ??= + bundleIdentifierMatches.reduce((a, b) => a.length <= b.length ? a : b); + + // Replace all occurrences + final newIosProjectString = iosProjectString.replaceAllMapped( + RegExp( + 'PRODUCT_BUNDLE_IDENTIFIER = ("?)${RegExp.escape(baseIdentifier)}(\\.[A-Za-z0-9._-]+)?("?);'), + (match) { + final openQuote = match.group(1) ?? ''; + final extension = match.group(2) ?? ''; + final closeQuote = match.group(3) ?? ''; + + return 'PRODUCT_BUNDLE_IDENTIFIER = $openQuote$packageName$extension$closeQuote;'; + }, + ); iosProjectFile.writeAsStringSync(newIosProjectString);