create.dart 15.3 KB
Newer Older
1 2 3 4 5 6
// 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';

7
import 'package:linter/src/rules/pub/package_names.dart' as package_names; // ignore: implementation_imports
8
import 'package:linter/src/utils.dart' as linter_utils; // ignore: implementation_imports
9

10
import '../android/android.dart' as android;
11
import '../android/android_sdk.dart' as android_sdk;
12
import '../android/gradle.dart' as gradle;
13
import '../base/common.dart';
14
import '../base/file_system.dart';
15
import '../base/os.dart';
16
import '../base/utils.dart';
17
import '../cache.dart';
18
import '../dart/pub.dart';
19
import '../doctor.dart';
20
import '../globals.dart';
21
import '../project.dart';
22
import '../runner/flutter_command.dart';
23
import '../template.dart';
24
import '../version.dart';
Hixie's avatar
Hixie committed
25

26
class CreateCommand extends FlutterCommand {
27
  CreateCommand() {
28 29 30 31
    argParser.addFlag('pub',
      defaultsTo: true,
      help: 'Whether to run "flutter packages get" after the project has been created.'
    );
32 33 34 35 36 37
    argParser.addFlag('offline',
      defaultsTo: false,
      help: 'When "flutter packages get" is run by the create command, this indicates '
        'whether to run it in offline mode or not. In offline mode, it will need to '
        'have all dependencies already available in the pub cache to succeed.'
    );
38 39 40 41
    argParser.addFlag(
      'with-driver-test',
      negatable: true,
      defaultsTo: false,
42
      help: 'Also add a flutter_driver dependency and generate a sample \'flutter drive\' test.'
43
    );
44 45 46 47 48 49 50 51 52
    argParser.addOption(
      'template',
      abbr: 't',
      allowed: <String>['app', 'package', 'plugin'],
      help: 'Specify the type of project to create.',
      valueHelp: 'type',
      allowedHelp: <String, String>{
        'app': '(default) Generate a Flutter application.',
        'package': 'Generate a shareable Flutter project containing modular Dart code.',
53 54
        'plugin': 'Generate a shareable Flutter project containing an API in Dart code\n'
            'with a platform-specific implementation for Android, for iOS code, or for both.',
55 56 57
      },
      defaultsTo: 'app',
    );
58 59
    argParser.addOption(
      'description',
60 61 62 63 64
      defaultsTo: 'A new Flutter project.',
      help: 'The description to use for your new Flutter project. This string ends up in the pubspec.yaml file.'
    );
    argParser.addOption(
      'org',
65
      defaultsTo: 'com.example',
66 67
      help: 'The organization responsible for your new Flutter project, in reverse domain name notation.\n'
            'This string is used in Java package names and as prefix in the iOS bundle identifier.'
68
    );
69 70 71 72 73 74 75 76 77 78 79 80
    argParser.addOption(
      'ios-language',
      abbr: 'i',
      defaultsTo: 'objc',
      allowed: <String>['objc', 'swift'],
    );
    argParser.addOption(
      'android-language',
      abbr: 'a',
      defaultsTo: 'java',
      allowed: <String>['java', 'kotlin'],
    );
81 82
  }

83 84 85 86 87 88 89
  @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.';

90
  @override
91
  String get invocation => '${runner.executableName} $name <output directory>';
92

93
  @override
94
  Future<Null> runCommand() async {
95 96
    if (argResults.rest.isEmpty)
      throwToolExit('No option specified for the output directory.\n$usage', exitCode: 2);
97

98
    if (argResults.rest.length > 1) {
99
      String message = 'Multiple output directories specified.';
100 101
      for (String arg in argResults.rest) {
        if (arg.startsWith('-')) {
102
          message += '\nTry moving $arg to be immediately following $name';
103 104 105
          break;
        }
      }
106
      throwToolExit(message, exitCode: 2);
107 108
    }

109 110 111
    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);
112

113 114
    await Cache.instance.updateAll();

115
    final String flutterRoot = fs.path.absolute(Cache.flutterRoot);
116

117 118
    final String flutterPackagesDirectory = fs.path.join(flutterRoot, 'packages');
    final String flutterPackagePath = fs.path.join(flutterPackagesDirectory, 'flutter');
119
    if (!fs.isFileSync(fs.path.join(flutterPackagePath, 'pubspec.yaml')))
120
      throwToolExit('Unable to find package:flutter in $flutterPackagePath', exitCode: 2);
121

122
    final String flutterDriverPackagePath = fs.path.join(flutterRoot, 'packages', 'flutter_driver');
123
    if (!fs.isFileSync(fs.path.join(flutterDriverPackagePath, 'pubspec.yaml')))
124
      throwToolExit('Unable to find package:flutter_driver in $flutterDriverPackagePath', exitCode: 2);
125

126
    final String template = argResults['template'];
127 128
    final bool generatePlugin = template == 'plugin';
    final bool generatePackage = template == 'package';
129

130
    final Directory projectDir = fs.directory(argResults.rest.first);
131 132 133 134
    String dirPath = fs.path.normalize(projectDir.absolute.path);
    // TODO(goderbauer): Work-around for: https://github.com/dart-lang/path/issues/24
    if (fs.path.basename(dirPath) == '.')
      dirPath = fs.path.dirname(dirPath);
135 136 137 138 139 140 141 142 143 144 145 146
    String organization = argResults['org'];
    if (!argResults.wasParsed('org')) {
      final Set<String> existingOrganizations = await new FlutterProject(projectDir).organizationNames();
      if (existingOrganizations.length == 1) {
        organization = existingOrganizations.first;
      } else if (1 < existingOrganizations.length) {
        throwToolExit(
          'Ambiguous organization in existing files: $existingOrganizations.\n'
          'The --org command line argument must be specified to recreate project.'
        );
      }
    }
147
    final String projectName = fs.path.basename(dirPath);
148

149
    String error =_validateProjectDir(dirPath, flutterRoot: flutterRoot);
150 151
    if (error != null)
      throwToolExit(error);
Devon Carew's avatar
Devon Carew committed
152

153
    error = _validateProjectName(projectName);
154 155
    if (error != null)
      throwToolExit(error);
156

157
    final Map<String, dynamic> templateContext = _templateContext(
158
      organization: organization,
159 160 161 162 163 164 165 166
      projectName: projectName,
      projectDescription: argResults['description'],
      dirPath: dirPath,
      flutterRoot: flutterRoot,
      renderDriverTest: argResults['with-driver-test'],
      withPluginHook: generatePlugin,
      androidLanguage: argResults['android-language'],
      iosLanguage: argResults['ios-language'],
167
    );
168 169 170

