[flutter_tools] Ensure flutter daemon clients can detect preview device (#140112)

Part of https://github.com/flutter/flutter/issues/130277
This commit is contained in:
Christopher Fujino 2023-12-21 11:01:16 -08:00 committed by GitHub
parent 90badf7050
commit 674fbd26bc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 359 additions and 81 deletions

View file

@ -62,14 +62,22 @@ The `shutdown()` command will terminate the flutter daemon. It is not necessary
#### daemon.getSupportedPlatforms
The `getSupportedPlatforms()` command will enumerate all platforms supported by the project located at the provided `projectRoot`. It returns a Map with the key 'platforms' containing a List of strings which describe the set of all possibly supported platforms. Possible values include:
- android
- ios
- linux #experimental
- macos #experimental
- windows #experimental
- fuchsia #experimental
- web #experimental
The `getSupportedPlatforms()` command will enumerate all platforms supported
by the project located at the provided `projectRoot`. It returns a Map with
the key 'platformTypes' containing a Map of platform types to a Map with the
following entries:
- isSupported (bool) - whether or not the platform type is supported
- reasons (List<Map<String, Object>>, only included if isSupported == false) - a list of reasons why the platform is not supported
The schema for each element in `reasons` is:
- reasonText (String) - a description of why the platform is not supported
- fixText (String) - human readable instructions of how to fix this reason
- fixCode (String) - stringified version of the `_ReasonCode` enum. To be used
by daemon clients who intend to auto-fix.
The possible platform types are the `PlatformType` enumeration in the lib/src/device.dart library.
#### Events

View file

@ -416,36 +416,167 @@ class DaemonDomain extends Domain {
/// is correct.
Future<Map<String, Object>> getSupportedPlatforms(Map<String, Object?> args) async {
final String? projectRoot = _getStringArg(args, 'projectRoot', required: true);
final List<String> result = <String>[];
final List<String> platformTypes = <String>[];
final Map<String, Object> platformTypesMap = <String, Object>{};
try {
final FlutterProject flutterProject = FlutterProject.fromDirectory(globals.fs.directory(projectRoot));
final Set<SupportedPlatform> supportedPlatforms = flutterProject.getSupportedPlatforms().toSet();
if (featureFlags.isLinuxEnabled && supportedPlatforms.contains(SupportedPlatform.linux)) {
result.add('linux');
}
if (featureFlags.isMacOSEnabled && supportedPlatforms.contains(SupportedPlatform.macos)) {
result.add('macos');
}
if (featureFlags.isWindowsEnabled && supportedPlatforms.contains(SupportedPlatform.windows)) {
result.add('windows');
}
if (featureFlags.isIOSEnabled && supportedPlatforms.contains(SupportedPlatform.ios)) {
result.add('ios');
}
if (featureFlags.isAndroidEnabled && supportedPlatforms.contains(SupportedPlatform.android)) {
result.add('android');
}
if (featureFlags.isWebEnabled && supportedPlatforms.contains(SupportedPlatform.web)) {
result.add('web');
}
if (featureFlags.isFuchsiaEnabled && supportedPlatforms.contains(SupportedPlatform.fuchsia)) {
result.add('fuchsia');
}
if (featureFlags.areCustomDevicesEnabled) {
result.add('custom');
void handlePlatformType(
PlatformType platform,
) {
final List<Map<String, Object>> reasons = <Map<String, Object>>[];
switch (platform) {
case PlatformType.linux:
if (!featureFlags.isLinuxEnabled) {
reasons.add(<String, Object>{
'reasonText': 'the Linux feature is not enabled',
'fixText': 'Run "flutter config --enable-linux-desktop"',
'fixCode': _ReasonCode.config.name,
});
}
if (!supportedPlatforms.contains(SupportedPlatform.linux)) {
reasons.add(<String, Object>{
'reasonText': 'the Linux platform is not enabled for this project',
'fixText': 'Run "flutter create --platforms=linux ." in your application directory',
'fixCode': _ReasonCode.create.name,
});
}
case PlatformType.macos:
if (!featureFlags.isMacOSEnabled) {
reasons.add(<String, Object>{
'reasonText': 'the macOS feature is not enabled',
'fixText': 'Run "flutter config --enable-macos-desktop"',
'fixCode': _ReasonCode.config.name,
});
}
if (!supportedPlatforms.contains(SupportedPlatform.macos)) {
reasons.add(<String, Object>{
'reasonText': 'the macOS platform is not enabled for this project',
'fixText': 'Run "flutter create --platforms=macos ." in your application directory',
'fixCode': _ReasonCode.create.name,
});
}
case PlatformType.windows:
if (!featureFlags.isWindowsEnabled) {
reasons.add(<String, Object>{
'reasonText': 'the Windows feature is not enabled',
'fixText': 'Run "flutter config --enable-windows-desktop"',
'fixCode': _ReasonCode.config.name,
});
}
if (!supportedPlatforms.contains(SupportedPlatform.windows)) {
reasons.add(<String, Object>{
'reasonText': 'the Windows platform is not enabled for this project',
'fixText': 'Run "flutter create --platforms=windows ." in your application directory',
'fixCode': _ReasonCode.create.name,
});
}
case PlatformType.ios:
if (!featureFlags.isIOSEnabled) {
reasons.add(<String, Object>{
'reasonText': 'the iOS feature is not enabled',
'fixText': 'Run "flutter config --enable-ios"',
'fixCode': _ReasonCode.config.name,
});
}
if (!supportedPlatforms.contains(SupportedPlatform.ios)) {
reasons.add(<String, Object>{
'reasonText': 'the iOS platform is not enabled for this project',
'fixText': 'Run "flutter create --platforms=ios ." in your application directory',
'fixCode': _ReasonCode.create.name,
});
}
case PlatformType.android:
if (!featureFlags.isAndroidEnabled) {
reasons.add(<String, Object>{
'reasonText': 'the Android feature is not enabled',
'fixText': 'Run "flutter config --enable-android"',
'fixCode': _ReasonCode.config.name,
});
}
if (!supportedPlatforms.contains(SupportedPlatform.android)) {
reasons.add(<String, Object>{
'reasonText': 'the Android platform is not enabled for this project',
'fixText': 'Run "flutter create --platforms=android ." in your application directory',
'fixCode': _ReasonCode.create.name,
});
}
case PlatformType.web:
if (!featureFlags.isWebEnabled) {
reasons.add(<String, Object>{
'reasonText': 'the Web feature is not enabled',
'fixText': 'Run "flutter config --enable-web"',
'fixCode': _ReasonCode.config.name,
});
}
if (!supportedPlatforms.contains(SupportedPlatform.web)) {
reasons.add(<String, Object>{
'reasonText': 'the Web platform is not enabled for this project',
'fixText': 'Run "flutter create --platforms=web ." in your application directory',
'fixCode': _ReasonCode.create.name,
});
}
case PlatformType.fuchsia:
if (!featureFlags.isFuchsiaEnabled) {
reasons.add(<String, Object>{
'reasonText': 'the Fuchsia feature is not enabled',
'fixText': 'Run "flutter config --enable-fuchsia"',
'fixCode': _ReasonCode.config.name,
});
}
if (!supportedPlatforms.contains(SupportedPlatform.fuchsia)) {
reasons.add(<String, Object>{
'reasonText': 'the Fuchsia platform is not enabled for this project',
'fixText': 'Run "flutter create --platforms=fuchsia ." in your application directory',
'fixCode': _ReasonCode.create.name,
});
}
case PlatformType.custom:
if (!featureFlags.areCustomDevicesEnabled) {
reasons.add(<String, Object>{
'reasonText': 'the custom devices feature is not enabled',
'fixText': 'Run "flutter config --enable-custom-devices"',
'fixCode': _ReasonCode.config.name,
});
}
case PlatformType.windowsPreview:
// TODO(fujino): detect if there any plugins with native code
if (!featureFlags.isPreviewDeviceEnabled) {
reasons.add(<String, Object>{
'reasonText': 'the Preview Device feature is not enabled',
'fixText': 'Run "flutter config --enable-flutter-preview',
'fixCode': _ReasonCode.config.name,
});
}
if (!supportedPlatforms.contains(SupportedPlatform.windows)) {
reasons.add(<String, Object>{
'reasonText': 'the Windows platform is not enabled for this project',
'fixText': 'Run "flutter create --platforms=windows ." in your application directory',
'fixCode': _ReasonCode.create.name,
});
}
}
if (reasons.isEmpty) {
platformTypes.add(platform.name);
platformTypesMap[platform.name] = const <String, Object>{
'isSupported': true,
};
} else {
platformTypesMap[platform.name] = <String, Object>{
'isSupported': false,
'reasons': reasons,
};
}
}
PlatformType.values.forEach(handlePlatformType);
return <String, Object>{
'platforms': result,
// TODO(fujino): delete this key https://github.com/flutter/flutter/issues/140473
'platforms': platformTypes,
'platformTypes': platformTypesMap,
};
} on Exception catch (err, stackTrace) {
sendEvent('log', <String, Object?>{
@ -454,12 +585,16 @@ class DaemonDomain extends Domain {
'error': true,
});
// On any sort of failure, fall back to Android and iOS for backwards
// comparability.
return <String, Object>{
// compatibility.
return const <String, Object>{
'platforms': <String>[
'android',
'ios',
],
'platformTypes': <String, Object>{
'android': <String, Object>{'isSupported': true},
'ios': <String, Object>{'isSupported': true},
},
};
}
}
@ -470,6 +605,14 @@ class DaemonDomain extends Domain {
}
}
/// The reason a [PlatformType] is not currently supported.
///
/// The [name] of this value will be sent as a response to daemon client.
enum _ReasonCode {
create,
config,
}
typedef RunOrAttach = Future<void> Function({
Completer<DebugConnectionInfo>? connectionInfoCompleter,
Completer<void>? appStartedCompleter,

View file

@ -45,34 +45,20 @@ enum Category {
/// The platform sub-folder that a device type supports.
enum PlatformType {
web._('web'),
android._('android'),
ios._('ios'),
linux._('linux'),
macos._('macos'),
windows._('windows'),
fuchsia._('fuchsia'),
custom._('custom');
const PlatformType._(this.value);
final String value;
web,
android,
ios,
linux,
macos,
windows,
fuchsia,
custom,
windowsPreview;
@override
String toString() => value;
String toString() => name;
static PlatformType? fromString(String platformType) {
return const <String, PlatformType>{
'web': web,
'android': android,
'ios': ios,
'linux': linux,
'macos': macos,
'windows': windows,
'fuchsia': fuchsia,
'custom': custom,
}[platformType];
}
static PlatformType? fromString(String platformType) => values.asNameMap()[platformType];
}
/// A discovery mechanism for flutter-supported development devices.

View file

@ -29,7 +29,7 @@ BundleBuilder _defaultBundleBuilder() {
return BundleBuilder();
}
class PreviewDeviceDiscovery extends DeviceDiscovery {
class PreviewDeviceDiscovery extends PollingDeviceDiscovery {
PreviewDeviceDiscovery({
required Platform platform,
required Artifacts artifacts,
@ -42,7 +42,8 @@ class PreviewDeviceDiscovery extends DeviceDiscovery {
_processManager = processManager,
_fileSystem = fileSystem,
_platform = platform,
_features = featureFlags;
_features = featureFlags,
super('Flutter preview device');
final Platform _platform;
final Artifacts _artifacts;
@ -61,9 +62,8 @@ class PreviewDeviceDiscovery extends DeviceDiscovery {
List<String> get wellKnownIds => <String>['preview'];
@override
Future<List<Device>> devices({
Future<List<Device>> pollingGetDevices({
Duration? timeout,
DeviceDiscoveryFilter? filter,
}) async {
final File previewBinary = _fileSystem.file(_artifacts.getArtifactPath(Artifact.flutterPreviewDevice));
if (!previewBinary.existsSync()) {
@ -76,16 +76,8 @@ class PreviewDeviceDiscovery extends DeviceDiscovery {
processManager: _processManager,
previewBinary: previewBinary,
);
final bool matchesRequirements;
if (!_features.isPreviewDeviceEnabled) {
matchesRequirements = false;
} else if (filter == null) {
matchesRequirements = true;
} else {
matchesRequirements = await filter.matchesRequirements(device);
}
return <Device>[
if (matchesRequirements)
if (_features.isPreviewDeviceEnabled)
device,
];
}
@ -114,7 +106,7 @@ class PreviewDevice extends Device {
_fileSystem = fileSystem,
_bundleBuilderFactory = builderFactory,
_artifacts = artifacts,
super('preview', ephemeral: false, category: Category.desktop, platformType: PlatformType.custom);
super('preview', ephemeral: false, category: Category.desktop, platformType: PlatformType.windowsPreview);
final ProcessManager _processManager;
final Logger _logger;
@ -161,7 +153,7 @@ class PreviewDevice extends Device {
bool isSupportedForProject(FlutterProject flutterProject) => true;
@override
String get name => 'preview';
String get name => 'Preview';
@override
DevicePortForwarder get portForwarder => const NoOpDevicePortForwarder();

View file

@ -7,10 +7,12 @@ import 'dart:io' as io;
import 'dart:typed_data';
import 'package:fake_async/fake_async.dart';
import 'package:file/memory.dart';
import 'package:file/src/interface/file.dart';
import 'package:flutter_tools/src/android/android_device.dart';
import 'package:flutter_tools/src/android/android_workflow.dart';
import 'package:flutter_tools/src/application_package.dart';
import 'package:flutter_tools/src/artifacts.dart';
import 'package:flutter_tools/src/base/dds.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/base/utils.dart';
@ -22,8 +24,10 @@ import 'package:flutter_tools/src/features.dart';
import 'package:flutter_tools/src/fuchsia/fuchsia_workflow.dart';
import 'package:flutter_tools/src/globals.dart' as globals;
import 'package:flutter_tools/src/ios/ios_workflow.dart';
import 'package:flutter_tools/src/preview_device.dart';
import 'package:flutter_tools/src/resident_runner.dart';
import 'package:flutter_tools/src/vmservice.dart';
import 'package:flutter_tools/src/windows/windows_workflow.dart';
import 'package:test/fake.dart';
import '../../src/common.dart';
@ -121,10 +125,91 @@ void main() {
expect(response.data['id'], 0);
expect(response.data['result'], isNotEmpty);
expect((response.data['result']! as Map<String, Object?>)['platforms'], <String>{'macos'});
expect(
response.data['result']! as Map<String, Object?>,
const <String, Object>{
'platforms': <String>['macos', 'windows', 'windowsPreview'],
'platformTypes': <String, Map<String, Object>>{
'web': <String, Object>{
'isSupported': false,
'reasons': <Map<String, String>>[
<String, String>{
'reasonText': 'the Web feature is not enabled',
'fixText': 'Run "flutter config --enable-web"',
'fixCode': 'config',
},
],
},
'android': <String, Object>{
'isSupported': false,
'reasons': <Map<String, String>>[
<String, String>{
'reasonText': 'the Android feature is not enabled',
'fixText': 'Run "flutter config --enable-android"',
'fixCode': 'config',
},
],
},
'ios': <String, Object>{
'isSupported': false,
'reasons': <Map<String, String>>[
<String, String>{
'reasonText': 'the iOS feature is not enabled',
'fixText': 'Run "flutter config --enable-ios"',
'fixCode': 'config',
},
],
},
'linux': <String, Object>{
'isSupported': false,
'reasons': <Map<String, String>>[
<String, String>{
'reasonText': 'the Linux feature is not enabled',
'fixText': 'Run "flutter config --enable-linux-desktop"',
'fixCode': 'config',
},
],
},
'macos': <String, bool>{'isSupported': true},
'windows': <String, bool>{'isSupported': true},
'fuchsia': <String, Object>{
'isSupported': false,
'reasons': <Map<String, String>>[
<String, String>{
'reasonText': 'the Fuchsia feature is not enabled',
'fixText': 'Run "flutter config --enable-fuchsia"',
'fixCode': 'config',
},
<String, String>{
'reasonText': 'the Fuchsia platform is not enabled for this project',
'fixText': 'Run "flutter create --platforms=fuchsia ." in your application directory',
'fixCode': 'create',
},
],
},
'custom': <String, Object>{
'isSupported': false,
'reasons': <Map<String, String>>[
<String, String>{
'reasonText': 'the custom devices feature is not enabled',
'fixText': 'Run "flutter config --enable-custom-devices"',
'fixCode': 'config',
},
],
},
'windowsPreview': <String, bool>{'isSupported': true},
},
},
);
}, overrides: <Type, Generator>{
// Disable Android/iOS and enable macOS to make sure result is consistent and defaults are tested off.
FeatureFlags: () => TestFeatureFlags(isAndroidEnabled: false, isIOSEnabled: false, isMacOSEnabled: true),
FeatureFlags: () => TestFeatureFlags(
isAndroidEnabled: false,
isIOSEnabled: false,
isMacOSEnabled: true,
isPreviewDeviceEnabled: true,
isWindowsEnabled: true,
),
});
testUsingContext('printError should send daemon.logMessage event', () async {
@ -342,18 +427,75 @@ void main() {
final FakePollingDeviceDiscovery discoverer = FakePollingDeviceDiscovery();
daemon.deviceDomain.addDeviceDiscoverer(discoverer);
discoverer.addDevice(FakeAndroidDevice());
final MemoryFileSystem fs = MemoryFileSystem.test();
discoverer.addDevice(PreviewDevice(
processManager: FakeProcessManager.empty(),
logger: BufferLogger.test(),
fileSystem: fs,
previewBinary: fs.file(r'preview_device.exe'),
artifacts: Artifacts.test(fileSystem: fs),
builderFactory: () => throw UnimplementedError('TODO implement builder factory'),
));
return daemonStreams.outputs.stream.skipWhile(_isConnectedEvent).first.then<void>((DaemonMessage response) async {
final List<Map<String, Object?>> names = <Map<String, Object?>>[];
await daemonStreams.outputs.stream.skipWhile(_isConnectedEvent).take(2).forEach((DaemonMessage response) async {
expect(response.data['event'], 'device.added');
expect(response.data['params'], isMap);
final Map<String, Object?> params = castStringKeyedMap(response.data['params'])!;
expect(params['platform'], isNotEmpty); // the fake device has a platform of 'android-arm'
names.add(params);
});
await daemonStreams.outputs.close();
expect(
names,
containsAll(const <Map<String, Object?>>[
<String, Object?>{
'id': 'device',
'name': 'android device',
'platform': 'android-arm',
'emulator': false,
'category': 'mobile',
'platformType': 'android',
'ephemeral': false,
'emulatorId': 'device',
'sdk': 'Android 12',
'capabilities': <String, Object?>{
'hotReload': true,
'hotRestart': true,
'screenshot': true,
'fastStart': true,
'flutterExit': true,
'hardwareRendering': true,
'startPaused': true,
},
},
<String, Object?>{
'id': 'preview',
'name': 'Preview',
'platform': 'windows-x64',
'emulator': false,
'category': 'desktop',
'platformType': 'windowsPreview',
'ephemeral': false,
'emulatorId': null,
'sdk': 'preview',
'capabilities': <String, Object?>{
'hotReload': true,
'hotRestart': true,
'screenshot': false,
'fastStart': false,
'flutterExit': true,
'hardwareRendering': true,
'startPaused': true,
},
},
]),
);
}, overrides: <Type, Generator>{
AndroidWorkflow: () => FakeAndroidWorkflow(),
IOSWorkflow: () => FakeIOSWorkflow(),
FuchsiaWorkflow: () => FakeFuchsiaWorkflow(),
WindowsWorkflow: () => FakeWindowsWorkflow(),
});
testUsingContext('device.discoverDevices should respond with list', () async {
@ -930,6 +1072,13 @@ bool _notEvent(DaemonMessage message) => message.data['event'] == null;
bool _isConnectedEvent(DaemonMessage message) => message.data['event'] == 'daemon.connected';
class FakeWindowsWorkflow extends Fake implements WindowsWorkflow {
FakeWindowsWorkflow({ this.canListDevices = true });
@override
final bool canListDevices;
}
class FakeFuchsiaWorkflow extends Fake implements FuchsiaWorkflow {
FakeFuchsiaWorkflow({ this.canListDevices = true });
@ -956,7 +1105,7 @@ class FakeAndroidDevice extends Fake implements AndroidDevice {
final String id = 'device';
@override
final String name = 'device';
final String name = 'android device';
@override
Future<String> get emulatorId async => 'device';

View file

@ -52,7 +52,7 @@ void main() {
);
expect(await device.isLocalEmulator, false);
expect(device.name, 'preview');
expect(device.name, 'Preview');
expect(await device.sdkNameAndVersion, 'preview');
expect(await device.targetPlatform, TargetPlatform.windows_x64);
expect(device.category, Category.desktop);