create.dart 42.2 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
import 'package:meta/meta.dart';

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 15
import '../base/version.dart';
import '../base/version_range.dart';
16
import '../convert.dart';
17
import '../dart/pub.dart';
18
import '../features.dart';
19
import '../flutter_manifest.dart';
20
import '../flutter_project_metadata.dart';
21
import '../globals.dart' as globals;
22
import '../ios/code_signing.dart';
23
import '../project.dart';
24
import '../reporting/reporting.dart';
25
import '../runner/flutter_command.dart';
26
import 'create_base.dart';
27

28 29 30 31 32 33 34
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.';

35
class CreateCommand extends CreateBase {
36
  CreateCommand({
37 38
    super.verboseHelp = false,
  }) {
39
    addPlatformsOptions(customHelp: kPlatformHelp);
40 41 42
    argParser.addOption(
      'template',
      abbr: 't',
43 44
      allowed: FlutterProjectType.enabledValues
          .map<String>((FlutterProjectType e) => e.cliName),
45 46
      help: 'Specify the type of project to create.',
      valueHelp: 'type',
47
      allowedHelp: CliEnum.allowedHelp(FlutterProjectType.enabledValues),
48
    );
49 50 51
    argParser.addOption(
      'sample',
      abbr: 's',
52 53
      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 '
54
        'documentation website (https://api.flutter.dev/). An example can be found at: '
55
        'https://api.flutter.dev/flutter/widgets/SingleChildScrollView-class.html',
56
      valueHelp: 'id',
57
    );
58 59 60 61 62 63
    argParser.addFlag(
      'empty',
      abbr: 'e',
      help: 'Specifies creating using an application template with a main.dart that is minimal, '
        'including no comments, as a starting point for a new application. Implies "--template=app".',
    );
64 65 66
    argParser.addOption(
      'list-samples',
      help: 'Specifies a JSON output file for a listing of Flutter code samples '
67
        'that can be created with "--sample".',
68 69
      valueHelp: 'path',
    );
70 71
  }

72 73 74 75 76 77 78
  @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.';

79 80 81
  @override
  String get category => FlutterCommandCategory.project;

82
  @override
83
  String get invocation => '${runner?.executableName} $name <output directory>';
84

85
  @override
86 87
  Future<CustomDimensions> get usageValues async {
    return CustomDimensions(
88 89 90
      commandCreateProjectType: stringArg('template'),
      commandCreateAndroidLanguage: stringArg('android-language'),
      commandCreateIosLanguage: stringArg('ios-language'),
91
    );
92 93
  }

94
  // Lazy-initialize the net utilities with values from the context.
95
  late final Net _net = Net(
96
    httpClientFactory: context.get<HttpClientFactory>(),
97 98 99 100
    logger: globals.logger,
    platform: globals.platform,
  );

101
  /// The hostname for the Flutter docs for the current channel.
102
  String get _snippetsHost => globals.flutterVersion.channel == 'stable'
103
        ? 'api.flutter.dev'
104
        : 'main-api.flutter.dev';
105

106
  Future<String?> _fetchSampleFromServer(String sampleId) async {
107 108 109 110 111 112
    // 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.');
    }

113
    final Uri snippetsUri = Uri.https(_snippetsHost, 'snippets/$sampleId.dart');
114
    final List<int>? data = await _net.fetchUrl(snippetsUri);
115 116 117 118
    if (data == null || data.isEmpty) {
      return null;
    }
    return utf8.decode(data);
119 120 121
  }

  /// Fetches the samples index file from the Flutter docs website.
122
  Future<String?> _fetchSamplesIndexFromServer() async {
123
    final Uri snippetsUri = Uri.https(_snippetsHost, 'snippets/index.json');
124
    final List<int>? data = await _net.fetchUrl(snippetsUri, maxAttempts: 2);
125 126 127 128
    if (data == null || data.isEmpty) {
      return null;
    }
    return utf8.decode(data);
129 130 131 132 133 134
  }

  /// Fetches the samples index file from the server and writes it to
  /// [outputFilePath].
  Future<void> _writeSamplesJson(String outputFilePath) async {
    try {
135
      final File outputFile = globals.fs.file(outputFilePath);
136 137 138
      if (outputFile.existsSync()) {
        throwToolExit('File "$outputFilePath" already exists', exitCode: 1);
      }
139
      final String? samplesJson = await _fetchSamplesIndexFromServer();
140 141
      if (samplesJson == null) {
        throwToolExit('Unable to download samples', exitCode: 2);
142
      } else {
143
        outputFile.writeAsStringSync(samplesJson);
144
        globals.printStatus('Wrote samples JSON to "$outputFilePath"');
145
      }
146
    } on Exception catch (e) {
147 148
      throwToolExit('Failed to write samples JSON to "$outputFilePath": $e', exitCode: 2);
    }
149 150
  }

