blob: 70b8daab5d9fcf65a71310fc1ee7be8b788ce127 [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 'package:meta/meta.dart';
import 'package:uuid/uuid.dart';
import '../android/android.dart' as android_common;
import '../android/android_workflow.dart';
import '../android/gradle_utils.dart' as gradle;
import '../base/common.dart';
import '../base/file_system.dart';
import '../base/utils.dart';
import '../cache.dart';
import '../convert.dart';
import '../dart/pub.dart';
import '../features.dart';
import '../flutter_project_metadata.dart';
import '../globals_null_migrated.dart' as globals;
import '../project.dart';
import '../runner/flutter_command.dart';
import '../template.dart';
const List<String> _kAvailablePlatforms = <String>[
/// A list of all possible create platforms, even those that may not be enabled
/// with the current config.
const List<String> kAllCreatePlatforms = <String>[
const String _kDefaultPlatformArgumentHelp =
'(required) The platforms supported by this project. '
'Platform folders (e.g. android/) will be generated in the target project. '
'Adding desktop platforms requires the corresponding desktop config setting to be enabled.';
/// Common behavior for `flutter create` commands.
abstract class CreateBase extends FlutterCommand {
@required bool verboseHelp,
}) {
defaultsTo: true,
'Whether to run "flutter pub get" after the project has been created.',
defaultsTo: false,
'When "flutter pub get" is run by the create command, this indicates '
'whether to run it in offline mode or not. In offline mode, it will need to '
'have all dependencies already available in the pub cache to succeed.',
negatable: true,
defaultsTo: false,
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:',
hide: !verboseHelp,
negatable: true,
defaultsTo: false,
help: 'When performing operations, overwrite existing files.',
defaultsTo: 'A new Flutter project.',
'The description to use for your new Flutter project. This string ends up in the pubspec.yaml file.',
defaultsTo: 'com.example',
'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.',
defaultsTo: null,
'The project name for this new Flutter project. This must be a valid dart package name.',
abbr: 'i',
defaultsTo: 'swift',
allowed: <String>['objc', 'swift'],
help: 'The language to use for iOS-specific code, either ObjectiveC (legacy) or Swift (recommended).'
abbr: 'a',
defaultsTo: 'kotlin',
allowed: <String>['java', 'kotlin'],
help: 'The language to use for Android-specific code, either Java (legacy) or Kotlin (recommended).',
'Allow the creation of applications and plugins with invalid names. '
'This is only intended to enable testing of the tool itself.',
hide: !verboseHelp,
/// The output directory of the command.
Directory get projectDir {
/// The normalized absolute path of [projectDir].
String get projectDirPath {
return globals.fs.path.normalize(projectDir.absolute.path);
/// Adds a `--platforms` argument.
/// The help message of the argument is replaced with `customHelp` if `customHelp` is not null.
void addPlatformsOptions({String customHelp}) {
help: customHelp ?? _kDefaultPlatformArgumentHelp,
defaultsTo: <String>[
if (featureFlags.isWindowsUwpEnabled)
allowed: <String>[
if (featureFlags.isWindowsUwpEnabled)
/// Throw with exit code 2 if the output directory is invalid.
void validateOutputDirectoryArg() {
if ( {
'No option specified for the output directory.\n$usage',
exitCode: 2,
if ( > 1) {
String message = 'Multiple output directories specified.';
for (final String arg in {
if (arg.startsWith('-')) {
message += '\nTry moving $arg to be immediately following $name';
throwToolExit(message, exitCode: 2);
/// Gets the flutter root directory.
String get flutterRoot => Cache.flutterRoot;
/// Determines the project type in an existing flutter project.
/// If it has a .metadata file with the project_type in it, use that.
/// If it has an android dir and an android/app dir, it's a legacy app
/// If it has an ios dir and an ios/Flutter dir, it's a legacy app
/// Otherwise, we don't presume to know what type of project it could be, since
/// many of the files could be missing, and we can't really tell definitively.
/// Throws assertion if [projectDir] does not exist or empty.
/// Returns null if no project type can be determined.
FlutterProjectType determineTemplateType() {
assert(projectDir.existsSync() && projectDir.listSync().isNotEmpty);
final File metadataFile = globals.fs
.file(globals.fs.path.join(projectDir.absolute.path, '.metadata'));
final FlutterProjectMetadata projectMetadata =
FlutterProjectMetadata(metadataFile, globals.logger);
if (projectMetadata.projectType != null) {
return projectMetadata.projectType;
bool exists(List<String> path) {
return globals.fs
.joinAll(<String>[projectDir.absolute.path, ...path]))
// There either wasn't any metadata, or it didn't contain the project type,
// so try and figure out what type of project it is from the existing
// directory structure.
if (exists(<String>['android', 'app']) ||
exists(<String>['ios', 'Runner']) ||
exists(<String>['ios', 'Flutter'])) {
// Since we can't really be definitive on nearly-empty directories, err on
// the side of prudence and just say we don't know.
return null;
/// Determines the organization.
/// If `--org` is specified in the command, returns that directly.
/// If `--org` is not specified, returns the organization from the existing project.
Future<String> getOrganization() async {
String organization = stringArg('org');
if (!argResults.wasParsed('org')) {
final FlutterProject project = FlutterProject.fromDirectory(projectDir);
final Set<String> existingOrganizations = await project.organizationNames;
if (existingOrganizations.length == 1) {
organization = existingOrganizations.first;
} else if (existingOrganizations.length > 1) {
'Ambiguous organization in existing files: $existingOrganizations. '
'The --org command line argument must be specified to recreate project.');
return organization;
/// Throws with exit 2 if the project directory is illegal.
void validateProjectDir({bool overwrite = false}) {
if (globals.fs.path.isWithin(flutterRoot, projectDirPath)) {
// Make exception for dev and examples to facilitate example project development.
final String examplesDirectory = globals.fs.path.join(flutterRoot, 'examples');
final String devDirectory = globals.fs.path.join(flutterRoot, 'dev');
if (!globals.fs.path.isWithin(examplesDirectory, projectDirPath) &&
!globals.fs.path.isWithin(devDirectory, projectDirPath)) {
'Cannot create a project within the Flutter SDK. '
"Target directory '$projectDirPath' is within the Flutter SDK at '$flutterRoot'.",
exitCode: 2);
// If the destination directory is actually a file, then we refuse to
// overwrite, on the theory that the user probably didn't expect it to exist.
if (globals.fs.isFileSync(projectDirPath)) {
final String message =
"Invalid project name: '$projectDirPath' - refers to an existing file.";
? '$message Refusing to overwrite a file with a directory.'
: message,
exitCode: 2);
if (overwrite) {
final FileSystemEntityType type = globals.fs.typeSync(projectDirPath);
switch (type) {
case FileSystemEntityType.file:
// Do not overwrite files.
throwToolExit("Invalid project name: '$projectDirPath' - file exists.",
exitCode: 2);
// Do not overwrite links.
throwToolExit("Invalid project name: '$projectDirPath' - refers to a link.",
exitCode: 2);
/// Gets the project name based.
/// Use the current directory path name if the `--project-name` is not specified explicitly.
String get projectName {
final String projectName =
stringArg('project-name') ?? globals.fs.path.basename(projectDirPath);
if (!boolArg('skip-name-checks')) {
final String error = _validateProjectName(projectName);
if (error != null) {
return projectName;
/// Creates a template to use for [renderTemplate].
Map<String, Object> createTemplateContext({
String organization,
String projectName,
String projectDescription,
String androidLanguage,
String iosLanguage,
String flutterRoot,
String dartSdkVersionBounds,
bool withPluginHook = false,
bool ios = false,
bool android = false,
bool web = false,
bool linux = false,
bool macos = false,
bool windows = false,
bool windowsUwp = false,
}) {
final String pluginDartClass = _createPluginClassName(projectName);
final String pluginClass = pluginDartClass.endsWith('Plugin')
? pluginDartClass
: pluginDartClass + 'Plugin';
final String pluginClassSnakeCase = snakeCase(pluginClass);
final String pluginClassCapitalSnakeCase =
final String appleIdentifier =
createUTIIdentifier(organization, projectName);
final String androidIdentifier =
createAndroidIdentifier(organization, projectName);
final String windowsIdentifier =
createWindowsIdentifier(organization, projectName);
// Linux uses the same scheme as the Android identifier.
final String linuxIdentifier = androidIdentifier;
return <String, Object>{
'organization': organization,
'projectName': projectName,
'androidIdentifier': androidIdentifier,
'iosIdentifier': appleIdentifier,
'macosIdentifier': appleIdentifier,
'linuxIdentifier': linuxIdentifier,
'windowsIdentifier': windowsIdentifier,
'description': projectDescription,
'dartSdk': '$flutterRoot/bin/cache/dart-sdk',
'androidMinApiLevel': android_common.minApiLevel,
'androidSdkVersion': kAndroidSdkMinVersion,
'pluginClass': pluginClass,
'pluginClassSnakeCase': pluginClassSnakeCase,
'pluginClassCapitalSnakeCase': pluginClassCapitalSnakeCase,
'pluginDartClass': pluginDartClass,
// TODO(jonahwilliams): update after google3 uuid is updated.
// ignore: prefer_const_constructors
'pluginProjectUUID': Uuid().v4().toUpperCase(),
'withPluginHook': withPluginHook,
'androidLanguage': androidLanguage,
'iosLanguage': iosLanguage,
'flutterRevision': globals.flutterVersion.frameworkRevision,
'ios': ios,
'android': android,
'web': web,
'linux': linux,
'macos': macos,
'windows': windows,
'winuwp': windowsUwp,
'dartSdkVersionBounds': dartSdkVersionBounds,
/// Renders the template, generate files into `directory`.
/// `templateName` should match one of directory names under flutter_tools/template/.
/// If `overwrite` is true, overwrites existing files, `overwrite` defaults to `false`.
Future<int> renderTemplate(
String templateName, Directory directory, Map<String, Object> context,
{bool overwrite = false}) async {
final Template template = await Template.fromName(
fileSystem: globals.fs,
logger: globals.logger,
templateRenderer: globals.templateRenderer,
templateManifest: _templateManifest,
return template.render(directory, context, overwriteExisting: overwrite);
/// Generate application project in the `directory` using `templateContext`.
/// If `overwrite` is true, overwrites existing files, `overwrite` defaults to `false`.
Future<int> generateApp(
Directory directory, Map<String, Object> templateContext,
{bool overwrite = false, bool pluginExampleApp = false}) async {
int generatedCount = 0;
generatedCount += await renderTemplate(
overwrite: overwrite,
final FlutterProject project = FlutterProject.fromDirectory(directory);
if (templateContext['android'] == true) {
generatedCount += _injectGradleWrapper(project);
if (boolArg('pub')) {
await pub.get(
context: PubContext.create,
directory: directory.path,
offline: boolArg('offline'),
generateSyntheticPackage: false,
await project.ensureReadyForPlatformSpecificTooling(
androidPlatform: templateContext['android'] as bool ?? false,
iosPlatform: templateContext['ios'] as bool ?? false,
linuxPlatform: templateContext['linux'] as bool ?? false,
macOSPlatform: templateContext['macos'] as bool ?? false,
windowsPlatform: templateContext['windows'] as bool ?? false,
webPlatform: templateContext['web'] as bool ?? false,
winUwpPlatform: templateContext['winuwp'] as bool ?? false,
if (templateContext['android'] == true) {
gradle.updateLocalProperties(project: project, requireAndroidSdk: false);
return generatedCount;
/// Creates an android identifier.
/// Android application ID is specified in:
/// All characters must be alphanumeric or an underscore [a-zA-Z0-9_].
static String createAndroidIdentifier(String organization, String name) {
String tmpIdentifier = '$organization.$name';
final RegExp disallowed = RegExp(r'[^\w\.]');
tmpIdentifier = tmpIdentifier.replaceAll(disallowed, '');
// It must have at least two segments (one or more dots).
final List<String> segments = tmpIdentifier
.where((String segment) => segment.isNotEmpty)
while (segments.length < 2) {
// Each segment must start with a letter.
final RegExp segmentPatternRegex = RegExp(r'^[a-zA-Z][\w]*$');
final List<String> prefixedSegments = segment) {
if (!segmentPatternRegex.hasMatch(segment)) {
return 'u' + segment;
return segment;
return prefixedSegments.join('.');
/// Creates a Windows package name.
/// Package names must be a globally unique, commonly a GUID.
static String createWindowsIdentifier(String organization, String name) {
return const Uuid().v4().toUpperCase();
String _createPluginClassName(String name) {
final String camelizedName = camelCase(name);
return camelizedName[0].toUpperCase() + camelizedName.substring(1);
/// Create a UTI ( from a base name
static String createUTIIdentifier(String organization, String name) {
name = camelCase(name);
String tmpIdentifier = '$organization.$name';
final RegExp disallowed = RegExp(r'[^a-zA-Z0-9\-\.\u0080-\uffff]+');
tmpIdentifier = tmpIdentifier.replaceAll(disallowed, '');
// It must have at least two segments (one or more dots).
final List<String> segments = tmpIdentifier
.where((String segment) => segment.isNotEmpty)
while (segments.length < 2) {
return segments.join('.');
Set<Uri> get _templateManifest =>
__templateManifest ??= _computeTemplateManifest();
Set<Uri> __templateManifest;
Set<Uri> _computeTemplateManifest() {
final String flutterToolsAbsolutePath = globals.fs.path.join(
final String manifestPath = globals.fs.path.join(
final Map<String, Object> manifest = json.decode(
) as Map<String, Object>;
return Set<Uri>.from(
(manifest['files'] as List<Object>).cast<String>().map<Uri>(
(String path) =>
Uri.file(globals.fs.path.join(flutterToolsAbsolutePath, path))),
int _injectGradleWrapper(FlutterProject project) {
int filesCreated = 0;
onFileCopied: (File sourceFile, File destinationFile) {
final String modes = sourceFile.statSync().modeString();
if (modes != null && modes.contains('x')) {
return filesCreated;
// A valid Dart identifier that can be used for a package, i.e. no
// capital letters.
final RegExp _identifierRegExp = RegExp('[a-z_][a-z0-9_]*');
// non-contextual dart keywords.
const Set<String> _keywords = <String>{
const Set<String> _packageDependencies = <String>{
/// Whether [name] is a valid Pub package.
bool isValidPackageName(String name) {
final Match match = _identifierRegExp.matchAsPrefix(name);
return match != null &&
match.end == name.length &&
// Return null if the project name is legal. Return a validation message if
// we should disallow the project name.
String _validateProjectName(String projectName) {
if (!isValidPackageName(projectName)) {
return '"$projectName" is not a valid Dart package name.\n\n'
'See for more information.';
if (_packageDependencies.contains(projectName)) {
return "Invalid project name: '$projectName' - this will conflict with Flutter "
'package dependencies.';
return null;