blob: ffc29ce739eda0e31730eb11f5c1921689aa6b91 [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.
// @dart = 2.8
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 '../convert.dart';
import '../dart/pub.dart';
import '../features.dart';
import '../flutter_manifest.dart';
import '../flutter_project_metadata.dart';
import '../globals_null_migrated.dart' as globals;
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({
bool verboseHelp = false,
}) : super(verboseHelp: verboseHelp) {
addPlatformsOptions(customHelp: kPlatformHelp);
argParser.addOption(
'template',
abbr: 't',
allowed: FlutterProjectType.values.map<String>(flutterProjectTypeToString),
help: 'Specify the type of project to create.',
valueHelp: 'type',
allowedHelp: <String, String>{
flutterProjectTypeToString(FlutterProjectType.app): '(default) Generate a Flutter application.',
flutterProjectTypeToString(FlutterProjectType.package): 'Generate a shareable Flutter project containing only Dart code.',
flutterProjectTypeToString(FlutterProjectType.plugin): 'Generate a shareable Flutter project containing an API '
'in Dart code with a platform-specific implementation for Android, for iOS code, or '
'for both.',
flutterProjectTypeToString(FlutterProjectType.module): 'Generate a project to add a Flutter module to an '
'existing Android or iOS application.',
},
defaultsTo: null,
);
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 (http://docs.flutter.dev/). An example can be found at: '
'https://api.flutter.dev/flutter/widgets/SingleChildScrollView-class.html',
defaultsTo: null,
valueHelp: 'id',
);
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 invocation => '${runner.executableName} $name <output directory>';
@override
Future<Map<CustomDimensions, String>> get usageValues async {
return <CustomDimensions, String>{
CustomDimensions.commandCreateProjectType: stringArg('template'),
CustomDimensions.commandCreateAndroidLanguage: stringArg('android-language'),
CustomDimensions.commandCreateIosLanguage: stringArg('ios-language'),
};
}
// Lazy-initialize the net utilities with values from the context.
Net _cachedNet;
Net get _net => _cachedNet ??= 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();
if (argResults['template'] != null) {
template = stringToProjectType(stringArg('template'));
} else {
// 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 '${flutterProjectTypeToString(template)}' doesn't match the "
"existing template type of '${flutterProjectTypeToString(detectedProjectType)}'.");
}
return template;
}
@override
Future<FlutterCommandResult> runCommand() async {
if (argResults['list-samples'] != null) {
// _writeSamplesJson can potentially be long-lived.
await _writeSamplesJson(stringArg('list-samples'));
return FlutterCommandResult.success();
}
validateOutputDirectoryArg();
String sampleCode;
if (argResults['sample'] != null) {
if (argResults['template'] != null &&
stringToProjectType(stringArg('template') ?? 'app') != FlutterProjectType.app) {
throwToolExit('Cannot specify --sample with a project type other than '
'"${flutterProjectTypeToString(FlutterProjectType.app)}"');
}
// Fetch the sample from the server.
sampleCode = await _fetchSampleFromServer(stringArg('sample'));
}
final FlutterProjectType template = _getProjectType(projectDir);
final bool generateModule = template == FlutterProjectType.module;
final bool generatePlugin = template == FlutterProjectType.plugin;
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)) {
final String template = generateModule ? 'module' : 'package';
throwToolExit(
'The "--platforms" argument is not supported in $template template.',
exitCode: 2
);
} else if (platforms == null || platforms.isEmpty) {
throwToolExit('Must specify at least one platform using --platforms',
exitCode: 2);
}
final String organization = await getOrganization();
final bool overwrite = boolArg('overwrite');
validateProjectDir(overwrite: overwrite);
if (boolArg('with-driver-test')) {
globals.printError(
'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 Map<String, Object> templateContext = createTemplateContext(
organization: organization,
projectName: projectName,
projectDescription: stringArg('description'),
flutterRoot: flutterRoot,
withPluginHook: generatePlugin,
androidLanguage: stringArg('android-language'),
iosLanguage: stringArg('ios-language'),
ios: featureFlags.isIOSEnabled && platforms.contains('ios'),
android: featureFlags.isAndroidEnabled && platforms.contains('android'),
web: featureFlags.isWebEnabled && platforms.contains('web'),
linux: featureFlags.isLinuxEnabled && platforms.contains('linux'),
macos: featureFlags.isMacOSEnabled && platforms.contains('macos'),
windows: featureFlags.isWindowsEnabled && platforms.contains('windows'),
windowsUwp: featureFlags.isWindowsUwpEnabled && platforms.contains('winuwp'),
// Enable null safety everywhere.
dartSdkVersionBounds: '">=2.12.0 <3.0.0"'
);
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;
switch (template) {
case FlutterProjectType.app:
generatedFileCount += await generateApp(relativeDir, templateContext, overwrite: overwrite);
break;
case FlutterProjectType.module:
generatedFileCount += await _generateModule(relativeDir, templateContext, overwrite: overwrite);
break;
case FlutterProjectType.package:
generatedFileCount += await _generatePackage(relativeDir, templateContext, overwrite: overwrite);
break;
case FlutterProjectType.plugin:
generatedFileCount += await _generatePlugin(relativeDir, templateContext, overwrite: overwrite);
break;
}
if (sampleCode != null) {
generatedFileCount += _applySample(relativeDir, sampleCode);
}
globals.printStatus('Wrote $generatedFileCount files.');
globals.printStatus('\nAll done!');
final String application = sampleCode != null ? 'sample application' : '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 (generatePlugin) {
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);
}
_printPluginAddPlatformMessage(relativePluginPath);
} 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('''
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);
}
}
return FlutterCommandResult.success();
}
Future<int> _generateModule(Directory directory, Map<String, dynamic> templateContext, { bool overwrite = false }) 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);
if (boolArg('pub')) {
await pub.get(
context: PubContext.create,
directory: directory.path,
offline: boolArg('offline'),
generateSyntheticPackage: false,
);
final FlutterProject project = FlutterProject.fromDirectory(directory);
await project.ensureReadyForPlatformSpecificTooling(
androidPlatform: true,
iosPlatform: true,
);
}
return generatedCount;
}
Future<int> _generatePackage(Directory directory, Map<String, dynamic> templateContext, { bool overwrite = false }) 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);
if (boolArg('pub')) {
await pub.get(
context: PubContext.createPackage,
directory: directory.path,
offline: boolArg('offline'),
generateSyntheticPackage: false,
);
}
return generatedCount;
}
Future<int> _generatePlugin(Directory directory, Map<String, dynamic> templateContext, { bool overwrite = false }) 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 renderTemplate('plugin', directory, templateContext, overwrite: overwrite);
if (boolArg('pub')) {
await pub.get(
context: PubContext.createPlugin,
directory: directory.path,
offline: boolArg('offline'),
generateSyntheticPackage: false,
);
}
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;
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(project.example.directory, templateContext, overwrite: overwrite, pluginExampleApp: true);
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).
int _applySample(Directory directory, String sampleCode) {
final File mainDartFile = directory.childDirectory('lib').childFile('main.dart');
mainDartFile.createSync(recursive: true);
mainDartFile.writeAsStringSync(sampleCode);
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, dynamic> templateContext) {
return <String>[
for (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 List<String> platforms = manifest.validSupportedPlatforms == null
? <String>[]
: manifest.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.
You example app code is in $relativeExampleMain.
''');
if (platformsString != null && 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) {
globals.printStatus('''
To add platforms, run `flutter create -t plugin --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
''');
}
}