diff --git a/packages/flutterfire_cli/lib/src/firebase/firebase_apple_writes.dart b/packages/flutterfire_cli/lib/src/firebase/firebase_apple_writes.dart index 1f8fff19..a2ea1f5b 100644 --- a/packages/flutterfire_cli/lib/src/firebase/firebase_apple_writes.dart +++ b/packages/flutterfire_cli/lib/src/firebase/firebase_apple_writes.dart @@ -390,6 +390,7 @@ String _debugSymbolsScript( require 'xcodeproj' xcodeFile='${getXcodeProjectPath(platform)}' runScriptName='$debugSymbolScriptName' +bundleScriptName='$bundleServiceScriptName' project = Xcodeproj::Project.open(xcodeFile) @@ -412,19 +413,78 @@ ${isDevDependency ? 'dart run flutterfire_cli:flutterfire' : 'flutterfire'} uplo for target in project.targets if (target.name == '$target') + # Find existing debug symbols phase phase = target.shell_script_build_phases().find do |item| if defined? item && item.name item.name == runScriptName end end + + # Find bundle-service-file phase to determine insertion position + bundlePhase = target.shell_script_build_phases().find do |item| + if defined? item && item.name + item.name == bundleScriptName + end + end if phase.nil? + # Create new phase phase = target.new_shell_script_build_phase(runScriptName) phase.shell_script = bashScript + + # If bundle-service-file exists, ensure debug symbols is placed right after it + if (!bundlePhase.nil?) + # Get all build phases + allPhases = target.build_phases + bundleIndex = allPhases.index(bundlePhase) + currentDebugIndex = allPhases.index(phase) + + # If debug symbols is not right after bundle-service-file, reorder + if (currentDebugIndex != bundleIndex + 1) + # Remove from current position + target.build_phases.delete(phase) + # Insert right after bundle-service-file + target.build_phases.insert(bundleIndex + 1, phase) + end + end + project.save() elsif phase.shell_script != bashScript + # Update existing phase phase.shell_script = bashScript + + # Ensure correct ordering: debug symbols should be right after bundle-service-file + if (!bundlePhase.nil?) + allPhases = target.build_phases + bundleIndex = allPhases.index(bundlePhase) + debugIndex = allPhases.index(phase) + + # If debug symbols is not right after bundle-service-file, reorder + if (debugIndex != bundleIndex + 1) + # Remove from current position + target.build_phases.delete(phase) + # Insert right after bundle-service-file + target.build_phases.insert(bundleIndex + 1, phase) + end + end + project.save() + else + # Script exists and content is correct, but check ordering + if (!bundlePhase.nil?) + allPhases = target.build_phases + bundleIndex = allPhases.index(bundlePhase) + debugIndex = allPhases.index(phase) + + # If debug symbols is not right after bundle-service-file, reorder + if (debugIndex != bundleIndex + 1) + # Remove from current position + target.build_phases.delete(phase) + # Insert right after bundle-service-file + target.build_phases.insert(bundleIndex + 1, phase) + project.save() + end + end end end end diff --git a/packages/flutterfire_cli/test/configure_test.dart b/packages/flutterfire_cli/test/configure_test.dart index ba5cd9e5..3b652fb4 100644 --- a/packages/flutterfire_cli/test/configure_test.dart +++ b/packages/flutterfire_cli/test/configure_test.dart @@ -338,6 +338,108 @@ void main() { ), ); + test( + 'flutterfire configure: build configuration - verify script ordering after second configure', + () async { + // Add crashlytics dependency so debug symbols script gets added + final addCrashlyticsResult = Process.runSync( + 'flutter', + ['pub', 'add', 'firebase_crashlytics'], + workingDirectory: projectPath, + ); + + if (addCrashlyticsResult.exitCode != 0) { + fail(addCrashlyticsResult.stderr as String); + } + + // First configure run + final result = Process.runSync( + 'flutterfire', + [ + 'configure', + '--yes', + '--project=$firebaseProjectId', + '--platforms=macos', + '--macos-out=macos/$buildType', + '--macos-build-config=$appleBuildConfiguration', + ], + workingDirectory: projectPath, + runInShell: true, + ); + + if (result.exitCode != 0) { + fail(result.stderr as String); + } + + if (Platform.isMacOS) { + // Second configure run - this should trigger reordering if needed + final result2 = Process.runSync( + 'flutterfire', + [ + 'configure', + '--yes', + '--project=$firebaseProjectId', + '--platforms=macos', + '--macos-out=macos/$buildType', + '--macos-build-config=$appleBuildConfiguration', + ], + workingDirectory: projectPath, + runInShell: true, + ); + + if (result2.exitCode != 0) { + fail(result2.stderr as String); + } + + // Verify script ordering for iOS + final scriptOrderCheckIos = rubyScriptForCheckingScriptOrdering( + projectPath!, + kIos, + ); + + final iosOrderResult = Process.runSync( + 'ruby', + [ + '-e', + scriptOrderCheckIos, + ], + runInShell: true, + ); + + if (iosOrderResult.exitCode != 0) { + fail(iosOrderResult.stderr as String); + } + + expect(iosOrderResult.stdout, 'success'); + + // Verify script ordering for macOS + final scriptOrderCheckMacos = rubyScriptForCheckingScriptOrdering( + projectPath!, + kMacos, + ); + + final macosOrderResult = Process.runSync( + 'ruby', + [ + '-e', + scriptOrderCheckMacos, + ], + runInShell: true, + ); + + if (macosOrderResult.exitCode != 0) { + fail(macosOrderResult.stderr as String); + } + + expect(macosOrderResult.stdout, 'success'); + } + }, + skip: !Platform.isMacOS, + timeout: const Timeout( + Duration(minutes: 2), + ), + ); + test( 'flutterfire configure: android - "default" Apple - "target"', () async { diff --git a/packages/flutterfire_cli/test/test_utils.dart b/packages/flutterfire_cli/test/test_utils.dart index 606b7891..0df1e8aa 100644 --- a/packages/flutterfire_cli/test/test_utils.dart +++ b/packages/flutterfire_cli/test/test_utils.dart @@ -217,6 +217,65 @@ String rubyScriptForTestingDebugSymbolScriptExists( '''; } +String rubyScriptForCheckingScriptOrdering( + String projectPath, + String platform, { + String targetName = 'Runner', + String bundleScriptName = 'FlutterFire: "flutterfire bundle-service-file"', + String debugSymbolScriptName = + 'FlutterFire: "flutterfire upload-crashlytics-symbols"', +}) { + final xcodeProjectPath = p.join(projectPath, platform, 'Runner.xcodeproj'); + return ''' +require 'xcodeproj' +xcodeFile='$xcodeProjectPath' +bundleScriptName='$bundleScriptName' +debugSymbolScriptName='$debugSymbolScriptName' +targetName='$targetName' +project = Xcodeproj::Project.open(xcodeFile) + +target = project.targets.find { |target| target.name == targetName } + +if (target) + # Find both scripts + bundlePhase = target.shell_script_build_phases().find do |item| + if defined? item && item.name + item.name == bundleScriptName + end + end + + debugSymbolPhase = target.shell_script_build_phases().find do |item| + if defined? item && item.name + item.name == debugSymbolScriptName + end + end + + # Both scripts must exist + if (bundlePhase.nil?) + abort("failed, bundle-service-file script not found") + end + + if (debugSymbolPhase.nil?) + abort("failed, upload-crashlytics-symbols script not found") + end + + # Get all build phases to check ordering + allPhases = target.build_phases + bundleIndex = allPhases.index(bundlePhase) + debugSymbolIndex = allPhases.index(debugSymbolPhase) + + # bundle-service-file must come before upload-crashlytics-symbols + if (bundleIndex >= debugSymbolIndex) + abort("failed, bundle-service-file script (index: #{bundleIndex}) must come before upload-crashlytics-symbols script (index: #{debugSymbolIndex})") + end + + \$stdout.write("success") +else + abort("failed, #{targetName} target not found.") +end +'''; +} + Future findFileInDirectory( String directoryPath, String fileName,