151
  FlutterProjectType _getProjectType(Directory projectDir) {
152 153
    FlutterProjectType? template;
    FlutterProjectType? detectedProjectType;
154
    final bool metadataExists = projectDir.absolute.childFile('.metadata').existsSync();
155 156
    final String? templateArgument = stringArg('template');
    if (templateArgument != null) {
157
      template = FlutterProjectType.fromCliName(templateArgument);
Daco Harkes's avatar
Daco Harkes committed
158 159 160 161 162 163 164 165 166 167 168
    }
    // 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.');
169 170
      }
    }
171
    template ??= detectedProjectType ?? FlutterProjectType.app;
172 173 174
    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.
175 176
      throwToolExit("The requested template type '${template.cliName}' doesn't match the "
          "existing template type of '${detectedProjectType.cliName}'.");
177 178 179 180
    }
    return template;
  }

181
  @override
182
  Future<FlutterCommandResult> runCommand() async {
183 184
    final String? listSamples = stringArg('list-samples');
    if (listSamples != null) {
185
      // _writeSamplesJson can potentially be long-lived.
186
      await _writeSamplesJson(listSamples);
187
      return FlutterCommandResult.success();
188 189
    }

190 191 192 193 194 195 196
    if (argResults!.wasParsed('empty') && argResults!.wasParsed('sample')) {
      throwToolExit(
        'Only one of --empty or --sample may be specified, not both.',
        exitCode: 2,
      );
    }

197
    validateOutputDirectoryArg();
198 199
    String? sampleCode;
    final String? sampleArgument = stringArg('sample');
200
    final bool emptyArgument = boolArg('empty');
201 202
    if (sampleArgument != null) {
      final String? templateArgument = stringArg('template');
203
      if (templateArgument != null && FlutterProjectType.fromCliName(templateArgument) != FlutterProjectType.app) {
204
        throwToolExit('Cannot specify --sample with a project type other than '
205
          '"${FlutterProjectType.app.cliName}"');
206 207
      }
      // Fetch the sample from the server.
208
      sampleCode = await _fetchSampleFromServer(sampleArgument);
209 210
    }

211 212
    final FlutterProjectType template = _getProjectType(projectDir);
    final bool generateModule = template == FlutterProjectType.module;
Daco Harkes's avatar
Daco Harkes committed
213
    final bool generateMethodChannelsPlugin = template == FlutterProjectType.plugin;
214
    final bool generateFfiPackage = template == FlutterProjectType.packageFfi;
215
    final bool generateFfiPlugin = template == FlutterProjectType.pluginFfi;
216
    final bool generateFfi = generateFfiPlugin || generateFfiPackage;
217
    final bool generatePackage = template == FlutterProjectType.package;
218

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

Chris Yang's avatar
Chris Yang committed
249
    final String organization = await getOrganization();
250

251
    final bool overwrite = boolArg('overwrite');
Chris Yang's avatar
Chris Yang committed
252
    validateProjectDir(overwrite: overwrite);
253

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

262
    final String dartSdk = globals.cache.dartSdkBuild;
263 264 265 266 267 268 269 270 271 272 273 274 275 276
    final bool includeIos;
    final bool includeAndroid;
    final bool includeWeb;
    final bool includeLinux;
    final bool includeMacos;
    final bool includeWindows;
    if (template == FlutterProjectType.module) {
      // The module template only supports iOS and Android.
      includeIos = true;
      includeAndroid = true;
      includeWeb = false;
      includeLinux = false;
      includeMacos = false;
      includeWindows = false;
277 278 279 280 281 282 283 284
    } else if (template == FlutterProjectType.package) {
      // The package template does not supports any platform.
      includeIos = false;
      includeAndroid = false;
      includeWeb = false;
      includeLinux = false;
      includeMacos = false;
      includeWindows = false;
285 286 287 288 289 290 291 292 293
    } else {
      includeIos = featureFlags.isIOSEnabled && platforms.contains('ios');
      includeAndroid = featureFlags.isAndroidEnabled && platforms.contains('android');
      includeWeb = featureFlags.isWebEnabled && platforms.contains('web');
      includeLinux = featureFlags.isLinuxEnabled && platforms.contains('linux');
      includeMacos = featureFlags.isMacOSEnabled && platforms.contains('macos');
      includeWindows = featureFlags.isWindowsEnabled && platforms.contains('windows');
    }

294
    String? developmentTeam;
295 296 297 298 299 300 301 302 303
    if (includeIos) {
      developmentTeam = await getCodeSigningIdentityDevelopmentTeam(
        processManager: globals.processManager,
        platform: globals.platform,
        logger: globals.logger,
        config: globals.config,
        terminal: globals.terminal,
      );
    }
304

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

308
    final Map<String, Object?> templateContext = createTemplateContext(
309
      organization: organization,
310
      projectName: projectName,
311
      titleCaseProjectName: titleCaseProjectName,
312
      projectDescription: stringArg('description'),
313
      flutterRoot: flutterRoot,
Daco Harkes's avatar
Daco Harkes committed
314 315
      withPlatformChannelPluginHook: generateMethodChannelsPlugin,
      withFfiPluginHook: generateFfiPlugin,
316
      withFfiPackage: generateFfiPackage,
317
      withEmptyMain: emptyArgument,
318 319
      androidLanguage: stringArg('android-language'),
      iosLanguage: stringArg('ios-language'),
320 321
      iosDevelopmentTeam: developmentTeam,
      ios: includeIos,
322 323 324 325 326
      android: includeAndroid,
      web: includeWeb,
      linux: includeLinux,
      macos: includeMacos,
      windows: includeWindows,
327
      dartSdkVersionBounds: "'>=$dartSdk <4.0.0'",
328
      implementationTests: boolArg('implementation-tests'),
329 330 331
      agpVersion: gradle.templateAndroidGradlePluginVersion,
      kotlinVersion: gradle.templateKotlinGradlePluginVersion,
      gradleVersion: gradle.templateDefaultGradleVersion,
332
    );
333

334
    final String relativeDirPath = globals.fs.path.relative(projectDirPath);
335 336
    final bool creatingNewProject = !projectDir.existsSync() || projectDir.listSync().isEmpty;
    if (creatingNewProject) {
337
      globals.printStatus('Creating project $relativeDirPath...');
338
    } else {
339
      if (sampleCode != null && !overwrite) {
340 341 342
        throwToolExit('Will not overwrite existing project in $relativeDirPath: '
          'must specify --overwrite for samples to overwrite.');
      }
343
      globals.printStatus('Recreating project $relativeDirPath...');
344
    }
345

346
    final Directory relativeDir = globals.fs.directory(projectDirPath);
347
    int generatedFileCount = 0;
348
    final PubContext pubContext;
349
    switch (template) {
350
      case FlutterProjectType.app:
351
        generatedFileCount += await generateApp(
Daco Harkes's avatar
Daco Harkes committed
352
          <String>['app', 'app_test_widget'],
353 354 355 356
          relativeDir,
          templateContext,
          overwrite: overwrite,
          printStatusWhenWriting: !creatingNewProject,
357
          projectType: template,
358
        );
359
        pubContext = PubContext.create;
360
      case FlutterProjectType.skeleton:
361
        generatedFileCount += await generateApp(
Daco Harkes's avatar
Daco Harkes committed
362
          <String>['skeleton'],
363 364 365 366
          relativeDir,
          templateContext,
          overwrite: overwrite,
          printStatusWhenWriting: !creatingNewProject,
367
          generateMetadata: false,
368
        );
369
        pubContext = PubContext.create;
370
      case FlutterProjectType.module:
371 372 373 374 375 376
        generatedFileCount += await _generateModule(
          relativeDir,
          templateContext,
          overwrite: overwrite,
          printStatusWhenWriting: !creatingNewProject,
        );
377
        pubContext = PubContext.create;
378
      case FlutterProjectType.package:
379 380 381 382 383 384
        generatedFileCount += await _generatePackage(
          relativeDir,
          templateContext,
          overwrite: overwrite,
          printStatusWhenWriting: !creatingNewProject,
        );
385
        pubContext = PubContext.createPackage;
386
      case FlutterProjectType.plugin:
Daco Harkes's avatar
Daco Harkes committed
387 388 389 390 391
        generatedFileCount += await _generateMethodChannelPlugin(
          relativeDir,
          templateContext,
          overwrite: overwrite,
          printStatusWhenWriting: !creatingNewProject,
392
          projectType: template,
Daco Harkes's avatar
Daco Harkes committed
393
        );
394
        pubContext = PubContext.createPlugin;
395
      case FlutterProjectType.pluginFfi:
Daco Harkes's avatar
Daco Harkes committed
396
        generatedFileCount += await _generateFfiPlugin(
397 398 399 400
          relativeDir,
          templateContext,
          overwrite: overwrite,
          printStatusWhenWriting: !creatingNewProject,
401
          projectType: template,
402
        );
403
        pubContext = PubContext.createPlugin;
404 405 406 407 408 409 410 411 412
      case FlutterProjectType.packageFfi:
        generatedFileCount += await _generateFfiPackage(
          relativeDir,
          templateContext,
          overwrite: overwrite,
          printStatusWhenWriting: !creatingNewProject,
          projectType: template,
        );
        pubContext = PubContext.createPackage;
413
    }
414

415
    if (boolArg('pub')) {
416 417 418 419
      final FlutterProject project = FlutterProject.fromDirectory(relativeDir);
      await pub.get(
        context: pubContext,
        project: project,
420
        offline: boolArg('offline'),
421
        outputMode: PubOutputMode.summaryOnly,
422
      );
423 424 425 426 427 428 429 430 431 432 433 434 435 436 437
      // Setting `includeIos` etc to false as with FlutterProjectType.package
      // causes the example sub directory to not get os sub directories.
      // This will lead to `flutter build ios` to fail in the example.
      // TODO(dacoharkes): Uncouple the app and parent project platforms. https://github.com/flutter/flutter/issues/133874
      // Then this if can be removed.
      if (!generateFfiPackage) {
        await project.ensureReadyForPlatformSpecificTooling(
          androidPlatform: includeAndroid,
          iosPlatform: includeIos,
          linuxPlatform: includeLinux,
          macOSPlatform: includeMacos,
          windowsPlatform: includeWindows,
          webPlatform: includeWeb,
        );
      }
438
    }
439
    if (sampleCode != null) {
440 441 442 443
      _applySample(relativeDir, sampleCode);
    }
    if (sampleCode != null || emptyArgument) {
      generatedFileCount += _removeTestDir(relativeDir);
444
    }
445 446
    globals.printStatus('Wrote $generatedFileCount files.');
    globals.printStatus('\nAll done!');
447
    final String application =
448
      '${emptyArgument ? 'empty ' : ''}${sampleCode != null ? 'sample ' : ''}application';
449
    if (generatePackage) {
450
      final String relativeMainPath = globals.fs.path.normalize(globals.fs.path.join(
451 452 453 454
        relativeDirPath,
        'lib',
        '${templateContext['projectName']}.dart',
      ));
455
      globals.printStatus('Your package code is in $relativeMainPath');
456
    } else if (generateModule) {
457
      final String relativeMainPath = globals.fs.path.normalize(globals.fs.path.join(
458 459 460 461
          relativeDirPath,
          'lib',
          'main.dart',
      ));
462
      globals.printStatus('Your module code is in $relativeMainPath.');
463
    } else if (generateMethodChannelsPlugin || generateFfiPlugin) {
464 465 466 467 468 469
      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);
470
      } else if (_getSupportedPlatformsInPlugin(projectDir).isEmpty) {
471
        _printNoPluginMessage();
472
      }