    printStatus('Creating project ${fs.path.relative(dirPath)}...');
    int generatedCount = 0;
171 172 173 174 175 176 177
    if (generatePackage) {
      final String description = argResults.wasParsed('description')
          ? argResults['description']
          : 'A new flutter package project.';
      templateContext['description'] = description;
      generatedCount += _renderTemplate('package', dirPath, templateContext);

178
      if (argResults['pub'])
179 180 181
        await pubGet(
          context: PubContext.createPackage,
          directory: dirPath,
182
          offline: argResults['offline'],
183
        );
184 185 186 187

      final String relativePath = fs.path.relative(dirPath);
      printStatus('Wrote $generatedCount files.');
      printStatus('');
188
      printStatus('Your package code is in lib/$projectName.dart in the $relativePath directory.');
189 190 191
      return;
    }

192 193 194 195 196 197 198 199
    String appPath = dirPath;
    if (generatePlugin) {
      final String description = argResults.wasParsed('description')
          ? argResults['description']
          : 'A new flutter plugin project.';
      templateContext['description'] = description;
      generatedCount += _renderTemplate('plugin', dirPath, templateContext);

200
      if (argResults['pub'])
201 202 203
        await pubGet(
          context: PubContext.createPlugin,
          directory: dirPath,
204
          offline: argResults['offline'],
205
        );
206

207 208 209
      if (android_sdk.androidSdk != null)
        gradle.updateLocalProperties(projectPath: dirPath);

210 211 212 213
      appPath = fs.path.join(dirPath, 'example');
      final String androidPluginIdentifier = templateContext['androidIdentifier'];
      final String exampleProjectName = projectName + '_example';
      templateContext['projectName'] = exampleProjectName;
214 215
      templateContext['androidIdentifier'] = _createAndroidIdentifier(organization, exampleProjectName);
      templateContext['iosIdentifier'] = _createUTIIdentifier(organization, exampleProjectName);
216 217 218 219 220 221
      templateContext['description'] = 'Demonstrates how to use the $projectName plugin.';
      templateContext['pluginProjectName'] = projectName;
      templateContext['androidPluginIdentifier'] = androidPluginIdentifier;
    }

