// 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 '../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: ['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: ['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 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 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 '; @override Future 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(), 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 _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? 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 _fetchSamplesIndexFromServer() async { final snippetsUri = Uri.https(_snippetsHost, 'snippets/index.json'); final List? 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 _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 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 (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 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 Map templateContext = createTemplateContext( organization: organization, projectName: projectName, titleCaseProjectName: titleCaseProjectName, projectDescription: stringArg('description'), flutterRoot: flutterRoot, withPlatformChannelPluginHook: generateMethodChannelsPlugin, withSwiftPackageManager: featureFlags.isSwiftPackageManagerEnabled, 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( ['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; } 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 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 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 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 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 _generateModule( Directory directory, Map 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 _generatePackage( Directory directory, Map 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 _generateMethodChannelPlugin( Directory directory, Map 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 platformsToAdd = _getSupportedPlatformsFromTemplateContext(templateContext); final List 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 bool includeDarwin = templateContext['darwin'] as bool? ?? false; final bool originalIos = templateContext['ios'] as bool? ?? false; final bool originalMacos = templateContext['macos'] as bool? ?? false; 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 templates = ['plugin', 'plugin_shared']; final bool useSwiftPackageManager = (templateContext['ios'] == true || templateContext['macos'] == true || includeDarwin) && featureFlags.isSwiftPackageManagerEnabled; if (useSwiftPackageManager) { 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 { if (includeDarwin) { templates.add('plugin_darwin_cocoapods'); } else { templates.add('plugin_cocoapods'); } } generatedCount += await renderMerged( templates, directory, templateContext, overwrite: overwrite, printStatusWhenWriting: printStatusWhenWriting, ); // Restore the original ios and macos 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'] = originalIos; templateContext['macos'] = originalMacos; 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( ['app', 'app_test_widget', 'app_integration_test'], project.example.directory, templateContext, overwrite: overwrite, pluginExampleApp: true, printStatusWhenWriting: printStatusWhenWriting, projectType: projectType, ); return generatedCount; } Future _generateFfiPlugin( Directory directory, Map 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 platformsToAdd = _getSupportedPlatformsFromTemplateContext(templateContext); final List 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( ['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( ['app'], project.example.directory, templateContext, overwrite: overwrite, pluginExampleApp: true, printStatusWhenWriting: printStatusWhenWriting, projectType: projectType, ); return generatedCount; } Future _generateFfiPackage( Directory directory, Map 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( ['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( ['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 _getSupportedPlatformsFromTemplateContext(Map templateContext) { return [ 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 _getUserRequestedPlatforms() { if (!argResults!.wasParsed('platforms')) { return []; } return stringsArg('platforms'); } } // Determine what platforms are supported based on generated files. List _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? validSupportedPlatforms = manifest?.validSupportedPlatforms; final List platforms = validSupportedPlatforms == null ? [] : 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 .` under $pluginPath. For more information, see https://flutter.dev/to/pubspec-plugin-platforms. '''); } // returns a list disabled, but requested platforms List _getPlatformWarningList(List requestedPlatforms) { final platformsToWarn = [ 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 platforms) { final desktop = []; final web = []; final darwin = []; 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=`. 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= '''; } // 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? _getBuildGradleConfigurationFilePaths( FlutterTemplateType projectType, String projectDirPath, ) { final buildGradleConfigurationFilePaths = []; 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([ 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; }