[flutter_plugin_tools] Fix build-examples for packages (#4248)

The build-examples command was filtering what it attempted to build by plugin platform, which means it never does anything for non-plugin packages. flutter/packages has steps that run this command, which suggests it used to work and regressed at some point, but nobody noticed; this will re-enable those builds so that we are getting CI coverage that the examples in flutter/packages build.

Mostly fixes https://github.com/flutter/flutter/issues/88435 (needs a flutter/packages tool pin roll to pick this up)
This commit is contained in:
stuartmorgan
2021-08-31 22:57:43 -04:00
committed by GitHub
parent da676376b3
commit 4a92b627b3
7 changed files with 357 additions and 85 deletions

View File

@ -1,3 +1,7 @@
## 0.6.0+1
- Fixed `build-examples` to work for non-plugin packages.
## 0.6.0 ## 0.6.0
- Added Android native integration test support to `native-test`. - Added Android native integration test support to `native-test`.

View File

@ -117,39 +117,65 @@ class BuildExamplesCommand extends PackageLoopingCommand {
Future<PackageResult> runForPackage(RepositoryPackage package) async { Future<PackageResult> runForPackage(RepositoryPackage package) async {
final List<String> errors = <String>[]; final List<String> errors = <String>[];
final bool isPlugin = isFlutterPlugin(package);
final Iterable<_PlatformDetails> requestedPlatforms = _platforms.entries final Iterable<_PlatformDetails> requestedPlatforms = _platforms.entries
.where( .where(
(MapEntry<String, _PlatformDetails> entry) => getBoolArg(entry.key)) (MapEntry<String, _PlatformDetails> entry) => getBoolArg(entry.key))
.map((MapEntry<String, _PlatformDetails> entry) => entry.value); .map((MapEntry<String, _PlatformDetails> entry) => entry.value);
final Set<_PlatformDetails> buildPlatforms = <_PlatformDetails>{};
final Set<_PlatformDetails> unsupportedPlatforms = <_PlatformDetails>{}; // Platform support is checked at the package level for plugins; there is
for (final _PlatformDetails platform in requestedPlatforms) { // no package-level platform information for non-plugin packages.
if (pluginSupportsPlatform(platform.pluginPlatform, package, final Set<_PlatformDetails> buildPlatforms = isPlugin
variant: platform.pluginPlatformVariant)) { ? requestedPlatforms
buildPlatforms.add(platform); .where((_PlatformDetails platform) => pluginSupportsPlatform(
} else { platform.pluginPlatform, package,
unsupportedPlatforms.add(platform); variant: platform.pluginPlatformVariant))
} .toSet()
: requestedPlatforms.toSet();
String platformDisplayList(Iterable<_PlatformDetails> platforms) {
return platforms.map((_PlatformDetails p) => p.label).join(', ');
} }
if (buildPlatforms.isEmpty) { if (buildPlatforms.isEmpty) {
final String unsupported = requestedPlatforms.length == 1 final String unsupported = requestedPlatforms.length == 1
? '${requestedPlatforms.first.label} is not supported' ? '${requestedPlatforms.first.label} is not supported'
: 'None of [${requestedPlatforms.map((_PlatformDetails p) => p.label).join(',')}] are supported'; : 'None of [${platformDisplayList(requestedPlatforms)}] are supported';
return PackageResult.skip('$unsupported by this plugin'); return PackageResult.skip('$unsupported by this plugin');
} }
print('Building for: ' print('Building for: ${platformDisplayList(buildPlatforms)}');
'${buildPlatforms.map((_PlatformDetails platform) => platform.label).join(',')}');
final Set<_PlatformDetails> unsupportedPlatforms =
requestedPlatforms.toSet().difference(buildPlatforms);
if (unsupportedPlatforms.isNotEmpty) { if (unsupportedPlatforms.isNotEmpty) {
final List<String> skippedPlatforms = unsupportedPlatforms
.map((_PlatformDetails platform) => platform.label)
.toList();
skippedPlatforms.sort();
print('Skipping unsupported platform(s): ' print('Skipping unsupported platform(s): '
'${unsupportedPlatforms.map((_PlatformDetails platform) => platform.label).join(',')}'); '${skippedPlatforms.join(', ')}');
} }
print(''); print('');
bool builtSomething = false;
for (final RepositoryPackage example in package.getExamples()) { for (final RepositoryPackage example in package.getExamples()) {
final String packageName = final String packageName =
getRelativePosixPath(example.directory, from: packagesDir); getRelativePosixPath(example.directory, from: packagesDir);
for (final _PlatformDetails platform in buildPlatforms) { for (final _PlatformDetails platform in buildPlatforms) {
// Repo policy is that a plugin must have examples configured for all
// supported platforms. For packages, just log and skip any requested
// platform that a package doesn't have set up.
if (!isPlugin &&
!example.directory
.childDirectory(platform.flutterPlatformDirectory)
.existsSync()) {
print('Skipping ${platform.label} for $packageName; not supported.');
continue;
}
builtSomething = true;
String buildPlatform = platform.label; String buildPlatform = platform.label;
if (platform.label.toLowerCase() != platform.flutterBuildType) { if (platform.label.toLowerCase() != platform.flutterBuildType) {
buildPlatform += ' (${platform.flutterBuildType})'; buildPlatform += ' (${platform.flutterBuildType})';
@ -162,6 +188,15 @@ class BuildExamplesCommand extends PackageLoopingCommand {
} }
} }
if (!builtSomething) {
if (isPlugin) {
errors.add('No examples found');
} else {
return PackageResult.skip(
'No examples found supporting requested platform(s).');
}
}
return errors.isEmpty return errors.isEmpty
? PackageResult.success() ? PackageResult.success()
: PackageResult.fail(errors); : PackageResult.fail(errors);
@ -235,6 +270,11 @@ class _PlatformDetails {
/// The `flutter build` build type. /// The `flutter build` build type.
final String flutterBuildType; final String flutterBuildType;
/// The Flutter platform directory name.
// In practice, this is the same as the plugin platform key for all platforms.
// If that changes, this can be adjusted.
String get flutterPlatformDirectory => pluginPlatform;
/// Any extra flags to pass to `flutter build`. /// Any extra flags to pass to `flutter build`.
final List<String> extraBuildFlags; final List<String> extraBuildFlags;
} }

View File

@ -237,7 +237,14 @@ abstract class PackageLoopingCommand extends PluginCommand {
continue; continue;
} }
final PackageResult result = await runForPackage(entry.package); PackageResult result;
try {
result = await runForPackage(entry.package);
} catch (e, stack) {
printError(e.toString());
printError(stack.toString());
result = PackageResult.fail(<String>['Unhandled exception']);
}
if (result.state == RunState.skipped) { if (result.state == RunState.skipped) {
final String message = final String message =
'${indentation}SKIPPING: ${result.details.first}'; '${indentation}SKIPPING: ${result.details.first}';

View File

@ -17,6 +17,11 @@ enum PlatformSupport {
federated, federated,
} }
/// Returns true if [package] is a Flutter plugin.
bool isFlutterPlugin(RepositoryPackage package) {
return _readPluginPubspecSection(package) != null;
}
/// Returns true if [package] is a Flutter [platform] plugin. /// Returns true if [package] is a Flutter [platform] plugin.
/// ///
/// It checks this by looking for the following pattern in the pubspec: /// It checks this by looking for the following pattern in the pubspec:
@ -40,7 +45,7 @@ bool pluginSupportsPlatform(
platform == kPlatformMacos || platform == kPlatformMacos ||
platform == kPlatformWindows || platform == kPlatformWindows ||
platform == kPlatformLinux); platform == kPlatformLinux);
try {
final YamlMap? platformEntry = final YamlMap? platformEntry =
_readPlatformPubspecSectionForPlugin(platform, plugin); _readPlatformPubspecSectionForPlugin(platform, plugin);
if (platformEntry == null) { if (platformEntry == null) {
@ -77,9 +82,6 @@ bool pluginSupportsPlatform(
} }
return true; return true;
} on YamlException {
return false;
}
} }
/// Returns true if [plugin] includes native code for [platform], as opposed to /// Returns true if [plugin] includes native code for [platform], as opposed to
@ -89,7 +91,6 @@ bool pluginHasNativeCodeForPlatform(String platform, RepositoryPackage plugin) {
// Web plugins are always Dart-only. // Web plugins are always Dart-only.
return false; return false;
} }
try {
final YamlMap? platformEntry = final YamlMap? platformEntry =
_readPlatformPubspecSectionForPlugin(platform, plugin); _readPlatformPubspecSectionForPlugin(platform, plugin);
if (platformEntry == null) { if (platformEntry == null) {
@ -102,11 +103,6 @@ bool pluginHasNativeCodeForPlatform(String platform, RepositoryPackage plugin) {
// in the repository use that workaround. See // in the repository use that workaround. See
// https://github.com/flutter/flutter/issues/57497 for context. // https://github.com/flutter/flutter/issues/57497 for context.
return pluginClass != null && pluginClass != 'none'; return pluginClass != null && pluginClass != 'none';
} on FileSystemException {
return false;
} on YamlException {
return false;
}
} }
/// Returns the /// Returns the
@ -118,15 +114,7 @@ bool pluginHasNativeCodeForPlatform(String platform, RepositoryPackage plugin) {
/// or the pubspec couldn't be read. /// or the pubspec couldn't be read.
YamlMap? _readPlatformPubspecSectionForPlugin( YamlMap? _readPlatformPubspecSectionForPlugin(
String platform, RepositoryPackage plugin) { String platform, RepositoryPackage plugin) {
try { final YamlMap? pluginSection = _readPluginPubspecSection(plugin);
final File pubspecFile = plugin.pubspecFile;
final YamlMap pubspecYaml =
loadYaml(pubspecFile.readAsStringSync()) as YamlMap;
final YamlMap? flutterSection = pubspecYaml['flutter'] as YamlMap?;
if (flutterSection == null) {
return null;
}
final YamlMap? pluginSection = flutterSection['plugin'] as YamlMap?;
if (pluginSection == null) { if (pluginSection == null) {
return null; return null;
} }
@ -135,9 +123,24 @@ YamlMap? _readPlatformPubspecSectionForPlugin(
return null; return null;
} }
return platforms[platform] as YamlMap?; return platforms[platform] as YamlMap?;
} on FileSystemException { }
return null;
} on YamlException { /// Returns the
/// flutter:
/// plugin:
/// platforms:
/// section from [plugin]'s pubspec.yaml, or null if either it is not present,
/// or the pubspec couldn't be read.
YamlMap? _readPluginPubspecSection(RepositoryPackage package) {
final File pubspecFile = package.pubspecFile;
if (!pubspecFile.existsSync()) {
return null; return null;
} }
final YamlMap pubspecYaml =
loadYaml(pubspecFile.readAsStringSync()) as YamlMap;
final YamlMap? flutterSection = pubspecYaml['flutter'] as YamlMap?;
if (flutterSection == null) {
return null;
}
return flutterSection['plugin'] as YamlMap?;
} }

View File

@ -1,7 +1,7 @@
name: flutter_plugin_tools name: flutter_plugin_tools
description: Productivity utils for flutter/plugins and flutter/packages description: Productivity utils for flutter/plugins and flutter/packages
repository: https://github.com/flutter/plugins/tree/master/script/tool repository: https://github.com/flutter/plugins/tree/master/script/tool
version: 0.6.0 version: 0.6.0+1
dependencies: dependencies:
args: ^2.1.0 args: ^2.1.0

View File

@ -82,6 +82,35 @@ void main() {
])); ]));
}); });
test('fails if a plugin has no examples', () async {
createFakePlugin('plugin', packagesDir,
examples: <String>[],
platformSupport: <String, PlatformDetails>{
kPlatformIos: const PlatformDetails(PlatformSupport.inline)
});
processRunner
.mockProcessesForExecutable[getFlutterCommand(mockPlatform)] =
<io.Process>[
MockProcess(exitCode: 1) // flutter packages get
];
Error? commandError;
final List<String> output = await runCapturingPrint(
runner, <String>['build-examples', '--ios'], errorHandler: (Error e) {
commandError = e;
});
expect(commandError, isA<ToolExit>());
expect(
output,
containsAllInOrder(<Matcher>[
contains('The following packages had errors:'),
contains(' plugin:\n'
' No examples found'),
]));
});
test('building for iOS when plugin is not set up for iOS results in no-op', test('building for iOS when plugin is not set up for iOS results in no-op',
() async { () async {
mockPlatform.isMacOS = true; mockPlatform.isMacOS = true;
@ -517,5 +546,138 @@ void main() {
pluginExampleDirectory.path), pluginExampleDirectory.path),
])); ]));
}); });
test('logs skipped platforms', () async {
createFakePlugin('plugin', packagesDir,
platformSupport: <String, PlatformDetails>{
kPlatformAndroid: const PlatformDetails(PlatformSupport.inline),
});
final List<String> output = await runCapturingPrint(
runner, <String>['build-examples', '--apk', '--ios', '--macos']);
expect(
output,
containsAllInOrder(<Matcher>[
contains('Skipping unsupported platform(s): iOS, macOS'),
]),
);
});
group('packages', () {
test('builds when requested platform is supported by example', () async {
final Directory packageDirectory = createFakePackage(
'package', packagesDir, isFlutter: true, extraFiles: <String>[
'example/ios/Runner.xcodeproj/project.pbxproj'
]);
final List<String> output = await runCapturingPrint(
runner, <String>['build-examples', '--ios']);
expect(
output,
containsAllInOrder(<Matcher>[
contains('Running for package'),
contains('BUILDING package/example for iOS'),
]),
);
expect(
processRunner.recordedCalls,
orderedEquals(<ProcessCall>[
ProcessCall(
getFlutterCommand(mockPlatform),
const <String>[
'build',
'ios',
'--no-codesign',
],
packageDirectory.childDirectory('example').path),
]));
});
test('skips non-Flutter examples', () async {
createFakePackage('package', packagesDir, isFlutter: false);
final List<String> output = await runCapturingPrint(
runner, <String>['build-examples', '--ios']);
expect(
output,
containsAllInOrder(<Matcher>[
contains('Running for package'),
contains('No examples found supporting requested platform(s).'),
]),
);
expect(processRunner.recordedCalls, orderedEquals(<ProcessCall>[]));
});
test('skips when there is no example', () async {
createFakePackage('package', packagesDir,
isFlutter: true, examples: <String>[]);
final List<String> output = await runCapturingPrint(
runner, <String>['build-examples', '--ios']);
expect(
output,
containsAllInOrder(<Matcher>[
contains('Running for package'),
contains('No examples found supporting requested platform(s).'),
]),
);
expect(processRunner.recordedCalls, orderedEquals(<ProcessCall>[]));
});
test('skip when example does not support requested platform', () async {
createFakePackage('package', packagesDir,
isFlutter: true,
extraFiles: <String>['example/linux/CMakeLists.txt']);
final List<String> output = await runCapturingPrint(
runner, <String>['build-examples', '--ios']);
expect(
output,
containsAllInOrder(<Matcher>[
contains('Running for package'),
contains('Skipping iOS for package/example; not supported.'),
contains('No examples found supporting requested platform(s).'),
]),
);
expect(processRunner.recordedCalls, orderedEquals(<ProcessCall>[]));
});
test('logs skipped platforms when only some are supported', () async {
final Directory packageDirectory = createFakePackage(
'package', packagesDir,
isFlutter: true,
extraFiles: <String>['example/linux/CMakeLists.txt']);
final List<String> output = await runCapturingPrint(
runner, <String>['build-examples', '--apk', '--linux']);
expect(
output,
containsAllInOrder(<Matcher>[
contains('Running for package'),
contains('Building for: Android, Linux'),
contains('Skipping Android for package/example; not supported.'),
]),
);
expect(
processRunner.recordedCalls,
orderedEquals(<ProcessCall>[
ProcessCall(
getFlutterCommand(mockPlatform),
const <String>['build', 'linux'],
packageDirectory.childDirectory('example').path),
]));
});
});
}); });
} }

