diff --git a/.gitignore b/.gitignore index 00464f10..c4605e03 100644 --- a/.gitignore +++ b/.gitignore @@ -27,4 +27,6 @@ test_generation/ test_generation_dev/ # melos -pubspec_overrides.yaml \ No newline at end of file +pubspec_overrides.yaml + +context/ \ No newline at end of file diff --git a/packages/flutterfire_cli/lib/src/firebase.dart b/packages/flutterfire_cli/lib/src/firebase.dart index f282ce69..537ddda0 100644 --- a/packages/flutterfire_cli/lib/src/firebase.dart +++ b/packages/flutterfire_cli/lib/src/firebase.dart @@ -50,7 +50,7 @@ Future getDefaultFirebaseProjectId() async { final fileContents = firebaseRcFile.readAsStringSync(); try { final jsonMap = - const JsonDecoder().convert(fileContents) as Map; + const JsonDecoder().convert(fileContents) as Map; if (jsonMap['projects'] != null && (jsonMap['projects'] as Map)['default'] != null) { return (jsonMap['projects'] as Map)['default'] as String; @@ -67,11 +67,11 @@ Future getDefaultFirebaseProjectId() async { /// final result = await runFirebaseCommand(['projects:list']); /// print(result); Future> runFirebaseCommand( - List commandAndArgs, { - String? project, - String? account, - String? serviceAccount, - }) async { + List commandAndArgs, { + String? project, + String? account, + String? serviceAccount, +}) async { final cliExists = await exists(); if (!cliExists) { throw FirebaseCommandException( @@ -116,7 +116,7 @@ Future> runFirebaseCommand( if (jsonString.length > characterLimit) { // If the JSON string is large, write it to a temporary file final tempFile = - File('${Directory.systemTemp.path}/firebase_output.json'); + File('${Directory.systemTemp.path}/firebase_output.json'); await tempFile.writeAsString(jsonString); // Read from the temporary file to create a Dart object @@ -171,8 +171,8 @@ Future> getProjects({ return result .map( (Map e) => - FirebaseProject.fromJson(Map.from(e)), - ) + FirebaseProject.fromJson(Map.from(e)), + ) .where((project) => project.state == 'ACTIVE') .toList(); } catch (e) { @@ -231,8 +231,8 @@ Future> getApps({ return result .map( (Map e) => - FirebaseApp.fromJson(Map.from(e)), - ) + FirebaseApp.fromJson(Map.from(e)), + ) .toList(); } @@ -300,7 +300,7 @@ Future findOrCreateFirebaseApp({ _assertFirebaseSupportedPlatform(platformFirebase); final fetchingAppsSpinner = spinner( - (done) { + (done) { final loggingAppName = packageNameOrBundleIdentifier ?? webAppId ?? displayNameWithPlatform; if (!done) { @@ -333,7 +333,7 @@ Future findOrCreateFirebaseApp({ final flagOption = platform == kWeb ? kWebAppIdFlag : kWindowsAppIdFlag; // Find provided web app id for web and windows, otherwise, throw Exception that it doesn't exist final webApp = unfilteredFirebaseApps.firstWhere( - (firebaseApp) => firebaseApp.appId == webAppId, + (firebaseApp) => firebaseApp.appId == webAppId, orElse: () { fetchingAppsSpinner.done(); throw Exception( @@ -347,7 +347,7 @@ Future findOrCreateFirebaseApp({ } // Find web app for web and windows using display name with this signature: "flutter_app_name (platform) filteredFirebaseApps = unfilteredFirebaseApps.where( - (firebaseApp) { + (firebaseApp) { if (firebaseApp.displayName == displayNameWithPlatform) { return true; } @@ -357,17 +357,17 @@ Future findOrCreateFirebaseApp({ // Find any for that platform if no web app found with display name if (filteredFirebaseApps.isEmpty) { filteredFirebaseApps = unfilteredFirebaseApps.where( - (firebaseApp) { + (firebaseApp) { return firebaseApp.platform == platform; }, ); } } else { filteredFirebaseApps = unfilteredFirebaseApps.where( - (firebaseApp) { + (firebaseApp) { if (packageNameOrBundleIdentifier != null) { return firebaseApp.packageNameOrBundleIdentifier == - packageNameOrBundleIdentifier && + packageNameOrBundleIdentifier && firebaseApp.platform == platformFirebase; } return false; @@ -405,7 +405,7 @@ Future findOrCreateFirebaseApp({ ); break; case kWeb: - // This is used to also create windows app, Firebase has no concept of a windows app + // This is used to also create windows app, Firebase has no concept of a windows app createFirebaseAppFuture = createWebApp( project: project, displayName: displayNameWithPlatform, @@ -419,7 +419,7 @@ Future findOrCreateFirebaseApp({ } final creatingAppSpinner = spinner( - (done) { + (done) { if (!done) { return AnsiStyles.bold( 'Registering new Firebase ${AnsiStyles.cyan(platform)} app on Firebase project ${AnsiStyles.cyan(project)}.', @@ -519,7 +519,7 @@ Future getAccessToken() async { : Platform.environment['HOME']!; // Path to 'firebase-tools.json' final configPath = - path.join(homeDir, '.config', 'configstore', 'firebase-tools.json'); + path.join(homeDir, '.config', 'configstore', 'firebase-tools.json'); final configFile = File(configPath); if (!configFile.existsSync()) { throw Exception( @@ -536,7 +536,7 @@ Future getAccessToken() async { headers: {'Content-Type': 'application/x-www-form-urlencoded'}, // Values for obtaining the access token are taken from the Firebase CLI source code: https://github.com/firebase/firebase-tools/blob/b14b5f38fe23da6543778a588811b0e2391427c0/src/api.ts#L18 body: - 'grant_type=refresh_token&client_id=563584335869-fgrhgmd47bqnekij5i8b5pr03ho849e6.apps.googleusercontent.com&client_secret=j9iVZfS8kkCEFUPaAeJV0sAi&refresh_token=$refreshToken', + 'grant_type=refresh_token&client_id=563584335869-fgrhgmd47bqnekij5i8b5pr03ho849e6.apps.googleusercontent.com&client_secret=j9iVZfS8kkCEFUPaAeJV0sAi&refresh_token=$refreshToken', ); if (response.statusCode == 200) { @@ -551,19 +551,19 @@ Future getAccessToken() async { // Return string value of "GoogleService-Info.plist" or "google-services.json" file for relevant platform Future getServiceFileContent( - String projectId, - String appId, - String accessToken, - String platform, - ) async { + String projectId, + String appId, + String accessToken, + String platform, +) async { String? uri; if (platform == kIos || platform == kMacos) { uri = - 'https://firebase.googleapis.com/v1beta1/projects/$projectId/iosApps/$appId/config'; + 'https://firebase.googleapis.com/v1beta1/projects/$projectId/iosApps/$appId/config'; } else if (platform == kAndroid) { uri = - 'https://firebase.googleapis.com/v1beta1/projects/$projectId/androidApps/$appId/config'; + 'https://firebase.googleapis.com/v1beta1/projects/$projectId/androidApps/$appId/config'; } else { throw ServiceFileException( platform, 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..8cdb57a1 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) @@ -410,21 +411,74 @@ fi ${isDevDependency ? 'dart run flutterfire_cli:flutterfire' : 'flutterfire'} upload-crashlytics-symbols --upload-symbols-script-path="\$PATH_TO_CRASHLYTICS_UPLOAD_SCRIPT" --platform=$platform --apple-project-path="\${SRCROOT}" --env-platform-name="\${PLATFORM_NAME}" --env-configuration="\${CONFIGURATION}" --env-project-dir="\${PROJECT_DIR}" --env-built-products-dir="\${BUILT_PRODUCTS_DIR}" --env-dwarf-dsym-folder-path="\${DWARF_DSYM_FOLDER_PATH}" --env-dwarf-dsym-file-name="\${DWARF_DSYM_FILE_NAME}" --env-infoplist-path="\${INFOPLIST_PATH}" $projectType ) +def ensure_phase_is_after(target, phase, preceding_phase) + return if preceding_phase.nil? + + allPhases = target.build_phases + precedingIndex = allPhases.index(preceding_phase) + phaseIndex = allPhases.index(phase) + + return if precedingIndex.nil? || phaseIndex.nil? + return if phaseIndex == precedingIndex + 1 + + target.build_phases.delete(phase) + + insertionIndex = + if phaseIndex < precedingIndex + precedingIndex + else + precedingIndex + 1 + end + + target.build_phases.insert(insertionIndex, phase) +end + 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?) + ensure_phase_is_after(target, phase, bundlePhase) + 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?) + ensure_phase_is_after(target, phase, bundlePhase) + end + project.save() + else + # Script exists and content is correct, but check ordering + if (!bundlePhase.nil?) + currentOrdering = target.build_phases.dup + ensure_phase_is_after(target, phase, bundlePhase) + if (target.build_phases != currentOrdering) + 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 121cbce4..a06d3aaf 100644 --- a/packages/flutterfire_cli/test/configure_test.dart +++ b/packages/flutterfire_cli/test/configure_test.dart @@ -338,6 +338,85 @@ 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); + } + + // 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 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,