| // Copyright 2014 The Flutter Authors. All rights reserved. | |
| // Use of this source code is governed by a BSD-style license that can be | |
| // found in the LICENSE file. | |
| import 'package:meta/meta.dart'; | |
| import 'package:unified_analytics/unified_analytics.dart'; | |
| import 'package:yaml/yaml.dart'; | |
| import '../android/gradle_utils.dart' as gradle; | |
| import '../base/common.dart'; | |
| import '../base/context.dart'; | |
| import '../base/file_system.dart'; | |
| import '../base/net.dart'; | |
| import '../base/terminal.dart'; | |
| import '../base/utils.dart'; | |
| import '../base/version.dart'; | |
| import '../base/version_range.dart'; | |
| import '../cache.dart'; | |
| import '../convert.dart'; | |
| import '../dart/pub.dart'; | |
| import '../darwin/darwin.dart'; | |
| import '../features.dart'; | |
| import '../flutter_manifest.dart'; | |
| import '../flutter_project_metadata.dart'; | |
| import '../globals.dart' as globals; | |
| import '../ios/code_signing.dart'; | |
| import '../macos/swift_packages.dart'; | |
| import '../project.dart'; | |
| import '../runner/flutter_command.dart'; | |
| import 'create_base.dart'; | |
| const kPlatformHelp = | |
| 'The platforms supported by this project. ' | |
| 'Platform folders (e.g. android/) will be generated in the target project. ' | |
| 'This argument only works when "--template" is set to app or plugin. ' | |
| 'When adding platforms to a plugin project, the pubspec.yaml will be updated with the requested platform. ' | |
| 'Adding desktop platforms requires the corresponding desktop config setting to be enabled.'; | |
| class CreateCommand extends FlutterCommand with CreateBase { | |
| CreateCommand({bool verboseHelp = false}) { | |
| addPubOptions(); | |
| argParser.addFlag( | |
| 'with-driver-test', | |
| help: | |
| '(deprecated) Historically, this added a flutter_driver dependency and generated a ' | |
| 'sample "flutter drive" test. Now it does nothing. Consider using the ' | |
| '"integration_test" package: https://pub.dev/packages/integration_test', | |
| hide: !verboseHelp, | |
| ); | |
| argParser.addFlag('overwrite', help: 'When performing operations, overwrite existing files.'); | |
| argParser.addOption( | |
| 'description', | |
| defaultsTo: 'A new Flutter project.', | |
| help: | |
| 'The description to use for your new Flutter project. This string ends up in the pubspec.yaml file.', | |
| ); | |
| argParser.addOption( | |
| 'org', | |
| defaultsTo: 'com.example', | |
| help: | |
| 'The organization responsible for your new Flutter project, in reverse domain name notation. ' | |
| 'This string is used in Java package names and as prefix in the iOS bundle identifier.', | |
| ); | |
| argParser.addOption( | |
| 'project-name', | |
| help: | |
| 'The project name for this new Flutter project. This must be a valid dart package name.', | |
| ); | |
| argParser.addOption( | |
| 'ios-language', | |
| abbr: 'i', | |
| defaultsTo: 'swift', | |
| allowed: <String>['objc', 'swift'], | |
| help: | |
| '(deprecated) This option is deprecated and no longer has any effect. ' | |
| 'Swift is always used for iOS-specific code. ' | |
| 'This flag will be removed in a future version of Flutter.', | |
| hide: !verboseHelp, | |
| ); | |
| argParser.addOption( | |
| 'android-language', | |
| abbr: 'a', | |
| defaultsTo: 'kotlin', | |
| allowed: <String>['java', 'kotlin'], | |
| help: | |
| 'The language to use for Android-specific code, either Kotlin (recommended) or Java (legacy).', | |
| ); | |
| argParser.addFlag( | |
| 'skip-name-checks', | |
| help: | |
| 'Allow the creation of applications and plugins with invalid names. ' | |
| 'This is only intended to enable testing of the tool itself.', | |
| hide: !verboseHelp, | |
| ); | |
| argParser.addFlag( | |
| 'implementation-tests', | |
| help: | |
| 'Include implementation tests that verify the template functions correctly. ' | |
| 'This is only intended to enable testing of the tool itself.', | |
| hide: !verboseHelp, | |
| ); | |
| argParser.addOption( | |
| 'initial-create-revision', | |
| help: | |
| 'The Flutter SDK git commit hash to store in .migrate_config. This parameter is used by the tool ' | |
| 'internally and should generally not be used manually.', | |
| hide: !verboseHelp, | |
| ); | |
| final Map<String, String> platformsAllowedHelp = { | |
| for (final String p in kAllCreatePlatforms) p: '', | |
| }; | |
| platformsAllowedHelp['darwin'] = | |
| 'A shared platform for iOS and macOS. (only supported for plugins)'; | |
| addPlatformsOptions(customHelp: kPlatformHelp, allowedHelp: platformsAllowedHelp); | |
| final List<ParsedFlutterTemplateType> enabledTemplates = | |
| ParsedFlutterTemplateType.enabledValues(featureFlags); | |
| argParser.addOption( | |
| 'template', | |
| abbr: 't', | |
| allowed: enabledTemplates.map((ParsedFlutterTemplateType t) => t.cliName), | |
| help: 'Specify the type of project to create.', | |
| valueHelp: 'type', | |
| allowedHelp: CliEnum.allowedHelp(enabledTemplates), | |
| ); | |
| argParser.addOption( | |
| 'sample', | |
| abbr: 's', | |
| help: | |
| 'Specifies the Flutter code sample to use as the "main.dart" for an application. Implies ' | |
| '"--template=app". The value should be the sample ID of the desired sample from the API ' | |
| 'documentation website (https://api.flutter.dev/). An example can be found at: ' | |
| 'https://api.flutter.dev/flutter/widgets/SingleChildScrollView-class.html', | |
| valueHelp: 'id', | |
| hide: !verboseHelp, | |
| ); | |
| argParser.addFlag( | |
| 'empty', | |
| abbr: 'e', | |
| help: | |
| 'Specifies creating using an application template with a main.dart that is minimal, ' | |
| 'including no comments, as a starting point for a new application. Implies "--template=app".', | |
| ); | |
| argParser.addOption( | |
| 'list-samples', | |
| help: | |
| 'Specifies a JSON output file for a listing of Flutter code samples ' | |
| 'that can be created with "--sample".', | |
| valueHelp: 'path', | |
| hide: !verboseHelp, | |
| ); | |
| } | |
| @override | |
| final name = 'create'; | |
| @override | |
| final description = | |
| 'Create a new Flutter project.\n\n' | |
| 'If run on a project that already exists, this will repair the project, recreating any files that are missing.'; | |
| @override | |
| String get category => FlutterCommandCategory.project; | |
| @override | |
| String get invocation => '${runner?.executableName} $name <output directory>'; | |
| @override | |
| Future<Event> unifiedAnalyticsUsageValues(String commandPath) async => Event.commandUsageValues( | |
| workflow: commandPath, | |
| commandHasTerminal: hasTerminal, | |
| createProjectType: stringArg('template'), | |
| createAndroidLanguage: stringArg('android-language'), | |
| ); | |
| // Lazy-initialize the net utilities with values from the context. | |
| late final _net = Net( | |
| httpClientFactory: context.get<HttpClientFactory>(), | |
| logger: globals.logger, | |
| platform: globals.platform, | |
| ); | |
| /// The hostname for the Flutter docs for the current channel. | |
| String get _snippetsHost => | |
| globals.flutterVersion.channel == 'stable' ? 'api.flutter.dev' : 'main-api.flutter.dev'; | |
| Future<String?> _fetchSampleFromServer(String sampleId) async { | |
| // Sanity check the sampleId | |
| if (sampleId.contains(RegExp(r'[^-\w\.]'))) { | |
| throwToolExit( | |
| 'Sample ID "$sampleId" contains invalid characters. Check the ID in the ' | |
| 'documentation and try again.', | |
| ); | |
| } | |
| final snippetsUri = Uri.https(_snippetsHost, 'snippets/$sampleId.dart'); | |
| final List<int>? data = await _net.fetchUrl(snippetsUri); | |
| if (data == null || data.isEmpty) { | |
| return null; | |
| } | |
| return utf8.decode(data); | |
| } | |
| /// Fetches the samples index file from the Flutter docs website. | |
| Future<String?> _fetchSamplesIndexFromServer() async { | |
| final snippetsUri = Uri.https(_snippetsHost, 'snippets/index.json'); | |
| final List<int>? data = await _net.fetchUrl(snippetsUri, maxAttempts: 2); | |
| if (data == null || data.isEmpty) { | |
| return null; | |
| } | |
| return utf8.decode(data); | |
| } | |
| /// Fetches the samples index file from the server and writes it to | |
| /// [outputFilePath]. | |
| Future<void> _writeSamplesJson(String outputFilePath) async { | |
| try { | |
| final File outputFile = globals.fs.file(outputFilePath); | |
| if (outputFile.existsSync()) { | |
| throwToolExit('File "$outputFilePath" already exists', exitCode: 1); | |
| } | |
| final String? samplesJson = await _fetchSamplesIndexFromServer(); | |
| if (samplesJson == null) { | |
| throwToolExit('Unable to download samples', exitCode: 2); | |
| } else { | |
| outputFile.writeAsStringSync(samplesJson); | |
| globals.printStatus('Wrote samples JSON to "$outputFilePath"'); | |
| } | |
| } on Exception catch (e) { | |
| throwToolExit('Failed to write samples JSON to "$outputFilePath": $e', exitCode: 2); | |
| } | |
| } | |
| FlutterTemplateType _getProjectType(Directory projectDir) { | |
| FlutterTemplateType? template; | |
| FlutterTemplateType? detectedProjectType; | |
| final bool metadataExists = projectDir.absolute.childFile('.metadata').existsSync(); | |
| final String? templateArgument = stringArg('template'); | |
| if (templateArgument != null) { | |
| final ParsedFlutterTemplateType? parsedTemplate = ParsedFlutterTemplateType.fromCliName( | |
| templateArgument, | |
| ); | |
| switch (parsedTemplate) { | |
| case RemovedFlutterTemplateType(): | |
| throwToolExit( | |
| 'The template ${parsedTemplate.cliName} is no longer available. For ' | |
| 'your convenience the former help text is repeated below with context ' | |
| 'about the removal and other possible resources:\n\n' | |
| '${parsedTemplate.helpText}', | |
| ); | |
| case FlutterTemplateType(): | |
| template = parsedTemplate; | |
| case null: | |
| break; | |
| } | |
| } | |
| // If the project directory exists and isn't empty, then try to determine the template | |
| // type from the project directory. | |
| if (projectDir.existsSync() && projectDir.listSync().isNotEmpty) { | |
| detectedProjectType = determineTemplateType(); | |
| if (detectedProjectType == null && metadataExists) { | |
| // We can only be definitive that this is the wrong type if the .metadata file | |
| // exists and contains a type that we don't understand, or doesn't contain a type. | |
| throwToolExit( | |
| 'Sorry, unable to detect the type of project to recreate. ' | |
| 'Try creating a fresh project and migrating your existing code to ' | |
| 'the new project manually.', | |
| ); | |
| } | |
| } | |
| template ??= detectedProjectType ?? FlutterTemplateType.app; | |
| if (detectedProjectType != null && template != detectedProjectType && metadataExists) { | |
| // We can only be definitive that this is the wrong type if the .metadata file | |
| // exists and contains a type that doesn't match. | |
| throwToolExit( | |
| "The requested template type '${template.cliName}' doesn't match the " | |
| "existing template type of '${detectedProjectType.cliName}'.", | |
| ); | |
| } | |
| return template; | |
| } | |
| @override | |
| Future<FlutterCommandResult> runCommand() async { | |
| final String? listSamples = stringArg('list-samples'); | |
| if (listSamples != null) { | |
| // _writeSamplesJson can potentially be long-lived. | |
| await _writeSamplesJson(listSamples); | |
| return FlutterCommandResult.success(); | |
| } | |
| if (argResults!.wasParsed('empty') && argResults!.wasParsed('sample')) { | |
| throwToolExit('Only one of --empty or --sample may be specified, not both.', exitCode: 2); | |
| } | |
| validateOutputDirectoryArg(); | |
| String? sampleCode; | |
| final String? sampleArgument = stringArg('sample'); | |
| final bool emptyArgument = boolArg('empty'); | |
| final FlutterTemplateType template = _getProjectType(projectDir); | |
| if (template == FlutterTemplateType.pluginFfi) { | |
| globals.printWarning( | |
| 'The "plugin_ffi" template is deprecated and will be removed in a future ' | |
| 'version of Flutter. Use the "package_ffi" template instead. ' | |
| 'For more information, see: ' | |
| 'https://docs.flutter.dev/platform-integration/bind-native-code', | |
| ); | |
| } | |
| if (sampleArgument != null) { | |
| if (template != FlutterTemplateType.app) { | |
| throwToolExit( | |
| 'Cannot specify --sample with a project type other than ' | |
| '"${FlutterTemplateType.app.cliName}"', | |
| ); | |
| } | |
| // Fetch the sample from the server. | |
| sampleCode = await _fetchSampleFromServer(sampleArgument); | |
| } | |
| if (emptyArgument && template != FlutterTemplateType.app) { | |
| throwToolExit('The --empty flag is only supported for the app template.'); | |
| } | |
| final generateModule = template == FlutterTemplateType.module; | |
| final generateMethodChannelsPlugin = template == FlutterTemplateType.plugin; | |
| final generateFfiPackage = template == FlutterTemplateType.packageFfi; | |
| final generateFfiPlugin = template == FlutterTemplateType.pluginFfi; | |
| final bool generateFfi = generateFfiPlugin || generateFfiPackage; | |
| final generatePackage = template == FlutterTemplateType.package; | |
| final List<String> platforms = stringsArg('platforms'); | |
| // `--platforms` does not support module or package. | |
| if (argResults!.wasParsed('platforms') && | |
| (generateModule || generatePackage || generateFfiPackage)) { | |
| final template = generateModule ? 'module' : 'package'; | |
| throwToolExit( | |
| 'The "--platforms" argument is not supported in $template template.', | |
| exitCode: 2, | |
| ); | |
| } else if (platforms.isEmpty) { | |
| throwToolExit('Must specify at least one platform using --platforms', exitCode: 2); | |
| } else if (generateFfiPlugin && | |
| argResults!.wasParsed('platforms') && | |
| platforms.contains('web')) { | |
| throwToolExit('The web platform is not supported in plugin_ffi template.', exitCode: 2); | |
| } else if (generateFfi && argResults!.wasParsed('android-language')) { | |
| throwToolExit( | |
| 'The "android-language" option is not supported with the ${template.cliName} ' | |
| 'template: the language will always be C or C++.', | |
| exitCode: 2, | |
| ); | |
| } else if (argResults!.wasParsed('ios-language')) { | |
| globals.printWarning( | |
| 'The "--ios-language" option is deprecated and no longer has any effect. ' | |
| 'Swift is always used for iOS-specific code. ' | |
| 'This flag will be removed in a future version of Flutter.', | |
| ); | |
| } | |
| final String organization = await getOrganization(); | |
| final bool overwrite = boolArg('overwrite'); | |
| validateProjectDir(overwrite: overwrite); | |
| if (boolArg('with-driver-test')) { | |
| globals.printWarning( | |
| 'The "--with-driver-test" argument has been deprecated and will no longer add a flutter ' | |
| 'driver template. Instead, learn how to use package:integration_test by ' | |
| 'visiting https://pub.dev/packages/integration_test .', | |
| ); | |
| } | |
| final String dartSdk = globals.cache.dartSdkBuild; | |
| final bool includeIos; | |
| final bool includeDarwin; | |
| final bool includeAndroid; | |
| final bool includeWeb; | |
| final bool includeLinux; | |
| final bool includeMacos; | |
| final bool includeWindows; | |
| if (template == FlutterTemplateType.module) { | |
| // The module template only supports iOS and Android. | |
| includeIos = true; | |
| includeAndroid = true; | |
| includeWeb = false; | |
| includeLinux = false; | |
| includeMacos = false; | |
| includeWindows = false; | |
| includeDarwin = false; | |
| } else if (template == FlutterTemplateType.package) { | |
| // The package template does not supports any platform. | |
| includeIos = false; | |
| includeAndroid = false; | |
| includeWeb = false; | |
| includeLinux = false; | |
| includeMacos = false; | |
| includeWindows = false; | |
| includeDarwin = false; | |
| } else { | |
| final bool darwinRequested = platforms.contains('darwin'); | |
| final bool darwinSupported = | |
| (template == FlutterTemplateType.plugin) && | |
| featureFlags.isIOSEnabled && | |
| featureFlags.isMacOSEnabled; | |
| if (darwinRequested && darwinSupported) { | |
| includeDarwin = true; | |
| includeIos = true; | |
| includeMacos = true; | |
| } else { | |
| includeDarwin = false; | |
| includeIos = featureFlags.isIOSEnabled && platforms.contains('ios'); | |
| includeMacos = featureFlags.isMacOSEnabled && platforms.contains('macos'); | |
| if (darwinRequested && !darwinSupported) { | |
| globals.printWarning( | |
| 'Warning: To use the "darwin" platform, you must have both iOS and macOS enabled.\n' | |
| 'Run "flutter config --enable-ios --enable-macos-desktop" and try again.', | |
| ); | |
| } | |
| } | |
| includeAndroid = featureFlags.isAndroidEnabled && platforms.contains('android'); | |
| includeWeb = featureFlags.isWebEnabled && platforms.contains('web'); | |
| includeLinux = featureFlags.isLinuxEnabled && platforms.contains('linux'); | |
| includeWindows = featureFlags.isWindowsEnabled && platforms.contains('windows'); | |
| } | |
| String? developmentTeam; | |
| if (includeIos) { | |
| developmentTeam = await getCodeSigningIdentityDevelopmentTeam( | |
| processManager: globals.processManager, | |
| platform: globals.platform, | |
| logger: globals.logger, | |
| config: globals.config, | |
| terminal: globals.terminal, | |
| fileSystem: globals.fs, | |
| fileSystemUtils: globals.fsUtils, | |
| plistParser: globals.plistParser, | |
| ); | |
| } | |
| // The dart project_name is in snake_case, this variable is the Title Case of the Project Name. | |
| final String titleCaseProjectName = snakeCaseToTitleCase(projectName); | |
| final bool withSwiftPackageManager = featureFlags.isSwiftPackageManagerEnabled; | |
| final Map<String, Object?> templateContext = createTemplateContext( | |
| organization: organization, | |
| projectName: projectName, | |
| titleCaseProjectName: titleCaseProjectName, | |
| projectDescription: stringArg('description'), | |
| flutterRoot: flutterRoot, | |
| withPlatformChannelPluginHook: generateMethodChannelsPlugin, | |
| withSwiftPackageManager: withSwiftPackageManager, | |
| withFfiPluginHook: generateFfiPlugin, | |
| withFfiPackage: generateFfiPackage, | |
| withEmptyMain: emptyArgument, | |
| androidLanguage: stringArg('android-language'), | |
| iosDevelopmentTeam: developmentTeam, | |
| ios: includeIos, | |
| android: includeAndroid, | |
| darwin: includeDarwin, | |
| web: includeWeb, | |
| linux: includeLinux, | |
| macos: includeMacos, | |
| windows: includeWindows, | |
| dartSdkVersionBounds: '^$dartSdk', | |
| implementationTests: boolArg('implementation-tests'), | |
| agpVersion: gradle.templateAndroidGradlePluginVersion, | |
| kotlinVersion: gradle.templateKotlinGradlePluginVersion, | |
| gradleVersion: gradle.templateDefaultGradleVersion, | |
| ); | |
| final String relativeDirPath = globals.fs.path.relative(projectDirPath); | |
| final bool creatingNewProject = !projectDir.existsSync() || projectDir.listSync().isEmpty; | |
| if (creatingNewProject) { | |
| globals.printStatus('Creating project $relativeDirPath...'); | |
| } else { | |
| if (sampleCode != null && !overwrite) { | |
| throwToolExit( | |
| 'Will not overwrite existing project in $relativeDirPath: ' | |
| 'must specify --overwrite for samples to overwrite.', | |
| ); | |
| } | |
| globals.printStatus('Recreating project $relativeDirPath...'); | |
| } | |
| final Directory relativeDir = globals.fs.directory(projectDirPath); | |
| var generatedFileCount = 0; | |
| final PubContext pubContext; | |
| switch (template) { | |
| case FlutterTemplateType.app: | |
| final bool skipWidgetTestsGeneration = sampleCode != null || emptyArgument; | |
| generatedFileCount += await generateApp( | |
| <String>['app', if (!skipWidgetTestsGeneration) 'app_test_widget'], | |
| relativeDir, | |
| templateContext, | |
| overwrite: overwrite, | |
| printStatusWhenWriting: !creatingNewProject, | |
| projectType: template, | |
| ); | |
| pubContext = PubContext.create; | |
| case FlutterTemplateType.module: | |
| generatedFileCount += await _generateModule( | |
| relativeDir, | |
| templateContext, | |
| overwrite: overwrite, | |
| printStatusWhenWriting: !creatingNewProject, | |
| ); | |
| pubContext = PubContext.create; | |
| case FlutterTemplateType.package: | |
| generatedFileCount += await _generatePackage( | |
| relativeDir, | |
| templateContext, | |
| overwrite: overwrite, | |
| printStatusWhenWriting: !creatingNewProject, | |
| ); | |
| pubContext = PubContext.createPackage; | |
| case FlutterTemplateType.plugin: | |
| generatedFileCount += await _generateMethodChannelPlugin( | |
| relativeDir, | |
| templateContext, | |
| overwrite: overwrite, | |
| printStatusWhenWriting: !creatingNewProject, | |
| projectType: template, | |
| ); | |
| pubContext = PubContext.createPlugin; | |
| case FlutterTemplateType.pluginFfi: | |
| generatedFileCount += await _generateFfiPlugin( | |
| relativeDir, | |
| templateContext, | |
| overwrite: overwrite, | |
| printStatusWhenWriting: !creatingNewProject, | |
| projectType: template, | |
| ); | |
| pubContext = PubContext.createPlugin; | |
| case FlutterTemplateType.packageFfi: | |
| generatedFileCount += await _generateFfiPackage( | |
| relativeDir, | |
| templateContext, | |
| overwrite: overwrite, | |
| printStatusWhenWriting: !creatingNewProject, | |
| projectType: template, | |
| ); | |
| pubContext = PubContext.createPackage; | |
| } | |
| _generatePubspecLock(relativeDir); | |
| if (shouldCallPubGet) { | |
| final FlutterProject project = FlutterProject.fromDirectory(relativeDir); | |
| await pub.get( | |
| context: pubContext, | |
| project: project, | |
| offline: offline, | |
| outputMode: PubOutputMode.summaryOnly, | |
| ); | |
| // Setting `includeIos` etc to false as with FlutterProjectType.package | |
| // causes the example sub directory to not get os sub directories. | |
| // This will lead to `flutter build ios` to fail in the example. | |
| // TODO(dacoharkes): Uncouple the app and parent project platforms. https://github.com/flutter/flutter/issues/133874 | |
| // Then this if can be removed. | |
| if (!generateFfiPackage) { | |
| // TODO(matanlurey): https://github.com/flutter/flutter/issues/163774. | |
| // | |
| // `flutter packages get` inherently is neither a debug or release build, | |
| // and since a future build (`flutter build apk`) will regenerate tooling | |
| // anyway, we assume this is fine. | |
| // | |
| // It won't be if they do `flutter build --no-pub`, though. | |
| const ignoreReleaseModeSinceItsNotABuildAndHopeItWorks = false; | |
| await project.ensureReadyForPlatformSpecificTooling( | |
| releaseMode: ignoreReleaseModeSinceItsNotABuildAndHopeItWorks, | |
| androidPlatform: includeAndroid, | |
| iosPlatform: includeIos || includeDarwin, | |
| linuxPlatform: includeLinux, | |
| macOSPlatform: includeMacos || includeDarwin, | |
| windowsPlatform: includeWindows, | |
| webPlatform: includeWeb, | |
| ); | |
| } | |
| } | |
| if (sampleCode != null) { | |
| _applySample(relativeDir, sampleCode); | |
| } | |
| globals.printStatus('Wrote $generatedFileCount files.'); | |
| globals.printStatus('\nAll done!'); | |
| final application = | |
| '${emptyArgument ? 'empty ' : ''}${sampleCode != null ? 'sample ' : ''}application'; | |
| if (generatePackage) { | |
| final String relativeMainPath = globals.fs.path.normalize( | |
| globals.fs.path.join(relativeDirPath, 'lib', '${templateContext['projectName']}.dart'), | |
| ); | |
| globals.printStatus('Your package code is in $relativeMainPath'); | |
| } else if (generateModule) { | |
| final String relativeMainPath = globals.fs.path.normalize( | |
| globals.fs.path.join(relativeDirPath, 'lib', 'main.dart'), | |
| ); | |
| globals.printStatus('Your module code is in $relativeMainPath.'); | |
| } else if (generateMethodChannelsPlugin || generateFfiPlugin) { | |
| final String relativePluginPath = globals.fs.path.normalize( | |
| globals.fs.path.relative(projectDirPath), | |
| ); | |
| final List<String> requestedPlatforms = _getUserRequestedPlatforms(); | |
| final String platformsString = requestedPlatforms.join(', '); | |
| _printPluginDirectoryLocationMessage(relativePluginPath, projectName, platformsString); | |
| if (!creatingNewProject && requestedPlatforms.isNotEmpty) { | |
| _printPluginUpdatePubspecMessage(relativePluginPath, platformsString); | |
| } else if (_getSupportedPlatformsInPlugin(projectDir).isEmpty) { | |
| _printNoPluginMessage(); | |
| } | |
| final List<String> platformsToWarn = _getPlatformWarningList(requestedPlatforms); | |
| if (platformsToWarn.isNotEmpty) { | |
| _printWarningDisabledPlatform(platformsToWarn); | |
| } | |
| final template = generateMethodChannelsPlugin ? 'plugin' : 'plugin_ffi'; | |
| _printPluginAddPlatformMessage(relativePluginPath, template); | |
| } else { | |
| // Tell the user the next steps. | |
| final FlutterProject project = FlutterProject.fromDirectory( | |
| globals.fs.directory(projectDirPath), | |
| ); | |
| final FlutterProject app = project.hasExampleApp ? project.example : project; | |
| final String relativeAppPath = globals.fs.path.normalize( | |
| globals.fs.path.relative(app.directory.path), | |
| ); | |
| final String relativeAppMain = globals.fs.path.join(relativeAppPath, 'lib', 'main.dart'); | |
| final List<String> requestedPlatforms = _getUserRequestedPlatforms(); | |
| final String commandsToRun = [ | |
| if (relativeAppPath != '.') ' \$ cd $relativeAppPath', | |
| r' $ flutter run', | |
| ].join('\n'); | |
| // Let them know a summary of the state of their tooling. | |
| globals.printStatus(''' | |
| You can find general documentation for Flutter at: https://docs.flutter.dev/ | |
| Detailed API documentation is available at: https://api.flutter.dev/ | |
| If you prefer video documentation, consider: https://www.youtube.com/c/flutterdev | |
| In order to run your $application, type: | |
| $commandsToRun | |
| Your $application code is in $relativeAppMain. | |
| '''); | |
| // Show warning if any selected platform is not enabled | |
| final List<String> platformsToWarn = _getPlatformWarningList(requestedPlatforms); | |
| if (platformsToWarn.isNotEmpty) { | |
| _printWarningDisabledPlatform(platformsToWarn); | |
| } | |
| } | |
| // Show warning for Java/AGP or Java/Gradle incompatibility if building for | |
| // Android and Java version has been detected. | |
| if (includeAndroid && globals.java?.version != null) { | |
| _printIncompatibleJavaAgpGradleVersionsWarning( | |
| javaVersion: versionToParsableString(globals.java?.version)!, | |
| templateGradleVersion: templateContext['gradleVersion']! as String, | |
| templateAgpVersion: templateContext['agpVersion']! as String, | |
| templateAgpVersionForModule: templateContext['agpVersionForModule']! as String, | |
| projectType: template, | |
| projectDirPath: projectDirPath, | |
| ); | |
| } | |
| return FlutterCommandResult.success(); | |
| } | |
| Future<int> _generateModule( | |
| Directory directory, | |
| Map<String, Object?> templateContext, { | |
| bool overwrite = false, | |
| bool printStatusWhenWriting = true, | |
| }) async { | |
| var generatedCount = 0; | |
| final String? description = argResults!.wasParsed('description') | |
| ? stringArg('description') | |
| : 'A new Flutter module project.'; | |
| templateContext['description'] = description; | |
| generatedCount += await renderTemplate( | |
| globals.fs.path.join('module', 'common'), | |
| directory, | |
| templateContext, | |
| overwrite: overwrite, | |
| printStatusWhenWriting: printStatusWhenWriting, | |
| ); | |
| return generatedCount; | |
| } | |
| Future<int> _generatePackage( | |
| Directory directory, | |
| Map<String, Object?> templateContext, { | |
| bool overwrite = false, | |
| bool printStatusWhenWriting = true, | |
| }) async { | |
| var generatedCount = 0; | |
| final String? description = argResults!.wasParsed('description') | |
| ? stringArg('description') | |
| : 'A new Flutter package project.'; | |
| templateContext['description'] = description; | |
| generatedCount += await renderTemplate( | |
| 'package', | |
| directory, | |
| templateContext, | |
| overwrite: overwrite, | |
| printStatusWhenWriting: printStatusWhenWriting, | |
| ); | |
| return generatedCount; | |
| } | |
| Future<int> _generateMethodChannelPlugin( | |
| Directory directory, | |
| Map<String, Object?> templateContext, { | |
| bool overwrite = false, | |
| bool printStatusWhenWriting = true, | |
| required FlutterTemplateType projectType, | |
| }) async { | |
| // Plugins only add a platform if it was requested explicitly by the user. | |
| if (!argResults!.wasParsed('platforms')) { | |
| for (final String platform in kAllCreatePlatforms) { | |
| templateContext[platform] = false; | |
| } | |
| } | |
| final List<String> platformsToAdd = _getSupportedPlatformsFromTemplateContext(templateContext); | |
| final List<String> existingPlatforms = _getSupportedPlatformsInPlugin(directory); | |
| for (final existingPlatform in existingPlatforms) { | |
| // re-generate files for existing platforms | |
| templateContext[existingPlatform] = true; | |
| } | |
| final bool willAddPlatforms = platformsToAdd.isNotEmpty; | |
| templateContext['no_platforms'] = !willAddPlatforms; | |
| var generatedCount = 0; | |
| final String? description = argResults!.wasParsed('description') | |
| ? stringArg('description') | |
| : 'A new Flutter plugin project.'; | |
| templateContext['description'] = description; | |
| final projectName = templateContext['projectName'] as String?; | |
| final templates = <String>['plugin', 'plugin_shared']; | |
| final ({bool originalIos, bool originalMacos, bool? originalWithSwiftPackageManager}) | |
| darwinState = _updateContextAndTemplatesForDarwin( | |
| templateContext: templateContext, | |
| templates: templates, | |
| directory: directory, | |
| projectName: projectName, | |
| ); | |
| generatedCount += await renderMerged( | |
| templates, | |
| directory, | |
| templateContext, | |
| overwrite: overwrite, | |
| printStatusWhenWriting: printStatusWhenWriting, | |
| ); | |
| // Restore the original ios, macos, and withSwiftPackageManager values. | |
| // This is necessary in case the user requested the darwin platform, | |
| // and we need to restore them for the example app generation. | |
| templateContext['ios'] = darwinState.originalIos; | |
| templateContext['macos'] = darwinState.originalMacos; | |
| if (darwinState.originalWithSwiftPackageManager != null) { | |
| templateContext['withSwiftPackageManager'] = darwinState.originalWithSwiftPackageManager; | |
| } else { | |
| templateContext.remove('withSwiftPackageManager'); | |
| } | |
| final FlutterProject project = FlutterProject.fromDirectory(directory); | |
| final generateAndroid = templateContext['android'] == true; | |
| if (generateAndroid) { | |
| gradle.updateLocalProperties(project: project, requireAndroidSdk: false); | |
| } | |
| final organization = | |
| templateContext['organization']! as String; // Required to make the context. | |
| final androidPluginIdentifier = templateContext['androidIdentifier'] as String?; | |
| final exampleProjectName = '${projectName}_example'; | |
| templateContext['projectName'] = exampleProjectName; | |
| templateContext['androidIdentifier'] = CreateBase.createAndroidIdentifier( | |
| organization, | |
| exampleProjectName, | |
| ); | |
| templateContext['iosIdentifier'] = CreateBase.createUTIIdentifier( | |
| organization, | |
| exampleProjectName, | |
| ); | |
| templateContext['macosIdentifier'] = CreateBase.createUTIIdentifier( | |
| organization, | |
| exampleProjectName, | |
| ); | |
| templateContext['windowsIdentifier'] = CreateBase.createWindowsIdentifier( | |
| organization, | |
| exampleProjectName, | |
| ); | |
| templateContext['description'] = 'Demonstrates how to use the $projectName plugin.'; | |
| templateContext['pluginProjectName'] = projectName; | |
| templateContext['androidPluginIdentifier'] = androidPluginIdentifier; | |
| generatedCount += await generateApp( | |
| <String>['app', 'app_test_widget', 'app_integration_test'], | |
| project.example.directory, | |
| templateContext, | |
| overwrite: overwrite, | |
| pluginExampleApp: true, | |
| printStatusWhenWriting: printStatusWhenWriting, | |
| projectType: projectType, | |
| ); | |
| return generatedCount; | |
| } | |
| Future<int> _generateFfiPlugin( | |
| Directory directory, | |
| Map<String, Object?> templateContext, { | |
| bool overwrite = false, | |
| bool printStatusWhenWriting = true, | |
| required FlutterTemplateType projectType, | |
| }) async { | |
| // Plugins only add a platform if it was requested explicitly by the user. | |
| if (!argResults!.wasParsed('platforms')) { | |
| for (final String platform in kAllCreatePlatforms) { | |
| templateContext[platform] = false; | |
| } | |
| } | |
| final List<String> platformsToAdd = _getSupportedPlatformsFromTemplateContext(templateContext); | |
| final List<String> existingPlatforms = _getSupportedPlatformsInPlugin(directory); | |
| for (final existingPlatform in existingPlatforms) { | |
| // re-generate files for existing platforms | |
| templateContext[existingPlatform] = true; | |
| } | |
| final bool willAddPlatforms = platformsToAdd.isNotEmpty; | |
| templateContext['no_platforms'] = !willAddPlatforms; | |
| var generatedCount = 0; | |
| final String? description = argResults!.wasParsed('description') | |
| ? stringArg('description') | |
| : 'A new Flutter FFI plugin project.'; | |
| templateContext['description'] = description; | |
| generatedCount += await renderMerged( | |
| <String>['plugin_ffi', 'plugin_shared'], | |
| directory, | |
| templateContext, | |
| overwrite: overwrite, | |
| printStatusWhenWriting: printStatusWhenWriting, | |
| ); | |
| final FlutterProject project = FlutterProject.fromDirectory(directory); | |
| final generateAndroid = templateContext['android'] == true; | |
| if (generateAndroid) { | |
| gradle.updateLocalProperties(project: project, requireAndroidSdk: false); | |
| } | |
| final projectName = templateContext['projectName'] as String?; | |
| final organization = | |
| templateContext['organization']! as String; // Required to make the context. | |
| final androidPluginIdentifier = templateContext['androidIdentifier'] as String?; | |
| final exampleProjectName = '${projectName}_example'; | |
| templateContext['projectName'] = exampleProjectName; | |
| templateContext['androidIdentifier'] = CreateBase.createAndroidIdentifier( | |
| organization, | |
| exampleProjectName, | |
| ); | |
| templateContext['iosIdentifier'] = CreateBase.createUTIIdentifier( | |
| organization, | |
| exampleProjectName, | |
| ); | |
| templateContext['macosIdentifier'] = CreateBase.createUTIIdentifier( | |
| organization, | |
| exampleProjectName, | |
| ); | |
| templateContext['windowsIdentifier'] = CreateBase.createWindowsIdentifier( | |
| organization, | |
| exampleProjectName, | |
| ); | |
| templateContext['description'] = 'Demonstrates how to use the $projectName plugin.'; | |
| templateContext['pluginProjectName'] = projectName; | |
| templateContext['androidPluginIdentifier'] = androidPluginIdentifier; | |
| generatedCount += await generateApp( | |
| <String>['app'], | |
| project.example.directory, | |
| templateContext, | |
| overwrite: overwrite, | |
| pluginExampleApp: true, | |
| printStatusWhenWriting: printStatusWhenWriting, | |
| projectType: projectType, | |
| ); | |
| return generatedCount; | |
| } | |
| Future<int> _generateFfiPackage( | |
| Directory directory, | |
| Map<String, Object?> templateContext, { | |
| bool overwrite = false, | |
| bool printStatusWhenWriting = true, | |
| required FlutterTemplateType projectType, | |
| }) async { | |
| var generatedCount = 0; | |
| final String? description = argResults!.wasParsed('description') | |
| ? stringArg('description') | |
| : 'A new Dart FFI package project.'; | |
| templateContext['description'] = description; | |
| generatedCount += await renderMerged( | |
| <String>['package_ffi'], | |
| directory, | |
| templateContext, | |
| overwrite: overwrite, | |
| printStatusWhenWriting: printStatusWhenWriting, | |
| ); | |
| final FlutterProject project = FlutterProject.fromDirectory(directory); | |
| final projectName = templateContext['projectName'] as String?; | |
| final exampleProjectName = '${projectName}_example'; | |
| templateContext['projectName'] = exampleProjectName; | |
| templateContext['description'] = 'Demonstrates how to use the $projectName package.'; | |
| templateContext['pluginProjectName'] = projectName; | |
| generatedCount += await generateApp( | |
| <String>['app'], | |
| project.example.directory, | |
| templateContext, | |
| overwrite: overwrite, | |
| pluginExampleApp: true, | |
| printStatusWhenWriting: printStatusWhenWriting, | |
| projectType: projectType, | |
| ); | |
| return generatedCount; | |
| } | |
| // Takes an application template and replaces the main.dart with one from the | |
| // documentation website in sampleCode. Returns the difference in the number | |
| // of files after applying the sample, since it also deletes the application's | |
| // test directory (since the template's test doesn't apply to the sample). | |
| void _applySample(Directory directory, String sampleCode) { | |
| final File mainDartFile = directory.childDirectory('lib').childFile('main.dart'); | |
| mainDartFile.createSync(recursive: true); | |
| mainDartFile.writeAsStringSync(sampleCode); | |
| } | |
| List<String> _getSupportedPlatformsFromTemplateContext(Map<String, Object?> templateContext) { | |
| return <String>[ | |
| for (final String platform in kAllCreatePlatforms) | |
| if (templateContext[platform] == true) platform, | |
| ]; | |
| } | |
| // Returns a list of platforms that are explicitly requested by user via `--platforms`. | |
| List<String> _getUserRequestedPlatforms() { | |
| if (!argResults!.wasParsed('platforms')) { | |
| return <String>[]; | |
| } | |
| return stringsArg('platforms'); | |
| } | |
| ({bool originalIos, bool originalMacos, bool? originalWithSwiftPackageManager}) | |
| _updateContextAndTemplatesForDarwin({ | |
| required Map<String, Object?> templateContext, | |
| required List<String> templates, | |
| required Directory directory, | |
| required String? projectName, | |
| }) { | |
| final bool includeDarwin = templateContext['darwin'] as bool? ?? false; | |
| final bool originalIos = templateContext['ios'] as bool? ?? false; | |
| final bool originalMacos = templateContext['macos'] as bool? ?? false; | |
| final originalWithSwiftPackageManager = templateContext['withSwiftPackageManager'] as bool?; | |
| if (includeDarwin) { | |
| // Temporarily disable ios/macos for the plugin generation | |
| // so we don't get ios/ and macos/ directories in the plugin root. | |
| templateContext['ios'] = false; | |
| templateContext['macos'] = false; | |
| } | |
| final bool includeApplePlatforms = | |
| templateContext['ios'] == true || templateContext['macos'] == true || includeDarwin; | |
| if (!includeApplePlatforms) { | |
| return ( | |
| originalIos: originalIos, | |
| originalMacos: originalMacos, | |
| originalWithSwiftPackageManager: originalWithSwiftPackageManager, | |
| ); | |
| } | |
| // Check if this is an existing plugin with CocoaPods structure (Classes/ directory). | |
| // If so, preserve backward compatibility by using CocoaPods templates. | |
| // For new plugins, use SwiftPM structure. | |
| final bool hasExistingCocoaPodsStructure = const <String>['ios', 'macos', 'darwin'].any( | |
| (String platform) => | |
| directory.childDirectory(platform).childDirectory('Classes').existsSync(), | |
| ); | |
| final bool useSwiftPackageManagerStructure = !hasExistingCocoaPodsStructure; | |
| templateContext['withSwiftPackageManager'] = useSwiftPackageManagerStructure; | |
| if (useSwiftPackageManagerStructure) { | |
| if (includeDarwin) { | |
| templates.add('plugin_darwin_spm'); | |
| } else { | |
| templates.add('plugin_swift_package_manager'); | |
| } | |
| templateContext['swiftLibraryName'] = projectName?.replaceAll('_', '-'); | |
| templateContext['swiftToolsVersion'] = minimumSwiftToolchainVersion; | |
| templateContext['iosSupportedPlatform'] = FlutterDarwinPlatform.ios.supportedPackagePlatform | |
| .format(); | |
| templateContext['macosSupportedPlatform'] = FlutterDarwinPlatform | |
| .macos | |
| .supportedPackagePlatform | |
| .format(); | |
| } else { | |
| // Existing plugin with CocoaPods structure: preserve backward compatibility. | |
| if (includeDarwin) { | |
| templates.add('plugin_darwin_cocoapods'); | |
| } else { | |
| templates.add('plugin_cocoapods'); | |
| } | |
| } | |
| return ( | |
| originalIos: originalIos, | |
| originalMacos: originalMacos, | |
| originalWithSwiftPackageManager: originalWithSwiftPackageManager, | |
| ); | |
| } | |
| } | |
| // Determine what platforms are supported based on generated files. | |
| List<String> _getSupportedPlatformsInPlugin(Directory projectDir) { | |
| final String pubspecPath = globals.fs.path.join(projectDir.absolute.path, 'pubspec.yaml'); | |
| final FlutterManifest? manifest = FlutterManifest.createFromPath( | |
| pubspecPath, | |
| fileSystem: globals.fs, | |
| logger: globals.logger, | |
| ); | |
| final Map<String, Object?>? validSupportedPlatforms = manifest?.validSupportedPlatforms; | |
| final List<String> platforms = validSupportedPlatforms == null | |
| ? <String>[] | |
| : validSupportedPlatforms.keys.toList(); | |
| return platforms; | |
| } | |
| void _printPluginDirectoryLocationMessage( | |
| String pluginPath, | |
| String projectName, | |
| String platformsString, | |
| ) { | |
| final String relativePluginMain = globals.fs.path.join(pluginPath, 'lib', '$projectName.dart'); | |
| final String relativeExampleMain = globals.fs.path.join( | |
| pluginPath, | |
| 'example', | |
| 'lib', | |
| 'main.dart', | |
| ); | |
| globals.printStatus(''' | |
| Your plugin code is in $relativePluginMain. | |
| Your example app code is in $relativeExampleMain. | |
| '''); | |
| if (platformsString.isNotEmpty) { | |
| globals.printStatus(''' | |
| Host platform code is in the $platformsString directories under $pluginPath. | |
| To edit platform code in an IDE see https://flutter.dev/to/edit-plugins. | |
| '''); | |
| } | |
| } | |
| void _printPluginUpdatePubspecMessage(String pluginPath, String platformsString) { | |
| globals.printStatus( | |
| ''' | |
| You need to update $pluginPath/pubspec.yaml to support $platformsString. | |
| ''', | |
| emphasis: true, | |
| color: TerminalColor.red, | |
| ); | |
| } | |
| void _printNoPluginMessage() { | |
| globals.printError(''' | |
| You've created a plugin project that doesn't yet support any platforms. | |
| '''); | |
| } | |
| void _printPluginAddPlatformMessage(String pluginPath, String template) { | |
| globals.printStatus(''' | |
| To add platforms, run `flutter create -t $template --platforms <platforms> .` under $pluginPath. | |
| For more information, see https://flutter.dev/to/pubspec-plugin-platforms. | |
| '''); | |
| } | |
| // returns a list disabled, but requested platforms | |
| List<String> _getPlatformWarningList(List<String> requestedPlatforms) { | |
| final platformsToWarn = <String>[ | |
| if (requestedPlatforms.contains('web') && !featureFlags.isWebEnabled) 'web', | |
| if (requestedPlatforms.contains('macos') && !featureFlags.isMacOSEnabled) 'macos', | |
| if (requestedPlatforms.contains('windows') && !featureFlags.isWindowsEnabled) 'windows', | |
| if (requestedPlatforms.contains('linux') && !featureFlags.isLinuxEnabled) 'linux', | |
| if (requestedPlatforms.contains('darwin') && | |
| !featureFlags.isMacOSEnabled && | |
| !featureFlags.isIOSEnabled) | |
| 'darwin', | |
| ]; | |
| return platformsToWarn; | |
| } | |
| void _printWarningDisabledPlatform(List<String> platforms) { | |
| final desktop = <String>[]; | |
| final web = <String>[]; | |
| final darwin = <String>[]; | |
| for (final platform in platforms) { | |
| switch (platform) { | |
| case 'web': | |
| web.add(platform); | |
| case 'macos' || 'windows' || 'linux': | |
| desktop.add(platform); | |
| case 'darwin': | |
| darwin.add(platform); | |
| } | |
| } | |
| if (desktop.isNotEmpty) { | |
| final platforms = desktop.length > 1 ? 'platforms' : 'platform'; | |
| final verb = desktop.length > 1 ? 'are' : 'is'; | |
| globals.printStatus(''' | |
| The desktop $platforms: ${desktop.join(', ')} $verb currently not supported on your local environment. | |
| For more details, see: https://flutter.dev/to/add-desktop-support | |
| '''); | |
| } | |
| if (web.isNotEmpty) { | |
| globals.printStatus(''' | |
| The web is currently not supported on your local environment. | |
| For more details, see: https://flutter.dev/to/add-web-support | |
| '''); | |
| } | |
| if (darwin.isNotEmpty) { | |
| globals.printStatus(''' | |
| The darwin platform is currently not supported on your local environment. | |
| You must have a macOS host with Xcode installed to develop for iOS or macOS. | |
| '''); | |
| } | |
| } | |
| // Prints a warning if the specified Java version conflicts with either the | |
| // template Gradle or AGP version. | |
| // | |
| // Assumes the specified templateGradleVersion and templateAgpVersion are | |
| // compatible, meaning that the Java version may only conflict with one of the | |
| // template Gradle or AGP versions. | |
| void _printIncompatibleJavaAgpGradleVersionsWarning({ | |
| required String javaVersion, | |
| required String templateGradleVersion, | |
| required String templateAgpVersion, | |
| required String templateAgpVersionForModule, | |
| required FlutterTemplateType projectType, | |
| required String projectDirPath, | |
| }) { | |
| // Determine if the Java version specified conflicts with the template Gradle or AGP version. | |
| final bool javaGradleVersionsCompatible = gradle.validateJavaAndGradle( | |
| globals.logger, | |
| javaVersion: javaVersion, | |
| gradleVersion: templateGradleVersion, | |
| ); | |
| bool javaAgpVersionsCompatible = gradle.validateJavaAndAgp( | |
| globals.logger, | |
| javaV: javaVersion, | |
| agpV: templateAgpVersion, | |
| ); | |
| var relevantTemplateAgpVersion = templateAgpVersion; | |
| if (projectType == FlutterTemplateType.module && | |
| Version.parse(templateAgpVersion)! < Version.parse(templateAgpVersionForModule)!) { | |
| // If a module is being created, make sure to check for Java/AGP compatibility between the highest used version of AGP in the module template. | |
| javaAgpVersionsCompatible = gradle.validateJavaAndAgp( | |
| globals.logger, | |
| javaV: javaVersion, | |
| agpV: templateAgpVersionForModule, | |
| ); | |
| relevantTemplateAgpVersion = templateAgpVersionForModule; | |
| } | |
| if (javaGradleVersionsCompatible && javaAgpVersionsCompatible) { | |
| return; | |
| } | |
| // Determine header of warning with recommended fix of re-configuring Java version. | |
| final String incompatibleVersionsAndRecommendedOptionMessage = | |
| getIncompatibleJavaGradleAgpMessageHeader( | |
| javaGradleVersionsCompatible, | |
| templateGradleVersion, | |
| relevantTemplateAgpVersion, | |
| projectType.cliName, | |
| ); | |
| if (!javaGradleVersionsCompatible) { | |
| if (projectType == FlutterTemplateType.plugin || projectType == FlutterTemplateType.pluginFfi) { | |
| // Only impacted files could be in sample code. | |
| return; | |
| } | |
| // Gradle template version incompatible with Java version. | |
| final gradle.JavaGradleCompat? validCompatibleGradleVersionRange = gradle | |
| .getValidGradleVersionRangeForJavaVersion(globals.logger, javaV: javaVersion); | |
| final compatibleGradleVersionMessage = validCompatibleGradleVersionRange == null | |
| ? '' | |
| : ' (compatible Gradle version range: ${validCompatibleGradleVersionRange.gradleMin} - ${validCompatibleGradleVersionRange.gradleMax})'; | |
| globals.printWarning(''' | |
| $incompatibleVersionsAndRecommendedOptionMessage | |
| Alternatively, to continue using your configured Java version, update the Gradle | |
| version specified in the following file to a compatible Gradle version$compatibleGradleVersionMessage: | |
| ${_getGradleWrapperPropertiesFilePath(projectType, projectDirPath)} | |
| You may also update the Gradle version used by running | |
| `./gradlew wrapper --gradle-version=<COMPATIBLE_GRADLE_VERSION>`. | |
| See | |
| https://docs.gradle.org/current/userguide/compatibility.html#java for details | |
| on compatible Java/Gradle versions, and see | |
| https://docs.gradle.org/current/userguide/gradle_wrapper.html#sec:upgrading_wrapper | |
| for more details on using the Gradle Wrapper command to update the Gradle version | |
| used. | |
| ''', emphasis: true); | |
| return; | |
| } | |
| // AGP template version incompatible with Java version. | |
| final gradle.JavaAgpCompat? minimumCompatibleAgpVersion = gradle | |
| .getMinimumAgpVersionForJavaVersion(globals.logger, javaV: javaVersion); | |
| final compatibleAgpVersionMessage = minimumCompatibleAgpVersion == null | |
| ? '' | |
| : ' (minimum compatible AGP version: ${minimumCompatibleAgpVersion.agpMin})'; | |
| final gradleBuildFilePaths = | |
| ' ${_getBuildGradleConfigurationFilePaths(projectType, projectDirPath)!.join('\n - ')}'; | |
| globals.printWarning(''' | |
| $incompatibleVersionsAndRecommendedOptionMessage | |
| Alternatively, to continue using your current Java version, update the AGP | |
| version in the following file(s) to a compatible version$compatibleAgpVersionMessage: | |
| $gradleBuildFilePaths | |
| For details on compatible Java and AGP versions, see | |
| https://developer.android.com/build/releases/gradle-plugin | |
| ''', emphasis: true); | |
| } | |
| // Returns incompatible Java/template Gradle/template AGP message header based | |
| // on incompatibility and project type. | |
| @visibleForTesting | |
| String getIncompatibleJavaGradleAgpMessageHeader( | |
| bool javaGradleVersionsCompatible, | |
| String templateGradleVersion, | |
| String templateAgpVersion, | |
| String projectType, | |
| ) { | |
| final incompatibleDependency = javaGradleVersionsCompatible | |
| ? 'Android Gradle Plugin (AGP)' | |
| : 'Gradle'; | |
| final incompatibleDependencyVersion = javaGradleVersionsCompatible | |
| ? 'AGP version $templateAgpVersion' | |
| : 'Gradle version $templateGradleVersion'; | |
| final VersionRange validJavaRange = gradle.getJavaVersionFor( | |
| gradleV: templateGradleVersion, | |
| agpV: templateAgpVersion, | |
| ); | |
| // validJavaRange should have non-null versionMin and versionMax since it based on our template AGP and Gradle versions. | |
| final validJavaRangeMessage = | |
| '(Java ${validJavaRange.versionMin!} <= compatible Java version < Java ${validJavaRange.versionMax!})'; | |
| return ''' | |
| The configured version of Java detected may conflict with the $incompatibleDependency version in your new Flutter $projectType. | |
| To keep the default $incompatibleDependencyVersion, download a compatible Java version | |
| $validJavaRangeMessage. Configure this Java version globally for Flutter by running: | |
| flutter config --jdk-dir=<JDK_DIRECTORY> | |
| '''; | |
| } | |
| // Returns path of the gradle-wrapper.properties file for the specified | |
| // generated project type. | |
| String? _getGradleWrapperPropertiesFilePath( | |
| FlutterTemplateType projectType, | |
| String projectDirPath, | |
| ) { | |
| var gradleWrapperPropertiesFilePath = ''; | |
| switch (projectType) { | |
| case FlutterTemplateType.app: | |
| gradleWrapperPropertiesFilePath = globals.fs.path.join( | |
| projectDirPath, | |
| 'android/gradle/wrapper/gradle-wrapper.properties', | |
| ); | |
| case FlutterTemplateType.module: | |
| gradleWrapperPropertiesFilePath = globals.fs.path.join( | |
| projectDirPath, | |
| '.android/gradle/wrapper/gradle-wrapper.properties', | |
| ); | |
| case FlutterTemplateType.plugin: | |
| case FlutterTemplateType.pluginFfi: | |
| case FlutterTemplateType.package: | |
| case FlutterTemplateType.packageFfi: | |
| // TODO(camsim99): Add relevant file path for packageFfi when Android is supported. | |
| // No gradle-wrapper.properties files not part of sample code that | |
| // can be determined. | |
| return null; | |
| } | |
| return gradleWrapperPropertiesFilePath; | |
| } | |
| // Returns the path(s) of the build.gradle file(s) for the specified generated | |
| // project type. | |
| List<String>? _getBuildGradleConfigurationFilePaths( | |
| FlutterTemplateType projectType, | |
| String projectDirPath, | |
| ) { | |
| final buildGradleConfigurationFilePaths = <String>[]; | |
| switch (projectType) { | |
| case FlutterTemplateType.app: | |
| case FlutterTemplateType.pluginFfi: | |
| buildGradleConfigurationFilePaths.add( | |
| globals.fs.path.join(projectDirPath, 'android/build.gradle'), | |
| ); | |
| case FlutterTemplateType.module: | |
| const moduleBuildGradleFilePath = '.android/build.gradle'; | |
| const moduleAppBuildGradleFlePath = '.android/app/build.gradle'; | |
| const moduleFlutterBuildGradleFilePath = '.android/Flutter/build.gradle'; | |
| buildGradleConfigurationFilePaths.addAll(<String>[ | |
| globals.fs.path.join(projectDirPath, moduleBuildGradleFilePath), | |
| globals.fs.path.join(projectDirPath, moduleAppBuildGradleFlePath), | |
| globals.fs.path.join(projectDirPath, moduleFlutterBuildGradleFilePath), | |
| ]); | |
| case FlutterTemplateType.plugin: | |
| buildGradleConfigurationFilePaths.add( | |
| globals.fs.path.join(projectDirPath, 'android/app/build.gradle'), | |
| ); | |
| case FlutterTemplateType.package: | |
| case FlutterTemplateType.packageFfi: | |
| // TODO(camsim99): Add any relevant file paths for packageFfi when Android is supported. | |
| // No build.gradle file because there is no platform-specific implementation. | |
| return null; | |
| } | |
| return buildGradleConfigurationFilePaths; | |
| } | |
| /// Generate a pubspec.lock file that locks the versions of all dependencies to | |
| /// the exact versions the Flutter SDK is tested with. | |
| /// | |
| /// This ensures that a breaking change accidentally published to one of these | |
| /// packages that the Flutter SDK depends on cannot break the `flutter create` | |
| /// command. | |
| void _generatePubspecLock(Directory directory) { | |
| final FileSystem fs = directory.fileSystem; | |
| final String flutterRoot = Cache.flutterRoot!; | |
| final flutterPubspecLock = | |
| loadYaml(fs.file(fs.path.join(flutterRoot, 'pubspec.lock')).readAsStringSync()) as YamlMap; | |
| final flutterPackages = flutterPubspecLock['packages'] as YamlMap; | |
| final packages = <String, Object?>{ | |
| for (final package in gatherSdkPackageDependencies(directory)) | |
| package: flutterPackages[package], | |
| }; | |
| /// We are writing json which is valid yaml. Pub will rewrite this file as pretty yaml after resolution. | |
| directory | |
| .childFile('pubspec.lock') | |
| .writeAsStringSync(const JsonEncoder.withIndent(' ').convert({'packages': packages})); | |
| } | |
| /// Find the package names of external dependencies from the SDK packages that | |
| /// the package in [directory] depends on. | |
| List<String> gatherSdkPackageDependencies(Directory directory) { | |
| final sdkPackages = <String>[]; | |
| final FileSystem fs = directory.fileSystem; | |
| final pubspecYaml = loadYaml(directory.childFile('pubspec.yaml').readAsStringSync()) as YamlMap; | |
| for (final MapEntry<dynamic, dynamic> dependency | |
| in (pubspecYaml['dependencies'] as YamlMap).entries) { | |
| final descriptor = dependency.value as Object?; | |
| if (descriptor is YamlMap && descriptor['sdk'] == 'flutter') { | |
| // a flutter dependency. | |
| final name = dependency.key as String; | |
| sdkPackages.add(name); | |
| } | |
| } | |
| final result = <String>{}; | |
| // Initialized by FlutterCommandRunner on startup. | |
| // So it is safe to access it here. | |
| final String flutterRoot = Cache.flutterRoot!; | |
| for (final sdkPackage in sdkPackages) { | |
| final pubspecYaml = | |
| loadYaml( | |
| fs | |
| .file(fs.path.join(flutterRoot, 'packages', sdkPackage, 'pubspec.yaml')) | |
| .readAsStringSync(), | |
| ) | |
| as YamlMap; | |
| for (final MapEntry<dynamic, dynamic> dependency | |
| in (pubspecYaml['dependencies'] as YamlMap).entries) { | |
| final descriptor = dependency.value as Object?; | |
| if (descriptor is String) { | |
| // a hosted dependency. | |
| final name = dependency.key as String; | |
| result.add(name); | |
| } | |
| } | |
| } | |
| return result.toList(); | |
| } |