// 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 '../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 '../convert.dart'; import '../dart/pub.dart'; import '../features.dart'; import '../flutter_manifest.dart'; import '../flutter_project_metadata.dart'; import '../globals.dart' as globals; import '../ios/code_signing.dart'; import '../project.dart'; import '../reporting/reporting.dart'; import '../runner/flutter_command.dart'; import 'create_base.dart'; const String 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 CreateBase { CreateCommand({ super.verboseHelp = false, }) { addPlatformsOptions(customHelp: kPlatformHelp); argParser.addOption( 'template', abbr: 't', allowed: FlutterProjectType.enabledValues .map<String>((FlutterProjectType e) => e.cliName), help: 'Specify the type of project to create.', valueHelp: 'type', allowedHelp: CliEnum.allowedHelp(FlutterProjectType.enabledValues), ); 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', ); 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', ); } @override final String name = 'create'; @override final String 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<CustomDimensions> get usageValues async { return CustomDimensions( commandCreateProjectType: stringArg('template'), commandCreateAndroidLanguage: stringArg('android-language'), commandCreateIosLanguage: stringArg('ios-language'), ); } @override Future<Event> unifiedAnalyticsUsageValues(String commandPath) async => Event.commandUsageValues( workflow: commandPath, commandHasTerminal: hasTerminal, createProjectType: stringArg('template'), createAndroidLanguage: stringArg('android-language'), createIosLanguage: stringArg('ios-language'), ); // Lazy-initialize the net utilities with values from the context. late final Net _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 Uri 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 Uri 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); } } FlutterProjectType _getProjectType(Directory projectDir) { FlutterProjectType? template; FlutterProjectType? detectedProjectType; final bool metadataExists = projectDir.absolute.childFile('.metadata').existsSync(); final String? templateArgument = stringArg('template'); if (templateArgument != null) { template = FlutterProjectType.fromCliName(templateArgument); } // 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 ?? FlutterProjectType.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 FlutterProjectType template = _getProjectType(projectDir); if (sampleArgument != null) { if (template != FlutterProjectType.app) { throwToolExit('Cannot specify --sample with a project type other than ' '"${FlutterProjectType.app.cliName}"'); } // Fetch the sample from the server. sampleCode = await _fetchSampleFromServer(sampleArgument); } if (emptyArgument && template != FlutterProjectType.app) { throwToolExit('The --empty flag is only supported for the app template.'); } final bool generateModule = template == FlutterProjectType.module; final bool generateMethodChannelsPlugin = template == FlutterProjectType.plugin; final bool generateFfiPackage = template == FlutterProjectType.packageFfi; final bool generateFfiPlugin = template == FlutterProjectType.pluginFfi; final bool generateFfi = generateFfiPlugin || generateFfiPackage; final bool generatePackage = template == FlutterProjectType.package; final List<String> platforms = stringsArg('platforms'); // `--platforms` does not support module or package. if (argResults!.wasParsed('platforms') && (generateModule || generatePackage || generateFfiPackage)) { final String 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('ios-language')) { throwToolExit( 'The "ios-language" option is not supported with the ${template.cliName} ' 'template: the language will always be C or C++.', 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, ); } 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 includeAndroid; final bool includeWeb; final bool includeLinux; final bool includeMacos; final bool includeWindows; if (template == FlutterProjectType.module) { // The module template only supports iOS and Android. includeIos = true; includeAndroid = true; includeWeb = false; includeLinux = false; includeMacos = false; includeWindows = false; } else if (template == FlutterProjectType.package) { // The package template does not supports any platform. includeIos = false; includeAndroid = false; includeWeb = false; includeLinux = false; includeMacos = false; includeWindows = false; } else { includeIos = featureFlags.isIOSEnabled && platforms.contains('ios'); includeAndroid = featureFlags.isAndroidEnabled && platforms.contains('android'); includeWeb = featureFlags.isWebEnabled && platforms.contains('web'); includeLinux = featureFlags.isLinuxEnabled && platforms.contains('linux'); includeMacos = featureFlags.isMacOSEnabled && platforms.contains('macos'); 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, ); } // The dart project_name is in snake_case, this variable is the Title Case of the Project Name. final String titleCaseProjectName = snakeCaseToTitleCase(projectName); final Map<String, Object?> templateContext = createTemplateContext( organization: organization, projectName: projectName, titleCaseProjectName: titleCaseProjectName, projectDescription: stringArg('description'), flutterRoot: flutterRoot, withPlatformChannelPluginHook: generateMethodChannelsPlugin, withFfiPluginHook: generateFfiPlugin, withFfiPackage: generateFfiPackage, withEmptyMain: emptyArgument, androidLanguage: stringArg('android-language'), iosLanguage: stringArg('ios-language'), iosDevelopmentTeam: developmentTeam, ios: includeIos, android: includeAndroid, web: includeWeb, linux: includeLinux, macos: includeMacos, windows: includeWindows, dartSdkVersionBounds: "'>=$dartSdk <4.0.0'", 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); int generatedFileCount = 0; final PubContext pubContext; switch (template) { case FlutterProjectType.app: generatedFileCount += await generateApp( <String>['app', 'app_test_widget'], relativeDir, templateContext, overwrite: overwrite, printStatusWhenWriting: !creatingNewProject, projectType: template, ); pubContext = PubContext.create; case FlutterProjectType.skeleton: generatedFileCount += await generateApp( <String>['skeleton'], relativeDir, templateContext, overwrite: overwrite, printStatusWhenWriting: !creatingNewProject, generateMetadata: false, ); pubContext = PubContext.create; case FlutterProjectType.module: generatedFileCount += await _generateModule( relativeDir, templateContext, overwrite: overwrite, printStatusWhenWriting: !creatingNewProject, ); pubContext = PubContext.create; case FlutterProjectType.package: generatedFileCount += await _generatePackage( relativeDir, templateContext, overwrite: overwrite, printStatusWhenWriting: !creatingNewProject, ); pubContext = PubContext.createPackage; case FlutterProjectType.plugin: generatedFileCount += await _generateMethodChannelPlugin( relativeDir, templateContext, overwrite: overwrite, printStatusWhenWriting: !creatingNewProject, projectType: template, ); pubContext = PubContext.createPlugin; case FlutterProjectType.pluginFfi: generatedFileCount += await _generateFfiPlugin( relativeDir, templateContext, overwrite: overwrite, printStatusWhenWriting: !creatingNewProject, projectType: template, ); pubContext = PubContext.createPlugin; case FlutterProjectType.packageFfi: generatedFileCount += await _generateFfiPackage( relativeDir, templateContext, overwrite: overwrite, printStatusWhenWriting: !creatingNewProject, projectType: template, ); pubContext = PubContext.createPackage; } if (boolArg('pub')) { final FlutterProject project = FlutterProject.fromDirectory(relativeDir); await pub.get( context: pubContext, project: project, offline: boolArg('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) { await project.ensureReadyForPlatformSpecificTooling( androidPlatform: includeAndroid, iosPlatform: includeIos, linuxPlatform: includeLinux, macOSPlatform: includeMacos, windowsPlatform: includeWindows, webPlatform: includeWeb, ); } } if (sampleCode != null) { _applySample(relativeDir, sampleCode); } if (sampleCode != null || emptyArgument) { generatedFileCount += _removeTestDir(relativeDir); } globals.printStatus('Wrote $generatedFileCount files.'); globals.printStatus('\nAll done!'); final String 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 String 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(); // 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: \$ cd $relativeAppPath \$ flutter run 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 { int 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 { int 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 FlutterProjectType 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 String existingPlatform in existingPlatforms) { // re-generate files for existing platforms templateContext[existingPlatform] = true; } final bool willAddPlatforms = platformsToAdd.isNotEmpty; templateContext['no_platforms'] = !willAddPlatforms; int generatedCount = 0; final String? description = argResults!.wasParsed('description') ? stringArg('description') : 'A new Flutter plugin project.'; templateContext['description'] = description; generatedCount += await renderMerged( <String>['plugin', 'plugin_shared'], directory, templateContext, overwrite: overwrite, printStatusWhenWriting: printStatusWhenWriting, ); final FlutterProject project = FlutterProject.fromDirectory(directory); final bool generateAndroid = templateContext['android'] == true; if (generateAndroid) { gradle.updateLocalProperties( project: project, requireAndroidSdk: false); } final String? projectName = templateContext['projectName'] as String?; final String organization = templateContext['organization']! as String; // Required to make the context. final String? androidPluginIdentifier = templateContext['androidIdentifier'] as String?; final String 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 FlutterProjectType 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 String existingPlatform in existingPlatforms) { // re-generate files for existing platforms templateContext[existingPlatform] = true; } final bool willAddPlatforms = platformsToAdd.isNotEmpty; templateContext['no_platforms'] = !willAddPlatforms; int 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 bool generateAndroid = templateContext['android'] == true; if (generateAndroid) { gradle.updateLocalProperties(project: project, requireAndroidSdk: false); } final String? projectName = templateContext['projectName'] as String?; final String organization = templateContext['organization']! as String; // Required to make the context. final String? androidPluginIdentifier = templateContext['androidIdentifier'] as String?; final String 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 FlutterProjectType projectType, }) async { int 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 String? projectName = templateContext['projectName'] as String?; final String 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); } int _removeTestDir(Directory directory) { final Directory testDir = directory.childDirectory('test'); if (!testDir.existsSync()) { return 0; } final List<FileSystemEntity> files = testDir.listSync(recursive: true); try { testDir.deleteSync(recursive: true); } on FileSystemException catch (exception) { throwToolExit('Failed to delete test directory: $exception'); } return -files.length; } 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'); } } // 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/developing-packages/#edit-plugin-package. '''); } } 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/go/plugin-platforms. '''); } // returns a list disabled, but requested platforms List<String> _getPlatformWarningList(List<String> requestedPlatforms) { final List<String> 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', ]; return platformsToWarn; } void _printWarningDisabledPlatform(List<String> platforms) { final List<String> desktop = <String>[]; final List<String> web = <String>[]; for (final String platform in platforms) { if (platform == 'web') { web.add(platform); } else if (platform == 'macos' || platform == 'windows' || platform == 'linux') { desktop.add(platform); } } if (desktop.isNotEmpty) { final String platforms = desktop.length > 1 ? 'platforms' : 'platform'; final String 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/desktop '''); } if (web.isNotEmpty) { globals.printStatus(''' The web is currently not supported on your local environment. For more details, see: https://flutter.dev/docs/get-started/web '''); } } // 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 FlutterProjectType 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, javaV: javaVersion, gradleV: templateGradleVersion); bool javaAgpVersionsCompatible = gradle.validateJavaAndAgp(globals.logger, javaV: javaVersion, agpV: templateAgpVersion); String relevantTemplateAgpVersion = templateAgpVersion; if (projectType == FlutterProjectType.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 == FlutterProjectType.plugin || projectType == FlutterProjectType.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 String 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 String compatibleAgpVersionMessage = minimumCompatibleAgpVersion == null ? '' : ' (minimum compatible AGP version: ${minimumCompatibleAgpVersion.agpMin})'; final String gradleBuildFilePaths = ' - ${_getBuildGradleConfigurationFilePaths(projectType, projectDirPath)!.join('\n - ')}'; globals.printWarning(''' $incompatibleVersionsAndRecommendedOptionMessage Alternatively, to continue using your configured Java version, update the AGP version specified in the following files to a compatible AGP version$compatibleAgpVersionMessage as necessary: $gradleBuildFilePaths See https://developer.android.com/build/releases/gradle-plugin for details on compatible Java/AGP versions. ''', 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 String incompatibleDependency = javaGradleVersionsCompatible ? 'Android Gradle Plugin (AGP)' :'Gradle' ; final String 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 String 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. [RECOMMENDED] If so, to keep the default $incompatibleDependencyVersion, make sure to download a compatible Java version $validJavaRangeMessage. You may configure this compatible Java version by running: `flutter config --jdk-dir=<JDK_DIRECTORY>` Note that this is a global configuration for Flutter. '''; } // Returns path of the gradle-wrapper.properties file for the specified // generated project type. String? _getGradleWrapperPropertiesFilePath(FlutterProjectType projectType, String projectDirPath) { String gradleWrapperPropertiesFilePath = ''; switch (projectType) { case FlutterProjectType.app: case FlutterProjectType.skeleton: gradleWrapperPropertiesFilePath = globals.fs.path.join(projectDirPath, 'android/gradle/wrapper/gradle-wrapper.properties'); case FlutterProjectType.module: gradleWrapperPropertiesFilePath = globals.fs.path.join(projectDirPath, '.android/gradle/wrapper/gradle-wrapper.properties'); case FlutterProjectType.plugin: case FlutterProjectType.pluginFfi: case FlutterProjectType.package: case FlutterProjectType.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(FlutterProjectType projectType, String projectDirPath) { final List<String> buildGradleConfigurationFilePaths = <String>[]; switch (projectType) { case FlutterProjectType.app: case FlutterProjectType.skeleton: case FlutterProjectType.pluginFfi: buildGradleConfigurationFilePaths.add(globals.fs.path.join(projectDirPath, 'android/build.gradle')); case FlutterProjectType.module: const String moduleBuildGradleFilePath = '.android/build.gradle'; const String moduleAppBuildGradleFlePath = '.android/app/build.gradle'; const String 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 FlutterProjectType.plugin: buildGradleConfigurationFilePaths.add(globals.fs.path.join(projectDirPath, 'android/app/build.gradle')); case FlutterProjectType.package: case FlutterProjectType.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; }