create.dart 30.6 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5 6
// @dart = 2.8

7
import '../android/gradle_utils.dart' as gradle;
8
import '../base/common.dart';
9
import '../base/context.dart';
10
import '../base/file_system.dart';
11
import '../base/net.dart';
12
import '../base/terminal.dart';
13
import '../base/utils.dart';
14
import '../convert.dart';
15
import '../dart/pub.dart';
16
import '../features.dart';
17
import '../flutter_manifest.dart';
18
import '../flutter_project_metadata.dart';
19
import '../globals.dart' as globals;
20
import '../ios/code_signing.dart';
21
import '../project.dart';
22
import '../reporting/reporting.dart';
23
import '../runner/flutter_command.dart';
24
import 'create_base.dart';
25

26 27 28 29 30 31 32
const String kPlatformHelp =
  'The platforms supported by this project. '
  'Platform folders (e.g. android/) will be generated in the target project. '
  'This argument only works when "--template" is set to app or plugin. '
  'When adding platforms to a plugin project, the pubspec.yaml will be updated with the requested platform. '
  'Adding desktop platforms requires the corresponding desktop config setting to be enabled.';

33
class CreateCommand extends CreateBase {
34 35 36
  CreateCommand({
    bool verboseHelp = false,
  }) : super(verboseHelp: verboseHelp) {
37
    addPlatformsOptions(customHelp: kPlatformHelp);
38 39 40
    argParser.addOption(
      'template',
      abbr: 't',
41
      allowed: FlutterProjectType.values.map<String>(flutterProjectTypeToString),
42 43 44
      help: 'Specify the type of project to create.',
      valueHelp: 'type',
      allowedHelp: <String, String>{
45
        flutterProjectTypeToString(FlutterProjectType.app): '(default) Generate a Flutter application.',
46 47 48 49
        flutterProjectTypeToString(FlutterProjectType.skeleton): 'Generate a List View / Detail View Flutter '
            'application that follows community best practices.',
        flutterProjectTypeToString(FlutterProjectType.package): 'Generate a shareable Flutter project containing modular '
            'Dart code.',
50
        flutterProjectTypeToString(FlutterProjectType.plugin): 'Generate a shareable Flutter project containing an API '
Daco Harkes's avatar
Daco Harkes committed
51 52 53 54 55 56
            'in Dart code with a platform-specific implementation through method channels for Android, iOS, '
            'Linux, macOS, Windows, web, or any combination of these.',
        flutterProjectTypeToString(FlutterProjectType.ffiPlugin):
            'Generate a shareable Flutter project containing an API '
            'in Dart code with a platform-specific implementation through dart:ffi for Android, iOS, '
            'Linux, macOS, Windows, or any combination of these.',
57
        flutterProjectTypeToString(FlutterProjectType.module): 'Generate a project to add a Flutter module to an '
58
            'existing Android or iOS application.',
59
      },
60
      defaultsTo: null,
61
    );
62 63 64
    argParser.addOption(
      'sample',
      abbr: 's',
65 66 67 68
      help: 'Specifies the Flutter code sample to use as the "main.dart" for an application. Implies '
        '"--template=app". The value should be the sample ID of the desired sample from the API '
        'documentation website (http://docs.flutter.dev/). An example can be found at: '
        'https://api.flutter.dev/flutter/widgets/SingleChildScrollView-class.html',
69
      defaultsTo: null,
70
      valueHelp: 'id',
71
    );
72 73 74
    argParser.addOption(
      'list-samples',
      help: 'Specifies a JSON output file for a listing of Flutter code samples '
75
        'that can be created with "--sample".',
76 77
      valueHelp: 'path',
    );
78 79
  }

80 81 82 83 84 85 86
  @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.';

87 88 89
  @override
  String get category => FlutterCommandCategory.project;

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

93
  @override
94 95
  Future<CustomDimensions> get usageValues async {
    return CustomDimensions(
96 97 98
      commandCreateProjectType: stringArgDeprecated('template'),
      commandCreateAndroidLanguage: stringArgDeprecated('android-language'),
      commandCreateIosLanguage: stringArgDeprecated('ios-language'),
99
    );
100 101
  }

102 103 104
  // Lazy-initialize the net utilities with values from the context.
  Net _cachedNet;
  Net get _net => _cachedNet ??= Net(
105
    httpClientFactory: context.get<HttpClientFactory>(),
106 107 108 109
    logger: globals.logger,
    platform: globals.platform,
  );

110
  /// The hostname for the Flutter docs for the current channel.
111
  String get _snippetsHost => globals.flutterVersion.channel == 'stable'
112 113
        ? 'api.flutter.dev'
        : 'master-api.flutter.dev';
114

115 116 117 118 119 120 121
  Future<String> _fetchSampleFromServer(String sampleId) async {
    // Sanity check the sampleId
    if (sampleId.contains(RegExp(r'[^-\w\.]'))) {
      throwToolExit('Sample ID "$sampleId" contains invalid characters. Check the ID in the '
        'documentation and try again.');
    }

122
    final Uri snippetsUri = Uri.https(_snippetsHost, 'snippets/$sampleId.dart');
123 124 125 126 127
    final List<int> data = await _net.fetchUrl(snippetsUri);
    if (data == null || data.isEmpty) {
      return null;
    }
    return utf8.decode(data);
128 129 130 131
  }

