Skip to content
This repository was archived by the owner on Feb 22, 2023. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion script/tool/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
## NEXT
## 0.5.1+1

- Fixed `build-examples` to work for non-plugin packages.

## 0.5.1

- Added Android native integration test support to `native-test`.
- Added a new `android-lint` command to lint Android plugin native code.
Expand Down
62 changes: 49 additions & 13 deletions script/tool/lib/src/build_examples_command.dart
Original file line number Diff line number Diff line change
Expand Up @@ -99,38 +99,60 @@ class BuildExamplesCommand extends PackageLoopingCommand {
Future<PackageResult> runForPackage(Directory package) async {
final List<String> errors = <String>[];

final bool isPlugin = isFlutterPlugin(package);
final Iterable<_PlatformDetails> requestedPlatforms = _platforms.entries
.where(
(MapEntry<String, _PlatformDetails> entry) => getBoolArg(entry.key))
.map((MapEntry<String, _PlatformDetails> entry) => entry.value);
final Set<_PlatformDetails> buildPlatforms = <_PlatformDetails>{};
final Set<_PlatformDetails> unsupportedPlatforms = <_PlatformDetails>{};
for (final _PlatformDetails platform in requestedPlatforms) {
if (pluginSupportsPlatform(platform.pluginPlatform, package)) {
buildPlatforms.add(platform);
} else {
unsupportedPlatforms.add(platform);
}
}

// Platform support is checked at the package level for plugins; there is
// no package-level platform information for non-plugin packages.
final Set<_PlatformDetails> buildPlatforms = isPlugin
? requestedPlatforms
.where((_PlatformDetails platform) =>
pluginSupportsPlatform(platform.pluginPlatform, package))
.toSet()
: requestedPlatforms.toSet();
if (buildPlatforms.isEmpty) {
final String unsupported = requestedPlatforms.length == 1
? '${requestedPlatforms.first.label} is not supported'
: 'None of [${requestedPlatforms.map((_PlatformDetails p) => p.label).join(',')}] are supported';
: 'None of [${requestedPlatforms.map((_PlatformDetails p) => p.label).join(', ')}] are supported';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I ran into this too, it probably would be better to pull the join argument to a const or wrap that behavior in a function so it could be updated in one place.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I pulled out a local function for the two lines that were doing the same display list transform.

return PackageResult.skip('$unsupported by this plugin');
}
print('Building for: '
'${buildPlatforms.map((_PlatformDetails platform) => platform.label).join(',')}');
'${buildPlatforms.map((_PlatformDetails platform) => platform.label).join(', ')}');

final Set<_PlatformDetails> unsupportedPlatforms =
requestedPlatforms.toSet().difference(buildPlatforms);
if (unsupportedPlatforms.isNotEmpty) {
final List<String> skippedPlatforms = unsupportedPlatforms
.map((_PlatformDetails platform) => platform.label)
.toList();
skippedPlatforms.sort();
print('Skipping unsupported platform(s): '
'${unsupportedPlatforms.map((_PlatformDetails platform) => platform.label).join(',')}');
'${skippedPlatforms.join(', ')}');
}
print('');