473 474 475 476 477

      final List<String> platformsToWarn = _getPlatformWarningList(requestedPlatforms);
      if (platformsToWarn.isNotEmpty) {
        _printWarningDisabledPlatform(platformsToWarn);
      }
478 479
      final String template = generateMethodChannelsPlugin ? 'plugin' : 'plugin_ffi';
      _printPluginAddPlatformMessage(relativePluginPath, template);
480
    } else  {
481
      // Tell the user the next steps.
482
      final FlutterProject project = FlutterProject.fromDirectory(globals.fs.directory(projectDirPath));
483
      final FlutterProject app = project.hasExampleApp ? project.example : project;
484 485
      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');
486
      final List<String> requestedPlatforms = _getUserRequestedPlatforms();
487 488 489

      // Let them know a summary of the state of their tooling.
      globals.printStatus('''
490 491 492 493
You can find general documentation for Flutter at: https://docs.flutter.dev/
Detailed API documentation is available at: https://api.flutter.dev/
If you prefer video documentation, consider: https://www.youtube.com/c/flutterdev

494
In order to run your $application, type:
495 496 497 498

  \$ cd $relativeAppPath
  \$ flutter run

499
Your $application code is in $relativeAppMain.
500
''');
501 502 503 504 505
      // Show warning if any selected platform is not enabled
      final List<String> platformsToWarn = _getPlatformWarningList(requestedPlatforms);
      if (platformsToWarn.isNotEmpty) {
        _printWarningDisabledPlatform(platformsToWarn);
      }
506
    }
