create.dart 8.33 KB
Newer Older
1 2 3 4 5 6 7
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:async';
import 'dart:io';

8
import 'package:path/path.dart' as path;
9

10
import '../android/android.dart' as android;
11
import '../base/common.dart';
12
import '../base/utils.dart';
13
import '../cache.dart';
14
import '../dart/pub.dart';
15
import '../globals.dart';
16
import '../runner/flutter_command.dart';
17
import '../template.dart';
Hixie's avatar
Hixie committed
18

19
class CreateCommand extends FlutterCommand {
20
  CreateCommand() {
21
    argParser.addFlag('pub',
22
      defaultsTo: true,
23
      help: 'Whether to run "flutter packages get" after the project has been created.'
24
    );
25 26 27 28
    argParser.addFlag(
      'with-driver-test',
      negatable: true,
      defaultsTo: false,
29
      help: 'Also add a flutter_driver dependency and generate a sample \'flutter drive\' test.'
30
    );
31 32 33 34 35
    argParser.addOption(
      'description',
      defaultsTo: 'A new flutter project.',
      help: 'The description to use for your new flutter project. This string ends up in the pubspec.yaml file.'
    );
36 37
  }

38 39 40 41 42 43 44
  @override
  final String name = 'create';

  @override
  final String description = 'Create a new Flutter project.\n\n'
    'If run on a project that already exists, this will repair the project, recreating any files that are missing.';

45
  @override
46 47
  String get invocation => "${runner.executableName} $name <output directory>";

48
  @override
49
  Future<Null> runCommand() async {
50 51
    if (argResults.rest.isEmpty)
      throwToolExit('No option specified for the output directory.\n$usage', exitCode: 2);
52

53
    if (argResults.rest.length > 1) {
54
      String message = 'Multiple output directories specified.';
55 56
      for (String arg in argResults.rest) {
        if (arg.startsWith('-')) {
57
          message += '\nTry moving $arg to be immediately following $name';
58 59 60
          break;
        }
      }
61
      throwToolExit(message, exitCode: 2);
62 63
    }

64 65 66
    if (Cache.flutterRoot == null)
      throwToolExit('Neither the --flutter-root command line flag nor the FLUTTER_ROOT environment\n'
        'variable was specified. Unable to find package:flutter.', exitCode: 2);
67

68 69
    await Cache.instance.updateAll();

70
    String flutterRoot = path.absolute(Cache.flutterRoot);
71

72 73
    String flutterPackagesDirectory = path.join(flutterRoot, 'packages');
    String flutterPackagePath = path.join(flutterPackagesDirectory, 'flutter');
74 75
    if (!FileSystemEntity.isFileSync(path.join(flutterPackagePath, 'pubspec.yaml')))
      throwToolExit('Unable to find package:flutter in $flutterPackagePath', exitCode: 2);
76

77
    String flutterDriverPackagePath = path.join(flutterRoot, 'packages', 'flutter_driver');
78 79
    if (!FileSystemEntity.isFileSync(path.join(flutterDriverPackagePath, 'pubspec.yaml')))
      throwToolExit('Unable to find package:flutter_driver in $flutterDriverPackagePath', exitCode: 2);
80

81
    Directory projectDir = new Directory(argResults.rest.first);
82
    String dirPath = path.normalize(projectDir.absolute.path);
Devon Carew's avatar
Devon Carew committed
83
    String relativePath = path.relative(dirPath);
84 85
    String projectName = _normalizeProjectName(path.basename(dirPath));

86
    String error =_validateProjectDir(dirPath, flutterRoot: flutterRoot);
87 88
    if (error != null)
      throwToolExit(error);
Devon Carew's avatar
Devon Carew committed
89

90
    error = _validateProjectName(projectName);
91 92
    if (error != null)
      throwToolExit(error);
93

Devon Carew's avatar
Devon Carew committed
94
    int generatedCount = _renderTemplates(
95 96 97 98 99 100
      projectName,
      argResults['description'],
      dirPath,
      flutterPackagesDirectory,
      renderDriverTest: argResults['with-driver-test']
    );
Devon Carew's avatar
Devon Carew committed
101
    printStatus('Wrote $generatedCount files.');
102

103 104
    printStatus('');

105 106
    if (argResults['pub'])
      await pubGet(directory: dirPath);
Hixie's avatar
Hixie committed
107

108
    printStatus('');
109 110 111 112

    // Run doctor; tell the user the next steps.
    if (doctor.canLaunchAnything) {
      // Let them know a summary of the state of their tooling.
113
      await doctor.summary();
114 115 116 117

      printStatus('''
All done! In order to run your application, type:

Devon Carew's avatar
Devon Carew committed
118
  \$ cd $relativePath
119 120 121
  \$ flutter run

Your main program file is lib/main.dart in the $relativePath directory.
122 123 124 125 126 127 128
''');
    } else {
      printStatus("You'll need to install additional components before you can run "
        "your Flutter app:");
      printStatus('');

      // Give the user more detailed analysis.
129
      await doctor.diagnose();
130 131 132
      printStatus('');
      printStatus("After installing components, run 'flutter doctor' in order to "
        "re-validate your setup.");
133
      printStatus("When complete, type 'flutter run' from the '$relativePath' "
134
        "directory in order to launch your app.");
135
      printStatus("Your main program file is: $relativePath/lib/main.dart");
136
    }
Hixie's avatar
Hixie committed
137
  }
138

Devon Carew's avatar
Devon Carew committed
139
  int _renderTemplates(String projectName, String projectDescription, String dirPath,
140
      String flutterPackagesDirectory, { bool renderDriverTest: false }) {
Devon Carew's avatar
Devon Carew committed
141
    new Directory(dirPath).createSync(recursive: true);
142

Devon Carew's avatar
Devon Carew committed
143 144
    flutterPackagesDirectory = path.normalize(flutterPackagesDirectory);
    flutterPackagesDirectory = _relativePath(from: dirPath, to: flutterPackagesDirectory);
145

Devon Carew's avatar
Devon Carew committed
146
    printStatus('Creating project ${path.relative(dirPath)}...');
147

Ian Hickson's avatar
Ian Hickson committed
148
    Map<String, dynamic> templateContext = <String, dynamic>{
149
      'projectName': projectName,
150 151
      'androidIdentifier': _createAndroidIdentifier(projectName),
      'iosIdentifier': _createUTIIdentifier(projectName),
152
      'description': projectDescription,
Devon Carew's avatar
Devon Carew committed
153
      'flutterPackagesDirectory': flutterPackagesDirectory,
154
      'androidMinApiLevel': android.minApiLevel
155
    };
156

Devon Carew's avatar
Devon Carew committed
157 158
    int fileCount = 0;

159
    templateContext['withDriverTest'] = renderDriverTest;
160

161
    Template createTemplate = new Template.fromName('create');
162 163 164 165 166
    fileCount += createTemplate.render(
      new Directory(dirPath),
      templateContext, overwriteExisting: false,
      projectName: projectName
    );
167

168 169
    if (renderDriverTest) {
      Template driverTemplate = new Template.fromName('driver');
Devon Carew's avatar
Devon Carew committed
170
      fileCount += driverTemplate.render(new Directory(path.join(dirPath, 'test_driver')),
171 172
          templateContext, overwriteExisting: false);
    }
Devon Carew's avatar
Devon Carew committed
173 174

    return fileCount;
175
  }
176 177 178 179 180
}