  /// Fetches the samples index file from the Flutter docs website.
  Future<String> _fetchSamplesIndexFromServer() async {
132
    final Uri snippetsUri = Uri.https(_snippetsHost, 'snippets/index.json');
133 134 135 136 137
    final List<int> data = await _net.fetchUrl(snippetsUri, maxAttempts: 2);
    if (data == null || data.isEmpty) {
      return null;
    }
    return utf8.decode(data);
138 139 140 141 142 143
  }

  /// Fetches the samples index file from the server and writes it to
  /// [outputFilePath].
  Future<void> _writeSamplesJson(String outputFilePath) async {
    try {
144
      final File outputFile = globals.fs.file(outputFilePath);
145 146 147
      if (outputFile.existsSync()) {
        throwToolExit('File "$outputFilePath" already exists', exitCode: 1);
      }
148 149 150
      final String samplesJson = await _fetchSamplesIndexFromServer();
      if (samplesJson == null) {
        throwToolExit('Unable to download samples', exitCode: 2);
151
      } else {
152
        outputFile.writeAsStringSync(samplesJson);
153
        globals.printStatus('Wrote samples JSON to "$outputFilePath"');
154
      }
155
    } on Exception catch (e) {
156 157
      throwToolExit('Failed to write samples JSON to "$outputFilePath": $e', exitCode: 2);
    }
158 159
  }

160 161 162
  FlutterProjectType _getProjectType(Directory projectDir) {
    FlutterProjectType template;
    FlutterProjectType detectedProjectType;
163 164
    final bool metadataExists = projectDir.absolute.childFile('.metadata').existsSync();
    if (argResults['template'] != null) {
165
      template = stringToProjectType(stringArgDeprecated('template'));
Daco Harkes's avatar
Daco Harkes committed
166 167 168 169 170 171 172 173 174 175 176
    }
    // If the project directory exists and isn't empty, then try to determine the template
    // type from the project directory.
    if (projectDir.existsSync() && projectDir.listSync().isNotEmpty) {
      detectedProjectType = determineTemplateType();
      if (detectedProjectType == null && metadataExists) {
        // We can only be definitive that this is the wrong type if the .metadata file
        // exists and contains a type that we don't understand, or doesn't contain a type.
        throwToolExit('Sorry, unable to detect the type of project to recreate. '
            'Try creating a fresh project and migrating your existing code to '
            'the new project manually.');
177 178
      }
    }
179
    template ??= detectedProjectType ?? FlutterProjectType.app;
180 181 182
    if (detectedProjectType != null && template != detectedProjectType && metadataExists) {
      // We can only be definitive that this is the wrong type if the .metadata file
      // exists and contains a type that doesn't match.
183 184
      throwToolExit("The requested template type '${flutterProjectTypeToString(template)}' doesn't match the "
          "existing template type of '${flutterProjectTypeToString(detectedProjectType)}'.");
185 186 187 188
    }
    return template;
  }

189
  @override
190
  Future<FlutterCommandResult> runCommand() async {
191
    if (argResults['list-samples'] != null) {
192
      // _writeSamplesJson can potentially be long-lived.
193
      await _writeSamplesJson(stringArgDeprecated('list-samples'));
194
      return FlutterCommandResult.success();
195 196
    }

197
    validateOutputDirectoryArg();
198

199 200 201
    String sampleCode;
    if (argResults['sample'] != null) {
      if (argResults['template'] != null &&
202
        stringToProjectType(stringArgDeprecated('template') ?? 'app') != FlutterProjectType.app) {
203
        throwToolExit('Cannot specify --sample with a project type other than '
204
          '"${flutterProjectTypeToString(FlutterProjectType.app)}"');
205 206
      }
      // Fetch the sample from the server.
207
      sampleCode = await _fetchSampleFromServer(stringArgDeprecated('sample'));
208 209
    }

210 211
    final FlutterProjectType template = _getProjectType(projectDir);
    final bool generateModule = template == FlutterProjectType.module;
Daco Harkes's avatar
Daco Harkes committed
212 213
    final bool generateMethodChannelsPlugin = template == FlutterProjectType.plugin;
    final bool generateFfiPlugin = template == FlutterProjectType.ffiPlugin;
214
    final bool generatePackage = template == FlutterProjectType.package;
215

216 217 218 219 220 221 222 223 224 225 226
    final List<String> platforms = stringsArg('platforms');
    // `--platforms` does not support module or package.
    if (argResults.wasParsed('platforms') && (generateModule || generatePackage)) {
      final String template = generateModule ? 'module' : 'package';
      throwToolExit(
        'The "--platforms" argument is not supported in $template template.',
        exitCode: 2
      );
    } else if (platforms == null || platforms.isEmpty) {
      throwToolExit('Must specify at least one platform using --platforms',
        exitCode: 2);
Daco Harkes's avatar
Daco Harkes committed
227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243
    } else if (generateFfiPlugin && argResults.wasParsed('platforms') && platforms.contains('web')) {
      throwToolExit(
        'The web platform is not supported in plugin_ffi template.',
        exitCode: 2,
      );
    } else if (generateFfiPlugin && argResults.wasParsed('ios-language')) {
      throwToolExit(
        'The "ios-language" option is not supported with the plugin_ffi '
        'template: the language will always be C or C++.',
        exitCode: 2,
      );
    } else if (generateFfiPlugin && argResults.wasParsed('android-language')) {
      throwToolExit(
        'The "android-language" option is not supported with the plugin_ffi '
        'template: the language will always be C or C++.',
        exitCode: 2,
      );
244 245
    }

Chris Yang's avatar
Chris Yang committed
246
    final String organization = await getOrganization();
247

248
    final bool overwrite = boolArgDeprecated('overwrite');
Chris Yang's avatar
Chris Yang committed
249
    validateProjectDir(overwrite: overwrite);
250

251
    if (boolArgDeprecated('with-driver-test')) {
252
      globals.printWarning(
253
        'The "--with-driver-test" argument has been deprecated and will no longer add a flutter '
254 255 256 257 258
        'driver template. Instead, learn how to use package:integration_test by '
        'visiting https://pub.dev/packages/integration_test .'
      );
    }

259
    final String dartSdk = globals.cache.dartSdkBuild;
260 261 262 263 264 265 266 267 268 269 270
    final bool includeIos = featureFlags.isIOSEnabled && platforms.contains('ios');
    String developmentTeam;
    if (includeIos) {
      developmentTeam = await getCodeSigningIdentityDevelopmentTeam(
        processManager: globals.processManager,
        platform: globals.platform,
        logger: globals.logger,
        config: globals.config,
        terminal: globals.terminal,
      );
    }
271

272 273 274
    // The dart project_name is in snake_case, this variable is the Title Case of the Project Name.
    final String titleCaseProjectName = snakeCaseToTitleCase(projectName);

275
    final Map<String, Object> templateContext = createTemplateContext(
276
      organization: organization,
277
      projectName: projectName,
278
      titleCaseProjectName: titleCaseProjectName,
279
      projectDescription: stringArgDeprecated('description'),
280
      flutterRoot: flutterRoot,
Daco Harkes's avatar
Daco Harkes committed
281 282
      withPlatformChannelPluginHook: generateMethodChannelsPlugin,
      withFfiPluginHook: generateFfiPlugin,
283 284
      androidLanguage: stringArgDeprecated('android-language'),
      iosLanguage: stringArgDeprecated('ios-language'),
285 286
      iosDevelopmentTeam: developmentTeam,
      ios: includeIos,
287
      android: featureFlags.isAndroidEnabled && platforms.contains('android'),
288 289 290 291
      web: featureFlags.isWebEnabled && platforms.contains('web'),
      linux: featureFlags.isLinuxEnabled && platforms.contains('linux'),
      macos: featureFlags.isMacOSEnabled && platforms.contains('macos'),
      windows: featureFlags.isWindowsEnabled && platforms.contains('windows'),
292
      // Enable null safety everywhere.
293
      dartSdkVersionBounds: "'>=$dartSdk <3.0.0'",
294
      implementationTests: boolArgDeprecated('implementation-tests'),
295 296 297
      agpVersion: gradle.templateAndroidGradlePluginVersion,
      kotlinVersion: gradle.templateKotlinGradlePluginVersion,
      gradleVersion: gradle.templateDefaultGradleVersion,
298
    );
299

300
    final String relativeDirPath = globals.fs.path.relative(projectDirPath);
301 302
    final bool creatingNewProject = !projectDir.existsSync() || projectDir.listSync().isEmpty;
    if (creatingNewProject) {
303
      globals.printStatus('Creating project $relativeDirPath...');
304
    } else {
305
      if (sampleCode != null && !overwrite) {
306 307 308
        throwToolExit('Will not overwrite existing project in $relativeDirPath: '
          'must specify --overwrite for samples to overwrite.');
      }
309
      globals.printStatus('Recreating project $relativeDirPath...');
310
    }
311

312
    final Directory relativeDir = globals.fs.directory(projectDirPath);
313 314
    int generatedFileCount = 0;
    switch (template) {
315
      case FlutterProjectType.app:
316
        generatedFileCount += await generateApp(
Daco Harkes's avatar
Daco Harkes committed
317
          <String>['app', 'app_test_widget'],
318 319 320 321
          relativeDir,
          templateContext,
          overwrite: overwrite,
          printStatusWhenWriting: !creatingNewProject,
322
          projectType: template,
323
        );
324 325
        break;
      case FlutterProjectType.skeleton:
326
        generatedFileCount += await generateApp(
Daco Harkes's avatar
Daco Harkes committed
327
          <String>['skeleton'],
328 329 330 331
          relativeDir,
          templateContext,
          overwrite: overwrite,
          printStatusWhenWriting: !creatingNewProject,
332
          generateMetadata: false,
333
        );
334
        break;
335
      case FlutterProjectType.module:
336 337 338 339 340 341
        generatedFileCount += await _generateModule(
          relativeDir,
          templateContext,
          overwrite: overwrite,
          printStatusWhenWriting: !creatingNewProject,
        );
342
        break;
343
      case FlutterProjectType.package:
344 345 346 347 348 349
        generatedFileCount += await _generatePackage(
          relativeDir,
          templateContext,
          overwrite: overwrite,
          printStatusWhenWriting: !creatingNewProject,
        );
350
        break;
351
      case FlutterProjectType.plugin:
Daco Harkes's avatar
Daco Harkes committed
352 353 354 355 356
        generatedFileCount += await _generateMethodChannelPlugin(
          relativeDir,
          templateContext,
          overwrite: overwrite,
          printStatusWhenWriting: !creatingNewProject,
357
          projectType: template,
Daco Harkes's avatar
Daco Harkes committed
358 359 360 361
        );
        break;
      case FlutterProjectType.ffiPlugin:
        generatedFileCount += await _generateFfiPlugin(
362 363 364 365
          relativeDir,
          templateContext,
          overwrite: overwrite,
          printStatusWhenWriting: !creatingNewProject,
366
          projectType: template,
367
        );
368 369
        break;
    }
370
    if (sampleCode != null) {
371
      generatedFileCount += _applySample(relativeDir, sampleCode);
372
    }
373 374
    globals.printStatus('Wrote $generatedFileCount files.');
    globals.printStatus('\nAll done!');
375
    final String application = sampleCode != null ? 'sample application' : 'application';
376
    if (generatePackage) {
377
      final String relativeMainPath = globals.fs.path.normalize(globals.fs.path.join(
378 379 380 381
        relativeDirPath,
        'lib',
        '${templateContext['projectName']}.dart',
      ));
382
      globals.printStatus('Your package code is in $relativeMainPath');
383
    } else if (generateModule) {
384
      final String relativeMainPath = globals.fs.path.normalize(globals.fs.path.join(
385 386 387 388
          relativeDirPath,
          'lib',
          'main.dart',
      ));
389
      globals.printStatus('Your module code is in $relativeMainPath.');
Daco Harkes's avatar
Daco Harkes committed
390
    } else if (generateMethodChannelsPlugin) {
391 392 393 394 395 396
      final String relativePluginPath = globals.fs.path.normalize(globals.fs.path.relative(projectDirPath));
      final List<String> requestedPlatforms = _getUserRequestedPlatforms();
      final String platformsString = requestedPlatforms.join(', ');
      _printPluginDirectoryLocationMessage(relativePluginPath, projectName, platformsString);
      if (!creatingNewProject && requestedPlatforms.isNotEmpty) {
        _printPluginUpdatePubspecMessage(relativePluginPath, platformsString);
397
      } else if (_getSupportedPlatformsInPlugin(projectDir).isEmpty) {
398
        _printNoPluginMessage();
399
      }
400 401 402 403 404 405

      final List<String> platformsToWarn = _getPlatformWarningList(requestedPlatforms);
      if (platformsToWarn.isNotEmpty) {
        _printWarningDisabledPlatform(platformsToWarn);
      }
      _printPluginAddPlatformMessage(relativePluginPath);
406
    } else  {
407
      // Tell the user the next steps.
408
      final FlutterProject project = FlutterProject.fromDirectory(globals.fs.directory(projectDirPath));
409
      final FlutterProject app = project.hasExampleApp ? project.example : project;
410 411
      final String relativeAppPath = globals.fs.path.normalize(globals.fs.path.relative(app.directory.path));
      final String relativeAppMain = globals.fs.path.join(relativeAppPath, 'lib', 'main.dart');
412
      final List<String> requestedPlatforms = _getUserRequestedPlatforms();
413 414 415

      // Let them know a summary of the state of their tooling.
      globals.printStatus('''
416
In order to run your $application, type:
417 418 419 420

  \$ cd $relativeAppPath
  \$ flutter run

421
Your $application code is in $relativeAppMain.
422
''');
423 424 425 426 427
      // Show warning if any selected platform is not enabled
      final List<String> platformsToWarn = _getPlatformWarningList(requestedPlatforms);
      if (platformsToWarn.isNotEmpty) {
        _printWarningDisabledPlatform(platformsToWarn);
      }
428
    }
429

430
    return FlutterCommandResult.success();
431
  }
432

433 434 435 436 437 438
  Future<int> _generateModule(
    Directory directory,
    Map<String, dynamic> templateContext, {
    bool overwrite = false,
    bool printStatusWhenWriting = true,
  }) async {
439 440
    int generatedCount = 0;
    final String description = argResults.wasParsed('description')
441
        ? stringArgDeprecated('description')
442
        : 'A new Flutter module project.';
443
    templateContext['description'] = description;
444 445 446 447 448 449 450
    generatedCount += await renderTemplate(
      globals.fs.path.join('module', 'common'),
      directory,
      templateContext,
      overwrite: overwrite,
      printStatusWhenWriting: printStatusWhenWriting,
    );
451
    if (boolArgDeprecated('pub')) {
452
      await pub.get(
453
        context: PubContext.create,
454
        directory: directory.path,
455
        offline: boolArgDeprecated('offline'),
456
        generateSyntheticPackage: false,
457
      );
458
      final FlutterProject project = FlutterProject.fromDirectory(directory);
459 460 461 462
      await project.ensureReadyForPlatformSpecificTooling(
        androidPlatform: true,
        iosPlatform: true,
      );
463 464 465 466
    }
    return generatedCount;
  }

467 468 469 470 471 472
  Future<int> _generatePackage(
    Directory directory,
    Map<String, dynamic> templateContext, {
    bool overwrite = false,
    bool printStatusWhenWriting = true,
  }) async {
473 474
    int generatedCount = 0;
    final String description = argResults.wasParsed('description')
475
        ? stringArgDeprecated('description')
476
        : 'A new Flutter package project.';
477
    templateContext['description'] = description;
478 479 480 481 482 483 484
    generatedCount += await renderTemplate(
      'package',
      directory,
      templateContext,
      overwrite: overwrite,
      printStatusWhenWriting: printStatusWhenWriting,
    );
485
    if (boolArgDeprecated('pub')) {
486
      await pub.get(
487
        context: PubContext.createPackage,
488
        directory: directory.path,
489
        offline: boolArgDeprecated('offline'),
490
        generateSyntheticPackage: false,
491
      );
492
    }
493 494
    return generatedCount;
  }
495

Daco Harkes's avatar
Daco Harkes committed
496
  Future<int> _generateMethodChannelPlugin(
497 498 499 500
    Directory directory,
    Map<String, dynamic> templateContext, {
    bool overwrite = false,
    bool printStatusWhenWriting = true,
501
    FlutterProjectType projectType,
502
  }) async {
503
    // Plugins only add a platform if it was requested explicitly by the user.
504
    if (!argResults.wasParsed('platforms')) {
505 506 507
      for (final String platform in kAllCreatePlatforms) {
        templateContext[platform] = false;
      }
508
    }
509 510
    final List<String> platformsToAdd = _getSupportedPlatformsFromTemplateContext(templateContext);

511 512 513 514
    final List<String> existingPlatforms = _getSupportedPlatformsInPlugin(directory);
    for (final String existingPlatform in existingPlatforms) {
      // re-generate files for existing platforms
      templateContext[existingPlatform] = true;
515 516 517
    }

    final bool willAddPlatforms = platformsToAdd.isNotEmpty;
518
    templateContext['no_platforms'] = !willAddPlatforms;
519 520
    int generatedCount = 0;
    final String description = argResults.wasParsed('description')
521
        ? stringArgDeprecated('description')
522
        : 'A new Flutter plugin project.';
523
    templateContext['description'] = description;
Daco Harkes's avatar
Daco Harkes committed
524 525
    generatedCount += await renderMerged(
      <String>['plugin', 'plugin_shared'],
526 527 528 529 530
      directory,
      templateContext,
      overwrite: overwrite,
      printStatusWhenWriting: printStatusWhenWriting,
    );
531

532
    if (boolArgDeprecated('pub')) {
533
      await pub.get(
534
        context: PubContext.createPlugin,
535
        directory: directory.path,
536
        offline: boolArgDeprecated('offline'),
537
        generateSyntheticPackage: false,
538 539
      );
    }
540

541
    final FlutterProject project = FlutterProject.fromDirectory(directory);
542 543 544 545 546
    final bool generateAndroid = templateContext['android'] == true;
    if (generateAndroid) {
      gradle.updateLocalProperties(
        project: project, requireAndroidSdk: false);
    }
547

548 549 550
    final String projectName = templateContext['projectName'] as String;
    final String organization = templateContext['organization'] as String;
    final String androidPluginIdentifier = templateContext['androidIdentifier'] as String;
551
    final String exampleProjectName = '${projectName}_example';
552
    templateContext['projectName'] = exampleProjectName;
553 554 555 556
    templateContext['androidIdentifier'] = CreateBase.createAndroidIdentifier(organization, exampleProjectName);
    templateContext['iosIdentifier'] = CreateBase.createUTIIdentifier(organization, exampleProjectName);
    templateContext['macosIdentifier'] = CreateBase.createUTIIdentifier(organization, exampleProjectName);
    templateContext['windowsIdentifier'] = CreateBase.createWindowsIdentifier(organization, exampleProjectName);
557 558 559 560
    templateContext['description'] = 'Demonstrates how to use the $projectName plugin.';
    templateContext['pluginProjectName'] = projectName;
    templateContext['androidPluginIdentifier'] = androidPluginIdentifier;

561
    generatedCount += await generateApp(
Daco Harkes's avatar
Daco Harkes committed
562 563 564 565 566 567
      <String>['app', 'app_test_widget'],
      project.example.directory,
      templateContext,
      overwrite: overwrite,
      pluginExampleApp: true,
      printStatusWhenWriting: printStatusWhenWriting,
568
      projectType: projectType,
Daco Harkes's avatar
Daco Harkes committed
569 570 571 572 573 574 575 576 577
    );
    return generatedCount;
  }

