create.dart 14.9 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 '../build_info.dart';
18
import '../cache.dart';
19
import '../dart/pub.dart';
20
import '../doctor.dart';
21
import '../flx.dart' as flx;
22
import '../globals.dart';
23
import '../ios/xcodeproj.dart';
24
import '../plugins.dart';
25
import '../runner/flutter_command.dart';
26
import '../template.dart';
27
import '../version.dart';
Hixie's avatar
Hixie committed
28

29
class CreateCommand extends FlutterCommand {
30
  CreateCommand() {
31 32 33 34
    argParser.addFlag('pub',
      defaultsTo: true,
      help: 'Whether to run "flutter packages get" after the project has been created.'
    );
35 36 37 38 39 40
    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.'
    );
41 42 43 44
    argParser.addFlag(
      'with-driver-test',
      negatable: true,
      defaultsTo: false,
45
      help: 'Also add a flutter_driver dependency and generate a sample \'flutter drive\' test.'
46
    );
47 48 49 50 51 52 53 54 55
    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.',
56 57
        '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.',
58 59 60
      },
      defaultsTo: 'app',
    );
61 62
    argParser.addOption(
      'description',
63 64 65 66 67 68 69 70
      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',
      defaultsTo: 'com.yourcompany',
      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.'
71
    );
72 73 74 75 76 77 78 79 80 81 82 83
    argParser.addOption(
      'ios-language',
      abbr: 'i',
      defaultsTo: 'objc',
      allowed: <String>['objc', 'swift'],
    );
    argParser.addOption(
      'android-language',
      abbr: 'a',
      defaultsTo: 'java',
      allowed: <String>['java', 'kotlin'],
    );
84 85
  }

86 87 88 89 90 91 92
  @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.';

93
  @override
94
  String get invocation => '${runner.executableName} $name <output directory>';
95

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

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

112 113 114
    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);
115

116 117
    await Cache.instance.updateAll();

118
    final String flutterRoot = fs.path.absolute(Cache.flutterRoot);
119

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

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

129
    final String template = argResults['template'];
130 131
    final bool generatePlugin = template == 'plugin';
    final bool generatePackage = template == 'package';
132

133
    final Directory projectDir = fs.directory(argResults.rest.first);
134 135 136 137
    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);
138
    final String organization = argResults['org'];
139
    final String projectName = fs.path.basename(dirPath);
140

141
    String error =_validateProjectDir(dirPath, flutterRoot: flutterRoot);
142 143
    if (error != null)
      throwToolExit(error);
Devon Carew's avatar
Devon Carew committed
144

145
    error = _validateProjectName(projectName);
146 147
    if (error != null)
      throwToolExit(error);
148

149
    final Map<String, dynamic> templateContext = _templateContext(
150
      organization: organization,
151 152 153 154 155 156 157 158
      projectName: projectName,
      projectDescription: argResults['description'],
      dirPath: dirPath,
      flutterRoot: flutterRoot,
      renderDriverTest: argResults['with-driver-test'],
      withPluginHook: generatePlugin,
      androidLanguage: argResults['android-language'],
      iosLanguage: argResults['ios-language'],
159
    );
160 161 162

    printStatus('Creating project ${fs.path.relative(dirPath)}...');
    int generatedCount = 0;
163 164 165 166 167 168 169
    if (generatePackage) {
      final String description = argResults.wasParsed('description')
          ? argResults['description']
          : 'A new flutter package project.';
      templateContext['description'] = description;
      generatedCount += _renderTemplate('package', dirPath, templateContext);

170
      if (argResults['pub'])
171 172 173
        await pubGet(
          context: PubContext.createPackage,
          directory: dirPath,
174
          offline: argResults['offline'],
175
        );
176 177 178 179

      final String relativePath = fs.path.relative(dirPath);
      printStatus('Wrote $generatedCount files.');
      printStatus('');
180
      printStatus('Your package code is in lib/$projectName.dart in the $relativePath directory.');
181 182 183
      return;
    }

184 185 186 187 188 189 190 191
    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);

192
      if (argResults['pub'])
193 194 195
        await pubGet(
          context: PubContext.createPlugin,
          directory: dirPath,
196
          offline: argResults['offline'],
197
        );
198

199 200 201
      if (android_sdk.androidSdk != null)
        gradle.updateLocalProperties(projectPath: dirPath);