String _normalizeProjectName(String name) {
  name = name.replaceAll('-', '_').replaceAll(' ', '_');
  // Strip any extension (like .dart).
181
  if (name.contains('.'))
182 183 184 185
    name = name.substring(0, name.indexOf('.'));
  return name;
}

186 187 188 189 190
String _createAndroidIdentifier(String name) {
  return 'com.yourcompany.${camelCase(name)}';
}

String _createUTIIdentifier(String name) {
191
  // Create a UTI (https://en.wikipedia.org/wiki/Uniform_Type_Identifier) from a base name
192
  RegExp disallowed = new RegExp(r"[^a-zA-Z0-9\-\.\u0080-\uffff]+");
193 194
  name = camelCase(name).replaceAll(disallowed, '');
  name = name.isEmpty ? 'untitled' : name;
Chinmay Garde's avatar
Chinmay Garde committed
195
  return 'com.yourcompany.$name';
196
}
197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222

final Set<String> _packageDependencies = new Set<String>.from(<String>[
  'args',
  'async',
  'collection',
  'convert',
  'flutter',
  'html',
  'intl',
  'logging',
  'matcher',
  'mime',
  'path',
  'plugin',
  'pool',
  'test',
  'utf',
  'watcher',
  'yaml'
]);

/// Return `null` if the project name is legal. Return a validation message if
/// we should disallow the project name.
String _validateProjectName(String projectName) {
  if (_packageDependencies.contains(projectName)) {
    return "Invalid project name: '$projectName' - this will conflict with Flutter "
223
      "package dependencies.";
224
  }
225 226
  return null;
}
227

228 229
/// Return `null` if the project directory is legal. Return a validation message
/// if we should disallow the directory name.
230 231 232 233 234 235 236
String _validateProjectDir(String dirPath, { String flutterRoot }) {
  if (path.isWithin(flutterRoot, dirPath)) {
    return "Cannot create a project within the Flutter SDK.\n"
      "Target directory '$dirPath' is within the Flutter SDK at '$flutterRoot'.";
  }

  FileSystemEntityType type = FileSystemEntity.typeSync(dirPath);
237

238 239 240 241
  if (type != FileSystemEntityType.NOT_FOUND) {
    switch(type) {
      case FileSystemEntityType.FILE:
        // Do not overwrite files.
242
        return "Invalid project name: '$dirPath' - file exists.";
243 244
      case FileSystemEntityType.LINK:
        // Do not overwrite links.
245
        return "Invalid project name: '$dirPath' - refers to a link.";
246 247
    }
  }
248

249 250
  return null;
}
Devon Carew's avatar
Devon Carew committed
251 252 253 254 255 256 257 258

String _relativePath({ String from, String to }) {
  String result = path.relative(to, from: from);
  // `path.relative()` doesn't always return a correct result: dart-lang/path#12.
  if (FileSystemEntity.isDirectorySync(path.join(from, result)))
    return result;
  return to;
}