for (final Directory example in getExamplesForPlugin(package)) {
bool builtSomething = false;
for (final Directory example in getExamplesForPackage(package)) {
final String packageName =
getRelativePosixPath(example, from: packagesDir);

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
.childDirectory(platform.flutterPlatformDirectory)
.existsSync()) {
print('Skipping ${platform.label} for $packageName; not supported.');
continue;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Instead of using continue do you think an else clause would be preferable?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I generally don't like to nest non-trivial primary logic in an else clause.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SGTM, not my preference but not in violation of our style. Academics have argued about it for decades: https://en.wikipedia.org/wiki/Structured_programming#Early_exit

}

builtSomething = true;

String buildPlatform = platform.label;
if (platform.label.toLowerCase() != platform.flutterBuildType) {
buildPlatform += ' (${platform.flutterBuildType})';
Expand All @@ -143,6 +165,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
? PackageResult.success()
: PackageResult.fail(errors);
Expand Down Expand Up @@ -188,6 +219,11 @@ class _PlatformDetails {
/// The `flutter build` build type.
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`.
final List<String> extraBuildFlags;
}
8 changes: 4 additions & 4 deletions script/tool/lib/src/common/plugin_command.dart
Original file line number Diff line number Diff line change
Expand Up @@ -358,10 +358,10 @@ abstract class PluginCommand extends Command<void> {
return entity is Directory && entity.childFile('pubspec.yaml').existsSync();
}

/// Returns the example Dart packages contained in the specified plugin, or
/// an empty List, if the plugin has no examples.
Iterable<Directory> getExamplesForPlugin(Directory plugin) {
final Directory exampleFolder = plugin.childDirectory('example');
/// Returns the example Flutter packages contained in the specified package,
/// or an empty iterable if the package has no Flutter examples.
Iterable<Directory> getExamplesForPackage(Directory package) {
final Directory exampleFolder = package.childDirectory('example');
if (!exampleFolder.existsSync()) {
return <Directory>[];
}
Expand Down
19 changes: 19 additions & 0 deletions script/tool/lib/src/common/plugin_utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,25 @@ enum PlatformSupport {
federated,
}

/// Returns true if [package] is a Flutter plugin.
bool isFlutterPlugin(Directory package) {
try {
final File pubspecFile = package.childFile('pubspec.yaml');
final YamlMap pubspecYaml =
loadYaml(pubspecFile.readAsStringSync()) as YamlMap;
final YamlMap? flutterSection = pubspecYaml['flutter'] as YamlMap?;
if (flutterSection == null) {
return false;
}
final YamlMap? pluginSection = flutterSection['plugin'] as YamlMap?;
return pluginSection != null;
} on FileSystemException {
return false;
} on YamlException {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't it be better to use explicit checks instead of relying on exceptions? It seems like a malformed yaml file would result in a isFlutterPlugin == false instead of crashing the tool which I would have expected.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't it be better to use explicit checks instead of relying on exceptions?

Removed the FileSystemException in favor of an explicit check. (I'm not sure why the code was originally written that way; this was just a move refactor).

It seems like a malformed yaml file would result in a isFlutterPlugin == false instead of crashing the tool which I would have expected.

In practice that wouldn't actually be a problem since we have a different CI test that validates the pubspecs, but I agree we shouldn't rely on it (and in fact, that's much more recent than this pubspec parsing code). I removed the exception handler for YamlException, and I've added a catch-all to PackageLoopingCommand's runForPackage. That covers most of the code the tool runs at this point, and is a good idea to add in general since a central goal of that command base class is to always keep going, and then report all errors at the end.

return false;
}
}

/// Returns whether the given directory contains a Flutter [platform] plugin.
///
/// It checks this by looking for the following pattern in the pubspec:
Expand Down
2 changes: 1 addition & 1 deletion script/tool/lib/src/drive_examples_command.dart
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ class DriveExamplesCommand extends PackageLoopingCommand {
int examplesFound = 0;
bool testsRan = false;
final List<String> errors = <String>[];
for (final Directory example in getExamplesForPlugin(package)) {
for (final Directory example in getExamplesForPackage(package)) {
++examplesFound;
final String exampleName =
getRelativePosixPath(example, from: packagesDir);
Expand Down
2 changes: 1 addition & 1 deletion script/tool/lib/src/list_command.dart
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ class ListCommand extends PluginCommand {
case _example:
final Stream<Directory> examples = getTargetPackages()
.map((PackageEnumerationEntry entry) => entry.directory)
.expand<Directory>(getExamplesForPlugin);
.expand<Directory>(getExamplesForPackage);
await for (final Directory package in examples) {
print(package.path);
}
Expand Down
4 changes: 2 additions & 2 deletions script/tool/lib/src/native_test_command.dart
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ this command.
});
}

final Iterable<Directory> examples = getExamplesForPlugin(plugin);
final Iterable<Directory> examples = getExamplesForPackage(plugin);

bool ranTests = false;
bool failed = false;
Expand Down Expand Up @@ -330,7 +330,7 @@ this command.

// Assume skipped until at least one test has run.
RunState overallResult = RunState.skipped;
for (final Directory example in getExamplesForPlugin(plugin)) {
for (final Directory example in getExamplesForPackage(plugin)) {
final String exampleName = getPackageDescription(example);

if (testTarget != null) {
Expand Down
2 changes: 1 addition & 1 deletion script/tool/lib/src/xcode_analyze_command.dart
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ class XcodeAnalyzeCommand extends PackageLoopingCommand {
List<String> extraFlags = const <String>[],
}) async {
bool passing = true;
for (final Directory example in getExamplesForPlugin(plugin)) {
for (final Directory example in getExamplesForPackage(plugin)) {
// Running tests and static analyzer.
final String examplePath =
getRelativePosixPath(example, from: plugin.parent);
Expand Down
2 changes: 1 addition & 1 deletion script/tool/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name: flutter_plugin_tools
description: Productivity utils for flutter/plugins and flutter/packages
repository: https://github.com/flutter/plugins/tree/master/script/tool
version: 0.5.0
version: 0.5.1+1

dependencies:
args: ^2.1.0
Expand Down
162 changes: 162 additions & 0 deletions script/tool/test/build_examples_command_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,35 @@ void main() {
]));
});

test('fails if a plugin has no examples', () async {
createFakePlugin('plugin', packagesDir,
examples: <String>[],
platformSupport: <String, PlatformSupport>{
kPlatformIos: PlatformSupport.inline
});

processRunner
.mockProcessesForExecutable[getFlutterCommand(mockPlatform)] =
<io.Process>[
MockProcess.failing() // 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',
() async {
mockPlatform.isMacOS = true;
Expand Down Expand Up @@ -432,5 +461,138 @@ void main() {
pluginExampleDirectory.path),
]));
});

test('logs skipped platforms', () async {
createFakePlugin('plugin', packagesDir,
platformSupport: <String, PlatformSupport>{
kPlatformAndroid: 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),
]));
});
});
});
}