  Future<int> _generateFfiPlugin(
    Directory directory,
    Map<String, dynamic> templateContext, {
    bool overwrite = false,
    bool printStatusWhenWriting = true,
578
    FlutterProjectType projectType,
Daco Harkes's avatar
Daco Harkes committed
579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599
  }) async {
    // Plugins only add a platform if it was requested explicitly by the user.
    if (!argResults.wasParsed('platforms')) {
      for (final String platform in kAllCreatePlatforms) {
        templateContext[platform] = false;
      }
    }
    final List<String> platformsToAdd =
        _getSupportedPlatformsFromTemplateContext(templateContext);

    final List<String> existingPlatforms =
        _getSupportedPlatformsInPlugin(directory);
    for (final String existingPlatform in existingPlatforms) {
      // re-generate files for existing platforms
      templateContext[existingPlatform] = true;
    }

    final bool willAddPlatforms = platformsToAdd.isNotEmpty;
    templateContext['no_platforms'] = !willAddPlatforms;
    int generatedCount = 0;
    final String description = argResults.wasParsed('description')
600
        ? stringArgDeprecated('description')
Daco Harkes's avatar
Daco Harkes committed
601 602 603 604 605 606 607 608 609 610
        : 'A new Flutter FFI plugin project.';
    templateContext['description'] = description;
    generatedCount += await renderMerged(
      <String>['plugin_ffi', 'plugin_shared'],
      directory,
      templateContext,
      overwrite: overwrite,
      printStatusWhenWriting: printStatusWhenWriting,
    );

611
    if (boolArgDeprecated('pub')) {
Daco Harkes's avatar
Daco Harkes committed
612 613 614
      await pub.get(
        context: PubContext.createPlugin,
        directory: directory.path,
615
        offline: boolArgDeprecated('offline'),
Daco Harkes's avatar
Daco Harkes committed
616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640
        generateSyntheticPackage: false,
      );
    }

    final FlutterProject project = FlutterProject.fromDirectory(directory);
    final bool generateAndroid = templateContext['android'] == true;
    if (generateAndroid) {
      gradle.updateLocalProperties(project: project, requireAndroidSdk: false);
    }

    final String projectName = templateContext['projectName'] as String;
    final String organization = templateContext['organization'] as String;
    final String androidPluginIdentifier = templateContext['androidIdentifier'] as String;
    final String exampleProjectName = '${projectName}_example';
    templateContext['projectName'] = exampleProjectName;
    templateContext['androidIdentifier'] = CreateBase.createAndroidIdentifier(organization, exampleProjectName);
    templateContext['iosIdentifier'] = CreateBase.createUTIIdentifier(organization, exampleProjectName);
    templateContext['macosIdentifier'] = CreateBase.createUTIIdentifier(organization, exampleProjectName);
    templateContext['windowsIdentifier'] = CreateBase.createWindowsIdentifier(organization, exampleProjectName);
    templateContext['description'] = 'Demonstrates how to use the $projectName plugin.';
    templateContext['pluginProjectName'] = projectName;
    templateContext['androidPluginIdentifier'] = androidPluginIdentifier;

    generatedCount += await generateApp(
      <String>['app'],
641 642 643 644 645
      project.example.directory,
      templateContext,
      overwrite: overwrite,
      pluginExampleApp: true,
      printStatusWhenWriting: printStatusWhenWriting,
646
      projectType: projectType,
647
    );
648
    return generatedCount;
Hixie's avatar
Hixie committed
649
  }
650

651 652 653 654
  // Takes an application template and replaces the main.dart with one from the
  // documentation website in sampleCode.  Returns the difference in the number
  // of files after applying the sample, since it also deletes the application's
  // test directory (since the template's test doesn't apply to the sample).
655
  int _applySample(Directory directory, String sampleCode) {
656
    final File mainDartFile = directory.childDirectory('lib').childFile('main.dart');
657 658
    mainDartFile.createSync(recursive: true);
    mainDartFile.writeAsStringSync(sampleCode);
659 660
    final Directory testDir = directory.childDirectory('test');
    final List<FileSystemEntity> files = testDir.listSync(recursive: true);
661
    testDir.deleteSync(recursive: true);
662 663 664
    return -files.length;
  }

665 666
  List<String> _getSupportedPlatformsFromTemplateContext(Map<String, dynamic> templateContext) {
    return <String>[
667
      for (String platform in kAllCreatePlatforms)
668
        if (templateContext[platform] == true) platform,
669 670
    ];
  }
671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689