    generatedCount += _renderTemplate('create', appPath, templateContext);
222
    generatedCount += _injectGradleWrapper(appPath);
223 224 225 226 227
    if (argResults['with-driver-test']) {
      final String testPath = fs.path.join(appPath, 'test_driver');
      generatedCount += _renderTemplate('driver', testPath, templateContext);
    }

Devon Carew's avatar
Devon Carew committed
228
    printStatus('Wrote $generatedCount files.');
229 230
    printStatus('');

231
    if (argResults['pub']) {
232
      await pubGet(context: PubContext.create, directory: appPath, offline: argResults['offline']);
233
      new FlutterProject(fs.directory(appPath)).ensureReadyForPlatformSpecificTooling();
234
    }
Hixie's avatar
Hixie committed
235

236 237 238
    if (android_sdk.androidSdk != null)
      gradle.updateLocalProperties(projectPath: appPath);

239
    printStatus('');
240 241

    // Run doctor; tell the user the next steps.
242 243
    final String relativeAppPath = fs.path.relative(appPath);
    final String relativePluginPath = fs.path.relative(dirPath);
244 245
    if (doctor.canLaunchAnything) {
      // Let them know a summary of the state of their tooling.
246
      await doctor.summary();
247 248 249 250

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

251
  \$ cd $relativeAppPath
252 253
  \$ flutter run

254 255 256 257 258 259 260
Your main program file is lib/main.dart in the $relativeAppPath directory.
''');
      if (generatePlugin) {
        printStatus('''
Your plugin code is in lib/$projectName.dart in the $relativePluginPath directory.

Host platform code is in the android/ and ios/ directories under $relativePluginPath.
261
To edit platform code in an IDE see https://flutter.io/platform-plugins/#edit-code.
262
''');
263
      }
264 265
    } else {
      printStatus("You'll need to install additional components before you can run "
266
        'your Flutter app:');
267 268 269
      printStatus('');

      // Give the user more detailed analysis.
270
      await doctor.diagnose();
271 272
      printStatus('');
      printStatus("After installing components, run 'flutter doctor' in order to "
273
        're-validate your setup.');
274
      printStatus("When complete, type 'flutter run' from the '$relativeAppPath' "
275 276
        'directory in order to launch your app.');
      printStatus('Your main program file is: $relativeAppPath/lib/main.dart');
277
    }
Hixie's avatar
Hixie committed
278
  }
279

280
  Map<String, dynamic> _templateContext({
281
    String organization,
282 283 284 285 286 287 288 289 290
    String projectName,
    String projectDescription,
    String androidLanguage,
    String iosLanguage,
    String dirPath,
    String flutterRoot,
    bool renderDriverTest: false,
    bool withPluginHook: false,
  }) {
291
    flutterRoot = fs.path.normalize(flutterRoot);
292

293 294 295 296
    final String pluginDartClass = _createPluginClassName(projectName);
    final String pluginClass = pluginDartClass.endsWith('Plugin')
        ? pluginDartClass
        : pluginDartClass + 'Plugin';
297

298
    return <String, dynamic>{
299
      'organization': organization,
300
      'projectName': projectName,
301 302
      'androidIdentifier': _createAndroidIdentifier(organization, projectName),
      'iosIdentifier': _createUTIIdentifier(organization, projectName),
303
      'description': projectDescription,
304
      'dartSdk': '$flutterRoot/bin/cache/dart-sdk',
305
      'androidMinApiLevel': android.minApiLevel,
306
      'androidSdkVersion': android_sdk.minimumAndroidSdkVersion,
307
      'androidFlutterJar': '$flutterRoot/bin/cache/artifacts/engine/android-arm/flutter.jar',
308 309 310 311
      'withDriverTest': renderDriverTest,
      'pluginClass': pluginClass,
      'pluginDartClass': pluginDartClass,
      'withPluginHook': withPluginHook,
312 313
      'androidLanguage': androidLanguage,
      'iosLanguage': iosLanguage,
314 315
      'flutterRevision': FlutterVersion.instance.frameworkRevision,
      'flutterChannel': FlutterVersion.instance.channel,
316
    };
317
  }
318

319 320 321
  int _renderTemplate(String templateName, String dirPath, Map<String, dynamic> context) {
    final Template template = new Template.fromName(templateName);
    return template.render(fs.directory(dirPath), context, overwriteExisting: false);
322
  }
323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338

  int _injectGradleWrapper(String projectDir) {
    int filesCreated = 0;
    copyDirectorySync(
      cache.getArtifactDirectory('gradle_wrapper'),
      fs.directory(fs.path.join(projectDir, 'android')),
      (File sourceFile, File destinationFile) {
        filesCreated++;
        final String modes = sourceFile.statSync().modeString();
        if (modes != null && modes.contains('x')) {
          os.makeExecutable(destinationFile);
        }
      },
    );
    return filesCreated;
  }
339 340
}

341
String _createAndroidIdentifier(String organization, String name) {
342
  return '$organization.$name'.replaceAll('_', '');
343 344 345 346 347
}

String _createPluginClassName(String name) {
  final String camelizedName = camelCase(name);
  return camelizedName[0].toUpperCase() + camelizedName.substring(1);
348 349
}

350
String _createUTIIdentifier(String organization, String name) {
351
  // Create a UTI (https://en.wikipedia.org/wiki/Uniform_Type_Identifier) from a base name
352
  final RegExp disallowed = new RegExp(r'[^a-zA-Z0-9\-\.\u0080-\uffff]+');
353 354
  name = camelCase(name).replaceAll(disallowed, '');
  name = name.isEmpty ? 'untitled' : name;
355
  return '$organization.$name';
356
}
357 358

final Set<String> _packageDependencies = new Set<String>.from(<String>[
359
  'analyzer',
360 361 362 363
  'args',
  'async',
  'collection',
  'convert',
364
  'crypto',
365
  'flutter',
366 367
  'flutter_test',
  'front_end',
368
  'html',
369
  'http',
370
  'intl',
371 372 373
  'io',
  'isolate',
  'kernel',
374 375
  'logging',
  'matcher',
376
  'meta',
377 378 379 380 381 382 383 384 385 386
  'mime',
  'path',
  'plugin',
  'pool',
  'test',
  'utf',
  'watcher',
  'yaml'
]);

387
/// Return null if the project name is legal. Return a validation message if
388 389
/// we should disallow the project name.
String _validateProjectName(String projectName) {
390 391 392 393
  if (!linter_utils.isValidPackageName(projectName)) {
    final String packageNameDetails = new package_names.PubPackageNames().details;
    return '"$projectName" is not a valid Dart package name.\n\n$packageNameDetails';
  }
394 395
  if (_packageDependencies.contains(projectName)) {
    return "Invalid project name: '$projectName' - this will conflict with Flutter "
396
      'package dependencies.';
397
  }
398 399
  return null;
}
400

401
/// Return null if the project directory is legal. Return a validation message
402
/// if we should disallow the directory name.
403
String _validateProjectDir(String dirPath, { String flutterRoot }) {
404
  if (fs.path.isWithin(flutterRoot, dirPath)) {
405
    return 'Cannot create a project within the Flutter SDK.\n'
406 407 408
      "Target directory '$dirPath' is within the Flutter SDK at '$flutterRoot'.";
  }

409
  final FileSystemEntityType type = fs.typeSync(dirPath);
410

411
  if (type != FileSystemEntityType.NOT_FOUND) { // ignore: deprecated_member_use
412
    switch (type) {
413
      case FileSystemEntityType.FILE: // ignore: deprecated_member_use
414
        // Do not overwrite files.
415
        return "Invalid project name: '$dirPath' - file exists.";
416
      case FileSystemEntityType.LINK: // ignore: deprecated_member_use
417
        // Do not overwrite links.
418
        return "Invalid project name: '$dirPath' - refers to a link.";
419 420
    }
  }
421

422 423
  return null;
}