diff --git a/packages/flutterfire_cli/lib/src/commands/config.dart b/packages/flutterfire_cli/lib/src/commands/config.dart index b3756097..d06b3dc6 100644 --- a/packages/flutterfire_cli/lib/src/commands/config.dart +++ b/packages/flutterfire_cli/lib/src/commands/config.dart @@ -15,15 +15,13 @@ * */ -import 'dart:io'; - import 'package:ansi_styles/ansi_styles.dart'; -import 'package:path/path.dart' as path; import '../common/exception.dart'; import '../common/platform.dart'; import '../common/utils.dart'; import '../firebase.dart' as firebase; +import '../firebase/firebase_android_gradle_plugins.dart'; import '../firebase/firebase_android_options.dart'; import '../firebase/firebase_app_id_file.dart'; import '../firebase/firebase_apple_options.dart'; @@ -35,22 +33,6 @@ import '../flutter_app.dart'; import 'base.dart'; -final _androidBuildGradleRegex = RegExp( - r'''(?:\s*?dependencies\s?{$\n(?[\s\S\w]*?)classpath\s?['"]{1}com.android.tools.build:gradle:.*?['"]{1}\s*?$)''', - multiLine: true, -); -final _androidAppBuildGradleRegex = RegExp( - r'''(?:^[\s]+apply[\s]+plugin\:[\s]+['"]{1}com\.android\.application['"]{1})''', - multiLine: true, -); -const _googleServicesPluginClass = 'com.google.gms:google-services'; -const _googleServicesPluginName = 'com.google.gms.google-services'; -const _googleServicesPluginVersion = '4.3.10'; -const _googleServicesPlugin = - "classpath '$_googleServicesPluginClass:$_googleServicesPluginVersion'"; -const _googleServicesConfigStart = '// START: FlutterFire Configuration'; -const _googleServicesConfigEnd = '// END: FlutterFire Configuration'; - class ConfigCommand extends FlutterFireCommand { ConfigCommand(FlutterApp? flutterApp) : super(flutterApp) { setupDefaultFirebaseCliOptions(); @@ -104,21 +86,21 @@ class ConfigCommand extends FlutterFireCommand { 'automatically detect it from your "android" folder (if it exists).', ); argParser.addFlag( - 'apply-gradle-plugin', + 'apply-gradle-plugins', defaultsTo: true, + hide: true, abbr: 'g', help: - "Whether to add the Firebase Gradle plugin to your Android app's build.gradle files " - 'and create the google-services.json file in your ./android/app folder. ', + "Whether to add the Firebase related Gradle plugins (such as Crashlytics and Performance) to your Android app's build.gradle files " + 'and create the google-services.json file in your ./android/app folder.', ); - argParser.addFlag( 'app-id-json', defaultsTo: true, hide: true, abbr: 'j', help: - 'Whether to generate the firebase_app_id.json files used by native iOS and Android builds. ', + 'Whether to generate the firebase_app_id.json files used by native iOS and Android builds.', ); } @@ -140,8 +122,8 @@ class ConfigCommand extends FlutterFireCommand { return argResults!['yes'] as bool || false; } - bool get applyGradlePlugin { - return argResults!['apply-gradle-plugin'] as bool; + bool get applyGradlePlugins { + return argResults!['apply-gradle-plugins'] as bool; } bool get generateAppIdJson { @@ -228,128 +210,6 @@ class ConfigCommand extends FlutterFireCommand { return newProject; } - Future conditionallySetupAndroidGoogleServices({ - required FlutterApp flutterApp, - required FirebaseOptions firebaseOptions, - bool force = false, - }) async { - if (!flutterApp.android) { - // Flutter application is not configured to target Android. - return; - } - - // /android/app/google-services.json - var existingProjectId = ''; - var shouldPromptOverwriteGoogleServicesJson = false; - final androidGoogleServicesJsonFile = File( - path.join( - flutterApp.androidDirectory.path, - 'app', - firebaseOptions.optionsSourceFileName, - ), - ); - if (androidGoogleServicesJsonFile.existsSync()) { - final existingGoogleServicesJsonContents = - await androidGoogleServicesJsonFile.readAsString(); - existingProjectId = FirebaseAndroidOptions.projectIdFromFileContents( - existingGoogleServicesJsonContents, - ); - if (existingProjectId != firebaseOptions.projectId) { - shouldPromptOverwriteGoogleServicesJson = true; - } - } - if (shouldPromptOverwriteGoogleServicesJson && !force) { - final overwriteGoogleServicesJson = promptBool( - 'The ${AnsiStyles.cyan(firebaseOptions.optionsSourceFileName)} file already exists but for a different Firebase project (${AnsiStyles.grey(existingProjectId)}). ' - 'Do you want to replace it with Firebase project ${AnsiStyles.green(firebaseOptions.projectId)}?', - ); - if (!overwriteGoogleServicesJson) { - logger.stdout( - 'Skipping ${AnsiStyles.cyan(firebaseOptions.optionsSourceFileName)} setup. This may cause issues with some Firebase services on Android in your application.', - ); - return; - } - } - await androidGoogleServicesJsonFile.writeAsString( - firebaseOptions.optionsSourceContent, - ); - - // DETECT /android/build.gradle - var shouldPromptUpdateAndroidBuildGradle = false; - final androidBuildGradleFile = File( - path.join(flutterApp.androidDirectory.path, 'build.gradle'), - ); - final androidBuildGradleFileContents = - await androidBuildGradleFile.readAsString(); - if (!androidBuildGradleFileContents.contains(_googleServicesPluginClass)) { - final hasMatch = - _androidBuildGradleRegex.hasMatch(androidBuildGradleFileContents); - if (!hasMatch) { - // TODO some unrecoverable error here - return; - } - shouldPromptUpdateAndroidBuildGradle = true; - // TODO should we check if has google() repositories configured? - } else { - // TODO already contains google services, should we upgrade version? - } - - // DETECT /android/app/build.gradle - var shouldPromptUpdateAndroidAppBuildGradle = false; - final androidAppBuildGradleFile = File( - path.join(flutterApp.androidDirectory.path, 'app', 'build.gradle'), - ); - final androidAppBuildGradleFileContents = - await androidAppBuildGradleFile.readAsString(); - if (!androidAppBuildGradleFileContents - .contains(_googleServicesPluginClass)) { - final hasMatch = _androidAppBuildGradleRegex - .hasMatch(androidAppBuildGradleFileContents); - if (!hasMatch) { - // TODO some unrecoverable error here? - return; - } - shouldPromptUpdateAndroidAppBuildGradle = true; - } - - if ((shouldPromptUpdateAndroidBuildGradle || - shouldPromptUpdateAndroidAppBuildGradle) && - !force) { - final updateAndroidGradleFiles = promptBool( - 'The files ${AnsiStyles.cyan('android/build.gradle')} & ${AnsiStyles.cyan('android/app/build.gradle')} will be updated to apply the Firebase configuration. ' - 'Do you want to continue?', - ); - if (!updateAndroidGradleFiles) { - logger.stdout( - 'Skipping applying Firebase Google Services gradle plugin for Android. This may cause issues with some Firebase services on Android in your application.', - ); - return; - } - } - - // WRITE /android/build.gradle - if (shouldPromptUpdateAndroidBuildGradle) { - final updatedAndroidBuildGradleFileContents = - androidBuildGradleFileContents - .replaceFirstMapped(_androidBuildGradleRegex, (match) { - final indentation = match.group(1); - return '${match.group(0)}\n$indentation$_googleServicesConfigStart\n$indentation$_googleServicesPlugin\n$indentation$_googleServicesConfigEnd'; - }); - await androidBuildGradleFile - .writeAsString(updatedAndroidBuildGradleFileContents); - } - // WRITE /android/app/build.gradle - if (shouldPromptUpdateAndroidAppBuildGradle) { - final updatedAndroidAppBuildGradleFileContents = - androidAppBuildGradleFileContents - .replaceFirstMapped(_androidAppBuildGradleRegex, (match) { - return "${match.group(0)}\n$_googleServicesConfigStart\napply plugin: '$_googleServicesPluginName'\n$_googleServicesConfigEnd"; - }); - await androidAppBuildGradleFile - .writeAsString(updatedAndroidAppBuildGradleFileContents); - } - } - Future _selectFirebaseProject() async { var selectedProjectId = projectId; selectedProjectId ??= await firebase.getDefaultFirebaseProjectId(); @@ -525,13 +385,13 @@ class ConfigCommand extends FlutterFireCommand { ); } - if (androidOptions != null && applyGradlePlugin) { + if (androidOptions != null && applyGradlePlugins) { futures.add( - conditionallySetupAndroidGoogleServices( - firebaseOptions: androidOptions, - flutterApp: flutterApp!, - force: isCI || yes, - ), + FirebaseAndroidGradlePlugins( + flutterApp!, + androidOptions, + logger, + ).apply(force: isCI || yes), ); } diff --git a/packages/flutterfire_cli/lib/src/firebase.dart b/packages/flutterfire_cli/lib/src/firebase.dart index 525e929a..e8671157 100644 --- a/packages/flutterfire_cli/lib/src/firebase.dart +++ b/packages/flutterfire_cli/lib/src/firebase.dart @@ -227,7 +227,7 @@ Future findOrCreateFirebaseApp({ ); } return AnsiStyles.bold( - 'Firebase ${AnsiStyles.cyan(platform)} app ${AnsiStyles.cyan(loggingAppName)} is already registered.', + 'Firebase ${AnsiStyles.cyan(platform)} app ${AnsiStyles.cyan(loggingAppName)} registered.', ); }, ); diff --git a/packages/flutterfire_cli/lib/src/firebase/firebase_android_gradle_plugins.dart b/packages/flutterfire_cli/lib/src/firebase/firebase_android_gradle_plugins.dart new file mode 100644 index 00000000..3019f0ec --- /dev/null +++ b/packages/flutterfire_cli/lib/src/firebase/firebase_android_gradle_plugins.dart @@ -0,0 +1,276 @@ +import 'dart:io'; + +import 'package:ansi_styles/ansi_styles.dart'; +import 'package:cli_util/cli_logging.dart'; +import 'package:path/path.dart' as path; +import '../common/utils.dart'; +import '../flutter_app.dart'; +import 'firebase_android_options.dart'; +import 'firebase_options.dart'; + +// https://regex101.com/r/w2ovos/1 +final _androidBuildGradleRegex = RegExp( + r'''(?:\s*?dependencies\s?{$\n(?[\s\S\w]*?)classpath\s?['"]{1}com\.android\.tools\.build:gradle:.*?['"]{1}\s*?$)''', + multiLine: true, +); +// https://regex101.com/r/rbfAdd/1 +final _androidAppBuildGradleRegex = RegExp( + r'''(?:^[\s]*?apply[\s]+plugin\:[\s]+['"]{1}com\.android\.application['"]{1})''', + multiLine: true, +); +// https://regex101.com/r/ndlYVL/1 +final _androidBuildGradleGoogleServicesRegex = RegExp( + r'''((?^[\s]*?)classpath\s?['"]{1}com\.google\.gms:google-services:.*?['"]{1}\s*?$)''', + multiLine: true, +); +// https://regex101.com/r/buEbed/1 +final _androidAppBuildGradleGoogleServicesRegex = RegExp( + r'''(?:^[\s]*?apply[\s]+plugin\:[\s]+['"]{1}com\.google\.gms\.google-services['"]{1})''', + multiLine: true, +); + +// Google services JSON. +const _googleServicesPluginClass = 'com.google.gms:google-services'; +const _googleServicesPluginName = 'com.google.gms.google-services'; +// TODO read from firebase_core pubspec.yaml firebase.google_services_gradle_plugin_version +const _googleServicesPluginVersion = '4.3.10'; +const _googleServicesPlugin = + "classpath '$_googleServicesPluginClass:$_googleServicesPluginVersion'"; + +// Firebase Crashlytics +const _crashlyticsPluginClassPath = + 'com.google.firebase:firebase-crashlytics-gradle'; +// TODO read from firebase_core pubspec.yaml firebase.crashlytics_gradle_plugin_version +const _crashlyticsPluginClassPathVersion = '2.8.1'; +const _crashlyticsPluginClass = 'com.google.firebase.crashlytics'; + +// Firebase Performance +const _performancePluginClassPath = 'com.google.firebase:perf-plugin'; +// TODO read from firebase_core pubspec.yaml firebase.performance_gradle_plugin_version +const _performancePluginClassPathVersion = '1.4.1'; +const _performancePluginClass = 'com.google.firebase.firebase-perf'; + +const _flutterFireConfigCommentStart = '// START: FlutterFire Configuration'; +const _flutterFireConfigCommentEnd = '// END: FlutterFire Configuration'; + +class FirebaseAndroidGradlePlugins { + FirebaseAndroidGradlePlugins( + this.flutterApp, + this.firebaseOptions, + this.logger, + ); + + final FlutterApp flutterApp; + final FirebaseOptions firebaseOptions; + final Logger logger; + + File get androidGoogleServicesJsonFile => File( + path.join( + flutterApp.androidDirectory.path, + 'app', + firebaseOptions.optionsSourceFileName, + ), + ); + + File get androidBuildGradleFile => + File(path.join(flutterApp.androidDirectory.path, 'build.gradle')); + String? _androidBuildGradleFileContents; + set androidBuildGradleFileContents(String contents) => + _androidBuildGradleFileContents = contents; + String get androidBuildGradleFileContents => + _androidBuildGradleFileContents ??= + androidBuildGradleFile.readAsStringSync(); + + File get androidAppBuildGradleFile => + File(path.join(flutterApp.androidDirectory.path, 'app', 'build.gradle')); + String? _androidAppBuildGradleFileContents; + set androidAppBuildGradleFileContents(String contents) => + _androidAppBuildGradleFileContents = contents; + String get androidAppBuildGradleFileContents => + _androidAppBuildGradleFileContents ??= + androidAppBuildGradleFile.readAsStringSync(); + + Future applyGoogleServicesPlugin({ + bool force = false, + }) async { + var existingProjectId = ''; + var shouldPromptOverwriteGoogleServicesJson = false; + if (androidGoogleServicesJsonFile.existsSync()) { + final existingGoogleServicesJsonContents = + await androidGoogleServicesJsonFile.readAsString(); + existingProjectId = FirebaseAndroidOptions.projectIdFromFileContents( + existingGoogleServicesJsonContents, + ); + if (existingProjectId != firebaseOptions.projectId) { + shouldPromptOverwriteGoogleServicesJson = true; + } + } + if (shouldPromptOverwriteGoogleServicesJson && !force) { + final overwriteGoogleServicesJson = promptBool( + 'The ${AnsiStyles.cyan(firebaseOptions.optionsSourceFileName)} file already exists but for a different Firebase project (${AnsiStyles.grey(existingProjectId)}). ' + 'Do you want to replace it with Firebase project ${AnsiStyles.green(firebaseOptions.projectId)}?', + ); + if (!overwriteGoogleServicesJson) { + logger.stdout( + 'Skipping ${AnsiStyles.cyan(firebaseOptions.optionsSourceFileName)} setup. This may cause issues with some Firebase services on Android in your application.', + ); + return; + } + } + await androidGoogleServicesJsonFile.writeAsString( + firebaseOptions.optionsSourceContent, + ); + + if (!androidBuildGradleFileContents.contains(_googleServicesPluginClass)) { + final hasMatch = + _androidBuildGradleRegex.hasMatch(androidBuildGradleFileContents); + if (!hasMatch) { + // TODO some unrecoverable error here + return; + } + } else { + // TODO already contains google services, should we upgrade version? + return; + } + androidBuildGradleFileContents = androidBuildGradleFileContents + .replaceFirstMapped(_androidBuildGradleRegex, (match) { + final indentation = match.group(1); + return '${match.group(0)}\n$indentation$_flutterFireConfigCommentStart\n$indentation$_googleServicesPlugin\n$indentation$_flutterFireConfigCommentEnd'; + }); + + if (!androidAppBuildGradleFileContents + .contains(_googleServicesPluginClass)) { + final hasMatch = _androidAppBuildGradleRegex + .hasMatch(androidAppBuildGradleFileContents); + if (!hasMatch) { + // TODO some unrecoverable error here? + return; + } + } else { + // Already applied. + return; + } + androidAppBuildGradleFileContents = androidAppBuildGradleFileContents + .replaceFirstMapped(_androidAppBuildGradleRegex, (match) { + return "${match.group(0)}\n$_flutterFireConfigCommentStart\napply plugin: '$_googleServicesPluginName'\n$_flutterFireConfigCommentEnd"; + }); + } + + void _applyFirebaseAndroidPlugin({ + required String pluginClassPath, + required String pluginClassPathVersion, + required String pluginClass, + }) { + if (!androidBuildGradleFileContents.contains(pluginClassPath)) { + final hasMatch = _androidBuildGradleGoogleServicesRegex + .hasMatch(androidBuildGradleFileContents); + if (!hasMatch) { + // TODO some unrecoverable error here + return; + } + } else { + // TODO already contains plugin, should we upgrade version? + return; + } + androidBuildGradleFileContents = androidBuildGradleFileContents + .replaceFirstMapped(_androidBuildGradleGoogleServicesRegex, (match) { + final indentation = match.group(2); + return "${match.group(1)}\n${indentation}classpath '$pluginClassPath:$pluginClassPathVersion'"; + }); + + if (!androidAppBuildGradleFileContents.contains(pluginClass)) { + final hasMatch = _androidAppBuildGradleGoogleServicesRegex + .hasMatch(androidAppBuildGradleFileContents); + if (!hasMatch) { + // TODO some unrecoverable error here? + return; + } + } else { + // Already applied. + return; + } + androidAppBuildGradleFileContents = androidAppBuildGradleFileContents + .replaceFirstMapped(_androidAppBuildGradleGoogleServicesRegex, (match) { + return "${match.group(0)}\napply plugin: '$pluginClass'"; + }); + } + + Future applyCrashlyticsPlugin({ + bool force = false, + }) async { + if (!flutterApp.package.dependencies.contains('firebase_crashlytics') && + !flutterApp.package.devDependencies.contains('firebase_crashlytics')) { + // Skip since user doesn't have the plugin installed. + return; + } + _applyFirebaseAndroidPlugin( + pluginClassPath: _crashlyticsPluginClassPath, + pluginClassPathVersion: _crashlyticsPluginClassPathVersion, + pluginClass: _crashlyticsPluginClass, + ); + } + + Future applyPerformancePlugin({ + bool force = false, + }) async { + if (!flutterApp.package.dependencies.contains('firebase_performance') && + !flutterApp.package.devDependencies.contains('firebase_performance')) { + // Skip since user doesn't have the plugin installed. + return; + } + _applyFirebaseAndroidPlugin( + pluginClassPath: _performancePluginClassPath, + pluginClassPathVersion: _performancePluginClassPathVersion, + pluginClass: _performancePluginClass, + ); + } + + Future apply({ + bool force = false, + }) async { + if (!flutterApp.android) { + // Flutter application is not configured to target Android. + return; + } + final originalAndroidBuildGradleContents = androidBuildGradleFileContents; + final originalAndroidAppBuildGradleContents = + androidAppBuildGradleFileContents; + + await applyGoogleServicesPlugin(force: force); + await applyCrashlyticsPlugin(force: force); + await applyPerformancePlugin(force: force); + + final shouldPromptUpdateAndroidBuildGradle = + originalAndroidBuildGradleContents != androidBuildGradleFileContents; + final shouldPromptUpdateAndroidAppBuildGradle = + originalAndroidAppBuildGradleContents != + androidAppBuildGradleFileContents; + if ((shouldPromptUpdateAndroidBuildGradle || + shouldPromptUpdateAndroidAppBuildGradle) && + !force) { + final updateAndroidGradleFiles = promptBool( + 'The files ${AnsiStyles.cyan('android/build.gradle')} & ${AnsiStyles.cyan('android/app/build.gradle')} will be updated to apply Firebase configuration and build plugins. ' + 'Do you want to continue?', + ); + if (!updateAndroidGradleFiles) { + logger.stdout( + 'Skipping applying Firebase gradle plugins for Android. This may cause issues with some Firebase services on Android in your application.', + ); + return; + } + } + + // WRITE /android/build.gradle + if (shouldPromptUpdateAndroidBuildGradle) { + await androidBuildGradleFile + .writeAsString(androidBuildGradleFileContents); + } + + // WRITE /android/app/build.gradle + if (shouldPromptUpdateAndroidAppBuildGradle) { + await androidAppBuildGradleFile.writeAsString( + androidAppBuildGradleFileContents, + ); + } + } +} diff --git a/packages/flutterfire_cli/lib/src/firebase/firebase_configuration_file.dart b/packages/flutterfire_cli/lib/src/firebase/firebase_configuration_file.dart index 6ca1ddc0..ac8af8da 100644 --- a/packages/flutterfire_cli/lib/src/firebase/firebase_configuration_file.dart +++ b/packages/flutterfire_cli/lib/src/firebase/firebase_configuration_file.dart @@ -60,7 +60,8 @@ class FirebaseConfigurationFile { if (outputFile.existsSync() && !force) { final existingFileContents = await outputFile.readAsString(); // Only prompt overwrite if contents have changed. - if (existingFileContents != newFileContents) { + // Trimming since some IDEs/git auto apply a trailing newline. + if (existingFileContents.trim() != newFileContents.trim()) { final shouldOverwrite = promptBool( 'Generated FirebaseOptions file ${AnsiStyles.cyan(outputFilePath)} already exists, do you want to override it?', );