mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00

Plugin projects are created by running `flutter create --plugin <name>`. An example app is also created in the plugin project, using the normal 'create' template, which has been modified to allow for conditional plugin code. Modified the android package name to match package naming conventions (all lower-case, and must match the directory name).
304 lines
11 KiB
Dart
304 lines
11 KiB
Dart
// Copyright 2015 The Chromium 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 'dart:async';
|
|
|
|
import '../android/android.dart' as android;
|
|
import '../base/common.dart';
|
|
import '../base/file_system.dart';
|
|
import '../base/utils.dart';
|
|
import '../build_info.dart';
|
|
import '../cache.dart';
|
|
import '../dart/pub.dart';
|
|
import '../doctor.dart';
|
|
import '../flx.dart' as flx;
|
|
import '../globals.dart';
|
|
import '../ios/xcodeproj.dart';
|
|
import '../runner/flutter_command.dart';
|
|
import '../template.dart';
|
|
|
|
class CreateCommand extends FlutterCommand {
|
|
CreateCommand() {
|
|
argParser.addFlag('pub',
|
|
defaultsTo: true,
|
|
help: 'Whether to run "flutter packages get" after the project has been created.'
|
|
);
|
|
argParser.addFlag(
|
|
'with-driver-test',
|
|
negatable: true,
|
|
defaultsTo: false,
|
|
help: 'Also add a flutter_driver dependency and generate a sample \'flutter drive\' test.'
|
|
);
|
|
argParser.addFlag(
|
|
'plugin',
|
|
negatable: true,
|
|
defaultsTo: false,
|
|
help: 'Generate a new Flutter Plugin project.'
|
|
);
|
|
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.'
|
|
);
|
|
}
|
|
|
|
@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<Null> runCommand() async {
|
|
if (argResults.rest.isEmpty)
|
|
throwToolExit('No option specified for the output directory.\n$usage', exitCode: 2);
|
|
|
|
if (argResults.rest.length > 1) {
|
|
String message = 'Multiple output directories specified.';
|
|
for (String arg in argResults.rest) {
|
|
if (arg.startsWith('-')) {
|
|
message += '\nTry moving $arg to be immediately following $name';
|
|
break;
|
|
}
|
|
}
|
|
throwToolExit(message, exitCode: 2);
|
|
}
|
|
|
|
if (Cache.flutterRoot == null)
|
|
throwToolExit('Neither the --flutter-root command line flag nor the FLUTTER_ROOT environment\n'
|
|
'variable was specified. Unable to find package:flutter.', exitCode: 2);
|
|
|
|
await Cache.instance.updateAll();
|
|
|
|
final String flutterRoot = fs.path.absolute(Cache.flutterRoot);
|
|
|
|
final String flutterPackagesDirectory = fs.path.join(flutterRoot, 'packages');
|
|
final String flutterPackagePath = fs.path.join(flutterPackagesDirectory, 'flutter');
|
|
if (!fs.isFileSync(fs.path.join(flutterPackagePath, 'pubspec.yaml')))
|
|
throwToolExit('Unable to find package:flutter in $flutterPackagePath', exitCode: 2);
|
|
|
|
final String flutterDriverPackagePath = fs.path.join(flutterRoot, 'packages', 'flutter_driver');
|
|
if (!fs.isFileSync(fs.path.join(flutterDriverPackagePath, 'pubspec.yaml')))
|
|
throwToolExit('Unable to find package:flutter_driver in $flutterDriverPackagePath', exitCode: 2);
|
|
|
|
final bool generatePlugin = argResults['plugin'];
|
|
|
|
final Directory projectDir = fs.directory(argResults.rest.first);
|
|
final String dirPath = fs.path.normalize(projectDir.absolute.path);
|
|
final String projectName = _normalizeProjectName(fs.path.basename(dirPath));
|
|
|
|
String error =_validateProjectDir(dirPath, flutterRoot: flutterRoot);
|
|
if (error != null)
|
|
throwToolExit(error);
|
|
|
|
error = _validateProjectName(projectName);
|
|
if (error != null)
|
|
throwToolExit(error);
|
|
|
|
final Map<String, dynamic> templateContext = _templateContext(
|
|
projectName, argResults['description'], dirPath,
|
|
flutterPackagesDirectory, renderDriverTest: argResults['with-driver-test'],
|
|
withPluginHook: generatePlugin,
|
|
);
|
|
|
|
printStatus('Creating project ${fs.path.relative(dirPath)}...');
|
|
int generatedCount = 0;
|
|
String appPath = dirPath;
|
|
if (generatePlugin) {
|
|
final String description = argResults.wasParsed('description')
|
|
? argResults['description']
|
|
: 'A new flutter plugin project.';
|
|
templateContext['description'] = description;
|
|
generatedCount += _renderTemplate('plugin', dirPath, templateContext);
|
|
|
|
if (argResults['pub'])
|
|
await pubGet(directory: dirPath);
|
|
|
|
appPath = fs.path.join(dirPath, 'example');
|
|
final String androidPluginIdentifier = templateContext['androidIdentifier'];
|
|
final String exampleProjectName = projectName + '_example';
|
|
templateContext['projectName'] = exampleProjectName;
|
|
templateContext['androidIdentifier'] = _createAndroidIdentifier(exampleProjectName);
|
|
templateContext['iosIdentifier'] = _createUTIIdentifier(exampleProjectName);
|
|
templateContext['description'] = 'Demonstrates how to use the $projectName plugin.';
|
|
templateContext['pluginProjectName'] = projectName;
|
|
templateContext['androidPluginIdentifier'] = androidPluginIdentifier;
|
|
}
|
|
|
|
generatedCount += _renderTemplate('create', appPath, templateContext);
|
|
if (argResults['with-driver-test']) {
|
|
final String testPath = fs.path.join(appPath, 'test_driver');
|
|
generatedCount += _renderTemplate('driver', testPath, templateContext);
|
|
}
|
|
|
|
printStatus('Wrote $generatedCount files.');
|
|
printStatus('');
|
|
|
|
updateXcodeGeneratedProperties(appPath, BuildMode.debug, flx.defaultMainPath);
|
|
|
|
if (argResults['pub'])
|
|
await pubGet(directory: appPath);
|
|
|
|
printStatus('');
|
|
|
|
// Run doctor; tell the user the next steps.
|
|
final String relativeAppPath = fs.path.relative(appPath);
|
|
final String relativePluginPath = fs.path.relative(dirPath);
|
|
if (doctor.canLaunchAnything) {
|
|
// Let them know a summary of the state of their tooling.
|
|
await doctor.summary();
|
|
|
|
printStatus('''
|
|
All done! In order to run your application, type:
|
|
|
|
\$ cd $relativeAppPath
|
|
\$ flutter run
|
|
|
|
Your main program file is lib/main.dart in the $relativeAppPath directory.
|
|
''');
|
|
if (generatePlugin) {
|
|
printStatus('''
|
|
Your plugin code is in lib/$projectName.dart in the $relativePluginPath directory.
|
|
|
|
Host platform code is in the android/ and ios/ directories under $relativePluginPath.
|
|
''');
|
|
}
|
|
} else {
|
|
printStatus("You'll need to install additional components before you can run "
|
|
"your Flutter app:");
|
|
printStatus('');
|
|
|
|
// Give the user more detailed analysis.
|
|
await doctor.diagnose();
|
|
printStatus('');
|
|
printStatus("After installing components, run 'flutter doctor' in order to "
|
|
"re-validate your setup.");
|
|
printStatus("When complete, type 'flutter run' from the '$relativeAppPath' "
|
|
"directory in order to launch your app.");
|
|
printStatus("Your main program file is: $relativeAppPath/lib/main.dart");
|
|
}
|
|
}
|
|
|
|
Map<String, dynamic> _templateContext(String projectName,
|
|
String projectDescription, String dirPath, String flutterPackagesDirectory,
|
|
{ bool renderDriverTest: false, bool withPluginHook: false }) {
|
|
flutterPackagesDirectory = fs.path.normalize(flutterPackagesDirectory);
|
|
flutterPackagesDirectory = _relativePath(from: dirPath, to: flutterPackagesDirectory);
|
|
|
|
final String pluginDartClass = _createPluginClassName(projectName);
|
|
final String pluginClass = pluginDartClass.endsWith('Plugin')
|
|
? pluginDartClass
|
|
: pluginDartClass + 'Plugin';
|
|
|
|
return <String, dynamic>{
|
|
'projectName': projectName,
|
|
'androidIdentifier': _createAndroidIdentifier(projectName),
|
|
'iosIdentifier': _createUTIIdentifier(projectName),
|
|
'description': projectDescription,
|
|
'flutterPackagesDirectory': flutterPackagesDirectory,
|
|
'androidMinApiLevel': android.minApiLevel,
|
|
'withDriverTest': renderDriverTest,
|
|
'pluginClass': pluginClass,
|
|
'pluginDartClass': pluginDartClass,
|
|
'withPluginHook': withPluginHook,
|
|
};
|
|
}
|
|
|
|
int _renderTemplate(String templateName, String dirPath, Map<String, dynamic> context) {
|
|
final Template template = new Template.fromName(templateName);
|
|
return template.render(fs.directory(dirPath), context, overwriteExisting: false);
|
|
}
|
|
}
|
|
|
|
String _normalizeProjectName(String name) {
|
|
name = name.replaceAll('-', '_').replaceAll(' ', '_');
|
|
// Strip any extension (like .dart).
|
|
if (name.contains('.'))
|
|
name = name.substring(0, name.indexOf('.'));
|
|
return name;
|
|
}
|
|
|
|
String _createAndroidIdentifier(String name) {
|
|
return 'com.yourcompany.$name';
|
|
}
|
|
|
|
String _createPluginClassName(String name) {
|
|
final String camelizedName = camelCase(name);
|
|
return camelizedName[0].toUpperCase() + camelizedName.substring(1);
|
|
}
|
|
|
|
String _createUTIIdentifier(String name) {
|
|
// Create a UTI (https://en.wikipedia.org/wiki/Uniform_Type_Identifier) from a base name
|
|
final RegExp disallowed = new RegExp(r"[^a-zA-Z0-9\-\.\u0080-\uffff]+");
|
|
name = camelCase(name).replaceAll(disallowed, '');
|
|
name = name.isEmpty ? 'untitled' : name;
|
|
return 'com.yourcompany.$name';
|
|
}
|
|
|
|
final Set<String> _packageDependencies = new Set<String>.from(<String>[
|
|
'args',
|
|
'async',
|
|
'collection',
|
|
'convert',
|
|
'flutter',
|
|
'html',
|
|
'intl',
|
|
'logging',
|
|
'matcher',
|
|
'mime',
|
|
'path',
|
|
'plugin',
|
|
'pool',
|
|
'test',
|
|
'utf',
|
|
'watcher',
|
|
'yaml'
|
|
]);
|
|
|
|
/// Return `null` if the project name is legal. Return a validation message if
|
|
/// we should disallow the project name.
|
|
String _validateProjectName(String projectName) {
|
|
if (_packageDependencies.contains(projectName)) {
|
|
return "Invalid project name: '$projectName' - this will conflict with Flutter "
|
|
"package dependencies.";
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/// Return `null` if the project directory is legal. Return a validation message
|
|
/// if we should disallow the directory name.
|
|
String _validateProjectDir(String dirPath, { String flutterRoot }) {
|
|
if (fs.path.isWithin(flutterRoot, dirPath)) {
|
|
return "Cannot create a project within the Flutter SDK.\n"
|
|
"Target directory '$dirPath' is within the Flutter SDK at '$flutterRoot'.";
|
|
}
|
|
|
|
final FileSystemEntityType type = fs.typeSync(dirPath);
|
|
|
|
if (type != FileSystemEntityType.NOT_FOUND) {
|
|
switch(type) {
|
|
case FileSystemEntityType.FILE:
|
|
// Do not overwrite files.
|
|
return "Invalid project name: '$dirPath' - file exists.";
|
|
case FileSystemEntityType.LINK:
|
|
// Do not overwrite links.
|
|
return "Invalid project name: '$dirPath' - refers to a link.";
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
String _relativePath({ String from, String to }) {
|
|
final String result = fs.path.relative(to, from: from);
|
|
// `fs.path.relative()` doesn't always return a correct result: dart-lang/path#12.
|
|
if (fs.isDirectorySync(fs.path.join(from, result)))
|
|
return result;
|
|
return to;
|
|
}
|