  // Returns a list of platforms that are explicitly requested by user via `--platforms`.
  List<String> _getUserRequestedPlatforms() {
    if (!argResults.wasParsed('platforms')) {
      return <String>[];
    }
    return stringsArg('platforms');
  }
}


// Determine what platforms are supported based on generated files.
List<String> _getSupportedPlatformsInPlugin(Directory projectDir) {
  final String pubspecPath = globals.fs.path.join(projectDir.absolute.path, 'pubspec.yaml');
  final FlutterManifest manifest = FlutterManifest.createFromPath(pubspecPath, fileSystem: globals.fs, logger: globals.logger);
  final List<String> platforms = manifest.validSupportedPlatforms == null
    ? <String>[]
    : manifest.validSupportedPlatforms.keys.toList();
  return platforms;
690
}
691 692 693 694 695 696 697 698

void _printPluginDirectoryLocationMessage(String pluginPath, String projectName, String platformsString) {
  final String relativePluginMain = globals.fs.path.join(pluginPath, 'lib', '$projectName.dart');
  final String relativeExampleMain = globals.fs.path.join(pluginPath, 'example', 'lib', 'main.dart');
  globals.printStatus('''

Your plugin code is in $relativePluginMain.

699
Your example app code is in $relativeExampleMain.
700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716

''');
  if (platformsString != null && platformsString.isNotEmpty) {
    globals.printStatus('''
Host platform code is in the $platformsString directories under $pluginPath.
To edit platform code in an IDE see https://flutter.dev/developing-packages/#edit-plugin-package.

    ''');
  }
}