507

508 509 510 511 512 513 514 515 516 517 518 519 520
    // Show warning for Java/AGP or Java/Gradle incompatibility if building for
    // Android and Java version has been detected.
    if (includeAndroid && globals.java?.version != null) {
      _printIncompatibleJavaAgpGradleVersionsWarning(
        javaVersion: versionToParsableString(globals.java?.version)!,
        templateGradleVersion: templateContext['gradleVersion']! as String,
        templateAgpVersion: templateContext['agpVersion']! as String,
        templateAgpVersionForModule: templateContext['agpVersionForModule']! as String,
        projectType: template,
        projectDirPath: projectDirPath,
      );
    }

521
    return FlutterCommandResult.success();
522
  }
523

524 525
  Future<int> _generateModule(
    Directory directory,
526
    Map<String, Object?> templateContext, {
527 528 529
    bool overwrite = false,
    bool printStatusWhenWriting = true,
  }) async {
530
    int generatedCount = 0;
531
    final String? description = argResults!.wasParsed('description')
532
        ? stringArg('description')
533
        : 'A new Flutter module project.';
534
    templateContext['description'] = description;
535 536 537 538 539 540 541
    generatedCount += await renderTemplate(
      globals.fs.path.join('module', 'common'),
      directory,
      templateContext,
      overwrite: overwrite,
      printStatusWhenWriting: printStatusWhenWriting,
    );
542 543 544
    return generatedCount;
  }

