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/7] 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/7] 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); From 0bbdac2f61f589526caa9d6e864d7b382a916ea0 Mon Sep 17 00:00:00 2001 From: OutdatedGuy Date: Wed, 12 Nov 2025 09:23:57 +0530 Subject: [PATCH 3/7] Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- lib/platforms/ios.dart | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/lib/platforms/ios.dart b/lib/platforms/ios.dart index 739f394..46cb9c7 100644 --- a/lib/platforms/ios.dart +++ b/lib/platforms/ios.dart @@ -119,29 +119,32 @@ void _setIOSPackageName(dynamic packageName) { .toSet(); if (bundleIdentifierMatches.isEmpty) { - throw Exception('No bundle identifiers found in project file'); + throw _PackageRenameException( + 'No bundle identifiers found in project file', + 254, + ); } // Find the base identifier by counting extensions String? baseIdentifier; int maxExtensions = 0; + // Build a map of identifier to extension count in a single pass + final extensionCountMap = {}; for (final identifier in bundleIdentifierMatches) { - int extensionCount = 0; - - for (final other in bundleIdentifierMatches) { - // Check if 'other' extends 'identifier' with a dot separator + extensionCountMap[identifier] = 0; + } + for (final other in bundleIdentifierMatches) { + for (final identifier in bundleIdentifierMatches) { if (identifier != other && other.startsWith('$identifier.')) { - extensionCount++; + extensionCountMap[identifier] = (extensionCountMap[identifier] ?? 0) + 1; } } - - // Use the identifier that has the most extensions - if (extensionCount > maxExtensions) { - maxExtensions = extensionCount; - baseIdentifier = identifier; - } } + baseIdentifier = extensionCountMap.entries + .reduce((a, b) => a.value >= b.value ? a : b) + .key; + maxExtensions = extensionCountMap[baseIdentifier] ?? 0; // If no base identifier found, use the shortest unique identifier baseIdentifier ??= From 78031a5c592511f18de794cffff6da913bab62f2 Mon Sep 17 00:00:00 2001 From: OutdatedGuy Date: Wed, 12 Nov 2025 09:40:07 +0530 Subject: [PATCH 4/7] feat(ios): add base identifier not found exception and message --- lib/exceptions.dart | 5 +++++ lib/messages.dart | 6 ++++++ lib/platforms/ios.dart | 18 +++++------------- 3 files changed, 16 insertions(+), 13 deletions(-) diff --git a/lib/exceptions.dart b/lib/exceptions.dart index d4524ba..cdbbfa3 100644 --- a/lib/exceptions.dart +++ b/lib/exceptions.dart @@ -185,4 +185,9 @@ class _PackageRenameErrors { _invalidShortAppNameMessage, 34, ); + + static const baseIdentifierNotFound = _PackageRenameException( + _baseIdentifierNotFoundMessage, + 35, + ); } diff --git a/lib/messages.dart b/lib/messages.dart index f5803f5..fb80b78 100644 --- a/lib/messages.dart +++ b/lib/messages.dart @@ -209,3 +209,9 @@ const _macOSProjectFileNotFoundMessage = ''' ║ project.pbxproj not found in `macos/Runner.xcodeproj/`. ║ ╚═════════════════════════════════════════════════════════════╝ '''; + +const _baseIdentifierNotFoundMessage = ''' +╔════════════════════════════════════════════════════╗ +║ No bundle identifiers found in project file!!! ║ +╚════════════════════════════════════════════════════╝ +'''; diff --git a/lib/platforms/ios.dart b/lib/platforms/ios.dart index 46cb9c7..148295d 100644 --- a/lib/platforms/ios.dart +++ b/lib/platforms/ios.dart @@ -110,7 +110,7 @@ void _setIOSPackageName(dynamic packageName) { // Extract all bundle identifiers, using only allowed characters final bundleIdRegex = RegExp( - r'PRODUCT_BUNDLE_IDENTIFIER = "?([A-Za-z0-9._-]+)"?;', + r'PRODUCT_BUNDLE_IDENTIFIER = "?([A-Za-z0-9.-]+)"?;', ); final bundleIdentifierMatches = bundleIdRegex @@ -119,15 +119,11 @@ void _setIOSPackageName(dynamic packageName) { .toSet(); if (bundleIdentifierMatches.isEmpty) { - throw _PackageRenameException( - 'No bundle identifiers found in project file', - 254, - ); + throw _PackageRenameErrors.baseIdentifierNotFound; } // Find the base identifier by counting extensions String? baseIdentifier; - int maxExtensions = 0; // Build a map of identifier to extension count in a single pass final extensionCountMap = {}; @@ -137,23 +133,19 @@ void _setIOSPackageName(dynamic packageName) { for (final other in bundleIdentifierMatches) { for (final identifier in bundleIdentifierMatches) { if (identifier != other && other.startsWith('$identifier.')) { - extensionCountMap[identifier] = (extensionCountMap[identifier] ?? 0) + 1; + extensionCountMap[identifier] = extensionCountMap[identifier]! + 1; } } } baseIdentifier = extensionCountMap.entries .reduce((a, b) => a.value >= b.value ? a : b) .key; - maxExtensions = extensionCountMap[baseIdentifier] ?? 0; - - // 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._-]+)?("?);'), + 'PRODUCT_BUNDLE_IDENTIFIER = ("?)${RegExp.escape(baseIdentifier)}(\\.[A-Za-z0-9.-]+)?("?);', + ), (match) { final openQuote = match.group(1) ?? ''; final extension = match.group(2) ?? ''; From c934bcbb34141a3e48906de870a73d0f8c1f92e4 Mon Sep 17 00:00:00 2001 From: OutdatedGuy Date: Wed, 12 Nov 2025 09:50:23 +0530 Subject: [PATCH 5/7] Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- lib/messages.dart | 7 ++++--- lib/platforms/ios.dart | 13 ++++--------- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/lib/messages.dart b/lib/messages.dart index fb80b78..0d2cfdb 100644 --- a/lib/messages.dart +++ b/lib/messages.dart @@ -211,7 +211,8 @@ const _macOSProjectFileNotFoundMessage = ''' '''; const _baseIdentifierNotFoundMessage = ''' -╔════════════════════════════════════════════════════╗ -║ No bundle identifiers found in project file!!! ║ -╚════════════════════════════════════════════════════╝ +╔════════════════════════════════════════════════════════════════════════════════════╗ +║ No bundle identifiers found in `ios/Runner.xcodeproj/project.pbxproj`!!! ║ +║ The file may be corrupted or in an unexpected format. ║ +╚════════════════════════════════════════════════════════════════════════════════════╝ '''; diff --git a/lib/platforms/ios.dart b/lib/platforms/ios.dart index 148295d..10b0cee 100644 --- a/lib/platforms/ios.dart +++ b/lib/platforms/ios.dart @@ -125,17 +125,12 @@ void _setIOSPackageName(dynamic packageName) { // Find the base identifier by counting extensions String? baseIdentifier; - // Build a map of identifier to extension count in a single pass + // Build a map of identifier to extension count final extensionCountMap = {}; for (final identifier in bundleIdentifierMatches) { - extensionCountMap[identifier] = 0; - } - for (final other in bundleIdentifierMatches) { - for (final identifier in bundleIdentifierMatches) { - if (identifier != other && other.startsWith('$identifier.')) { - extensionCountMap[identifier] = extensionCountMap[identifier]! + 1; - } - } + extensionCountMap[identifier] = bundleIdentifierMatches + .where((other) => other != identifier && other.startsWith('$identifier.')) + .length; } baseIdentifier = extensionCountMap.entries .reduce((a, b) => a.value >= b.value ? a : b) From 05b90c7a24edb0772cfea51e4bdd6d1ad2285d17 Mon Sep 17 00:00:00 2001 From: OutdatedGuy Date: Wed, 12 Nov 2025 09:54:48 +0530 Subject: [PATCH 6/7] fix(ios): correct formatting in base identifier not found message and improve bundle identifier matching logic --- lib/messages.dart | 8 ++++---- lib/platforms/ios.dart | 11 +++++------ 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/lib/messages.dart b/lib/messages.dart index 0d2cfdb..ab8545f 100644 --- a/lib/messages.dart +++ b/lib/messages.dart @@ -211,8 +211,8 @@ const _macOSProjectFileNotFoundMessage = ''' '''; const _baseIdentifierNotFoundMessage = ''' -╔════════════════════════════════════════════════════════════════════════════════════╗ -║ No bundle identifiers found in `ios/Runner.xcodeproj/project.pbxproj`!!! ║ -║ The file may be corrupted or in an unexpected format. ║ -╚════════════════════════════════════════════════════════════════════════════════════╝ +╔══════════════════════════════════════════════════════════════════════════════╗ +║ No bundle identifiers found in `ios/Runner.xcodeproj/project.pbxproj`!!! ║ +║ The file may be corrupted or in an unexpected format. ║ +╚══════════════════════════════════════════════════════════════════════════════╝ '''; diff --git a/lib/platforms/ios.dart b/lib/platforms/ios.dart index 10b0cee..f37faf6 100644 --- a/lib/platforms/ios.dart +++ b/lib/platforms/ios.dart @@ -128,9 +128,9 @@ void _setIOSPackageName(dynamic packageName) { // Build a map of identifier to extension count final extensionCountMap = {}; for (final identifier in bundleIdentifierMatches) { - extensionCountMap[identifier] = bundleIdentifierMatches - .where((other) => other != identifier && other.startsWith('$identifier.')) - .length; + extensionCountMap[identifier] = bundleIdentifierMatches.where((other) { + return other != identifier && other.startsWith('$identifier.'); + }).length; } baseIdentifier = extensionCountMap.entries .reduce((a, b) => a.value >= b.value ? a : b) @@ -142,11 +142,10 @@ void _setIOSPackageName(dynamic packageName) { 'PRODUCT_BUNDLE_IDENTIFIER = ("?)${RegExp.escape(baseIdentifier)}(\\.[A-Za-z0-9.-]+)?("?);', ), (match) { - final openQuote = match.group(1) ?? ''; + final quote = match.group(1) ?? match.group(3) ?? ''; final extension = match.group(2) ?? ''; - final closeQuote = match.group(3) ?? ''; - return 'PRODUCT_BUNDLE_IDENTIFIER = $openQuote$packageName$extension$closeQuote;'; + return 'PRODUCT_BUNDLE_IDENTIFIER = $quote$packageName$extension$quote;'; }, ); From b3f9968efacffbac840299d6f74363da18103594 Mon Sep 17 00:00:00 2001 From: OutdatedGuy Date: Wed, 12 Nov 2025 09:58:06 +0530 Subject: [PATCH 7/7] fix(ios): rename variable for clarity in bundle identifier replacement logic --- lib/platforms/ios.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/platforms/ios.dart b/lib/platforms/ios.dart index f37faf6..defd1ee 100644 --- a/lib/platforms/ios.dart +++ b/lib/platforms/ios.dart @@ -137,7 +137,7 @@ void _setIOSPackageName(dynamic packageName) { .key; // Replace all occurrences - final newIosProjectString = iosProjectString.replaceAllMapped( + final newBundleIDIOSProjectString = iosProjectString.replaceAllMapped( RegExp( 'PRODUCT_BUNDLE_IDENTIFIER = ("?)${RegExp.escape(baseIdentifier)}(\\.[A-Za-z0-9.-]+)?("?);', ), @@ -149,7 +149,7 @@ void _setIOSPackageName(dynamic packageName) { }, ); - iosProjectFile.writeAsStringSync(newIosProjectString); + iosProjectFile.writeAsStringSync(newBundleIDIOSProjectString); _logger.i('iOS bundle identifier set to: `$packageName` (project.pbxproj)'); } on _PackageRenameException catch (e) {