1// Copyright 2015 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5import 'dart:async'; 6 7import 'package:linter/src/rules/pub/package_names.dart' as package_names; // ignore: implementation_imports 8import 'package:linter/src/utils.dart' as linter_utils; // ignore: implementation_imports 9import 'package:yaml/yaml.dart' as yaml; 10 11import '../android/android.dart' as android; 12import '../android/android_sdk.dart' as android_sdk; 13import '../android/gradle.dart' as gradle; 14import '../base/common.dart'; 15import '../base/file_system.dart'; 16import '../base/net.dart'; 17import '../base/os.dart'; 18import '../base/utils.dart'; 19import '../cache.dart'; 20import '../convert.dart'; 21import '../dart/pub.dart'; 22import '../doctor.dart'; 23import '../features.dart'; 24import '../globals.dart'; 25import '../project.dart'; 26import '../reporting/reporting.dart'; 27import '../runner/flutter_command.dart'; 28import '../template.dart'; 29import '../version.dart'; 30 31enum _ProjectType { 32 /// This is the default project with the user-managed host code. 33 /// It is different than the "module" template in that it exposes and doesn't 34 /// manage the platform code. 35 app, 36 /// The is a project that has managed platform host code. It is an application with 37 /// ephemeral .ios and .android directories that can be updated automatically. 38 module, 39 /// This is a Flutter Dart package project. It doesn't have any native 40 /// components, only Dart. 41 package, 42 /// This is a native plugin project. 43 plugin, 44} 45 46_ProjectType _stringToProjectType(String value) { 47 _ProjectType result; 48 for (_ProjectType type in _ProjectType.values) { 49 if (value == getEnumName(type)) { 50 result = type; 51 break; 52 } 53 } 54 return result; 55} 56 57class CreateCommand extends FlutterCommand { 58 CreateCommand() { 59 argParser.addFlag('pub', 60 defaultsTo: true, 61 help: 'Whether to run "flutter pub get" after the project has been created.', 62 ); 63 argParser.addFlag('offline', 64 defaultsTo: false, 65 help: 'When "flutter pub get" is run by the create command, this indicates ' 66 'whether to run it in offline mode or not. In offline mode, it will need to ' 67 'have all dependencies already available in the pub cache to succeed.', 68 ); 69 argParser.addFlag( 70 'with-driver-test', 71 negatable: true, 72 defaultsTo: false, 73 help: "Also add a flutter_driver dependency and generate a sample 'flutter drive' test.", 74 ); 75 argParser.addOption( 76 'template', 77 abbr: 't', 78 allowed: _ProjectType.values.map<String>((_ProjectType type) => getEnumName(type)), 79 help: 'Specify the type of project to create.', 80 valueHelp: 'type', 81 allowedHelp: <String, String>{ 82 getEnumName(_ProjectType.app): '(default) Generate a Flutter application.', 83 getEnumName(_ProjectType.package): 'Generate a shareable Flutter project containing modular ' 84 'Dart code.', 85 getEnumName(_ProjectType.plugin): 'Generate a shareable Flutter project containing an API ' 86 'in Dart code with a platform-specific implementation for Android, for iOS code, or ' 87 'for both.', 88 }, 89 defaultsTo: null, 90 ); 91 argParser.addOption( 92 'sample', 93 abbr: 's', 94 help: 'Specifies the Flutter code sample to use as the main.dart for an application. Implies ' 95 '--template=app. The value should be the sample ID of the desired sample from the API ' 96 'documentation website (http://docs.flutter.dev). An example can be found at ' 97 'https://master-api.flutter.dev/flutter/widgets/SingleChildScrollView-class.html', 98 defaultsTo: null, 99 valueHelp: 'id', 100 ); 101 argParser.addOption( 102 'list-samples', 103 help: 'Specifies a JSON output file for a listing of Flutter code samples ' 104 'that can created with --sample.', 105 valueHelp: 'path', 106 ); 107 argParser.addFlag( 108 'overwrite', 109 negatable: true, 110 defaultsTo: false, 111 help: 'When performing operations, overwrite existing files.', 112 ); 113 argParser.addOption( 114 'description', 115 defaultsTo: 'A new Flutter project.', 116 help: 'The description to use for your new Flutter project. This string ends up in the pubspec.yaml file.', 117 ); 118 argParser.addOption( 119 'org', 120 defaultsTo: 'com.example', 121 help: 'The organization responsible for your new Flutter project, in reverse domain name notation. ' 122 'This string is used in Java package names and as prefix in the iOS bundle identifier.', 123 ); 124 argParser.addOption( 125 'project-name', 126 defaultsTo: null, 127 help: 'The project name for this new Flutter project. This must be a valid dart package name.', 128 ); 129 argParser.addOption( 130 'ios-language', 131 abbr: 'i', 132 defaultsTo: 'swift', 133 allowed: <String>['objc', 'swift'], 134 ); 135 argParser.addOption( 136 'android-language', 137 abbr: 'a', 138 defaultsTo: 'kotlin', 139 allowed: <String>['java', 'kotlin'], 140 ); 141 argParser.addFlag( 142 'androidx', 143 negatable: true, 144 defaultsTo: false, 145 help: 'Generate a project using the AndroidX support libraries', 146 ); 147 argParser.addFlag( 148 'web', 149 negatable: true, 150 defaultsTo: false, 151 hide: true, 152 help: '(Experimental) Generate the web specific tooling. Only supported ' 153 'on non-stable branches', 154 ); 155 } 156 157 @override 158 final String name = 'create'; 159 160 @override 161 final String description = 'Create a new Flutter project.\n\n' 162 'If run on a project that already exists, this will repair the project, recreating any files that are missing.'; 163 164 @override 165 String get invocation => '${runner.executableName} $name <output directory>'; 166 167 @override 168 Future<Map<CustomDimensions, String>> get usageValues async { 169 return <CustomDimensions, String>{ 170 CustomDimensions.commandCreateProjectType: argResults['template'], 171 CustomDimensions.commandCreateAndroidLanguage: argResults['android-language'], 172 CustomDimensions.commandCreateIosLanguage: argResults['ios-language'], 173 }; 174 } 175 176 // If it has a .metadata file with the project_type in it, use that. 177 // If it has an android dir and an android/app dir, it's a legacy app 178 // If it has an ios dir and an ios/Flutter dir, it's a legacy app 179 // Otherwise, we don't presume to know what type of project it could be, since 180 // many of the files could be missing, and we can't really tell definitively. 181 _ProjectType _determineTemplateType(Directory projectDir) { 182 yaml.YamlMap loadMetadata(Directory projectDir) { 183 if (!projectDir.existsSync()) 184 return null; 185 final File metadataFile = fs.file(fs.path.join(projectDir.absolute.path, '.metadata')); 186 if (!metadataFile.existsSync()) 187 return null; 188 return yaml.loadYaml(metadataFile.readAsStringSync()); 189 } 190 191 bool exists(List<String> path) { 192 return fs.directory(fs.path.joinAll(<String>[projectDir.absolute.path] + path)).existsSync(); 193 } 194 195 // If it exists, the project type in the metadata is definitive. 196 final yaml.YamlMap metadata = loadMetadata(projectDir); 197 if (metadata != null && metadata['project_type'] != null) { 198 return _stringToProjectType(metadata['project_type']); 199 } 200 201 // There either wasn't any metadata, or it didn't contain the project type, 202 // so try and figure out what type of project it is from the existing 203 // directory structure. 204 if (exists(<String>['android', 'app']) 205 || exists(<String>['ios', 'Runner']) 206 || exists(<String>['ios', 'Flutter'])) { 207 return _ProjectType.app; 208 } 209 // Since we can't really be definitive on nearly-empty directories, err on 210 // the side of prudence and just say we don't know. 211 return null; 212 } 213 214 /// The hostname for the Flutter docs for the current channel. 215 String get _snippetsHost => FlutterVersion.instance.channel == 'stable' 216 ? 'docs.flutter.io' 217 : 'master-docs.flutter.io'; 218 219 Future<String> _fetchSampleFromServer(String sampleId) async { 220 // Sanity check the sampleId 221 if (sampleId.contains(RegExp(r'[^-\w\.]'))) { 222 throwToolExit('Sample ID "$sampleId" contains invalid characters. Check the ID in the ' 223 'documentation and try again.'); 224 } 225 226 return utf8.decode(await fetchUrl(Uri.https(_snippetsHost, 'snippets/$sampleId.dart'))); 227 } 228 229 /// Fetches the samples index file from the Flutter docs website. 230 Future<String> _fetchSamplesIndexFromServer() async { 231 return utf8.decode( 232 await fetchUrl(Uri.https(_snippetsHost, 'snippets/index.json'), maxAttempts: 2)); 233 } 234 235 /// Fetches the samples index file from the server and writes it to 236 /// [outputFilePath]. 237 Future<void> _writeSamplesJson(String outputFilePath) async { 238 try { 239 final File outputFile = fs.file(outputFilePath); 240 if (outputFile.existsSync()) { 241 throwToolExit('File "$outputFilePath" already exists', exitCode: 1); 242 } 243 final String samplesJson = await _fetchSamplesIndexFromServer(); 244 if (samplesJson == null) { 245 throwToolExit('Unable to download samples', exitCode: 2); 246 } 247 else { 248 outputFile.writeAsStringSync(samplesJson); 249 printStatus('Wrote samples JSON to "$outputFilePath"'); 250 } 251 } catch (e) { 252 throwToolExit('Failed to write samples JSON to "$outputFilePath": $e', exitCode: 2); 253 } 254 } 255 256 _ProjectType _getProjectType(Directory projectDir) { 257 _ProjectType template; 258 _ProjectType detectedProjectType; 259 final bool metadataExists = projectDir.absolute.childFile('.metadata').existsSync(); 260 if (argResults['template'] != null) { 261 template = _stringToProjectType(argResults['template']); 262 } else { 263 // If the project directory exists and isn't empty, then try to determine the template 264 // type from the project directory. 265 if (projectDir.existsSync() && projectDir.listSync().isNotEmpty) { 266 detectedProjectType = _determineTemplateType(projectDir); 267 if (detectedProjectType == null && metadataExists) { 268 // We can only be definitive that this is the wrong type if the .metadata file 269 // exists and contains a type that we don't understand, or doesn't contain a type. 270 throwToolExit('Sorry, unable to detect the type of project to recreate. ' 271 'Try creating a fresh project and migrating your existing code to ' 272 'the new project manually.'); 273 } 274 } 275 } 276 template ??= detectedProjectType ?? _ProjectType.app; 277 if (detectedProjectType != null && template != detectedProjectType && metadataExists) { 278 // We can only be definitive that this is the wrong type if the .metadata file 279 // exists and contains a type that doesn't match. 280 throwToolExit("The requested template type '${getEnumName(template)}' doesn't match the " 281 "existing template type of '${getEnumName(detectedProjectType)}'."); 282 } 283 return template; 284 } 285 286 @override 287 Future<FlutterCommandResult> runCommand() async { 288 if (argResults['list-samples'] != null) { 289 // _writeSamplesJson can potentially be long-lived. 290 Cache.releaseLockEarly(); 291 292 await _writeSamplesJson(argResults['list-samples']); 293 return null; 294 } 295 296 if (argResults.rest.isEmpty) 297 throwToolExit('No option specified for the output directory.\n$usage', exitCode: 2); 298 299 if (argResults.rest.length > 1) { 300 String message = 'Multiple output directories specified.'; 301 for (String arg in argResults.rest) { 302 if (arg.startsWith('-')) { 303 message += '\nTry moving $arg to be immediately following $name'; 304 break; 305 } 306 } 307 throwToolExit(message, exitCode: 2); 308 } 309 310 if (Cache.flutterRoot == null) 311 throwToolExit('Neither the --flutter-root command line flag nor the FLUTTER_ROOT environment ' 312 'variable was specified. Unable to find package:flutter.', exitCode: 2); 313 314 await Cache.instance.updateAll(<DevelopmentArtifact>{ DevelopmentArtifact.universal }); 315 316 final String flutterRoot = fs.path.absolute(Cache.flutterRoot); 317 318 final String flutterPackagesDirectory = fs.path.join(flutterRoot, 'packages'); 319 final String flutterPackagePath = fs.path.join(flutterPackagesDirectory, 'flutter'); 320 if (!fs.isFileSync(fs.path.join(flutterPackagePath, 'pubspec.yaml'))) 321 throwToolExit('Unable to find package:flutter in $flutterPackagePath', exitCode: 2); 322 323 final String flutterDriverPackagePath = fs.path.join(flutterRoot, 'packages', 'flutter_driver'); 324 if (!fs.isFileSync(fs.path.join(flutterDriverPackagePath, 'pubspec.yaml'))) 325 throwToolExit('Unable to find package:flutter_driver in $flutterDriverPackagePath', exitCode: 2); 326 327 final Directory projectDir = fs.directory(argResults.rest.first); 328 final String projectDirPath = fs.path.normalize(projectDir.absolute.path); 329 330 String sampleCode; 331 if (argResults['sample'] != null) { 332 if (argResults['template'] != null && 333 _stringToProjectType(argResults['template'] ?? 'app') != _ProjectType.app) { 334 throwToolExit('Cannot specify --sample with a project type other than ' 335 '"${getEnumName(_ProjectType.app)}"'); 336 } 337 // Fetch the sample from the server. 338 sampleCode = await _fetchSampleFromServer(argResults['sample']); 339 } 340 341 final _ProjectType template = _getProjectType(projectDir); 342 final bool generateModule = template == _ProjectType.module; 343 final bool generatePlugin = template == _ProjectType.plugin; 344 final bool generatePackage = template == _ProjectType.package; 345 346 String organization = argResults['org']; 347 if (!argResults.wasParsed('org')) { 348 final FlutterProject project = FlutterProject.fromDirectory(projectDir); 349 final Set<String> existingOrganizations = project.organizationNames; 350 if (existingOrganizations.length == 1) { 351 organization = existingOrganizations.first; 352 } else if (1 < existingOrganizations.length) { 353 throwToolExit( 354 'Ambiguous organization in existing files: $existingOrganizations. ' 355 'The --org command line argument must be specified to recreate project.' 356 ); 357 } 358 } 359 360 String error = _validateProjectDir(projectDirPath, flutterRoot: flutterRoot, overwrite: argResults['overwrite']); 361 if (error != null) 362 throwToolExit(error); 363 364 final String projectName = argResults['project-name'] ?? fs.path.basename(projectDirPath); 365 error = _validateProjectName(projectName); 366 if (error != null) 367 throwToolExit(error); 368 369 final Map<String, dynamic> templateContext = _templateContext( 370 organization: organization, 371 projectName: projectName, 372 projectDescription: argResults['description'], 373 flutterRoot: flutterRoot, 374 renderDriverTest: argResults['with-driver-test'], 375 withPluginHook: generatePlugin, 376 androidX: argResults['androidx'], 377 androidLanguage: argResults['android-language'], 378 iosLanguage: argResults['ios-language'], 379 web: argResults['web'], 380 ); 381 382 final String relativeDirPath = fs.path.relative(projectDirPath); 383 if (!projectDir.existsSync() || projectDir.listSync().isEmpty) { 384 printStatus('Creating project $relativeDirPath...'); 385 } else { 386 if (sampleCode != null && !argResults['overwrite']) { 387 throwToolExit('Will not overwrite existing project in $relativeDirPath: ' 388 'must specify --overwrite for samples to overwrite.'); 389 } 390 printStatus('Recreating project $relativeDirPath...'); 391 } 392 393 final Directory relativeDir = fs.directory(projectDirPath); 394 int generatedFileCount = 0; 395 switch (template) { 396 case _ProjectType.app: 397 generatedFileCount += await _generateApp(relativeDir, templateContext, overwrite: argResults['overwrite']); 398 break; 399 case _ProjectType.module: 400 generatedFileCount += await _generateModule(relativeDir, templateContext, overwrite: argResults['overwrite']); 401 break; 402 case _ProjectType.package: 403 generatedFileCount += await _generatePackage(relativeDir, templateContext, overwrite: argResults['overwrite']); 404 break; 405 case _ProjectType.plugin: 406 generatedFileCount += await _generatePlugin(relativeDir, templateContext, overwrite: argResults['overwrite']); 407 break; 408 } 409 if (sampleCode != null) { 410 generatedFileCount += await _applySample(relativeDir, sampleCode); 411 } 412 printStatus('Wrote $generatedFileCount files.'); 413 printStatus('\nAll done!'); 414 final String application = sampleCode != null ? 'sample application' : 'application'; 415 if (generatePackage) { 416 final String relativeMainPath = fs.path.normalize(fs.path.join( 417 relativeDirPath, 418 'lib', 419 '${templateContext['projectName']}.dart', 420 )); 421 printStatus('Your package code is in $relativeMainPath'); 422 } else if (generateModule) { 423 final String relativeMainPath = fs.path.normalize(fs.path.join( 424 relativeDirPath, 425 'lib', 426 'main.dart', 427 )); 428 printStatus('Your module code is in $relativeMainPath.'); 429 } else { 430 // Run doctor; tell the user the next steps. 431 final FlutterProject project = FlutterProject.fromPath(projectDirPath); 432 final FlutterProject app = project.hasExampleApp ? project.example : project; 433 final String relativeAppPath = fs.path.normalize(fs.path.relative(app.directory.path)); 434 final String relativeAppMain = fs.path.join(relativeAppPath, 'lib', 'main.dart'); 435 final String relativePluginPath = fs.path.normalize(fs.path.relative(projectDirPath)); 436 final String relativePluginMain = fs.path.join(relativePluginPath, 'lib', '$projectName.dart'); 437 if (doctor.canLaunchAnything) { 438 // Let them know a summary of the state of their tooling. 439 await doctor.summary(); 440 441 printStatus(''' 442In order to run your $application, type: 443 444 \$ cd $relativeAppPath 445 \$ flutter run 446 447Your $application code is in $relativeAppMain. 448'''); 449 if (generatePlugin) { 450 printStatus(''' 451Your plugin code is in $relativePluginMain. 452 453Host platform code is in the "android" and "ios" directories under $relativePluginPath. 454To edit platform code in an IDE see https://flutter.dev/developing-packages/#edit-plugin-package. 455'''); 456 } 457 } else { 458 printStatus("You'll need to install additional components before you can run " 459 'your Flutter app:'); 460 printStatus(''); 461 462 // Give the user more detailed analysis. 463 await doctor.diagnose(); 464 printStatus(''); 465 printStatus("After installing components, run 'flutter doctor' in order to " 466 're-validate your setup.'); 467 printStatus("When complete, type 'flutter run' from the '$relativeAppPath' " 468 'directory in order to launch your app.'); 469 printStatus('Your $application code is in $relativeAppMain'); 470 } 471 } 472 473 return null; 474 } 475 476 Future<int> _generateModule(Directory directory, Map<String, dynamic> templateContext, { bool overwrite = false }) async { 477 int generatedCount = 0; 478 final String description = argResults.wasParsed('description') 479 ? argResults['description'] 480 : 'A new flutter module project.'; 481 templateContext['description'] = description; 482 generatedCount += _renderTemplate(fs.path.join('module', 'common'), directory, templateContext, overwrite: overwrite); 483 if (argResults['pub']) { 484 await pubGet( 485 context: PubContext.create, 486 directory: directory.path, 487 offline: argResults['offline'], 488 ); 489 final FlutterProject project = FlutterProject.fromDirectory(directory); 490 await project.ensureReadyForPlatformSpecificTooling(checkProjects: false); 491 } 492 return generatedCount; 493 } 494 495 Future<int> _generatePackage(Directory directory, Map<String, dynamic> templateContext, { bool overwrite = false }) async { 496 int generatedCount = 0; 497 final String description = argResults.wasParsed('description') 498 ? argResults['description'] 499 : 'A new Flutter package project.'; 500 templateContext['description'] = description; 501 generatedCount += _renderTemplate('package', directory, templateContext, overwrite: overwrite); 502 if (argResults['pub']) { 503 await pubGet( 504 context: PubContext.createPackage, 505 directory: directory.path, 506 offline: argResults['offline'], 507 ); 508 } 509 return generatedCount; 510 } 511 512 Future<int> _generatePlugin(Directory directory, Map<String, dynamic> templateContext, { bool overwrite = false }) async { 513 int generatedCount = 0; 514 final String description = argResults.wasParsed('description') 515 ? argResults['description'] 516 : 'A new flutter plugin project.'; 517 templateContext['description'] = description; 518 generatedCount += _renderTemplate('plugin', directory, templateContext, overwrite: overwrite); 519 if (argResults['pub']) { 520 await pubGet( 521 context: PubContext.createPlugin, 522 directory: directory.path, 523 offline: argResults['offline'], 524 ); 525 } 526 final FlutterProject project = FlutterProject.fromDirectory(directory); 527 gradle.updateLocalProperties(project: project, requireAndroidSdk: false); 528 529 final String projectName = templateContext['projectName']; 530 final String organization = templateContext['organization']; 531 final String androidPluginIdentifier = templateContext['androidIdentifier']; 532 final String exampleProjectName = projectName + '_example'; 533 templateContext['projectName'] = exampleProjectName; 534 templateContext['androidIdentifier'] = _createAndroidIdentifier(organization, exampleProjectName); 535 templateContext['iosIdentifier'] = _createUTIIdentifier(organization, exampleProjectName); 536 templateContext['description'] = 'Demonstrates how to use the $projectName plugin.'; 537 templateContext['pluginProjectName'] = projectName; 538 templateContext['androidPluginIdentifier'] = androidPluginIdentifier; 539 540 generatedCount += await _generateApp(project.example.directory, templateContext, overwrite: overwrite); 541 return generatedCount; 542 } 543 544 Future<int> _generateApp(Directory directory, Map<String, dynamic> templateContext, { bool overwrite = false }) async { 545 int generatedCount = 0; 546 generatedCount += _renderTemplate('app', directory, templateContext, overwrite: overwrite); 547 final FlutterProject project = FlutterProject.fromDirectory(directory); 548 generatedCount += _injectGradleWrapper(project); 549 550 if (argResults['with-driver-test']) { 551 final Directory testDirectory = directory.childDirectory('test_driver'); 552 generatedCount += _renderTemplate('driver', testDirectory, templateContext, overwrite: overwrite); 553 } 554 555 if (argResults['pub']) { 556 await pubGet(context: PubContext.create, directory: directory.path, offline: argResults['offline']); 557 await project.ensureReadyForPlatformSpecificTooling(checkProjects: false); 558 } 559 560 gradle.updateLocalProperties(project: project, requireAndroidSdk: false); 561 562 return generatedCount; 563 } 564 565 // Takes an application template and replaces the main.dart with one from the 566 // documentation website in sampleCode. Returns the difference in the number 567 // of files after applying the sample, since it also deletes the application's 568 // test directory (since the template's test doesn't apply to the sample). 569 Future<int> _applySample(Directory directory, String sampleCode) async { 570 final File mainDartFile = directory.childDirectory('lib').childFile('main.dart'); 571 await mainDartFile.create(recursive: true); 572 await mainDartFile.writeAsString(sampleCode); 573 final Directory testDir = directory.childDirectory('test'); 574 final List<FileSystemEntity> files = testDir.listSync(recursive: true); 575 await testDir.delete(recursive: true); 576 return -files.length; 577 } 578 579 Map<String, dynamic> _templateContext({ 580 String organization, 581 String projectName, 582 String projectDescription, 583 String androidLanguage, 584 bool androidX, 585 String iosLanguage, 586 String flutterRoot, 587 bool renderDriverTest = false, 588 bool withPluginHook = false, 589 bool web = false, 590 }) { 591 flutterRoot = fs.path.normalize(flutterRoot); 592 593 final String pluginDartClass = _createPluginClassName(projectName); 594 final String pluginClass = pluginDartClass.endsWith('Plugin') 595 ? pluginDartClass 596 : pluginDartClass + 'Plugin'; 597 598 return <String, dynamic>{ 599 'organization': organization, 600 'projectName': projectName, 601 'androidIdentifier': _createAndroidIdentifier(organization, projectName), 602 'iosIdentifier': _createUTIIdentifier(organization, projectName), 603 'description': projectDescription, 604 'dartSdk': '$flutterRoot/bin/cache/dart-sdk', 605 'androidX': androidX, 606 'androidMinApiLevel': android.minApiLevel, 607 'androidSdkVersion': android_sdk.minimumAndroidSdkVersion, 608 'androidFlutterJar': '$flutterRoot/bin/cache/artifacts/engine/android-arm/flutter.jar', 609 'withDriverTest': renderDriverTest, 610 'pluginClass': pluginClass, 611 'pluginDartClass': pluginDartClass, 612 'withPluginHook': withPluginHook, 613 'androidLanguage': androidLanguage, 614 'iosLanguage': iosLanguage, 615 'flutterRevision': FlutterVersion.instance.frameworkRevision, 616 'flutterChannel': FlutterVersion.instance.channel, 617 'web': web && featureFlags.isWebEnabled, 618 }; 619 } 620 621 int _renderTemplate(String templateName, Directory directory, Map<String, dynamic> context, { bool overwrite = false }) { 622 final Template template = Template.fromName(templateName); 623 return template.render(directory, context, overwriteExisting: overwrite); 624 } 625 626 int _injectGradleWrapper(FlutterProject project) { 627 int filesCreated = 0; 628 copyDirectorySync( 629 cache.getArtifactDirectory('gradle_wrapper'), 630 project.android.hostAppGradleRoot, 631 (File sourceFile, File destinationFile) { 632 filesCreated++; 633 final String modes = sourceFile.statSync().modeString(); 634 if (modes != null && modes.contains('x')) { 635 os.makeExecutable(destinationFile); 636 } 637 }, 638 ); 639 return filesCreated; 640 } 641} 642 643String _createAndroidIdentifier(String organization, String name) { 644 // Android application ID is specified in: https://developer.android.com/studio/build/application-id 645 // All characters must be alphanumeric or an underscore [a-zA-Z0-9_]. 646 String tmpIdentifier = '$organization.$name'; 647 final RegExp disallowed = RegExp(r'[^\w\.]'); 648 tmpIdentifier = tmpIdentifier.replaceAll(disallowed, ''); 649 650 // It must have at least two segments (one or more dots). 651 final List<String> segments = tmpIdentifier 652 .split('.') 653 .where((String segment) => segment.isNotEmpty) 654 .toList(); 655 while (segments.length < 2) { 656 segments.add('untitled'); 657 } 658 659 // Each segment must start with a letter. 660 final RegExp segmentPatternRegex = RegExp(r'^[a-zA-Z][\w]*$'); 661 final List<String> prefixedSegments = segments 662 .map((String segment) { 663 if (!segmentPatternRegex.hasMatch(segment)) { 664 return 'u'+segment; 665 } 666 return segment; 667 }) 668 .toList(); 669 return prefixedSegments.join('.'); 670} 671 672String _createPluginClassName(String name) { 673 final String camelizedName = camelCase(name); 674 return camelizedName[0].toUpperCase() + camelizedName.substring(1); 675} 676 677String _createUTIIdentifier(String organization, String name) { 678 // Create a UTI (https://en.wikipedia.org/wiki/Uniform_Type_Identifier) from a base name 679 name = camelCase(name); 680 String tmpIdentifier = '$organization.$name'; 681 final RegExp disallowed = RegExp(r'[^a-zA-Z0-9\-\.\u0080-\uffff]+'); 682 tmpIdentifier = tmpIdentifier.replaceAll(disallowed, ''); 683 684 // It must have at least two segments (one or more dots). 685 final List<String> segments = tmpIdentifier 686 .split('.') 687 .where((String segment) => segment.isNotEmpty) 688 .toList(); 689 while (segments.length < 2) { 690 segments.add('untitled'); 691 } 692 693 return segments.join('.'); 694} 695 696const Set<String> _packageDependencies = <String>{ 697 'analyzer', 698 'args', 699 'async', 700 'collection', 701 'convert', 702 'crypto', 703 'flutter', 704 'flutter_test', 705 'front_end', 706 'html', 707 'http', 708 'intl', 709 'io', 710 'isolate', 711 'kernel', 712 'logging', 713 'matcher', 714 'meta', 715 'mime', 716 'path', 717 'plugin', 718 'pool', 719 'test', 720 'utf', 721 'watcher', 722 'yaml', 723}; 724 725/// Return null if the project name is legal. Return a validation message if 726/// we should disallow the project name. 727String _validateProjectName(String projectName) { 728 if (!linter_utils.isValidPackageName(projectName)) { 729 final String packageNameDetails = package_names.PubPackageNames().details; 730 return '"$projectName" is not a valid Dart package name.\n\n$packageNameDetails'; 731 } 732 if (_packageDependencies.contains(projectName)) { 733 return "Invalid project name: '$projectName' - this will conflict with Flutter " 734 'package dependencies.'; 735 } 736 return null; 737} 738 739/// Return null if the project directory is legal. Return a validation message 740/// if we should disallow the directory name. 741String _validateProjectDir(String dirPath, { String flutterRoot, bool overwrite = false }) { 742 if (fs.path.isWithin(flutterRoot, dirPath)) { 743 return 'Cannot create a project within the Flutter SDK. ' 744 "Target directory '$dirPath' is within the Flutter SDK at '$flutterRoot'."; 745 } 746 747 // If the destination directory is actually a file, then we refuse to 748 // overwrite, on the theory that the user probably didn't expect it to exist. 749 if (fs.isFileSync(dirPath)) { 750 return "Invalid project name: '$dirPath' - refers to an existing file." 751 '${overwrite ? ' Refusing to overwrite a file with a directory.' : ''}'; 752 } 753 754 if (overwrite) 755 return null; 756 757 final FileSystemEntityType type = fs.typeSync(dirPath); 758 759 if (type != FileSystemEntityType.notFound) { 760 switch (type) { 761 case FileSystemEntityType.file: 762 // Do not overwrite files. 763 return "Invalid project name: '$dirPath' - file exists."; 764 case FileSystemEntityType.link: 765 // Do not overwrite links. 766 return "Invalid project name: '$dirPath' - refers to a link."; 767 } 768 } 769 770 return null; 771} 772