545 546
  Future<int> _generatePackage(
    Directory directory,
547
    Map<String, Object?> templateContext, {
548 549 550
    bool overwrite = false,
    bool printStatusWhenWriting = true,
  }) async {
551
    int generatedCount = 0;
552
    final String? description = argResults!.wasParsed('description')
553
        ? stringArg('description')
554
        : 'A new Flutter package project.';
555
    templateContext['description'] = description;
556 557 558 559 560 561 562
    generatedCount += await renderTemplate(
      'package',
      directory,
      templateContext,
      overwrite: overwrite,
      printStatusWhenWriting: printStatusWhenWriting,
    );
563 564
    return generatedCount;
  }
565

Daco Harkes's avatar
Daco Harkes committed
566
  Future<int> _generateMethodChannelPlugin(
567
    Directory directory,
568
    Map<String, Object?> templateContext, {
569 570
    bool overwrite = false,
    bool printStatusWhenWriting = true,
571
    required FlutterProjectType projectType,
572
  }) async {
573
    // Plugins only add a platform if it was requested explicitly by the user.
574
    if (!argResults!.wasParsed('platforms')) {
575 576 577
      for (final String platform in kAllCreatePlatforms) {
        templateContext[platform] = false;
      }
578
    }
579 580
    final List<String> platformsToAdd = _getSupportedPlatformsFromTemplateContext(templateContext);

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

    final bool willAddPlatforms = platformsToAdd.isNotEmpty;
588
    templateContext['no_platforms'] = !willAddPlatforms;
589
    int generatedCount = 0;
590
    final String? description = argResults!.wasParsed('description')
591
        ? stringArg('description')
592
        : 'A new Flutter plugin project.';
593
    templateContext['description'] = description;
Daco Harkes's avatar
Daco Harkes committed
594 595
    generatedCount += await renderMerged(
      <String>['plugin', 'plugin_shared'],
596 597 598 599 600
      directory,
      templateContext,
      overwrite: overwrite,
      printStatusWhenWriting: printStatusWhenWriting,
    );
601

602
    final FlutterProject project = FlutterProject.fromDirectory(directory);
603 604 605 606 607
    final bool generateAndroid = templateContext['android'] == true;
    if (generateAndroid) {
      gradle.updateLocalProperties(
        project: project, requireAndroidSdk: false);
    }
608

609 610 611
    final String? projectName = templateContext['projectName'] as String?;
    final String organization = templateContext['organization']! as String; // Required to make the context.
    final String? androidPluginIdentifier = templateContext['androidIdentifier'] as String?;
612
    final String exampleProjectName = '${projectName}_example';
613
    templateContext['projectName'] = exampleProjectName;
614 615 616 617
    templateContext['androidIdentifier'] = CreateBase.createAndroidIdentifier(organization, exampleProjectName);
    templateContext['iosIdentifier'] = CreateBase.createUTIIdentifier(organization, exampleProjectName);
    templateContext['macosIdentifier'] = CreateBase.createUTIIdentifier(organization, exampleProjectName);
    templateContext['windowsIdentifier'] = CreateBase.createWindowsIdentifier(organization, exampleProjectName);
618 619 620 621
    templateContext['description'] = 'Demonstrates how to use the $projectName plugin.';
    templateContext['pluginProjectName'] = projectName;
    templateContext['androidPluginIdentifier'] = androidPluginIdentifier;

622
    generatedCount += await generateApp(
623
      <String>['app', 'app_test_widget', 'app_integration_test'],
Daco Harkes's avatar
Daco Harkes committed
624 625 626 627 628
      project.example.directory,
      templateContext,
      overwrite: overwrite,
      pluginExampleApp: true,
      printStatusWhenWriting: printStatusWhenWriting,
629
      projectType: projectType,
Daco Harkes's avatar
Daco Harkes committed
630 631 632 633 634 635
    );
    return generatedCount;
  }

  Future<int> _generateFfiPlugin(
    Directory directory,
636
    Map<String, Object?> templateContext, {
Daco Harkes's avatar
Daco Harkes committed
637 638
    bool overwrite = false,
    bool printStatusWhenWriting = true,
639
    required FlutterProjectType projectType,
Daco Harkes's avatar
Daco Harkes committed
640 641
  }) async {
    // Plugins only add a platform if it was requested explicitly by the user.
642
    if (!argResults!.wasParsed('platforms')) {
Daco Harkes's avatar
Daco Harkes committed
643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659
      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;
660
    final String? description = argResults!.wasParsed('description')
661
        ? stringArg('description')
Daco Harkes's avatar
Daco Harkes committed
662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677
        : 'A new Flutter FFI plugin project.';
    templateContext['description'] = description;
    generatedCount += await renderMerged(
      <String>['plugin_ffi', 'plugin_shared'],
      directory,
      templateContext,
      overwrite: overwrite,
      printStatusWhenWriting: printStatusWhenWriting,
    );

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

678 679 680
    final String? projectName = templateContext['projectName'] as String?;
    final String organization = templateContext['organization']! as String; // Required to make the context.
    final String? androidPluginIdentifier = templateContext['androidIdentifier'] as String?;
Daco Harkes's avatar
Daco Harkes committed
681 682 683 684 685 686 687 688 689 690 691 692
    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'],
693 694 695 696 697
      project.example.directory,
      templateContext,
      overwrite: overwrite,
      pluginExampleApp: true,
      printStatusWhenWriting: printStatusWhenWriting,
698
      projectType: projectType,
699
    );
700
    return generatedCount;
Hixie's avatar
Hixie committed
701
  }
