| // 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.package): 'Generate a shareable Flutter project containing only 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<Map<CustomDimensions, String>> get usageValues async { |
| return <CustomDimensions, String>{ |
| CustomDimensions.commandCreateProjectType: stringArg('template'), |
| CustomDimensions.commandCreateAndroidLanguage: stringArg('android-language'), |
| CustomDimensions.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"' |
| ); |
| |
| 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(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(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. |
| |
| You 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 |
| '''); |
| } |
| } |