blob: 3de6c2b84443b4e8293fbd224ab9e45b4c503f0d [file] [log] [blame]
Ian Hickson449f4a62019-11-27 15:04:02 -08001// Copyright 2014 The Flutter Authors. All rights reserved.
Devon Carew2efd1312015-08-07 14:20:14 -07002// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
Devon Carew2efd1312015-08-07 14:20:14 -07005import 'dart:async';
Devon Carew2efd1312015-08-07 14:20:14 -07006
Jonah Williams0a2d8e02019-12-16 18:37:20 -08007import 'package:meta/meta.dart';
Greg Spencer9f238662018-10-04 13:03:20 -07008import 'package:yaml/yaml.dart' as yaml;
Michael Goderbauerca4d7212017-05-08 14:08:59 -07009
Devon Carew5ee1cba2016-02-24 23:40:00 -080010import '../android/android.dart' as android;
Brian Slesinsky5cb90972017-04-14 10:12:48 -070011import '../android/android_sdk.dart' as android_sdk;
Emmanuel Garcia175b3722019-10-31 13:19:15 -070012import '../android/gradle_utils.dart' as gradle;
Dan Rubelcccd9172016-11-11 10:42:09 -050013import '../base/common.dart';
Todd Volkert8bb27032017-01-06 16:51:44 -080014import '../base/file_system.dart';
Greg Spencerf9c6f302018-10-30 16:01:14 -070015import '../base/net.dart';
Mikkel Nygaard Ravnc5999c72017-06-26 12:47:43 +020016import '../base/os.dart';
Devon Carew4c569192016-03-02 11:22:19 -080017import '../base/utils.dart';
Adam Barth412ce9d2016-04-08 08:51:44 -070018import '../cache.dart';
Jonah Williams91fd89e2019-01-25 16:16:26 -080019import '../convert.dart';
Adam Barthcf811632016-02-16 11:02:41 -080020import '../dart/pub.dart';
John McCutchan0b737ac2016-11-29 07:54:20 -080021import '../doctor.dart';
Jonah Williams3fedb8c2019-07-22 15:34:03 -070022import '../features.dart';
Jonah Williamsee7a37f2020-01-06 11:04:20 -080023import '../globals.dart' as globals;
Mikkel Nygaard Ravn43532972018-01-18 09:21:24 +010024import '../project.dart';
Zachary Andersonef146f62019-07-29 07:24:02 -070025import '../reporting/reporting.dart';
Devon Carewadac9272016-04-26 16:25:11 -070026import '../runner/flutter_command.dart';
Chinmay Garde038367a2016-02-23 15:40:51 -080027import '../template.dart';
xsterc17099f2017-11-03 10:07:57 -070028import '../version.dart';
Hixiea0227ca2015-11-11 10:29:05 -080029
Greg Spencer21a32fd2018-10-05 15:49:53 -070030enum _ProjectType {
Greg Spencer0ff9e8a2018-10-10 11:01:40 -070031 /// This is the default project with the user-managed host code.
32 /// It is different than the "module" template in that it exposes and doesn't
33 /// manage the platform code.
Greg Spencer9f238662018-10-04 13:03:20 -070034 app,
Greg Spencer0ff9e8a2018-10-10 11:01:40 -070035 /// The is a project that has managed platform host code. It is an application with
Greg Spencer21a32fd2018-10-05 15:49:53 -070036 /// ephemeral .ios and .android directories that can be updated automatically.
Greg Spencer0ff9e8a2018-10-10 11:01:40 -070037 module,
Greg Spencer21a32fd2018-10-05 15:49:53 -070038 /// This is a Flutter Dart package project. It doesn't have any native
39 /// components, only Dart.
Greg Spencer9f238662018-10-04 13:03:20 -070040 package,
Greg Spencer21a32fd2018-10-05 15:49:53 -070041 /// This is a native plugin project.
Greg Spencer9f238662018-10-04 13:03:20 -070042 plugin,
43}
44
Greg Spencer21a32fd2018-10-05 15:49:53 -070045_ProjectType _stringToProjectType(String value) {
46 _ProjectType result;
Alexandre Ardhuin4f9b6cf2020-01-07 16:32:04 +010047 for (final _ProjectType type in _ProjectType.values) {
Greg Spencer9f238662018-10-04 13:03:20 -070048 if (value == getEnumName(type)) {
49 result = type;
50 break;
51 }
52 }
Greg Spencer9f238662018-10-04 13:03:20 -070053 return result;
54}
55
Devon Carewadac9272016-04-26 16:25:11 -070056class CreateCommand extends FlutterCommand {
Greg Spencer0ff9e8a2018-10-10 11:01:40 -070057 CreateCommand() {
Kevin Moore64feb952017-12-07 14:29:12 -080058 argParser.addFlag('pub',
59 defaultsTo: true,
Michael Thomsen7ae3caf2019-05-21 16:38:58 +020060 help: 'Whether to run "flutter pub get" after the project has been created.',
Kevin Moore64feb952017-12-07 14:29:12 -080061 );
Greg Spencer6b8ceb92017-12-11 21:21:41 -080062 argParser.addFlag('offline',
63 defaultsTo: false,
Michael Thomsen7ae3caf2019-05-21 16:38:58 +020064 help: 'When "flutter pub get" is run by the create command, this indicates '
Greg Spencer6b8ceb92017-12-11 21:21:41 -080065 'whether to run it in offline mode or not. In offline mode, it will need to '
Alexandre Ardhuin387f8852019-03-01 08:17:55 +010066 'have all dependencies already available in the pub cache to succeed.',
Greg Spencer6b8ceb92017-12-11 21:21:41 -080067 );
yjbanov278630e2016-02-22 14:55:17 -080068 argParser.addFlag(
69 'with-driver-test',
70 negatable: true,
71 defaultsTo: false,
Alexandre Ardhuin387f8852019-03-01 08:17:55 +010072 help: "Also add a flutter_driver dependency and generate a sample 'flutter drive' test.",
yjbanov278630e2016-02-22 14:55:17 -080073 );
Jakob Andersen5d0d6122017-08-23 13:29:31 +020074 argParser.addOption(
75 'template',
76 abbr: 't',
Greg Spencer21a32fd2018-10-05 15:49:53 -070077 allowed: _ProjectType.values.map<String>((_ProjectType type) => getEnumName(type)),
Jakob Andersen5d0d6122017-08-23 13:29:31 +020078 help: 'Specify the type of project to create.',
79 valueHelp: 'type',
80 allowedHelp: <String, String>{
Greg Spencer0ff9e8a2018-10-10 11:01:40 -070081 getEnumName(_ProjectType.app): '(default) Generate a Flutter application.',
Greg Spencer21a32fd2018-10-05 15:49:53 -070082 getEnumName(_ProjectType.package): 'Generate a shareable Flutter project containing modular '
Greg Spencer9f238662018-10-04 13:03:20 -070083 'Dart code.',
Greg Spencer21a32fd2018-10-05 15:49:53 -070084 getEnumName(_ProjectType.plugin): 'Generate a shareable Flutter project containing an API '
Greg Spencer9f238662018-10-04 13:03:20 -070085 'in Dart code with a platform-specific implementation for Android, for iOS code, or '
86 'for both.',
Jenn Magdere57ab1f2019-11-13 18:28:45 -080087 getEnumName(_ProjectType.module): 'Generate a project to add a Flutter module to an '
88 'existing Android or iOS application.',
Greg Spencer0ff9e8a2018-10-10 11:01:40 -070089 },
Greg Spencer9f238662018-10-04 13:03:20 -070090 defaultsTo: null,
Jakob Andersen5d0d6122017-08-23 13:29:31 +020091 );
Ian Hicksona7016062016-03-19 18:37:32 -070092 argParser.addOption(
Greg Spencerf9c6f302018-10-30 16:01:14 -070093 'sample',
94 abbr: 's',
95 help: 'Specifies the Flutter code sample to use as the main.dart for an application. Implies '
Greg Spencerb3169402019-02-21 15:56:59 -080096 '--template=app. The value should be the sample ID of the desired sample from the API '
Jonah Williamsbe739692019-04-22 22:22:46 -070097 'documentation website (http://docs.flutter.dev). An example can be found at '
98 'https://master-api.flutter.dev/flutter/widgets/SingleChildScrollView-class.html',
Greg Spencerf9c6f302018-10-30 16:01:14 -070099 defaultsTo: null,
Greg Spencerb3169402019-02-21 15:56:59 -0800100 valueHelp: 'id',
Greg Spencerf9c6f302018-10-30 16:01:14 -0700101 );
Danny Tuppeny126c58e2019-03-12 11:47:17 +0000102 argParser.addOption(
103 'list-samples',
104 help: 'Specifies a JSON output file for a listing of Flutter code samples '
105 'that can created with --sample.',
106 valueHelp: 'path',
107 );
Greg Spencerf9c6f302018-10-30 16:01:14 -0700108 argParser.addFlag(
109 'overwrite',
110 negatable: true,
111 defaultsTo: false,
112 help: 'When performing operations, overwrite existing files.',
113 );
114 argParser.addOption(
Ian Hicksona7016062016-03-19 18:37:32 -0700115 'description',
Mikkel Nygaard Ravn251d83a2017-05-24 16:19:16 +0200116 defaultsTo: 'A new Flutter project.',
Alexandre Ardhuin387f8852019-03-01 08:17:55 +0100117 help: 'The description to use for your new Flutter project. This string ends up in the pubspec.yaml file.',
Mikkel Nygaard Ravn251d83a2017-05-24 16:19:16 +0200118 );
119 argParser.addOption(
120 'org',
Mikkel Nygaard Ravn43532972018-01-18 09:21:24 +0100121 defaultsTo: 'com.example',
Greg Spencer081d2a72018-10-10 18:17:56 -0700122 help: 'The organization responsible for your new Flutter project, in reverse domain name notation. '
Alexandre Ardhuin387f8852019-03-01 08:17:55 +0100123 'This string is used in Java package names and as prefix in the iOS bundle identifier.',
Ian Hicksona7016062016-03-19 18:37:32 -0700124 );
Mikkel Nygaard Ravn10f64832017-05-24 08:22:50 +0200125 argParser.addOption(
Sebastian Rothb95b67a2018-10-17 23:25:46 +0800126 'project-name',
127 defaultsTo: null,
Alexandre Ardhuin387f8852019-03-01 08:17:55 +0100128 help: 'The project name for this new Flutter project. This must be a valid dart package name.',
Sebastian Rothb95b67a2018-10-17 23:25:46 +0800129 );
130 argParser.addOption(
Mikkel Nygaard Ravn10f64832017-05-24 08:22:50 +0200131 'ios-language',
132 abbr: 'i',
Zachary Andersone24a27d2019-08-15 12:13:28 -0700133 defaultsTo: 'swift',
Mikkel Nygaard Ravn10f64832017-05-24 08:22:50 +0200134 allowed: <String>['objc', 'swift'],
135 );
136 argParser.addOption(
137 'android-language',
138 abbr: 'a',
Zachary Andersone24a27d2019-08-15 12:13:28 -0700139 defaultsTo: 'kotlin',
Mikkel Nygaard Ravn10f64832017-05-24 08:22:50 +0200140 allowed: <String>['java', 'kotlin'],
141 );
Josh Burtond0e45a22019-06-01 13:33:02 +1200142 argParser.addFlag(
143 'androidx',
144 negatable: true,
Emmanuel Garcia48ce6082019-09-24 16:55:09 -0700145 defaultsTo: true,
Josh Burtond0e45a22019-06-01 13:33:02 +1200146 help: 'Generate a project using the AndroidX support libraries',
147 );
Devon Carew2efd1312015-08-07 14:20:14 -0700148 }
149
Hixie797e27e2016-03-14 13:31:43 -0700150 @override
Devon Carewadac9272016-04-26 16:25:11 -0700151 final String name = 'create';
152
153 @override
154 final String description = 'Create a new Flutter project.\n\n'
155 'If run on a project that already exists, this will repair the project, recreating any files that are missing.';
156
157 @override
Alexandre Ardhuin1fce14a2017-10-22 18:11:36 +0200158 String get invocation => '${runner.executableName} $name <output directory>';
Devon Carew0da74632016-02-18 14:33:59 -0800159
Emmanuel Garcia3bbdf012019-05-29 20:56:28 -0700160 @override
Zachary Andersonef146f62019-07-29 07:24:02 -0700161 Future<Map<CustomDimensions, String>> get usageValues async {
162 return <CustomDimensions, String>{
Alexandre Ardhuinadc73512019-11-19 07:57:42 +0100163 CustomDimensions.commandCreateProjectType: stringArg('template'),
164 CustomDimensions.commandCreateAndroidLanguage: stringArg('android-language'),
165 CustomDimensions.commandCreateIosLanguage: stringArg('ios-language'),
Emmanuel Garcia3bbdf012019-05-29 20:56:28 -0700166 };
167 }
168
Greg Spencer9f238662018-10-04 13:03:20 -0700169 // If it has a .metadata file with the project_type in it, use that.
170 // If it has an android dir and an android/app dir, it's a legacy app
Greg Spencer21a32fd2018-10-05 15:49:53 -0700171 // If it has an ios dir and an ios/Flutter dir, it's a legacy app
172 // Otherwise, we don't presume to know what type of project it could be, since
173 // many of the files could be missing, and we can't really tell definitively.
174 _ProjectType _determineTemplateType(Directory projectDir) {
Greg Spencer9f238662018-10-04 13:03:20 -0700175 yaml.YamlMap loadMetadata(Directory projectDir) {
Zachary Andersone2340c62019-09-13 14:51:35 -0700176 if (!projectDir.existsSync()) {
Greg Spencer9f238662018-10-04 13:03:20 -0700177 return null;
Zachary Andersone2340c62019-09-13 14:51:35 -0700178 }
Jonah Williamsee7a37f2020-01-06 11:04:20 -0800179 final File metadataFile = globals.fs.file(globals.fs.path.join(projectDir.absolute.path, '.metadata'));
Zachary Andersone2340c62019-09-13 14:51:35 -0700180 if (!metadataFile.existsSync()) {
Greg Spencer9f238662018-10-04 13:03:20 -0700181 return null;
Zachary Andersone2340c62019-09-13 14:51:35 -0700182 }
Alexandre Ardhuinadc73512019-11-19 07:57:42 +0100183 final dynamic metadataYaml = yaml.loadYaml(metadataFile.readAsStringSync());
184 if (metadataYaml is yaml.YamlMap) {
185 return metadataYaml;
186 } else {
187 throwToolExit('pubspec.yaml is malformed.');
188 return null;
189 }
Greg Spencer9f238662018-10-04 13:03:20 -0700190 }
191
Greg Spencer21a32fd2018-10-05 15:49:53 -0700192 bool exists(List<String> path) {
Jonah Williamsee7a37f2020-01-06 11:04:20 -0800193 return globals.fs.directory(globals.fs.path.joinAll(<String>[projectDir.absolute.path, ...path])).existsSync();
Greg Spencer21a32fd2018-10-05 15:49:53 -0700194 }
195
Greg Spencer9f238662018-10-04 13:03:20 -0700196 // If it exists, the project type in the metadata is definitive.
197 final yaml.YamlMap metadata = loadMetadata(projectDir);
198 if (metadata != null && metadata['project_type'] != null) {
Alexandre Ardhuinadc73512019-11-19 07:57:42 +0100199 final dynamic projectType = metadata['project_type'];
200 if (projectType is String) {
201 return _stringToProjectType(projectType);
202 } else {
203 throwToolExit('.metadata is malformed.');
204 return null;
205 }
Greg Spencer9f238662018-10-04 13:03:20 -0700206 }
207
208 // There either wasn't any metadata, or it didn't contain the project type,
209 // so try and figure out what type of project it is from the existing
210 // directory structure.
Greg Spencer21a32fd2018-10-05 15:49:53 -0700211 if (exists(<String>['android', 'app'])
212 || exists(<String>['ios', 'Runner'])
213 || exists(<String>['ios', 'Flutter'])) {
214 return _ProjectType.app;
215 }
216 // Since we can't really be definitive on nearly-empty directories, err on
217 // the side of prudence and just say we don't know.
Greg Spencer9f238662018-10-04 13:03:20 -0700218 return null;
219 }
220
Danny Tuppeny126c58e2019-03-12 11:47:17 +0000221 /// The hostname for the Flutter docs for the current channel.
222 String get _snippetsHost => FlutterVersion.instance.channel == 'stable'
223 ? 'docs.flutter.io'
224 : 'master-docs.flutter.io';
225
Greg Spencerf9c6f302018-10-30 16:01:14 -0700226 Future<String> _fetchSampleFromServer(String sampleId) async {
227 // Sanity check the sampleId
228 if (sampleId.contains(RegExp(r'[^-\w\.]'))) {
229 throwToolExit('Sample ID "$sampleId" contains invalid characters. Check the ID in the '
230 'documentation and try again.');
231 }
232
Danny Tuppeny126c58e2019-03-12 11:47:17 +0000233 return utf8.decode(await fetchUrl(Uri.https(_snippetsHost, 'snippets/$sampleId.dart')));
234 }
235
236 /// Fetches the samples index file from the Flutter docs website.
237 Future<String> _fetchSamplesIndexFromServer() async {
Devon Carew50a9c312019-05-16 18:20:30 -0700238 return utf8.decode(
239 await fetchUrl(Uri.https(_snippetsHost, 'snippets/index.json'), maxAttempts: 2));
Danny Tuppeny126c58e2019-03-12 11:47:17 +0000240 }
241
242 /// Fetches the samples index file from the server and writes it to
243 /// [outputFilePath].
244 Future<void> _writeSamplesJson(String outputFilePath) async {
245 try {
Jonah Williamsee7a37f2020-01-06 11:04:20 -0800246 final File outputFile = globals.fs.file(outputFilePath);
Danny Tuppeny126c58e2019-03-12 11:47:17 +0000247 if (outputFile.existsSync()) {
248 throwToolExit('File "$outputFilePath" already exists', exitCode: 1);
249 }
Devon Carew50a9c312019-05-16 18:20:30 -0700250 final String samplesJson = await _fetchSamplesIndexFromServer();
251 if (samplesJson == null) {
252 throwToolExit('Unable to download samples', exitCode: 2);
253 }
254 else {
255 outputFile.writeAsStringSync(samplesJson);
Jonah Williamsee7a37f2020-01-06 11:04:20 -0800256 globals.printStatus('Wrote samples JSON to "$outputFilePath"');
Devon Carew50a9c312019-05-16 18:20:30 -0700257 }
Danny Tuppeny126c58e2019-03-12 11:47:17 +0000258 } catch (e) {
259 throwToolExit('Failed to write samples JSON to "$outputFilePath": $e', exitCode: 2);
260 }
Greg Spencerf9c6f302018-10-30 16:01:14 -0700261 }
262
Emmanuel Garcia3bbdf012019-05-29 20:56:28 -0700263 _ProjectType _getProjectType(Directory projectDir) {
264 _ProjectType template;
265 _ProjectType detectedProjectType;
266 final bool metadataExists = projectDir.absolute.childFile('.metadata').existsSync();
267 if (argResults['template'] != null) {
Alexandre Ardhuinadc73512019-11-19 07:57:42 +0100268 template = _stringToProjectType(stringArg('template'));
Emmanuel Garcia3bbdf012019-05-29 20:56:28 -0700269 } else {
270 // If the project directory exists and isn't empty, then try to determine the template
271 // type from the project directory.
272 if (projectDir.existsSync() && projectDir.listSync().isNotEmpty) {
273 detectedProjectType = _determineTemplateType(projectDir);
274 if (detectedProjectType == null && metadataExists) {
275 // We can only be definitive that this is the wrong type if the .metadata file
276 // exists and contains a type that we don't understand, or doesn't contain a type.
277 throwToolExit('Sorry, unable to detect the type of project to recreate. '
278 'Try creating a fresh project and migrating your existing code to '
279 'the new project manually.');
280 }
281 }
282 }
283 template ??= detectedProjectType ?? _ProjectType.app;
284 if (detectedProjectType != null && template != detectedProjectType && metadataExists) {
285 // We can only be definitive that this is the wrong type if the .metadata file
286 // exists and contains a type that doesn't match.
287 throwToolExit("The requested template type '${getEnumName(template)}' doesn't match the "
288 "existing template type of '${getEnumName(detectedProjectType)}'.");
289 }
290 return template;
291 }
292
Ian Fischer384ded52015-09-10 14:40:25 -0700293 @override
Alexandre Ardhuin2d3ff102018-10-05 07:54:56 +0200294 Future<FlutterCommandResult> runCommand() async {
Danny Tuppeny126c58e2019-03-12 11:47:17 +0000295 if (argResults['list-samples'] != null) {
Devon Carew50a9c312019-05-16 18:20:30 -0700296 // _writeSamplesJson can potentially be long-lived.
297 Cache.releaseLockEarly();
298
Alexandre Ardhuinadc73512019-11-19 07:57:42 +0100299 await _writeSamplesJson(stringArg('list-samples'));
Danny Tuppeny126c58e2019-03-12 11:47:17 +0000300 return null;
301 }
302
Zachary Andersone2340c62019-09-13 14:51:35 -0700303 if (argResults.rest.isEmpty) {
Dan Rubelcccd9172016-11-11 10:42:09 -0500304 throwToolExit('No option specified for the output directory.\n$usage', exitCode: 2);
Zachary Andersone2340c62019-09-13 14:51:35 -0700305 }
Devon Carew2efd1312015-08-07 14:20:14 -0700306
Ian Hicksona7016062016-03-19 18:37:32 -0700307 if (argResults.rest.length > 1) {
Dan Rubelcccd9172016-11-11 10:42:09 -0500308 String message = 'Multiple output directories specified.';
Alexandre Ardhuin4f9b6cf2020-01-07 16:32:04 +0100309 for (final String arg in argResults.rest) {
Dan Rubel949e27a2016-10-26 19:11:55 +0100310 if (arg.startsWith('-')) {
Dan Rubelcccd9172016-11-11 10:42:09 -0500311 message += '\nTry moving $arg to be immediately following $name';
Dan Rubel949e27a2016-10-26 19:11:55 +0100312 break;
313 }
314 }
Dan Rubelcccd9172016-11-11 10:42:09 -0500315 throwToolExit(message, exitCode: 2);
Ian Hicksona7016062016-03-19 18:37:32 -0700316 }
317
Zachary Andersone2340c62019-09-13 14:51:35 -0700318 if (Cache.flutterRoot == null) {
Greg Spencer081d2a72018-10-10 18:17:56 -0700319 throwToolExit('Neither the --flutter-root command line flag nor the FLUTTER_ROOT environment '
Dan Rubelcccd9172016-11-11 10:42:09 -0500320 'variable was specified. Unable to find package:flutter.', exitCode: 2);
Zachary Andersone2340c62019-09-13 14:51:35 -0700321 }
Devon Carew0da74632016-02-18 14:33:59 -0800322
Jonah Williamsee7a37f2020-01-06 11:04:20 -0800323 final String flutterRoot = globals.fs.path.absolute(Cache.flutterRoot);
Adam Barth2710e0f2015-11-07 15:02:27 -0800324
Jonah Williamsee7a37f2020-01-06 11:04:20 -0800325 final String flutterPackagesDirectory = globals.fs.path.join(flutterRoot, 'packages');
326 final String flutterPackagePath = globals.fs.path.join(flutterPackagesDirectory, 'flutter');
327 if (!globals.fs.isFileSync(globals.fs.path.join(flutterPackagePath, 'pubspec.yaml'))) {
Dan Rubelcccd9172016-11-11 10:42:09 -0500328 throwToolExit('Unable to find package:flutter in $flutterPackagePath', exitCode: 2);
Zachary Andersone2340c62019-09-13 14:51:35 -0700329 }
Adam Barth2710e0f2015-11-07 15:02:27 -0800330
Jonah Williamsee7a37f2020-01-06 11:04:20 -0800331 final String flutterDriverPackagePath = globals.fs.path.join(flutterRoot, 'packages', 'flutter_driver');
332 if (!globals.fs.isFileSync(globals.fs.path.join(flutterDriverPackagePath, 'pubspec.yaml'))) {
Dan Rubelcccd9172016-11-11 10:42:09 -0500333 throwToolExit('Unable to find package:flutter_driver in $flutterDriverPackagePath', exitCode: 2);
Zachary Andersone2340c62019-09-13 14:51:35 -0700334 }
yjbanov278630e2016-02-22 14:55:17 -0800335
Jonah Williamsee7a37f2020-01-06 11:04:20 -0800336 final Directory projectDir = globals.fs.directory(argResults.rest.first);
337 final String projectDirPath = globals.fs.path.normalize(projectDir.absolute.path);
Greg Spencer9f238662018-10-04 13:03:20 -0700338
Greg Spencerf9c6f302018-10-30 16:01:14 -0700339 String sampleCode;
340 if (argResults['sample'] != null) {
341 if (argResults['template'] != null &&
Alexandre Ardhuinadc73512019-11-19 07:57:42 +0100342 _stringToProjectType(stringArg('template') ?? 'app') != _ProjectType.app) {
Greg Spencerf9c6f302018-10-30 16:01:14 -0700343 throwToolExit('Cannot specify --sample with a project type other than '
344 '"${getEnumName(_ProjectType.app)}"');
345 }
346 // Fetch the sample from the server.
Alexandre Ardhuinadc73512019-11-19 07:57:42 +0100347 sampleCode = await _fetchSampleFromServer(stringArg('sample'));
Greg Spencerf9c6f302018-10-30 16:01:14 -0700348 }
349
Emmanuel Garcia3bbdf012019-05-29 20:56:28 -0700350 final _ProjectType template = _getProjectType(projectDir);
Greg Spencer0ff9e8a2018-10-10 11:01:40 -0700351 final bool generateModule = template == _ProjectType.module;
Greg Spencer21a32fd2018-10-05 15:49:53 -0700352 final bool generatePlugin = template == _ProjectType.plugin;
353 final bool generatePackage = template == _ProjectType.package;
Greg Spencer9f238662018-10-04 13:03:20 -0700354
Alexandre Ardhuinadc73512019-11-19 07:57:42 +0100355 String organization = stringArg('org');
Mikkel Nygaard Ravn43532972018-01-18 09:21:24 +0100356 if (!argResults.wasParsed('org')) {
Jonah Williams4ff46712019-04-29 08:21:32 -0700357 final FlutterProject project = FlutterProject.fromDirectory(projectDir);
Zachary Anderson8a33d242019-09-16 07:51:50 -0700358 final Set<String> existingOrganizations = await project.organizationNames;
Mikkel Nygaard Ravn43532972018-01-18 09:21:24 +0100359 if (existingOrganizations.length == 1) {
360 organization = existingOrganizations.first;
Afzaal Ahmad Zeeshan9492dcc2019-11-07 23:17:03 +0500361 } else if (existingOrganizations.length > 1) {
Mikkel Nygaard Ravn43532972018-01-18 09:21:24 +0100362 throwToolExit(
Greg Spencer081d2a72018-10-10 18:17:56 -0700363 'Ambiguous organization in existing files: $existingOrganizations. '
Mikkel Nygaard Ravn43532972018-01-18 09:21:24 +0100364 'The --org command line argument must be specified to recreate project.'
365 );
366 }
367 }
Devon Carewebf1ecc2016-03-01 10:01:37 -0800368
Alexandre Ardhuinadc73512019-11-19 07:57:42 +0100369 final bool overwrite = boolArg('overwrite');
370 String error = _validateProjectDir(projectDirPath, flutterRoot: flutterRoot, overwrite: overwrite);
Zachary Andersone2340c62019-09-13 14:51:35 -0700371 if (error != null) {
Dan Rubelcccd9172016-11-11 10:42:09 -0500372 throwToolExit(error);
Zachary Andersone2340c62019-09-13 14:51:35 -0700373 }
Devon Carew95fa9e32016-10-05 08:26:17 -0700374
Jonah Williamsee7a37f2020-01-06 11:04:20 -0800375 final String projectName = stringArg('project-name') ?? globals.fs.path.basename(projectDirPath);
Adam Barth030f6232016-10-28 20:29:19 -0700376 error = _validateProjectName(projectName);
Zachary Andersone2340c62019-09-13 14:51:35 -0700377 if (error != null) {
Dan Rubelcccd9172016-11-11 10:42:09 -0500378 throwToolExit(error);
Zachary Andersone2340c62019-09-13 14:51:35 -0700379 }
Devon Carewebf1ecc2016-03-01 10:01:37 -0800380
Jakob Andersenf34f8a32017-03-30 12:39:21 +0200381 final Map<String, dynamic> templateContext = _templateContext(
Mikkel Nygaard Ravn251d83a2017-05-24 16:19:16 +0200382 organization: organization,
Mikkel Nygaard Ravn10f64832017-05-24 08:22:50 +0200383 projectName: projectName,
Alexandre Ardhuinadc73512019-11-19 07:57:42 +0100384 projectDescription: stringArg('description'),
Mikkel Nygaard Ravn10f64832017-05-24 08:22:50 +0200385 flutterRoot: flutterRoot,
Alexandre Ardhuinadc73512019-11-19 07:57:42 +0100386 renderDriverTest: boolArg('with-driver-test'),
Mikkel Nygaard Ravn10f64832017-05-24 08:22:50 +0200387 withPluginHook: generatePlugin,
Alexandre Ardhuinadc73512019-11-19 07:57:42 +0100388 androidX: boolArg('androidx'),
389 androidLanguage: stringArg('android-language'),
390 iosLanguage: stringArg('ios-language'),
Jonah Williams76ebcc82019-09-03 11:53:27 -0700391 web: featureFlags.isWebEnabled,
Jonah Williams0b2bf992019-12-03 08:13:08 -0800392 macos: featureFlags.isMacOSEnabled,
Ian Hicksona7016062016-03-19 18:37:32 -0700393 );
Jakob Andersenf34f8a32017-03-30 12:39:21 +0200394
Jonah Williamsee7a37f2020-01-06 11:04:20 -0800395 final String relativeDirPath = globals.fs.path.relative(projectDirPath);
Greg Spencerf9c6f302018-10-30 16:01:14 -0700396 if (!projectDir.existsSync() || projectDir.listSync().isEmpty) {
Jonah Williamsee7a37f2020-01-06 11:04:20 -0800397 globals.printStatus('Creating project $relativeDirPath... androidx: ${boolArg('androidx')}');
Greg Spencer21a32fd2018-10-05 15:49:53 -0700398 } else {
Alexandre Ardhuinadc73512019-11-19 07:57:42 +0100399 if (sampleCode != null && !overwrite) {
Greg Spencerf9c6f302018-10-30 16:01:14 -0700400 throwToolExit('Will not overwrite existing project in $relativeDirPath: '
401 'must specify --overwrite for samples to overwrite.');
402 }
Jonah Williamsee7a37f2020-01-06 11:04:20 -0800403 globals.printStatus('Recreating project $relativeDirPath...');
Greg Spencer21a32fd2018-10-05 15:49:53 -0700404 }
Greg Spencerf9c6f302018-10-30 16:01:14 -0700405
Jonah Williamsee7a37f2020-01-06 11:04:20 -0800406 final Directory relativeDir = globals.fs.directory(projectDirPath);
Mikkel Nygaard Ravn0d1574c2018-05-30 11:25:21 +0200407 int generatedFileCount = 0;
Mikkel Nygaard Ravn0d1574c2018-05-30 11:25:21 +0200408 switch (template) {
Greg Spencer21a32fd2018-10-05 15:49:53 -0700409 case _ProjectType.app:
Alexandre Ardhuinadc73512019-11-19 07:57:42 +0100410 generatedFileCount += await _generateApp(relativeDir, templateContext, overwrite: overwrite);
Mikkel Nygaard Ravn0d1574c2018-05-30 11:25:21 +0200411 break;
Greg Spencer21a32fd2018-10-05 15:49:53 -0700412 case _ProjectType.module:
Alexandre Ardhuinadc73512019-11-19 07:57:42 +0100413 generatedFileCount += await _generateModule(relativeDir, templateContext, overwrite: overwrite);
Mikkel Nygaard Ravnd89a6b52018-06-22 18:19:37 +0200414 break;
Greg Spencer21a32fd2018-10-05 15:49:53 -0700415 case _ProjectType.package:
Alexandre Ardhuinadc73512019-11-19 07:57:42 +0100416 generatedFileCount += await _generatePackage(relativeDir, templateContext, overwrite: overwrite);
Mikkel Nygaard Ravn0d1574c2018-05-30 11:25:21 +0200417 break;
Greg Spencer21a32fd2018-10-05 15:49:53 -0700418 case _ProjectType.plugin:
Alexandre Ardhuinadc73512019-11-19 07:57:42 +0100419 generatedFileCount += await _generatePlugin(relativeDir, templateContext, overwrite: overwrite);
Mikkel Nygaard Ravn0d1574c2018-05-30 11:25:21 +0200420 break;
Jakob Andersenf34f8a32017-03-30 12:39:21 +0200421 }
Greg Spencerf9c6f302018-10-30 16:01:14 -0700422 if (sampleCode != null) {
Zachary Anderson398ac1f2019-08-20 13:15:08 -0700423 generatedFileCount += _applySample(relativeDir, sampleCode);
Greg Spencerf9c6f302018-10-30 16:01:14 -0700424 }
Jonah Williamsee7a37f2020-01-06 11:04:20 -0800425 globals.printStatus('Wrote $generatedFileCount files.');
426 globals.printStatus('\nAll done!');
Greg Spencerf9c6f302018-10-30 16:01:14 -0700427 final String application = sampleCode != null ? 'sample application' : 'application';
Mikkel Nygaard Ravn0d1574c2018-05-30 11:25:21 +0200428 if (generatePackage) {
Jonah Williamsee7a37f2020-01-06 11:04:20 -0800429 final String relativeMainPath = globals.fs.path.normalize(globals.fs.path.join(
Greg Spencer21a32fd2018-10-05 15:49:53 -0700430 relativeDirPath,
431 'lib',
432 '${templateContext['projectName']}.dart',
433 ));
Jonah Williamsee7a37f2020-01-06 11:04:20 -0800434 globals.printStatus('Your package code is in $relativeMainPath');
Greg Spencer0ff9e8a2018-10-10 11:01:40 -0700435 } else if (generateModule) {
Jonah Williamsee7a37f2020-01-06 11:04:20 -0800436 final String relativeMainPath = globals.fs.path.normalize(globals.fs.path.join(
Greg Spencer21a32fd2018-10-05 15:49:53 -0700437 relativeDirPath,
438 'lib',
439 'main.dart',
440 ));
Jonah Williamsee7a37f2020-01-06 11:04:20 -0800441 globals.printStatus('Your module code is in $relativeMainPath.');
Mikkel Nygaard Ravn0d1574c2018-05-30 11:25:21 +0200442 } else {
443 // Run doctor; tell the user the next steps.
Jonah Williams4ff46712019-04-29 08:21:32 -0700444 final FlutterProject project = FlutterProject.fromPath(projectDirPath);
Sigurd Meldgaard2d3a5c72018-07-20 08:00:30 +0200445 final FlutterProject app = project.hasExampleApp ? project.example : project;
Jonah Williamsee7a37f2020-01-06 11:04:20 -0800446 final String relativeAppPath = globals.fs.path.normalize(globals.fs.path.relative(app.directory.path));
447 final String relativeAppMain = globals.fs.path.join(relativeAppPath, 'lib', 'main.dart');
448 final String relativePluginPath = globals.fs.path.normalize(globals.fs.path.relative(projectDirPath));
449 final String relativePluginMain = globals.fs.path.join(relativePluginPath, 'lib', '$projectName.dart');
Mikkel Nygaard Ravn0d1574c2018-05-30 11:25:21 +0200450 if (doctor.canLaunchAnything) {
451 // Let them know a summary of the state of their tooling.
452 await doctor.summary();
Jakob Andersenf34f8a32017-03-30 12:39:21 +0200453
Jonah Williamsee7a37f2020-01-06 11:04:20 -0800454 globals.printStatus('''
Greg Spencerf9c6f302018-10-30 16:01:14 -0700455In order to run your $application, type:
Mikkel Nygaard Ravn0d1574c2018-05-30 11:25:21 +0200456
457 \$ cd $relativeAppPath
458 \$ flutter run
459
Greg Spencerf9c6f302018-10-30 16:01:14 -0700460Your $application code is in $relativeAppMain.
Mikkel Nygaard Ravn0d1574c2018-05-30 11:25:21 +0200461''');
462 if (generatePlugin) {
Jonah Williamsee7a37f2020-01-06 11:04:20 -0800463 globals.printStatus('''
Greg Spencer9f238662018-10-04 13:03:20 -0700464Your plugin code is in $relativePluginMain.
Mikkel Nygaard Ravn0d1574c2018-05-30 11:25:21 +0200465
Greg Spencer9f238662018-10-04 13:03:20 -0700466Host platform code is in the "android" and "ios" directories under $relativePluginPath.
Tim Sneath52918972019-04-05 11:39:30 -0700467To edit platform code in an IDE see https://flutter.dev/developing-packages/#edit-plugin-package.
Mikkel Nygaard Ravn0d1574c2018-05-30 11:25:21 +0200468''');
469 }
470 } else {
Jonah Williamsee7a37f2020-01-06 11:04:20 -0800471 globals.printStatus("You'll need to install additional components before you can run "
Mikkel Nygaard Ravn0d1574c2018-05-30 11:25:21 +0200472 'your Flutter app:');
Jonah Williamsee7a37f2020-01-06 11:04:20 -0800473 globals.printStatus('');
Mikkel Nygaard Ravn0d1574c2018-05-30 11:25:21 +0200474
475 // Give the user more detailed analysis.
476 await doctor.diagnose();
Jonah Williamsee7a37f2020-01-06 11:04:20 -0800477 globals.printStatus('');
478 globals.printStatus("After installing components, run 'flutter doctor' in order to "
Mikkel Nygaard Ravn0d1574c2018-05-30 11:25:21 +0200479 're-validate your setup.');
Jonah Williamsee7a37f2020-01-06 11:04:20 -0800480 globals.printStatus("When complete, type 'flutter run' from the '$relativeAppPath' "
Mikkel Nygaard Ravn0d1574c2018-05-30 11:25:21 +0200481 'directory in order to launch your app.');
Jonah Williamsee7a37f2020-01-06 11:04:20 -0800482 globals.printStatus('Your $application code is in $relativeAppMain');
Mikkel Nygaard Ravn0d1574c2018-05-30 11:25:21 +0200483 }
484 }
Alexandre Ardhuin2d3ff102018-10-05 07:54:56 +0200485
486 return null;
Mikkel Nygaard Ravn0d1574c2018-05-30 11:25:21 +0200487 }
488
Alexandre Ardhuin5169ab52019-02-21 09:27:07 +0100489 Future<int> _generateModule(Directory directory, Map<String, dynamic> templateContext, { bool overwrite = false }) async {
Mikkel Nygaard Ravnd89a6b52018-06-22 18:19:37 +0200490 int generatedCount = 0;
491 final String description = argResults.wasParsed('description')
Alexandre Ardhuinadc73512019-11-19 07:57:42 +0100492 ? stringArg('description')
Greg Spencer0ff9e8a2018-10-10 11:01:40 -0700493 : 'A new flutter module project.';
Mikkel Nygaard Ravnd89a6b52018-06-22 18:19:37 +0200494 templateContext['description'] = description;
Jonah Williamsee7a37f2020-01-06 11:04:20 -0800495 generatedCount += _renderTemplate(globals.fs.path.join('module', 'common'), directory, templateContext, overwrite: overwrite);
Alexandre Ardhuinadc73512019-11-19 07:57:42 +0100496 if (boolArg('pub')) {
Jonah Williamsfde26752019-10-08 14:53:28 -0700497 await pub.get(
Mikkel Nygaard Ravnd89a6b52018-06-22 18:19:37 +0200498 context: PubContext.create,
Mikkel Nygaard Ravnb2800742018-08-02 14:12:25 +0200499 directory: directory.path,
Alexandre Ardhuinadc73512019-11-19 07:57:42 +0100500 offline: boolArg('offline'),
Mikkel Nygaard Ravnd89a6b52018-06-22 18:19:37 +0200501 );
Jonah Williams4ff46712019-04-29 08:21:32 -0700502 final FlutterProject project = FlutterProject.fromDirectory(directory);
Jonah Williamsa476a082019-04-22 15:18:15 -0700503 await project.ensureReadyForPlatformSpecificTooling(checkProjects: false);
Mikkel Nygaard Ravnd89a6b52018-06-22 18:19:37 +0200504 }
505 return generatedCount;
506 }
507
Alexandre Ardhuin5169ab52019-02-21 09:27:07 +0100508 Future<int> _generatePackage(Directory directory, Map<String, dynamic> templateContext, { bool overwrite = false }) async {
Mikkel Nygaard Ravn0d1574c2018-05-30 11:25:21 +0200509 int generatedCount = 0;
510 final String description = argResults.wasParsed('description')
Alexandre Ardhuinadc73512019-11-19 07:57:42 +0100511 ? stringArg('description')
Greg Spencer0ff9e8a2018-10-10 11:01:40 -0700512 : 'A new Flutter package project.';
Mikkel Nygaard Ravn0d1574c2018-05-30 11:25:21 +0200513 templateContext['description'] = description;
Greg Spencerf9c6f302018-10-30 16:01:14 -0700514 generatedCount += _renderTemplate('package', directory, templateContext, overwrite: overwrite);
Alexandre Ardhuinadc73512019-11-19 07:57:42 +0100515 if (boolArg('pub')) {
Jonah Williamsfde26752019-10-08 14:53:28 -0700516 await pub.get(
Mikkel Nygaard Ravn0d1574c2018-05-30 11:25:21 +0200517 context: PubContext.createPackage,
Mikkel Nygaard Ravnb2800742018-08-02 14:12:25 +0200518 directory: directory.path,
Alexandre Ardhuinadc73512019-11-19 07:57:42 +0100519 offline: boolArg('offline'),
Mikkel Nygaard Ravn0d1574c2018-05-30 11:25:21 +0200520 );
521 }
522 return generatedCount;
523 }
524
Alexandre Ardhuin5169ab52019-02-21 09:27:07 +0100525 Future<int> _generatePlugin(Directory directory, Map<String, dynamic> templateContext, { bool overwrite = false }) async {
Mikkel Nygaard Ravn0d1574c2018-05-30 11:25:21 +0200526 int generatedCount = 0;
527 final String description = argResults.wasParsed('description')
Alexandre Ardhuinadc73512019-11-19 07:57:42 +0100528 ? stringArg('description')
Mikkel Nygaard Ravn0d1574c2018-05-30 11:25:21 +0200529 : 'A new flutter plugin project.';
530 templateContext['description'] = description;
Greg Spencerf9c6f302018-10-30 16:01:14 -0700531 generatedCount += _renderTemplate('plugin', directory, templateContext, overwrite: overwrite);
Alexandre Ardhuinadc73512019-11-19 07:57:42 +0100532 if (boolArg('pub')) {
Jonah Williamsfde26752019-10-08 14:53:28 -0700533 await pub.get(
Mikkel Nygaard Ravn0d1574c2018-05-30 11:25:21 +0200534 context: PubContext.createPlugin,
Mikkel Nygaard Ravnb2800742018-08-02 14:12:25 +0200535 directory: directory.path,
Alexandre Ardhuinadc73512019-11-19 07:57:42 +0100536 offline: boolArg('offline'),
Mikkel Nygaard Ravn0d1574c2018-05-30 11:25:21 +0200537 );
538 }
Jonah Williams4ff46712019-04-29 08:21:32 -0700539 final FlutterProject project = FlutterProject.fromDirectory(directory);
Mikkel Nygaard Ravnd4e5e1e2018-08-16 13:21:55 +0200540 gradle.updateLocalProperties(project: project, requireAndroidSdk: false);
Mikkel Nygaard Ravn0d1574c2018-05-30 11:25:21 +0200541
Alexandre Ardhuinadc73512019-11-19 07:57:42 +0100542 final String projectName = templateContext['projectName'] as String;
543 final String organization = templateContext['organization'] as String;
544 final String androidPluginIdentifier = templateContext['androidIdentifier'] as String;
Mikkel Nygaard Ravn0d1574c2018-05-30 11:25:21 +0200545 final String exampleProjectName = projectName + '_example';
546 templateContext['projectName'] = exampleProjectName;
547 templateContext['androidIdentifier'] = _createAndroidIdentifier(organization, exampleProjectName);
548 templateContext['iosIdentifier'] = _createUTIIdentifier(organization, exampleProjectName);
549 templateContext['description'] = 'Demonstrates how to use the $projectName plugin.';
550 templateContext['pluginProjectName'] = projectName;
551 templateContext['androidPluginIdentifier'] = androidPluginIdentifier;
552
Greg Spencerf9c6f302018-10-30 16:01:14 -0700553 generatedCount += await _generateApp(project.example.directory, templateContext, overwrite: overwrite);
Mikkel Nygaard Ravn0d1574c2018-05-30 11:25:21 +0200554 return generatedCount;
555 }
556
Alexandre Ardhuin5169ab52019-02-21 09:27:07 +0100557 Future<int> _generateApp(Directory directory, Map<String, dynamic> templateContext, { bool overwrite = false }) async {
Mikkel Nygaard Ravn0d1574c2018-05-30 11:25:21 +0200558 int generatedCount = 0;
Greg Spencerf9c6f302018-10-30 16:01:14 -0700559 generatedCount += _renderTemplate('app', directory, templateContext, overwrite: overwrite);
Jonah Williams4ff46712019-04-29 08:21:32 -0700560 final FlutterProject project = FlutterProject.fromDirectory(directory);
Sigurd Meldgaard2d3a5c72018-07-20 08:00:30 +0200561 generatedCount += _injectGradleWrapper(project);
Mikkel Nygaard Ravn0d1574c2018-05-30 11:25:21 +0200562
Alexandre Ardhuinadc73512019-11-19 07:57:42 +0100563 if (boolArg('with-driver-test')) {
Mikkel Nygaard Ravnb2800742018-08-02 14:12:25 +0200564 final Directory testDirectory = directory.childDirectory('test_driver');
Greg Spencerf9c6f302018-10-30 16:01:14 -0700565 generatedCount += _renderTemplate('driver', testDirectory, templateContext, overwrite: overwrite);
Jakob Andersenf34f8a32017-03-30 12:39:21 +0200566 }
567
Alexandre Ardhuinadc73512019-11-19 07:57:42 +0100568 if (boolArg('pub')) {
569 await pub.get(context: PubContext.create, directory: directory.path, offline: boolArg('offline'));
Jonah Williamsa476a082019-04-22 15:18:15 -0700570 await project.ensureReadyForPlatformSpecificTooling(checkProjects: false);
Jakob Andersen7ffa82a2017-04-10 15:44:19 +0200571 }
Devon Carewe6b45c52015-09-11 16:12:27 -0700572
Mikkel Nygaard Ravnd4e5e1e2018-08-16 13:21:55 +0200573 gradle.updateLocalProperties(project: project, requireAndroidSdk: false);
Jakob Andersen6b541372017-05-05 14:53:51 +0200574
Mikkel Nygaard Ravn0d1574c2018-05-30 11:25:21 +0200575 return generatedCount;
Devon Carew2efd1312015-08-07 14:20:14 -0700576 }
Devon Carew2efd1312015-08-07 14:20:14 -0700577
Greg Spencerf9c6f302018-10-30 16:01:14 -0700578 // Takes an application template and replaces the main.dart with one from the
579 // documentation website in sampleCode. Returns the difference in the number
580 // of files after applying the sample, since it also deletes the application's
581 // test directory (since the template's test doesn't apply to the sample).
Zachary Anderson398ac1f2019-08-20 13:15:08 -0700582 int _applySample(Directory directory, String sampleCode) {
Greg Spencerf9c6f302018-10-30 16:01:14 -0700583 final File mainDartFile = directory.childDirectory('lib').childFile('main.dart');
Zachary Anderson398ac1f2019-08-20 13:15:08 -0700584 mainDartFile.createSync(recursive: true);
585 mainDartFile.writeAsStringSync(sampleCode);
Greg Spencerf9c6f302018-10-30 16:01:14 -0700586 final Directory testDir = directory.childDirectory('test');
587 final List<FileSystemEntity> files = testDir.listSync(recursive: true);
Zachary Anderson398ac1f2019-08-20 13:15:08 -0700588 testDir.deleteSync(recursive: true);
Greg Spencerf9c6f302018-10-30 16:01:14 -0700589 return -files.length;
590 }
591
Mikkel Nygaard Ravn10f64832017-05-24 08:22:50 +0200592 Map<String, dynamic> _templateContext({
Mikkel Nygaard Ravn251d83a2017-05-24 16:19:16 +0200593 String organization,
Mikkel Nygaard Ravn10f64832017-05-24 08:22:50 +0200594 String projectName,
595 String projectDescription,
596 String androidLanguage,
Josh Burtond0e45a22019-06-01 13:33:02 +1200597 bool androidX,
Mikkel Nygaard Ravn10f64832017-05-24 08:22:50 +0200598 String iosLanguage,
Mikkel Nygaard Ravn10f64832017-05-24 08:22:50 +0200599 String flutterRoot,
Alexandre Ardhuin09276be2018-06-05 08:50:40 +0200600 bool renderDriverTest = false,
601 bool withPluginHook = false,
Jonah Williamsbd413bf2019-06-07 13:15:38 -0700602 bool web = false,
stuartmorgan04129772019-09-19 17:06:18 -0700603 bool macos = false,
Mikkel Nygaard Ravn10f64832017-05-24 08:22:50 +0200604 }) {
Jonah Williamsee7a37f2020-01-06 11:04:20 -0800605 flutterRoot = globals.fs.path.normalize(flutterRoot);
Chinmay Garde038367a2016-02-23 15:40:51 -0800606
Jakob Andersenf34f8a32017-03-30 12:39:21 +0200607 final String pluginDartClass = _createPluginClassName(projectName);
608 final String pluginClass = pluginDartClass.endsWith('Plugin')
609 ? pluginDartClass
610 : pluginDartClass + 'Plugin';
stuartmorgan04129772019-09-19 17:06:18 -0700611 final String appleIdentifier = _createUTIIdentifier(organization, projectName);
Devon Carew2efd1312015-08-07 14:20:14 -0700612
Jakob Andersenf34f8a32017-03-30 12:39:21 +0200613 return <String, dynamic>{
Mikkel Nygaard Ravn251d83a2017-05-24 16:19:16 +0200614 'organization': organization,
Chinmay Garde038367a2016-02-23 15:40:51 -0800615 'projectName': projectName,
Mikkel Nygaard Ravn251d83a2017-05-24 16:19:16 +0200616 'androidIdentifier': _createAndroidIdentifier(organization, projectName),
stuartmorgan04129772019-09-19 17:06:18 -0700617 'iosIdentifier': appleIdentifier,
618 'macosIdentifier': appleIdentifier,
Ian Hicksona7016062016-03-19 18:37:32 -0700619 'description': projectDescription,
Brian Slesinsky5cb90972017-04-14 10:12:48 -0700620 'dartSdk': '$flutterRoot/bin/cache/dart-sdk',
Josh Burtond0e45a22019-06-01 13:33:02 +1200621 'androidX': androidX,
Emmanuel Garcia08c645b2019-10-16 21:26:10 -0700622 'useAndroidEmbeddingV2': featureFlags.isAndroidEmbeddingV2Enabled,
Jakob Andersenf34f8a32017-03-30 12:39:21 +0200623 'androidMinApiLevel': android.minApiLevel,
Brian Slesinsky5cb90972017-04-14 10:12:48 -0700624 'androidSdkVersion': android_sdk.minimumAndroidSdkVersion,
Alexandre Ardhuin1fce14a2017-10-22 18:11:36 +0200625 'androidFlutterJar': '$flutterRoot/bin/cache/artifacts/engine/android-arm/flutter.jar',
Jakob Andersenf34f8a32017-03-30 12:39:21 +0200626 'withDriverTest': renderDriverTest,
627 'pluginClass': pluginClass,
628 'pluginDartClass': pluginDartClass,
629 'withPluginHook': withPluginHook,
Mikkel Nygaard Ravn10f64832017-05-24 08:22:50 +0200630 'androidLanguage': androidLanguage,
631 'iosLanguage': iosLanguage,
xsterc17099f2017-11-03 10:07:57 -0700632 'flutterRevision': FlutterVersion.instance.frameworkRevision,
633 'flutterChannel': FlutterVersion.instance.channel,
Jonah Williams76ebcc82019-09-03 11:53:27 -0700634 'web': web,
stuartmorgan04129772019-09-19 17:06:18 -0700635 'macos': macos,
636 'year': DateTime.now().year,
637 // For now, the new plugin schema is only used when a desktop plugin is
638 // enabled. Once the new schema is supported on stable, this should be
639 // removed, and the new schema should always be used.
640 'useNewPluginSchema': macos,
641 // If a desktop platform is included, add a workaround for #31366.
Greg Spencer245d1b52019-11-26 18:32:34 -0800642 // When Linux and Windows are added, we will need this workaround again.
643 'includeTargetPlatformWorkaround': false,
Chinmay Garde038367a2016-02-23 15:40:51 -0800644 };
Jakob Andersenf34f8a32017-03-30 12:39:21 +0200645 }
Chinmay Garde038367a2016-02-23 15:40:51 -0800646
Alexandre Ardhuin5169ab52019-02-21 09:27:07 +0100647 int _renderTemplate(String templateName, Directory directory, Map<String, dynamic> context, { bool overwrite = false }) {
Alexandre Ardhuind927c932018-09-12 08:29:29 +0200648 final Template template = Template.fromName(templateName);
Greg Spencerf9c6f302018-10-30 16:01:14 -0700649 return template.render(directory, context, overwriteExisting: overwrite);
Devon Carew2efd1312015-08-07 14:20:14 -0700650 }
Mikkel Nygaard Ravnc5999c72017-06-26 12:47:43 +0200651
Sigurd Meldgaard2d3a5c72018-07-20 08:00:30 +0200652 int _injectGradleWrapper(FlutterProject project) {
Mikkel Nygaard Ravnc5999c72017-06-26 12:47:43 +0200653 int filesCreated = 0;
654 copyDirectorySync(
Jonah Williamsee7a37f2020-01-06 11:04:20 -0800655 globals.cache.getArtifactDirectory('gradle_wrapper'),
Mikkel Nygaard Ravnd4e5e1e2018-08-16 13:21:55 +0200656 project.android.hostAppGradleRoot,
Emmanuel Garcia4a1c62c2019-08-28 14:52:08 -0700657 onFileCopied: (File sourceFile, File destinationFile) {
Mikkel Nygaard Ravnc5999c72017-06-26 12:47:43 +0200658 filesCreated++;
659 final String modes = sourceFile.statSync().modeString();
660 if (modes != null && modes.contains('x')) {
661 os.makeExecutable(destinationFile);
662 }
663 },
664 );
665 return filesCreated;
666 }
Devon Carew2efd1312015-08-07 14:20:14 -0700667}
668
Mikkel Nygaard Ravn251d83a2017-05-24 16:19:16 +0200669String _createAndroidIdentifier(String organization, String name) {
KyleWong199ebaa2019-02-13 11:40:53 +0800670 // Android application ID is specified in: https://developer.android.com/studio/build/application-id
671 // All characters must be alphanumeric or an underscore [a-zA-Z0-9_].
672 String tmpIdentifier = '$organization.$name';
673 final RegExp disallowed = RegExp(r'[^\w\.]');
674 tmpIdentifier = tmpIdentifier.replaceAll(disallowed, '');
675
676 // It must have at least two segments (one or more dots).
677 final List<String> segments = tmpIdentifier
678 .split('.')
679 .where((String segment) => segment.isNotEmpty)
680 .toList();
681 while (segments.length < 2) {
682 segments.add('untitled');
683 }
684
685 // Each segment must start with a letter.
686 final RegExp segmentPatternRegex = RegExp(r'^[a-zA-Z][\w]*$');
687 final List<String> prefixedSegments = segments
688 .map((String segment) {
689 if (!segmentPatternRegex.hasMatch(segment)) {
690 return 'u'+segment;
691 }
692 return segment;
693 })
694 .toList();
695 return prefixedSegments.join('.');
Jakob Andersenf34f8a32017-03-30 12:39:21 +0200696}
697
698String _createPluginClassName(String name) {
699 final String camelizedName = camelCase(name);
700 return camelizedName[0].toUpperCase() + camelizedName.substring(1);
Devon Carew4c569192016-03-02 11:22:19 -0800701}
702
Mikkel Nygaard Ravn251d83a2017-05-24 16:19:16 +0200703String _createUTIIdentifier(String organization, String name) {
Chinmay Gardef6a24772016-02-19 12:30:18 -0800704 // Create a UTI (https://en.wikipedia.org/wiki/Uniform_Type_Identifier) from a base name
KyleWong199ebaa2019-02-13 11:40:53 +0800705 name = camelCase(name);
706 String tmpIdentifier = '$organization.$name';
Alexandre Ardhuind927c932018-09-12 08:29:29 +0200707 final RegExp disallowed = RegExp(r'[^a-zA-Z0-9\-\.\u0080-\uffff]+');
KyleWong199ebaa2019-02-13 11:40:53 +0800708 tmpIdentifier = tmpIdentifier.replaceAll(disallowed, '');
709
710 // It must have at least two segments (one or more dots).
711 final List<String> segments = tmpIdentifier
712 .split('.')
713 .where((String segment) => segment.isNotEmpty)
714 .toList();
715 while (segments.length < 2) {
716 segments.add('untitled');
717 }
718
719 return segments.join('.');
Chinmay Gardef6a24772016-02-19 12:30:18 -0800720}
Devon Carewebf1ecc2016-03-01 10:01:37 -0800721
Phil Quitslund802eca22019-03-06 11:05:16 -0800722const Set<String> _packageDependencies = <String>{
Jason Simmons96ce9d62018-03-09 13:11:33 -0800723 'analyzer',
Devon Carewebf1ecc2016-03-01 10:01:37 -0800724 'args',
725 'async',
726 'collection',
727 'convert',
Jason Simmons96ce9d62018-03-09 13:11:33 -0800728 'crypto',
Devon Carewebf1ecc2016-03-01 10:01:37 -0800729 'flutter',
Jason Simmons96ce9d62018-03-09 13:11:33 -0800730 'flutter_test',
731 'front_end',
Devon Carewebf1ecc2016-03-01 10:01:37 -0800732 'html',
Jason Simmons96ce9d62018-03-09 13:11:33 -0800733 'http',
Devon Carewebf1ecc2016-03-01 10:01:37 -0800734 'intl',
Jason Simmons96ce9d62018-03-09 13:11:33 -0800735 'io',
736 'isolate',
737 'kernel',
Devon Carewebf1ecc2016-03-01 10:01:37 -0800738 'logging',
739 'matcher',
Jason Simmons96ce9d62018-03-09 13:11:33 -0800740 'meta',
Devon Carewebf1ecc2016-03-01 10:01:37 -0800741 'mime',
742 'path',
743 'plugin',
744 'pool',
745 'test',
746 'utf',
747 'watcher',
Alexandre Ardhuin387f8852019-03-01 08:17:55 +0100748 'yaml',
Phil Quitslund802eca22019-03-06 11:05:16 -0800749};
Devon Carewebf1ecc2016-03-01 10:01:37 -0800750
Jonah Williams0a2d8e02019-12-16 18:37:20 -0800751// A valid Dart identifier.
752// https://dart.dev/guides/language/language-tour#important-concepts
753final RegExp _identifierRegExp = RegExp('[a-zA-Z_][a-zA-Z0-9_]*');
754
755// non-contextual dart keywords.
756//' https://dart.dev/guides/language/language-tour#keywords
757const Set<String> _keywords = <String>{
758 'abstract',
759 'as',
760 'assert',
761 'async',
762 'await',
763 'break',
764 'case',
765 'catch',
766 'class',
767 'const',
768 'continue',
769 'covariant',
770 'default',
771 'deferred',
772 'do',
773 'dynamic',
774 'else',
775 'enum',
776 'export',
777 'extends',
778 'extension',
779 'external',
780 'factory',
781 'false',
782 'final',
783 'finally',
784 'for',
785 'function',
786 'get',
787 'hide',
788 'if',
789 'implements',
790 'import',
791 'in',
792 'inout',
793 'interface',
794 'is',
795 'late',
796 'library',
797 'mixin',
798 'native',
799 'new',
800 'null',
801 'of',
802 'on',
803 'operator',
804 'out',
805 'part',
806 'patch',
807 'required',
808 'rethrow',
809 'return',
810 'set',
811 'show',
812 'source',
813 'static',
814 'super',
815 'switch',
816 'sync',
817 'this',
818 'throw',
819 'true',
820 'try',
821 'typedef',
822 'var',
823 'void',
824 'while',
825 'with',
826 'yield',
827};
828
829/// Whether [name] is a valid Pub package.
830@visibleForTesting
831bool isValidPackageName(String name) {
832 final Match match = _identifierRegExp.matchAsPrefix(name);
833 return match != null && match.end == name.length && !_keywords.contains(name);
834}
835
Ian Hickson0f1a7032017-06-08 17:13:03 -0700836/// Return null if the project name is legal. Return a validation message if
Devon Carewebf1ecc2016-03-01 10:01:37 -0800837/// we should disallow the project name.
838String _validateProjectName(String projectName) {
Jonah Williams0a2d8e02019-12-16 18:37:20 -0800839 if (!isValidPackageName(projectName)) {
840 return '"$projectName" is not a valid Dart package name.\n\n'
841 'See https://dart.dev/tools/pub/pubspec#name for more information.';
Phil Quitslunda4bc4702017-12-01 10:54:24 -0800842 }
Devon Carewebf1ecc2016-03-01 10:01:37 -0800843 if (_packageDependencies.contains(projectName)) {
844 return "Invalid project name: '$projectName' - this will conflict with Flutter "
Alexandre Ardhuin1fce14a2017-10-22 18:11:36 +0200845 'package dependencies.';
Devon Carewebf1ecc2016-03-01 10:01:37 -0800846 }
stevemessick8847b862016-04-08 20:37:50 -0700847 return null;
848}
Devon Carewebf1ecc2016-03-01 10:01:37 -0800849
Ian Hickson0f1a7032017-06-08 17:13:03 -0700850/// Return null if the project directory is legal. Return a validation message
stevemessick8847b862016-04-08 20:37:50 -0700851/// if we should disallow the directory name.
Greg Spencerf9c6f302018-10-30 16:01:14 -0700852String _validateProjectDir(String dirPath, { String flutterRoot, bool overwrite = false }) {
Jonah Williamsee7a37f2020-01-06 11:04:20 -0800853 if (globals.fs.path.isWithin(flutterRoot, dirPath)) {
Greg Spencer081d2a72018-10-10 18:17:56 -0700854 return 'Cannot create a project within the Flutter SDK. '
Adam Barth030f6232016-10-28 20:29:19 -0700855 "Target directory '$dirPath' is within the Flutter SDK at '$flutterRoot'.";
856 }
857
Greg Spencerf9c6f302018-10-30 16:01:14 -0700858 // If the destination directory is actually a file, then we refuse to
859 // overwrite, on the theory that the user probably didn't expect it to exist.
Jonah Williamsee7a37f2020-01-06 11:04:20 -0800860 if (globals.fs.isFileSync(dirPath)) {
Greg Spencerf9c6f302018-10-30 16:01:14 -0700861 return "Invalid project name: '$dirPath' - refers to an existing file."
862 '${overwrite ? ' Refusing to overwrite a file with a directory.' : ''}';
863 }
864
Zachary Andersone2340c62019-09-13 14:51:35 -0700865 if (overwrite) {
Greg Spencerf9c6f302018-10-30 16:01:14 -0700866 return null;
Zachary Andersone2340c62019-09-13 14:51:35 -0700867 }
Greg Spencerf9c6f302018-10-30 16:01:14 -0700868
Jonah Williamsee7a37f2020-01-06 11:04:20 -0800869 final FileSystemEntityType type = globals.fs.typeSync(dirPath);
Devon Carew80fabfd2016-04-19 19:30:49 -0700870
Leaf Petersen32f94442018-07-20 15:07:24 -0700871 if (type != FileSystemEntityType.notFound) {
Ian Hicksonefb45ea2017-09-27 16:13:48 -0700872 switch (type) {
Leaf Petersen32f94442018-07-20 15:07:24 -0700873 case FileSystemEntityType.file:
stevemessick8847b862016-04-08 20:37:50 -0700874 // Do not overwrite files.
Adam Barth030f6232016-10-28 20:29:19 -0700875 return "Invalid project name: '$dirPath' - file exists.";
Leaf Petersen32f94442018-07-20 15:07:24 -0700876 case FileSystemEntityType.link:
stevemessick8847b862016-04-08 20:37:50 -0700877 // Do not overwrite links.
Adam Barth030f6232016-10-28 20:29:19 -0700878 return "Invalid project name: '$dirPath' - refers to a link.";
stevemessick8847b862016-04-08 20:37:50 -0700879 }
880 }
Devon Carew80fabfd2016-04-19 19:30:49 -0700881
Devon Carewebf1ecc2016-03-01 10:01:37 -0800882 return null;
883}