Unverified Commit 0a08f8af authored by Chris Yang's avatar Chris Yang Committed by GitHub

[flutter_tools] Display "no platforms" message based on results when creating...

[flutter_tools] Display "no platforms" message based on results when creating plugins project (#70215)
parent 2b85eeed
......@@ -2,8 +2,6 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:yaml/yaml.dart';
import '../android/gradle_utils.dart' as gradle;
import '../base/common.dart';
import '../base/context.dart';
......@@ -17,19 +15,11 @@ import '../features.dart';
import '../flutter_manifest.dart';
import '../flutter_project_metadata.dart';
import '../globals.dart' as globals;
import '../plugins.dart';
import '../project.dart';
import '../reporting/reporting.dart';
import '../runner/flutter_command.dart';
import 'create_base.dart';
const String _kNoPlatformsErrorMessage = '''
The plugin project was generated without specifying the `--platforms` flag, no new platforms are added.
To add platforms, run `flutter create -t plugin --platforms <platforms> .` under the same
directory. You can also find detailed instructions on how to add platforms in the `pubspec.yaml`
at https://flutter.dev/docs/development/packages-and-plugins/developing-packages#plugin-platforms.
''';
class CreateCommand extends CreateBase {
CreateCommand() {
addPlatformsOptions(customHelp: 'The platforms supported by this project. '
......@@ -249,7 +239,8 @@ class CreateCommand extends CreateBase {
);
final String relativeDirPath = globals.fs.path.relative(projectDirPath);
if (!projectDir.existsSync() || projectDir.listSync().isEmpty) {
final bool creatingNewProject = !projectDir.existsSync() || projectDir.listSync().isEmpty;
if (creatingNewProject) {
globals.printStatus('Creating project $relativeDirPath...');
} else {
if (sampleCode != null && !overwrite) {
......@@ -295,18 +286,24 @@ class CreateCommand extends CreateBase {
'main.dart',
));
globals.printStatus('Your module code is in $relativeMainPath.');
} else {
} 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){
globals.printError(_kNoPlatformsArgMessage);
}
} else {
// Tell the user the next steps.
final FlutterProject project = FlutterProject.fromPath(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 String relativePluginPath = globals.fs.path.normalize(globals.fs.path.relative(projectDirPath));
final String relativePluginMain = globals.fs.path.join(relativePluginPath, 'lib', '$projectName.dart');
// Let them know a summary of the state of their tooling.
final List<String> platforms = _getSupportedPlatformsFromTemplateContext(templateContext);
final String platformsString = platforms.join(', ');
globals.printStatus('''
In order to run your $application, type:
......@@ -315,14 +312,6 @@ In order to run your $application, type:
Your $application code is in $relativeAppMain.
''');
if (generatePlugin) {
globals.printStatus('''
Your plugin code is in $relativePluginMain.
Host platform code is in the $platformsString directories under $relativePluginPath.
To edit platform code in an IDE see https://flutter.dev/developing-packages/#edit-plugin-package.
''');
}
}
return FlutterCommandResult.success();
}
......@@ -378,19 +367,13 @@ To edit platform code in an IDE see https://flutter.dev/developing-packages/#edi
templateContext['linux'] = false;
templateContext['macos'] = false;
templateContext['windows'] = false;
globals.printError(_kNoPlatformsErrorMessage);
}
final List<String> platformsToAdd = _getSupportedPlatformsFromTemplateContext(templateContext);
final String pubspecPath = globals.fs.path.join(directory.absolute.path, 'pubspec.yaml');
final FlutterManifest manifest = FlutterManifest.createFromPath(pubspecPath, fileSystem: globals.fs, logger: globals.logger);
List<String> existingPlatforms = <String>[];
if (manifest.supportedPlatforms != null) {
existingPlatforms = manifest.supportedPlatforms.keys.toList();
for (final String existingPlatform in existingPlatforms) {
// re-generate files for existing platforms
templateContext[existingPlatform] = true;
}
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;
......@@ -411,35 +394,6 @@ To edit platform code in an IDE see https://flutter.dev/developing-packages/#edi
);
}
final bool addPlatformsToExistingPlugin = willAddPlatforms && existingPlatforms.isNotEmpty;
if (addPlatformsToExistingPlugin) {
// If adding new platforms to an existing plugin project, prints
// a help message containing the platforms maps need to be added to the `platforms` key in the pubspec.
platformsToAdd.removeWhere(existingPlatforms.contains);
final YamlMap platformsMapToPrint = Plugin.createPlatformsYamlMap(platformsToAdd, templateContext['pluginClass'] as String, templateContext['androidIdentifier'] as String);
if (platformsMapToPrint.isNotEmpty) {
String prettyYaml = '';
for (final String platform in platformsMapToPrint.keys.toList().cast<String>()) {
prettyYaml += '$platform:\n';
for (final String key in (platformsMapToPrint[platform] as YamlMap).keys.toList().cast<String>()) {
prettyYaml += ' $key: ${platformsMapToPrint[platform][key] as String}\n';
}
}
globals.printStatus('''
The `pubspec.yaml` under the project directory must be updated to support ${platformsToAdd.join(', ')},
Add below lines to under the `platform:` key:
''', emphasis: true);
globals.printStatus(prettyYaml, emphasis: true, color: TerminalColor.blue);
globals.printStatus('''
If the `platforms` key does not exist in the `pubspec.yaml`, it might because that the plugin project does not
use the multi-platforms plugin format. We highly recommend a migration to the multi-platforms plugin format.
For detailed instructions on how to format the pubspec.yaml to support platforms using the multi-platforms format, see:
https://flutter.dev/docs/development/packages-and-plugins/developing-packages#plugin-platforms
''', emphasis: true);
}
}
final FlutterProject project = FlutterProject.fromDirectory(directory);
final bool generateAndroid = templateContext['android'] == true;
if (generateAndroid) {
......@@ -493,4 +447,57 @@ https://flutter.dev/docs/development/packages-and-plugins/developing-packages#pl
'macos',
];
}
// 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.
For more information, see https://flutter.dev/go/developing-plugins.
''', emphasis: true, color: TerminalColor.red);
}
const String _kNoPlatformsArgMessage = '''
Must specify at least one platform using --platforms.
For more information, see https://flutter.dev/go/developing-plugins.
''';
......@@ -12,6 +12,10 @@ import 'base/user_messages.dart';
import 'base/utils.dart';
import 'plugins.dart';
const Set<String> _kValidPluginPlatforms = <String>{
'android', 'ios', 'web', 'windows', 'linux', 'macos'
};
/// A wrapper around the `flutter` section in the `pubspec.yaml` file.
class FlutterManifest {
FlutterManifest._(this._logger);
......@@ -199,6 +203,20 @@ class FlutterManifest {
return null;
}
/// Like [supportedPlatforms], but only returns the valid platforms that are supported in flutter plugins.
Map<String, dynamic> get validSupportedPlatforms {
final Map<String, dynamic> allPlatforms = supportedPlatforms;
if (allPlatforms == null) {
return null;
}
final Map<String, dynamic> platforms = <String, dynamic>{}..addAll(supportedPlatforms);
platforms.removeWhere((String key, dynamic _) => !_kValidPluginPlatforms.contains(key));
if (platforms.isEmpty) {
return null;
}
return platforms;
}
List<Map<String, dynamic>> get fontsDescriptor {
return fonts.map((Font font) => font.descriptor).toList();
}
......
......@@ -33,7 +33,7 @@ flutter:
platforms:
{{#no_platforms}}
# This plugin project was generated without specifying any
# platforms with the `--platform` argument. If you see the `fake_platform` map below, remove it and
# platforms with the `--platform` argument. If you see the `some_platform` map below, remove it and
# then add platforms following the instruction here:
# https://flutter.dev/docs/development/packages-and-plugins/developing-packages#plugin-platforms
# -------------------
......
......@@ -32,6 +32,7 @@ import '../../src/context.dart';
import '../../src/pubspec_schema.dart';
import '../../src/testbed.dart';
const String _kNoPlatformsMessage = 'Must specify at least one platform using --platforms.\n';
const String frameworkRevision = '12345678';
const String frameworkChannel = 'omega';
// TODO(fujino): replace FakePlatform.fromPlatform() with FakePlatform()
......@@ -390,8 +391,6 @@ void main() {
'android/src/main/java/com/example/flutter_project/FlutterProjectPlugin.java',
'example/android/app/src/main/java/com/example/flutter_project_example/MainActivity.java',
'lib/flutter_project_web.dart',
// TODO(cyanglaz): no-op iOS folder should be removed after 1.20.0 release
// https://github.com/flutter/flutter/issues/59787
],
);
return _runFlutterTest(projectDir.childDirectory('example'));
......@@ -422,6 +421,7 @@ void main() {
// The platform is correctly registered
expect(pubspec.flutter['plugin']['platforms']['web']['pluginClass'], 'FlutterProjectWeb');
expect(pubspec.flutter['plugin']['platforms']['web']['fileName'], 'flutter_project_web.dart');
expect(logger.errorText, isNot(contains(_kNoPlatformsMessage)));
}, overrides: <Type, Generator>{
FeatureFlags: () => TestFeatureFlags(isWebEnabled: true),
Pub: () => Pub(
......@@ -432,6 +432,7 @@ void main() {
botDetector: globals.botDetector,
platform: globals.platform,
),
Logger: ()=>logger,
});
testUsingContext('plugin example app depends on plugin', () async {
......@@ -661,7 +662,9 @@ void main() {
// Import for the new embedding class.
expect(mainActivity.contains('import io.flutter.embedding.android.FlutterActivity'), true);
expect(testLogger.statusText, isNot(contains('https://flutter.dev/go/android-project-migration')));
expect(logger.statusText, isNot(contains('https://flutter.dev/go/android-project-migration')));
}, overrides: <Type, Generator>{
Logger: () => logger,
});
testUsingContext('app does not include desktop or web by default', () async {
......@@ -732,8 +735,10 @@ void main() {
expect(projectDir.childDirectory('windows'), isNot(exists));
expect(projectDir.childDirectory('macos'), isNot(exists));
expect(projectDir.childDirectory('web'), isNot(exists));
expect(logger.errorText, isNot(contains(_kNoPlatformsMessage)));
}, overrides: <Type, Generator>{
FeatureFlags: () => TestFeatureFlags(isLinuxEnabled: true),
Logger: () => logger,
});
testUsingContext('plugin supports Linux if requested', () async {
......@@ -767,8 +772,10 @@ void main() {
],
pluginClass: 'FlutterProjectPlugin',
unexpectedPlatforms: <String>['some_platform']);
expect(logger.errorText, isNot(contains(_kNoPlatformsMessage)));
}, overrides: <Type, Generator>{
FeatureFlags: () => TestFeatureFlags(isLinuxEnabled: true),
Logger: () => logger,
});
testUsingContext('app supports macOS if requested', () async {
......@@ -794,8 +801,10 @@ void main() {
expect(projectDir.childDirectory('linux'), isNot(exists));
expect(projectDir.childDirectory('windows'), isNot(exists));
expect(projectDir.childDirectory('web'), isNot(exists));
expect(logger.errorText, isNot(contains(_kNoPlatformsMessage)));
}, overrides: <Type, Generator>{
FeatureFlags: () => TestFeatureFlags(isMacOSEnabled: true),
Logger: () => logger,
});
testUsingContext('plugin supports macOS if requested', () async {
......@@ -826,8 +835,10 @@ void main() {
'macos',
], pluginClass: 'FlutterProjectPlugin',
unexpectedPlatforms: <String>['some_platform']);
expect(logger.errorText, isNot(contains(_kNoPlatformsMessage)));
}, overrides: <Type, Generator>{
FeatureFlags: () => TestFeatureFlags(isMacOSEnabled: true),
Logger: () => logger,
});
testUsingContext('app supports Windows if requested', () async {
......@@ -852,8 +863,10 @@ void main() {
expect(projectDir.childDirectory('linux'), isNot(exists));
expect(projectDir.childDirectory('macos'), isNot(exists));
expect(projectDir.childDirectory('web'), isNot(exists));
expect(logger.errorText, isNot(contains(_kNoPlatformsMessage)));
}, overrides: <Type, Generator>{
FeatureFlags: () => TestFeatureFlags(isWindowsEnabled: true),
Logger: () => logger,
});
testUsingContext('Windows has correct VERSIONINFO', () async {
......@@ -914,8 +927,10 @@ void main() {
'windows'
], pluginClass: 'FlutterProjectPlugin',
unexpectedPlatforms: <String>['some_platform']);
expect(logger.errorText, isNot(contains(_kNoPlatformsMessage)));
}, overrides: <Type, Generator>{
FeatureFlags: () => TestFeatureFlags(isWindowsEnabled: true),
Logger: () => logger,
});
testUsingContext('app supports web if requested', () async {
......@@ -941,8 +956,10 @@ void main() {
expect(projectDir.childDirectory('linux'), isNot(exists));
expect(projectDir.childDirectory('macos'), isNot(exists));
expect(projectDir.childDirectory('windows'), isNot(exists));
expect(logger.errorText, isNot(contains(_kNoPlatformsMessage)));
}, overrides: <Type, Generator>{
FeatureFlags: () => TestFeatureFlags(isWebEnabled: true),
Logger: () => logger,
});
testUsingContext('plugin uses new platform schema', () async {
......@@ -1652,8 +1669,6 @@ void main() {
await runner.run(<String>['create', '--no-pub', '--template=plugin', projectDir.path]);
// TODO(cyanglaz): no-op iOS folder should be removed after 1.20.0 release
// https://github.com/flutter/flutter/issues/59787
expect(projectDir.childDirectory('ios'), isNot(exists));
expect(projectDir.childDirectory('android'), isNot(exists));
expect(projectDir.childDirectory('web'), isNot(exists));
......@@ -1661,8 +1676,6 @@ void main() {
expect(projectDir.childDirectory('windows'), isNot(exists));
expect(projectDir.childDirectory('macos'), isNot(exists));
// TODO(cyanglaz): no-op iOS folder should be removed after 1.20.0 release
// https://github.com/flutter/flutter/issues/59787
expect(projectDir.childDirectory('example').childDirectory('ios'),
isNot(exists));
expect(projectDir.childDirectory('example').childDirectory('android'),
......@@ -1683,7 +1696,6 @@ void main() {
FeatureFlags: () => TestFeatureFlags(isLinuxEnabled: false),
});
testUsingContext('plugin supports ios if requested', () async {
Cache.flutterRoot = '../..';
when(mockFlutterVersion.frameworkRevision).thenReturn(frameworkRevision);
......@@ -1700,8 +1712,10 @@ void main() {
'ios',
], pluginClass: 'FlutterProjectPlugin',
unexpectedPlatforms: <String>['some_platform']);
expect(logger.errorText, isNot(contains(_kNoPlatformsMessage)));
}, overrides: <Type, Generator>{
FeatureFlags: () => TestFeatureFlags(isLinuxEnabled: false),
Logger: () => logger,
});
testUsingContext('plugin supports android if requested', () async {
......@@ -1722,8 +1736,10 @@ void main() {
], pluginClass: 'FlutterProjectPlugin',
unexpectedPlatforms: <String>['some_platform'],
androidIdentifier: 'com.example.flutter_project');
expect(logger.errorText, isNot(contains(_kNoPlatformsMessage)));
}, overrides: <Type, Generator>{
FeatureFlags: () => TestFeatureFlags(isLinuxEnabled: false),
Logger: () => logger,
});
testUsingContext('plugin supports web if requested', () async {
......@@ -1744,8 +1760,10 @@ void main() {
unexpectedPlatforms: <String>['some_platform'],
androidIdentifier: 'com.example.flutter_project',
webFileName: 'flutter_project_web.dart');
expect(logger.errorText, isNot(contains(_kNoPlatformsMessage)));
}, overrides: <Type, Generator>{
FeatureFlags: () => TestFeatureFlags(isWebEnabled: true),
Logger: () => logger,
});
testUsingContext('plugin doe not support web if feature is not enabled', () async {
......@@ -1764,8 +1782,10 @@ void main() {
'some_platform'
], pluginClass: 'somePluginClass',
unexpectedPlatforms: <String>['web']);
expect(logger.errorText, contains(_kNoPlatformsMessage));
}, overrides: <Type, Generator>{
FeatureFlags: () => TestFeatureFlags(isWebEnabled: false),
Logger: () => logger,
});
testUsingContext('create an empty plugin, then add ios', () async {
......@@ -2081,7 +2101,9 @@ void main() {
final CreateCommand command = CreateCommand();
final CommandRunner<void> runner = createTestCommandRunner(command);
await runner.run(<String>['create', '--no-pub', '--template=plugin', '--platforms=android', projectDir.path]);
expect(logger.statusText, isNot(contains('The `pubspec.yaml` under the project directory must be updated to support')));
final String projectDirPath = globals.fs.path.normalize(projectDir.absolute.path);
final String relativePluginPath = globals.fs.path.normalize(globals.fs.path.relative(projectDirPath));
expect(logger.statusText, isNot(contains('You need to update $relativePluginPath/pubspec.yaml to support android.\n')));
}, overrides: <Type, Generator> {
Logger: () => logger,
});
......@@ -2093,10 +2115,12 @@ void main() {
final CreateCommand command = CreateCommand();
final CommandRunner<void> runner = createTestCommandRunner(command);
final String projectDirPath = globals.fs.path.normalize(projectDir.absolute.path);
final String relativePluginPath = globals.fs.path.normalize(globals.fs.path.relative(projectDirPath));
await runner.run(<String>['create', '--no-pub', '--template=plugin', '--platforms=ios', projectDir.path]);
expect(logger.statusText, isNot(contains('The `pubspec.yaml` under the project directory must be updated to support')));
expect(logger.statusText, isNot(contains('You need to update $relativePluginPath/pubspec.yaml to support ios.\n')));
await runner.run(<String>['create', '--no-pub', '--template=plugin', '--platforms=android', projectDir.path]);
expect(logger.statusText, contains('The `pubspec.yaml` under the project directory must be updated to support'));
expect(logger.statusText, contains('You need to update $relativePluginPath/pubspec.yaml to support android.\n'));
}, overrides: <Type, Generator> {
Logger: () => logger,
});
......@@ -2276,6 +2300,39 @@ void main() {
}, overrides: <Type, Generator>{
FeatureFlags: () => TestFeatureFlags(isWindowsEnabled: true),
});
testUsingContext('created plugin supports no platforms should print `no platforms` message', () async {
Cache.flutterRoot = '../..';
when(mockFlutterVersion.frameworkRevision).thenReturn(frameworkRevision);
when(mockFlutterVersion.channel).thenReturn(frameworkChannel);
final CreateCommand command = CreateCommand();
final CommandRunner<void> runner = createTestCommandRunner(command);
await runner.run(<String>['create', '--no-pub', '--template=plugin', projectDir.path]);
expect(logger.errorText, contains(_kNoPlatformsMessage));
}, overrides: <Type, Generator>{
FeatureFlags: () => TestFeatureFlags(isLinuxEnabled: false),
Logger: ()=> logger,
});
testUsingContext('created plugin with no --platforms flag should not print `no platforms` message if the existing plugin supports a platform.', () async {
Cache.flutterRoot = '../..';
when(mockFlutterVersion.frameworkRevision).thenReturn(frameworkRevision);
when(mockFlutterVersion.channel).thenReturn(frameworkChannel);
final CreateCommand command = CreateCommand();
final CommandRunner<void> runner = createTestCommandRunner(command);
await runner.run(<String>['create', '--no-pub', '--template=plugin', '--platforms=ios', projectDir.path]);
await runner.run(<String>['create', '--no-pub', '--template=plugin', projectDir.path]);
expect(logger.errorText, isNot(contains(_kNoPlatformsMessage)));
}, overrides: <Type, Generator>{
FeatureFlags: () => TestFeatureFlags(isLinuxEnabled: false),
Logger: () => logger,
});
}
Future<void> _createProject(
......
......@@ -972,6 +972,49 @@ flutter:
expect(flutterManifest.supportedPlatforms, null);
});
testWithoutContext('FlutterManifest validSupportedPlatforms return null if the platform keys are not valid', () {
const String manifest = '''
name: test
flutter:
plugin:
platforms:
some_platform:
pluginClass: SomeClass
''';
final BufferLogger logger = BufferLogger.test();
final FlutterManifest flutterManifest = FlutterManifest.createFromString(
manifest,
logger: logger,
);
expect(flutterManifest.isPlugin, true);
expect(flutterManifest.validSupportedPlatforms, null);
});
testWithoutContext('FlutterManifest validSupportedPlatforms only returns valid platforms', () {
const String manifest = '''
name: test
flutter:
plugin:
platforms:
some_platform:
pluginClass: SomeClass
ios:
pluginClass: SomeClass
''';
final BufferLogger logger = BufferLogger.test();
final FlutterManifest flutterManifest = FlutterManifest.createFromString(
manifest,
logger: logger,
);
expect(flutterManifest.isPlugin, true);
expect(flutterManifest.validSupportedPlatforms['ios'],
<String, dynamic>{'pluginClass': 'SomeClass'});
expect(flutterManifest.validSupportedPlatforms['some_platform'],
isNull);
});
testWithoutContext('FlutterManifest getSupportedPlatforms returns valid platforms.', () {
const String manifest = '''
name: test
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment