diff --git a/packages/flutter_tools/lib/src/commands/attach.dart b/packages/flutter_tools/lib/src/commands/attach.dart index ae1d8b5f618..ef72ce344c0 100644 --- a/packages/flutter_tools/lib/src/commands/attach.dart +++ b/packages/flutter_tools/lib/src/commands/attach.dart @@ -24,6 +24,7 @@ import '../fuchsia/fuchsia_device.dart'; import '../globals_null_migrated.dart' as globals; import '../ios/devices.dart'; import '../ios/simulators.dart'; +import '../macos/macos_ipad_device.dart'; import '../mdns_discovery.dart'; import '../project.dart'; import '../protocol_discovery.dart'; @@ -176,6 +177,9 @@ known, it can be explicitly provided to attach via the command-line, e.g. @override Future validateCommand() async { + // ARM macOS as an iOS target is hidden, except for attach. + MacOSDesignedForIPadDevices.allowDiscovery = true; + await super.validateCommand(); if (await findTargetDevice() == null) { throwToolExit(null); @@ -262,7 +266,7 @@ known, it can be explicitly provided to attach via the command-line, e.g. } rethrow; } - } else if ((device is IOSDevice) || (device is IOSSimulator)) { + } else if ((device is IOSDevice) || (device is IOSSimulator) || (device is MacOSDesignedForIPadDevice)) { final Uri uriFromMdns = await MDnsObservatoryDiscovery.instance.getObservatoryUri( appId, diff --git a/packages/flutter_tools/lib/src/flutter_device_manager.dart b/packages/flutter_tools/lib/src/flutter_device_manager.dart index 10c3158c8bf..7d31a389ebb 100644 --- a/packages/flutter_tools/lib/src/flutter_device_manager.dart +++ b/packages/flutter_tools/lib/src/flutter_device_manager.dart @@ -30,6 +30,7 @@ import 'ios/ios_workflow.dart'; import 'ios/simulators.dart'; import 'linux/linux_device.dart'; import 'macos/macos_device.dart'; +import 'macos/macos_ipad_device.dart'; import 'macos/macos_workflow.dart'; import 'macos/xcdevice.dart'; import 'tester/flutter_tester.dart'; @@ -105,6 +106,14 @@ class FlutterDeviceManager extends DeviceManager { fileSystem: fileSystem, operatingSystemUtils: operatingSystemUtils, ), + MacOSDesignedForIPadDevices( + processManager: processManager, + iosWorkflow: iosWorkflow, + logger: logger, + platform: platform, + fileSystem: fileSystem, + operatingSystemUtils: operatingSystemUtils, + ), LinuxDevices( platform: platform, featureFlags: featureFlags, diff --git a/packages/flutter_tools/lib/src/macos/macos_ipad_device.dart b/packages/flutter_tools/lib/src/macos/macos_ipad_device.dart new file mode 100644 index 00000000000..ada068b91e4 --- /dev/null +++ b/packages/flutter_tools/lib/src/macos/macos_ipad_device.dart @@ -0,0 +1,147 @@ +// 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 'dart:async'; + +import 'package:meta/meta.dart'; +import 'package:process/process.dart'; + +import '../application_package.dart'; +import '../base/file_system.dart'; +import '../base/logger.dart'; +import '../base/os.dart'; +import '../base/platform.dart'; +import '../build_info.dart'; +import '../desktop_device.dart'; +import '../device.dart'; +import '../ios/application_package.dart'; +import '../ios/ios_workflow.dart'; +import '../project.dart'; + +/// Represents an ARM macOS target that can run iPad apps. +/// +/// https://developer.apple.com/documentation/apple-silicon/running-your-ios-apps-on-macos +class MacOSDesignedForIPadDevice extends DesktopDevice { + MacOSDesignedForIPadDevice({ + @required ProcessManager processManager, + @required Logger logger, + @required FileSystem fileSystem, + @required OperatingSystemUtils operatingSystemUtils, + }) : _operatingSystemUtils = operatingSystemUtils, + super( + 'designed-for-ipad', + platformType: PlatformType.macos, + ephemeral: false, + processManager: processManager, + logger: logger, + fileSystem: fileSystem, + operatingSystemUtils: operatingSystemUtils, + ); + + final OperatingSystemUtils _operatingSystemUtils; + + @override + String get name => 'Mac Designed for iPad'; + + @override + Future get targetPlatform async => TargetPlatform.darwin; + + @override + bool isSupported() => _operatingSystemUtils.hostPlatform == HostPlatform.darwin_arm; + + @override + bool isSupportedForProject(FlutterProject flutterProject) { + return flutterProject.ios.existsSync() && _operatingSystemUtils.hostPlatform == HostPlatform.darwin_arm; + } + + @override + String executablePathForDevice(ApplicationPackage package, BuildMode buildMode) => null; + + @override + Future startApp( + IOSApp package, { + String mainPath, + String route, + @required DebuggingOptions debuggingOptions, + Map platformArgs = const {}, + bool prebuiltApplication = false, + bool ipv6 = false, + String userIdentifier, + }) async { + // Only attaching to a running app launched from Xcode is supported. + throw UnimplementedError('Building for "$name" is not supported.'); + } + + @override + Future stopApp( + IOSApp app, { + String userIdentifier, + }) async => false; + + @override + Future buildForDevice( + covariant IOSApp package, { + String mainPath, + BuildInfo buildInfo, + }) async { + // Only attaching to a running app launched from Xcode is supported. + throw UnimplementedError('Building for "$name" is not supported.'); + } +} + +class MacOSDesignedForIPadDevices extends PollingDeviceDiscovery { + MacOSDesignedForIPadDevices({ + @required Platform platform, + @required IOSWorkflow iosWorkflow, + @required ProcessManager processManager, + @required Logger logger, + @required FileSystem fileSystem, + @required OperatingSystemUtils operatingSystemUtils, + }) : _logger = logger, + _platform = platform, + _iosWorkflow = iosWorkflow, + _processManager = processManager, + _fileSystem = fileSystem, + _operatingSystemUtils = operatingSystemUtils, + super('Mac designed for iPad devices'); + + final IOSWorkflow _iosWorkflow; + final Platform _platform; + final ProcessManager _processManager; + final Logger _logger; + final FileSystem _fileSystem; + final OperatingSystemUtils _operatingSystemUtils; + + @override + bool get supportsPlatform => _platform.isMacOS; + + /// iOS (not desktop macOS) development is enabled, the host is an ARM Mac, + /// and discovery is allowed for this command. + @override + bool get canListAnything => + _iosWorkflow.canListDevices && _operatingSystemUtils.hostPlatform == HostPlatform.darwin_arm && allowDiscovery; + + /// Set to show ARM macOS as an iOS device target. + static bool allowDiscovery = false; + + @override + Future> pollingGetDevices({Duration timeout}) async { + if (!canListAnything) { + return const []; + } + return [ + MacOSDesignedForIPadDevice( + processManager: _processManager, + logger: _logger, + fileSystem: _fileSystem, + operatingSystemUtils: _operatingSystemUtils, + ), + ]; + } + + @override + Future> getDiagnostics() async => const []; +} diff --git a/packages/flutter_tools/test/commands.shard/hermetic/attach_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/attach_test.dart index 39f219128b9..60385852346 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/attach_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/attach_test.dart @@ -24,6 +24,7 @@ import 'package:flutter_tools/src/device_port_forwarder.dart'; import 'package:flutter_tools/src/globals_null_migrated.dart' as globals; import 'package:flutter_tools/src/ios/application_package.dart'; import 'package:flutter_tools/src/ios/devices.dart'; +import 'package:flutter_tools/src/macos/macos_ipad_device.dart'; import 'package:flutter_tools/src/project.dart'; import 'package:flutter_tools/src/resident_runner.dart'; import 'package:flutter_tools/src/run_hot.dart'; @@ -58,6 +59,10 @@ final vm_service.Isolate fakeUnpausedIsolate = vm_service.Isolate( ); void main() { + tearDown(() { + MacOSDesignedForIPadDevices.allowDiscovery = false; + }); + group('attach', () { StreamLogger logger; FileSystem testFileSystem; @@ -410,6 +415,7 @@ void main() { expect(testLogger.statusText, containsIgnoringWhitespace('More than one device')); expect(testLogger.statusText, contains('xx1')); expect(testLogger.statusText, contains('yy2')); + expect(MacOSDesignedForIPadDevices.allowDiscovery, isTrue); }, overrides: { FileSystem: () => testFileSystem, ProcessManager: () => FakeProcessManager.any(), diff --git a/packages/flutter_tools/test/general.shard/macos/macos_ipad_device_test.dart b/packages/flutter_tools/test/general.shard/macos/macos_ipad_device_test.dart new file mode 100644 index 00000000000..bc357e4ce14 --- /dev/null +++ b/packages/flutter_tools/test/general.shard/macos/macos_ipad_device_test.dart @@ -0,0 +1,149 @@ +// 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 'package:file/memory.dart'; +import 'package:flutter_tools/src/base/logger.dart'; +import 'package:flutter_tools/src/base/os.dart'; +import 'package:flutter_tools/src/base/platform.dart'; +import 'package:flutter_tools/src/build_info.dart'; +import 'package:flutter_tools/src/desktop_device.dart'; +import 'package:flutter_tools/src/device.dart'; +import 'package:flutter_tools/src/ios/ios_workflow.dart'; +import 'package:flutter_tools/src/macos/macos_ipad_device.dart'; +import 'package:meta/meta.dart'; +import 'package:test/fake.dart'; + +import '../../src/common.dart'; +import '../../src/context.dart'; +import '../../src/fakes.dart'; + +void main() { + group('MacOSDesignedForIPadDevices', () { + tearDown(() { + MacOSDesignedForIPadDevices.allowDiscovery = false; + }); + + testWithoutContext('does not support non-macOS plaforms', () async { + MacOSDesignedForIPadDevices.allowDiscovery = true; + final MacOSDesignedForIPadDevices discoverer = MacOSDesignedForIPadDevices( + platform: FakePlatform(operatingSystem: 'windows'), + logger: BufferLogger.test(), + processManager: FakeProcessManager.any(), + fileSystem: MemoryFileSystem.test(), + operatingSystemUtils: FakeOperatingSystemUtils(hostPlatform: HostPlatform.darwin_arm), + iosWorkflow: FakeIOSWorkflow(canListDevices: true), + ); + + expect(discoverer.supportsPlatform, isFalse); + }); + + testWithoutContext('discovery not allowed', () async { + final MacOSDesignedForIPadDevices discoverer = MacOSDesignedForIPadDevices( + platform: FakePlatform(operatingSystem: 'macos'), + logger: BufferLogger.test(), + processManager: FakeProcessManager.any(), + fileSystem: MemoryFileSystem.test(), + operatingSystemUtils: FakeOperatingSystemUtils(hostPlatform: HostPlatform.darwin_arm), + iosWorkflow: FakeIOSWorkflow(canListDevices: true), + ); + expect(discoverer.supportsPlatform, isTrue); + + final List devices = await discoverer.devices; + expect(devices, isEmpty); + }); + + testWithoutContext('no device on x86', () async { + MacOSDesignedForIPadDevices.allowDiscovery = true; + final MacOSDesignedForIPadDevices discoverer = MacOSDesignedForIPadDevices( + platform: FakePlatform(operatingSystem: 'macos'), + logger: BufferLogger.test(), + processManager: FakeProcessManager.any(), + fileSystem: MemoryFileSystem.test(), + operatingSystemUtils: FakeOperatingSystemUtils(hostPlatform: HostPlatform.darwin_x64), + iosWorkflow: FakeIOSWorkflow(canListDevices: true), + ); + expect(discoverer.supportsPlatform, isTrue); + + final List devices = await discoverer.devices; + expect(devices, isEmpty); + }); + + testWithoutContext('no device on when iOS development off', () async { + MacOSDesignedForIPadDevices.allowDiscovery = true; + final MacOSDesignedForIPadDevices discoverer = MacOSDesignedForIPadDevices( + platform: FakePlatform(operatingSystem: 'macos'), + logger: BufferLogger.test(), + processManager: FakeProcessManager.any(), + fileSystem: MemoryFileSystem.test(), + operatingSystemUtils: FakeOperatingSystemUtils(hostPlatform: HostPlatform.darwin_arm), + iosWorkflow: FakeIOSWorkflow(canListDevices: false), + ); + expect(discoverer.supportsPlatform, isTrue); + + final List devices = await discoverer.devices; + expect(devices, isEmpty); + }); + + testWithoutContext('device discovery on arm', () async { + MacOSDesignedForIPadDevices.allowDiscovery = true; + final MacOSDesignedForIPadDevices discoverer = MacOSDesignedForIPadDevices( + platform: FakePlatform(operatingSystem: 'macos'), + logger: BufferLogger.test(), + processManager: FakeProcessManager.any(), + fileSystem: MemoryFileSystem.test(), + operatingSystemUtils: FakeOperatingSystemUtils(hostPlatform: HostPlatform.darwin_arm), + iosWorkflow: FakeIOSWorkflow(canListDevices: true), + ); + expect(discoverer.supportsPlatform, isTrue); + + List devices = await discoverer.devices; + expect(devices, hasLength(1)); + + final Device device = devices.single; + expect(device, isA()); + expect(device.id, 'designed-for-ipad'); + + // Timeout ignored. + devices = await discoverer.discoverDevices(timeout: const Duration(seconds: 10)); + expect(devices, hasLength(1)); + }); + }); + + testWithoutContext('MacOSDesignedForIPadDevice properties', () async { + final MacOSDesignedForIPadDevice device = MacOSDesignedForIPadDevice( + logger: BufferLogger.test(), + processManager: FakeProcessManager.any(), + fileSystem: MemoryFileSystem.test(), + operatingSystemUtils: FakeOperatingSystemUtils(hostPlatform: HostPlatform.darwin_arm), + ); + expect(device.id, 'designed-for-ipad'); + expect(await device.isLocalEmulator, isFalse); + expect(device.name, 'Mac Designed for iPad'); + expect(device.portForwarder, isNot(isNull)); + expect(await device.targetPlatform, TargetPlatform.darwin); + + expect(await device.installApp(null), isTrue); + expect(await device.isAppInstalled(null), isTrue); + expect(await device.isLatestBuildInstalled(null), isTrue); + expect(await device.uninstallApp(null), isTrue); + + expect(device.isSupported(), isTrue); + expect(device.getLogReader(), isA()); + + expect(await device.stopApp(null), isFalse); + + await expectLater(() => device.startApp(null, debuggingOptions: null), throwsA(isA())); + await expectLater(() => device.buildForDevice(null), throwsA(isA())); + expect(device.executablePathForDevice(null, null), null); + }); +} + +class FakeIOSWorkflow extends Fake implements IOSWorkflow { + FakeIOSWorkflow({@required this.canListDevices}); + + @override + final bool canListDevices; +}