702

703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744
  Future<int> _generateFfiPackage(
    Directory directory,
    Map<String, Object?> templateContext, {
    bool overwrite = false,
    bool printStatusWhenWriting = true,
    required FlutterProjectType projectType,
  }) async {
    int generatedCount = 0;
    final String? description = argResults!.wasParsed('description')
        ? stringArg('description')
        : 'A new Dart FFI package project.';
    templateContext['description'] = description;
    generatedCount += await renderMerged(
      <String>[
        'package_ffi',
      ],
      directory,
      templateContext,
      overwrite: overwrite,
      printStatusWhenWriting: printStatusWhenWriting,
    );

    final FlutterProject project = FlutterProject.fromDirectory(directory);

    final String? projectName = templateContext['projectName'] as String?;
    final String exampleProjectName = '${projectName}_example';
    templateContext['projectName'] = exampleProjectName;
    templateContext['description'] = 'Demonstrates how to use the $projectName package.';
    templateContext['pluginProjectName'] = projectName;

    generatedCount += await generateApp(
      <String>['app'],
      project.example.directory,
      templateContext,
      overwrite: overwrite,
      pluginExampleApp: true,
      printStatusWhenWriting: printStatusWhenWriting,
      projectType: projectType,
    );
    return generatedCount;
  }

745
  // Takes an application template and replaces the main.dart with one from the
746
  // documentation website in sampleCode. Returns the difference in the number
747 748
  // 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).
749
  void _applySample(Directory directory, String sampleCode) {
750
    final File mainDartFile = directory.childDirectory('lib').childFile('main.dart');
751 752
    mainDartFile.createSync(recursive: true);
    mainDartFile.writeAsStringSync(sampleCode);
753 754 755
  }

  int _removeTestDir(Directory directory) {
756 757
    final Directory testDir = directory.childDirectory('test');
    final List<FileSystemEntity> files = testDir.listSync(recursive: true);
758
    testDir.deleteSync(recursive: true);
759 760 761
    return -files.length;
  }

762
  List<String> _getSupportedPlatformsFromTemplateContext(Map<String, Object?> templateContext) {
763
    return <String>[
764
      for (final String platform in kAllCreatePlatforms)
765
        if (templateContext[platform] == true) platform,
766 767
    ];
  }
