blob: 4eb742dbd31a3373b9f45960acba71d14b7c4c07 [file] [log] [blame]
// 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: <String>['objc', 'swift'],
help:
'(deprecated) This option is deprecated and no longer has any effect. '
'Swift is always used for iOS-specific code. '
'This flag will be removed in a future version of Flutter.',
hide: !verboseHelp,
);
argParser.addOption(
'android-language',
abbr: 'a',
defaultsTo: 'kotlin',
allowed: <String>['java', 'kotlin'],
help:
'The language to use for Android-specific code, either Kotlin (recommended) or Java (legacy).',
);
argParser.addFlag(
'skip-name-checks',
help:
'Allow the creation of applications and plugins with invalid names. '
'This is only intended to enable testing of the tool itself.',
hide: !verboseHelp,
);
argParser.addFlag(
'implementation-tests',
help:
'Include implementation tests that verify the template functions correctly. '
'This is only intended to enable testing of the tool itself.',
hide: !verboseHelp,
);
argParser.addOption(
'initial-create-revision',
help:
'The Flutter SDK git commit hash to store in .migrate_config. This parameter is used by the tool '
'internally and should generally not be used manually.',
hide: !verboseHelp,
);
addPlatformsOptions(customHelp: kPlatformHelp);
final List<ParsedFlutterTemplateType> enabledTemplates =
ParsedFlutterTemplateType.enabledValues(featureFlags);
argParser.addOption(
'template',
abbr: 't',
allowed: enabledTemplates.map((ParsedFlutterTemplateType t) => t.cliName),
help: 'Specify the type of project to create.',
valueHelp: 'type',
allowedHelp: CliEnum.allowedHelp(enabledTemplates),
);
argParser.addOption(
'sample',
abbr: 's',
help:
'Specifies the Flutter code sample to use as the "main.dart" for an application. Implies '
'"--template=app". The value should be the sample ID of the desired sample from the API '
'documentation website (https://api.flutter.dev/). An example can be found at: '
'https://api.flutter.dev/flutter/widgets/SingleChildScrollView-class.html',
valueHelp: 'id',
hide: !verboseHelp,
);
argParser.addFlag(
'empty',
abbr: 'e',
help:
'Specifies creating using an application template with a main.dart that is minimal, '
'including no comments, as a starting point for a new application. Implies "--template=app".',
);
argParser.addOption(
'list-samples',
help:
'Specifies a JSON output file for a listing of Flutter code samples '
'that can be created with "--sample".',
valueHelp: 'path',
hide: !verboseHelp,
);
}
@override
final name = 'create';
@override
final description =
'Create a new Flutter project.\n\n'
'If run on a project that already exists, this will repair the project, recreating any files that are missing.';
@override
String get category => FlutterCommandCategory.project;
@override
String get invocation => '${runner?.executableName} $name <output directory>';
@override
Future<Event> unifiedAnalyticsUsageValues(String commandPath) async => Event.commandUsageValues(
workflow: commandPath,
commandHasTerminal: hasTerminal,
createProjectType: stringArg('template'),
createAndroidLanguage: stringArg('android-language'),
);
// Lazy-initialize the net utilities with values from the context.
late final _net = Net(
httpClientFactory: context.get<HttpClientFactory>(),
logger: globals.logger,
platform: globals.platform,
);
/// The hostname for the Flutter docs for the current channel.
String get _snippetsHost =>
globals.flutterVersion.channel == 'stable' ? 'api.flutter.dev' : 'main-api.flutter.dev';
Future<String?> _fetchSampleFromServer(String sampleId) async {
// Sanity check the sampleId
if (sampleId.contains(RegExp(r'[^-\w\.]'))) {
throwToolExit(
'Sample ID "$sampleId" contains invalid characters. Check the ID in the '
'documentation and try again.',
);
}
final snippetsUri = Uri.https(_snippetsHost, 'snippets/$sampleId.dart');
final List<int>? data = await _net.fetchUrl(snippetsUri);
if (data == null || data.isEmpty) {
return null;
}
return utf8.decode(data);
}
/// Fetches the samples index file from the Flutter docs website.
Future<String?> _fetchSamplesIndexFromServer() async {
final snippetsUri = Uri.https(_snippetsHost, 'snippets/index.json');
final List<int>? data = await _net.fetchUrl(snippetsUri, maxAttempts: 2);
if (data == null || data.isEmpty) {
return null;
}
return utf8.decode(data);
}
/// Fetches the samples index file from the server and writes it to
/// [outputFilePath].
Future<void> _writeSamplesJson(String outputFilePath) async {
try {
final File outputFile = globals.fs.file(outputFilePath);
if (outputFile.existsSync()) {
throwToolExit('File "$outputFilePath" already exists', exitCode: 1);
}
final String? samplesJson = await _fetchSamplesIndexFromServer();
if (samplesJson == null) {
throwToolExit('Unable to download samples', exitCode: 2);
} else {
outputFile.writeAsStringSync(samplesJson);
globals.printStatus('Wrote samples JSON to "$outputFilePath"');
}
} on Exception catch (e) {
throwToolExit('Failed to write samples JSON to "$outputFilePath": $e', exitCode: 2);
}
}
FlutterTemplateType _getProjectType(Directory projectDir) {
FlutterTemplateType? template;
FlutterTemplateType? detectedProjectType;
final bool metadataExists = projectDir.absolute.childFile('.metadata').existsSync();
final String? templateArgument = stringArg('template');
if (templateArgument != null) {
final ParsedFlutterTemplateType? parsedTemplate = ParsedFlutterTemplateType.fromCliName(
templateArgument,
);
switch (parsedTemplate) {
case RemovedFlutterTemplateType():
throwToolExit(
'The template ${parsedTemplate.cliName} is no longer available. For '
'your convenience the former help text is repeated below with context '
'about the removal and other possible resources:\n\n'
'${parsedTemplate.helpText}',
);
case FlutterTemplateType():
template = parsedTemplate;
case null:
break;
}
}
// If the project directory exists and isn't empty, then try to determine the template
// type from the project directory.
if (projectDir.existsSync() && projectDir.listSync().isNotEmpty) {
detectedProjectType = determineTemplateType();
if (detectedProjectType == null && metadataExists) {
// We can only be definitive that this is the wrong type if the .metadata file
// exists and contains a type that we don't understand, or doesn't contain a type.
throwToolExit(
'Sorry, unable to detect the type of project to recreate. '
'Try creating a fresh project and migrating your existing code to '
'the new project manually.',
);
}
}
template ??= detectedProjectType ?? FlutterTemplateType.app;
if (detectedProjectType != null && template != detectedProjectType && metadataExists) {
// We can only be definitive that this is the wrong type if the .metadata file
// exists and contains a type that doesn't match.
throwToolExit(
"The requested template type '${template.cliName}' doesn't match the "
"existing template type of '${detectedProjectType.cliName}'.",
);
}
return template;
}
@override
Future<FlutterCommandResult> runCommand() async {
final String? listSamples = stringArg('list-samples');
if (listSamples != null) {
// _writeSamplesJson can potentially be long-lived.
await _writeSamplesJson(listSamples);
return FlutterCommandResult.success();
}
if (argResults!.wasParsed('empty') && argResults!.wasParsed('sample')) {
throwToolExit('Only one of --empty or --sample may be specified, not both.', exitCode: 2);
}
validateOutputDirectoryArg();
String? sampleCode;
final String? sampleArgument = stringArg('sample');
final bool emptyArgument = boolArg('empty');
final FlutterTemplateType template = _getProjectType(projectDir);
if (sampleArgument != null) {
if (template != FlutterTemplateType.app) {
throwToolExit(
'Cannot specify --sample with a project type other than '
'"${FlutterTemplateType.app.cliName}"',
);
}
// Fetch the sample from the server.
sampleCode = await _fetchSampleFromServer(sampleArgument);
}
if (emptyArgument && template != FlutterTemplateType.app) {
throwToolExit('The --empty flag is only supported for the app template.');
}
final generateModule = template == FlutterTemplateType.module;
final generateMethodChannelsPlugin = template == FlutterTemplateType.plugin;
final generateFfiPackage = template == FlutterTemplateType.packageFfi;
final generateFfiPlugin = template == FlutterTemplateType.pluginFfi;
final bool generateFfi = generateFfiPlugin || generateFfiPackage;
final generatePackage = template == FlutterTemplateType.package;
final List<String> platforms = stringsArg('platforms');
// `--platforms` does not support module or package.
if (argResults!.wasParsed('platforms') &&
(generateModule || generatePackage || generateFfiPackage)) {
final template = generateModule ? 'module' : 'package';
throwToolExit(
'The "--platforms" argument is not supported in $template template.',
exitCode: 2,
);
} else if (platforms.isEmpty) {
throwToolExit('Must specify at least one platform using --platforms', exitCode: 2);
} else if (generateFfiPlugin &&
argResults!.wasParsed('platforms') &&
platforms.contains('web')) {
throwToolExit('The web platform is not supported in plugin_ffi template.', exitCode: 2);
} else if (generateFfi && argResults!.wasParsed('android-language')) {
throwToolExit(
'The "android-language" option is not supported with the ${template.cliName} '
'template: the language will always be C or C++.',
exitCode: 2,
);
} else if (argResults!.wasParsed('ios-language')) {
globals.printWarning(
'The "--ios-language" option is deprecated and no longer has any effect. '
'Swift is always used for iOS-specific code. '
'This flag will be removed in a future version of Flutter.',
);
}
final String organization = await getOrganization();
final bool overwrite = boolArg('overwrite');
validateProjectDir(overwrite: overwrite);
if (boolArg('with-driver-test')) {
globals.printWarning(
'The "--with-driver-test" argument has been deprecated and will no longer add a flutter '
'driver template. Instead, learn how to use package:integration_test by '
'visiting https://pub.dev/packages/integration_test .',
);
}
final String dartSdk = globals.cache.dartSdkBuild;
final bool includeIos;
final bool 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;
} 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;
} 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,
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<String, Object?> 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,
web: includeWeb,
linux: includeLinux,
macos: includeMacos,
windows: includeWindows,
dartSdkVersionBounds: '^$dartSdk',
implementationTests: boolArg('implementation-tests'),
agpVersion: gradle.templateAndroidGradlePluginVersion,
kotlinVersion: gradle.templateKotlinGradlePluginVersion,
gradleVersion: gradle.templateDefaultGradleVersion,
);
final String relativeDirPath = globals.fs.path.relative(projectDirPath);
final bool creatingNewProject = !projectDir.existsSync() || projectDir.listSync().isEmpty;
if (creatingNewProject) {
globals.printStatus('Creating project $relativeDirPath...');
} else {
if (sampleCode != null && !overwrite) {
throwToolExit(
'Will not overwrite existing project in $relativeDirPath: '
'must specify --overwrite for samples to overwrite.',
);
}
globals.printStatus('Recreating project $relativeDirPath...');
}
final Directory relativeDir = globals.fs.directory(projectDirPath);
var generatedFileCount = 0;
final PubContext pubContext;
switch (template) {
case FlutterTemplateType.app:
final bool skipWidgetTestsGeneration = sampleCode != null || emptyArgument;
generatedFileCount += await generateApp(
<String>['app', if (!skipWidgetTestsGeneration) 'app_test_widget'],
relativeDir,
templateContext,
overwrite: overwrite,
printStatusWhenWriting: !creatingNewProject,
projectType: template,
);
pubContext = PubContext.create;
case FlutterTemplateType.module:
generatedFileCount += await _generateModule(
relativeDir,
templateContext,
overwrite: overwrite,
printStatusWhenWriting: !creatingNewProject,
);
pubContext = PubContext.create;
case FlutterTemplateType.package:
generatedFileCount += await _generatePackage(
relativeDir,
templateContext,
overwrite: overwrite,
printStatusWhenWriting: !creatingNewProject,
);
pubContext = PubContext.createPackage;
case FlutterTemplateType.plugin:
generatedFileCount += await _generateMethodChannelPlugin(
relativeDir,
templateContext,
overwrite: overwrite,
printStatusWhenWriting: !creatingNewProject,
projectType: template,
);
pubContext = PubContext.createPlugin;
case FlutterTemplateType.pluginFfi:
generatedFileCount += await _generateFfiPlugin(
relativeDir,
templateContext,
overwrite: overwrite,
printStatusWhenWriting: !creatingNewProject,
projectType: template,
);
pubContext = PubContext.createPlugin;
case FlutterTemplateType.packageFfi:
generatedFileCount += await _generateFfiPackage(
relativeDir,
templateContext,
overwrite: overwrite,
printStatusWhenWriting: !creatingNewProject,
projectType: template,
);
pubContext = PubContext.createPackage;
}
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,
linuxPlatform: includeLinux,
macOSPlatform: includeMacos,
windowsPlatform: includeWindows,
webPlatform: includeWeb,
);
}
}
if (sampleCode != null) {
_applySample(relativeDir, sampleCode);
}
globals.printStatus('Wrote $generatedFileCount files.');
globals.printStatus('\nAll done!');
final application =
'${emptyArgument ? 'empty ' : ''}${sampleCode != null ? 'sample ' : ''}application';
if (generatePackage) {
final String relativeMainPath = globals.fs.path.normalize(
globals.fs.path.join(relativeDirPath, 'lib', '${templateContext['projectName']}.dart'),
);
globals.printStatus('Your package code is in $relativeMainPath');
} else if (generateModule) {
final String relativeMainPath = globals.fs.path.normalize(
globals.fs.path.join(relativeDirPath, 'lib', 'main.dart'),
);
globals.printStatus('Your module code is in $relativeMainPath.');
} else if (generateMethodChannelsPlugin || generateFfiPlugin) {
final String relativePluginPath = globals.fs.path.normalize(
globals.fs.path.relative(projectDirPath),
);
final List<String> requestedPlatforms = _getUserRequestedPlatforms();
final String platformsString = requestedPlatforms.join(', ');
_printPluginDirectoryLocationMessage(relativePluginPath, projectName, platformsString);
if (!creatingNewProject && requestedPlatforms.isNotEmpty) {
_printPluginUpdatePubspecMessage(relativePluginPath, platformsString);
} else if (_getSupportedPlatformsInPlugin(projectDir).isEmpty) {
_printNoPluginMessage();
}
final List<String> platformsToWarn = _getPlatformWarningList(requestedPlatforms);
if (platformsToWarn.isNotEmpty) {
_printWarningDisabledPlatform(platformsToWarn);
}
final template = generateMethodChannelsPlugin ? 'plugin' : 'plugin_ffi';
_printPluginAddPlatformMessage(relativePluginPath, template);
} else {
// Tell the user the next steps.
final FlutterProject project = FlutterProject.fromDirectory(
globals.fs.directory(projectDirPath),
);
final FlutterProject app = project.hasExampleApp ? project.example : project;
final String relativeAppPath = globals.fs.path.normalize(
globals.fs.path.relative(app.directory.path),
);
final String relativeAppMain = globals.fs.path.join(relativeAppPath, 'lib', 'main.dart');
final List<String> requestedPlatforms = _getUserRequestedPlatforms();
final String commandsToRun = [
if (relativeAppPath != '.') ' \$ cd $relativeAppPath',
r' $ flutter run',
].join('\n');
// Let them know a summary of the state of their tooling.
globals.printStatus('''
You can find general documentation for Flutter at: https://docs.flutter.dev/
Detailed API documentation is available at: https://api.flutter.dev/
If you prefer video documentation, consider: https://www.youtube.com/c/flutterdev
In order to run your $application, type:
$commandsToRun
Your $application code is in $relativeAppMain.
''');
// Show warning if any selected platform is not enabled
final List<String> platformsToWarn = _getPlatformWarningList(requestedPlatforms);
if (platformsToWarn.isNotEmpty) {
_printWarningDisabledPlatform(platformsToWarn);
}
}
// Show warning for Java/AGP or Java/Gradle incompatibility if building for
// Android and Java version has been detected.
if (includeAndroid && globals.java?.version != null) {
_printIncompatibleJavaAgpGradleVersionsWarning(
javaVersion: versionToParsableString(globals.java?.version)!,
templateGradleVersion: templateContext['gradleVersion']! as String,
templateAgpVersion: templateContext['agpVersion']! as String,
templateAgpVersionForModule: templateContext['agpVersionForModule']! as String,
projectType: template,
projectDirPath: projectDirPath,
);
}
return FlutterCommandResult.success();
}
Future<int> _generateModule(
Directory directory,
Map<String, Object?> templateContext, {
bool overwrite = false,
bool printStatusWhenWriting = true,
}) async {
var generatedCount = 0;
final String? description = argResults!.wasParsed('description')
? stringArg('description')
: 'A new Flutter module project.';
templateContext['description'] = description;
generatedCount += await renderTemplate(
globals.fs.path.join('module', 'common'),
directory,
templateContext,
overwrite: overwrite,
printStatusWhenWriting: printStatusWhenWriting,
);
return generatedCount;
}
Future<int> _generatePackage(
Directory directory,
Map<String, Object?> templateContext, {
bool overwrite = false,
bool printStatusWhenWriting = true,
}) async {
var generatedCount = 0;
final String? description = argResults!.wasParsed('description')
? stringArg('description')
: 'A new Flutter package project.';
templateContext['description'] = description;
generatedCount += await renderTemplate(
'package',
directory,
templateContext,
overwrite: overwrite,
printStatusWhenWriting: printStatusWhenWriting,
);
return generatedCount;
}
Future<int> _generateMethodChannelPlugin(
Directory directory,
Map<String, Object?> templateContext, {
bool overwrite = false,
bool printStatusWhenWriting = true,
required FlutterTemplateType projectType,
}) async {
// Plugins only add a platform if it was requested explicitly by the user.
if (!argResults!.wasParsed('platforms')) {
for (final String platform in kAllCreatePlatforms) {
templateContext[platform] = false;
}
}
final List<String> platformsToAdd = _getSupportedPlatformsFromTemplateContext(templateContext);
final List<String> existingPlatforms = _getSupportedPlatformsInPlugin(directory);
for (final existingPlatform in existingPlatforms) {
// re-generate files for existing platforms
templateContext[existingPlatform] = true;
}
final bool willAddPlatforms = platformsToAdd.isNotEmpty;
templateContext['no_platforms'] = !willAddPlatforms;
var generatedCount = 0;
final String? description = argResults!.wasParsed('description')
? stringArg('description')
: 'A new Flutter plugin project.';
templateContext['description'] = description;
final projectName = templateContext['projectName'] as String?;
final templates = <String>['plugin', 'plugin_shared'];
if ((templateContext['ios'] == true || templateContext['macos'] == true) &&
featureFlags.isSwiftPackageManagerEnabled) {
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 {
templates.add('plugin_cocoapods');
}
generatedCount += await renderMerged(
templates,
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 organization =
templateContext['organization']! as String; // Required to make the context.
final androidPluginIdentifier = templateContext['androidIdentifier'] as String?;
final exampleProjectName = '${projectName}_example';
templateContext['projectName'] = exampleProjectName;
templateContext['androidIdentifier'] = CreateBase.createAndroidIdentifier(
organization,
exampleProjectName,
);
templateContext['iosIdentifier'] = CreateBase.createUTIIdentifier(
organization,
exampleProjectName,
);
templateContext['macosIdentifier'] = CreateBase.createUTIIdentifier(
organization,
exampleProjectName,
);
templateContext['windowsIdentifier'] = CreateBase.createWindowsIdentifier(
organization,
exampleProjectName,
);
templateContext['description'] = 'Demonstrates how to use the $projectName plugin.';
templateContext['pluginProjectName'] = projectName;
templateContext['androidPluginIdentifier'] = androidPluginIdentifier;
generatedCount += await generateApp(
<String>['app', 'app_test_widget', 'app_integration_test'],
project.example.directory,
templateContext,
overwrite: overwrite,
pluginExampleApp: true,
printStatusWhenWriting: printStatusWhenWriting,
projectType: projectType,
);
return generatedCount;
}
Future<int> _generateFfiPlugin(
Directory directory,
Map<String, Object?> templateContext, {
bool overwrite = false,
bool printStatusWhenWriting = true,
required FlutterTemplateType projectType,
}) async {
// Plugins only add a platform if it was requested explicitly by the user.
if (!argResults!.wasParsed('platforms')) {
for (final String platform in kAllCreatePlatforms) {
templateContext[platform] = false;
}
}
final List<String> platformsToAdd = _getSupportedPlatformsFromTemplateContext(templateContext);
final List<String> existingPlatforms = _getSupportedPlatformsInPlugin(directory);
for (final existingPlatform in existingPlatforms) {
// re-generate files for existing platforms
templateContext[existingPlatform] = true;
}
final bool willAddPlatforms = platformsToAdd.isNotEmpty;
templateContext['no_platforms'] = !willAddPlatforms;
var generatedCount = 0;
final String? description = argResults!.wasParsed('description')
? stringArg('description')
: 'A new Flutter FFI plugin project.';
templateContext['description'] = description;
generatedCount += await renderMerged(
<String>['plugin_ffi', 'plugin_shared'],
directory,
templateContext,
overwrite: overwrite,
printStatusWhenWriting: printStatusWhenWriting,
);
final FlutterProject project = FlutterProject.fromDirectory(directory);
final generateAndroid = templateContext['android'] == true;
if (generateAndroid) {
gradle.updateLocalProperties(project: project, requireAndroidSdk: false);
}
final projectName = templateContext['projectName'] as String?;
final organization =
templateContext['organization']! as String; // Required to make the context.
final androidPluginIdentifier = templateContext['androidIdentifier'] as String?;
final exampleProjectName = '${projectName}_example';
templateContext['projectName'] = exampleProjectName;
templateContext['androidIdentifier'] = CreateBase.createAndroidIdentifier(
organization,
exampleProjectName,
);
templateContext['iosIdentifier'] = CreateBase.createUTIIdentifier(
organization,
exampleProjectName,
);
templateContext['macosIdentifier'] = CreateBase.createUTIIdentifier(
organization,
exampleProjectName,
);
templateContext['windowsIdentifier'] = CreateBase.createWindowsIdentifier(
organization,
exampleProjectName,
);
templateContext['description'] = 'Demonstrates how to use the $projectName plugin.';
templateContext['pluginProjectName'] = projectName;
templateContext['androidPluginIdentifier'] = androidPluginIdentifier;
generatedCount += await generateApp(
<String>['app'],
project.example.directory,
templateContext,
overwrite: overwrite,
pluginExampleApp: true,
printStatusWhenWriting: printStatusWhenWriting,
projectType: projectType,
);
return generatedCount;
}
Future<int> _generateFfiPackage(
Directory directory,
Map<String, Object?> templateContext, {
bool overwrite = false,
bool printStatusWhenWriting = true,
required FlutterTemplateType projectType,
}) async {
var generatedCount = 0;
final String? description = argResults!.wasParsed('description')
? stringArg('description')
: 'A new Dart FFI package project.';
templateContext['description'] = description;
generatedCount += await renderMerged(
<String>['package_ffi'],
directory,
templateContext,
overwrite: overwrite,
printStatusWhenWriting: printStatusWhenWriting,
);
final FlutterProject project = FlutterProject.fromDirectory(directory);
final projectName = templateContext['projectName'] as String?;
final exampleProjectName = '${projectName}_example';
templateContext['projectName'] = exampleProjectName;
templateContext['description'] = 'Demonstrates how to use the $projectName package.';
templateContext['pluginProjectName'] = projectName;
generatedCount += await generateApp(
<String>['app'],
project.example.directory,
templateContext,
overwrite: overwrite,
pluginExampleApp: true,
printStatusWhenWriting: printStatusWhenWriting,
projectType: projectType,
);
return generatedCount;
}
// Takes an application template and replaces the main.dart with one from the
// documentation website in sampleCode. Returns the difference in the number
// of files after applying the sample, since it also deletes the application's
// test directory (since the template's test doesn't apply to the sample).
void _applySample(Directory directory, String sampleCode) {
final File mainDartFile = directory.childDirectory('lib').childFile('main.dart');
mainDartFile.createSync(recursive: true);
mainDartFile.writeAsStringSync(sampleCode);
}
List<String> _getSupportedPlatformsFromTemplateContext(Map<String, Object?> templateContext) {
return <String>[
for (final String platform in kAllCreatePlatforms)
if (templateContext[platform] == true) platform,
];
}
// Returns a list of platforms that are explicitly requested by user via `--platforms`.
List<String> _getUserRequestedPlatforms() {
if (!argResults!.wasParsed('platforms')) {
return <String>[];
}
return stringsArg('platforms');
}
}
// Determine what platforms are supported based on generated files.
List<String> _getSupportedPlatformsInPlugin(Directory projectDir) {
final String pubspecPath = globals.fs.path.join(projectDir.absolute.path, 'pubspec.yaml');
final FlutterManifest? manifest = FlutterManifest.createFromPath(
pubspecPath,
fileSystem: globals.fs,
logger: globals.logger,
);
final Map<String, Object?>? validSupportedPlatforms = manifest?.validSupportedPlatforms;
final List<String> platforms = validSupportedPlatforms == null
? <String>[]
: validSupportedPlatforms.keys.toList();
return platforms;
}
void _printPluginDirectoryLocationMessage(
String pluginPath,
String projectName,
String platformsString,
) {
final String relativePluginMain = globals.fs.path.join(pluginPath, 'lib', '$projectName.dart');
final String relativeExampleMain = globals.fs.path.join(
pluginPath,
'example',
'lib',
'main.dart',
);
globals.printStatus('''
Your plugin code is in $relativePluginMain.
Your example app code is in $relativeExampleMain.
''');
if (platformsString.isNotEmpty) {
globals.printStatus('''
Host platform code is in the $platformsString directories under $pluginPath.
To edit platform code in an IDE see https://flutter.dev/to/edit-plugins.
''');
}
}
void _printPluginUpdatePubspecMessage(String pluginPath, String platformsString) {
globals.printStatus(
'''
You need to update $pluginPath/pubspec.yaml to support $platformsString.
''',
emphasis: true,
color: TerminalColor.red,
);
}
void _printNoPluginMessage() {
globals.printError('''
You've created a plugin project that doesn't yet support any platforms.
''');
}
void _printPluginAddPlatformMessage(String pluginPath, String template) {
globals.printStatus('''
To add platforms, run `flutter create -t $template --platforms <platforms> .` under $pluginPath.
For more information, see https://flutter.dev/to/pubspec-plugin-platforms.
''');
}
// returns a list disabled, but requested platforms
List<String> _getPlatformWarningList(List<String> requestedPlatforms) {
final platformsToWarn = <String>[
if (requestedPlatforms.contains('web') && !featureFlags.isWebEnabled) 'web',
if (requestedPlatforms.contains('macos') && !featureFlags.isMacOSEnabled) 'macos',
if (requestedPlatforms.contains('windows') && !featureFlags.isWindowsEnabled) 'windows',
if (requestedPlatforms.contains('linux') && !featureFlags.isLinuxEnabled) 'linux',
];
return platformsToWarn;
}
void _printWarningDisabledPlatform(List<String> platforms) {
final desktop = <String>[];
final web = <String>[];
for (final platform in platforms) {
switch (platform) {
case 'web':
web.add(platform);
case 'macos' || 'windows' || 'linux':
desktop.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
''');
}
}
// Prints a warning if the specified Java version conflicts with either the
// template Gradle or AGP version.
//
// Assumes the specified templateGradleVersion and templateAgpVersion are
// compatible, meaning that the Java version may only conflict with one of the
// template Gradle or AGP versions.
void _printIncompatibleJavaAgpGradleVersionsWarning({
required String javaVersion,
required String templateGradleVersion,
required String templateAgpVersion,
required String templateAgpVersionForModule,
required FlutterTemplateType projectType,
required String projectDirPath,
}) {
// Determine if the Java version specified conflicts with the template Gradle or AGP version.
final bool javaGradleVersionsCompatible = gradle.validateJavaAndGradle(
globals.logger,
javaVersion: javaVersion,
gradleVersion: templateGradleVersion,
);
bool javaAgpVersionsCompatible = gradle.validateJavaAndAgp(
globals.logger,
javaV: javaVersion,
agpV: templateAgpVersion,
);
var relevantTemplateAgpVersion = templateAgpVersion;
if (projectType == FlutterTemplateType.module &&
Version.parse(templateAgpVersion)! < Version.parse(templateAgpVersionForModule)!) {
// If a module is being created, make sure to check for Java/AGP compatibility between the highest used version of AGP in the module template.
javaAgpVersionsCompatible = gradle.validateJavaAndAgp(
globals.logger,
javaV: javaVersion,
agpV: templateAgpVersionForModule,
);
relevantTemplateAgpVersion = templateAgpVersionForModule;
}
if (javaGradleVersionsCompatible && javaAgpVersionsCompatible) {
return;
}
// Determine header of warning with recommended fix of re-configuring Java version.
final String incompatibleVersionsAndRecommendedOptionMessage =
getIncompatibleJavaGradleAgpMessageHeader(
javaGradleVersionsCompatible,
templateGradleVersion,
relevantTemplateAgpVersion,
projectType.cliName,
);
if (!javaGradleVersionsCompatible) {
if (projectType == FlutterTemplateType.plugin || projectType == FlutterTemplateType.pluginFfi) {
// Only impacted files could be in sample code.
return;
}
// Gradle template version incompatible with Java version.
final gradle.JavaGradleCompat? validCompatibleGradleVersionRange = gradle
.getValidGradleVersionRangeForJavaVersion(globals.logger, javaV: javaVersion);
final compatibleGradleVersionMessage = validCompatibleGradleVersionRange == null
? ''
: ' (compatible Gradle version range: ${validCompatibleGradleVersionRange.gradleMin} - ${validCompatibleGradleVersionRange.gradleMax})';
globals.printWarning('''
$incompatibleVersionsAndRecommendedOptionMessage
Alternatively, to continue using your configured Java version, update the Gradle
version specified in the following file to a compatible Gradle version$compatibleGradleVersionMessage:
${_getGradleWrapperPropertiesFilePath(projectType, projectDirPath)}
You may also update the Gradle version used by running
`./gradlew wrapper --gradle-version=<COMPATIBLE_GRADLE_VERSION>`.
See
https://docs.gradle.org/current/userguide/compatibility.html#java for details
on compatible Java/Gradle versions, and see
https://docs.gradle.org/current/userguide/gradle_wrapper.html#sec:upgrading_wrapper
for more details on using the Gradle Wrapper command to update the Gradle version
used.
''', emphasis: true);
return;
}
// AGP template version incompatible with Java version.
final gradle.JavaAgpCompat? minimumCompatibleAgpVersion = gradle
.getMinimumAgpVersionForJavaVersion(globals.logger, javaV: javaVersion);
final compatibleAgpVersionMessage = minimumCompatibleAgpVersion == null
? ''
: ' (minimum compatible AGP version: ${minimumCompatibleAgpVersion.agpMin})';
final gradleBuildFilePaths =
' ${_getBuildGradleConfigurationFilePaths(projectType, projectDirPath)!.join('\n - ')}';
globals.printWarning('''
$incompatibleVersionsAndRecommendedOptionMessage
Alternatively, to continue using your current Java version, update the AGP
version in the following file(s) to a compatible version$compatibleAgpVersionMessage:
$gradleBuildFilePaths
For details on compatible Java and AGP versions, see
https://developer.android.com/build/releases/gradle-plugin
''', emphasis: true);
}
// Returns incompatible Java/template Gradle/template AGP message header based
// on incompatibility and project type.
@visibleForTesting
String getIncompatibleJavaGradleAgpMessageHeader(
bool javaGradleVersionsCompatible,
String templateGradleVersion,
String templateAgpVersion,
String projectType,
) {
final incompatibleDependency = javaGradleVersionsCompatible
? 'Android Gradle Plugin (AGP)'
: 'Gradle';
final incompatibleDependencyVersion = javaGradleVersionsCompatible
? 'AGP version $templateAgpVersion'
: 'Gradle version $templateGradleVersion';
final VersionRange validJavaRange = gradle.getJavaVersionFor(
gradleV: templateGradleVersion,
agpV: templateAgpVersion,
);
// validJavaRange should have non-null versionMin and versionMax since it based on our template AGP and Gradle versions.
final validJavaRangeMessage =
'(Java ${validJavaRange.versionMin!} <= compatible Java version < Java ${validJavaRange.versionMax!})';
return '''
The configured version of Java detected may conflict with the $incompatibleDependency version in your new Flutter $projectType.
To keep the default $incompatibleDependencyVersion, download a compatible Java version
$validJavaRangeMessage. Configure this Java version globally for Flutter by running:
flutter config --jdk-dir=<JDK_DIRECTORY>
''';
}
// Returns path of the gradle-wrapper.properties file for the specified
// generated project type.
String? _getGradleWrapperPropertiesFilePath(
FlutterTemplateType projectType,
String projectDirPath,
) {
var gradleWrapperPropertiesFilePath = '';
switch (projectType) {
case FlutterTemplateType.app:
gradleWrapperPropertiesFilePath = globals.fs.path.join(
projectDirPath,
'android/gradle/wrapper/gradle-wrapper.properties',
);
case FlutterTemplateType.module:
gradleWrapperPropertiesFilePath = globals.fs.path.join(
projectDirPath,
'.android/gradle/wrapper/gradle-wrapper.properties',
);
case FlutterTemplateType.plugin:
case FlutterTemplateType.pluginFfi:
case FlutterTemplateType.package:
case FlutterTemplateType.packageFfi:
// TODO(camsim99): Add relevant file path for packageFfi when Android is supported.
// No gradle-wrapper.properties files not part of sample code that
// can be determined.
return null;
}
return gradleWrapperPropertiesFilePath;
}
// Returns the path(s) of the build.gradle file(s) for the specified generated
// project type.
List<String>? _getBuildGradleConfigurationFilePaths(
FlutterTemplateType projectType,
String projectDirPath,
) {
final buildGradleConfigurationFilePaths = <String>[];
switch (projectType) {
case FlutterTemplateType.app:
case FlutterTemplateType.pluginFfi:
buildGradleConfigurationFilePaths.add(
globals.fs.path.join(projectDirPath, 'android/build.gradle'),
);
case FlutterTemplateType.module:
const moduleBuildGradleFilePath = '.android/build.gradle';
const moduleAppBuildGradleFlePath = '.android/app/build.gradle';
const moduleFlutterBuildGradleFilePath = '.android/Flutter/build.gradle';
buildGradleConfigurationFilePaths.addAll(<String>[
globals.fs.path.join(projectDirPath, moduleBuildGradleFilePath),
globals.fs.path.join(projectDirPath, moduleAppBuildGradleFlePath),
globals.fs.path.join(projectDirPath, moduleFlutterBuildGradleFilePath),
]);
case FlutterTemplateType.plugin:
buildGradleConfigurationFilePaths.add(
globals.fs.path.join(projectDirPath, 'android/app/build.gradle'),
);
case FlutterTemplateType.package:
case FlutterTemplateType.packageFfi:
// TODO(camsim99): Add any relevant file paths for packageFfi when Android is supported.
// No build.gradle file because there is no platform-specific implementation.
return null;
}
return buildGradleConfigurationFilePaths;
}