// Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. // @dart = 2.8 import '../android/gradle_utils.dart' as gradle; import '../base/common.dart'; import '../base/context.dart'; import '../base/file_system.dart'; import '../base/net.dart'; import '../base/terminal.dart'; import '../convert.dart'; import '../dart/pub.dart'; import '../features.dart'; import '../flutter_manifest.dart'; import '../flutter_project_metadata.dart'; import '../globals_null_migrated.dart' as globals; import '../project.dart'; import '../reporting/reporting.dart'; import '../runner/flutter_command.dart'; import 'create_base.dart'; 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.'; class CreateCommand extends CreateBase { CreateCommand({ bool verboseHelp = false, }) : super(verboseHelp: verboseHelp) { addPlatformsOptions(customHelp: kPlatformHelp); argParser.addOption( 'template', abbr: 't', allowed: FlutterProjectType.values.map<String>(flutterProjectTypeToString), help: 'Specify the type of project to create.', valueHelp: 'type', allowedHelp: <String, String>{ flutterProjectTypeToString(FlutterProjectType.app): '(default) Generate a Flutter application.', 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.', flutterProjectTypeToString(FlutterProjectType.plugin): 'Generate a shareable Flutter project containing an API ' 'in Dart code with a platform-specific implementation for Android, for iOS code, or ' 'for both.', flutterProjectTypeToString(FlutterProjectType.module): 'Generate a project to add a Flutter module to an ' 'existing Android or iOS application.', }, defaultsTo: null, ); argParser.addOption( 'sample', abbr: 's', 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', defaultsTo: null, valueHelp: 'id', ); argParser.addOption( 'list-samples', help: 'Specifies a JSON output file for a listing of Flutter code samples ' 'that can be created with "--sample".', valueHelp: 'path', ); } @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.'; @override String get invocation => '${runner.executableName} $name <output directory>'; @override Future<CustomDimensions> get usageValues async { return CustomDimensions( commandCreateProjectType: stringArg('template'), commandCreateAndroidLanguage: stringArg('android-language'), commandCreateIosLanguage: stringArg('ios-language'), ); } // Lazy-initialize the net utilities with values from the context. Net _cachedNet; Net get _net => _cachedNet ??= Net( httpClientFactory: context.get<HttpClientFactory>(), logger: globals.logger, platform: globals.platform, ); /// The hostname for the Flutter docs for the current channel. String get _snippetsHost => globals.flutterVersion.channel == 'stable' ? 'api.flutter.dev' : 'master-api.flutter.dev'; 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.'); } final Uri snippetsUri = Uri.https(_snippetsHost, 'snippets/$sampleId.dart'); final List<int> data = await _net.fetchUrl(snippetsUri); if (data == null || data.isEmpty) { return null; } return utf8.decode(data); } /// Fetches the samples index file from the Flutter docs website. Future<String> _fetchSamplesIndexFromServer() async { final Uri snippetsUri = Uri.https(_snippetsHost, 'snippets/index.json'); final List<int> data = await _net.fetchUrl(snippetsUri, maxAttempts: 2); if (data == null || data.isEmpty) { return null; } return utf8.decode(data); } /// Fetches the samples index file from the server and writes it to /// [outputFilePath]. Future<void> _writeSamplesJson(String outputFilePath) async { try { final File outputFile = globals.fs.file(outputFilePath); if (outputFile.existsSync()) { throwToolExit('File "$outputFilePath" already exists', exitCode: 1); } final String samplesJson = await _fetchSamplesIndexFromServer(); if (samplesJson == null) { throwToolExit('Unable to download samples', exitCode: 2); } else { outputFile.writeAsStringSync(samplesJson); globals.printStatus('Wrote samples JSON to "$outputFilePath"'); } } on Exception catch (e) { throwToolExit('Failed to write samples JSON to "$outputFilePath": $e', exitCode: 2); } } FlutterProjectType _getProjectType(Directory projectDir) { FlutterProjectType template; FlutterProjectType detectedProjectType; final bool metadataExists = projectDir.absolute.childFile('.metadata').existsSync(); if (argResults['template'] != null) { template = stringToProjectType(stringArg('template')); } else { // 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.'); } } } template ??= detectedProjectType ?? FlutterProjectType.app; 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. throwToolExit("The requested template type '${flutterProjectTypeToString(template)}' doesn't match the " "existing template type of '${flutterProjectTypeToString(detectedProjectType)}'."); } return template; } @override Future<FlutterCommandResult> runCommand() async { if (argResults['list-samples'] != null) { // _writeSamplesJson can potentially be long-lived. await _writeSamplesJson(stringArg('list-samples')); return FlutterCommandResult.success(); } validateOutputDirectoryArg(); String sampleCode; if (argResults['sample'] != null) { if (argResults['template'] != null && stringToProjectType(stringArg('template') ?? 'app') != FlutterProjectType.app) { throwToolExit('Cannot specify --sample with a project type other than ' '"${flutterProjectTypeToString(FlutterProjectType.app)}"'); } // Fetch the sample from the server. sampleCode = await _fetchSampleFromServer(stringArg('sample')); } final FlutterProjectType template = _getProjectType(projectDir); final bool generateModule = template == FlutterProjectType.module; final bool generatePlugin = template == FlutterProjectType.plugin; final bool generatePackage = template == FlutterProjectType.package; 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); } final String organization = await getOrganization(); final bool overwrite = boolArg('overwrite'); validateProjectDir(overwrite: overwrite); if (boolArg('with-driver-test')) { globals.printError( 'The "--with-driver-test" argument has been deprecated and will no longer add a flutter ' 'driver template. Instead, learn how to use package:integration_test by ' 'visiting https://pub.dev/packages/integration_test .' ); } final Map<String, Object> templateContext = createTemplateContext( organization: organization, projectName: projectName, projectDescription: stringArg('description'), flutterRoot: flutterRoot, withPluginHook: generatePlugin, androidLanguage: stringArg('android-language'), iosLanguage: stringArg('ios-language'), ios: featureFlags.isIOSEnabled && platforms.contains('ios'), android: featureFlags.isAndroidEnabled && platforms.contains('android'), web: featureFlags.isWebEnabled && platforms.contains('web'), linux: featureFlags.isLinuxEnabled && platforms.contains('linux'), macos: featureFlags.isMacOSEnabled && platforms.contains('macos'), windows: featureFlags.isWindowsEnabled && platforms.contains('windows'), windowsUwp: featureFlags.isWindowsUwpEnabled && platforms.contains('winuwp'), // Enable null safety everywhere. dartSdkVersionBounds: '">=2.12.0 <3.0.0"', implementationTests: boolArg('implementation-tests'), ); final String relativeDirPath = globals.fs.path.relative(projectDirPath); final bool creatingNewProject = !projectDir.existsSync() || projectDir.listSync().isEmpty; if (creatingNewProject) { globals.printStatus('Creating project $relativeDirPath...'); } else { if (sampleCode != null && !overwrite) { throwToolExit('Will not overwrite existing project in $relativeDirPath: ' 'must specify --overwrite for samples to overwrite.'); } globals.printStatus('Recreating project $relativeDirPath...'); } final Directory relativeDir = globals.fs.directory(projectDirPath); int generatedFileCount = 0; switch (template) { case FlutterProjectType.app: generatedFileCount += await generateApp('app', relativeDir, templateContext, overwrite: overwrite); break; case FlutterProjectType.skeleton: generatedFileCount += await generateApp('skeleton', relativeDir, templateContext, overwrite: overwrite); break; case FlutterProjectType.module: generatedFileCount += await _generateModule(relativeDir, templateContext, overwrite: overwrite); break; case FlutterProjectType.package: generatedFileCount += await _generatePackage(relativeDir, templateContext, overwrite: overwrite); break; case FlutterProjectType.plugin: generatedFileCount += await _generatePlugin(relativeDir, templateContext, overwrite: overwrite); break; } if (sampleCode != null) { generatedFileCount += _applySample(relativeDir, sampleCode); } globals.printStatus('Wrote $generatedFileCount files.'); globals.printStatus('\nAll done!'); final String application = sampleCode != null ? 'sample application' : 'application'; if (generatePackage) { final String relativeMainPath = globals.fs.path.normalize(globals.fs.path.join( relativeDirPath, 'lib', '${templateContext['projectName']}.dart', )); globals.printStatus('Your package code is in $relativeMainPath'); } else if (generateModule) { final String relativeMainPath = globals.fs.path.normalize(globals.fs.path.join( relativeDirPath, 'lib', 'main.dart', )); globals.printStatus('Your module code is in $relativeMainPath.'); } else if (generatePlugin) { 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); } else if (_getSupportedPlatformsInPlugin(projectDir).isEmpty) { _printNoPluginMessage(); } final List<String> platformsToWarn = _getPlatformWarningList(requestedPlatforms); if (platformsToWarn.isNotEmpty) { _printWarningDisabledPlatform(platformsToWarn); } _printPluginAddPlatformMessage(relativePluginPath); } else { // Tell the user the next steps. final FlutterProject project = FlutterProject.fromDirectory(globals.fs.directory(projectDirPath)); final FlutterProject app = project.hasExampleApp ? project.example : project; 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'); final List<String> requestedPlatforms = _getUserRequestedPlatforms(); // Let them know a summary of the state of their tooling. globals.printStatus(''' In order to run your $application, type: \$ cd $relativeAppPath \$ flutter run Your $application code is in $relativeAppMain. '''); // Show warning if any selected platform is not enabled final List<String> platformsToWarn = _getPlatformWarningList(requestedPlatforms); if (platformsToWarn.isNotEmpty) { _printWarningDisabledPlatform(platformsToWarn); } } return FlutterCommandResult.success(); } Future<int> _generateModule(Directory directory, Map<String, dynamic> templateContext, { bool overwrite = false }) async { int generatedCount = 0; final String description = argResults.wasParsed('description') ? stringArg('description') : 'A new flutter module project.'; templateContext['description'] = description; generatedCount += await renderTemplate(globals.fs.path.join('module', 'common'), directory, templateContext, overwrite: overwrite); if (boolArg('pub')) { await pub.get( context: PubContext.create, directory: directory.path, offline: boolArg('offline'), generateSyntheticPackage: false, ); final FlutterProject project = FlutterProject.fromDirectory(directory); await project.ensureReadyForPlatformSpecificTooling( androidPlatform: true, iosPlatform: true, ); } return generatedCount; } Future<int> _generatePackage(Directory directory, Map<String, dynamic> templateContext, { bool overwrite = false }) async { int generatedCount = 0; final String description = argResults.wasParsed('description') ? stringArg('description') : 'A new Flutter package project.'; templateContext['description'] = description; generatedCount += await renderTemplate('package', directory, templateContext, overwrite: overwrite); if (boolArg('pub')) { await pub.get( context: PubContext.createPackage, directory: directory.path, offline: boolArg('offline'), generateSyntheticPackage: false, ); } return generatedCount; } Future<int> _generatePlugin(Directory directory, Map<String, dynamic> templateContext, { bool overwrite = false }) 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') ? stringArg('description') : 'A new flutter plugin project.'; templateContext['description'] = description; generatedCount += await renderTemplate('plugin', directory, templateContext, overwrite: overwrite); if (boolArg('pub')) { await pub.get( context: PubContext.createPlugin, directory: directory.path, offline: boolArg('offline'), 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('app', project.example.directory, templateContext, overwrite: overwrite, pluginExampleApp: true); return generatedCount; } // 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). int _applySample(Directory directory, String sampleCode) { final File mainDartFile = directory.childDirectory('lib').childFile('main.dart'); mainDartFile.createSync(recursive: true); mainDartFile.writeAsStringSync(sampleCode); final Directory testDir = directory.childDirectory('test'); final List<FileSystemEntity> files = testDir.listSync(recursive: true); testDir.deleteSync(recursive: true); return -files.length; } List<String> _getSupportedPlatformsFromTemplateContext(Map<String, dynamic> templateContext) { return <String>[ for (String platform in kAllCreatePlatforms) if (templateContext[platform] == true) platform ]; } // 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; } 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. Your example app code is in $relativeExampleMain. '''); 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); } void _printNoPluginMessage() { globals.printError(''' You've created a plugin project that doesn't yet support any platforms. '''); } void _printPluginAddPlatformMessage(String pluginPath) { globals.printStatus(''' To add platforms, run `flutter create -t plugin --platforms <platforms> .` under $pluginPath. For more information, see https://flutter.dev/go/plugin-platforms. '''); } // 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 '''); } }