View File

@ -36,6 +36,8 @@ const String _errorFile = 'errors';
const String _skipFile = 'skip'; const String _skipFile = 'skip';
// The filename within a package containing warnings to log during runForPackage. // The filename within a package containing warnings to log during runForPackage.
const String _warningFile = 'warnings'; const String _warningFile = 'warnings';
// The filename within a package indicating that it should throw.
const String _throwFile = 'throw';
void main() { void main() {
late FileSystem fileSystem; late FileSystem fileSystem;
@ -117,7 +119,7 @@ void main() {
expect(() => runCommand(command), throwsA(isA<ToolExit>())); expect(() => runCommand(command), throwsA(isA<ToolExit>()));
}); });
test('does not stop looping', () async { test('does not stop looping on error', () async {
createFakePackage('package_a', packagesDir); createFakePackage('package_a', packagesDir);
final Directory failingPackage = final Directory failingPackage =
createFakePlugin('package_b', packagesDir); createFakePlugin('package_b', packagesDir);
@ -141,6 +143,31 @@ void main() {
'${_startHeadingColor}Running for package_c...$_endColor', '${_startHeadingColor}Running for package_c...$_endColor',
])); ]));
}); });
test('does not stop looping on exceptions', () async {
createFakePackage('package_a', packagesDir);
final Directory failingPackage =
createFakePlugin('package_b', packagesDir);
createFakePackage('package_c', packagesDir);
failingPackage.childFile(_throwFile).createSync();
final TestPackageLoopingCommand command =
createTestCommand(hasLongOutput: false);
Error? commandError;
final List<String> output =
await runCommand(command, errorHandler: (Error e) {
commandError = e;
});
expect(commandError, isA<ToolExit>());
expect(
output,
containsAllInOrder(<String>[
'${_startHeadingColor}Running for package_a...$_endColor',
'${_startHeadingColor}Running for package_b...$_endColor',
'${_startHeadingColor}Running for package_c...$_endColor',
]));
});
}); });
group('package iteration', () { group('package iteration', () {
@ -437,6 +464,31 @@ void main() {
])); ]));
}); });
test('logs unhandled exceptions as errors', () async {
createFakePackage('package_a', packagesDir);
final Directory failingPackage =
createFakePlugin('package_b', packagesDir);
createFakePackage('package_c', packagesDir);
failingPackage.childFile(_throwFile).createSync();
final TestPackageLoopingCommand command =
createTestCommand(hasLongOutput: false);
Error? commandError;
final List<String> output =
await runCommand(command, errorHandler: (Error e) {
commandError = e;
});
expect(commandError, isA<ToolExit>());
expect(
output,
containsAllInOrder(<String>[
'${_startErrorColor}Exception: Uh-oh$_endColor',
'${_startErrorColor}The following packages had errors:$_endColor',
'$_startErrorColor package_b:\n Unhandled exception$_endColor',
]));
});
test('prints run summary on success', () async { test('prints run summary on success', () async {
final Directory warnPackage1 = final Directory warnPackage1 =
createFakePackage('package_a', packagesDir); createFakePackage('package_a', packagesDir);
@ -657,6 +709,10 @@ class TestPackageLoopingCommand extends PackageLoopingCommand {
if (errorFile.existsSync()) { if (errorFile.existsSync()) {
return PackageResult.fail(errorFile.readAsLinesSync()); return PackageResult.fail(errorFile.readAsLinesSync());
} }
final File throwFile = package.directory.childFile(_throwFile);
if (throwFile.existsSync()) {
throw Exception('Uh-oh');
}
return PackageResult.success(); return PackageResult.success();
} }