202 203 204 205
      appPath = fs.path.join(dirPath, 'example');
      final String androidPluginIdentifier = templateContext['androidIdentifier'];
      final String exampleProjectName = projectName + '_example';
      templateContext['projectName'] = exampleProjectName;
206 207
      templateContext['androidIdentifier'] = _createAndroidIdentifier(organization, exampleProjectName);
      templateContext['iosIdentifier'] = _createUTIIdentifier(organization, exampleProjectName);
208 209 210 211 212 213
      templateContext['description'] = 'Demonstrates how to use the $projectName plugin.';
      templateContext['pluginProjectName'] = projectName;
      templateContext['androidPluginIdentifier'] = androidPluginIdentifier;
    }

    generatedCount += _renderTemplate('create', appPath, templateContext);
214 215 216 217
    generatedCount += _injectGradleWrapper(appPath);
    if (appPath != dirPath) {
      generatedCount += _injectGradleWrapper(dirPath);
    }
218 219 220 221 222
    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
223
    printStatus('Wrote $generatedCount files.');
224 225
    printStatus('');

226 227
    updateXcodeGeneratedProperties(
      projectPath: appPath,
228
      buildInfo: BuildInfo.debug,
229 230
      target: flx.defaultMainPath,
      hasPlugins: generatePlugin,
231
      previewDart2: false,
232
    );
233

234
    if (argResults['pub']) {
235
      await pubGet(context: PubContext.create, directory: appPath, offline: argResults['offline']);
236 237
      injectPlugins(directory: appPath);
    }
Hixie's avatar
Hixie committed
238

239 240 241
    if (android_sdk.androidSdk != null)
      gradle.updateLocalProperties(projectPath: appPath);

242
    printStatus('');
243 244

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

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

254
  \$ cd $relativeAppPath
255 256
  \$ flutter run

257 258 259 260 261 262 263
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.
264
To edit platform code in an IDE see https://flutter.io/platform-plugins/#edit-code.
265
''');
266
      }
267 268
    } else {
      printStatus("You'll need to install additional components before you can run "
269
        'your Flutter app:');
270 271 272
      printStatus('');

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

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

296 297 298 299
    final String pluginDartClass = _createPluginClassName(projectName);
    final String pluginClass = pluginDartClass.endsWith('Plugin')
        ? pluginDartClass
        : pluginDartClass + 'Plugin';
300

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

322 323 324
  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);
325
  }
326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341

  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;
  }
342 343
}

344
String _createAndroidIdentifier(String organization, String name) {
345
  return '$organization.$name'.replaceAll('_', '');
346 347 348 349 350
}

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

353
String _createUTIIdentifier(String organization, String name) {
354
  // Create a UTI (https://en.wikipedia.org/wiki/Uniform_Type_Identifier) from a base name
355
  final RegExp disallowed = new RegExp(r'[^a-zA-Z0-9\-\.\u0080-\uffff]+');
356 357
  name = camelCase(name).replaceAll(disallowed, '');
  name = name.isEmpty ? 'untitled' : name;
358
  return '$organization.$name';
359
}
360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380

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'
]);

381
/// Return null if the project name is legal. Return a validation message if
382 383
/// we should disallow the project name.
String _validateProjectName(String projectName) {
384 385 386 387
  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';
  }
388 389
  if (_packageDependencies.contains(projectName)) {
    return "Invalid project name: '$projectName' - this will conflict with Flutter "
390
      'package dependencies.';
391
  }
392 393
  return null;
}
394

395
/// Return null if the project directory is legal. Return a validation message
396
/// if we should disallow the directory name.
397
String _validateProjectDir(String dirPath, { String flutterRoot }) {
398
  if (fs.path.isWithin(flutterRoot, dirPath)) {
399
    return 'Cannot create a project within the Flutter SDK.\n'
400 401 402
      "Target directory '$dirPath' is within the Flutter SDK at '$flutterRoot'.";
  }

403
  final FileSystemEntityType type = fs.typeSync(dirPath);
404

405
  if (type != FileSystemEntityType.NOT_FOUND) {
406
    switch (type) {
407 408
      case FileSystemEntityType.FILE:
        // Do not overwrite files.
409
        return "Invalid project name: '$dirPath' - file exists.";
410 411
      case FileSystemEntityType.LINK:
        // Do not overwrite links.
412
        return "Invalid project name: '$dirPath' - refers to a link.";
413 414
    }
  }
415

416 417
  return null;
}