• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright 2015 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import 'dart:async';
6
7import 'package:linter/src/rules/pub/package_names.dart' as package_names; // ignore: implementation_imports
8import 'package:linter/src/utils.dart' as linter_utils; // ignore: implementation_imports
9import 'package:yaml/yaml.dart' as yaml;
10
11import '../android/android.dart' as android;
12import '../android/android_sdk.dart' as android_sdk;
13import '../android/gradle.dart' as gradle;
14import '../base/common.dart';
15import '../base/file_system.dart';
16import '../base/net.dart';
17import '../base/os.dart';
18import '../base/utils.dart';
19import '../cache.dart';
20import '../convert.dart';
21import '../dart/pub.dart';
22import '../doctor.dart';
23import '../features.dart';
24import '../globals.dart';
25import '../project.dart';
26import '../reporting/reporting.dart';
27import '../runner/flutter_command.dart';
28import '../template.dart';
29import '../version.dart';
30
31enum _ProjectType {
32  /// This is the default project with the user-managed host code.
33  /// It is different than the "module" template in that it exposes and doesn't
34  /// manage the platform code.
35  app,
36  /// The is a project that has managed platform host code. It is an application with
37  /// ephemeral .ios and .android directories that can be updated automatically.
38  module,
39  /// This is a Flutter Dart package project. It doesn't have any native
40  /// components, only Dart.
41  package,
42  /// This is a native plugin project.
43  plugin,
44}
45
46_ProjectType _stringToProjectType(String value) {
47  _ProjectType result;
48  for (_ProjectType type in _ProjectType.values) {
49    if (value == getEnumName(type)) {
50      result = type;
51      break;
52    }
53  }
54  return result;
55}
56
57class CreateCommand extends FlutterCommand {
58  CreateCommand() {
59    argParser.addFlag('pub',
60      defaultsTo: true,
61      help: 'Whether to run "flutter pub get" after the project has been created.',
62    );
63    argParser.addFlag('offline',
64      defaultsTo: false,
65      help: 'When "flutter pub get" is run by the create command, this indicates '
66        'whether to run it in offline mode or not. In offline mode, it will need to '
67        'have all dependencies already available in the pub cache to succeed.',
68    );
69    argParser.addFlag(
70      'with-driver-test',
71      negatable: true,
72      defaultsTo: false,
73      help: "Also add a flutter_driver dependency and generate a sample 'flutter drive' test.",
74    );
75    argParser.addOption(
76      'template',
77      abbr: 't',
78      allowed: _ProjectType.values.map<String>((_ProjectType type) => getEnumName(type)),
79      help: 'Specify the type of project to create.',
80      valueHelp: 'type',
81      allowedHelp: <String, String>{
82        getEnumName(_ProjectType.app): '(default) Generate a Flutter application.',
83        getEnumName(_ProjectType.package): 'Generate a shareable Flutter project containing modular '
84            'Dart code.',
85        getEnumName(_ProjectType.plugin): 'Generate a shareable Flutter project containing an API '
86            'in Dart code with a platform-specific implementation for Android, for iOS code, or '
87            'for both.',
88      },
89      defaultsTo: null,
90    );
91    argParser.addOption(
92      'sample',
93      abbr: 's',
94      help: 'Specifies the Flutter code sample to use as the main.dart for an application. Implies '
95        '--template=app. The value should be the sample ID of the desired sample from the API '
96        'documentation website (http://docs.flutter.dev). An example can be found at '
97        'https://master-api.flutter.dev/flutter/widgets/SingleChildScrollView-class.html',
98      defaultsTo: null,
99      valueHelp: 'id',
100    );
101    argParser.addOption(
102      'list-samples',
103      help: 'Specifies a JSON output file for a listing of Flutter code samples '
104        'that can created with --sample.',
105      valueHelp: 'path',
106    );
107    argParser.addFlag(
108      'overwrite',
109      negatable: true,
110      defaultsTo: false,
111      help: 'When performing operations, overwrite existing files.',
112    );
113    argParser.addOption(
114      'description',
115      defaultsTo: 'A new Flutter project.',
116      help: 'The description to use for your new Flutter project. This string ends up in the pubspec.yaml file.',
117    );
118    argParser.addOption(
119      'org',
120      defaultsTo: 'com.example',
121      help: 'The organization responsible for your new Flutter project, in reverse domain name notation. '
122            'This string is used in Java package names and as prefix in the iOS bundle identifier.',
123    );
124    argParser.addOption(
125      'project-name',
126      defaultsTo: null,
127      help: 'The project name for this new Flutter project. This must be a valid dart package name.',
128    );
129    argParser.addOption(
130      'ios-language',
131      abbr: 'i',
132      defaultsTo: 'swift',
133      allowed: <String>['objc', 'swift'],
134    );
135    argParser.addOption(
136      'android-language',
137      abbr: 'a',
138      defaultsTo: 'kotlin',
139      allowed: <String>['java', 'kotlin'],
140    );
141    argParser.addFlag(
142      'androidx',
143      negatable: true,
144      defaultsTo: false,
145      help: 'Generate a project using the AndroidX support libraries',
146    );
147    argParser.addFlag(
148      'web',
149      negatable: true,
150      defaultsTo: false,
151      hide: true,
152      help: '(Experimental) Generate the web specific tooling. Only supported '
153        'on non-stable branches',
154    );
155  }
156
157  @override
158  final String name = 'create';
159
160  @override
161  final String description = 'Create a new Flutter project.\n\n'
162    'If run on a project that already exists, this will repair the project, recreating any files that are missing.';
163
164  @override
165  String get invocation => '${runner.executableName} $name <output directory>';
166
167  @override
168  Future<Map<CustomDimensions, String>> get usageValues async {
169    return <CustomDimensions, String>{
170      CustomDimensions.commandCreateProjectType: argResults['template'],
171      CustomDimensions.commandCreateAndroidLanguage: argResults['android-language'],
172      CustomDimensions.commandCreateIosLanguage: argResults['ios-language'],
173    };
174  }
175
176  // If it has a .metadata file with the project_type in it, use that.
177  // If it has an android dir and an android/app dir, it's a legacy app
178  // If it has an ios dir and an ios/Flutter dir, it's a legacy app
179  // Otherwise, we don't presume to know what type of project it could be, since
180  // many of the files could be missing, and we can't really tell definitively.
181  _ProjectType _determineTemplateType(Directory projectDir) {
182    yaml.YamlMap loadMetadata(Directory projectDir) {
183      if (!projectDir.existsSync())
184        return null;
185      final File metadataFile = fs.file(fs.path.join(projectDir.absolute.path, '.metadata'));
186      if (!metadataFile.existsSync())
187        return null;
188      return yaml.loadYaml(metadataFile.readAsStringSync());
189    }
190
191    bool exists(List<String> path) {
192      return fs.directory(fs.path.joinAll(<String>[projectDir.absolute.path] + path)).existsSync();
193    }
194
195    // If it exists, the project type in the metadata is definitive.
196    final yaml.YamlMap metadata = loadMetadata(projectDir);
197    if (metadata != null && metadata['project_type'] != null) {
198      return _stringToProjectType(metadata['project_type']);
199    }
200
201    // There either wasn't any metadata, or it didn't contain the project type,
202    // so try and figure out what type of project it is from the existing
203    // directory structure.
204    if (exists(<String>['android', 'app'])
205        || exists(<String>['ios', 'Runner'])
206        || exists(<String>['ios', 'Flutter'])) {
207      return _ProjectType.app;
208    }
209    // Since we can't really be definitive on nearly-empty directories, err on
210    // the side of prudence and just say we don't know.
211    return null;
212  }
213
214  /// The hostname for the Flutter docs for the current channel.
215  String get _snippetsHost => FlutterVersion.instance.channel == 'stable'
216        ? 'docs.flutter.io'
217        : 'master-docs.flutter.io';
218
219  Future<String> _fetchSampleFromServer(String sampleId) async {
220    // Sanity check the sampleId
221    if (sampleId.contains(RegExp(r'[^-\w\.]'))) {
222      throwToolExit('Sample ID "$sampleId" contains invalid characters. Check the ID in the '
223        'documentation and try again.');
224    }
225
226    return utf8.decode(await fetchUrl(Uri.https(_snippetsHost, 'snippets/$sampleId.dart')));
227  }
228
229  /// Fetches the samples index file from the Flutter docs website.
230  Future<String> _fetchSamplesIndexFromServer() async {
231    return utf8.decode(
232      await fetchUrl(Uri.https(_snippetsHost, 'snippets/index.json'), maxAttempts: 2));
233  }
234
235  /// Fetches the samples index file from the server and writes it to
236  /// [outputFilePath].
237  Future<void> _writeSamplesJson(String outputFilePath) async {
238    try {
239      final File outputFile = fs.file(outputFilePath);
240      if (outputFile.existsSync()) {
241        throwToolExit('File "$outputFilePath" already exists', exitCode: 1);
242      }
243      final String samplesJson = await _fetchSamplesIndexFromServer();
244      if (samplesJson == null) {
245        throwToolExit('Unable to download samples', exitCode: 2);
246      }
247      else {
248        outputFile.writeAsStringSync(samplesJson);
249        printStatus('Wrote samples JSON to "$outputFilePath"');
250      }
251    } catch (e) {
252      throwToolExit('Failed to write samples JSON to "$outputFilePath": $e', exitCode: 2);
253    }
254  }
255
256  _ProjectType _getProjectType(Directory projectDir) {
257    _ProjectType template;
258    _ProjectType detectedProjectType;
259    final bool metadataExists = projectDir.absolute.childFile('.metadata').existsSync();
260    if (argResults['template'] != null) {
261      template = _stringToProjectType(argResults['template']);
262    } else {
263      // If the project directory exists and isn't empty, then try to determine the template
264      // type from the project directory.
265      if (projectDir.existsSync() && projectDir.listSync().isNotEmpty) {
266        detectedProjectType = _determineTemplateType(projectDir);
267        if (detectedProjectType == null && metadataExists) {
268          // We can only be definitive that this is the wrong type if the .metadata file
269          // exists and contains a type that we don't understand, or doesn't contain a type.
270          throwToolExit('Sorry, unable to detect the type of project to recreate. '
271              'Try creating a fresh project and migrating your existing code to '
272              'the new project manually.');
273        }
274      }
275    }
276    template ??= detectedProjectType ?? _ProjectType.app;
277    if (detectedProjectType != null && template != detectedProjectType && metadataExists) {
278      // We can only be definitive that this is the wrong type if the .metadata file
279      // exists and contains a type that doesn't match.
280      throwToolExit("The requested template type '${getEnumName(template)}' doesn't match the "
281          "existing template type of '${getEnumName(detectedProjectType)}'.");
282    }
283    return template;
284  }
285
286  @override
287  Future<FlutterCommandResult> runCommand() async {
288    if (argResults['list-samples'] != null) {
289      // _writeSamplesJson can potentially be long-lived.
290      Cache.releaseLockEarly();
291
292      await _writeSamplesJson(argResults['list-samples']);
293      return null;
294    }
295
296    if (argResults.rest.isEmpty)
297      throwToolExit('No option specified for the output directory.\n$usage', exitCode: 2);
298
299    if (argResults.rest.length > 1) {
300      String message = 'Multiple output directories specified.';
301      for (String arg in argResults.rest) {
302        if (arg.startsWith('-')) {
303          message += '\nTry moving $arg to be immediately following $name';
304          break;
305        }
306      }
307      throwToolExit(message, exitCode: 2);
308    }
309
310    if (Cache.flutterRoot == null)
311      throwToolExit('Neither the --flutter-root command line flag nor the FLUTTER_ROOT environment '
312        'variable was specified. Unable to find package:flutter.', exitCode: 2);
313
314    await Cache.instance.updateAll(<DevelopmentArtifact>{ DevelopmentArtifact.universal });
315
316    final String flutterRoot = fs.path.absolute(Cache.flutterRoot);
317
318    final String flutterPackagesDirectory = fs.path.join(flutterRoot, 'packages');
319    final String flutterPackagePath = fs.path.join(flutterPackagesDirectory, 'flutter');
320    if (!fs.isFileSync(fs.path.join(flutterPackagePath, 'pubspec.yaml')))
321      throwToolExit('Unable to find package:flutter in $flutterPackagePath', exitCode: 2);
322
323    final String flutterDriverPackagePath = fs.path.join(flutterRoot, 'packages', 'flutter_driver');
324    if (!fs.isFileSync(fs.path.join(flutterDriverPackagePath, 'pubspec.yaml')))
325      throwToolExit('Unable to find package:flutter_driver in $flutterDriverPackagePath', exitCode: 2);
326
327    final Directory projectDir = fs.directory(argResults.rest.first);
328    final String projectDirPath = fs.path.normalize(projectDir.absolute.path);
329
330    String sampleCode;
331    if (argResults['sample'] != null) {
332      if (argResults['template'] != null &&
333        _stringToProjectType(argResults['template'] ?? 'app') != _ProjectType.app) {
334        throwToolExit('Cannot specify --sample with a project type other than '
335          '"${getEnumName(_ProjectType.app)}"');
336      }
337      // Fetch the sample from the server.
338      sampleCode = await _fetchSampleFromServer(argResults['sample']);
339    }
340
341    final _ProjectType template = _getProjectType(projectDir);
342    final bool generateModule = template == _ProjectType.module;
343    final bool generatePlugin = template == _ProjectType.plugin;
344    final bool generatePackage = template == _ProjectType.package;
345
346    String organization = argResults['org'];
347    if (!argResults.wasParsed('org')) {
348      final FlutterProject project = FlutterProject.fromDirectory(projectDir);
349      final Set<String> existingOrganizations = project.organizationNames;
350      if (existingOrganizations.length == 1) {
351        organization = existingOrganizations.first;
352      } else if (1 < existingOrganizations.length) {
353        throwToolExit(
354          'Ambiguous organization in existing files: $existingOrganizations. '
355          'The --org command line argument must be specified to recreate project.'
356        );
357      }
358    }
359
360    String error = _validateProjectDir(projectDirPath, flutterRoot: flutterRoot, overwrite: argResults['overwrite']);
361    if (error != null)
362      throwToolExit(error);
363
364    final String projectName = argResults['project-name'] ?? fs.path.basename(projectDirPath);
365    error = _validateProjectName(projectName);
366    if (error != null)
367      throwToolExit(error);
368
369    final Map<String, dynamic> templateContext = _templateContext(
370      organization: organization,
371      projectName: projectName,
372      projectDescription: argResults['description'],
373      flutterRoot: flutterRoot,
374      renderDriverTest: argResults['with-driver-test'],
375      withPluginHook: generatePlugin,
376      androidX: argResults['androidx'],
377      androidLanguage: argResults['android-language'],
378      iosLanguage: argResults['ios-language'],
379      web: argResults['web'],
380    );
381
382    final String relativeDirPath = fs.path.relative(projectDirPath);
383    if (!projectDir.existsSync() || projectDir.listSync().isEmpty) {
384      printStatus('Creating project $relativeDirPath...');
385    } else {
386      if (sampleCode != null && !argResults['overwrite']) {
387        throwToolExit('Will not overwrite existing project in $relativeDirPath: '
388          'must specify --overwrite for samples to overwrite.');
389      }
390      printStatus('Recreating project $relativeDirPath...');
391    }
392
393    final Directory relativeDir = fs.directory(projectDirPath);
394    int generatedFileCount = 0;
395    switch (template) {
396      case _ProjectType.app:
397        generatedFileCount += await _generateApp(relativeDir, templateContext, overwrite: argResults['overwrite']);
398        break;
399      case _ProjectType.module:
400        generatedFileCount += await _generateModule(relativeDir, templateContext, overwrite: argResults['overwrite']);
401        break;
402      case _ProjectType.package:
403        generatedFileCount += await _generatePackage(relativeDir, templateContext, overwrite: argResults['overwrite']);
404        break;
405      case _ProjectType.plugin:
406        generatedFileCount += await _generatePlugin(relativeDir, templateContext, overwrite: argResults['overwrite']);
407        break;
408    }
409    if (sampleCode != null) {
410      generatedFileCount += await _applySample(relativeDir, sampleCode);
411    }
412    printStatus('Wrote $generatedFileCount files.');
413    printStatus('\nAll done!');
414    final String application = sampleCode != null ? 'sample application' : 'application';
415    if (generatePackage) {
416      final String relativeMainPath = fs.path.normalize(fs.path.join(
417        relativeDirPath,
418        'lib',
419        '${templateContext['projectName']}.dart',
420      ));
421      printStatus('Your package code is in $relativeMainPath');
422    } else if (generateModule) {
423      final String relativeMainPath = fs.path.normalize(fs.path.join(
424          relativeDirPath,
425          'lib',
426          'main.dart',
427      ));
428      printStatus('Your module code is in $relativeMainPath.');
429    } else {
430      // Run doctor; tell the user the next steps.
431      final FlutterProject project = FlutterProject.fromPath(projectDirPath);
432      final FlutterProject app = project.hasExampleApp ? project.example : project;
433      final String relativeAppPath = fs.path.normalize(fs.path.relative(app.directory.path));
434      final String relativeAppMain = fs.path.join(relativeAppPath, 'lib', 'main.dart');
435      final String relativePluginPath = fs.path.normalize(fs.path.relative(projectDirPath));
436      final String relativePluginMain = fs.path.join(relativePluginPath, 'lib', '$projectName.dart');
437      if (doctor.canLaunchAnything) {
438        // Let them know a summary of the state of their tooling.
439        await doctor.summary();
440
441        printStatus('''
442In order to run your $application, type:
443
444  \$ cd $relativeAppPath
445  \$ flutter run
446
447Your $application code is in $relativeAppMain.
448''');
449        if (generatePlugin) {
450          printStatus('''
451Your plugin code is in $relativePluginMain.
452
453Host platform code is in the "android" and "ios" directories under $relativePluginPath.
454To edit platform code in an IDE see https://flutter.dev/developing-packages/#edit-plugin-package.
455''');
456        }
457      } else {
458        printStatus("You'll need to install additional components before you can run "
459            'your Flutter app:');
460        printStatus('');
461
462        // Give the user more detailed analysis.
463        await doctor.diagnose();
464        printStatus('');
465        printStatus("After installing components, run 'flutter doctor' in order to "
466            're-validate your setup.');
467        printStatus("When complete, type 'flutter run' from the '$relativeAppPath' "
468            'directory in order to launch your app.');
469        printStatus('Your $application code is in $relativeAppMain');
470      }
471    }
472
473    return null;
474  }
475
476  Future<int> _generateModule(Directory directory, Map<String, dynamic> templateContext, { bool overwrite = false }) async {
477    int generatedCount = 0;
478    final String description = argResults.wasParsed('description')
479        ? argResults['description']
480        : 'A new flutter module project.';
481    templateContext['description'] = description;
482    generatedCount += _renderTemplate(fs.path.join('module', 'common'), directory, templateContext, overwrite: overwrite);
483    if (argResults['pub']) {
484      await pubGet(
485        context: PubContext.create,
486        directory: directory.path,
487        offline: argResults['offline'],
488      );
489      final FlutterProject project = FlutterProject.fromDirectory(directory);
490      await project.ensureReadyForPlatformSpecificTooling(checkProjects: false);
491    }
492    return generatedCount;
493  }
494
495  Future<int> _generatePackage(Directory directory, Map<String, dynamic> templateContext, { bool overwrite = false }) async {
496    int generatedCount = 0;
497    final String description = argResults.wasParsed('description')
498        ? argResults['description']
499        : 'A new Flutter package project.';
500    templateContext['description'] = description;
501    generatedCount += _renderTemplate('package', directory, templateContext, overwrite: overwrite);
502    if (argResults['pub']) {
503      await pubGet(
504        context: PubContext.createPackage,
505        directory: directory.path,
506        offline: argResults['offline'],
507      );
508    }
509    return generatedCount;
510  }
511
512  Future<int> _generatePlugin(Directory directory, Map<String, dynamic> templateContext, { bool overwrite = false }) async {
513    int generatedCount = 0;
514    final String description = argResults.wasParsed('description')
515        ? argResults['description']
516        : 'A new flutter plugin project.';
517    templateContext['description'] = description;
518    generatedCount += _renderTemplate('plugin', directory, templateContext, overwrite: overwrite);
519    if (argResults['pub']) {
520      await pubGet(
521        context: PubContext.createPlugin,
522        directory: directory.path,
523        offline: argResults['offline'],
524      );
525    }
526    final FlutterProject project = FlutterProject.fromDirectory(directory);
527    gradle.updateLocalProperties(project: project, requireAndroidSdk: false);
528
529    final String projectName = templateContext['projectName'];
530    final String organization = templateContext['organization'];
531    final String androidPluginIdentifier = templateContext['androidIdentifier'];
532    final String exampleProjectName = projectName + '_example';
533    templateContext['projectName'] = exampleProjectName;
534    templateContext['androidIdentifier'] = _createAndroidIdentifier(organization, exampleProjectName);
535    templateContext['iosIdentifier'] = _createUTIIdentifier(organization, exampleProjectName);
536    templateContext['description'] = 'Demonstrates how to use the $projectName plugin.';
537    templateContext['pluginProjectName'] = projectName;
538    templateContext['androidPluginIdentifier'] = androidPluginIdentifier;
539
540    generatedCount += await _generateApp(project.example.directory, templateContext, overwrite: overwrite);
541    return generatedCount;
542  }
543
544  Future<int> _generateApp(Directory directory, Map<String, dynamic> templateContext, { bool overwrite = false }) async {
545    int generatedCount = 0;
546    generatedCount += _renderTemplate('app', directory, templateContext, overwrite: overwrite);
547    final FlutterProject project = FlutterProject.fromDirectory(directory);
548    generatedCount += _injectGradleWrapper(project);
549
550    if (argResults['with-driver-test']) {
551      final Directory testDirectory = directory.childDirectory('test_driver');
552      generatedCount += _renderTemplate('driver', testDirectory, templateContext, overwrite: overwrite);
553    }
554
555    if (argResults['pub']) {
556      await pubGet(context: PubContext.create, directory: directory.path, offline: argResults['offline']);
557      await project.ensureReadyForPlatformSpecificTooling(checkProjects: false);
558    }
559
560    gradle.updateLocalProperties(project: project, requireAndroidSdk: false);
561
562    return generatedCount;
563  }
564
565  // Takes an application template and replaces the main.dart with one from the
566  // documentation website in sampleCode.  Returns the difference in the number
567  // of files after applying the sample, since it also deletes the application's
568  // test directory (since the template's test doesn't apply to the sample).
569  Future<int> _applySample(Directory directory, String sampleCode) async {
570    final File mainDartFile = directory.childDirectory('lib').childFile('main.dart');
571    await mainDartFile.create(recursive: true);
572    await mainDartFile.writeAsString(sampleCode);
573    final Directory testDir = directory.childDirectory('test');
574    final List<FileSystemEntity> files = testDir.listSync(recursive: true);
575    await testDir.delete(recursive: true);
576    return -files.length;
577  }
578
579  Map<String, dynamic> _templateContext({
580    String organization,
581    String projectName,
582    String projectDescription,
583    String androidLanguage,
584    bool androidX,
585    String iosLanguage,
586    String flutterRoot,
587    bool renderDriverTest = false,
588    bool withPluginHook = false,
589    bool web = false,
590  }) {
591    flutterRoot = fs.path.normalize(flutterRoot);
592
593    final String pluginDartClass = _createPluginClassName(projectName);
594    final String pluginClass = pluginDartClass.endsWith('Plugin')
595        ? pluginDartClass
596        : pluginDartClass + 'Plugin';
597
598    return <String, dynamic>{
599      'organization': organization,
600      'projectName': projectName,
601      'androidIdentifier': _createAndroidIdentifier(organization, projectName),
602      'iosIdentifier': _createUTIIdentifier(organization, projectName),
603      'description': projectDescription,
604      'dartSdk': '$flutterRoot/bin/cache/dart-sdk',
605      'androidX': androidX,
606      'androidMinApiLevel': android.minApiLevel,
607      'androidSdkVersion': android_sdk.minimumAndroidSdkVersion,
608      'androidFlutterJar': '$flutterRoot/bin/cache/artifacts/engine/android-arm/flutter.jar',
609      'withDriverTest': renderDriverTest,
610      'pluginClass': pluginClass,
611      'pluginDartClass': pluginDartClass,
612      'withPluginHook': withPluginHook,
613      'androidLanguage': androidLanguage,
614      'iosLanguage': iosLanguage,
615      'flutterRevision': FlutterVersion.instance.frameworkRevision,
616      'flutterChannel': FlutterVersion.instance.channel,
617      'web': web && featureFlags.isWebEnabled,
618    };
619  }
620
621  int _renderTemplate(String templateName, Directory directory, Map<String, dynamic> context, { bool overwrite = false }) {
622    final Template template = Template.fromName(templateName);
623    return template.render(directory, context, overwriteExisting: overwrite);
624  }
625
626  int _injectGradleWrapper(FlutterProject project) {
627    int filesCreated = 0;
628    copyDirectorySync(
629      cache.getArtifactDirectory('gradle_wrapper'),
630      project.android.hostAppGradleRoot,
631      (File sourceFile, File destinationFile) {
632        filesCreated++;
633        final String modes = sourceFile.statSync().modeString();
634        if (modes != null && modes.contains('x')) {
635          os.makeExecutable(destinationFile);
636        }
637      },
638    );
639    return filesCreated;
640  }
641}
642
643String _createAndroidIdentifier(String organization, String name) {
644  // Android application ID is specified in: https://developer.android.com/studio/build/application-id
645  // All characters must be alphanumeric or an underscore [a-zA-Z0-9_].
646  String tmpIdentifier = '$organization.$name';
647  final RegExp disallowed = RegExp(r'[^\w\.]');
648  tmpIdentifier = tmpIdentifier.replaceAll(disallowed, '');
649
650  // It must have at least two segments (one or more dots).
651  final List<String> segments = tmpIdentifier
652      .split('.')
653      .where((String segment) => segment.isNotEmpty)
654      .toList();
655  while (segments.length < 2) {
656    segments.add('untitled');
657  }
658
659  // Each segment must start with a letter.
660  final RegExp segmentPatternRegex = RegExp(r'^[a-zA-Z][\w]*$');
661  final List<String> prefixedSegments = segments
662      .map((String segment) {
663        if (!segmentPatternRegex.hasMatch(segment)) {
664          return 'u'+segment;
665        }
666        return segment;
667      })
668      .toList();
669  return prefixedSegments.join('.');
670}
671
672String _createPluginClassName(String name) {
673  final String camelizedName = camelCase(name);
674  return camelizedName[0].toUpperCase() + camelizedName.substring(1);
675}
676
677String _createUTIIdentifier(String organization, String name) {
678  // Create a UTI (https://en.wikipedia.org/wiki/Uniform_Type_Identifier) from a base name
679  name = camelCase(name);
680  String tmpIdentifier = '$organization.$name';
681  final RegExp disallowed = RegExp(r'[^a-zA-Z0-9\-\.\u0080-\uffff]+');
682  tmpIdentifier = tmpIdentifier.replaceAll(disallowed, '');
683
684  // It must have at least two segments (one or more dots).
685  final List<String> segments = tmpIdentifier
686      .split('.')
687      .where((String segment) => segment.isNotEmpty)
688      .toList();
689  while (segments.length < 2) {
690    segments.add('untitled');
691  }
692
693  return segments.join('.');
694}
695
696const Set<String> _packageDependencies = <String>{
697  'analyzer',
698  'args',
699  'async',
700  'collection',
701  'convert',
702  'crypto',
703  'flutter',
704  'flutter_test',
705  'front_end',
706  'html',
707  'http',
708  'intl',
709  'io',
710  'isolate',
711  'kernel',
712  'logging',
713  'matcher',
714  'meta',
715  'mime',
716  'path',
717  'plugin',
718  'pool',
719  'test',
720  'utf',
721  'watcher',
722  'yaml',
723};
724
725/// Return null if the project name is legal. Return a validation message if
726/// we should disallow the project name.
727String _validateProjectName(String projectName) {
728  if (!linter_utils.isValidPackageName(projectName)) {
729    final String packageNameDetails = package_names.PubPackageNames().details;
730    return '"$projectName" is not a valid Dart package name.\n\n$packageNameDetails';
731  }
732  if (_packageDependencies.contains(projectName)) {
733    return "Invalid project name: '$projectName' - this will conflict with Flutter "
734      'package dependencies.';
735  }
736  return null;
737}
738
739/// Return null if the project directory is legal. Return a validation message
740/// if we should disallow the directory name.
741String _validateProjectDir(String dirPath, { String flutterRoot, bool overwrite = false }) {
742  if (fs.path.isWithin(flutterRoot, dirPath)) {
743    return 'Cannot create a project within the Flutter SDK. '
744      "Target directory '$dirPath' is within the Flutter SDK at '$flutterRoot'.";
745  }
746
747  // If the destination directory is actually a file, then we refuse to
748  // overwrite, on the theory that the user probably didn't expect it to exist.
749  if (fs.isFileSync(dirPath)) {
750    return "Invalid project name: '$dirPath' - refers to an existing file."
751        '${overwrite ? ' Refusing to overwrite a file with a directory.' : ''}';
752  }
753
754  if (overwrite)
755    return null;
756
757  final FileSystemEntityType type = fs.typeSync(dirPath);
758
759  if (type != FileSystemEntityType.notFound) {
760    switch (type) {
761      case FileSystemEntityType.file:
762        // Do not overwrite files.
763        return "Invalid project name: '$dirPath' - file exists.";
764      case FileSystemEntityType.link:
765        // Do not overwrite links.
766        return "Invalid project name: '$dirPath' - refers to a link.";
767    }
768  }
769
770  return null;
771}
772