768 769 770

  // Returns a list of platforms that are explicitly requested by user via `--platforms`.
  List<String> _getUserRequestedPlatforms() {
771
    if (!argResults!.wasParsed('platforms')) {
772 773 774 775 776 777 778 779 780 781
      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');
782 783 784
  final FlutterManifest? manifest = FlutterManifest.createFromPath(pubspecPath, fileSystem: globals.fs, logger: globals.logger);
  final Map<String, Object?>? validSupportedPlatforms = manifest?.validSupportedPlatforms;
  final List<String> platforms = validSupportedPlatforms == null
785
    ? <String>[]
786
    : validSupportedPlatforms.keys.toList();
787
  return platforms;
788
}
789 790 791 792 793 794 795 796

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.

797
Your example app code is in $relativeExampleMain.
798 799

''');
800
  if (platformsString.isNotEmpty) {
801 802 803 804 805 806 807 808 809 810 811 812 813 814
    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);
}

815 816 817 818 819 820
void _printNoPluginMessage() {
    globals.printError('''
You've created a plugin project that doesn't yet support any platforms.
''');
}

821
void _printPluginAddPlatformMessage(String pluginPath, String template) {
822
  globals.printStatus('''
823
To add platforms, run `flutter create -t $template --platforms <platforms> .` under $pluginPath.
824
For more information, see https://flutter.dev/go/plugin-platforms.
825

826 827
''');
}
828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872

// 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
''');
  }
}
873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970

// Prints a warning if the specified Java version conflicts with either the
// template Gradle or AGP version.
//
// Assumes the specified templateGradleVersion and templateAgpVersion are
// compatible, meaning that the Java version may only conflict with one of the
// template Gradle or AGP versions.
void _printIncompatibleJavaAgpGradleVersionsWarning({
  required String javaVersion,
  required String templateGradleVersion,
  required String templateAgpVersion,
  required String templateAgpVersionForModule,
  required FlutterProjectType projectType,
  required String projectDirPath}) {
  // Determine if the Java version specified conflicts with the template Gradle or AGP version.
  final bool javaGradleVersionsCompatible = gradle.validateJavaAndGradle(globals.logger, javaV: javaVersion, gradleV: templateGradleVersion);
  bool javaAgpVersionsCompatible = gradle.validateJavaAndAgp(globals.logger, javaV: javaVersion, agpV: templateAgpVersion);
  String relevantTemplateAgpVersion = templateAgpVersion;

  if (projectType == FlutterProjectType.module && Version.parse(templateAgpVersion)! < Version.parse(templateAgpVersionForModule)!) {
    // If a module is being created, make sure to check for Java/AGP compatibility between the highest used version of AGP in the module template.
    javaAgpVersionsCompatible = gradle.validateJavaAndAgp(globals.logger, javaV: javaVersion, agpV: templateAgpVersionForModule);
    relevantTemplateAgpVersion = templateAgpVersionForModule;
  }

  if (javaGradleVersionsCompatible && javaAgpVersionsCompatible) {
    return;
  }

  // Determine header of warning with recommended fix of re-configuring Java version.
  final String incompatibleVersionsAndRecommendedOptionMessage = getIncompatibleJavaGradleAgpMessageHeader(javaGradleVersionsCompatible, templateGradleVersion, relevantTemplateAgpVersion, projectType.cliName);

  if (!javaGradleVersionsCompatible) {

    if (projectType == FlutterProjectType.plugin || projectType == FlutterProjectType.pluginFfi) {
      // Only impacted files could be in sample code.
      return;
    }

    // Gradle template version incompatible with Java version.
    final gradle.JavaGradleCompat? validCompatibleGradleVersionRange = gradle.getValidGradleVersionRangeForJavaVersion(globals.logger, javaV: javaVersion);
    final String compatibleGradleVersionMessage = validCompatibleGradleVersionRange == null ? '' : ' (compatible Gradle version range: ${validCompatibleGradleVersionRange.gradleMin} - ${validCompatibleGradleVersionRange.gradleMax})';

    globals.printWarning('''
$incompatibleVersionsAndRecommendedOptionMessage

Alternatively, to continue using your configured Java version, update the Gradle
version specified in the following file to a compatible Gradle version$compatibleGradleVersionMessage:
${_getGradleWrapperPropertiesFilePath(projectType, projectDirPath)}

You may also update the Gradle version used by running
`./gradlew wrapper --gradle-version=<COMPATIBLE_GRADLE_VERSION>`.

See
https://docs.gradle.org/current/userguide/compatibility.html#java for details
on compatible Java/Gradle versions, and see
https://docs.gradle.org/current/userguide/gradle_wrapper.html#sec:upgrading_wrapper
for more details on using the Gradle Wrapper command to update the Gradle version
used.
''',
      emphasis: true
    );
    return;
  }

  // AGP template version incompatible with Java version.
  final gradle.JavaAgpCompat? minimumCompatibleAgpVersion = gradle.getMinimumAgpVersionForJavaVersion(globals.logger, javaV: javaVersion);
  final String compatibleAgpVersionMessage = minimumCompatibleAgpVersion == null ? '' : ' (minimum compatible AGP version: ${minimumCompatibleAgpVersion.agpMin})';
  final String gradleBuildFilePaths = '    -  ${_getBuildGradleConfigurationFilePaths(projectType, projectDirPath)!.join('\n    - ')}';

  globals.printWarning('''
$incompatibleVersionsAndRecommendedOptionMessage

Alternatively, to continue using your configured Java version, update the AGP
version specified in the following files to a compatible AGP
version$compatibleAgpVersionMessage as necessary:
$gradleBuildFilePaths

See
https://developer.android.com/build/releases/gradle-plugin for details on
compatible Java/AGP versions.
''',
    emphasis: true
  );
}