void _printPluginUpdatePubspecMessage(String pluginPath, String platformsString) {
  globals.printStatus('''
You need to update $pluginPath/pubspec.yaml to support $platformsString.
''', emphasis: true, color: TerminalColor.red);
}

717 718 719 720 721 722
void _printNoPluginMessage() {
    globals.printError('''
You've created a plugin project that doesn't yet support any platforms.
''');
}

723
void _printPluginAddPlatformMessage(String pluginPath) {
724 725 726
  globals.printStatus('''
To add platforms, run `flutter create -t plugin --platforms <platforms> .` under $pluginPath.
For more information, see https://flutter.dev/go/plugin-platforms.
727

728 729
''');
}
730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774

// returns a list disabled, but requested platforms
List<String> _getPlatformWarningList(List<String> requestedPlatforms) {
  final List<String> platformsToWarn = <String>[
  if (requestedPlatforms.contains('web') && !featureFlags.isWebEnabled)
    'web',
  if (requestedPlatforms.contains('macos') && !featureFlags.isMacOSEnabled)
    'macos',
  if (requestedPlatforms.contains('windows') && !featureFlags.isWindowsEnabled)
    'windows',
  if (requestedPlatforms.contains('linux') && !featureFlags.isLinuxEnabled)
    'linux',
  ];

  return platformsToWarn;
}

void _printWarningDisabledPlatform(List<String> platforms) {
  final List<String> desktop = <String>[];
  final List<String> web = <String>[];

  for (final String platform in platforms) {
    if (platform == 'web') {
      web.add(platform);
    } else if (platform == 'macos' || platform == 'windows' || platform == 'linux') {
      desktop.add(platform);
    }
  }

  if (desktop.isNotEmpty) {
    final String platforms = desktop.length > 1 ? 'platforms' : 'platform';
    final String verb = desktop.length > 1 ? 'are' : 'is';

    globals.printStatus('''
The desktop $platforms: ${desktop.join(', ')} $verb currently not supported on your local environment.
For more details, see: https://flutter.dev/desktop
''');
  }
  if (web.isNotEmpty) {
    globals.printStatus('''
The web is currently not supported on your local environment.
For more details, see: https://flutter.dev/docs/get-started/web
''');
  }
}