| // 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 '../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'), |
| ); |
| } |
| |
| // 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' |
| : 'master-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'); |
| if (sampleArgument != null) { |
| final String? templateArgument = stringArg('template'); |
| if (templateArgument != null && FlutterProjectType.fromCliName(templateArgument) != 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); |
| } |
| |
| final FlutterProjectType template = _getProjectType(projectDir); |
| 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'); |
| final List<FileSystemEntity> files = testDir.listSync(recursive: true); |
| testDir.deleteSync(recursive: true); |
| 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 verisonMin 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; |
| } |