// Returns incompatible Java/template Gradle/template AGP message header based
// on incompatibility and project type.
@visibleForTesting
String getIncompatibleJavaGradleAgpMessageHeader(
  bool javaGradleVersionsCompatible,
  String templateGradleVersion,
  String templateAgpVersion,
  String projectType) {
  final String incompatibleDependency = javaGradleVersionsCompatible ? 'Android Gradle Plugin (AGP)' :'Gradle' ;
  final String incompatibleDependencyVersion = javaGradleVersionsCompatible ? 'AGP version $templateAgpVersion' : 'Gradle version $templateGradleVersion';
  final VersionRange validJavaRange = gradle.getJavaVersionFor(gradleV: templateGradleVersion, agpV: templateAgpVersion);
  // validJavaRange should have non-null verisonMin and versionMax since it based on our template AGP and Gradle versions.
971
  final String validJavaRangeMessage = '(Java ${validJavaRange.versionMin!} <= compatible Java version < Java ${validJavaRange.versionMax!})';
972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034

  return '''
The configured version of Java detected may conflict with the $incompatibleDependency version in your new Flutter $projectType.

[RECOMMENDED] If so, to keep the default $incompatibleDependencyVersion, make
sure to download a compatible Java version
$validJavaRangeMessage.
You may configure this compatible Java version by running:
`flutter config --jdk-dir=<JDK_DIRECTORY>`
Note that this is a global configuration for Flutter.
''';
}

// Returns path of the gradle-wrapper.properties file for the specified
// generated project type.
String? _getGradleWrapperPropertiesFilePath(FlutterProjectType projectType, String projectDirPath) {
  String gradleWrapperPropertiesFilePath = '';
  switch (projectType) {
      case FlutterProjectType.app:
      case FlutterProjectType.skeleton:
        gradleWrapperPropertiesFilePath = globals.fs.path.join(projectDirPath, 'android/gradle/wrapper/gradle-wrapper.properties');
      case FlutterProjectType.module:
        gradleWrapperPropertiesFilePath = globals.fs.path.join(projectDirPath, '.android/gradle/wrapper/gradle-wrapper.properties');
      case FlutterProjectType.plugin:
      case FlutterProjectType.pluginFfi:
      case FlutterProjectType.package:
      case FlutterProjectType.packageFfi:
        // TODO(camsim99): Add relevant file path for packageFfi when Android is supported.
        // No gradle-wrapper.properties files not part of sample code that
        // can be determined.
        return null;
  }
  return gradleWrapperPropertiesFilePath;
}

// Returns the path(s) of the build.gradle file(s) for the specified generated
// project type.
List<String>? _getBuildGradleConfigurationFilePaths(FlutterProjectType projectType, String projectDirPath) {
  final List<String> buildGradleConfigurationFilePaths = <String>[];
  switch (projectType) {
      case FlutterProjectType.app:
      case FlutterProjectType.skeleton:
      case FlutterProjectType.pluginFfi:
        buildGradleConfigurationFilePaths.add(globals.fs.path.join(projectDirPath, 'android/build.gradle'));
      case FlutterProjectType.module:
        const String moduleBuildGradleFilePath = '.android/build.gradle';
        const String moduleAppBuildGradleFlePath = '.android/app/build.gradle';
        const String moduleFlutterBuildGradleFilePath = '.android/Flutter/build.gradle';
        buildGradleConfigurationFilePaths.addAll(<String>[
          globals.fs.path.join(projectDirPath, moduleBuildGradleFilePath),
          globals.fs.path.join(projectDirPath, moduleAppBuildGradleFlePath),
          globals.fs.path.join(projectDirPath, moduleFlutterBuildGradleFilePath),
        ]);
      case FlutterProjectType.plugin:
        buildGradleConfigurationFilePaths.add(globals.fs.path.join(projectDirPath, 'android/app/build.gradle'));
      case FlutterProjectType.package:
      case FlutterProjectType.packageFfi:
        // TODO(camsim99): Add any relevant file paths for packageFfi when Android is supported.
        // No build.gradle file because there is no platform-specific implementation.
        return null;
  }
  return buildGradleConfigurationFilePaths;
}